?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