Pārlūkot izejas kodu

fixing pdf generation

Medowar 3 nedēļas atpakaļ
vecāks
revīzija
4af27d13b3
4 mainītis faili ar 403 papildinājumiem un 74 dzēšanām
  1. 5 2
      .htaccess
  2. 6 0
      assets/css/style.css
  3. 386 72
      includes/functions.php
  4. 6 0
      product.php

+ 5 - 2
.htaccess

@@ -14,8 +14,11 @@ Options -Indexes
     # Block hidden files/folders except ACME challenge path.
     RewriteRule "(^|/)\.(?!well-known/)" - [F]
 
-    # Deny direct access to writable data directory except uploads.
-    RewriteRule ^data/(?!uploads/)(?:/|$) - [F,L]
+    # Allow public access to uploaded product images only.
+    RewriteRule ^data/uploads/[^/]+\.(?:jpe?g|png|webp|gif)$ - [L,NC]
+
+    # Deny direct access to the rest of the writable data directory.
+    RewriteRule ^data(?:/|$) - [F,L]
 </IfModule>
 
 <IfModule mod_authz_core.c>

+ 6 - 0
assets/css/style.css

@@ -843,6 +843,12 @@ body.admin-page .container {
     margin: 1.5rem 0 2rem;
 }
 
+.product-category-text {
+    margin: -0.25rem 0 1.2rem;
+    font-size: 0.96rem;
+    color: var(--brand-muted);
+}
+
 .product-description {
     margin-top: 0.5rem;
     line-height: 1.8;

+ 386 - 72
includes/functions.php

@@ -2207,49 +2207,38 @@ function sendEmail($to, $subject, $message, $isHtml = true, $attachments = [])
     );
 }
 
