headlessChrome_pdf.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <?php
  2. /**
  3. * pdf-files/headlessChrome_pdf.php
  4. *
  5. * Generic headless Chrome PDF generator.
  6. * Renders any registered report type to a native Chrome PDF.
  7. *
  8. * GET params:
  9. * type string Report type key (see $reportTypes below)
  10. * rid int Primary record ID
  11. * rand string Security token (soil_records.rand / plant_records.rand etc.)
  12. * cid int Client ID (optional, passed through to print page)
  13. * stid string Soil/crop type (optional, passed through)
  14. *
  15. * Usage examples:
  16. * /pdf-files/headlessChrome_pdf.php?type=soil&rid=123&rand=abc
  17. * /pdf-files/headlessChrome_pdf.php?type=soil-combined&rid=123&rand=abc
  18. * /pdf-files/headlessChrome_pdf.php?type=plant&rid=456&rand=def&cid=12
  19. *
  20. * To add a new report type: add an entry to $reportTypes below.
  21. */
  22. require_once __DIR__ . '/../config/database.php';
  23. require_once __DIR__ . '/../lib/auth.php';
  24. require_once __DIR__ . '/../vendor/autoload.php';
  25. use daandesmedt\PHPHeadlessChrome\HeadlessChrome;
  26. if (session_status() === PHP_SESSION_NONE) {
  27. session_start();
  28. }
  29. requireLogin();
  30. // ── Input ─────────────────────────────────────────────────────────────────────
  31. $type = trim($_GET['type'] ?? 'soil');
  32. $recordId = (int) ($_GET['rid'] ?? 0);
  33. $randId = trim( $_GET['rand'] ?? '');
  34. $clientId = (int) ($_GET['cid'] ?? 0);
  35. $stid = trim( $_GET['stid'] ?? '');
  36. // ── Report type registry ──────────────────────────────────────────────────────
  37. //
  38. // 'print_page' — path relative to site root; {rid}, {rand}, {cid}, {stid}
  39. // placeholders are substituted at runtime
  40. // 'verify_table' — DB table used to verify the rand token before generating
  41. // 'filename' — prefix for the downloaded PDF filename
  42. // 'label' — human-readable name (for error messages)
  43. //
  44. $reportTypes = [
  45. // Individual pages
  46. 'soil-analysis' => [
  47. 'print_page' => '/dashboard/crop-analysis/soil-test-data/soil-analysis.php?rid={rid}&rand={rand}&cid={cid}&stid={stid}&print=1',
  48. 'verify_table' => 'soil_records',
  49. 'filename' => 'soil-analysis',
  50. 'label' => 'Soil Analysis',
  51. ],
  52. 'soil-report' => [
  53. 'print_page' => '/dashboard/crop-analysis/soil-test-data/soil-report-pdf.php?rid={rid}&rand={rand}',
  54. 'verify_table' => 'soil_records',
  55. 'filename' => 'soil-report',
  56. 'label' => 'Soil Report',
  57. ],
  58. // Combined: analysis + AI report in one PDF
  59. 'soil' => [
  60. 'print_page' => '/dashboard/crop-analysis/soil-test-data/soil-print-combined.php?rid={rid}&rand={rand}&cid={cid}&stid={stid}',
  61. 'verify_table' => 'soil_records',
  62. 'filename' => 'soil-analysis-report',
  63. 'label' => 'Soil Analysis & Report',
  64. ],
  65. // Plant
  66. 'plant-analysis' => [
  67. 'print_page' => '/dashboard/crop-analysis/plant-test-data/plant-analysis-print.php?rid={rid}&rand={rand}&cid={cid}',
  68. 'verify_table' => 'plant_records',
  69. 'filename' => 'plant-analysis',
  70. 'label' => 'Plant Analysis',
  71. ],
  72. 'plant-report' => [
  73. 'print_page' => '/dashboard/crop-analysis/plant-test-data/plant-report-pdf.php?rid={rid}&rand={rand}',
  74. 'verify_table' => 'plant_records',
  75. 'filename' => 'plant-report',
  76. 'label' => 'Plant Report',
  77. ],
  78. 'plant' => [
  79. 'print_page' => '/dashboard/crop-analysis/plant-test-data/plant-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
  80. 'verify_table' => 'plant_records',
  81. 'filename' => 'plant-analysis-report',
  82. 'label' => 'Plant Analysis & Report',
  83. ],
  84. // Water
  85. 'water' => [
  86. 'print_page' => '/dashboard/crop-analysis/water-test-data/water-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
  87. 'verify_table' => 'water_records',
  88. 'filename' => 'water-analysis-report',
  89. 'label' => 'Water Analysis & Report',
  90. ],
  91. // Animal
  92. 'animal' => [
  93. 'print_page' => '/dashboard/crop-analysis/animal-dietary-balance/animal-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
  94. 'verify_table' => 'animal_records',
  95. 'filename' => 'animal-dietary-report',
  96. 'label' => 'Animal Dietary Report',
  97. ],
  98. ];
  99. if (!isset($reportTypes[$type])) {
  100. http_response_code(400);
  101. $valid = implode(', ', array_keys($reportTypes));
  102. die("Unknown report type \"" . htmlspecialchars($type, ENT_QUOTES, 'UTF-8') . "\". Valid types: $valid");
  103. }
  104. $config = $reportTypes[$type];
  105. if ($recordId <= 0 || $randId === '') {
  106. http_response_code(400);
  107. die('Missing required parameters: rid, rand');
  108. }
  109. // ── Verify record exists via its table ───────────────────────────────────────
  110. $pdo = getDBConnection();
  111. $table = $config['verify_table'];
  112. // Allowlist the table name against known tables to prevent SQL injection
  113. $allowedTables = ['soil_records', 'plant_records', 'water_records', 'animal_records'];
  114. if (!in_array($table, $allowedTables, true)) {
  115. http_response_code(500);
  116. die('Invalid verify_table in report type config.');
  117. }
  118. $stmt = $pdo->prepare("SELECT lab_no FROM `{$table}` WHERE id = ? AND rand = ? LIMIT 1");
  119. $stmt->execute([$recordId, $randId]);
  120. $record = $stmt->fetch();
  121. if (!$record) {
  122. http_response_code(404);
  123. die('Record not found or access denied.');
  124. }
  125. // ── Write one-time print token ────────────────────────────────────────────────
  126. $token = bin2hex(random_bytes(16));
  127. $expires = time() + 120;
  128. $tokenDir = __DIR__ . '/tokens';
  129. if (!is_dir($tokenDir)) {
  130. mkdir($tokenDir, 0750, true);
  131. }
  132. file_put_contents($tokenDir . '/' . $token . '.tmp', json_encode([
  133. 'rid' => $recordId,
  134. 'rand' => $randId,
  135. 'expires' => $expires,
  136. 'type' => $type,
  137. ]));
  138. // ── Build print page URL ──────────────────────────────────────────────────────
  139. $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
  140. $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
  141. $baseUrl = $scheme . '://' . $host;
  142. $printPath = str_replace(
  143. ['{rid}', '{rand}', '{cid}', '{stid}'],
  144. [$recordId, urlencode($randId), $clientId, urlencode($stid)],
  145. $config['print_page']
  146. );
  147. $printUrl = $baseUrl . $printPath . '&ptoken=' . urlencode($token);
  148. // ── Output directory + filename ───────────────────────────────────────────────
  149. $outputDir = __DIR__;
  150. $today = date('Y-m-d');
  151. $labNo = preg_replace('/[^A-Za-z0-9\-_]/', '_', $record['lab_no'] ?? $recordId);
  152. $filename = $config['filename'] . '-' . $labNo . '-' . $today;
  153. // ── Headless Chrome ───────────────────────────────────────────────────────────
  154. $chromeBinary = '/usr/bin/google-chrome';
  155. foreach (['/usr/bin/google-chrome', '/usr/bin/chromium-browser', '/usr/bin/chromium'] as $bin) {
  156. if (file_exists($bin)) { $chromeBinary = $bin; break; }
  157. }
  158. $arguments = [
  159. '--headless' => '',
  160. '--disable-gpu' => '',
  161. '--hide-scrollbars' => '',
  162. '--enable-viewport' => '',
  163. '--disable-web-security' => '',
  164. '--run-all-compositor-stages-before-draw' => '',
  165. '--virtual-time-budget' => '60000',
  166. '--timeout=' => '8000',
  167. '--no-sandbox' => '',
  168. '--disable-setuid-sandbox' => '',
  169. ];
  170. try {
  171. $chrome = new HeadlessChrome();
  172. $chrome->disablePDFHeader();
  173. $chrome->setUrl($printUrl);
  174. $chrome->setBinaryPath($chromeBinary);
  175. $chrome->setOutputDirectory($outputDir);
  176. $chrome->setChromeArguments($arguments);
  177. $chrome->toPDF($filename . '.pdf');
  178. $pdfPath = $chrome->getFilePath();
  179. } catch (Exception $e) {
  180. error_log('HeadlessChrome PDF error [' . $type . ']: ' . $e->getMessage());
  181. @unlink($tokenDir . '/' . $token . '.tmp');
  182. http_response_code(500);
  183. die('PDF generation failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'));
  184. }
  185. @unlink($tokenDir . '/' . $token . '.tmp');
  186. // ── Stream PDF ────────────────────────────────────────────────────────────────
  187. if (!file_exists($pdfPath)) {
  188. http_response_code(500);
  189. die('PDF file was not created. Ensure Chrome is installed at: ' . $chromeBinary);
  190. }
  191. chmod($pdfPath, 0644);
  192. header('Content-Type: application/pdf');
  193. header('Content-Disposition: attachment; filename="' . $filename . '.pdf"');
  194. header('Content-Length: ' . filesize($pdfPath));
  195. header('Expires: 0');
  196. header('Cache-Control: must-revalidate');
  197. header('Pragma: public');
  198. readfile($pdfPath);
  199. @unlink($pdfPath);
  200. exit;