headlessChrome_pdf.php 9.1 KB

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