8 private $dd_media =
'dbxMedia';
9 private $dd_media_usage =
'dbxMediaUsage';
17 if ($cid <= 0)
return 'Keine dbx_cid gesetzt!';
19 $db =
dbx()->get_system_obj(
'dbxDB');
20 $tpl =
dbx()->get_system_obj(
'dbxTPL');
22 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0) {
23 return $tpl->get_tpl(
'dbxContent|no-page');
26 $rights = $this->resolve_content_rights(
$db, $rec);
27 if (!
dbx()->can($rights)) {
28 if (empty($options[
'admin_help']) || !
dbx()->can(
'admin')) {
29 return '<div class="alert alert-warning" role="alert">Sie haben keinen Zugriff auf diese Seite.</div>';
33 if (empty($options[
'skip_hits'])) {
34 $this->update_hits(
$db, $rec);
37 $forceTemplate = trim((
string)($options[
'template'] ??
''));
38 if ($forceTemplate !==
'') {
39 $template = $this->normalize_content_template($forceTemplate);
41 $template = $this->normalize_content_template($this->resolve_content_setting(
$db, $rec,
'template',
'template',
'c-content'));
46 $template_html = $tpl->get_tpl(
'dbxContent|' . $template);
47 $slots = $this->detect_template_slots($template_html);
48 $content_html = $this->convert_mod_placeholders((
string)($rec[
'content'] ??
''));
49 $parsed = $this->parse_content($content_html, $slots);
50 $parsed = $this->render_inline_media_placeholders(
$db, $parsed);
51 $cms_cols = max(1, min(3, (
int)($slots[
'cols'] ?? 1)));
53 $settings = $this->content_settings(
$db, $rec);
54 $hero_classes = array(
55 $this->css_class_token(
'hero-template', $settings[
'hero_template'] ??
'image-hero'),
56 $this->css_class_token(
'hero-variant', $settings[
'hero_variant'] ??
'original'),
57 $this->is_enabled_value($settings[
'hero_sticky'] ??
'0') ?
'hero-sticky' :
'hero-not-sticky',
58 $this->css_class_token(
'hero-scroll', $settings[
'hero_scroll_layer'] ??
'under'),
60 $gallery_classes = array(
61 $this->css_class_token(
'gallery-template', $settings[
'gallery_template'] ??
'image-gallery'),
62 $this->css_class_token(
'gallery-overflow', $settings[
'gallery_overflow'] ??
'grid'),
63 $this->css_class_token(
'gallery-click', $settings[
'gallery_click_behavior'] ??
'lightbox'),
64 $this->css_class_token(
'gallery-count', $settings[
'gallery_visible_count'] ??
'3'),
65 $this->css_class_token(
'gallery-size', $settings[
'gallery_image_size'] ??
'original'),
66 $this->css_class_token(
'gallery-lightbox-width', $settings[
'gallery_lightbox_width'] ??
'100vw'),
68 $hero_style = $this->css_custom_properties(array(
69 'cms-hero-margin-top' => $this->css_length_value($settings[
'hero_margin_top'] ??
'0',
'0'),
70 'cms-hero-height' => $this->css_length_value($settings[
'hero_height'] ??
'auto',
'auto'),
71 ) + $this->hero_text_custom_properties($settings[
'hero_height'] ??
'auto'));
72 $gallery_style = $this->css_custom_properties(array(
73 'dbx-gallery-visible-count' => (
string)max(1, min(12, (
int)($settings[
'gallery_visible_count'] ?? 3))),
74 'dbx-gallery-aspect-ratio' => $this->gallery_aspect_ratio($settings[
'gallery_image_size'] ??
'original'),
75 'dbx-gallery-lightbox-width' => $this->css_length_value($settings[
'gallery_lightbox_width'] ??
'100vw',
'100vw'),
79 'i' =>
dbx()->next_id(),
80 'id' => (
string)($rec[
'id'] ??
''),
81 'title' => (
string)($rec[
'title'] ??
''),
82 'description' => (
string)($rec[
'description'] ??
''),
83 'keywords' => (
string)($rec[
'keywords'] ??
''),
84 'permalink' => (
string)($rec[
'permalink'] ??
''),
85 'template' => $template,
86 'content' => $parsed[
'content'],
87 'body' => $parsed[
'body'],
88 'h1' => $parsed[
'h1'],
89 'header' => $parsed[
'header'],
90 'teaser' => $parsed[
'teaser'],
91 'thesar' => (
string)($rec[
'thesar'] ??
''),
92 'footer' => $parsed[
'footer'],
93 'col_1' => $parsed[
'col1'],
94 'col_2' => $parsed[
'col2'],
95 'col_3' => $parsed[
'col3'],
96 'content:h1' => $parsed[
'h1'],
97 'content:header' => $parsed[
'header'],
98 'content:hero_text' => $parsed[
'hero_text'],
99 'content:teaser' => $parsed[
'teaser'],
100 'content:thesar' => $parsed[
'teaser'],
101 'content:footer' => $parsed[
'footer'],
102 'content:body' => $parsed[
'body'],
103 'content:col1' => $parsed[
'col1'],
104 'content:col2' => $parsed[
'col2'],
105 'content:col3' => $parsed[
'col3'],
106 'cms:title' => $this->render_title_slot($tpl, $template_html, (
string)($rec[
'title'] ??
'')),
107 'cms:hero_text' => $this->render_inline($tpl,
'hero', $parsed[
'hero_text'],
'hero'),
108 'cms:header' => $this->render_inline($tpl,
'header', $parsed[
'header']),
109 'cms:teaser' => $this->render_inline($tpl,
'header', $parsed[
'header'],
'header'),
110 'cms:content' => $this->render_inline($tpl,
'content', $parsed[
'content']),
111 'cms:cols' => (
string)$cms_cols,
112 'cms:hero_class' =>
'no-hero',
113 'cms:gallery_class' =>
'no-gallery',
116 'cms:footer' => $this->render_inline($tpl,
'footer', $parsed[
'footer']),
117 'cms:col1' => $cms_cols === 1 ? $parsed[
'body'] : $this->render_inline($tpl,
'col', $parsed[
'col1'],
'col'),
118 'cms:col2' => $this->render_inline($tpl,
'col', $parsed[
'col2'],
'col'),
119 'cms:col3' => $this->render_inline($tpl,
'col', $parsed[
'col3'],
'col'),
122 $merge = array_merge($merge, array(
123 'hero:template' => htmlspecialchars((
string)($settings[
'hero_template'] ??
'image-hero'), ENT_QUOTES,
'UTF-8'),
124 'hero:image_id' => htmlspecialchars((
string)($settings[
'hero_image_id'] ??
''), ENT_QUOTES,
'UTF-8'),
125 'hero:margin_top' => htmlspecialchars((
string)($settings[
'hero_margin_top'] ??
'0'), ENT_QUOTES,
'UTF-8'),
126 'hero:height' => htmlspecialchars((
string)($settings[
'hero_height'] ??
'auto'), ENT_QUOTES,
'UTF-8'),
127 'hero:variant' => htmlspecialchars((
string)($settings[
'hero_variant'] ??
'original'), ENT_QUOTES,
'UTF-8'),
128 'hero:sticky' => htmlspecialchars((
string)($settings[
'hero_sticky'] ??
'0'), ENT_QUOTES,
'UTF-8'),
129 'hero:scroll_layer' => htmlspecialchars((
string)($settings[
'hero_scroll_layer'] ??
'under'), ENT_QUOTES,
'UTF-8'),
130 'hero:class' => implode(
' ', array_filter($hero_classes)),
131 'hero:style' => $hero_style,
132 'gallery:template' => htmlspecialchars((
string)($settings[
'gallery_template'] ??
'image-gallery'), ENT_QUOTES,
'UTF-8'),
133 'gallery:visible_count' => htmlspecialchars((
string)($settings[
'gallery_visible_count'] ??
'3'), ENT_QUOTES,
'UTF-8'),
134 'gallery:image_size' => htmlspecialchars((
string)($settings[
'gallery_image_size'] ??
'original'), ENT_QUOTES,
'UTF-8'),
135 'gallery:lightbox_width' => htmlspecialchars($this->css_length_value($settings[
'gallery_lightbox_width'] ??
'100vw',
'100vw'), ENT_QUOTES,
'UTF-8'),
136 'gallery:overflow' => htmlspecialchars((
string)($settings[
'gallery_overflow'] ??
'grid'), ENT_QUOTES,
'UTF-8'),
137 'gallery:click_behavior' => htmlspecialchars((
string)($settings[
'gallery_click_behavior'] ??
'lightbox'), ENT_QUOTES,
'UTF-8'),
138 'gallery:class' => implode(
' ', array_filter($gallery_classes)),
139 'gallery:style' => $gallery_style,
141 $merge = array_merge($merge, $this->render_media_merge(
$db, $tpl, $cid, $slots, $rec, $settings));
142 $merge[
'cms:hero'] = $merge[
'media:hero'] ??
'';
143 $merge[
'cms:gallery'] = $merge[
'media:gallery'] ??
'';
144 $merge[
'cms:hero_class'] = trim((trim($merge[
'cms:hero']) !==
'' ?
'has-hero ' :
'no-hero ') . ($merge[
'hero:class'] ??
''));
145 $merge[
'cms:gallery_class'] = trim((trim($merge[
'cms:gallery']) !==
'' ?
'has-gallery ' :
'no-gallery ') . ($merge[
'gallery:class'] ??
''));
146 $has_gallery_slot = !empty($slots[
'media'][
'gallery']) || !empty($slots[
'cms'][
'gallery']);
147 $has_gallery_media = trim((
string)($merge[
'cms:gallery'] ??
'')) !==
'';
148 $merge[
'gallery:data_dbx'] = $this->gallery_data_dbx_attrs($settings, $has_gallery_slot, $has_gallery_media);
149 $content_html = $this->replace_content_markers($template_html, $merge);
150 $content_html = $this->cleanup_empty_sections($content_html);
151 $content_html = $this->decorate_brand_text($content_html);
152 $content_html = $tpl->replaces($content_html, $merge);
153 return $content_html;
156 private function decorate_brand_text(
string $html): string {
157 if ($html ===
'' || strpos($html,
'dbXapp') === false) {
161 return $this->replace_text_segments($html,
function(
string $text):
string {
162 return str_replace(
'dbXapp',
'db<span style="color:red">X</span>app', $text);
166 private function replace_text_segments(
string $html, callable $callback): string {
167 $parts = preg_split(
'/(<[^>]+>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
168 if (!is_array($parts)) {
169 return (
string)$callback($html);
172 foreach ($parts as $idx => $part) {
173 if ($part ===
'' || $part[0] ===
'<') {
176 $parts[$idx] = (string)$callback($part);
179 return implode(
'', $parts);
182 public function applySeoMeta(
int $cid, array $rec =
null, array $meta =
null) {
184 if ($cid <= 0)
return;
186 $db =
dbx()->get_system_obj(
'dbxDB');
187 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0) {
188 if (is_array($meta) && !empty($meta[
'seo']) && is_array($meta[
'seo'])) {
189 $rec = array_merge(array(
'id' => $cid), $meta[
'seo']);
192 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0)
return;
196 $pageTitle = trim((
string)($rec[
'title'] ??
''));
197 $seoTitle = trim((
string)($rec[
'seo_title'] ??
''));
198 $displayTitle = $seoTitle !==
'' ? $seoTitle : $pageTitle;
199 dbx()->set_system_var(
'dbx_title', $displayTitle);
201 $description = trim((
string)($rec[
'description'] ??
''));
202 if ($description ===
'') {
203 $description = $this->seoExcerptFromContent((
string)($rec[
'content'] ??
''));
206 $keywords = trim((
string)($rec[
'keywords'] ??
''));
207 $canonical = $this->seoCanonicalUrl((
string)($rec[
'permalink'] ??
''));
208 $activ = (int)($rec[
'activ'] ?? 1);
209 $metaRobots = trim((
string)($rec[
'meta_robots'] ??
''));
210 if ($metaRobots ===
'') {
211 $metaRobots = (
$activ === 0) ?
'noindex,nofollow' :
'index,follow';
212 } elseif (
$activ === 0 && stripos($metaRobots,
'noindex') ===
false) {
213 $metaRobots =
'noindex,nofollow';
216 $og_title = $displayTitle;
217 $og_image = $this->seoOgImageUrl(
$db, $rec);
218 $currentLng = strtolower(trim((
string)
dbx()->get_system_var(
'dbx_lng',
'de')));
219 if ($currentLng ===
'') $currentLng =
'de';
221 dbx()->set_system_var(
'dbx_meta_description', $description);
222 dbx()->set_system_var(
'dbx_meta_keywords', $keywords);
223 dbx()->set_system_var(
'dbx_canonical', $canonical);
224 dbx()->set_system_var(
'dbx_robots', $metaRobots);
225 dbx()->set_system_var(
'dbx_og_title', $og_title);
226 dbx()->set_system_var(
'dbx_og_description', $description);
227 dbx()->set_system_var(
'dbx_og_url', $canonical);
228 dbx()->set_system_var(
'dbx_og_image', $og_image);
229 dbx()->set_system_var(
'dbx_hreflang', $this->seoHreflangBlock(
$db, $rec, $currentLng));
230 dbx()->set_system_var(
'dbx_json_ld', $this->seoJsonLd($rec, $displayTitle, $description, $canonical, $currentLng));
235 'title' => (string)($rec[
'title'] ??
''),
236 'seo_title' => (string)($rec[
'seo_title'] ??
''),
237 'description' => (string)($rec[
'description'] ??
''),
238 'keywords' => (string)($rec[
'keywords'] ??
''),
239 'meta_robots' => (string)($rec[
'meta_robots'] ??
''),
240 'seo_image_id' => (int)($rec[
'seo_image_id'] ?? 0),
241 'permalink' => (string)($rec[
'permalink'] ??
''),
242 'activ' => (int)($rec[
'activ'] ?? 1),
243 'update_date' => (string)($rec[
'update_date'] ??
''),
244 'lng_uid' => (string)($rec[
'lng_uid'] ??
''),
250 'dbx_meta_description',
255 'dbx_og_description',
261 dbx()->set_system_var($key,
'');
266 return $this->interpret_content_modules($html);
269 private function interpret_content_modules($html) {
270 $html = (string)$html;
271 if ($html ===
'' || stripos($html,
'[modul=') ===
false) {
274 $interpreter =
dbx()->get_system_obj(
'dbxInterpreter');
275 if (!is_object($interpreter) || !method_exists($interpreter,
'run')) {
278 return $interpreter->run($html);
282 $db =
dbx()->get_system_obj(
'dbxDB');
283 return $this->resolve_folder_rights(
$db, $folder_id);
286 private function resolve_content_rights(
$db, array $rec): string {
289 return $this->resolve_folder_rights(
$db, (int)($rec[
'folder'] ?? 0));
292 private function resolve_folder_rights(
$db, $folder_id, array $visited = array()) {
293 $folder_id = (int)$folder_id;
294 if ($folder_id <= 0)
return '*';
295 if (isset($visited[$folder_id]))
return '*';
296 $visited[$folder_id] = 1;
298 $rec =
$db->select1(dbxContentLng::ddFolder(), $folder_id,
'*', 0);
299 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0)
return '*';
301 $parent_id = (int)($rec[
'parent_id'] ?? 0);
302 $raw = trim((
string)($rec[
'group_read'] ??
''));
303 if ($raw ===
'')
return '*';
305 $parts = preg_split(
'/\s*,\s*/', $raw, -1, PREG_SPLIT_NO_EMPTY);
307 $uses_parent =
false;
309 foreach ($parts as $part) {
310 $part = trim((
string)$part);
311 if ($part ===
'')
continue;
312 if (strtolower($part) ===
'parent') {
320 $parent_rights = $this->resolve_folder_rights(
$db, $parent_id, $visited);
321 foreach (preg_split(
'/\s*,\s*/', $parent_rights, -1, PREG_SPLIT_NO_EMPTY) as $part) {
322 $part = trim((
string)$part);
323 if ($part !==
'' && strtolower($part) !==
'parent') $out[$part] = 1;
327 if (!count($out))
return '*';
328 return implode(
',', array_keys($out));
331 private function is_parent_value($value) {
332 $value = trim((
string)$value);
333 return $value ===
'' || strtolower($value) ===
'parent';
336 private function is_no_hero_value($value) {
337 $value = strtolower(trim((
string)$value));
338 return in_array($value, array(
'none',
'no-hero',
'0',
'off'),
true);
341 private function is_enabled_value($value) {
342 $value = strtolower(trim((
string)$value));
343 return in_array($value, array(
'1',
'true',
'on',
'yes',
'sticky'),
true);
346 private function resolve_folder_setting(
$db, $folder_id,
$field, $fallback, array $visited = array()) {
347 $folder_id = (int)$folder_id;
348 if ($folder_id <= 0 || isset($visited[$folder_id]))
return $fallback;
349 $visited[$folder_id] = 1;
351 $rec =
$db->select1(dbxContentLng::ddFolder(), $folder_id,
'*', 0);
352 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0)
return $fallback;
354 $value = trim((
string)($rec[
$field] ??
''));
355 if (!$this->is_parent_value($value))
return $value;
356 return $this->resolve_folder_setting(
$db, (
int)($rec[
'parent_id'] ?? 0),
$field, $fallback, $visited);
359 private function resolve_content_setting(
$db, array $rec, $content_field, $folder_field, $fallback) {
360 $value = trim((
string)($rec[$content_field] ??
''));
361 if (!$this->is_parent_value($value))
return $value;
362 return $this->resolve_folder_setting(
$db, (
int)($rec[
'folder'] ?? 0), $folder_field, $fallback);
365 private function local_content_setting(array $rec,
$field, $fallback) {
366 $value = trim((
string)($rec[
$field] ??
''));
367 return $this->is_parent_value($value) ? $fallback : $value;
370 private function content_settings(
$db, array $rec) {
372 'hero_template' => $this->resolve_content_setting(
$db, $rec,
'hero_template',
'hero_template',
'image-hero'),
373 'hero_image_id' => $this->resolve_content_setting(
$db, $rec,
'hero_image_id',
'hero_image_id',
''),
374 'hero_margin_top' => $this->resolve_content_setting(
$db, $rec,
'hero_margin_top',
'hero_margin_top',
'0'),
375 'hero_height' => $this->resolve_content_setting(
$db, $rec,
'hero_height',
'hero_height',
'auto'),
376 'hero_variant' => $this->resolve_content_setting(
$db, $rec,
'hero_variant',
'hero_variant',
'original'),
377 'hero_sticky' => $this->resolve_content_setting(
$db, $rec,
'hero_sticky',
'hero_sticky',
'0'),
378 'hero_scroll_layer' => $this->resolve_content_setting(
$db, $rec,
'hero_scroll_layer',
'hero_scroll_layer',
'under'),
379 'gallery_template' =>
'image-gallery',
380 'gallery_visible_count' =>
'3',
381 'gallery_image_size' => $this->local_content_setting($rec,
'gallery_image_size',
'original'),
382 'gallery_lightbox_width' => $this->local_content_setting($rec,
'gallery_lightbox_width',
'100vw'),
383 'gallery_overflow' => $this->local_content_setting($rec,
'gallery_overflow',
'grid'),
384 'gallery_click_behavior' => $this->local_content_setting($rec,
'gallery_click_behavior',
'lightbox'),
388 private function update_hits(
$db, array $rec) {
389 $hits = (int)($rec[
'hits'] ?? 0);
390 if ($hits < 0 || empty($rec[
'id']))
return;
391 $db->update(dbxContentLng::ddContent(), array(
'hits' => $hits + 1), (
int)$rec[
'id'], 0, 1, 1, 0);
394 private function get_content_tpl_from_folder($folder = 0) {
395 $folder = (int)$folder;
396 $db =
dbx()->get_system_obj(
'dbxDB');
397 while ($folder > 0) {
398 $rec =
$db->select1(dbxContentLng::ddFolder(), $folder,
'*', 0);
399 if (!is_array($rec) || (
int)($rec[
'id'] ?? 0) <= 0)
break;
400 $tpl = trim((
string)($rec[
'template'] ??
''));
401 if ($tpl !==
'')
return $tpl;
402 $folder = (int)($rec[
'parent_id'] ?? 0);
407 private function content_template_exists($template) {
408 $template = trim((
string)$template);
409 if ($template ===
'')
return false;
410 $path =
dbx()->os_path(
dbx()->get_base_dir() .
'dbx/modules/dbxContent/tpl/htm/' . $template .
'.htm');
411 return is_file($path);
414 private function normalize_content_template($template) {
415 $template = trim((
string)$template);
416 if ($template ===
'' || strtolower($template) ===
'parent') $template =
'c-content';
417 if ($this->content_template_exists($template))
return $template;
420 'c-hero_header-gallery-body3-footer' =>
'c-title-hero_header-gallery-body3-footer',
422 if (isset($aliases[$template]) && $this->content_template_exists($aliases[$template])) {
423 return $aliases[$template];
426 $template_key = strtolower($template);
427 $cols = strpos($template_key,
'col3') !==
false ?
'3' : (strpos($template_key,
'col2') !==
false ?
'2' :
'');
428 $has_hero = strpos($template_key,
'hero') !==
false;
429 $has_gallery = strpos($template_key,
'gallery') !==
false;
430 $has_header = strpos($template_key,
'teaser') !==
false || strpos($template_key,
'thesar') !==
false;
432 if ($has_hero && $has_gallery && $cols ===
'3' && $this->content_template_exists(
'c-title-hero_header-gallery-body3-footer')) {
433 return 'c-title-hero_header-gallery-body3-footer';
435 if ($has_hero && $has_gallery)
return 'c-hero-header-gallery-body' . $cols .
'-footer';
436 if ($has_hero)
return 'c-hero-header-body' . $cols .
'-footer';
437 if ($has_gallery)
return 'c-header-gallery-body' . $cols .
'-footer';
438 if ($has_header || strpos($template_key,
'footer') !==
false || $cols !==
'')
return 'c-header-body' . $cols .
'-footer';
442 private function detect_template_slots($template_html) {
443 $template_html = (string)$template_html;
445 'content' => array(),
451 if (preg_match_all(
'/\{content:([a-z0-9_]+)\}/i', $template_html, $m)) {
452 foreach ($m[1] as $slot) {
453 $slots[
'content'][strtolower($slot)] = 1;
456 if (preg_match_all(
'/\{media:([a-z0-9_]+)\}/i', $template_html, $m)) {
457 foreach ($m[1] as $slot) {
458 $slots[
'media'][strtolower($slot)] = 1;
461 if (preg_match_all(
'/\{cms:([a-z0-9_]+)\}/i', $template_html, $m)) {
462 foreach ($m[1] as $slot) {
463 $slot = strtolower($slot);
464 $slots[
'cms'][$slot] = 1;
465 if (in_array($slot, array(
'hero',
'gallery'),
true)) $slots[
'media'][$slot] = 1;
466 if ($slot ===
'header') $slots[
'content'][
'header'] = 1;
467 if ($slot ===
'hero_text') $slots[
'content'][
'hero_text'] = 1;
468 if ($slot ===
'teaser') $slots[
'content'][
'teaser'] = 1;
469 if ($slot ===
'content') $slots[
'content'][
'content'] = 1;
470 if ($slot ===
'footer') $slots[
'content'][
'footer'] = 1;
471 if (in_array($slot, array(
'col1',
'col2',
'col3'),
true)) $slots[
'content'][$slot] = 1;
475 if (strpos($template_html,
'{content}') !==
false || strpos($template_html,
'{content:body}') !==
false) {
476 $slots[
'content'][
'content'] = 1;
479 if (isset($slots[
'content'][
'col3']) || strpos($template_html,
'{col_3}') !==
false || strpos($template_html,
'{cms:col3}') !==
false) $slots[
'cols'] = 3;
480 elseif (isset($slots[
'content'][
'col2']) || strpos($template_html,
'{col_2}') !==
false || strpos($template_html,
'{cms:col2}') !==
false) $slots['cols'] = 2;
481 elseif (isset($slots['content']['col1']) || strpos($template_html, '{col_1}
') !== false || strpos($template_html, '{cms:col1}
') !== false) $slots['cols
'] = 1;
482 else $slots['cols
'] = 1;
487 private function parse_content($html, array $slots) {
488 $blocks = $this->html_blocks($html);
489 $segments = $this->segment_content_blocks($blocks);
491 $hero_blocks = $segments['hero
'];
492 $header_blocks = $segments['header
'];
493 $footer_blocks = $segments['footer
'];
494 $body_blocks = $segments['content
'];
496 $has_hero_text_slot = !empty($slots['content
']['hero_text
']) || !empty($slots['cms
']['hero_text
']);
497 $has_header_slot = !empty($slots['content
']['header
']) || !empty($slots['content
']['teaser
'])
498 || !empty($slots['content
']['thesar
']) || !empty($slots['cms
']['header
']);
499 $has_footer_slot = !empty($slots['content
']['footer
']) || !empty($slots['cms
']['footer
']);
501 if (!$has_hero_text_slot) {
502 $body_blocks = array_merge($hero_blocks, $body_blocks);
503 $hero_blocks = array();
505 if (!$has_header_slot) {
506 $body_blocks = array_merge($header_blocks, $body_blocks);
507 $header_blocks = array();
509 if (!$has_footer_slot) {
510 $body_blocks = array_merge($body_blocks, $footer_blocks);
511 $footer_blocks = array();
514 $cols = max(1, min(3, (int)($slots['cols
'] ?? 1)));
515 $col_blocks = $this->split_content_for_columns($body_blocks, $cols);
516 $all_content = $this->strip_dbx_markers($this->join_blocks($blocks));
517 $body_html = $this->strip_dbx_markers($this->join_blocks($body_blocks));
521 'hero_text
' => $this->strip_dbx_markers($this->join_blocks($hero_blocks)),
522 'header
' => $this->strip_dbx_markers($this->join_blocks($header_blocks)),
523 'teaser
' => $this->strip_dbx_markers($this->join_blocks($header_blocks)),
524 'footer
' => $this->strip_dbx_markers($this->join_blocks($footer_blocks)),
525 'body
' => $body_html,
526 'content
' => (isset($slots['content
']['content
']) || $cols === 1) ? $body_html : $all_content,
527 'col1
' => $cols === 1 ? $body_html : $this->strip_dbx_markers($this->join_blocks($col_blocks[0] ?? array())),
528 'col2
' => $this->strip_dbx_markers($this->join_blocks($col_blocks[1] ?? array())),
529 'col3
' => $this->strip_dbx_markers($this->join_blocks($col_blocks[2] ?? array())),
533 private function html_blocks($html) {
534 $html = trim((string)$html);
535 if ($html === '') return array();
537 $pattern = '/(<!--\s*
dbx:[^>]*?-->|<hr\b[^>]*>|<(h1|h2|h3|h4|h5|h6|p|ul|ol|blockquote|figure|table|div)\b[^>]*>.*?<\/\2>|<img\b[^>]*>)/is
';
538 preg_match_all($pattern, $html, $matches, PREG_OFFSET_CAPTURE);
542 foreach ($matches[0] as $idx => $match) {
543 $before = trim(substr($html, $pos, $match[1] - $pos));
544 if ($before !== '') $blocks[] = array('type
' => 'html
', 'html
' => $before);
547 $type = strtolower((string)($matches[2][$idx][0] ?? 'html
'));
548 if (preg_match('/^<!--\s*
dbx:/i
', $chunk)) $type = 'marker
';
549 elseif (preg_match('/^<hr/i
', $chunk)) $type = $this->marker_name_from_html($chunk) !== '' ? 'marker
' : 'break';
550 elseif (preg_match('/^<img/i
', $chunk)) $type = 'img
';
552 $blocks[] = array('type
' => $type, 'html
' => $chunk);
553 $pos = $match[1] + strlen($chunk);
556 $rest = trim(substr($html, $pos));
557 if ($rest !== '') $blocks[] = array('type
' => 'html
', 'html
' => $rest);
561 private function segment_content_blocks(array $blocks) {
565 $footer_hr_idx = null;
567 foreach ($blocks as $idx => $block) {
568 if ($hero_idx === null && $this->is_dbx_marker($block, array('hero
'))) {
571 if ($header_idx === null && $this->is_dbx_marker($block, array('header
', 'teaser
', 'thesar
'))) {
574 if ($this->is_dbx_marker($block, array('footer
'))) {
575 if ($footer_idx === null) $footer_idx = $idx;
576 if ($footer_hr_idx === null && $this->is_hr_marker($block)) $footer_hr_idx = $idx;
579 if ($footer_hr_idx !== null) $footer_idx = $footer_hr_idx;
584 'content
' => array(),
588 $has_hero_marker = $hero_idx !== null;
591 if ($hero_idx !== null) $markers[] = array('type
' => 'hero
', 'idx
' => $hero_idx);
592 if ($header_idx !== null) $markers[] = array('type
' => 'header
', 'idx
' => $header_idx);
593 if ($footer_idx !== null) $markers[] = array('type
' => 'footer
', 'idx
' => $footer_idx);
594 usort($markers, function($a, $b) {
595 return (int)$a['idx
'] <=> (int)$b['idx
'];
599 foreach ($markers as $marker) {
600 $idx = (int)$marker['idx
'];
601 $gap = $idx > $pos ? array_slice($blocks, $pos, $idx - $pos) : array();
602 if ($marker['type
'] === 'hero
') {
603 $result['hero
'] = $gap;
607 if ($marker['type
'] === 'header
') {
608 if ($has_hero_marker && $hero_idx < $idx) {
609 $result['header
'] = $gap;
611 $result['content
'] = array_merge($result['content
'], $gap);
616 if ($marker['type
'] === 'footer
') {
617 $result['content
'] = array_merge($result['content
'], $gap);
618 $result['footer
'] = array_slice($blocks, $idx + 1);
624 $result['content
'] = array_merge($result['content
'], array_slice($blocks, $pos));
629 private function split_content_for_columns(array $blocks, $cols) {
630 $cols = max(1, min(3, (int)$cols));
631 $result = array_fill(0, $cols, array());
633 $result[0] = $blocks;
637 $marker_split = $this->split_by_column_markers($blocks, $cols);
638 if ($marker_split !== null) return $marker_split;
640 return $this->split_by_text_weight($blocks, $cols);
643 private function split_by_column_markers(array $blocks, $cols) {
644 $result = array_fill(0, $cols, array());
647 foreach ($blocks as $block) {
648 if ($cols === 2 && $this->is_dbx_marker($block, array('col2
', 'split
'))) {
653 if ($cols === 3 && $this->is_dbx_marker($block, array('col2
', 'col3a
'))) {
658 if ($cols === 3 && $this->is_dbx_marker($block, array('col3
', 'col3b
'))) {
663 $result[$current][] = $block;
665 return $has ? $result : null;
668 private function split_by_text_weight(array $blocks, $cols) {
669 $units = $this->column_units_from_blocks($blocks, $cols);
670 $result = array_fill(0, $cols, array());
671 if (empty($units)) return $result;
673 $parts = $this->partition_column_units($units, $cols);
674 $parts = $this->keep_leading_blanks_with_previous_column($parts);
675 foreach ($parts as $idx => $part) {
676 if ($idx >= $cols) break;
677 $result[$idx] = $this->column_units_to_blocks($part);
682 private function column_units_from_blocks(array $blocks, $cols) {
685 $paragraph_index = 0;
687 foreach ($blocks as $block) {
688 $type = strtolower((string)($block['type
'] ?? 'html
'));
689 if ($type === 'ul
' || $type === 'ol
') {
690 $list_units = $this->list_units_from_block($block, 'list-
' . $list_index, $cols);
692 if ($list_units !== null) {
693 $units = array_merge($units, $list_units);
698 $paragraph_units = $this->paragraph_units_from_block($block, 'p-
' . $paragraph_index, $cols);
700 if ($paragraph_units !== null) {
701 $units = array_merge($units, $paragraph_units);
709 'weight
' => $this->estimate_block_weight($block, $cols),
716 private function paragraph_units_from_block(array $block, $paragraph_key, $cols) {
717 $html = trim((string)($block['html
'] ?? ''));
718 if ($html === '' || $this->is_blank_flow_html($html)) return null;
719 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;
720 if (!preg_match('/^<\s*p\b([^>]*)>([\s\S]*)<\/\s*p\s*>$/i
', $html, $m)) return null;
722 $attrs = (string)$m[1];
723 $chunks = $this->inline_safe_chunks((string)$m[2], $cols);
724 if (count($chunks) < 5) return null;
728 foreach ($chunks as $chunk) {
729 $weight = $this->estimate_inline_chunk_weight($chunk, $cols);
730 $weights[] = $weight;
734 $threshold = max(260, ((int)$cols === 3 ? 240 : 320));
735 if ($total <= $threshold) return null;
738 foreach ($chunks as $idx => $chunk) {
739 if ($chunk === '') continue;
741 'kind
' => 'paragraph_piece
',
742 'paragraph_key
' => $paragraph_key,
745 'weight
' => max(0, (int)($weights[$idx] ?? 0)),
749 return count($units) > 1 ? $units : null;
752 private function inline_safe_chunks($inner_html, $cols) {
753 $tokens = preg_split('/(<[^>]+>)/
', (string)$inner_html, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
754 if (!is_array($tokens) || empty($tokens)) return array();
757 $count = count($tokens);
758 $chunk_limit = $this->column_chars_per_line($cols);
759 for ($i = 0; $i < $count; $i++) {
760 $token = $tokens[$i];
761 if (preg_match('/^<[^>]+>$/
', $token)) {
762 $tag = $this->tag_name($token);
763 if ($tag !== '' && $this->is_opening_tag($token) && !$this->is_void_tag($tag) && !preg_match('/\/\s*>$/
', $token)) {
766 while ($depth > 0 && $i + 1 < $count) {
770 if (!preg_match('/^<[^>]+>$/
', $next)) continue;
771 $next_tag = $this->tag_name($next);
772 if ($next_tag !== $tag) continue;
773 if ($this->is_closing_tag($next)) {
775 } elseif ($this->is_opening_tag($next) && !preg_match('/\/\s*>$/
', $next)) {
786 if (preg_match_all('/\S+\s*/u
', $token, $words)) {
788 foreach ($words[0] as $word) {
789 $candidate = $buffer . $word;
790 if ($buffer !== '' && $this->text_length(strip_tags($candidate)) > $chunk_limit) {
794 $buffer = $candidate;
797 if ($buffer !== '') $chunks[] = $buffer;
804 private function tag_name($tag) {
805 return preg_match('/^<\s*\/?\s*([a-z0-9]+)/i
', (string)$tag, $m) ? strtolower($m[1]) : '';
808 private function is_opening_tag($tag) {
809 return preg_match('/^<\s*[a-z0-9]/i
', (string)$tag) === 1;
812 private function is_closing_tag($tag) {
813 return preg_match('/^<\s*\
816 private function is_void_tag($tag) {
817 return in_array(strtolower((
string)$tag), array(
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'link',
'meta',
'param',
'source',
'track',
'wbr'),
true);
820 private function estimate_inline_chunk_weight($html, $cols) {
821 $html = (string)$html;
822 $text = trim(preg_replace(
'/\s+/',
' ', html_entity_decode(strip_tags($html), ENT_QUOTES,
'UTF-8')));
823 $chars = $this->text_length($text);
824 $weight = (int)ceil(($chars / max(1, $this->column_chars_per_line($cols))) * 100);
825 $weight += substr_count(strtolower($html),
'<br') * 100;
826 return max(1, $weight);
829 private function list_units_from_block(array $block, $list_key, $cols) {
830 $html = trim((
string)($block[
'html'] ??
''));
831 if (!preg_match(
'/^<\s*(ul|ol)\b([^>]*)>([\s\S]*)<\/\s*\1\s*>$/i', $html, $m))
return null;
833 $tag = strtolower($m[1]);
834 $attrs = (string)$m[2];
835 $items = $this->extract_top_level_list_items((
string)$m[3]);
836 if (count($items) < 2)
return null;
839 foreach ($items as $item_html) {
841 'kind' =>
'list_item',
842 'list_key' => $list_key,
845 'html' => $item_html,
846 'weight' => $this->estimate_list_item_weight($item_html, $cols),
852 private function extract_top_level_list_items($inner_html) {
853 $inner_html = (string)$inner_html;
854 if (!preg_match_all(
'/<li\b[^>]*>|<\/li\s*>/i', $inner_html, $matches, PREG_OFFSET_CAPTURE)) {
862 foreach ($matches[0] as $match) {
865 if (preg_match(
'/^<li\b/i', $tag)) {
866 if ($depth === 0) $start = $offset;
871 if ($depth === 0)
continue;
873 if ($depth === 0 && $start !==
null) {
874 $items[] = substr($inner_html, $start, $offset + strlen($tag) - $start);
879 return $depth === 0 ? $items : array();
882 private function estimate_list_item_weight($html, $cols) {
883 $nested_items = max(0, substr_count(strtolower((
string)$html),
'<li') - 1);
884 return max(100, $this->estimate_text_weight((
string)$html, $cols, 1) + ($nested_items * 120));
887 private function partition_column_units(array $units, $cols) {
888 $cols = max(1, min(3, (
int)$cols));
890 if ($cols === 1 || $n === 0)
return array($units);
893 $parts = array_fill(0, $cols, array());
894 foreach ($units as $idx => $unit) $parts[$idx][] = $unit;
899 foreach ($units as $unit) {
900 $prefix[] = end($prefix) + max(0, (
int)($unit[
'weight'] ?? 0));
903 $total = $prefix[$n];
904 $target = $total > 0 ? $total / $cols : 1;
909 for ($c = 0; $c <= $cols; $c++) {
910 $dp[$c] = array_fill(0, $n + 1, $inf);
911 $prev[$c] = array_fill(0, $n + 1, -1);
914 for ($i = 1; $i <= $n; $i++) {
915 $dp[1][$i] = $this->column_partition_cost($prefix, 0, $i, $target);
919 for ($c = 2; $c <= $cols; $c++) {
920 for ($i = $c; $i <= $n; $i++) {
921 for ($j = $c - 1; $j < $i; $j++) {
922 if ($dp[$c - 1][$j] === $inf)
continue;
923 $cost = $dp[$c - 1][$j] + $this->column_partition_cost($prefix, $j, $i, $target);
924 if ($cost < $dp[$c][$i]) {
932 $parts = array_fill(0, $cols, array());
934 for ($c = $cols; $c >= 1; $c--) {
936 if ($j < 0) $j = max(0, $i - 1);
937 $parts[$c - 1] = array_slice($units, $j, $i - $j);
944 private function keep_leading_blanks_with_previous_column(array $parts) {
945 for ($i = 1; $i < count($parts); $i++) {
946 while (!empty($parts[$i]) && $this->is_blank_column_unit($parts[$i][0])) {
947 $parts[$i - 1][] = array_shift($parts[$i]);
953 private function is_blank_column_unit(array $unit) {
954 if (($unit[
'kind'] ??
'') !==
'block' || !isset($unit[
'block']) || !is_array($unit[
'block']))
return false;
955 $block = $unit[
'block'];
956 return strtolower((
string)($block[
'type'] ??
'')) ===
'p' && $this->is_blank_flow_html((
string)($block[
'html'] ??
''));
959 private function column_partition_cost(array $prefix, $start, $end, $target) {
960 $sum = $prefix[$end] - $prefix[$start];
961 $diff = $sum - $target;
962 return $diff * $diff;
965 private function column_units_to_blocks(array $units) {
969 foreach ($units as $unit) {
970 if (($unit[
'kind'] ??
'') ===
'list_item') {
971 $key = (string)($unit[
'list_key'] ??
'');
972 if ($pending ===
null || ($pending[
'kind'] ??
'') !==
'list' || $pending[
'list_key'] !== $key) {
973 $this->flush_pending_column_group($pending, $blocks);
977 'tag' => (
string)($unit[
'tag'] ??
'ul'),
978 'attrs' => (
string)($unit[
'attrs'] ??
''),
982 $pending[
'items'][] = (string)($unit[
'html'] ??
'');
985 if (($unit[
'kind'] ??
'') ===
'paragraph_piece') {
986 $key = (string)($unit[
'paragraph_key'] ??
'');
987 if ($pending ===
null || ($pending[
'kind'] ??
'') !==
'paragraph' || $pending[
'paragraph_key'] !== $key) {
988 $this->flush_pending_column_group($pending, $blocks);
990 'kind' =>
'paragraph',
991 'paragraph_key' => $key,
992 'attrs' => (
string)($unit[
'attrs'] ??
''),
996 $pending[
'pieces'][] = (string)($unit[
'html'] ??
'');
999 $this->flush_pending_column_group($pending, $blocks);
1000 if (isset($unit[
'block']) && is_array($unit[
'block'])) $blocks[] = $unit[
'block'];
1003 $this->flush_pending_column_group($pending, $blocks);
1007 private function flush_pending_column_group(&$pending, array &$blocks) {
1008 if ($pending ===
null)
return;
1009 $kind = (string)($pending[
'kind'] ??
'');
1010 $tag = (string)($pending[
'tag'] ??
'');
1011 if ($kind ===
'list') {
1012 $tag = preg_match(
'/^(ul|ol)$/i', $tag) ? strtolower($tag) :
'ul';
1013 $html =
'<' . $tag . (string)$pending[
'attrs'] .
'>' . implode(
'', $pending[
'items']) .
'</' . $tag .
'>';
1014 } elseif ($kind ===
'paragraph') {
1016 $html =
'<p' . (string)$pending[
'attrs'] .
'>' . implode(
'', $pending[
'pieces']) .
'</p>';
1028 private function estimate_block_weight(array $block, $cols) {
1029 $html = (string)($block[
'html'] ??
'');
1030 $type = strtolower((
string)($block[
'type'] ??
'html'));
1032 if ($type ===
'marker')
return 0;
1033 if ($type ===
'img')
return $this->estimate_media_weight($html, $cols);
1034 if ($type ===
'figure')
return $this->estimate_media_weight($html, $cols) + min(300, $this->estimate_text_weight($html, $cols, 0));
1035 if ($type ===
'table')
return 500 + substr_count(strtolower($html),
'<tr') * 100;
1036 if ($type ===
'ul' || $type ===
'ol')
return max(200, substr_count(strtolower($html),
'<li') * 100 + $this->estimate_text_weight($html, $cols, 0));
1037 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));
1038 if ($type ===
'p' && $this->is_blank_flow_html($html))
return 0;
1039 if (in_array($type, array(
'h1',
'h2',
'h3',
'h4',
'h5',
'h6'),
true))
return max(130, $this->estimate_text_weight($html, $cols, 1) + 60);
1040 if ($type ===
'blockquote')
return max(180, $this->estimate_text_weight($html, $cols, 1) + 80);
1042 return max(60, $this->estimate_text_weight($html, $cols, 1));
1045 private function estimate_text_weight($html, $cols, $min_lines = 0) {
1046 $html = (string)$html;
1047 $text = trim(preg_replace(
'/\s+/',
' ', html_entity_decode(strip_tags($html), ENT_QUOTES,
'UTF-8')));
1048 $chars = $this->text_length($text);
1049 $lines = $chars > 0 ? ($chars / max(1, $this->column_chars_per_line($cols))) : 0;
1050 $lines += substr_count(strtolower($html),
'<br');
1051 $lines = max((
float)$min_lines, $lines);
1052 return (
int)ceil($lines * 100);
1055 private function estimate_media_weight($html, $cols) {
1056 if (!preg_match_all(
'/<(img|video)\b[^>]*>/i', (
string)$html, $matches)) {
1057 return $this->default_media_lines($cols) * 100;
1061 foreach ($matches[0] as $tag) {
1062 $weight += $this->estimate_media_lines_from_tag($tag, $cols) * 100;
1064 return max($this->default_media_lines($cols) * 100, $weight);
1067 private function estimate_media_lines_from_tag($tag, $cols) {
1068 $default = $this->default_media_lines($cols);
1071 if (preg_match(
'/\bwidth=["\']?([0-9.]+)/i', (
string)$tag, $m)) $width = (float)$m[1];
1072 if (preg_match(
'/\bheight=["\']?([0-9.]+)/i', (
string)$tag, $m)) $height = (float)$m[1];
1073 if ($width <= 0 || $height <= 0)
return $default;
1075 $ratio = $height / $width;
1076 $base = ((int)$cols === 3) ? 14 : 19;
1077 return max(8, min(24, (
int)round($ratio *
$base)));
1080 private function default_media_lines($cols) {
1081 return ((
int)$cols === 3) ? 10 : 14;
1084 private function column_chars_per_line($cols) {
1085 if ((
int)$cols === 3)
return 42;
1086 if ((
int)$cols === 2)
return 58;
1090 private function text_length($text) {
1091 $text = (string)$text;
1092 return function_exists(
'mb_strlen') ? mb_strlen($text,
'UTF-8') : strlen($text);
1095 private function is_blank_flow_html($html) {
1096 $html = (string)$html;
1097 if (preg_match(
'/<(img|video|picture|source|iframe|embed|object|svg|canvas)\b/i', $html))
return false;
1098 $html = preg_replace(
'/<\/?p\b[^>]*>/i',
'', $html);
1099 $html = preg_replace(
'/<br\s*\/?>/i',
'', $html);
1100 $html = str_replace(
' ',
'', $html);
1101 return trim(html_entity_decode(strip_tags($html), ENT_QUOTES,
'UTF-8')) ===
'';
1104 private function join_blocks(array $blocks) {
1106 foreach ($blocks as $block) {
1107 if ($this->is_dbx_marker($block, array(
'pagebreak'))) {
1108 $html .=
'<div class="dbx-content-pagebreak" aria-hidden="true"></div>' .
"\n";
1111 if (($block[
'type'] ??
'') ===
'marker')
continue;
1112 $html .= ($block[
'html'] ??
'') .
"\n";
1117 private function is_dbx_marker(array $block, array $names) {
1118 if (($block[
'type'] ??
'') !==
'marker')
return false;
1119 $name = $this->marker_name_from_html((
string)($block[
'html'] ??
''));
1120 return $name !==
'' && in_array($name, $names,
true);
1123 private function is_hr_marker(array $block) {
1124 if (($block[
'type'] ??
'') !==
'marker')
return false;
1125 return preg_match(
'/^\s*<hr\b/i', (
string)($block[
'html'] ??
'')) === 1;
1128 private function marker_name_from_html($html) {
1129 $html = (string)$html;
1130 if (preg_match(
'/<!--\s*dbx:([a-z0-9_-]+)/i', $html, $m))
return strtolower($m[1]);
1131 if (preg_match(
'/\bdata-dbx-marker=["\'](?:dbx:)?([a-z0-9_-]+)["\']/i', $html, $m))
return strtolower($m[1]);
1132 if (preg_match(
'/\bclass=["\'][^"\']*\bdbx-cms-marker-([a-z0-9_-]+)\b/i', $html, $m))
return strtolower($m[1]);
1133 if (preg_match(
'/\bclass=["\'][^"\']*\bdbx_marker_([a-z0-9_-]+)\b/i', $html, $m))
return strtolower($m[1]);
1134 if (preg_match(
'/\bclass=["\'][^"\']*\bdbx_split\b/i', $html))
return 'col2';
1138 private function convert_mod_placeholders($html) {
1139 $html = (string)$html;
1140 if ($html ===
'' || stripos($html,
'dbx-cms-mod-placeholder') ===
false)
return $html;
1142 return preg_replace_callback(
1143 '/<(p|figure|div)\b([^>]*\bdbx-cms-mod-placeholder\b[^>]*)>([\s\S]*?)<\/\1>/i',
1145 $attrs = (string)($m[2] ??
'');
1146 $inner = (string)($m[3] ??
'');
1152 if (preg_match(
'/\bdata-cms-mod-modul=["\']([^"\']*)["\']/i', $attrs, $mm)) {
1153 $modul = trim((
string)$mm[1]);
1155 if (preg_match(
'/\bdata-cms-mod-params=["\']([^"\']*)["\']/i', $attrs, $mm)) {
1156 $params = trim((
string)$mm[1]);
1158 if ($modul ===
'' && preg_match(
'/\bdata-cms-mod-modul=["\']([^"\']*)["\']/i', $inner, $mm)) {
1159 $modul = trim((
string)$mm[1]);
1161 if ($params ===
'' && preg_match(
'/\bdata-cms-mod-params=["\']([^"\']*)["\']/i', $inner, $mm)) {
1162 $params = trim((
string)$mm[1]);
1164 if ($modul ===
'' && preg_match(
'/<img\b[^>]*\bdata-cms-mod-modul=["\']([^"\']*)["\']/i', $inner, $mm)) {
1165 $modul = trim((
string)$mm[1]);
1167 if ($params ===
'' && preg_match(
'/<img\b[^>]*\bdata-cms-mod-params=["\']([^"\']*)["\']/i', $inner, $mm)) {
1168 $params = trim((
string)$mm[1]);
1170 if (preg_match(
'/<img\b[^>]*\bdata-dbx=["\']([^"\']*)["\']/i', $inner, $mm)) {
1171 $data_dbx = trim((
string)$mm[1]);
1173 if (preg_match(
'/<img\b[^>]*\balt=["\']([^"\']*)["\']/i', $inner, $mm)) {
1174 $alt = trim((
string)$mm[1]);
1177 if (($modul ===
'' || $params ===
'') && preg_match(
'/modul_image[^"\']*file=([^"\'&]+)/i', $inner . $attrs, $fileMatch)) {
1178 $base = dirname(__DIR__, 3) .
'/dbxAdmin/include/dbxModuleImages.class.php';
1179 if (is_file(
$base)) {
1181 $resolved = (new \dbx\dbxAdmin\dbxModuleImages())->resolveFromFilename(rawurldecode((
string)$fileMatch[1]));
1182 if (is_array($resolved) && $resolved) {
1183 if ($modul ===
'') {
1184 $modul = (string)($resolved[
'default_modul'] ??
'');
1186 if ($params ===
'') {
1187 $params = (string)($resolved[
'default_params'] ??
'');
1193 $modul = preg_replace(
'/[^A-Za-z0-9_]+/',
'', $modul);
1194 if ($modul ===
'')
return $m[0];
1196 $params = str_replace(array(
"\r",
"\n"),
'', $params);
1197 $marker =
'[modul=' . $modul .
']' . $params .
'[/modul]';
1198 $data_dbx = htmlspecialchars($data_dbx, ENT_QUOTES,
'UTF-8');
1199 $alt_attr = $alt !==
'' ?
' title="' . htmlspecialchars($alt, ENT_QUOTES,
'UTF-8') .
'"' :
'';
1201 if ($data_dbx !==
'') {
1202 return '<div class="dbx-cms-mod-output" data-dbx="' . $data_dbx .
'"' . $alt_attr .
'>' . $marker .
'</div>';
1210 private function strip_dbx_markers($html) {
1211 $html = preg_replace(
'/<!--\s*dbx:[\s\S]*?-->/i',
'', (
string)$html);
1212 $html = preg_replace(
'/<hr\b[^>]*(?:data-dbx-marker|dbx-cms-marker|dbx_marker_|dbx_split)[^>]*>/i',
'', $html);
1216 private function render_inline_media_placeholders(
$db, array $parsed) {
1217 foreach ($parsed as $key => $html) {
1218 if (!is_string($html))
continue;
1219 if (stripos($html,
'dbx-cms-inline-video-block') !==
false) {
1220 $html = $this->render_inline_video_placeholders(
$db, $html);
1222 if (stripos($html,
'dbx_mid=') !==
false || stripos($html,
'data-cms-media-id') !==
false) {
1223 $html = $this->render_inline_image_sanitize(
$db, $html);
1225 $parsed[$key] = $html;
1230 private function render_inline_image_sanitize(
$db, $html) {
1231 return preg_replace_callback(
'/<(img)\b([^>]*?)>/i',
function($m) use (
$db) {
1232 $attrs = (string)($m[2] ??
'');
1234 if (preg_match(
'/data-cms-media-id=["\']?([0-9]+)/i', $attrs, $id_match)) {
1235 $id = (int)$id_match[1];
1236 } elseif (preg_match(
'/dbx_mid=([0-9]+)/i', $attrs, $id_match)) {
1237 $id = (int)$id_match[1];
1239 if ($id <= 0)
return $m[0];
1240 $row =
$db->select1($this->dd_media, $id);
1241 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || !$this->media_file_exists($row))
return '';
1246 private function render_inline_video_placeholders(
$db, $html) {
1247 return preg_replace_callback(
'/<figure\b([^>]*\bdbx-cms-inline-video-block\b[^>]*)>[\s\S]*?<\/figure>/i',
function($m) use (
$db) {
1249 if (!preg_match(
'/data-cms-media-id=["\']?([0-9]+)/i', $attrs, $id_match))
return $m[0];
1250 $id = (int)$id_match[1];
1251 $row =
$db->select1($this->dd_media, $id);
1252 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || !$this->media_file_exists($row))
return '';
1253 return $this->render_inline_video_player($row, $attrs, $m[0]);
1257 private function inline_media_type(array $row) {
1258 if ((
string)($row[
'media_type'] ??
'') ===
'external_video')
return 'external_video';
1259 $mime = (string)($row[
'mime'] ??
'');
1260 if (strpos($mime,
'video/') === 0 || preg_match(
'/\.(mp4|webm|ogv|ogg|mov|m4v)$/i', (
string)($row[
'file_name'] ?? $row[
'file_path'] ??
'')))
return 'video';
1261 if (strtolower((
string)($row[
'storage_type'] ??
'')) ===
'external')
return 'external_video';
1265 private function render_inline_video_player(array $row, $source_attrs =
'', $source_html =
'') {
1266 $id = (int)($row[
'id'] ?? 0);
1267 $type = $this->inline_media_type($row);
1268 if ($id <= 0 || !in_array($type, array(
'video',
'external_video'),
true))
return '';
1270 $source_attrs = (string)$source_attrs;
1271 $source_html = (string)$source_html;
1272 $video_options = $this->inline_video_options_from_html($source_attrs, $source_html);
1273 $style = $video_options[
'style'] !==
'' ?
' style="' . htmlspecialchars($video_options[
'style'], ENT_QUOTES,
'UTF-8') .
'"' :
'';
1274 $autoplay = !empty($video_options[
'autoplay']);
1275 $loop = !empty($video_options[
'loop']);
1276 $muted = !empty($video_options[
'muted']);
1281 $title = htmlspecialchars((
string)($row[
'title'] ?? $row[
'alt'] ?? $row[
'file_name'] ??
'Video'), ENT_QUOTES,
'UTF-8');
1282 $caption = trim((
string)($row[
'caption'] ??
''));
1283 $caption_html = $caption !==
'' ?
'<figcaption>' . htmlspecialchars($caption, ENT_QUOTES,
'UTF-8') .
'</figcaption>' :
'';
1284 $figure_attr =
' class="dbx-content-inline-media dbx-content-inline-video" data-media-id="' . $id .
'"' . $style;
1286 if ($type ===
'external_video') {
1287 $url = (string)($row[
'embed_url'] ?? $row[
'external_url'] ??
'');
1288 if ($url ===
'')
return '';
1289 $url = $this->external_video_option_url($url, $autoplay, $muted, $loop);
1290 $loading = $autoplay ?
'eager' :
'lazy';
1291 if ($this->is_youtube_media($row, $url)) {
1292 return '<figure' . $figure_attr .
'>' . $this->render_youtube_consent_block($url, (
string)($row[
'title'] ?? $row[
'alt'] ?? $row[
'file_name'] ??
'Video'), $loading) . $caption_html .
'</figure>';
1294 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>';
1297 $url =
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $id;
1298 $poster = !empty($row[
'thumb_file_path']) ? $url .
'&dbx_thumb=1' :
'';
1299 $poster_attr = $poster !==
'' ?
' poster="' . htmlspecialchars($poster, ENT_QUOTES,
'UTF-8') .
'"' :
'';
1300 $mime_attr = trim((
string)($row[
'mime'] ??
'')) !==
'' ?
' type="' . htmlspecialchars((
string)$row[
'mime'], ENT_QUOTES,
'UTF-8') .
'"' :
'';
1301 $autoplay_attr = $autoplay ?
' autoplay' :
'';
1302 $loop_attr = $loop ?
' loop' :
'';
1303 $muted_attr = $muted ?
' muted' :
'';
1304 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>';
1307 private function inline_video_options_from_html($source_attrs, $source_html) {
1309 if (preg_match(
'/\sstyle=(["\'])(.*?)\1/i', (
string)$source_attrs, $m)) {
1310 $style = $this->safe_inline_video_style($m[2]);
1313 $width = $this->attr_value_from_html($source_attrs,
'data-cms-video-width');
1314 $height = $this->attr_value_from_html($source_attrs,
'data-cms-video-height');
1315 if ($width ===
'') $width = $this->attr_value_from_html($source_attrs,
'width');
1316 if ($height ===
'') $height = $this->attr_value_from_html($source_attrs,
'height');
1318 if ((
string)$source_html !==
'' && preg_match(
'/<(?:img|video|iframe)\b([^>]*)>/i', (
string)$source_html, $media_match)) {
1319 $media_attrs = $media_match[1];
1320 if ($width ===
'') $width = $this->attr_value_from_html($media_attrs,
'data-cms-video-width');
1321 if ($height ===
'') $height = $this->attr_value_from_html($media_attrs,
'data-cms-video-height');
1322 if ($width ===
'') $width = $this->attr_value_from_html($media_attrs,
'width');
1323 if ($height ===
'') $height = $this->attr_value_from_html($media_attrs,
'height');
1324 if ($style ===
'' && preg_match(
'/\sstyle=(["\'])(.*?)\1/i', $media_attrs, $style_match)) {
1325 $style = $this->safe_inline_video_style($style_match[2]);
1329 $width = $this->css_size_value($width);
1330 $height = $this->css_size_value($height);
1331 $style_parts = $this->style_to_map($style);
1332 if ($width !==
'') $style_parts[
'width'] = $width;
1333 if ($height !==
'') $style_parts[
'height'] = $height;
1335 $autoplay_raw = $this->attr_value_from_html($source_attrs,
'data-cms-video-autoplay');
1336 if ($autoplay_raw ===
'' && (
string)$source_html !==
'' && preg_match(
'/<(?:img|video|iframe)\b([^>]*)>/i', (
string)$source_html, $media_match)) {
1337 $autoplay_raw = $this->attr_value_from_html($media_match[1],
'data-cms-video-autoplay');
1339 $muted_raw = $this->attr_value_from_html($source_attrs,
'data-cms-video-muted');
1340 if ($muted_raw ===
'' && (
string)$source_html !==
'' && preg_match(
'/<(?:img|video|iframe)\b([^>]*)>/i', (
string)$source_html, $media_match)) {
1341 $muted_raw = $this->attr_value_from_html($media_match[1],
'data-cms-video-muted');
1344 $loop_raw = $this->attr_value_from_html($source_attrs,
'data-cms-video-loop');
1345 if ($loop_raw ===
'' && (
string)$source_html !==
'' && preg_match(
'/<(?:img|video|iframe)\b([^>]*)>/i', (
string)$source_html, $media_match)) {
1346 $loop_raw = $this->attr_value_from_html($media_match[1],
'data-cms-video-loop');
1350 'style' => $this->style_map_to_string($style_parts),
1351 'autoplay' => preg_match(
'/^(1|true|yes|ja|on)$/i', trim((
string)$autoplay_raw)) === 1,
1352 'loop' => preg_match(
'/^(1|true|yes|ja|on)$/i', trim((
string)$loop_raw)) === 1,
1353 'muted' => preg_match(
'/^(1|true|yes|ja|on)$/i', trim((
string)$muted_raw)) === 1,
1357 private function attr_value_from_html($attrs, $name) {
1358 $name = preg_quote((
string)$name,
'/');
1359 if (preg_match(
'/\s' . $name .
'\s*=\s*(["\'])(.*?)\1/i', (
string)$attrs, $m))
return trim((
string)$m[2]);
1360 if (preg_match(
'/\s' . $name .
'\s*=\s*([^\s>]+)/i', (
string)$attrs, $m))
return trim((
string)$m[1],
"\"' ");
1364 private function css_size_value($value) {
1365 $value = trim((
string)$value);
1366 if ($value ===
'')
return '';
1367 if (preg_match(
'/^[0-9]+(?:\.[0-9]+)?$/', $value))
return $value .
'px';
1368 if (preg_match(
'/^[0-9]+(?:\.[0-9]+)?(?:px|%|vw|vh|rem|em)$/i', $value))
return $value;
1369 if (strtolower($value) ===
'auto')
return 'auto';
1373 private function safe_inline_video_style($style) {
1375 foreach ($this->style_to_map($style) as $key => $value) {
1376 if (!in_array($key, array(
'width',
'height',
'max-width',
'float',
'margin-left',
'margin-right',
'display'),
true))
continue;
1377 if (in_array($key, array(
'width',
'height',
'max-width',
'margin-left',
'margin-right'),
true)) {
1378 $value = $this->css_size_value($value);
1379 if ($value ===
'')
continue;
1380 } elseif ($key ===
'float') {
1381 if (!in_array(strtolower($value), array(
'left',
'right',
'none'),
true))
continue;
1382 $value = strtolower($value);
1383 } elseif ($key ===
'display') {
1384 if (!in_array(strtolower($value), array(
'block',
'inline-block'),
true))
continue;
1385 $value = strtolower($value);
1387 $out[$key] = $value;
1389 return $this->style_map_to_string($out);
1392 private function style_to_map($style) {
1394 foreach (explode(
';', (
string)$style) as $part) {
1395 if (strpos($part,
':') ===
false)
continue;
1396 list($key, $value) = array_map(
'trim', explode(
':', $part, 2));
1397 $key = strtolower($key);
1398 if ($key ===
'' || $value ===
'')
continue;
1399 $out[$key] = $value;
1404 private function style_map_to_string(array $style) {
1406 foreach ($style as $key => $value) {
1407 if ((
string)$value ===
'')
continue;
1408 $out[] = $key .
': ' . $value;
1410 return implode(
'; ', $out);
1413 private function external_video_option_url($url, $autoplay =
false, $muted =
false, $loop =
false) {
1414 $url = trim((
string)$url);
1415 if ($url ===
'')
return '';
1416 $join = (strpos($url,
'?') ===
false) ?
'?' :
'&';
1417 $params = array(
'playsinline' =>
'1',
'rel' =>
'0');
1418 if ($autoplay) $params[
'autoplay'] =
'1';
1419 $params[
'mute'] = $muted ?
'1' :
'0';
1421 $params[
'loop'] =
'1';
1422 $video_id = $this->youtube_video_id_from_url($url);
1423 if ($video_id !==
'') $params[
'playlist'] = $video_id;
1425 foreach ($params as $key => $value) {
1426 if (preg_match(
'/(?:[?&])' . preg_quote($key,
'/') .
'=/i', $url))
continue;
1427 $url .= $join . rawurlencode($key) .
'=' . rawurlencode($value);
1433 private function youtube_video_id_from_url($url) {
1434 $url = trim((
string)$url);
1435 if ($url ===
'')
return '';
1436 if (preg_match(
'~(?:embed/|v=|youtu\.be/)([A-Za-z0-9_-]{11})~i', $url, $m)) {
1437 return (
string)$m[1];
1442 private function is_youtube_media(array $row, $url) {
1443 $provider = strtolower(trim((
string)($row[
'provider'] ??
'')));
1444 if ($provider ===
'youtube')
return true;
1445 $url = trim((
string)$url);
1446 if ($url !==
'' && preg_match(
'~(?:youtube\.com|youtu\.be)~i', $url))
return true;
1447 $embed = trim((
string)($row[
'embed_url'] ?? $row[
'external_url'] ??
''));
1448 if ($embed !==
'' && preg_match(
'~(?:youtube\.com|youtu\.be)~i', $embed))
return true;
1449 $provider_id = trim((
string)($row[
'provider_id'] ??
''));
1450 return $provider_id !==
'' && preg_match(
'~^[A-Za-z0-9_-]{11}$~', $provider_id) && $provider !==
'vimeo';
1453 private function render_youtube_consent_block($embed_url, $title, $loading =
'lazy') {
1454 $embed_url = trim((
string)$embed_url);
1455 if ($embed_url ===
'')
return '';
1456 $title = htmlspecialchars(trim((
string)$title), ENT_QUOTES,
'UTF-8');
1457 $loading = ($loading ===
'eager') ?
'eager' :
'lazy';
1458 $video_id = $this->youtube_video_id_from_url($embed_url);
1459 $thumb = $video_id !==
'' ?
'https://img.youtube.com/vi/' . $video_id .
'/hqdefault.jpg' :
'';
1460 $thumb_html = $thumb !==
''
1461 ?
'<img class="dbx-youtube-consent-thumb" src="' . htmlspecialchars($thumb, ENT_QUOTES,
'UTF-8') .
'" alt="" loading="lazy">'
1463 return '<div class="dbx-youtube-consent-placeholder"'
1464 .
' data-youtube-embed-url="' . htmlspecialchars($embed_url, ENT_QUOTES,
'UTF-8') .
'"'
1465 .
' data-youtube-title="' . $title .
'"'
1466 .
' data-youtube-loading="' . $loading .
'">'
1468 .
'<button type="button" class="dbx-youtube-consent-play" aria-label="Video abspielen">'
1469 .
'<i class="bi bi-play-fill"></i></button>'
1473 private function render_youtube_hero_figure(array $row, $url, array $settings) {
1474 $url = $this->external_video_option_url($url,
false,
false,
false);
1475 $title = (string)($row[
'title'] ?? $row[
'alt'] ?? $row[
'file_name'] ??
'Video');
1476 $caption = trim((
string)($row[
'caption'] ??
''));
1477 $caption_html = $caption !==
'' ?
'<figcaption>' . htmlspecialchars($caption, ENT_QUOTES,
'UTF-8') .
'</figcaption>' :
'';
1478 $hero_variant = preg_replace(
'/[^a-z0-9_-]+/i',
'-', strtolower((
string)($settings[
'hero_variant'] ??
'original')));
1479 $hero_scroll = preg_replace(
'/[^a-z0-9_-]+/i',
'-', strtolower((
string)($settings[
'hero_scroll_layer'] ??
'under')));
1480 $hero_classes = array(
1483 'dbx-media-external-video',
1485 'dbx-media-hero-' . ($hero_variant !==
'' ? $hero_variant :
'original'),
1486 $this->is_enabled_value($settings[
'hero_sticky'] ??
'0') ?
'dbx-media-hero-sticky' :
'',
1487 'dbx-media-hero-scroll-' . ($hero_scroll !==
'' ? $hero_scroll :
'under'),
1489 $style = $this->css_custom_properties(array(
1490 'cms-hero-margin-top' => $this->css_length_value($settings[
'hero_margin_top'] ??
'0',
'0'),
1491 'cms-hero-height' => $this->css_length_value($settings[
'hero_height'] ??
'auto',
'auto'),
1493 return '<figure class="' . htmlspecialchars(implode(
' ', array_filter($hero_classes)), ENT_QUOTES,
'UTF-8') .
'"'
1494 . ($style !==
'' ?
' style="' . $style .
'"' :
'')
1495 .
'>' . $this->render_youtube_consent_block($url, $title,
'lazy') . $caption_html .
'</figure>';
1498 private function render_media_merge(
$db, $tpl, $cid, array $slots, array $rec, array $settings = array()) {
1501 'media:gallery' =>
'',
1502 'media:header' =>
'',
1503 'media:teaser' =>
'',
1504 'media:footer' =>
'',
1506 'media_gallery' =>
'',
1507 'media_header' =>
'',
1508 'media_teaser' =>
'',
1509 'media_footer' =>
'',
1512 $usage_rows =
$db->select($this->dd_media_usage,
'content_id = ' . (
int)$cid .
' AND active = 1',
'*',
'slot,sorter,id',
'ASC',
'', 0, 0, 0);
1514 if (is_array($usage_rows) && !empty($usage_rows)) {
1515 foreach ($usage_rows as $usage) {
1516 if (!is_array($usage))
continue;
1517 $media_id = (int)($usage[
'media_id'] ?? 0);
1518 if ($media_id <= 0)
continue;
1519 $row =
$db->select1($this->dd_media, $media_id);
1520 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1)
continue;
1521 $row[
'slot'] = (string)($usage[
'slot'] ??
'gallery');
1522 $row[
'sorter'] = (string)($usage[
'sorter'] ??
'');
1523 if (!empty($usage[
'template'])) $row[
'template'] = $usage[
'template'];
1524 if (!empty($usage[
'caption'])) $row[
'caption'] = $usage[
'caption'];
1525 $row[
'usage_id'] = (int)($usage[
'id'] ?? 0);
1529 $rows =
$db->select($this->dd_media,
'content_id = ' . (
int)$cid .
' AND active = 1',
'*',
'slot,sorter,title,id',
'ASC',
'', 0, 0, 0);
1531 if (!is_array($rows))
return $merge;
1534 foreach ($rows as $row) {
1535 if (!is_array($row))
continue;
1536 if (!$this->media_file_exists($row))
continue;
1537 $slot = trim((
string)($row[
'slot'] ??
'gallery')) ?:
'gallery';
1538 if (!isset($by_slot[$slot])) $by_slot[$slot] = array();
1539 $by_slot[$slot][] = $row;
1542 if (!$settings) $settings = $this->content_settings(
$db, $rec);
1543 $needed = array_keys($slots[
'media'] ?? array());
1544 foreach (array(
'hero',
'gallery',
'header',
'teaser',
'footer') as $slot) {
1545 if (!in_array($slot, $needed,
true))
continue;
1546 $rows_for_slot = $by_slot[$slot] ?? array();
1547 if ($slot ===
'hero') {
1548 if ($this->is_no_hero_value($settings[
'hero_template'] ??
'') || $this->is_no_hero_value($settings[
'hero_image_id'] ??
''))
continue;
1549 $rows_for_slot = $this->one_hero_row(
$db, $rows_for_slot, $settings[
'hero_image_id']);
1551 if (empty($rows_for_slot))
continue;
1552 $html = $this->render_media_slot($tpl, $rows_for_slot, $slot, $settings);
1553 $merge[
'media:' . $slot] = $html;
1554 $merge[
'media_' . $slot] = $html;
1559 private function one_hero_row(
$db, array $rows, $hero_id) {
1560 $hero_id = (int)$hero_id;
1562 $row =
$db->select1($this->dd_media, $hero_id);
1563 if (is_array($row) && (
int)($row[
'active'] ?? 0) === 1 && $this->media_file_exists($row))
return array($row);
1565 return isset($rows[0]) ? array($rows[0]) : array();
1568 private function render_media_slot($tpl, array $rows, $slot, array $settings) {
1569 if (!$rows)
return '';
1571 foreach ($rows as $row) {
1572 $mime = (string)($row[
'mime'] ??
'');
1573 $type = ((string)($row[
'media_type'] ??
'') ===
'external_video')
1575 : ((strpos($mime,
'video/') === 0) ?
'video' : ((strpos($mime,
'image/') === 0 || preg_match(
'/\.(jpg|jpeg|png|gif|webp|svg)$/i', (string)($row[
'file_name'] ??
''))) ?
'image' :
'file'));
1576 $template = trim((
string)($row[
'template'] ??
''));
1577 if ($slot ===
'hero' && !empty($settings[
'hero_template'])) $template = $settings[
'hero_template'];
1578 if ($slot ===
'gallery' && !empty($settings[
'gallery_template'])) {
1579 $template = $settings[
'gallery_template'];
1580 if ($type ===
'image' && $template ===
'video-gallery') $template =
'image-gallery';
1581 if (($type ===
'video' || $type ===
'external_video') && in_array($template, array(
'image-gallery',
'carousel3',
'cols3'),
true)) $template = $type .
'-gallery';
1582 if ($type ===
'file' && in_array($template, array(
'image-gallery',
'video-gallery',
'carousel3',
'cols3'),
true)) $template =
'file-gallery';
1584 if ($template ===
'' || strtolower($template) ===
'parent') $template = $type .
'-' . ($slot ===
'gallery' ?
'gallery' : $slot);
1585 if (!$this->media_template_exists($template)) $template = $type .
'-' . ($slot ===
'gallery' ?
'gallery' : $slot);
1586 if (!$this->media_template_exists($template)) $template =
'file-' . ($slot ===
'gallery' ?
'gallery' : $slot);
1588 $url = $type ===
'external_video'
1589 ? (string)($row[
'embed_url'] ?? $row[
'external_url'] ??
'')
1590 :
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . (int)($row[
'id'] ?? 0);
1591 if ($type ===
'external_video' && $slot ===
'hero' && $this->is_youtube_media($row, $url)) {
1592 $html .= $this->render_youtube_hero_figure($row, $url, $settings);
1595 if ($type ===
'external_video') {
1596 $thumb_url = $this->external_video_thumb_url($row);
1597 $poster_url = $thumb_url;
1599 $thumb_url = !empty($row[
'thumb_file_path']) ? $url .
'&dbx_thumb=1' : $url;
1600 $poster_url = !empty($row[
'thumb_file_path']) ? $url .
'&dbx_thumb=1' :
'';
1601 if ($slot ===
'gallery'
1602 && $type ===
'image'
1603 && strtolower(trim((
string)($settings[
'gallery_overflow'] ??
''))) ===
'tutorial') {
1608 'id' => (
string)($row[
'id'] ??
''),
1609 'url' => htmlspecialchars($url, ENT_QUOTES,
'UTF-8'),
1610 'thumb_url' => htmlspecialchars($thumb_url, ENT_QUOTES,
'UTF-8'),
1611 'poster_url' => htmlspecialchars($poster_url, ENT_QUOTES,
'UTF-8'),
1612 'media_type' => htmlspecialchars($type, ENT_QUOTES,
'UTF-8'),
1613 'title' => htmlspecialchars((
string)($row[
'title'] ??
''), ENT_QUOTES,
'UTF-8'),
1614 'alt' => htmlspecialchars((
string)($row[
'alt'] ?? $row[
'title'] ??
''), ENT_QUOTES,
'UTF-8'),
1615 'caption' => htmlspecialchars((
string)($row[
'caption'] ??
''), ENT_QUOTES,
'UTF-8'),
1616 'slot' => htmlspecialchars($slot, ENT_QUOTES,
'UTF-8'),
1617 'mime' => htmlspecialchars($mime, ENT_QUOTES,
'UTF-8'),
1618 'hero_margin_top' => htmlspecialchars($this->css_length_value($settings[
'hero_margin_top'] ??
'0',
'0'), ENT_QUOTES,
'UTF-8'),
1619 'hero_height' => htmlspecialchars($this->css_length_value($settings[
'hero_height'] ??
'auto',
'auto'), ENT_QUOTES,
'UTF-8'),
1620 'hero_variant' => htmlspecialchars((
string)($settings[
'hero_variant'] ??
'original'), ENT_QUOTES,
'UTF-8'),
1621 'hero_sticky_class' => $this->is_enabled_value($settings[
'hero_sticky'] ??
'0') ?
'dbx-media-hero-sticky' :
'',
1622 'hero_scroll_class' =>
'dbx-media-hero-scroll-' . htmlspecialchars((
string)($settings[
'hero_scroll_layer'] ??
'under'), ENT_QUOTES,
'UTF-8'),
1624 $html .= $tpl->get_tpl(
'dbxContent|media-' . $template, $data);
1629 private function external_video_thumb_url(array $row) {
1630 $provider = strtolower(trim((
string)($row[
'provider'] ??
'')));
1631 $provider_id = trim((
string)($row[
'provider_id'] ??
''));
1632 if ($provider ===
'youtube' && preg_match(
'~^[A-Za-z0-9_-]{11}$~', $provider_id)) {
1633 return 'https://img.youtube.com/vi/' . $provider_id .
'/hqdefault.jpg';
1638 private function media_template_exists($template) {
1639 $template = preg_replace(
'/[^a-z0-9_-]+/i',
'', (
string)$template);
1640 if ($template ===
'')
return false;
1641 $path =
dbx()->os_path(
dbx()->get_base_dir() .
'dbx/modules/dbxContent/tpl/htm/media-' . strtolower($template) .
'.htm');
1642 return is_file($path);
1645 private function media_file_exists(array $row) {
1646 if (strtolower((
string)($row[
'storage_type'] ??
'')) ===
'external') {
1647 return trim((
string)($row[
'embed_url'] ?? $row[
'external_url'] ??
'')) !==
'';
1649 $rel = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
1650 if ($rel ===
'' || strpos($rel,
'..') !==
false)
return false;
1652 $root =
dbx()->os_path(rtrim(
dbx()->get_file_dir(),
'/\\') .
'/');
1653 $file =
dbx()->os_path(
$root . $rel);
1656 $real = realpath($file);
1657 return $base && $real && strpos($real,
$base) === 0 && is_file($real) && is_readable($real);
1660 private function replace_content_markers($html, array $merge) {
1661 foreach ($merge as $key => $value) {
1662 if (strpos($key,
':') !==
false) {
1663 $html = str_replace(
'{' . $key .
'}', $value, $html);
1669 private function render_inline($tpl, $slot, $value, $obj_key =
'') {
1670 if (!$this->html_has_visible_content($value))
return '';
1671 $file =
'i-' . $slot;
1672 $data_key = $obj_key !==
'' ? $obj_key : $slot;
1673 $path =
dbx()->os_path(
dbx()->get_base_dir() .
'dbx/modules/dbxContent/tpl/htm/' . strtolower($file) .
'.htm');
1674 if (!is_file($path))
return $value;
1675 $html = $tpl->get_tpl(
'dbxContent|' . $file, array(
'obj:' . $data_key => $value, $data_key => $value));
1676 if (strpos($html,
'not found') !==
false || trim($html) ===
'')
return $value;
1677 $html = str_replace(
'{obj:' . $data_key .
'}', $value, $html);
1681 private function html_has_visible_content($html) {
1682 $html = $this->strip_dbx_markers((
string)$html);
1683 if (trim($html) ===
'')
return false;
1684 if (stripos($html,
'[modul=') !==
false)
return true;
1685 if (preg_match(
'/<(img|video|iframe|source|object|embed|table|ul|ol|li|figure)\b/i', $html))
return true;
1686 $text = preg_replace(
'/<!--[\s\S]*?-->/u',
'', $html);
1687 $text = preg_replace(
'/<br\s*\/?>/iu',
'', $text);
1688 $text = strip_tags($text);
1689 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5,
'UTF-8');
1690 $text = str_replace(
"\xC2\xA0",
' ', $text);
1691 return trim($text) !==
'';
1694 private function render_title_slot($tpl, $template_html, $title) {
1695 $title = htmlspecialchars((
string)$title, ENT_QUOTES,
'UTF-8');
1696 if (preg_match(
'/<h[1-6]\b[^>]*>[^<]*\{cms:title\}/i', (
string)$template_html)) {
1699 return $this->render_inline($tpl,
'title', $title);
1702 private function css_class_token($prefix, $value) {
1703 $prefix = preg_replace(
'/[^a-z0-9_-]+/i',
'-', strtolower((
string)$prefix));
1704 $value = preg_replace(
'/[^a-z0-9_-]+/i',
'-', strtolower((
string)$value));
1705 $value = trim($value,
'-');
1706 if ($value ===
'') $value =
'default';
1707 return trim($prefix .
'-' . $value,
'-');
1710 private function css_style_value($value, $fallback =
'') {
1711 $value = trim((
string)$value);
1712 if ($value ===
'' || strtolower($value) ===
'parent') $value = (string)$fallback;
1713 $value = preg_replace(
'/[;"<>]+/',
'', $value);
1714 $value = preg_replace(
'/[^a-z0-9\s#.,%()+\-\/_]+/i',
'', $value);
1715 return trim($value);
1718 private function css_length_value($value, $fallback =
'') {
1719 $value = $this->css_style_value($value, $fallback);
1720 if ($value ===
'')
return '';
1721 if (preg_match(
'/^-?(?:[0-9]+|[0-9]*\.[0-9]+)$/', $value)) {
1722 return $value ===
'0' ?
'0' : $value .
'px';
1727 private function hero_text_custom_properties($height) {
1728 $scale = $this->hero_text_scale($height);
1730 'cms-hero-text-scale' => $this->css_number($scale),
1731 'cms-hero-text-size' => $this->css_px(18 * $scale),
1732 'cms-hero-title-size' => $this->css_px(46 * $scale),
1733 'cms-hero-subtitle-size' => $this->css_px(30 * $scale),
1734 'cms-hero-text-padding' => $this->css_px(10 * $scale),
1735 'cms-hero-text-offset' => $this->css_px(34 * $scale),
1739 private function hero_text_scale($height) {
1740 $px = $this->css_length_to_px($height);
1741 if ($px <= 0)
return 1.0;
1743 $scale = $px / 420.0;
1744 if ($scale < 0.72) $scale = 0.72;
1745 if ($scale > 1.85) $scale = 1.85;
1749 private function css_length_to_px($value) {
1750 $value = strtolower(trim($this->css_length_value($value,
'auto')));
1751 if ($value ===
'' || $value ===
'auto' || $value ===
'parent')
return 0.0;
1753 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)px$/', $value, $m))
return (
float)$m[1];
1754 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)$/', $value, $m))
return (
float)$m[1];
1755 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)(rem|em)$/', $value, $m))
return (
float)$m[1] * 16.0;
1756 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)(vh|svh|lvh|dvh)$/', $value, $m))
return (
float)$m[1] * 7.2;
1757 if (preg_match(
'/^([0-9]+(?:\.[0-9]+)?)vw$/', $value, $m))
return (
float)$m[1] * 12.8;
1762 private function css_px($value) {
1763 return $this->css_number($value) .
'px';
1766 private function css_number($value) {
1767 $value = (float)$value;
1768 $out = number_format($value, 3,
'.',
'');
1769 return rtrim(rtrim($out,
'0'),
'.');
1772 private function css_custom_properties(array $props) {
1774 foreach ($props as $name => $value) {
1775 $name = preg_replace(
'/[^a-z0-9_-]+/i',
'-', (
string)$name);
1776 $value = trim((
string)$value);
1777 if ($name ===
'' || $value ===
'')
continue;
1778 $out[] =
'--' . strtolower($name) .
':' . htmlspecialchars($value, ENT_QUOTES,
'UTF-8');
1780 return implode(
';', $out);
1783 private function gallery_aspect_ratio($size) {
1784 if (preg_match(
'/^\s*([0-9]{1,5})\s*x\s*([0-9]{1,5})\s*$/i', (
string)$size, $m)) {
1785 $w = max(1, (
int)$m[1]);
1786 $h = max(1, (
int)$m[2]);
1787 return $w .
' / ' . $h;
1792 private function gallery_data_dbx_attrs(array $settings,
bool $has_slot,
bool $has_media): string {
1793 if (!$has_slot || !$has_media) {
1799 'img-count=' . max(1, min(12, (
int)($settings[
'gallery_visible_count'] ?? 3))),
1800 'img-size=' . trim((
string)($settings[
'gallery_image_size'] ??
'original')),
1801 'lightbox-width=' . $this->css_length_value($settings[
'gallery_lightbox_width'] ??
'100vw',
'100vw'),
1802 'overflow=' . trim((
string)($settings[
'gallery_overflow'] ??
'grid')),
1803 'click=' . trim((
string)($settings[
'gallery_click_behavior'] ??
'lightbox')),
1806 return ' data-dbx="' . htmlspecialchars(implode(
'|', $parts), ENT_QUOTES,
'UTF-8') .
'"';
1809 private function seoExcerptFromContent($html, $max = 160) {
1810 $text = trim(preg_replace(
'/\s+/u',
' ', html_entity_decode(strip_tags((
string)$html), ENT_QUOTES,
'UTF-8')));
1811 if ($text ===
'')
return '';
1813 $len = function_exists(
'mb_strlen') ? mb_strlen($text,
'UTF-8') : strlen($text);
1814 if ($len <= $max)
return $text;
1816 $excerpt = function_exists(
'mb_substr') ? mb_substr($text, 0, $max,
'UTF-8') : substr($text, 0, $max);
1817 if (function_exists(
'mb_strrpos')) {
1818 $cut = mb_strrpos($excerpt,
' ', 0,
'UTF-8');
1819 if ($cut !==
false && $cut > 40) {
1820 $excerpt = mb_substr($excerpt, 0, $cut,
'UTF-8');
1822 } elseif (($cut = strrpos($excerpt,
' ')) !==
false && $cut > 40) {
1823 $excerpt = substr($excerpt, 0, $cut);
1826 return rtrim($excerpt,
".,;:- \t") .
'…';
1830 private function seoAbsoluteUrl(
string $path): string {
1831 return rtrim((string)
dbx()->get_base_url(),
'/') .
'/' . ltrim($path,
'/');
1834 private function seoCanonicalUrl($permalink) {
1835 $permalink = trim((
string)$permalink);
1836 if ($permalink !==
'' && preg_match(
'/^https?:\/\//i', $permalink)) {
1839 return $this->seoAbsoluteUrl($permalink);
1842 private function seoOgImageUrl(
$db, array $rec) {
1843 $seo_image_id = (int)($rec[
'seo_image_id'] ?? 0);
1844 if ($seo_image_id > 0) {
1845 $url = $this->seoOgImageFromMediaId(
$db, $seo_image_id);
1846 if ($url !==
'')
return $url;
1849 $settings = $this->content_settings(
$db, $rec);
1850 $hero_id = (int)($settings[
'hero_image_id'] ?? 0);
1851 if ($hero_id <= 0) {
1852 $cid = (int)($rec[
'id'] ?? 0);
1854 $usage_rows =
$db->select($this->dd_media_usage,
'content_id = ' . $cid .
' AND active = 1 AND slot = \'hero\'',
'media_id',
'sorter,id',
'ASC',
'', 1, 0, 0);
1855 if (is_array($usage_rows) && !empty($usage_rows[0][
'media_id'])) {
1856 $hero_id = (int)$usage_rows[0][
'media_id'];
1860 if ($hero_id <= 0)
return '';
1862 $row =
$db->select1($this->dd_media, $hero_id);
1863 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || !$this->media_file_exists($row)) {
1867 $mime = (string)($row[
'mime'] ??
'');
1868 $file_name = (string)($row[
'file_name'] ?? $row[
'file_path'] ??
'');
1869 if (strpos($mime,
'image/') !== 0 && !preg_match(
'/\.(jpg|jpeg|png|gif|webp|svg)$/i', $file_name)) {
1873 return $this->seoAbsoluteUrl(
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $hero_id);
1876 private function seoOgImageFromMediaId(
$db,
int $media_id) {
1877 $media_id = (int)$media_id;
1878 if ($media_id <= 0)
return '';
1880 $row =
$db->select1($this->dd_media, $media_id);
1881 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || !$this->media_file_exists($row)) {
1885 $mime = (string)($row[
'mime'] ??
'');
1886 $file_name = (string)($row[
'file_name'] ?? $row[
'file_path'] ??
'');
1887 if (strpos($mime,
'image/') !== 0 && !preg_match(
'/\.(jpg|jpeg|png|gif|webp|svg)$/i', $file_name)) {
1891 return $this->seoAbsoluteUrl(
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $media_id);
1894 private function seoHreflangBlock(
$db, array $rec,
string $currentLng): string {
1895 $lngUid = trim((string)($rec[
'lng_uid'] ??
''));
1896 if ($lngUid ===
'' || !function_exists(
'dbx_accessible_lngs')) {
1900 $lngs = dbx_accessible_lngs();
1901 if (!is_array($lngs) || count($lngs) < 2) {
1905 $alternates = array();
1906 $escapedUid = str_replace(
"'",
"''", $lngUid);
1908 foreach ($lngs as $lng) {
1909 $lng = strtolower(trim((
string)$lng));
1910 if ($lng ===
'' || !preg_match(
'/^[a-z]{2,3}$/', $lng))
continue;
1912 $permalink = $this->seoHreflangPermalink(
$db, $lng, $lngUid, $escapedUid, $rec, $currentLng);
1913 if ($permalink ===
'')
continue;
1915 $alternates[] = array(
1917 'url' => $this->seoCanonicalUrl($permalink),
1921 if (count($alternates) < 2) {
1926 $defaultLng = strtolower(trim((
string)
dbx()->get_config(
'dbx',
'default_lng',
'de')));
1927 if ($defaultLng ===
'' || $defaultLng ===
'undef') $defaultLng =
'de';
1929 foreach ($alternates as $alt) {
1930 $lines[] =
'<link rel="alternate" hreflang="' . htmlspecialchars((
string)$alt[
'lng'], ENT_QUOTES,
'UTF-8') .
'" href="' . htmlspecialchars((
string)$alt[
'url'], ENT_QUOTES,
'UTF-8') .
'">';
1933 foreach ($alternates as $alt) {
1934 if ((
string)$alt[
'lng'] === $defaultLng) {
1935 $lines[] =
'<link rel="alternate" hreflang="x-default" href="' . htmlspecialchars((
string)$alt[
'url'], ENT_QUOTES,
'UTF-8') .
'">';
1940 return count($lines) ?
"\n " . implode(
"\n ", $lines) :
'';
1943 private function seoHreflangPermalink(
$db,
string $lng,
string $lngUid,
string $escapedUid, array $rec,
string $currentLng): string {
1944 if ($lng === $currentLng) {
1945 return trim((
string)($rec[
'permalink'] ??
''));
1948 if (dbxContentPageCache::isConfigEnabled()) {
1949 $index = dbxContentPermalinkIndex::load($lng);
1950 foreach (
$index as $permalink => $row) {
1951 if (!is_array($row) || (
int)($row[
'activ'] ?? 0) !== 1)
continue;
1952 $cid = (int)($row[
'cid'] ?? 0);
1953 if ($cid <= 0)
continue;
1954 $sibling =
$db->select1(dbxContentLng::ddContent($lng), $cid,
'lng_uid,permalink,activ', 0);
1955 if (!is_array($sibling) || (
int)($sibling[
'activ'] ?? 0) !== 1)
continue;
1956 if (trim((
string)($sibling[
'lng_uid'] ??
'')) !== $lngUid)
continue;
1957 return trim((
string)($sibling[
'permalink'] ?? $permalink));
1961 $sibling =
$db->select1(
1962 dbxContentLng::ddContent($lng),
1963 "lng_uid = '" . $escapedUid .
"' AND activ = 1",
1967 if (!is_array($sibling)) {
1971 return trim((
string)($sibling[
'permalink'] ??
''));
1974 private function seoJsonLd(array $rec,
string $title,
string $description,
string $canonical,
string $lng): string {
1975 if ($title ===
'' && $description ===
'' && $canonical ===
'') {
1980 '@context' =>
'https://schema.org',
1981 '@type' =>
'WebPage',
1983 'description' => $description,
1984 'url' => $canonical,
1985 'inLanguage' => $lng,
1988 $modified = trim((
string)($rec[
'update_date'] ??
''));
1989 if ($modified !==
'') {
1990 $ts = strtotime($modified);
1991 if ($ts !==
false) {
1992 $data[
'dateModified'] = gmdate(
'c', $ts);
1996 $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1997 if (!is_string($json) || $json ===
'') {
2001 return '<script type="application/ld+json">' . $json .
'</script>';
2004 private function cleanup_empty_sections($html) {
2005 $html = preg_replace(
'/<section\b[^>]*class="[^"]*\bcms-header\b[^"]*"[^>]*>\s*<\/section>/i',
'', (
string)$html);
2006 $html = preg_replace(
'/<section\b[^>]*class="[^"]*\bgallery\b[^"]*\bno-gallery\b[^"]*"[^>]*>[\s\S]*?<\/section>/i',
'', (
string)$html);
2007 $html = preg_replace(
'/<section\b([^>]*class="[^"]*\bdbx-content-(?:gallery-section|teaser-section|header-text)\b[^"]*"[^>]*)>\s*<\/section>/i',
'', (
string)$html);
2008 $html = preg_replace(
'/<footer\b([^>]*class="[^"]*\bdbx-content-footer\b[^"]*"[^>]*)>\s*<\/footer>/i',
'', $html);
2009 $html = preg_replace(
'/<div\b([^>]*class="[^"]*\bdbx-content-teaser\b[^"]*"[^>]*)>\s*<\/div>/i',
'', $html);
2010 $html = preg_replace(
'/<section\b([^>]*class="[^"]*\bdbx-content-hero\b[^"]*"[^>]*)>\s*<\/section>/i',
'', $html);
2011 $html = preg_replace(
'/<div\b([^>]*class="[^"]*\bhero-text\b[^"]*"[^>]*)>\s*<\/div>/i',
'', $html);