headlessChrome_pdf.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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.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-analysis' => [
  89. 'print_page' => '/dashboard/crop-analysis/water-test-data/water-analysis-pdf.php?rid={rid}&rand={rand}&cid={cid}',
  90. 'verify_table' => 'water_records',
  91. 'filename' => 'water-analysis',
  92. 'label' => 'Water Analysis',
  93. ],
  94. 'water-report' => [
  95. 'print_page' => '/dashboard/crop-analysis/water-test-data/water-report-pdf.php?rid={rid}&rand={rand}',
  96. 'verify_table' => 'water_records',
  97. 'filename' => 'water-report',
  98. 'label' => 'Water Report',
  99. ],
  100. 'water' => [
  101. 'print_page' => '/dashboard/crop-analysis/water-test-data/water-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
  102. 'verify_table' => 'water_records',
  103. 'filename' => 'water-analysis-report',
  104. 'label' => 'Water Analysis & Report',
  105. ],
  106. // Animal
  107. 'animal-analysis' => [
  108. 'print_page' => '/dashboard/crop-analysis/animal-dietary-balance/animal-dietary-balance.php?rid={rid}&rand={rand}&cid={cid}',
  109. 'verify_table' => 'animal_records',
  110. 'filename' => 'animal-analysis',
  111. 'label' => 'Animal Dietary Analysis',
  112. ],
  113. 'animal-report' => [
  114. 'print_page' => '/dashboard/crop-analysis/animal-dietary-balance/animal-report-pdf.php?rid={rid}&rand={rand}',
  115. 'verify_table' => 'animal_records',
  116. 'filename' => 'animal-report',
  117. 'label' => 'Animal Dietary Report',
  118. ],
  119. 'animal' => [
  120. 'print_page' => '/dashboard/crop-analysis/animal-dietary-balance/animal-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
  121. 'verify_table' => 'animal_records',
  122. 'filename' => 'animal-dietary-report',
  123. 'label' => 'Animal Dietary Analysis & Report',
  124. ],
  125. ];
  126. if (!isset($reportTypes[$type])) {
  127. http_response_code(400);
  128. $valid = implode(', ', array_keys($reportTypes));
  129. die("Unknown report type \"" . htmlspecialchars($type, ENT_QUOTES, 'UTF-8') . "\". Valid types: $valid");
  130. }
  131. $config = $reportTypes[$type];
  132. if ($recordId <= 0 || $randId === '') {
  133. http_response_code(400);
  134. die('Missing required parameters: rid, rand');
  135. }
  136. // ── Verify record exists via its table ───────────────────────────────────────
  137. $pdo = getDBConnection();
  138. $table = $config['verify_table'];
  139. // Allowlist the table name against known tables to prevent SQL injection
  140. $allowedTables = ['soil_records', 'plant_records', 'water_records', 'animal_records'];
  141. if (!in_array($table, $allowedTables, true)) {
  142. http_response_code(500);
  143. die('Invalid verify_table in report type config.');
  144. }
  145. $stmt = $pdo->prepare("SELECT lab_no FROM `{$table}` WHERE id = ? AND rand = ? LIMIT 1");
  146. $stmt->execute([$recordId, $randId]);
  147. $record = $stmt->fetch();
  148. if (!$record) {
  149. http_response_code(404);
  150. die('Record not found or access denied.');
  151. }
  152. // ── Write one-time print token ────────────────────────────────────────────────
  153. $token = bin2hex(random_bytes(16));
  154. $expires = time() + 120;
  155. $tokenDir = __DIR__ . '/tokens';
  156. if (!is_dir($tokenDir)) {
  157. mkdir($tokenDir, 0750, true);
  158. }
  159. file_put_contents($tokenDir . '/' . $token . '.tmp', json_encode([
  160. 'rid' => $recordId,
  161. 'rand' => $randId,
  162. 'expires' => $expires,
  163. 'type' => $type,
  164. ]));
  165. // ── Build print page URL ──────────────────────────────────────────────────────
  166. $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
  167. $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
  168. $baseUrl = $scheme . '://' . $host;
  169. $printPath = str_replace(
  170. ['{rid}', '{rand}', '{cid}', '{stid}'],
  171. [$recordId, urlencode($randId), $clientId, urlencode($stid)],
  172. $config['print_page']
  173. );
  174. $printUrl = $baseUrl . $printPath . '&ptoken=' . urlencode($token);
  175. // ── Output directory + filename ───────────────────────────────────────────────
  176. $outputDir = __DIR__;
  177. $today = date('Y-m-d');
  178. $labNo = preg_replace('/[^A-Za-z0-9\-_]/', '_', $record['lab_no'] ?? $recordId);
  179. $filename = $config['filename'] . '-' . $labNo . '-' . $today;
  180. // ── Headless Chrome ───────────────────────────────────────────────────────────
  181. $chromeBinary = '/usr/bin/google-chrome';
  182. foreach (['/usr/bin/google-chrome', '/usr/bin/chromium-browser', '/usr/bin/chromium'] as $bin) {
  183. if (file_exists($bin)) { $chromeBinary = $bin; break; }
  184. }
  185. $arguments = [
  186. '--headless' => '',
  187. '--disable-gpu' => '',
  188. '--hide-scrollbars' => '',
  189. '--enable-viewport' => '',
  190. '--timeout=' => '6000',
  191. '--disable-web-security' => '',
  192. '--run-all-compositor-stages-before-draw' => '',
  193. '--virtual-time-budget' => '40000',
  194. ];
  195. try {
  196. $chrome = new HeadlessChrome();
  197. $chrome->disablePDFHeader();
  198. $chrome->setUrl($printUrl);
  199. $chrome->setBinaryPath($chromeBinary);
  200. $chrome->setOutputDirectory($outputDir);
  201. $chrome->toPDF($filename . '.pdf');
  202. $pdfPath = $chrome->getFilePath();
  203. } catch (Exception $e) {
  204. error_log('HeadlessChrome PDF error [' . $type . ']: ' . $e->getMessage());
  205. @unlink($tokenDir . '/' . $token . '.tmp');
  206. http_response_code(500);
  207. die('PDF generation failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'));
  208. }
  209. @unlink($tokenDir . '/' . $token . '.tmp');
  210. // ── Stream PDF ────────────────────────────────────────────────────────────────
  211. if (!file_exists($pdfPath)) {
  212. http_response_code(500);
  213. die('PDF file was not created. Ensure Chrome is installed at: ' . $chromeBinary);
  214. }
  215. chmod($pdfPath, 0644);
  216. header('Content-Type: application/pdf');
  217. header('Content-Disposition: attachment; filename="' . $filename . '.pdf"');
  218. header('Content-Length: ' . filesize($pdfPath));
  219. header('Expires: 0');
  220. header('Cache-Control: must-revalidate');
  221. header('Pragma: public');
  222. readfile($pdfPath);
  223. @unlink($pdfPath);
  224. exit;