?php namespace ForminatorAdminPDF\PDF; use Mpdf\Mpdf; use Mpdf\MpdfException; use Mpdf\HTMLParserMode; use Forminator_Form_Model; defined('ABSPATH') || exit; class Generator { public static function generate(array $mapped, int $form_id) { if (empty($mapped)) { fap_log("FAP: Generator::generate() - No data provided"); return false; } /* ---------- load layout ---------- */ $layout = require FAP_PATH . 'pdf-templates/layout.php'; $embedded_pdfs = []; $image_vars = []; $sealed_uploads = []; $form = Forminator_Form_Model::model()->load($form_id); $title = 'Form Submission'; if ($form && !empty($form->settings['formName'])) { $title = $form->settings['formName']; } $metadata = [ 'generated' => current_time('mysql'), 'form_id' => $form_id, 'form_name' => $title, ]; $full_data = [ 'metadata' => $metadata, 'fields' => $mapped, ]; /* ---------- base HTML ---------- */ $html = ''; $html .= $layout['header']($title); $html .= $layout['base_css'](); /* ---------- fields ---------- */ foreach ($mapped as $key => $field) { $field_id = 'field_' . $key; /* ---------- HTML fields ---------- */ if ($field['type'] === 'html') { // Invisible start/end markers $start_marker_html = '
[FAP_FIELD_' . $field_id . ']
'; $end_marker_html = '
[FAP_FIELD_END]
'; if (!empty($field['label'])) { $html .= $layout['field']( $field['label'], $start_marker_html . $field['value'] . $end_marker_html ); } else { $html .= '
' . $start_marker_html . $field['value'] . $end_marker_html . '
'; } continue; } if (!isset($field['label'], $field['value'])) { continue; } $cell_html = esc_html($field['value']); /* ---------- uploads & signatures ---------- */ if ( in_array($field['type'], ['upload', 'signature'], true) && !empty($field['materialized_files']) && is_array($field['materialized_files']) ) { foreach ($field['materialized_files'] as $file) { $mime = $file['mime'] ?? ''; $name = $file['name'] ?? 'file'; /* ---------- embedded PDF ---------- */ if ($mime === 'application/pdf' && !empty($file['base64'])) { $embed_dir = self::get_embed_storage_dir(); $safe_name = preg_replace('/[^a-zA-Z0-9._-]/', '_', $name); $temp_path = $embed_dir . '/' . uniqid('embed_', true) . '_' . $safe_name; $binary = base64_decode($file['base64'], true); if ($binary === false) continue; $file_hash = hash('sha256', $binary); if (file_put_contents($temp_path, $binary) === false) { fap_log("FAP: ERROR: Could not write embed file " . basename($temp_path)); continue; } /* ---- seal upload ---- */ $sealed_uploads[] = [ 'name' => $safe_name, 'mime' => 'application/pdf', 'sha256' => $file_hash, 'hash_input' => '', ]; $embedded_pdfs[$safe_name] = [ 'path' => $temp_path, 'name' => $safe_name, 'mime' => 'application/pdf', 'description' => 'Hochgeladenes Dokument', 'AFRelationship' => 'Supplement', ]; continue; } /* ---------- images ---------- */ if (str_starts_with($mime, 'image/') && !empty($file['base64'])) { $binary = base64_decode($file['base64'], true); if ($binary === false) continue; $width = $height = 0; $colorspace = 'DeviceRGB'; // default // Get image dimensions $info = getimagesizefromstring($binary); if ($info) { $width = $info[0]; $height = $info[1]; // Determine colorspace (simplified) $channels = $info['channels'] ?? 3; // 'channels' is available for PNG/JPEG if ($channels === 1) { $colorspace = 'DeviceGray'; } elseif ($channels === 3) { $colorspace = 'DeviceRGB'; } elseif ($channels === 4) { $colorspace = 'DeviceCMYK'; } } // JPEG survives PDF round-trip as raw DCT bytes → use content hash. // PNG is decoded to raw pixels inside the PDF XObject, so the byte // representation changes; fall back to the dimension fingerprint which // is stable across the encode/decode cycle. $is_jpeg = in_array($mime, ['image/jpeg', 'image/jpg'], true); if ($is_jpeg) { $img_hash = hash('sha256', $binary); $hash_input = ''; // empty sentinel = content hash } else { $img_hash = hash('sha256', $colorspace . '|' . $width . '|' . $height); $hash_input = $colorspace . '|' . $width . '|' . $height; } /* ---- seal upload ---- */ $sealed_uploads[] = [ 'name' => (string) $name, 'mime' => (string) $mime, 'sha256' => $img_hash, 'hash_input' => $hash_input, ]; $image_var = 'img_' . uniqid(); $image_vars[$image_var] = $binary; $cell_html .= $layout['image']($image_var); } } } // Wrap field value with invisible markers for PDF text extraction $start_marker_html = '
[FAP_FIELD_' . $field_id . ']
'; $end_marker_html = '
[FAP_FIELD_END]
'; $cell_html = $start_marker_html . $cell_html . $end_marker_html; $html .= $layout['field']($field['label'], $cell_html); } /* ---------- Add Metadata HTML ---------- */ $html .= $layout['document_metadata']($full_data); $template = []; $logo_path = FAP_PATH . 'pdf-templates/logo.png'; $line_path = FAP_PATH . 'pdf-templates/line.png'; $grid_svg = FAP_PATH . 'pdf-templates/construction-grid.svg'; foreach ([$logo_path, $line_path, $grid_svg] as $path) { if (!file_exists($path) || !is_readable($path)) { error_log("Template file missing: {$path}"); continue; } $data = file_get_contents($path); $mime = mime_content_type($path) ?: 'application/octet-stream'; $width = $height = 0; $colorspace = 'DeviceRGB'; // fallback if (in_array($mime, ['image/png','image/jpeg'], true)) { $info = getimagesizefromstring($data); if ($info) { $width = $info[0]; $height = $info[1]; // Determine colorspace properly if ($mime === 'image/png') { // read IHDR color type $colorType = ord($data[25]); // IHDR: 25th byte in PNG switch ($colorType) { case 0: $colorspace = 'DeviceGray'; break; case 2: $colorspace = 'DeviceRGB'; break; case 3: $colorspace = 'IndexedRGB'; break; case 4: $colorspace = 'DeviceGray'; break; // grayscale + alpha case 6: $colorspace = 'DeviceRGB'; break; // RGB + alpha default: $colorspace = 'DeviceRGB'; } } else { // JPEG $colorspace = ($info['channels'] ?? 3) === 1 ? 'DeviceGray' : 'DeviceRGB'; } } } // --- Per-image fingerprint matching verifier --- $img_id = hash('sha256', $colorspace . '|' . $width . '|' . $height); $template[] = [ 'name' => basename($path), 'mime' => $mime, 'sha256' => $img_id, 'hash_input' => $colorspace . '|' . $width . '|' . $height, ]; } /* ---------- PDF generation (2-pass with font sniffing) ---------- */ try { $upload_dir = wp_upload_dir(); $safe_dir = $upload_dir['basedir'] . '/forminator-secure-pdf'; // Ensure directories exist with restricted permissions. foreach (['', '/pdf', '/embed', '/mpdf'] as $sub) { $dir = $safe_dir . $sub; if (!is_dir($dir)) { wp_mkdir_p($dir); chmod($dir, 0750); file_put_contents($dir . '/index.php', ""); chmod($dir . '/index.php', 0640); } } // Block all direct HTTP access to the secure-pdf tree. $htaccess = $safe_dir . '/.htaccess'; if (!file_exists($htaccess)) { file_put_contents( $htaccess, "Options -Indexes\nDeny from all\n" ); chmod($htaccess, 0640); } $pdf_dir = $safe_dir . '/pdf'; // final PDFs $embed_dir = $safe_dir . '/embed'; // embedded PDFs $mpdf_temp = $safe_dir . '/mpdf'; // mPDF temp files $form_name_clean = preg_replace('/[^a-zA-Z0-9_-]/', '_', $metadata['form_name']); $date_time = wp_date('D_d_m_Y_T_H_i'); /* ============================================================ * PASS 1 — Generate PDF WITHOUT seal (font discovery) * ============================================================ */ $mpdf = new Mpdf([ 'tempDir' => $mpdf_temp, ]); // Background $grid_svg = FAP_PATH . 'pdf-templates/construction-grid.svg'; $mpdf->SetDefaultBodyCSS('background', "url('{$grid_svg}')"); $mpdf->SetDefaultBodyCSS('background-repeat', 'repeat'); $mpdf->SetDefaultBodyCSS('background-position', 'center center'); // Footer $mpdf->SetHTMLFooter( '
[FAP_PAGENO_START] Seite {PAGENO} von {nbpg} [FAP_PAGENO_END]
' ); // Images if (!empty($image_vars)) { $mpdf->imageVars = $image_vars; } // Write main HTML ONLY self::write_html_chunked($mpdf, $html, 1500000); // Output temporary PDF $sl_file_name = "SL_{$form_name_clean}_{$date_time}.pdf"; $sl_file_path = $mpdf_temp . '/' . $sl_file_name; $mpdf->Output($sl_file_path, \Mpdf\Output\Destination::FILE); unset($mpdf); /* ============================================================ * Extract fonts from temporary PDF * ============================================================ */ $fonts = []; $expected_pages = 0; $pdf_raw = file_get_contents($sl_file_path); if ($pdf_raw !== false) { if (preg_match_all('/\/BaseFont\s*\/([A-Za-z0-9\+\-_]+)/', $pdf_raw, $matches)) { foreach ($matches[1] as $font) { // Strip subset prefix (e.g. MPDFAA+) $font = preg_replace('/^[A-Z]{6}\+/', '', $font); $fonts[$font] = true; } } preg_match_all('/\/Type\s*\/Page\b/', $pdf_raw, $page_matches); $expected_pages = count($page_matches[0]); } $fonts = array_keys($fonts); // Remove temporary SL PDF if (!unlink($sl_file_path)) { error_log("FAP: WARNING: Could not delete temp SL PDF: " . basename($sl_file_path)); } /* ============================================================ * PASS 2 — Generate FINAL PDF WITH seal * ============================================================ */ $mpdf = new Mpdf([ 'tempDir' => $mpdf_temp, ]); // Re-apply same setup $mpdf->SetDefaultBodyCSS('background', "url('{$grid_svg}')"); $mpdf->SetDefaultBodyCSS('background-repeat', 'repeat'); $mpdf->SetDefaultBodyCSS('background-position', 'center center'); $mpdf->SetHTMLFooter( '
[FAP_PAGENO_START] Seite {PAGENO} von {nbpg} [FAP_PAGENO_END]
' ); if (!empty($image_vars)) { $mpdf->imageVars = $image_vars; } // Write main HTML again self::write_html_chunked($mpdf, $html, 1500000); // Build seal with REAL fonts $seal_data = [ 'generated' => trim((string)$metadata['generated']), 'form_id' => (int)$metadata['form_id'], 'form_name' => self::normalize_field_value($metadata['form_name']), 'fields' => self::build_seal_fields($mapped), 'uploads' => $sealed_uploads, 'template' => $template, 'fonts' => $fonts, 'expected_pages' => $expected_pages, ]; $hash = HashSeal::generate($seal_data); $seal_data['seal'] = $hash; $seal_json = json_encode($seal_data, JSON_UNESCAPED_SLASHES); $seal_base64 = base64_encode($seal_json); $seal_html = '
---BEGIN-SEAL--- ' . $seal_base64 . ' ---END-SEAL---
'; self::write_html_chunked($mpdf, $seal_html, 1500000); // Final output $final_file_name = "Entry_{$form_name_clean}_{$date_time}.pdf"; $final_file_path = $pdf_dir . '/' . $final_file_name; $mpdf->Output($final_file_path, \Mpdf\Output\Destination::FILE); // Cleanup embedded PDFs foreach ($embedded_pdfs as $pdf) { if (!empty($pdf['path']) && file_exists($pdf['path'])) { @unlink($pdf['path']); } } return $final_file_path; } catch (MpdfException $e) { error_log("FAP: PDF generation error: " . $e->getMessage() . " in " . basename($e->getFile()) . ":" . $e->getLine()); return false; } } private static function get_embed_storage_dir(): string { $upload_dir = wp_upload_dir(); $dir = $upload_dir['basedir'] . '/forminator-secure-pdf/embed'; if (!is_dir($dir)) { wp_mkdir_p($dir); file_put_contents($dir . '/index.php', ""); } return $dir; } private static function write_html_chunked(Mpdf $mpdf, string $html, int $chunkSize = 800000): void { // Normalize line endings $html = str_replace(["\r\n", "\r"], "\n", $html); // Split on ANY closing tag (much more aggressive) $parts = preg_split( '/(<\/[^>]+>)/i', $html, -1, PREG_SPLIT_DELIM_CAPTURE ); $buffer = ''; $first = true; $chunk = 0; foreach ($parts as $part) { $buffer .= $part; // HARD limit: force flush if buffer is too large if (strlen($buffer) >= $chunkSize) { $chunk++; $mpdf->WriteHTML( $buffer, $first ? HTMLParserMode::DEFAULT_MODE : HTMLParserMode::HTML_BODY ); $buffer = ''; $first = false; } } if ($buffer !== '') { $chunk++; $mpdf->WriteHTML( $buffer, $first ? HTMLParserMode::DEFAULT_MODE : HTMLParserMode::HTML_BODY ); } } private static function build_seal_fields(array $mapped): array { $fields = []; foreach ($mapped as $field) { $entry = [ 'label' => (string) ($field['label'] ?? ''), 'value' => '', ]; // Normalize field value if (!empty($field['value']) && is_string($field['value'])) { $entry['value'] = self::normalize_field_value($field['value']); } $fields[] = $entry; } return $fields; } private static function normalize_field_value(string $value): string { // Decode HTML entities $value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); // Strip all HTML $value = wp_strip_all_tags($value); // Collapse whitespace $value = preg_replace('/\s+/u', ' ', $value); return trim($value); } } https://dghk.makerundcode.de/wp-sitemap-posts-page-1.xmlhttps://dghk.makerundcode.de/wp-sitemap-posts-event-1.xmlhttps://dghk.makerundcode.de/wp-sitemap-posts-location-1.xmlhttps://dghk.makerundcode.de/wp-sitemap-taxonomies-event-tags-1.xmlhttps://dghk.makerundcode.de/wp-sitemap-taxonomies-event-categories-1.xml