9 private $dd_media =
'dbxMedia';
10 private $dd_media_usage =
'dbxMediaUsage';
11 private array $folderRowCache = array();
14 private function folder_row(
$db,
int $folderId): array {
15 if ($folderId <= 0)
return array();
17 if (!array_key_exists($key, $this->folderRowCache)) {
19 $this->folderRowCache[$key] = is_array($row) ? $row : array();
21 return $this->folderRowCache[$key];
30 $priorityAssigned = false;
31 $result = preg_replace_callback(
'/<img\b[^>]*>/i',
static function(array $match) use (&$priorityAssigned):
string {
32 $tag = (string)($match[0] ??
'');
33 if ($tag ===
'')
return $tag;
35 if (!$priorityAssigned) {
36 $tag = self::withHtmlImageAttr($tag,
'loading',
'eager',
true);
37 $tag = self::withHtmlImageAttr($tag,
'fetchpriority',
'high',
true);
38 $priorityAssigned =
true;
43 $tag = self::withHtmlImageAttr($tag,
'loading',
'lazy',
true);
44 $tag = self::withHtmlImageAttr($tag,
'fetchpriority',
'low',
true);
47 return self::withHtmlImageAttr($tag,
'decoding',
'async');
53 private static function withHtmlImageAttr(
string $tag,
string $name,
string $value,
bool $replace =
false): string {
54 $attrPattern =
'/\s' . preg_quote($name,
'/') .
'\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i';
55 $attribute =
' ' . $name .
'="' . htmlspecialchars(
$value, ENT_QUOTES,
'UTF-8') .
'"';
56 if (preg_match($attrPattern, $tag)) {
57 if (!$replace)
return $tag;
58 $updated = preg_replace($attrPattern, $attribute, $tag, 1);
62 $closing = str_ends_with(rtrim($tag),
'/>') ?
'/>' :
'>';
64 return substr(
$trimmed, 0, -strlen($closing)) . $attribute . $closing;
72 $tpl =
dbx()->get_system_obj(
'dbxTPL');
73 $permalink = trim((
string)
dbx()->get_system_var(
'dbx_permalink',
''),
'/');
74 return $tpl->get_tpl(
'dbxContent|no-page', array(
75 'permalink' =>
dbx()->
esc($permalink !==
'' ? $permalink :
'/'),
81 if ($cid <= 0)
return 'Keine dbx_cid gesetzt!';
83 $db =
dbx()->get_system_obj(
'dbxDB');
84 $tpl =
dbx()->get_system_obj(
'dbxTPL');
86 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0) {
90 $rights = $this->resolve_content_rights(
$db, $rec);
91 if (!
dbx()->
can($rights)) {
93 return '<div class="alert alert-warning" role="alert">Sie haben keinen Zugriff auf diese Seite.</div>';
98 $this->update_hits(
$db, $rec);
101 $forceTemplate = trim((
string)(
$options[
'template'] ??
''));
102 if ($forceTemplate !==
'') {
103 $template = $this->normalize_content_template($forceTemplate);
105 $template = $this->normalize_content_template($this->resolve_content_setting(
$db, $rec,
'template',
'template',
'c-content'));
111 $slots = $this->detect_template_slots($template_html);
112 $content_html = $this->convert_mod_placeholders((
string)($rec[
'content'] ??
''));
115 $cms_cols = max(1, min(3, (
int)(
$slots[
'cols'] ?? 1)));
117 $settings = $this->content_settings(
$db, $rec);
118 $hero_classes = array(
119 $this->css_class_token(
'hero-template', $settings[
'hero_template'] ??
'image-hero'),
120 $this->css_class_token(
'hero-variant', $settings[
'hero_variant'] ??
'original'),
121 $this->is_enabled_value($settings[
'hero_sticky'] ??
'0') ?
'hero-sticky' :
'hero-not-sticky',
122 $this->css_class_token(
'hero-scroll', $settings[
'hero_scroll_layer'] ??
'under'),
124 $gallery_classes = array(
125 $this->css_class_token(
'gallery-template', $settings[
'gallery_template'] ??
'image-gallery'),
126 $this->css_class_token(
'gallery-overflow', $settings[
'gallery_overflow'] ??
'grid'),
127 $this->css_class_token(
'gallery-click', $settings[
'gallery_click_behavior'] ??
'lightbox'),
128 $this->css_class_token(
'gallery-count', $settings[
'gallery_visible_count'] ??
'3'),
129 $this->css_class_token(
'gallery-size', $settings[
'gallery_image_size'] ??
'original'),
130 $this->css_class_token(
'gallery-lightbox-width', $settings[
'gallery_lightbox_width'] ??
'100vw'),
132 $hero_style = $this->css_custom_properties(array(
133 'cms-hero-margin-top' => $this->css_length_value($settings[
'hero_margin_top'] ??
'0',
'0'),
134 'cms-hero-height' => $this->css_length_value($settings[
'hero_height'] ??
'auto',
'auto'),
135 ) + $this->hero_content_custom_properties($settings[
'hero_height'] ??
'auto'));
136 $gallery_style = $this->css_custom_properties(array(
137 'dbx-gallery-visible-count' => (
string)max(1, min(12, (
int)($settings[
'gallery_visible_count'] ?? 3))),
138 'dbx-gallery-aspect-ratio' => $this->gallery_aspect_ratio($settings[
'gallery_image_size'] ??
'original'),
139 'dbx-gallery-lightbox-width' => $this->css_length_value($settings[
'gallery_lightbox_width'] ??
'100vw',
'100vw'),
144 'id' => (
string)($rec[
'id'] ??
''),
145 'title' => (
string)($rec[
'title'] ??
''),
146 'description' => (
string)($rec[
'description'] ??
''),
147 'keywords' => (
string)($rec[
'keywords'] ??
''),
148 'permalink' => (
string)($rec[
'permalink'] ??
''),
150 'content' =>
$parsed[
'content'],
155 'thesar' => (
string)($rec[
'thesar'] ??
''),
161 'content:header' =>
$parsed[
'header'],
162 'content:hero' =>
$parsed[
'hero'],
163 'content:teaser' =>
$parsed[
'teaser'],
164 'content:thesar' =>
$parsed[
'teaser'],
165 'content:footer' =>
$parsed[
'footer'],
166 'content:body' =>
$parsed[
'body'],
167 'content:col1' =>
$parsed[
'col1'],
168 'content:col2' =>
$parsed[
'col2'],
169 'content:col3' =>
$parsed[
'col3'],
170 'cms:title' => $this->render_title_slot(
$tpl, $template_html, (
string)($rec[
'title'] ??
'')),
171 'cms:header' => $this->render_inline(
$tpl,
'header',
$parsed[
'header']),
172 'cms:teaser' => $this->render_inline(
$tpl,
'header',
$parsed[
'header'],
'header'),
173 'cms:content' => $this->render_inline(
$tpl,
'content',
$parsed[
'content']),
174 'cms:cols' => (
string)$cms_cols,
175 'cms:hero_class' =>
'no-hero',
176 'cms:gallery_class' =>
'no-gallery',
179 'cms:footer' => $this->render_inline(
$tpl,
'footer',
$parsed[
'footer']),
180 'cms:col1' => $cms_cols === 1 ?
$parsed[
'body'] : $this->render_inline(
$tpl,
'col',
$parsed[
'col1'],
'col'),
181 'cms:col2' => $this->render_inline(
$tpl,
'col',
$parsed[
'col2'],
'col'),
182 'cms:col3' => $this->render_inline(
$tpl,
'col',
$parsed[
'col3'],
'col'),
186 'hero:template' => htmlspecialchars((
string)($settings[
'hero_template'] ??
'image-hero'), ENT_QUOTES,
'UTF-8'),
187 'hero:image_id' => htmlspecialchars((
string)($settings[
'hero_image_id'] ??
''), ENT_QUOTES,
'UTF-8'),
188 'hero:margin_top' => htmlspecialchars((
string)($settings[
'hero_margin_top'] ??
'0'), ENT_QUOTES,
'UTF-8'),
189 'hero:height' => htmlspecialchars((
string)($settings[
'hero_height'] ??
'auto'), ENT_QUOTES,
'UTF-8'),
190 'hero:variant' => htmlspecialchars((
string)($settings[
'hero_variant'] ??
'original'), ENT_QUOTES,
'UTF-8'),
191 'hero:sticky' => htmlspecialchars((
string)($settings[
'hero_sticky'] ??
'0'), ENT_QUOTES,
'UTF-8'),
192 'hero:scroll_layer' => htmlspecialchars((
string)($settings[
'hero_scroll_layer'] ??
'under'), ENT_QUOTES,
'UTF-8'),
193 'hero:class' => implode(
' ', array_filter($hero_classes)),
194 'hero:style' => $hero_style,
195 'gallery:template' => htmlspecialchars((
string)($settings[
'gallery_template'] ??
'image-gallery'), ENT_QUOTES,
'UTF-8'),
196 'gallery:visible_count' => htmlspecialchars((
string)($settings[
'gallery_visible_count'] ??
'3'), ENT_QUOTES,
'UTF-8'),
197 'gallery:image_size' => htmlspecialchars((
string)($settings[
'gallery_image_size'] ??
'original'), ENT_QUOTES,
'UTF-8'),
198 'gallery:lightbox_width' => htmlspecialchars($this->css_length_value($settings[
'gallery_lightbox_width'] ??
'100vw',
'100vw'), ENT_QUOTES,
'UTF-8'),
199 'gallery:overflow' => htmlspecialchars((
string)($settings[
'gallery_overflow'] ??
'grid'), ENT_QUOTES,
'UTF-8'),
200 'gallery:click_behavior' => htmlspecialchars((
string)($settings[
'gallery_click_behavior'] ??
'lightbox'), ENT_QUOTES,
'UTF-8'),
201 'gallery:class' => implode(
' ', array_filter($gallery_classes)),
202 'gallery:style' => $gallery_style,
205 $hero_media = trim((
string)(
$merge[
'media:hero'] ??
''));
206 $hero_content = $this->render_inline(
$tpl,
'hero',
$parsed[
'hero'],
'hero');
207 $hero_content = trim((
string)$hero_content);
208 $merge[
'cms:hero'] = $hero_media
209 . ($hero_content !==
'' ?
'<div class="hero-content">' . $hero_content .
'</div>' :
'');
211 $hero_state_classes = array(
212 ($hero_media !==
'' || $hero_content !==
'') ?
'has-hero' :
'no-hero',
213 $hero_media !==
'' ?
'has-hero-media' :
'',
214 $hero_content !==
'' ?
'has-hero-content' :
'',
216 $merge[
'cms:hero_class'] = trim(implode(
' ', array_filter($hero_state_classes)) .
' ' . (
$merge[
'hero:class'] ??
''));
217 $merge[
'cms:gallery_class'] = trim((trim(
$merge[
'cms:gallery']) !==
'' ?
'has-gallery ' :
'no-gallery ') . (
$merge[
'gallery:class'] ??
''));
218 $has_gallery_slot = !empty(
$slots[
'media'][
'gallery']) || !empty(
$slots[
'cms'][
'gallery']);
219 $has_gallery_media = trim((
string)(
$merge[
'cms:gallery'] ??
'')) !==
'';
220 $merge[
'gallery:data_dbx'] = $this->gallery_data_dbx_attrs($settings, $has_gallery_slot, $has_gallery_media);
221 $content_html = $this->replace_content_markers($template_html,
$merge);
222 $content_html = $this->cleanup_empty_sections($content_html);
223 $content_html = $this->decorate_brand_text($content_html);
224 $content_html =
$tpl->replaces($content_html,
$merge);
225 return $content_html;
228 private function decorate_brand_text(
string $html): string {
229 if (
$html ===
'' || strpos(
$html,
'dbXapp') === false) {
233 return $this->replace_text_segments(
$html,
function(
string $text):
string {
234 return str_replace(
'dbXapp',
'db<span style="color:red">X</span>app', $text);
238 private function replace_text_segments(
string $html, callable $callback): string {
239 $parts = preg_split(
'/(<[^>]+>)/',
$html, -1, PREG_SPLIT_DELIM_CAPTURE);
240 if (!is_array($parts)) {
241 return (
string)$callback(
$html);
244 foreach ($parts as $idx => $part) {
245 if ($part ===
'' || $part[0] ===
'<') {
248 $parts[$idx] = (string)$callback($part);
251 return implode(
'', $parts);
254 public function applySeoMeta(
int $cid, array $rec =
null, array $meta =
null) {
256 if ($cid <= 0)
return;
258 $db =
dbx()->get_system_obj(
'dbxDB');
259 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0) {
260 if (is_array($meta) && !empty($meta[
'seo']) && is_array($meta[
'seo'])) {
261 $rec = array_merge(array(
'id' => $cid), $meta[
'seo']);
264 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0)
return;
268 $pageTitle = trim((
string)($rec[
'title'] ??
''));
269 $seoTitle = trim((
string)($rec[
'seo_title'] ??
''));
270 $displayTitle = $seoTitle !==
'' ? $seoTitle : $pageTitle;
271 dbx()->set_system_var(
'dbx_title', $displayTitle);
273 $description = trim((
string)($rec[
'description'] ??
''));
274 if ($description ===
'') {
275 $description = $this->seoExcerptFromContent((
string)($rec[
'content'] ??
''));
278 $keywords = trim((
string)($rec[
'keywords'] ??
''));
279 $currentLng = strtolower(trim((
string)
dbx()->get_system_var(
'dbx_lng',
'de')));
280 if ($currentLng ===
'') $currentLng =
'de';
283 $canonical = $this->seoCanonicalUrl((
string)($rec[
'permalink'] ??
''), $isHomePage);
284 $activ = (int)($rec[
'activ'] ?? 1);
285 $metaRobots = trim((
string)($rec[
'meta_robots'] ??
''));
286 if ($metaRobots ===
'') {
287 $metaRobots = (
$activ === 0) ?
'noindex,nofollow' :
'index,follow';
288 } elseif (
$activ === 0 && stripos($metaRobots,
'noindex') ===
false) {
289 $metaRobots =
'noindex,nofollow';
292 $og_title = $displayTitle;
293 $og_image = $this->seoOgImageUrl(
$db, $rec);
294 dbx()->set_system_var(
'dbx_meta_description', $description);
295 dbx()->set_system_var(
'dbx_meta_keywords', $keywords);
296 dbx()->set_system_var(
'dbx_canonical', $canonical);
297 dbx()->set_system_var(
'dbx_robots', $metaRobots);
298 dbx()->set_system_var(
'dbx_og_title', $og_title);
299 dbx()->set_system_var(
'dbx_og_description', $description);
300 dbx()->set_system_var(
'dbx_og_url', $canonical);
301 dbx()->set_system_var(
'dbx_og_image', $og_image);
302 dbx()->set_system_var(
'dbx_hreflang', $this->seoHreflangBlock(
$db, $rec, $currentLng));
303 dbx()->set_system_var(
'dbx_json_ld', $this->seoJsonLd($rec, $displayTitle, $description, $canonical, $currentLng));
308 'title' => (string)($rec[
'title'] ??
''),
309 'seo_title' => (string)($rec[
'seo_title'] ??
''),
310 'description' => (string)($rec[
'description'] ??
''),
311 'keywords' => (string)($rec[
'keywords'] ??
''),
312 'meta_robots' => (string)($rec[
'meta_robots'] ??
''),
313 'seo_image_id' => (int)($rec[
'seo_image_id'] ?? 0),
314 'permalink' => (string)($rec[
'permalink'] ??
''),
315 'activ' => (int)($rec[
'activ'] ?? 1),
316 'update_date' => (string)($rec[
'update_date'] ??
''),
317 'lng_uid' => (string)($rec[
'lng_uid'] ??
''),
323 'dbx_meta_description',
328 'dbx_og_description',
334 dbx()->set_system_var($key,
'');
339 return $this->interpret_content_modules(
$html);
342 private function interpret_content_modules(
$html) {
344 if (
$html ===
'' || stripos(
$html,
'[modul=') ===
false) {
347 $interpreter =
dbx()->get_system_obj(
'dbxInterpreter');
348 if (!is_object($interpreter) || !method_exists($interpreter,
'run')) {
351 return $interpreter->run(
$html);
355 $db =
dbx()->get_system_obj(
'dbxDB');
356 return $this->resolve_folder_rights(
$db, $folder_id);
359 private function resolve_content_rights(
$db, array $rec): string {
362 return $this->resolve_folder_rights(
$db, (int)($rec[
'folder'] ?? 0));
365 private function resolve_folder_rights(
$db, $folder_id, array $visited = array()) {
366 $folder_id = (int)$folder_id;
367 if ($folder_id <= 0)
return '*';
368 if (isset($visited[$folder_id]))
return '*';
369 $visited[$folder_id] = 1;
371 $rec = $this->folder_row(
$db, $folder_id);
372 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0)
return '*';
374 $parent_id = (int)($rec[
'parent_id'] ?? 0);
375 $raw = trim((
string)($rec[
'group_read'] ??
''));
376 if ($raw ===
'')
return '*';
378 $parts = preg_split(
'/\s*,\s*/', $raw, -1, PREG_SPLIT_NO_EMPTY);
380 $uses_parent =
false;
382 foreach ($parts as $part) {
383 $part = trim((
string)$part);
384 if ($part ===
'')
continue;
385 if (strtolower($part) ===
'parent') {
393 $parent_rights = $this->resolve_folder_rights(
$db, $parent_id, $visited);
394 foreach (preg_split(
'/\s*,\s*/', $parent_rights, -1, PREG_SPLIT_NO_EMPTY) as $part) {
395 $part = trim((
string)$part);
396 if ($part !==
'' && strtolower($part) !==
'parent')
$out[$part] = 1;
400 if (!count(
$out))
return '*';
401 return implode(
',', array_keys(
$out));
404 private function is_parent_value(
$value) {
406 return $value ===
'' || strtolower(
$value) ===
'parent';
409 private function is_no_hero_value(
$value) {
411 return in_array(
$value, array(
'none',
'no-hero',
'0',
'off'),
true);
414 private function is_enabled_value(
$value) {
416 return in_array(
$value, array(
'1',
'true',
'on',
'yes',
'sticky'),
true);
419 private function resolve_folder_setting(
$db, $folder_id,
$field,
$fallback, array $visited = array()) {
420 $folder_id = (int)$folder_id;
421 if ($folder_id <= 0 || isset($visited[$folder_id]))
return $fallback;
422 $visited[$folder_id] = 1;
424 $rec = $this->folder_row(
$db, $folder_id);
425 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0)
return $fallback;
429 return $this->resolve_folder_setting(
$db, (
int)($rec[
'parent_id'] ?? 0),
$field,
$fallback, $visited);
432 private function resolve_content_setting(
$db, array $rec, $content_field, $folder_field,
$fallback) {
433 $value = trim((
string)($rec[$content_field] ??
''));
435 return $this->resolve_folder_setting(
$db, (
int)($rec[
'folder'] ?? 0), $folder_field,
$fallback);
438 private function local_content_setting(array $rec,
$field,
$fallback) {
443 private function content_settings(
$db, array $rec) {
445 'hero_template' => $this->resolve_content_setting(
$db, $rec,
'hero_template',
'hero_template',
'image-hero'),
446 'hero_image_id' => $this->resolve_content_setting(
$db, $rec,
'hero_image_id',
'hero_image_id',
''),
447 'hero_margin_top' => $this->resolve_content_setting(
$db, $rec,
'hero_margin_top',
'hero_margin_top',
'0'),
448 'hero_height' => $this->resolve_content_setting(
$db, $rec,
'hero_height',
'hero_height',
'auto'),
449 'hero_variant' => $this->resolve_content_setting(
$db, $rec,
'hero_variant',
'hero_variant',
'original'),
450 'hero_sticky' => $this->resolve_content_setting(
$db, $rec,
'hero_sticky',
'hero_sticky',
'0'),
451 'hero_scroll_layer' => $this->resolve_content_setting(
$db, $rec,
'hero_scroll_layer',
'hero_scroll_layer',
'under'),
452 'gallery_template' =>
'image-gallery',
453 'gallery_visible_count' =>
'3',
454 'gallery_image_size' => $this->local_content_setting($rec,
'gallery_image_size',
'original'),
455 'gallery_lightbox_width' => $this->local_content_setting($rec,
'gallery_lightbox_width',
'100vw'),
456 'gallery_overflow' => $this->local_content_setting($rec,
'gallery_overflow',
'grid'),
457 'gallery_click_behavior' => $this->local_content_setting($rec,
'gallery_click_behavior',
'lightbox'),
461 private function update_hits(
$db, array $rec) {
462 $hits = (int)($rec[
'hits'] ?? 0);
463 if ($hits < 0 || empty($rec[
'id']))
return;
464 $db->update(dbxContentLng::ddContent(), array(
'hits' => $hits + 1), (
int)$rec[
'id'], 0, 1, 1, 0);
467 private function get_content_tpl_from_folder($folder = 0) {
468 $folder = (int)$folder;
469 $db =
dbx()->get_system_obj(
'dbxDB');
470 while ($folder > 0) {
471 $rec = $this->folder_row(
$db, $folder);
472 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0)
break;
473 $tpl = trim((
string)($rec[
'template'] ??
''));
475 $folder = (int)($rec[
'parent_id'] ?? 0);
480 private function content_template_exists(
$template) {
484 return is_file($path);
487 private function normalize_content_template(
$template) {
493 'c-hero_header-gallery-body3-footer' =>
'c-title-hero_header-gallery-body3-footer',
495 if (isset($aliases[
$template]) && $this->content_template_exists($aliases[
$template])) {
500 $cols = strpos($template_key,
'col3') !==
false ?
'3' : (strpos($template_key,
'col2') !==
false ?
'2' :
'');
501 $has_hero = strpos($template_key,
'hero') !==
false;
502 $has_gallery = strpos($template_key,
'gallery') !==
false;
503 $has_header = strpos($template_key,
'teaser') !==
false || strpos($template_key,
'thesar') !==
false;
505 if ($has_hero && $has_gallery && $cols ===
'3' && $this->content_template_exists(
'c-title-hero_header-gallery-body3-footer')) {
506 return 'c-title-hero_header-gallery-body3-footer';
508 if ($has_hero && $has_gallery)
return 'c-hero-header-gallery-body' . $cols .
'-footer';
509 if ($has_hero)
return 'c-hero-header-body' . $cols .
'-footer';
510 if ($has_gallery)
return 'c-header-gallery-body' . $cols .
'-footer';
511 if ($has_header || strpos($template_key,
'footer') !==
false || $cols !==
'')
return 'c-header-body' . $cols .
'-footer';
515 private function detect_template_slots($template_html) {
516 $template_html = (string)$template_html;
518 'content' => array(),
524 if (preg_match_all(
'/\{content:([a-z0-9_]+)\}/i', $template_html, $m)) {
525 foreach ($m[1] as $slot) {
526 $slots[
'content'][strtolower($slot)] = 1;
529 if (preg_match_all(
'/\{media:([a-z0-9_]+)\}/i', $template_html, $m)) {
530 foreach ($m[1] as $slot) {
531 $slots[
'media'][strtolower($slot)] = 1;
534 if (preg_match_all(
'/\{cms:([a-z0-9_]+)\}/i', $template_html, $m)) {
535 foreach ($m[1] as $slot) {
536 $slot = strtolower($slot);
538 if (in_array($slot, array(
'hero',
'gallery'),
true))
$slots[
'media'][$slot] = 1;
539 if ($slot ===
'hero')
$slots[
'content'][
'hero'] = 1;
540 if ($slot ===
'header')
$slots[
'content'][
'header'] = 1;
541 if ($slot ===
'teaser')
$slots[
'content'][
'teaser'] = 1;
542 if ($slot ===
'content')
$slots[
'content'][
'content'] = 1;
543 if ($slot ===
'footer')
$slots[
'content'][
'footer'] = 1;
544 if (in_array($slot, array(
'col1',
'col2',
'col3'),
true))
$slots[
'content'][$slot] = 1;
548 if (strpos($template_html,
'{content}') !==
false || strpos($template_html,
'{content:body}') !==
false) {
549 $slots[
'content'][
'content'] = 1;
552 if (isset(
$slots[
'content'][
'col3']) || strpos($template_html,
'{col_3}') !==
false || strpos($template_html,
'{cms:col3}') !==
false)
$slots[
'cols'] = 3;
553 elseif (isset(
$slots[
'content'][
'col2']) || strpos($template_html,
'{col_2}') !==
false || strpos($template_html,
'{cms:col2}') !==
false)
$slots['cols'] = 2;
554 elseif (isset(
$slots['content']['col1']) || strpos($template_html, '{col_1}
') !== false || strpos($template_html, '{cms:col1}
') !== false) $slots['cols
'] = 1;
555 else $slots['cols
'] = 1;
560 private function parse_content($html, array $slots) {
561 $blocks = $this->html_blocks($html);
562 $segments = $this->segment_content_blocks($blocks);
564 $hero_blocks = $segments['hero
'];
565 $header_blocks = $segments['header
'];
566 $footer_blocks = $segments['footer
'];
567 $body_blocks = $segments['content
'];
569 $has_hero_slot = !empty($slots['content
']['hero
']) || !empty($slots['cms
']['hero
']);
570 $has_header_slot = !empty($slots['content
']['header
']) || !empty($slots['content
']['teaser
'])
571 || !empty($slots['content
']['thesar
']) || !empty($slots['cms
']['header
']);
572 $has_footer_slot = !empty($slots['content
']['footer
']) || !empty($slots['cms
']['footer
']);
574 if (!$has_hero_slot) {
575 $body_blocks = array_merge($hero_blocks, $body_blocks);
576 $hero_blocks = array();
578 if (!$has_header_slot) {
579 $body_blocks = array_merge($header_blocks, $body_blocks);
580 $header_blocks = array();
582 if (!$has_footer_slot) {
583 $body_blocks = array_merge($body_blocks, $footer_blocks);
584 $footer_blocks = array();
587 $cols = max(1, min(3, (int)($slots['cols
'] ?? 1)));
588 $col_blocks = $this->split_content_for_columns($body_blocks, $cols);
589 $all_content = $this->strip_dbx_markers($this->join_blocks($blocks));
590 $body_html = $this->strip_dbx_markers($this->join_blocks($body_blocks));
594 'hero
' => $this->strip_dbx_markers($this->join_blocks($hero_blocks)),
595 'header
' => $this->strip_dbx_markers($this->join_blocks($header_blocks)),
596 'teaser
' => $this->strip_dbx_markers($this->join_blocks($header_blocks)),
597 'footer
' => $this->strip_dbx_markers($this->join_blocks($footer_blocks)),
598 'body
' => $body_html,
599 'content
' => (isset($slots['content
']['content
']) || $cols === 1) ? $body_html : $all_content,
600 'col1
' => $cols === 1 ? $body_html : $this->strip_dbx_markers($this->join_blocks($col_blocks[0] ?? array())),
601 'col2
' => $this->strip_dbx_markers($this->join_blocks($col_blocks[1] ?? array())),
602 'col3
' => $this->strip_dbx_markers($this->join_blocks($col_blocks[2] ?? array())),
606 private function html_blocks($html) {
607 $html = trim((string)$html);
608 if ($html === '') return array();
610 $pattern = '/(<!--\s*
dbx:[^>]*?-->|<hr\b[^>]*>|<(h1|h2|h3|h4|h5|h6|p|ul|ol|blockquote|figure|table|div)\b[^>]*>.*?<\/\2>|<img\b[^>]*>)/is
';
611 preg_match_all($pattern, $html, $matches, PREG_OFFSET_CAPTURE);
615 foreach ($matches[0] as $idx => $match) {
616 $before = trim(substr($html, $pos, $match[1] - $pos));
617 if ($before !== '') $blocks[] = array('type
' => 'html', 'html' => $before);
620 $type = strtolower((string)($matches[2][$idx][0] ?? 'html'));
621 if (preg_match('/^<!--\s*
dbx:/i
', $chunk)) $type = 'marker
';
622 elseif (preg_match('/^<hr/i
', $chunk)) $type = $this->marker_name_from_html($chunk) !== '' ? 'marker
' : 'break';
623 elseif (preg_match('/^<img/i
', $chunk)) $type = 'img
';
625 $blocks[] = array('type
' => $type, 'html' => $chunk);
626 $pos = $match[1] + strlen($chunk);
629 $rest = trim(substr($html, $pos));
630 if ($rest !== '') $blocks[] = array('type
' => 'html', 'html' => $rest);
634 private function segment_content_blocks(array $blocks) {
638 $footer_hr_idx = null;
640 foreach ($blocks as $idx => $block) {
641 if ($hero_idx === null && $this->is_dbx_marker($block, array('hero
'))) {
644 if ($header_idx === null && $this->is_dbx_marker($block, array('header
', 'teaser
', 'thesar
'))) {
647 if ($this->is_dbx_marker($block, array('footer
'))) {
648 if ($footer_idx === null) $footer_idx = $idx;
649 if ($footer_hr_idx === null && $this->is_hr_marker($block)) $footer_hr_idx = $idx;
652 if ($footer_hr_idx !== null) $footer_idx = $footer_hr_idx;
657 'content
' => array(),
661 $has_hero_marker = $hero_idx !== null;
664 if ($hero_idx !== null) $markers[] = array('type
' => 'hero
', 'idx
' => $hero_idx);
665 if ($header_idx !== null) $markers[] = array('type
' => 'header
', 'idx
' => $header_idx);
666 if ($footer_idx !== null) $markers[] = array('type
' => 'footer
', 'idx
' => $footer_idx);
667 usort($markers, function($a, $b) {
668 return (int)$a['idx
'] <=> (int)$b['idx
'];
672 foreach ($markers as $marker) {
673 $idx = (int)$marker['idx
'];
674 $gap = $idx > $pos ? array_slice($blocks, $pos, $idx - $pos) : array();
675 if ($marker['type
'] === 'hero
') {
676 $result['hero
'] = $gap;
680 if ($marker['type
'] === 'header
') {
681 if ($has_hero_marker && $hero_idx < $idx) {
682 $result['header
'] = $gap;
684 $result['content
'] = array_merge($result['content
'], $gap);
689 if ($marker['type
'] === 'footer
') {
690 $result['content
'] = array_merge($result['content
'], $gap);
691 $result['footer
'] = array_slice($blocks, $idx + 1);
697 $result['content
'] = array_merge($result['content
'], array_slice($blocks, $pos));
702 private function split_content_for_columns(array $blocks, $cols) {
703 $cols = max(1, min(3, (int)$cols));
704 $result = array_fill(0, $cols, array());
706 $result[0] = $blocks;
710 $marker_split = $this->split_by_column_markers($blocks, $cols);
711 if ($marker_split !== null) return $marker_split;
713 return $this->split_by_text_weight($blocks, $cols);
716 private function split_by_column_markers(array $blocks, $cols) {
717 $result = array_fill(0, $cols, array());
720 foreach ($blocks as $block) {
721 if ($cols === 2 && $this->is_dbx_marker($block, array('col2
', 'split
'))) {
726 if ($cols === 3 && $this->is_dbx_marker($block, array('col2
', 'col3a
'))) {
731 if ($cols === 3 && $this->is_dbx_marker($block, array('col3
', 'col3b
'))) {
736 $result[$current][] = $block;
738 return $has ? $result : null;
741 private function split_by_text_weight(array $blocks, $cols) {
742 $units = $this->column_units_from_blocks($blocks, $cols);
743 $result = array_fill(0, $cols, array());
744 if (empty($units)) return $result;
746 $parts = $this->partition_column_units($units, $cols);
747 $parts = $this->keep_leading_blanks_with_previous_column($parts);
748 foreach ($parts as $idx => $part) {
749 if ($idx >= $cols) break;
750 $result[$idx] = $this->column_units_to_blocks($part);
755 private function column_units_from_blocks(array $blocks, $cols) {
758 $paragraph_index = 0;
760 foreach ($blocks as $block) {
761 $type = strtolower((string)($block['type
'] ?? 'html'));
762 if ($type === 'ul
' || $type === 'ol
') {
763 $list_units = $this->list_units_from_block($block, 'list-
' . $list_index, $cols);
765 if ($list_units !== null) {
766 $units = array_merge($units, $list_units);
771 $paragraph_units = $this->paragraph_units_from_block($block, 'p-
' . $paragraph_index, $cols);
773 if ($paragraph_units !== null) {
774 $units = array_merge($units, $paragraph_units);
782 'weight
' => $this->estimate_block_weight($block, $cols),
789 private function paragraph_units_from_block(array $block, $paragraph_key, $cols) {
790 $html = trim((string)($block['html'] ?? ''));
791 if ($html === '' || $this->is_blank_flow_html($html)) return null;
792 if (preg_match('/<(img|video|table|ul|ol|figure|blockquote|div|section|article|aside|header|footer|h[1-6]|pre)\b/i
', $html)) return null;
793 if (!preg_match('/^<\s*p\b([^>]*)>([\s\S]*)<\/\s*p\s*>$/i
', $html, $m)) return null;
795 $attrs = (string)$m[1];
796 $chunks = $this->inline_safe_chunks((string)$m[2], $cols);
797 if (count($chunks) < 5) return null;
801 foreach ($chunks as $chunk) {
802 $weight = $this->estimate_inline_chunk_weight($chunk, $cols);
803 $weights[] = $weight;
807 $threshold = max(260, ((int)$cols === 3 ? 240 : 320));
808 if ($total <= $threshold) return null;
811 foreach ($chunks as $idx => $chunk) {
812 if ($chunk === '') continue;
814 'kind
' => 'paragraph_piece
',
815 'paragraph_key
' => $paragraph_key,
818 'weight
' => max(0, (int)($weights[$idx] ?? 0)),
822 return count($units) > 1 ? $units : null;
825 private function inline_safe_chunks($inner_html, $cols) {
826 $tokens = preg_split('/(<[^>]+>)/
', (string)$inner_html, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
827 if (!is_array($tokens) || empty($tokens)) return array();
830 $count = count($tokens);
831 $chunk_limit = $this->column_chars_per_line($cols);
832 for ($i = 0; $i < $count; $i++) {
833 $token = $tokens[$i];
834 if (preg_match('/^<[^>]+>$/
', $token)) {
835 $tag = $this->tag_name($token);
836 if ($tag !== '' && $this->is_opening_tag($token) && !$this->is_void_tag($tag) && !preg_match('/\/\s*>$/
', $token)) {
839 while ($depth > 0 && $i + 1 < $count) {
843 if (!preg_match('/^<[^>]+>$/
', $next)) continue;
844 $next_tag = $this->tag_name($next);
845 if ($next_tag !== $tag) continue;
846 if ($this->is_closing_tag($next)) {
848 } elseif ($this->is_opening_tag($next) && !preg_match('/\/\s*>$/
', $next)) {
859 if (preg_match_all('/\S+\s*/u
', $token, $words)) {
861 foreach ($words[0] as $word) {
862 $candidate = $buffer . $word;
863 if ($buffer !== '' && $this->text_length(strip_tags($candidate)) > $chunk_limit) {
867 $buffer = $candidate;
870 if ($buffer !== '') $chunks[] = $buffer;
877 private function tag_name($tag) {
878 return preg_match('/^<\s*\/?\s*([a-z0-9]+)/i
', (string)$tag, $m) ? strtolower($m[1]) : '';
881 private function is_opening_tag($tag) {
882 return preg_match('/^<\s*[a-z0-9]/i
', (string)$tag) === 1;
885 private function is_closing_tag($tag) {
886 return preg_match('/^<\s*\
889 private function is_void_tag($tag) {
890 return in_array(strtolower((
string)$tag), array(
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'link',
'meta',
'param',
'source',
'track',
'wbr'),
true);
893 private function estimate_inline_chunk_weight(
$html, $cols) {
895 $text = trim(preg_replace(
'/\s+/',
' ', html_entity_decode(strip_tags(
$html), ENT_QUOTES,
'UTF-8')));
896 $chars = $this->text_length($text);
897 $weight = (int)ceil(($chars / max(1, $this->column_chars_per_line($cols))) * 100);
898 $weight += substr_count(strtolower(
$html),
'<br') * 100;
899 return max(1, $weight);
902 private function list_units_from_block(array $block, $list_key, $cols) {
903 $html = trim((
string)($block[
'html'] ??
''));
904 if (!preg_match(
'/^<\s*(ul|ol)\b([^>]*)>([\s\S]*)<\/\s*\1\s*>$/i',
$html, $m))
return null;
906 $tag = strtolower($m[1]);
907 $attrs = (string)$m[2];
908 $items = $this->extract_top_level_list_items((
string)$m[3]);
909 if (count(
$items) < 2)
return null;
912 foreach (
$items as $item_html) {
914 'kind' =>
'list_item',
915 'list_key' => $list_key,
918 'html' => $item_html,
919 'weight' => $this->estimate_list_item_weight($item_html, $cols),
925 private function extract_top_level_list_items($inner_html) {
926 $inner_html = (string)$inner_html;
927 if (!preg_match_all(
'/<li\b[^>]*>|<\/li\s*>/i', $inner_html, $matches, PREG_OFFSET_CAPTURE)) {
935 foreach ($matches[0] as $match) {
938 if (preg_match(
'/^<li\b/i', $tag)) {
939 if ($depth === 0) $start = $offset;
944 if ($depth === 0)
continue;
946 if ($depth === 0 && $start !==
null) {
947 $items[] = substr($inner_html, $start, $offset + strlen($tag) - $start);
952 return $depth === 0 ?
$items : array();
955 private function estimate_list_item_weight(
$html, $cols) {
956 $nested_items = max(0, substr_count(strtolower((
string)
$html),
'<li') - 1);
957 return max(100, $this->estimate_text_weight((
string)
$html, $cols, 1) + ($nested_items * 120));
960 private function partition_column_units(array $units, $cols) {
961 $cols = max(1, min(3, (
int)$cols));
963 if ($cols === 1 || $n === 0)
return array($units);
966 $parts = array_fill(0, $cols, array());
967 foreach ($units as $idx => $unit) $parts[$idx][] = $unit;
972 foreach ($units as $unit) {
973 $prefix[] = end($prefix) + max(0, (
int)($unit[
'weight'] ?? 0));
976 $total = $prefix[$n];
977 $target = $total > 0 ? $total / $cols : 1;
982 for ($c = 0; $c <= $cols; $c++) {
983 $dp[$c] = array_fill(0, $n + 1, $inf);
984 $prev[$c] = array_fill(0, $n + 1, -1);
987 for ($i = 1; $i <= $n; $i++) {
988 $dp[1][$i] = $this->column_partition_cost($prefix, 0, $i,
$target);
992 for ($c = 2; $c <= $cols; $c++) {
993 for ($i = $c; $i <= $n; $i++) {
994 for ($j = $c - 1; $j < $i; $j++) {
995 if ($dp[$c - 1][$j] === $inf)
continue;
996 $cost = $dp[$c - 1][$j] + $this->column_partition_cost($prefix, $j, $i,
$target);
997 if ($cost < $dp[$c][$i]) {
1005 $parts = array_fill(0, $cols, array());
1007 for ($c = $cols; $c >= 1; $c--) {
1009 if ($j < 0) $j = max(0, $i - 1);
1010 $parts[$c - 1] = array_slice($units, $j, $i - $j);
1017 private function keep_leading_blanks_with_previous_column(array $parts) {
1018 for ($i = 1; $i < count($parts); $i++) {
1019 while (!empty($parts[$i]) && $this->is_blank_column_unit($parts[$i][0])) {
1020 $parts[$i - 1][] = array_shift($parts[$i]);
1026 private function is_blank_column_unit(array $unit) {
1027 if (($unit[
'kind'] ??
'') !==
'block' || !isset($unit[
'block']) || !is_array($unit[
'block']))
return false;
1028 $block = $unit[
'block'];
1029 return strtolower((
string)($block[
'type'] ??
'')) ===
'p' && $this->is_blank_flow_html((
string)($block[
'html'] ??
''));
1032 private function column_partition_cost(array $prefix, $start, $end,
$target) {
1033 $sum = $prefix[$end] - $prefix[$start];
1035 return $diff * $diff;
1038 private function column_units_to_blocks(array $units) {
1042 foreach ($units as $unit) {
1043 if (($unit[
'kind'] ??
'') ===
'list_item') {
1044 $key = (string)($unit[
'list_key'] ??
'');
1045 if ($pending ===
null || ($pending[
'kind'] ??
'') !==
'list' || $pending[
'list_key'] !== $key) {
1046 $this->flush_pending_column_group($pending, $blocks);
1050 'tag' => (
string)($unit[
'tag'] ??
'ul'),
1051 'attrs' => (
string)($unit[
'attrs'] ??
''),
1055 $pending[
'items'][] = (string)($unit[
'html'] ??
'');
1058 if (($unit[
'kind'] ??
'') ===
'paragraph_piece') {
1059 $key = (string)($unit[
'paragraph_key'] ??
'');
1060 if ($pending ===
null || ($pending[
'kind'] ??
'') !==
'paragraph' || $pending[
'paragraph_key'] !== $key) {
1061 $this->flush_pending_column_group($pending, $blocks);
1063 'kind' =>
'paragraph',
1064 'paragraph_key' => $key,
1065 'attrs' => (
string)($unit[
'attrs'] ??
''),
1066 'pieces' => array(),
1069 $pending[
'pieces'][] = (string)($unit[
'html'] ??
'');
1072 $this->flush_pending_column_group($pending, $blocks);
1073 if (isset($unit[
'block']) && is_array($unit[
'block'])) $blocks[] = $unit[
'block'];
1076 $this->flush_pending_column_group($pending, $blocks);
1080 private function flush_pending_column_group(&$pending, array &$blocks) {
1081 if ($pending ===
null)
return;
1082 $kind = (string)($pending[
'kind'] ??
'');
1083 $tag = (string)($pending[
'tag'] ??
'');
1084 if ($kind ===
'list') {
1085 $tag = preg_match(
'/^(ul|ol)$/i', $tag) ? strtolower($tag) :
'ul';
1086 $html =
'<' . $tag . (string)$pending[
'attrs'] .
'>' . implode(
'', $pending[
'items']) .
'</' . $tag .
'>';
1087 } elseif ($kind ===
'paragraph') {
1089 $html =
'<p' . (string)$pending[
'attrs'] .
'>' . implode(
'', $pending[
'pieces']) .
'</p>';
1101 private function estimate_block_weight(array $block, $cols) {
1102 $html = (string)($block[
'html'] ??
'');
1103 $type = strtolower((
string)($block[
'type'] ??
'html'));
1105 if ($type ===
'marker')
return 0;
1106 if ($type ===
'img')
return $this->estimate_media_weight(
$html, $cols);
1107 if ($type ===
'figure')
return $this->estimate_media_weight(
$html, $cols) + min(300, $this->estimate_text_weight(
$html, $cols, 0));
1108 if ($type ===
'table')
return 500 + substr_count(strtolower(
$html),
'<tr') * 100;
1109 if ($type ===
'ul' || $type ===
'ol')
return max(200, substr_count(strtolower(
$html),
'<li') * 100 + $this->estimate_text_weight(
$html, $cols, 0));
1110 if ($type ===
'p' && preg_match(
'/<(img|video)\b/i',
$html))
return $this->estimate_media_weight(
$html, $cols) + min(300, $this->estimate_text_weight(
$html, $cols, 0));
1111 if ($type ===
'p' && $this->is_blank_flow_html(
$html))
return 0;
1112 if (in_array($type, array(
'h1',
'h2',
'h3',
'h4',
'h5',
'h6'),
true))
return max(130, $this->estimate_text_weight(
$html, $cols, 1) + 60);
1113 if ($type ===
'blockquote')
return max(180, $this->estimate_text_weight(
$html, $cols, 1) + 80);
1115 return max(60, $this->estimate_text_weight(
$html, $cols, 1));
1118 private function estimate_text_weight(
$html, $cols, $min_lines = 0) {
1120 $text = trim(preg_replace(
'/\s+/',
' ', html_entity_decode(strip_tags(
$html), ENT_QUOTES,
'UTF-8')));
1121 $chars = $this->text_length($text);
1122 $lines = $chars > 0 ? ($chars / max(1, $this->column_chars_per_line($cols))) : 0;
1123 $lines += substr_count(strtolower(
$html),
'<br');
1124 $lines = max((
float)$min_lines, $lines);
1125 return (
int)ceil($lines * 100);
1128 private function estimate_media_weight(
$html, $cols) {
1129 if (!preg_match_all(
'/<(img|video)\b[^>]*>/i', (
string)
$html, $matches)) {
1130 return $this->default_media_lines($cols) * 100;
1134 foreach ($matches[0] as $tag) {
1135 $weight += $this->estimate_media_lines_from_tag($tag, $cols) * 100;
1137 return max($this->default_media_lines($cols) * 100, $weight);
1140 private function estimate_media_lines_from_tag($tag, $cols) {
1141 $default = $this->default_media_lines($cols);
1144 if (preg_match(
'/\bwidth=["\']?([0-9.]+)/i', (
string)$tag, $m)) $width = (float)$m[1];
1145 if (preg_match(
'/\bheight=["\']?([0-9.]+)/i', (
string)$tag, $m)) $height = (float)$m[1];
1146 if ($width <= 0 || $height <= 0)
return $default;
1148 $ratio = $height / $width;
1149 $base = ((int)$cols === 3) ? 14 : 19;
1150 return max(8, min(24, (
int)round($ratio *
$base)));
1153 private function default_media_lines($cols) {
1154 return ((
int)$cols === 3) ? 10 : 14;
1157 private function column_chars_per_line($cols) {
1158 if ((
int)$cols === 3)
return 42;
1159 if ((
int)$cols === 2)
return 58;
1163 private function text_length($text) {
1164 $text = (string)$text;
1165 return function_exists(
'mb_strlen') ? mb_strlen($text,
'UTF-8') : strlen($text);
1168 private function is_blank_flow_html(
$html) {
1170 if (preg_match(
'/<(img|video|picture|source|iframe|embed|object|svg|canvas)\b/i',
$html))
return false;
1171 $html = preg_replace(
'/<\/?p\b[^>]*>/i',
'',
$html);
1172 $html = preg_replace(
'/<br\s*\/?>/i',
'',
$html);
1174 return trim(html_entity_decode(strip_tags(
$html), ENT_QUOTES,
'UTF-8')) ===
'';
1177 private function join_blocks(array $blocks) {
1179 foreach ($blocks as $block) {
1180 if ($this->is_dbx_marker($block, array(
'pagebreak'))) {
1181 $html .=
'<div class="dbx-content-pagebreak" aria-hidden="true"></div>' .
"\n";
1184 if (($block[
'type'] ??
'') ===
'marker')
continue;
1185 $html .= ($block[
'html'] ??
'') .
"\n";
1190 private function is_dbx_marker(array $block, array $names) {
1191 if (($block[
'type'] ??
'') !==
'marker')
return false;
1192 $name = $this->marker_name_from_html((
string)($block[
'html'] ??
''));
1193 return $name !==
'' && in_array($name, $names,
true);
1196 private function is_hr_marker(array $block) {
1197 if (($block[
'type'] ??
'') !==
'marker')
return false;
1198 return preg_match(
'/^\s*<hr\b/i', (
string)($block[
'html'] ??
'')) === 1;
1201 private function marker_name_from_html(
$html) {
1203 if (preg_match(
'/<!--\s*dbx:([a-z0-9_-]+)/i',
$html, $m))
return $this->canonical_marker_name($m[1]);
1204 if (preg_match(
'/\bdata-dbx-marker=["\'](?:dbx:)?([a-z0-9_-]+)["\']/i',
$html, $m))
return $this->canonical_marker_name($m[1]);
1205 if (preg_match(
'/\bclass=["\'][^"\']*\bdbx-cms-marker-([a-z0-9_-]+)\b/i',
$html, $m))
return $this->canonical_marker_name($m[1]);
1206 if (preg_match(
'/\bclass=["\'][^"\']*\bdbx_marker_([a-z0-9_-]+)\b/i',
$html, $m))
return $this->canonical_marker_name($m[1]);
1207 if (preg_match(
'/\bclass=["\'][^"\']*\bdbx_split\b/i',
$html))
return 'col2';
1211 private function canonical_marker_name($name) {
1212 $name = strtolower(trim((
string)$name));
1213 if (in_array($name, array(
'hero_text',
'hero-text',
'herotext'),
true))
return 'hero';
1217 private function convert_mod_placeholders(
$html) {
1219 if (
$html ===
'' || stripos(
$html,
'dbx-cms-mod-placeholder') ===
false)
return $html;
1221 return preg_replace_callback(
1222 '/<(p|figure|div)\b([^>]*\bdbx-cms-mod-placeholder\b[^>]*)>([\s\S]*?)<\/\1>/i',
1224 $attrs = (string)($m[2] ??
'');
1225 $inner = (string)($m[3] ??
'');
1231 if (preg_match(
'/\bdata-cms-mod-modul=["\']([^"\']*)["\']/i', $attrs, $mm)) {
1232 $modul = trim((
string)$mm[1]);
1234 if (preg_match(
'/\bdata-cms-mod-params=["\']([^"\']*)["\']/i', $attrs, $mm)) {
1235 $params = trim((
string)$mm[1]);
1237 if ($modul ===
'' && preg_match(
'/\bdata-cms-mod-modul=["\']([^"\']*)["\']/i', $inner, $mm)) {
1238 $modul = trim((
string)$mm[1]);
1240 if ($params ===
'' && preg_match(
'/\bdata-cms-mod-params=["\']([^"\']*)["\']/i', $inner, $mm)) {
1241 $params = trim((
string)$mm[1]);
1243 if ($modul ===
'' && preg_match(
'/<img\b[^>]*\bdata-cms-mod-modul=["\']([^"\']*)["\']/i', $inner, $mm)) {
1244 $modul = trim((
string)$mm[1]);
1246 if ($params ===
'' && preg_match(
'/<img\b[^>]*\bdata-cms-mod-params=["\']([^"\']*)["\']/i', $inner, $mm)) {
1247 $params = trim((
string)$mm[1]);
1249 if (preg_match(
'/<img\b[^>]*\bdata-dbx=["\']([^"\']*)["\']/i', $inner, $mm)) {
1250 $data_dbx = trim((
string)$mm[1]);
1252 if (preg_match(
'/<img\b[^>]*\balt=["\']([^"\']*)["\']/i', $inner, $mm)) {
1253 $alt = trim((
string)$mm[1]);
1256 if (($modul ===
'' || $params ===
'') && preg_match(
'/modul_image[^"\']*file=([^"\'&]+)/i', $inner . $attrs, $fileMatch)) {
1257 $base = dirname(__DIR__, 3) .
'/dbxAdmin/include/dbxModuleImages.class.php';
1258 if (is_file(
$base)) {
1260 $resolved = (new \dbx\dbxAdmin\dbxModuleImages())->resolveFromFilename(rawurldecode((
string)$fileMatch[1]));
1262 if ($modul ===
'') {
1263 $modul = (string)(
$resolved[
'default_modul'] ??
'');
1265 if ($params ===
'') {
1266 $params = (string)(
$resolved[
'default_params'] ??
'');
1272 $modul = preg_replace(
'/[^A-Za-z0-9_]+/',
'', $modul);
1273 if ($modul ===
'')
return $m[0];
1275 $params = str_replace(array(
"\r",
"\n"),
'', $params);
1276 $marker =
'[modul=' . $modul .
']' . $params .
'[/modul]';
1277 $data_dbx = htmlspecialchars($data_dbx, ENT_QUOTES,
'UTF-8');
1278 $alt_attr = $alt !==
'' ?
' title="' . htmlspecialchars($alt, ENT_QUOTES,
'UTF-8') .
'"' :
'';
1280 if ($data_dbx !==
'') {
1281 return '<div class="dbx-cms-mod-output" data-dbx="' . $data_dbx .
'"' . $alt_attr .
'>' . $marker .
'</div>';
1289 private function strip_dbx_markers(
$html) {
1290 $html = preg_replace(
'/<!--\s*dbx:[\s\S]*?-->/i',
'', (
string)
$html);
1291 $html = preg_replace(
'/<hr\b[^>]*(?:data-dbx-marker|dbx-cms-marker|dbx_marker_|dbx_split)[^>]*>/i',
'',
$html);
1295 private function render_inline_media_placeholders(
$db, array
$parsed) {
1297 if (!is_string(
$html))
continue;
1298 if (stripos(
$html,
'dbx-cms-inline-video-block') !==
false) {
1299 $html = $this->render_inline_video_placeholders(
$db,
$html);
1301 if (stripos(
$html,
'dbx_mid=') !==
false || stripos(
$html,
'data-cms-media-id') !==
false) {
1309 private function render_inline_image_sanitize(
$db,
$html) {
1310 return preg_replace_callback(
'/<(img)\b([^>]*?)>/i',
function($m) use (
$db) {
1311 $attrs = (string)($m[2] ??
'');
1313 if (preg_match(
'/data-cms-media-id=["\']?([0-9]+)/i', $attrs, $id_match)) {
1314 $id = (int)$id_match[1];
1315 } elseif (preg_match(
'/dbx_mid=([0-9]+)/i', $attrs, $id_match)) {
1316 $id = (int)$id_match[1];
1318 if ($id <= 0)
return $m[0];
1319 $row =
$db->select1($this->dd_media, $id);
1320 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || !$this->media_file_exists($row))
return '';
1321 $tag = (string)$m[0];
1322 $width = max(0, (
int)($row[
'width'] ?? 0));
1323 $height = max(0, (
int)($row[
'height'] ?? 0));
1324 if ($width > 0 && $height > 0) {
1325 $tag = self::withHtmlImageAttr($tag,
'width', (
string)$width);
1326 $tag = self::withHtmlImageAttr($tag,
'height', (
string)$height);
1332 private function render_inline_video_placeholders(
$db,
$html) {
1333 return preg_replace_callback(
'/<figure\b([^>]*\bdbx-cms-inline-video-block\b[^>]*)>[\s\S]*?<\/figure>/i',
function($m) use (
$db) {
1335 if (!preg_match(
'/data-cms-media-id=["\']?([0-9]+)/i', $attrs, $id_match))
return $m[0];
1336 $id = (int)$id_match[1];
1337 $row =
$db->select1($this->dd_media, $id);
1338 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || !$this->media_file_exists($row))
return '';
1339 return $this->render_inline_video_player($row, $attrs, $m[0]);
1343 private function inline_media_type(array $row) {
1344 if ((
string)($row[
'media_type'] ??
'') ===
'external_video')
return 'external_video';
1345 $mime = (string)($row[
'mime'] ??
'');
1346 if (strpos($mime,
'video/') === 0 || preg_match(
'/\.(mp4|webm|ogv|ogg|mov|m4v)$/i', (
string)($row[
'file_name'] ?? $row[
'file_path'] ??
'')))
return 'video';
1347 if (strtolower((
string)($row[
'storage_type'] ??
'')) ===
'external')
return 'external_video';
1351 private function render_inline_video_player(array $row, $source_attrs =
'', $source_html =
'') {
1352 $id = (int)($row[
'id'] ?? 0);
1353 $type = $this->inline_media_type($row);
1354 if ($id <= 0 || !in_array($type, array(
'video',
'external_video'),
true))
return '';
1356 $source_attrs = (string)$source_attrs;
1357 $source_html = (string)$source_html;
1358 $video_options = $this->inline_video_options_from_html($source_attrs, $source_html);
1359 $style = $video_options[
'style'] !==
'' ?
' style="' . htmlspecialchars($video_options[
'style'], ENT_QUOTES,
'UTF-8') .
'"' :
'';
1360 $autoplay = !empty($video_options[
'autoplay']);
1361 $loop = !empty($video_options[
'loop']);
1362 $muted = !empty($video_options[
'muted']);
1363 $align = (string)($video_options[
'align'] ??
'left');
1368 $title = htmlspecialchars((
string)($row[
'title'] ?? $row[
'alt'] ?? $row[
'file_name'] ??
'Video'), ENT_QUOTES,
'UTF-8');
1369 $caption = trim((
string)($row[
'caption'] ??
''));
1370 $caption_html = $caption !==
'' ?
'<figcaption>' . htmlspecialchars($caption, ENT_QUOTES,
'UTF-8') .
'</figcaption>' :
'';
1371 $figure_attr =
' class="dbx-content-inline-media dbx-content-inline-video" data-media-id="' . $id .
'" data-video-align="' . htmlspecialchars($align, ENT_QUOTES,
'UTF-8') .
'"' .
$style;
1373 if ($type ===
'external_video') {
1374 $url = (string)($row[
'embed_url'] ?? $row[
'external_url'] ??
'');
1375 if (
$url ===
'')
return '';
1376 $url = $this->external_video_option_url(
$url, $autoplay, $muted, $loop);
1377 $loading = $autoplay ?
'eager' :
'lazy';
1378 if ($this->is_youtube_media($row,
$url)) {
1379 return '<figure' . $figure_attr .
'>' . $this->render_youtube_consent_block(
$url, (
string)($row[
'title'] ?? $row[
'alt'] ?? $row[
'file_name'] ??
'Video'), $loading) . $caption_html .
'</figure>';
1381 return '<figure' . $figure_attr .
'><iframe class="dbx-content-video-player" src="' . htmlspecialchars(
$url, ENT_QUOTES,
'UTF-8') .
'" title="' . $title .
'" loading="' . $loading .
'" allowfullscreen></iframe>' . $caption_html .
'</figure>';
1384 $url =
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $id;
1385 $poster = !empty($row[
'thumb_file_path']) ?
$url .
'&dbx_thumb=1' :
'';
1386 $poster_attr = $poster !==
'' ?
' poster="' . htmlspecialchars($poster, ENT_QUOTES,
'UTF-8') .
'"' :
'';
1387 $mime_attr = trim((
string)($row[
'mime'] ??
'')) !==
'' ?
' type="' . htmlspecialchars((
string)$row[
'mime'], ENT_QUOTES,
'UTF-8') .
'"' :
'';
1388 $autoplay_attr = $autoplay ?
' autoplay' :
'';
1389 $loop_attr = $loop ?
' loop' :
'';
1390 $muted_attr = $muted ?
' muted' :
'';
1391 return '<figure' . $figure_attr .
'><video class="dbx-content-video-player" controls preload="metadata" playsinline' . $autoplay_attr . $loop_attr . $muted_attr . $poster_attr .
'><source src="' . htmlspecialchars(
$url, ENT_QUOTES,
'UTF-8') .
'"' . $mime_attr .
'></video>' . $caption_html .
'</figure>';
1394 private function inline_video_options_from_html($source_attrs, $source_html) {
1396 if (preg_match(
'/\sstyle=(["\'])(.*?)\1/i', (
string)$source_attrs, $m)) {
1397 $style = $this->safe_inline_video_style($m[2]);
1400 $width = $this->attr_value_from_html($source_attrs,
'data-cms-video-width');
1401 $height = $this->attr_value_from_html($source_attrs,
'data-cms-video-height');
1402 $align_raw = $this->attr_value_from_html($source_attrs,
'data-cms-video-align');
1403 if ($width ===
'') $width = $this->attr_value_from_html($source_attrs,
'width');
1404 if ($height ===
'') $height = $this->attr_value_from_html($source_attrs,
'height');
1406 if ((
string)$source_html !==
'' && preg_match(
'/<(?:img|video|iframe)\b([^>]*)>/i', (
string)$source_html, $media_match)) {
1407 $media_attrs = $media_match[1];
1408 if ($width ===
'') $width = $this->attr_value_from_html($media_attrs,
'data-cms-video-width');
1409 if ($height ===
'') $height = $this->attr_value_from_html($media_attrs,
'data-cms-video-height');
1410 if ($width ===
'') $width = $this->attr_value_from_html($media_attrs,
'width');
1411 if ($height ===
'') $height = $this->attr_value_from_html($media_attrs,
'height');
1412 if ($align_raw ===
'') $align_raw = $this->attr_value_from_html($media_attrs,
'data-cms-video-align');
1413 if (
$style ===
'' && preg_match(
'/\sstyle=(["\'])(.*?)\1/i', $media_attrs, $style_match)) {
1414 $style = $this->safe_inline_video_style($style_match[2]);
1418 $width = $this->css_size_value($width);
1419 $height = $this->css_size_value($height);
1420 $style_parts = $this->style_to_map(
$style);
1421 if ($width !==
'') $style_parts[
'width'] = $width;
1422 if ($height !==
'') $style_parts[
'height'] = $height;
1423 $align = $this->inline_video_align_value($align_raw, $style_parts);
1424 if ($align ===
'center') {
1425 unset($style_parts[
'float']);
1426 $style_parts[
'margin-left'] =
'auto';
1427 $style_parts[
'margin-right'] =
'auto';
1428 } elseif ($align ===
'right') {
1429 unset($style_parts[
'float']);
1430 $style_parts[
'margin-left'] =
'auto';
1431 $style_parts[
'margin-right'] =
'0px';
1432 } elseif ($align_raw !==
'') {
1433 unset($style_parts[
'float'], $style_parts[
'margin-left'], $style_parts[
'margin-right']);
1436 $autoplay_raw = $this->attr_value_from_html($source_attrs,
'data-cms-video-autoplay');
1437 if ($autoplay_raw ===
'' && (
string)$source_html !==
'' && preg_match(
'/<(?:img|video|iframe)\b([^>]*)>/i', (
string)$source_html, $media_match)) {
1438 $autoplay_raw = $this->attr_value_from_html($media_match[1],
'data-cms-video-autoplay');
1440 $muted_raw = $this->attr_value_from_html($source_attrs,
'data-cms-video-muted');
1441 if ($muted_raw ===
'' && (
string)$source_html !==
'' && preg_match(
'/<(?:img|video|iframe)\b([^>]*)>/i', (
string)$source_html, $media_match)) {
1442 $muted_raw = $this->attr_value_from_html($media_match[1],
'data-cms-video-muted');
1445 $loop_raw = $this->attr_value_from_html($source_attrs,
'data-cms-video-loop');
1446 if ($loop_raw ===
'' && (
string)$source_html !==
'' && preg_match(
'/<(?:img|video|iframe)\b([^>]*)>/i', (
string)$source_html, $media_match)) {
1447 $loop_raw = $this->attr_value_from_html($media_match[1],
'data-cms-video-loop');
1451 'style' => $this->style_map_to_string($style_parts),
1452 'autoplay' => preg_match(
'/^(1|true|yes|ja|on)$/i', trim((
string)$autoplay_raw)) === 1,
1453 'loop' => preg_match(
'/^(1|true|yes|ja|on)$/i', trim((
string)$loop_raw)) === 1,
1454 'muted' => preg_match(
'/^(1|true|yes|ja|on)$/i', trim((
string)$muted_raw)) === 1,
1459 private function inline_video_align_value(
$value, array
$style = array()) {
1461 if (in_array(
$value, array(
'left',
'center',
'right'),
true))
return $value;
1462 $margin_left = strtolower(trim((
string)(
$style[
'margin-left'] ??
'')));
1463 $margin_right = strtolower(trim((
string)(
$style[
'margin-right'] ??
'')));
1464 if ($margin_left ===
'auto' && $margin_right ===
'auto')
return 'center';
1465 if ($margin_left ===
'auto')
return 'right';
1469 private function attr_value_from_html($attrs, $name) {
1470 $name = preg_quote((
string)$name,
'/');
1471 if (preg_match(
'/\s' . $name .
'\s*=\s*(["\'])(.*?)\1/i', (
string)$attrs, $m))
return trim((
string)$m[2]);
1472 if (preg_match(
'/\s' . $name .
'\s*=\s*([^\s>]+)/i', (
string)$attrs, $m))
return trim((
string)$m[1],
"\"' ");
1476 private function css_size_value(
$value) {
1478 if (
$value ===
'')
return '';
1479 if (preg_match(
'/^[0-9]+(?:\.[0-9]+)?$/',
$value))
return $value .
'px';
1480 if (preg_match(
'/^[0-9]+(?:\.[0-9]+)?(?:px|%|vw|vh|rem|em)$/i',
$value))
return $value;
1481 if (strtolower(
$value) ===
'auto')
return 'auto';
1485 private function safe_inline_video_style(
$style) {
1487 foreach ($this->style_to_map(
$style) as $key =>
$value) {
1488 if (!in_array($key, array(
'width',
'height',
'max-width',
'float',
'margin-left',
'margin-right',
'display'),
true))
continue;
1489 if (in_array($key, array(
'width',
'height',
'max-width',
'margin-left',
'margin-right'),
true)) {
1491 if (
$value ===
'')
continue;
1492 } elseif ($key ===
'float') {
1493 if (!in_array(strtolower(
$value), array(
'left',
'right',
'none'),
true))
continue;
1495 } elseif ($key ===
'display') {
1496 if (!in_array(strtolower(
$value), array(
'block',
'inline-block'),
true))
continue;
1501 return $this->style_map_to_string(
$out);
1504 private function style_to_map(
$style) {
1506 foreach (explode(
';', (
string)
$style) as $part) {
1507 if (strpos($part,
':') ===
false)
continue;
1508 list($key,
$value) = array_map(
'trim', explode(
':', $part, 2));
1509 $key = strtolower($key);
1510 if ($key ===
'' ||
$value ===
'')
continue;
1516 private function style_map_to_string(array
$style) {
1519 if ((
string)
$value ===
'')
continue;
1522 return implode(
'; ',
$out);
1525 private function external_video_option_url(
$url, $autoplay =
false, $muted =
false, $loop =
false) {
1527 if (
$url ===
'')
return '';
1528 $join = (strpos(
$url,
'?') ===
false) ?
'?' :
'&';
1529 $params = array(
'playsinline' =>
'1',
'rel' =>
'0');
1530 if ($autoplay) $params[
'autoplay'] =
'1';
1531 $params[
'mute'] = $muted ?
'1' :
'0';
1533 $params[
'loop'] =
'1';
1534 $video_id = $this->youtube_video_id_from_url(
$url);
1535 if ($video_id !==
'') $params[
'playlist'] = $video_id;
1537 foreach ($params as $key =>
$value) {
1538 if (preg_match(
'/(?:[?&])' . preg_quote($key,
'/') .
'=/i',
$url))
continue;
1539 $url .= $join . rawurlencode($key) .
'=' . rawurlencode(
$value);
1545 private function youtube_video_id_from_url(
$url) {
1547 if (
$url ===
'')
return '';
1548 if (preg_match(
'~(?:embed/|v=|youtu\.be/)([A-Za-z0-9_-]{11})~i',
$url, $m)) {
1549 return (
string)$m[1];
1554 private function is_youtube_media(array $row,
$url) {
1555 $provider = strtolower(trim((
string)($row[
'provider'] ??
'')));
1556 if ($provider ===
'youtube')
return true;
1558 if (
$url !==
'' && preg_match(
'~(?:youtube\.com|youtu\.be)~i',
$url))
return true;
1559 $embed = trim((
string)($row[
'embed_url'] ?? $row[
'external_url'] ??
''));
1560 if ($embed !==
'' && preg_match(
'~(?:youtube\.com|youtu\.be)~i', $embed))
return true;
1561 $provider_id = trim((
string)($row[
'provider_id'] ??
''));
1562 return $provider_id !==
'' && preg_match(
'~^[A-Za-z0-9_-]{11}$~', $provider_id) && $provider !==
'vimeo';
1565 private function render_youtube_consent_block($embed_url, $title, $loading =
'lazy') {
1566 $embed_url = trim((
string)$embed_url);
1567 if ($embed_url ===
'')
return '';
1568 $title = htmlspecialchars(trim((
string)$title), ENT_QUOTES,
'UTF-8');
1569 $loading = ($loading ===
'eager') ?
'eager' :
'lazy';
1570 $video_id = $this->youtube_video_id_from_url($embed_url);
1571 $thumb = $video_id !==
'' ?
'https://img.youtube.com/vi/' . $video_id .
'/hqdefault.jpg' :
'';
1572 $thumb_html = $thumb !==
''
1573 ?
'<img class="dbx-youtube-consent-thumb" src="' . htmlspecialchars($thumb, ENT_QUOTES,
'UTF-8') .
'" alt="" loading="lazy">'
1575 return '<div class="dbx-youtube-consent-placeholder"'
1576 .
' data-youtube-embed-url="' . htmlspecialchars($embed_url, ENT_QUOTES,
'UTF-8') .
'"'
1577 .
' data-youtube-title="' . $title .
'"'
1578 .
' data-youtube-loading="' . $loading .
'">'
1580 .
'<button type="button" class="dbx-youtube-consent-play" aria-label="Video abspielen">'
1581 .
'<i class="bi bi-play-fill"></i></button>'
1585 private function render_youtube_hero_figure(array $row,
$url, array $settings) {
1586 $url = $this->external_video_option_url(
$url,
false,
false,
false);
1587 $title = (string)($row[
'title'] ?? $row[
'alt'] ?? $row[
'file_name'] ??
'Video');
1588 $caption = trim((
string)($row[
'caption'] ??
''));
1589 $caption_html = $caption !==
'' ?
'<figcaption>' . htmlspecialchars($caption, ENT_QUOTES,
'UTF-8') .
'</figcaption>' :
'';
1590 $hero_variant = preg_replace(
'/[^a-z0-9_-]+/i',
'-', strtolower((
string)($settings[
'hero_variant'] ??
'original')));
1591 $hero_scroll = preg_replace(
'/[^a-z0-9_-]+/i',
'-', strtolower((
string)($settings[
'hero_scroll_layer'] ??
'under')));
1592 $hero_classes = array(
1595 'dbx-media-external-video',
1597 'dbx-media-hero-' . ($hero_variant !==
'' ? $hero_variant :
'original'),
1598 $this->is_enabled_value($settings[
'hero_sticky'] ??
'0') ?
'dbx-media-hero-sticky' :
'',
1599 'dbx-media-hero-scroll-' . ($hero_scroll !==
'' ? $hero_scroll :
'under'),
1601 $style = $this->css_custom_properties(array(
1602 'cms-hero-margin-top' => $this->css_length_value($settings[
'hero_margin_top'] ??
'0',
'0'),
1603 'cms-hero-height' => $this->css_length_value($settings[
'hero_height'] ??
'auto',
'auto'),
1605 return '<figure class="' . htmlspecialchars(implode(
' ', array_filter($hero_classes)), ENT_QUOTES,
'UTF-8') .
'"'
1607 .
'>' . $this->render_youtube_consent_block(
$url, $title,
'lazy') . $caption_html .
'</figure>';
1610 private function render_media_merge(
$db,
$tpl, $cid, array
$slots, array $rec, array $settings = array()) {
1613 'media:gallery' =>
'',
1614 'media:header' =>
'',
1615 'media:teaser' =>
'',
1616 'media:footer' =>
'',
1618 'media_gallery' =>
'',
1619 'media_header' =>
'',
1620 'media_teaser' =>
'',
1621 'media_footer' =>
'',
1624 $usage_rows =
$db->select(
1625 $this->dd_media_usage,
1626 dbxContentMediaUsageScope::withLanguage(
'content_id = ' . (
int)$cid .
' AND active = 1'),
1636 if (is_array($usage_rows) && !empty($usage_rows)) {
1637 $mediaIds = array_values(array_filter(array_unique(array_map(
1638 static fn($usage) => (
int)($usage[
'media_id'] ?? 0),
1641 $mediaById = array();
1643 $mediaRows =
$db->select(
1645 'id IN (' . implode(
',', $mediaIds) .
') AND active = 1',
1654 foreach ((is_array($mediaRows) ? $mediaRows : array()) as $mediaRow) {
1655 $mediaById[(int)($mediaRow[
'id'] ?? 0)] = $mediaRow;
1658 foreach ($usage_rows as $usage) {
1659 if (!is_array($usage))
continue;
1660 $media_id = (int)($usage[
'media_id'] ?? 0);
1661 if ($media_id <= 0)
continue;
1662 $row = $mediaById[$media_id] ?? array();
1663 if (!$row)
continue;
1664 $row[
'slot'] = (string)($usage[
'slot'] ??
'gallery');
1665 $row[
'sorter'] = (string)($usage[
'sorter'] ??
'');
1666 if (!empty($usage[
'template'])) $row[
'template'] = $usage[
'template'];
1667 if (!empty($usage[
'caption'])) $row[
'caption'] = $usage[
'caption'];
1668 $row[
'usage_id'] = (int)($usage[
'id'] ?? 0);
1672 $rows =
$db->select($this->dd_media,
'content_id = ' . (
int)$cid .
' AND active = 1',
'*',
'slot,sorter,title,id',
'ASC',
'', 0, 0, 0);
1677 foreach (
$rows as $row) {
1678 if (!is_array($row))
continue;
1679 if (!$this->media_file_exists($row))
continue;
1680 $slot = trim((
string)($row[
'slot'] ??
'gallery')) ?:
'gallery';
1681 if (!isset($by_slot[$slot])) $by_slot[$slot] = array();
1682 $by_slot[$slot][] = $row;
1685 if (!$settings) $settings = $this->content_settings(
$db, $rec);
1686 $needed = array_keys(
$slots[
'media'] ?? array());
1687 foreach (array(
'hero',
'gallery',
'header',
'teaser',
'footer') as $slot) {
1688 if (!in_array($slot, $needed,
true))
continue;
1689 $rows_for_slot = $by_slot[$slot] ?? array();
1690 if ($slot ===
'hero') {
1691 if ($this->is_no_hero_value($settings[
'hero_template'] ??
'') || $this->is_no_hero_value($settings[
'hero_image_id'] ??
''))
continue;
1692 $rows_for_slot = $this->one_hero_row(
$db, $rows_for_slot, $settings[
'hero_image_id']);
1694 if (empty($rows_for_slot))
continue;
1695 $html = $this->render_media_slot(
$tpl, $rows_for_slot, $slot, $settings);
1702 private function one_hero_row(
$db, array
$rows, $hero_id) {
1703 $hero_id = (int)$hero_id;
1705 $row =
$db->select1($this->dd_media, $hero_id);
1706 if (is_array($row) && (
int)($row[
'active'] ?? 0) === 1 && $this->media_file_exists($row))
return array($row);
1708 return isset(
$rows[0]) ? array(
$rows[0]) : array();
1711 private function render_media_slot(
$tpl, array
$rows, $slot, array $settings) {
1712 if (!
$rows)
return '';
1714 foreach (
$rows as $row) {
1715 $mime = (string)($row[
'mime'] ??
'');
1716 $type = ((string)($row[
'media_type'] ??
'') ===
'external_video')
1718 : ((strpos($mime,
'video/') === 0) ?
'video' : ((strpos($mime,
'image/') === 0 || preg_match(
'/\.(jpg|jpeg|png|gif|webp|svg)$/i', (string)($row[
'file_name'] ??
''))) ?
'image' :
'file'));
1719 $template = trim((
string)($row[
'template'] ??
''));
1720 if ($slot ===
'hero' && !empty($settings[
'hero_template']))
$template = $settings[
'hero_template'];
1721 if ($slot ===
'gallery' && !empty($settings[
'gallery_template'])) {
1722 $template = $settings[
'gallery_template'];
1724 if (($type ===
'video' || $type ===
'external_video') && in_array(
$template, array(
'image-gallery',
'carousel3',
'cols3'),
true))
$template = $type .
'-gallery';
1725 if ($type ===
'file' && in_array(
$template, array(
'image-gallery',
'video-gallery',
'carousel3',
'cols3'),
true))
$template =
'file-gallery';
1728 if (!$this->media_template_exists(
$template))
$template = $type .
'-' . ($slot ===
'gallery' ?
'gallery' : $slot);
1729 if (!$this->media_template_exists(
$template))
$template =
'file-' . ($slot ===
'gallery' ?
'gallery' : $slot);
1731 $url = $type ===
'external_video'
1732 ? (string)($row[
'embed_url'] ?? $row[
'external_url'] ??
'')
1733 :
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . (int)($row[
'id'] ?? 0);
1734 if ($type ===
'external_video' && $slot ===
'hero' && $this->is_youtube_media($row,
$url)) {
1735 $html .= $this->render_youtube_hero_figure($row,
$url, $settings);
1738 if ($type ===
'external_video') {
1739 $thumb_url = $this->external_video_thumb_url($row);
1740 $poster_url = $thumb_url;
1742 $thumb_url = !empty($row[
'thumb_file_path']) ?
$url .
'&dbx_thumb=1' :
$url;
1743 $poster_url = !empty($row[
'thumb_file_path']) ?
$url .
'&dbx_thumb=1' :
'';
1744 if ($slot ===
'gallery'
1745 && $type ===
'image'
1746 && strtolower(trim((
string)($settings[
'gallery_overflow'] ??
''))) ===
'tutorial') {
1751 'id' => (
string)($row[
'id'] ??
''),
1752 'url' => htmlspecialchars(
$url, ENT_QUOTES,
'UTF-8'),
1753 'thumb_url' => htmlspecialchars($thumb_url, ENT_QUOTES,
'UTF-8'),
1754 'poster_url' => htmlspecialchars($poster_url, ENT_QUOTES,
'UTF-8'),
1755 'media_type' => htmlspecialchars($type, ENT_QUOTES,
'UTF-8'),
1756 'title' => htmlspecialchars((
string)($row[
'title'] ??
''), ENT_QUOTES,
'UTF-8'),
1757 'alt' => htmlspecialchars((
string)($row[
'alt'] ?? $row[
'title'] ??
''), ENT_QUOTES,
'UTF-8'),
1758 'caption' => htmlspecialchars((
string)($row[
'caption'] ??
''), ENT_QUOTES,
'UTF-8'),
1759 'slot' => htmlspecialchars($slot, ENT_QUOTES,
'UTF-8'),
1760 'mime' => htmlspecialchars($mime, ENT_QUOTES,
'UTF-8'),
1761 'hero_margin_top' => htmlspecialchars($this->css_length_value($settings[
'hero_margin_top'] ??
'0',
'0'), ENT_QUOTES,
'UTF-8'),
1762 'hero_height' => htmlspecialchars($this->css_length_value($settings[
'hero_height'] ??
'auto',
'auto'), ENT_QUOTES,
'UTF-8'),
1763 'hero_variant' => htmlspecialchars((
string)($settings[
'hero_variant'] ??
'original'), ENT_QUOTES,
'UTF-8'),
1764 'hero_sticky_class' => $this->is_enabled_value($settings[
'hero_sticky'] ??
'0') ?
'dbx-media-hero-sticky' :
'',
1765 'hero_scroll_class' =>
'dbx-media-hero-scroll-' . htmlspecialchars((
string)($settings[
'hero_scroll_layer'] ??
'under'), ENT_QUOTES,
'UTF-8'),
1767 $width = max(0, (
int)($row[
'width'] ?? 0));
1768 $height = max(0, (
int)($row[
'height'] ?? 0));
1769 $thumbWidth = max(0, (
int)($row[
'thumb_width'] ?? 0));
1770 $thumbHeight = max(0, (
int)($row[
'thumb_height'] ?? 0));
1771 if ($thumbWidth <= 0 || $thumbHeight <= 0) {
1772 $thumbWidth = $width;
1773 $thumbHeight = $height;
1775 $visibleCount = max(1, min(12, (
int)($settings[
'gallery_visible_count'] ?? 3)));
1776 $data[
'image_dimensions'] = ($width > 0 && $height > 0)
1777 ?
' width="' . $width .
'" height="' . $height .
'"'
1779 $data[
'thumb_dimensions'] = ($thumbWidth > 0 && $thumbHeight > 0)
1780 ?
' width="' . $thumbWidth .
'" height="' . $thumbHeight .
'"'
1782 $data[
'image_sizes'] = $slot ===
'gallery'
1783 ?
'(max-width: 767px) 100vw, ' . (int)ceil(100 / $visibleCount) .
'vw'
1790 private function external_video_thumb_url(array $row) {
1791 $provider = strtolower(trim((
string)($row[
'provider'] ??
'')));
1792 $provider_id = trim((
string)($row[
'provider_id'] ??
''));
1793 if ($provider ===
'youtube' && preg_match(
'~^[A-Za-z0-9_-]{11}$~', $provider_id)) {
1794 return 'https://img.youtube.com/vi/' . $provider_id .
'/hqdefault.jpg';
1799 private function media_template_exists(
$template) {
1803 return is_file($path);
1806 private function media_file_exists(array $row) {
1807 if (strtolower((
string)($row[
'storage_type'] ??
'')) ===
'external') {
1808 return trim((
string)($row[
'embed_url'] ?? $row[
'external_url'] ??
'')) !==
'';
1810 $rel = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
1811 if ($rel ===
'' || strpos($rel,
'..') !==
false)
return false;
1817 $real = realpath(
$file);
1818 return $base && $real && strpos($real,
$base) === 0 && is_file($real) && is_readable($real);
1821 private function replace_content_markers(
$html, array
$merge) {
1823 if (strpos($key,
':') !==
false) {
1830 private function render_inline(
$tpl, $slot,
$value, $obj_key =
'') {
1831 if (!$this->html_has_visible_content(
$value))
return '';
1832 $file =
'i-' . $slot;
1833 $data_key = $obj_key !==
'' ? $obj_key : $slot;
1834 $path =
dbx()->os_path(
dbx()->
get_base_dir() .
'dbx/modules/dbxContent/tpl/htm/' . strtolower(
$file) .
'.htm');
1835 if (!is_file($path))
return $value;
1837 if (strpos(
$html,
'not found') !==
false || trim(
$html) ===
'')
return $value;
1842 private function html_has_visible_content(
$html) {
1843 $html = $this->strip_dbx_markers((
string)
$html);
1844 if (trim(
$html) ===
'')
return false;
1845 if (stripos(
$html,
'[modul=') !==
false)
return true;
1846 if (preg_match(
'/<(img|video|iframe|source|object|embed|table|ul|ol|li|figure)\b/i',
$html))
return true;
1847 $text = preg_replace(
'/<!--[\s\S]*?-->/u',
'',
$html);
1848 $text = preg_replace(
'/<br\s*\/?>/iu',
'', $text);
1849 $text = strip_tags($text);
1850 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5,
'UTF-8');
1851 $text = str_replace(
"\xC2\xA0",
' ', $text);
1852 return trim($text) !==
'';
1855 private function render_title_slot(
$tpl, $template_html, $title) {
1856 $title = htmlspecialchars((
string)$title, ENT_QUOTES,
'UTF-8');
1857 if (preg_match(
'/<h[1-6]\b[^>]*>[^<]*\{cms:title\}/i', (
string)$template_html)) {
1860 return $this->render_inline(
$tpl,
'title', $title);
1863 private function css_class_token($prefix,
$value) {
1864 $prefix = preg_replace(
'/[^a-z0-9_-]+/i',
'-', strtolower((
string)$prefix));
1865 $value = preg_replace(
'/[^a-z0-9_-]+/i',
'-', strtolower((
string)
$value));
1868 return trim($prefix .
'-' .
$value,
'-');
1875 $value = preg_replace(
'/[^a-z0-9\s#.,%()+\-\/_]+/i',
'',
$value);
1881 if (
$value ===
'')
return '';
1882 if (preg_match(
'/^-?(?:[0-9]+|[0-9]*\.[0-9]+)$/',
$value)) {
1888 private function hero_content_custom_properties($height) {
1889 $scale = $this->hero_content_scale($height);
1891 'cms-hero-content-scale' => $this->css_number($scale),
1892 'cms-hero-content-size' => $this->css_px(18 * $scale),
1893 'cms-hero-title-size' => $this->css_px(46 * $scale),
1894 'cms-hero-subtitle-size' => $this->css_px(30 * $scale),
1895 'cms-hero-content-padding' => $this->css_px(10 * $scale),
1896 'cms-hero-content-offset' => $this->css_px(34 * $scale),
1900 private function hero_content_scale($height) {
1901 $px = $this->css_length_to_px($height);
1902 if ($px <= 0)
return 1.0;
1904 $scale = $px / 420.0;
1905 if ($scale < 0.72) $scale = 0.72;
1906 if ($scale > 1.85) $scale = 1.85;
1910 private function css_length_to_px(
$value) {
1911 $value = strtolower(trim($this->css_length_value(
$value,
'auto')));
1914 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)px$/',
$value, $m))
return (
float)$m[1];
1915 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)$/',
$value, $m))
return (
float)$m[1];
1916 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)(rem|em)$/',
$value, $m))
return (
float)$m[1] * 16.0;
1917 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)(vh|svh|lvh|dvh)$/',
$value, $m))
return (
float)$m[1] * 7.2;
1918 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)vw$/',
$value, $m))
return (
float)$m[1] * 12.8;
1923 private function css_px(
$value) {
1924 return $this->css_number(
$value) .
'px';
1927 private function css_number(
$value) {
1930 return rtrim(rtrim(
$out,
'0'),
'.');
1933 private function css_custom_properties(array $props) {
1935 foreach ($props as $name =>
$value) {
1936 $name = preg_replace(
'/[^a-z0-9_-]+/i',
'-', (
string)$name);
1938 if ($name ===
'' ||
$value ===
'')
continue;
1939 $out[] =
'--' . strtolower($name) .
':' . htmlspecialchars(
$value, ENT_QUOTES,
'UTF-8');
1941 return implode(
';',
$out);
1944 private function gallery_aspect_ratio($size) {
1945 if (preg_match(
'/^\s*([0-9]{1,5})\s*x\s*([0-9]{1,5})\s*$/i', (
string)$size, $m)) {
1946 $w = max(1, (
int)$m[1]);
1947 $h = max(1, (
int)$m[2]);
1948 return $w .
' / ' . $h;
1953 private function gallery_data_dbx_attrs(array $settings,
bool $has_slot,
bool $has_media): string {
1954 if (!$has_slot || !$has_media) {
1960 'img-count=' . max(1, min(12, (
int)($settings[
'gallery_visible_count'] ?? 3))),
1961 'img-size=' . trim((
string)($settings[
'gallery_image_size'] ??
'original')),
1962 'lightbox-width=' . $this->css_length_value($settings[
'gallery_lightbox_width'] ??
'100vw',
'100vw'),
1963 'overflow=' . trim((
string)($settings[
'gallery_overflow'] ??
'grid')),
1964 'click=' . trim((
string)($settings[
'gallery_click_behavior'] ??
'lightbox')),
1967 return ' data-dbx="' . htmlspecialchars(implode(
'|', $parts), ENT_QUOTES,
'UTF-8') .
'"';
1970 private function seoExcerptFromContent(
$html, $max = 160) {
1971 $text = trim(preg_replace(
'/\s+/u',
' ', html_entity_decode(strip_tags((
string)
$html), ENT_QUOTES,
'UTF-8')));
1972 if ($text ===
'')
return '';
1974 $len = function_exists(
'mb_strlen') ? mb_strlen($text,
'UTF-8') : strlen($text);
1975 if ($len <= $max)
return $text;
1977 $excerpt = function_exists(
'mb_substr') ? mb_substr($text, 0, $max,
'UTF-8') : substr($text, 0, $max);
1978 if (function_exists(
'mb_strrpos')) {
1979 $cut = mb_strrpos($excerpt,
' ', 0,
'UTF-8');
1980 if ($cut !==
false && $cut > 40) {
1981 $excerpt = mb_substr($excerpt, 0, $cut,
'UTF-8');
1983 } elseif (($cut = strrpos($excerpt,
' ')) !==
false && $cut > 40) {
1984 $excerpt = substr($excerpt, 0, $cut);
1987 return rtrim($excerpt,
".,;:- \t") .
'…';
1991 private function seoAbsoluteUrl(
string $path): string {
1992 return rtrim((string)
dbx()->
get_base_url(),
'/') .
'/' . ltrim($path,
'/');
2002 private function seoCanonicalUrl($permalink,
bool $isHomePage =
false) {
2007 $permalink = trim((
string)$permalink);
2008 if ($permalink !==
'' && preg_match(
'/^https?:\/\//i', $permalink)) {
2011 return $this->seoAbsoluteUrl($permalink);
2014 private function seoOgImageUrl(
$db, array $rec) {
2015 $seo_image_id = (int)($rec[
'seo_image_id'] ?? 0);
2016 if ($seo_image_id > 0) {
2017 $url = $this->seoOgImageFromMediaId(
$db, $seo_image_id);
2021 $settings = $this->content_settings(
$db, $rec);
2022 $hero_id = (int)($settings[
'hero_image_id'] ?? 0);
2023 if ($hero_id <= 0) {
2024 $cid = (int)($rec[
'id'] ?? 0);
2026 $usage_rows =
$db->select(
2027 $this->dd_media_usage,
2028 dbxContentMediaUsageScope::withLanguage(
'content_id = ' . $cid .
' AND active = 1 AND slot = \'hero\''),
2037 if (is_array($usage_rows) && !empty($usage_rows[0][
'media_id'])) {
2038 $hero_id = (int)$usage_rows[0][
'media_id'];
2042 if ($hero_id <= 0)
return '';
2044 $row =
$db->select1($this->dd_media, $hero_id);
2045 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || !$this->media_file_exists($row)) {
2049 $mime = (string)($row[
'mime'] ??
'');
2050 $file_name = (string)($row[
'file_name'] ?? $row[
'file_path'] ??
'');
2051 if (strpos($mime,
'image/') !== 0 && !preg_match(
'/\.(jpg|jpeg|png|gif|webp|svg)$/i', $file_name)) {
2055 return $this->seoAbsoluteUrl(
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $hero_id);
2058 private function seoOgImageFromMediaId(
$db,
int $media_id) {
2059 $media_id = (int)$media_id;
2060 if ($media_id <= 0)
return '';
2062 $row =
$db->select1($this->dd_media, $media_id);
2063 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || !$this->media_file_exists($row)) {
2067 $mime = (string)($row[
'mime'] ??
'');
2068 $file_name = (string)($row[
'file_name'] ?? $row[
'file_path'] ??
'');
2069 if (strpos($mime,
'image/') !== 0 && !preg_match(
'/\.(jpg|jpeg|png|gif|webp|svg)$/i', $file_name)) {
2073 return $this->seoAbsoluteUrl(
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $media_id);
2076 private function seoHreflangBlock(
$db, array $rec,
string $currentLng): string {
2077 $lngUid = trim((string)($rec[
'lng_uid'] ??
''));
2078 if ($lngUid ===
'' || !function_exists(
'dbx_accessible_lngs')) {
2083 if (!is_array($lngs) || count($lngs) < 2) {
2087 $alternates = array();
2088 $escapedUid = str_replace(
"'",
"''", $lngUid);
2089 $isHomeGroup = dbxContentHome::resolveCid($currentLng) === (int)($rec[
'id'] ?? 0);
2090 $masterLng = dbxContentLngSync::masterLng();
2092 foreach ($lngs as
$lng) {
2093 $lng = strtolower(trim((
string)
$lng));
2094 if (
$lng ===
'' || !preg_match(
'/^[a-z]{2,3}$/',
$lng))
continue;
2096 $permalink = $this->seoHreflangPermalink(
$db,
$lng, $lngUid, $escapedUid, $rec, $currentLng);
2097 if ($permalink ===
'')
continue;
2099 $alternates[] = array(
2101 'url' => $this->seoCanonicalUrl(
2103 $isHomeGroup &&
$lng === $masterLng
2108 if (count($alternates) < 2) {
2113 $defaultLng = strtolower(trim((
string)
dbx()->get_config(
'dbx',
'default_lng',
'de')));
2114 if ($defaultLng ===
'' || $defaultLng ===
'undef') $defaultLng =
'de';
2116 foreach ($alternates as $alt) {
2117 $lines[] =
'<link rel="alternate" hreflang="' . htmlspecialchars((
string)$alt[
'lng'], ENT_QUOTES,
'UTF-8') .
'" href="' . htmlspecialchars((
string)$alt[
'url'], ENT_QUOTES,
'UTF-8') .
'">';
2120 foreach ($alternates as $alt) {
2121 if ((
string)$alt[
'lng'] === $defaultLng) {
2122 $lines[] =
'<link rel="alternate" hreflang="x-default" href="' . htmlspecialchars((
string)$alt[
'url'], ENT_QUOTES,
'UTF-8') .
'">';
2127 return count($lines) ?
"\n " . implode(
"\n ", $lines) :
'';
2130 private function seoHreflangPermalink(
$db,
string $lng,
string $lngUid,
string $escapedUid, array $rec,
string $currentLng): string {
2131 if (
$lng === $currentLng) {
2132 return trim((
string)($rec[
'permalink'] ??
''));
2138 $sibling =
$db->select1(
2139 dbxContentLng::ddContent(
$lng),
2140 "lng_uid = '" . $escapedUid .
"' AND activ = 1",
2144 if (!is_array($sibling)) {
2148 return trim((
string)($sibling[
'permalink'] ??
''));
2151 private function seoJsonLd(array $rec,
string $title,
string $description,
string $canonical,
string $lng): string {
2152 if ($title ===
'' && $description ===
'' && $canonical ===
'') {
2157 '@context' =>
'https://schema.org',
2158 '@type' =>
'WebPage',
2160 'description' => $description,
2161 'url' => $canonical,
2162 'inLanguage' =>
$lng,
2165 $modified = trim((
string)($rec[
'update_date'] ??
''));
2166 if ($modified !==
'') {
2167 $ts = strtotime($modified);
2168 if ($ts !==
false) {
2169 $data[
'dateModified'] = gmdate(
'c', $ts);
2173 $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
2178 return '<script type="application/ld+json">' .
$json .
'</script>';
2181 private function cleanup_empty_sections(
$html) {
2182 $html = preg_replace(
'/<section\b[^>]*class="[^"]*\bcms-header\b[^"]*"[^>]*>\s*<\/section>/i',
'', (
string)
$html);
2183 $html = preg_replace(
'/<section\b[^>]*class="[^"]*\bgallery\b[^"]*\bno-gallery\b[^"]*"[^>]*>[\s\S]*?<\/section>/i',
'', (
string)
$html);
2184 $html = preg_replace(
'/<section\b([^>]*class="[^"]*\bdbx-content-(?:gallery-section|teaser-section|header-text)\b[^"]*"[^>]*)>\s*<\/section>/i',
'', (
string)
$html);
2185 $html = preg_replace(
'/<footer\b([^>]*class="[^"]*\bdbx-content-footer\b[^"]*"[^>]*)>\s*<\/footer>/i',
'',
$html);
2186 $html = preg_replace(
'/<div\b([^>]*class="[^"]*\bdbx-content-teaser\b[^"]*"[^>]*)>\s*<\/div>/i',
'',
$html);
2187 $html = preg_replace(
'/<section\b([^>]*class="[^"]*\bdbx-content-hero\b[^"]*"[^>]*)>\s*<\/section>/i',
'',
$html);
2188 $html = preg_replace(
'/<div\b([^>]*class="[^"]*\bhero-content\b[^"]*"[^>]*)>\s*<\/div>/i',
'',
$html);