replaceTemplateFunctions($html, $context); // Triple curly braces for raw/unescaped HTML: {{{ $content }}} or {{{ content }}} // Supports both old and new syntax for backwards compatibility $html = preg_replace_callback( '/{{{\\s*\\$?([\\w.]+)\\s*}}}/', function ($matches) use ($context) { $expression = $matches[1]; return $this->resolveRaw($context->data, $expression); }, $html ); // Standard Variablen und Methoden: {{ $item.getRelativeFile() }} or {{ item.getRelativeFile() }} // Supports both old and new syntax for backwards compatibility return preg_replace_callback( '/{{\\s*\\$?([\\w.]+)(?:\\(\\s*([^)]*)\\s*\\))?\\s*}}/', function ($matches) use ($context) { $expression = $matches[1]; $params = isset($matches[2]) ? trim($matches[2]) : null; if ($params !== null) { return $this->resolveMethodCall($context->data, $expression, $params); } else { return $this->resolveEscaped($context->data, $expression, ENT_QUOTES | ENT_HTML5); } }, $html ); } private function replaceTemplateFunctions(string $html, RenderContext $context): string { return preg_replace_callback( '/{{\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\(([^)]*)\\)\\s*}}/', function ($matches) use ($context) { $functionName = $matches[1]; $params = trim($matches[2]); $functions = new TemplateFunctions($this->container, ImageSlotFunction::class, LazyComponentFunction::class, UrlFunction::class); if ($functions->has($functionName)) { $function = $functions->get($functionName); $args = $this->parseParams($params, $context->data); if (is_callable($function)) { return $function(...$args); } #return $function(...$args); } // Nur erlaubte Funktionen if (! in_array($functionName, $this->allowedTemplateFunctions)) { return $matches[0]; } try { $args = $this->parseParams($params, $context->data); // Custom Template-Funktionen if (method_exists($this, 'function_' . $functionName)) { $result = $this->{'function_' . $functionName}(...$args); return $result; #return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } // Standard PHP-Funktionen (begrenzt) if (function_exists($functionName)) { $result = $functionName(...$args); return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } } catch (\Throwable $e) { return $matches[0]; } return $matches[0]; }, $html ); } private function function_imageslot(string $slotName): string { $function = $this->container->get(ImageSlotFunction::class); return ($function('slot1')); } // Custom Template-Funktionen private function function_format_date(string|DateTime $date, string $format = 'Y-m-d H:i:s'): string { if (is_string($date)) { $date = new \DateTimeImmutable($date, new DateTimeZone('Europe/Berlin')); } return $date->format($format); } private function function_format_currency(float $amount, string $currency = 'EUR'): string { return number_format($amount, 2, ',', '.') . ' ' . $currency; } private function function_format_filesize(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $factor = floor((strlen((string)$bytes) - 1) / 3); return sprintf("%.1f %s", $bytes / pow(1024, $factor), $units[$factor]); } private function resolveMethodCall(array $data, string $expression, string $params): string { $parts = explode('.', $expression); $methodName = array_pop($parts); // Letzter Teil ist der Methodenname $objectPath = implode('.', $parts); // Pfad zum Objekt // Objekt auflösen $object = $this->resolveValue($data, $objectPath); if (! is_object($object)) { return '{{ ' . $expression . '() }}'; // Platzhalter beibehalten } if (! method_exists($object, $methodName)) { return '{{ ' . $expression . '() }}'; // Platzhalter beibehalten } try { // Parameter parsen (falls vorhanden) $parsedParams = empty($params) ? [] : $this->parseParams($params, $data); // Methode aufrufen $result = $object->$methodName(...$parsedParams); return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } catch (\Throwable $e) { // Bei Fehlern Platzhalter beibehalten return '{{ ' . $expression . '() }}'; } } private function resolveRaw(array $data, string $expr): string { $value = $this->resolveValue($data, $expr); if ($value === null) { // Bleibt als Platzhalter stehen return '{{{ ' . $expr . ' }}}'; } // LiveComponentContract - automatisch rendern mit Wrapper if ($value instanceof LiveComponentContract) { return $this->componentRegistry->renderWithWrapper($value); } // RawHtml-Objekte - direkt ausgeben if ($value instanceof RawHtml) { return $value->content; } // HtmlElement-Objekte - direkt ausgeben if ($value instanceof \App\Framework\View\ValueObjects\HtmlElement) { return (string) $value; } // Strings direkt ausgeben (KEIN escaping!) if (is_string($value)) { return $value; } // Arrays und komplexe Objekte können nicht direkt als String dargestellt werden if (is_array($value)) { return $this->handleArrayValue($expr, $value); } if (is_object($value) && ! method_exists($value, '__toString')) { return $this->handleObjectValue($expr, $value); } return (string)$value; } private function resolveEscaped(array $data, string $expr, int $flags): string { $value = $this->resolveValue($data, $expr); if ($value === null) { // Bleibt als Platzhalter stehen return '{{ ' . $expr . ' }}'; } // LiveComponentContract - automatisch rendern mit Wrapper // WICHTIG: Wrapper-HTML ist bereits escaped, also NICHT nochmal escapen! if ($value instanceof LiveComponentContract) { $wrapperHtml = $this->componentRegistry->renderWithWrapper($value); // Wrap in RawHtml to prevent double-escaping by code below (line 271) return $wrapperHtml; } // RawHtml-Objekte nicht escapen if ($value instanceof RawHtml) { return $value->content; } // HtmlElement-Objekte nicht escapen (Framework Components) if ($value instanceof \App\Framework\View\ValueObjects\HtmlElement) { return (string) $value; } // Arrays und komplexe Objekte können nicht direkt als String dargestellt werden if (is_array($value)) { return $this->handleArrayValue($expr, $value); } if (is_object($value) && ! method_exists($value, '__toString')) { return $this->handleObjectValue($expr, $value); } // Zusätzlicher Schutz gegen Array-to-String Conversion if (is_array($value) || (is_object($value) && ! method_exists($value, '__toString'))) { // In Debug: Exception mit Details werfen if ($this->isDebugMode()) { $type = is_array($value) ? 'array[' . count($value) . ']' : 'object(' . get_class($value) . ')'; throw new \InvalidArgumentException("Cannot convert {$type} to string in placeholder: {{ {$expr} }}"); } // In Production: leerer String return ''; } return htmlspecialchars((string)$value, $flags, 'UTF-8'); } private function resolveValue(array $data, string $expr): mixed { $keys = explode('.', $expr); $value = $data; foreach ($keys as $key) { if (is_array($value) && array_key_exists($key, $value)) { $value = $value[$key]; } elseif (is_object($value) && isset($value->$key)) { $value = $value->$key; } else { return null; } } return $value; } /** * Parst Parameter aus einem Parameter-String */ private function parseParams(string $paramsString, array $data): array { if (empty(trim($paramsString))) { return []; } $params = []; $parts = $this->splitParams($paramsString); foreach ($parts as $part) { $part = trim($part); // String-Literale: 'text' oder "text" if (preg_match('/^[\'\"](.*)[\'\"]/s', $part, $matches)) { $params[] = $matches[1]; continue; } // Zahlen if (is_numeric($part)) { $params[] = str_contains($part, '.') ? (float)$part : (int)$part; continue; } // Boolean if ($part === 'true') { $params[] = true; continue; } if ($part === 'false') { $params[] = false; continue; } // Null if ($part === 'null') { $params[] = null; continue; } // Variablen-Referenzen: $variable oder Pfade: objekt.eigenschaft if (str_starts_with($part, '$')) { $varName = substr($part, 1); if (array_key_exists($varName, $data)) { $params[] = $data[$varName]; continue; } } elseif (str_contains($part, '.')) { $value = $this->resolveValue($data, $part); if ($value !== null) { $params[] = $value; continue; } } else { // Einfache Variablennamen (ohne $ oder .) aus Template-Daten auflösen $value = $this->resolveValue($data, $part); if ($value !== null) { $params[] = $value; continue; } } // Fallback: Als String behandeln $params[] = $part; } return $params; } /** * Behandelt Array-Werte in Templates - Debug vs Production Verhalten */ private function handleArrayValue(string $expr, array $value): string { // In Debug-Mode: Exception werfen um Entwickler auf das Problem hinzuweisen if ($this->isDebugMode()) { throw new \InvalidArgumentException( "Template placeholder '{{ {$expr} }}' resolved to an array, which cannot be displayed as string. " . "Use array access like '{{ {$expr}.0 }}' or iterate with loop." ); } // In Production: Sinnvolle Fallback-Werte für häufige Arrays if (empty($value)) { return ''; // Leere Arrays werden zu leerem String } // Für Arrays mit nur einem Element: das Element zurückgeben if (count($value) === 1 && ! is_array(reset($value)) && ! is_object(reset($value))) { return htmlspecialchars((string)reset($value), ENT_QUOTES | ENT_HTML5, 'UTF-8'); } // Für einfache Arrays: Anzahl anzeigen return '[' . count($value) . ' items]'; } /** * Behandelt Object-Werte in Templates - Debug vs Production Verhalten */ private function handleObjectValue(string $expr, object $value): string { // In Debug-Mode: Exception werfen if ($this->isDebugMode()) { throw new \InvalidArgumentException( "Template placeholder '{{ {$expr} }}' resolved to object of type '" . get_class($value) . "', " . "which cannot be displayed as string. Implement __toString() method or access specific properties." ); } // In Production: Klassenname anzeigen $className = get_class($value); $shortName = substr($className, strrpos($className, '\\') + 1); return '[' . $shortName . ']'; } /** * Erkennt ob wir im Debug-Modus sind über AppConfig */ private function isDebugMode(): bool { try { /** @var AppConfig $appConfig */ $appConfig = $this->container->get(AppConfig::class); return $appConfig->isDebug(); } catch (\Throwable $e) { // Fallback zu Environment-Variable falls AppConfig nicht verfügbar return ($_ENV['APP_ENV'] ?? 'production') === 'development'; } } /** * Teilt einen Parameter-String in einzelne Parameter auf */ private function splitParams(string $paramsString): array { $params = []; $current = ''; $inQuotes = false; $quoteChar = null; $length = strlen($paramsString); for ($i = 0; $i < $length; $i++) { $char = $paramsString[$i]; if (! $inQuotes && ($char === '"' || $char === "'")) { $inQuotes = true; $quoteChar = $char; $current .= $char; } elseif ($inQuotes && $char === $quoteChar) { $inQuotes = false; $quoteChar = null; $current .= $char; } elseif (! $inQuotes && $char === ',') { $params[] = trim($current); $current = ''; } else { $current .= $char; } } if ($current !== '') { $params[] = trim($current); } return $params; } }