-function buildOrderPdfLines($order)
-{
-    $lines = [
-        SITE_FULL_NAME,
-        SITE_DEPARTMENT_NAME,
-        "",
-        SITE_SERVICE_HEADER,
-        "Bestellnummer: " . $order["id"],
-        "Erstellt am: " . formatDate($order["created_at"]),
-        "Name: " . $order["customer_name"],
-        "E-Mail: " . $order["customer_email"],
-        "Organisation: " . $order["organization_label"],
-        "",
-        "Artikel:",
-    ];
+function pdfEncodeWinAnsi($text)
+{
+    $text = str_replace("\r", "", (string) $text);
 
-    foreach ($order["items"] as $item) {
-        $line = "- " . $item["product_name"];
-        if ($item["size"] !== "") {
-            $line .= " | Größe: " . $item["size"];
-        }
-        if ($item["availability_label"] !== "") {
-            $line .=
-                " | Hinweis: " .
-                preg_replace("/\s+/", " ", $item["availability_label"]);
-        }
-        $lines[] = $line;
+    if (!function_exists("iconv")) {
+        return preg_replace('/[^\x09\x0A\x20-\x7E]/', "?", $text);
     }
 
-    $lines[] = "";
-    $lines[] = "Kommentar:";
-    if ($order["comment"] !== "") {
-        foreach (
-            preg_split('/\r\n|\r|\n/', $order["comment"])
-            as $commentLine
-        ) {
-            $lines[] = $commentLine;
+    $chars = preg_split("//u", $text, -1, PREG_SPLIT_NO_EMPTY);
+    if (!is_array($chars)) {
+        $fallback = @iconv("UTF-8", "Windows-1252//TRANSLIT//IGNORE", $text);
+        return is_string($fallback) ? $fallback : $text;
+    }
+
+    $result = "";
+    foreach ($chars as $char) {
+        $converted = @iconv("UTF-8", "Windows-1252", $char);
+        if ($converted !== false) {
+            $result .= $converted;
+            continue;
         }
-    } else {
-        $lines[] = "Kein Kommentar";
+
+        $fallback = @iconv("UTF-8", "Windows-1252//TRANSLIT", $char);
+        if ($fallback !== false && $fallback !== "") {
+            $result .= $fallback;
+            continue;
+        }
+
+        $result .= "?";
     }
 
-    return $lines;
+    return $result;
 }
 
 function pdfEscapeText($text)
@@ -2257,65 +2246,390 @@ function pdfEscapeText($text)
     $text = str_replace("\\", "\\\\", $text);
     $text = str_replace("(", "\(", $text);
     $text = str_replace(")", "\)", $text);
-    $text = str_replace("\r", "", $text);
+    return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', "", $text);
+}
 
-    if (function_exists("iconv")) {
-        $converted = @iconv("UTF-8", "Windows-1252//TRANSLIT//IGNORE", $text);
-        if (is_string($converted)) {
-            $text = $converted;
+function pdfWrapAnsiText($text, $maxChars)
+{
+    $lines = [];
+    $paragraphs = explode("\n", str_replace("\r", "", (string) $text));
+
+    foreach ($paragraphs as $paragraph) {
+        $normalized = trim(preg_replace("/\s+/", " ", $paragraph));
+        if ($normalized === "") {
+            $lines[] = "";
+            continue;
+        }
+
+        $words = explode(" ", $normalized);
+        $current = "";
+
+        foreach ($words as $word) {
+            if ($word === "") {
+                continue;
+            }
+
+            if (strlen($word) > $maxChars) {
+                if ($current !== "") {
+                    $lines[] = $current;
+                    $current = "";
+                }
+
+                $parts = str_split($word, $maxChars);
+                $lastIndex = count($parts) - 1;
+                for ($i = 0; $i < $lastIndex; $i++) {
+                    $lines[] = $parts[$i];
+                }
+                $current = $parts[$lastIndex];
+                continue;
+            }
+
+            $candidate = $current === "" ? $word : $current . " " . $word;
+            if (strlen($candidate) <= $maxChars) {
+                $current = $candidate;
+            } else {
+                if ($current !== "") {
+                    $lines[] = $current;
+                }
+                $current = $word;
+            }
+        }
+
+        if ($current !== "") {
+            $lines[] = $current;
         }
     }
 
-    return $text;
+    if (empty($lines)) {
+        $lines[] = "";
+    }
+
+    return $lines;
 }
 
 function generateOrderPdf($order)
 {
-    $lines = buildOrderPdfLines($order);
-    $content = "BT\n/F1 12 Tf\n14 TL\n50 790 Td\n";
-    $first = true;
+    $pageWidth = 595;
+    $pageHeight = 842;
+    $leftMargin = 45;
+    $rightMargin = 45;
+    $topY = 800;
+    $bottomY = 55;
+    $lineHeight = 14;
+
+    $orderId = pdfEncodeWinAnsi($order["id"]);
+    $createdAt = pdfEncodeWinAnsi(formatDate($order["created_at"]));
+    $customerName = pdfEncodeWinAnsi($order["customer_name"]);
+    $customerEmail = pdfEncodeWinAnsi($order["customer_email"]);
+    $organization = pdfEncodeWinAnsi($order["organization_label"]);
+    $comment = pdfEncodeWinAnsi($order["comment"]);
+    $labelBelongsTo = pdfEncodeWinAnsi("Gehört zu");
+    $labelSize = pdfEncodeWinAnsi("Größe");
+    $labelFullyIssued = pdfEncodeWinAnsi("Vollständig ausgegeben");
+
+    $pages = [];
+    $pageContent = "";
+    $y = $topY;
+    $isFirstPage = true;
+
+    $writeText = function ($x, $y, $text, $fontSize = 12) use (&$pageContent) {
+        $pageContent .=
+            "BT\n/F1 " .
+            $fontSize .
+            " Tf\n1 0 0 1 " .
+            number_format($x, 2, ".", "") .
+            " " .
+            number_format($y, 2, ".", "") .
+            " Tm\n(" .
+            pdfEscapeText($text) .
+            ") Tj\nET\n";
+    };
 
-    foreach ($lines as $line) {
-        if (!$first) {
-            $content .= "T*\n";
+    $drawLine = function ($x1, $y1, $x2, $y2) use (&$pageContent) {
+        $pageContent .=
+            number_format($x1, 2, ".", "") .
+            " " .
+            number_format($y1, 2, ".", "") .
+            " m " .
+            number_format($x2, 2, ".", "") .
+            " " .
+            number_format($y2, 2, ".", "") .
+            " l S\n";
+    };
+
+    $drawRect = function ($x, $y, $width, $height) use (&$pageContent) {
+        $pageContent .=
+            number_format($x, 2, ".", "") .
+            " " .
+            number_format($y, 2, ".", "") .
+            " " .
+            number_format($width, 2, ".", "") .
+            " " .
+            number_format($height, 2, ".", "") .
+            " re S\n";
+    };
+
+    $startPage = function () use (
+        &$pages,
+        &$pageContent,
+        &$y,
+        &$isFirstPage,
+        $topY,
+        $leftMargin,
+        $pageWidth,
+        $rightMargin,
+        $orderId,
+        $createdAt,
+        $customerName,
+        $customerEmail,
+        $organization,
+        $labelBelongsTo,
+        $writeText,
+        $drawLine
+    ) {
+        if ($pageContent !== "") {
+            $pages[] = $pageContent;
+        }
+
+        $pageContent = "";
+        $y = $topY;
+
+        $leftTitle = pdfEncodeWinAnsi(SITE_FULL_NAME);
+        $leftDepartment = pdfEncodeWinAnsi(SITE_DEPARTMENT_NAME);
+        $leftService = pdfEncodeWinAnsi(SITE_SERVICE_HEADER);
+
+        $writeText($leftMargin, $y, $leftTitle, 16);
+        $y -= 18;
+        $writeText($leftMargin, $y, $leftDepartment, 11);
+        $y -= 14;
+        $writeText($leftMargin, $y, $leftService, 11);
+
+        $writeText(350, $topY, "BESTELLUNG", 11);
+        $writeText(350, $topY - 22, "ID " . $orderId, 20);
+        $writeText(350, $topY - 44, "Erstellt am: " . $createdAt, 11);
+
+        $y = $topY - 62;
+        $drawLine($leftMargin, $y, $pageWidth - $rightMargin, $y);
+        $y -= 18;
+
+        if ($isFirstPage) {
+            $writeText($leftMargin, $y, $labelBelongsTo, 11);
+            $y -= 18;
+
+            $belongsFields = [
+                "Name: " . $customerName,
+                "Organisation: " . $organization,
+                "E-Mail: " . $customerEmail,
+            ];
+
+            foreach ($belongsFields as $fieldLine) {
+                foreach (pdfWrapAnsiText($fieldLine, 70) as $wrappedLine) {
+                    $writeText($leftMargin, $y, $wrappedLine, 12);
+                    $y -= 16;
+                }
+            }
+
+            $y -= 2;
+            $drawLine($leftMargin, $y, $pageWidth - $rightMargin, $y);
+            $y -= 18;
+            $isFirstPage = false;
+        } else {
+            $writeText($leftMargin, $y, "Fortsetzung Bestellung " . $orderId, 11);
+            $y -= 20;
+        }
+    };
+
+    $ensureSpace = function ($requiredHeight) use (&$y, $bottomY, $startPage) {
+        if ($y - $requiredHeight < $bottomY) {
+            $startPage();
+        }
+    };
+
+    $drawItemTableHeader = function () use (
+        &$y,
+        $lineHeight,
+        $leftMargin,
+        $writeText,
+        $labelSize,
+        $drawLine,
+        $pageWidth,
+        $rightMargin
+    ) {
+        $writeText($leftMargin, $y, "Nr.", 11);
+        $writeText(75, $y, "Artikel", 11);
+        $writeText(340, $y, $labelSize, 11);
+        $writeText(430, $y, "Hinweis", 11);
+        $y -= $lineHeight;
+        $drawLine($leftMargin, $y + 4, $pageWidth - $rightMargin, $y + 4);
+    };
+
+    $startPage();
+    $ensureSpace(36);
+    $writeText($leftMargin, $y, "Artikelliste", 13);
+    $y -= 20;
+    $drawItemTableHeader();
+
+    $itemNumber = 1;
+    foreach ($order["items"] as $item) {
+        $itemName = pdfEncodeWinAnsi($item["product_name"]);
+        $sizeLabel = pdfEncodeWinAnsi($item["size"]);
+        $hintLabel = pdfEncodeWinAnsi(
+            preg_replace("/\s+/", " ", (string) $item["availability_label"]),
+        );
+
+        $itemLines = pdfWrapAnsiText($itemName, 40);
+        $sizeLines = pdfWrapAnsiText($sizeLabel, 12);
+        $hintLines = pdfWrapAnsiText($hintLabel, 22);
+
+        $lineCount = max(count($itemLines), count($sizeLines), count($hintLines));
+        $rowHeight = $lineCount * $lineHeight + 4;
+
+        if ($y - $rowHeight < $bottomY) {
+            $startPage();
+            $ensureSpace(30);
+            $writeText($leftMargin, $y, "Artikelliste (Fortsetzung)", 13);
+            $y -= 20;
+            $drawItemTableHeader();
+        }
+
+        $rowTop = $y;
+        $writeText($leftMargin, $rowTop, (string) $itemNumber, 11);
+
+        for ($i = 0; $i < $lineCount; $i++) {
+            if (isset($itemLines[$i])) {
+                $writeText(75, $rowTop - $i * $lineHeight, $itemLines[$i], 11);
+            }
+            if (isset($sizeLines[$i])) {
+                $writeText(340, $rowTop - $i * $lineHeight, $sizeLines[$i], 11);
+            }
+            if (isset($hintLines[$i])) {
+                $writeText(430, $rowTop - $i * $lineHeight, $hintLines[$i], 11);
+            }
+        }
+
+        $y -= $rowHeight;
+        $drawLine($leftMargin, $y + 4, $pageWidth - $rightMargin, $y + 4);
+        $y -= 4;
+        $itemNumber++;
+    }
+
+    $commentSource = trim($comment) !== "" ? $comment : "Kein Kommentar";
+    $commentLines = [];
+    foreach (preg_split('/\r\n|\r|\n/', $commentSource) as $commentPart) {
+        $wrapped = pdfWrapAnsiText($commentPart, 92);
+        foreach ($wrapped as $line) {
+            $commentLines[] = $line;
         }
-        $first = false;
-        $content .= "(" . pdfEscapeText($line) . ") Tj\n";
     }
 
-    $content .= "ET";
-    $length = strlen($content);
+    $ensureSpace(40);
+    $y -= 8;
+    $writeText($leftMargin, $y, "Kommentar", 13);
+    $y -= 18;
 
+    foreach ($commentLines as $line) {
+        if ($y - $lineHeight < $bottomY) {
+            $startPage();
+            $ensureSpace(34);
+            $writeText($leftMargin, $y, "Kommentar (Fortsetzung)", 13);
+            $y -= 18;
+        }
+        $writeText($leftMargin, $y, $line, 11);
+        $y -= $lineHeight;
+    }
+
+    $footerHeight = 96;
+    $ensureSpace($footerHeight + 10);
+    $y -= 12;
+    $drawLine($leftMargin, $y + 8, $pageWidth - $rightMargin, $y + 8);
+    $writeText($leftMargin, $y, "Lagerbearbeitung", 13);
+    $y -= 18;
+    $writeText($leftMargin, $y, "Ausgegeben am: ____________________", 11);
+    $y -= 20;
+    $writeText($leftMargin, $y, "Ausgegeben durch: ____________________", 11);
+    $y -= 20;
+    $writeText($leftMargin, $y, "Unterschrift: ________________________", 11);
+    $y -= 22;
+
+    $drawRect($leftMargin, $y - 8, 10, 10);
+    $writeText($leftMargin + 16, $y, $labelFullyIssued, 11);
+    $y -= 18;
+    $drawRect($leftMargin, $y - 8, 10, 10);
+    $writeText($leftMargin + 16, $y, "Teilweise ausgegeben", 11);
+
+    if ($pageContent !== "") {
+        $pages[] = $pageContent;
+    }
+
+    if (empty($pages)) {
+        $pages[] = "";
+    }
+
+    $fontObjectNumber = 3;
     $objects = [];
-    $objects[] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
-    $objects[] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
-    $objects[] =
-        "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj\n";
-    $objects[] =
-        "4 0 obj\n<< /Length " .
-        $length .
-        " >>\nstream\n" .
-        $content .
-        "\nendstream\nendobj\n";
-    $objects[] =
-        "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n";
+    $objects[1] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
+    $objects[
+        $fontObjectNumber
+    ] = "3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n";
+
+    $kids = [];
+    $nextObjectNumber = 4;
+    foreach ($pages as $pageStream) {
+        $pageObjectNumber = $nextObjectNumber++;
+        $contentObjectNumber = $nextObjectNumber++;
+
+        $kids[] = $pageObjectNumber . " 0 R";
+        $objects[$pageObjectNumber] =
+            $pageObjectNumber .
+            " 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " .
+            $pageWidth .
+            " " .
+            $pageHeight .
+            "] /Contents " .
+            $contentObjectNumber .
+            " 0 R /Resources << /Font << /F1 " .
+            $fontObjectNumber .
+            " 0 R >> >> >>\nendobj\n";
+
+        $objects[$contentObjectNumber] =
+            $contentObjectNumber .
+            " 0 obj\n<< /Length " .
+            strlen($pageStream) .
+            " >>\nstream\n" .
+            $pageStream .
+            "\nendstream\nendobj\n";
+    }
+
+    $objects[2] =
+        "2 0 obj\n<< /Type /Pages /Kids [" .
+        implode(" ", $kids) .
+        "] /Count " .
+        count($kids) .
+        " >>\nendobj\n";
+
+    ksort($objects);
 
     $pdf = "%PDF-1.4\n";
     $offsets = [0];
-    foreach ($objects as $object) {
-        $offsets[] = strlen($pdf);
-        $pdf .= $object;
+    foreach ($objects as $number => $objectContent) {
+        $offsets[$number] = strlen($pdf);
+        $pdf .= $objectContent;
     }
 
+    $lastObjectNumber = (int) max(array_keys($objects));
     $xrefOffset = strlen($pdf);
-    $pdf .= "xref\n0 " . (count($objects) + 1) . "\n";
+    $pdf .= "xref\n0 " . ($lastObjectNumber + 1) . "\n";
     $pdf .= "0000000000 65535 f \n";
 
-    for ($i = 1; $i <= count($objects); $i++) {
-        $pdf .= sprintf("%010d 00000 n \n", $offsets[$i]);
+    for ($i = 1; $i <= $lastObjectNumber; $i++) {
+        if (isset($offsets[$i])) {
+            $pdf .= sprintf("%010d 00000 n \n", $offsets[$i]);
+        } else {
+            $pdf .= "0000000000 00000 f \n";
+        }
     }
 
-    $pdf .= "trailer\n<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\n";
+    $pdf .= "trailer\n<< /Size " . ($lastObjectNumber + 1) . " /Root 1 0 R >>\n";
     $pdf .= "startxref\n" . $xrefOffset . "\n%%EOF";
 
     return $pdf;

+ 6 - 0
product.php

@@ -12,6 +12,7 @@ if ($product === null) {
 
 $pageTitle = $product["name"];
 $sizes = getProductSizes($product);
+$categoryLabels = getCategoryLabels(getProductCategoryIds($product));
 
 if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['add_to_cart'])) {
     // Validate CSRF token
@@ -87,6 +88,11 @@ include __DIR__ . "/includes/header.php";
 
     <div class="product-copy">
         <h1><?php echo escape($product["name"]); ?></h1>
+        <?php if (!empty($categoryLabels)): ?>
+            <p class="product-category-text">
+                Kategorie: <?php echo escape(implode(", ", $categoryLabels)); ?>
+            </p>
+        <?php endif; ?>
 
         <div class="product-description-block">
             <h3>Beschreibung</h3>