dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
dbxContentRenderer.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxContent;
3
4require_once __DIR__ . '/dbxContentLng.class.php';
5
7
8 private $dd_media = 'dbxMedia';
9 private $dd_media_usage = 'dbxMediaUsage';
10
11 public function render($cid) {
12 return $this->interpretContentModules($this->renderStatic($cid));
13 }
14
15 public function renderStatic($cid, array $options = array()) {
16 $cid = (int)$cid;
17 if ($cid <= 0) return 'Keine dbx_cid gesetzt!';
18
19 $db = dbx()->get_system_obj('dbxDB');
20 $tpl = dbx()->get_system_obj('dbxTPL');
21 $rec = $db->select1(dbxContentLng::ddContent(), $cid, '*', 0);
22 if (!is_array($rec) || (int)($rec['id'] ?? 0) <= 0) {
23 return $tpl->get_tpl('dbxContent|no-page');
24 }
25
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>';
30 }
31 }
32
33 if (empty($options['skip_hits'])) {
34 $this->update_hits($db, $rec);
35 }
36
37 $forceTemplate = trim((string)($options['template'] ?? ''));
38 if ($forceTemplate !== '') {
39 $template = $this->normalize_content_template($forceTemplate);
40 } else {
41 $template = $this->normalize_content_template($this->resolve_content_setting($db, $rec, 'template', 'template', 'c-content'));
42 }
43
44 $this->applySeoMeta($cid, $rec);
45
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)));
52
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'),
59 );
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'),
67 );
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'),
76 ));
77
78 $merge = array(
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',
114 'cms:hero' => '',
115 'cms: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'),
120 );
121
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,
140 ));
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;
154 }
155
156 private function decorate_brand_text(string $html): string {
157 if ($html === '' || strpos($html, 'dbXapp') === false) {
158 return $html;
159 }
160
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);
163 });
164 }
165
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);
170 }
171
172 foreach ($parts as $idx => $part) {
173 if ($part === '' || $part[0] === '<') {
174 continue;
175 }
176 $parts[$idx] = (string)$callback($part);
177 }
178
179 return implode('', $parts);
180 }
181
182 public function applySeoMeta(int $cid, array $rec = null, array $meta = null) {
183 $cid = (int)$cid;
184 if ($cid <= 0) return;
185
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']);
190 } else {
191 $rec = $db->select1(dbxContentLng::ddContent(), $cid, '*', 0);
192 if (!is_array($rec) || (int)($rec['id'] ?? 0) <= 0) return;
193 }
194 }
195
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);
200
201 $description = trim((string)($rec['description'] ?? ''));
202 if ($description === '') {
203 $description = $this->seoExcerptFromContent((string)($rec['content'] ?? ''));
204 }
205
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';
214 }
215
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';
220
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));
231 }
232
233 public static function seoMetaFromRecord(array $rec): array {
234 return array(
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'] ?? ''),
245 );
246 }
247
248 public static function resetSeoMeta() {
249 foreach (array(
250 'dbx_meta_description',
251 'dbx_meta_keywords',
252 'dbx_canonical',
253 'dbx_robots',
254 'dbx_og_title',
255 'dbx_og_description',
256 'dbx_og_url',
257 'dbx_og_image',
258 'dbx_hreflang',
259 'dbx_json_ld',
260 ) as $key) {
261 dbx()->set_system_var($key, '');
262 }
263 }
264
265 public function interpretContentModules($html) {
266 return $this->interpret_content_modules($html);
267 }
268
269 private function interpret_content_modules($html) {
270 $html = (string)$html;
271 if ($html === '' || stripos($html, '[modul=') === false) {
272 return $html;
273 }
274 $interpreter = dbx()->get_system_obj('dbxInterpreter');
275 if (!is_object($interpreter) || !method_exists($interpreter, 'run')) {
276 return $html;
277 }
278 return $interpreter->run($html);
279 }
280
281 public function getPublicFolderRights(int $folder_id): string {
282 $db = dbx()->get_system_obj('dbxDB');
283 return $this->resolve_folder_rights($db, $folder_id);
284 }
285
286 private function resolve_content_rights($db, array $rec): string {
287 // Content-Seiten besitzen keine eigenen Leserechte. Ausschliesslich die
288 // effektiven, gegebenenfalls vererbten Rechte ihres Ordners entscheiden.
289 return $this->resolve_folder_rights($db, (int)($rec['folder'] ?? 0));
290 }
291
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;
297
298 $rec = $db->select1(dbxContentLng::ddFolder(), $folder_id, '*', 0);
299 if (!is_array($rec) || (int)($rec['id'] ?? 0) <= 0) return '*';
300
301 $parent_id = (int)($rec['parent_id'] ?? 0);
302 $raw = trim((string)($rec['group_read'] ?? ''));
303 if ($raw === '') return '*';
304
305 $parts = preg_split('/\s*,\s*/', $raw, -1, PREG_SPLIT_NO_EMPTY);
306 $out = array();
307 $uses_parent = false;
308
309 foreach ($parts as $part) {
310 $part = trim((string)$part);
311 if ($part === '') continue;
312 if (strtolower($part) === 'parent') {
313 $uses_parent = true;
314 continue;
315 }
316 $out[$part] = 1;
317 }
318
319 if ($uses_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;
324 }
325 }
326
327 if (!count($out)) return '*';
328 return implode(',', array_keys($out));
329 }
330
331 private function is_parent_value($value) {
332 $value = trim((string)$value);
333 return $value === '' || strtolower($value) === 'parent';
334 }
335
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);
339 }
340
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);
344 }
345
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;
350
351 $rec = $db->select1(dbxContentLng::ddFolder(), $folder_id, '*', 0);
352 if (!is_array($rec) || (int)($rec['id'] ?? 0) <= 0) return $fallback;
353
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);
357 }
358
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);
363 }
364
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;
368 }
369
370 private function content_settings($db, array $rec) {
371 return array(
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'),
385 );
386 }
387
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);
392 }
393
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);
403 }
404 return 'c-content';
405 }
406
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);
412 }
413
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;
418
419 $aliases = array(
420 'c-hero_header-gallery-body3-footer' => 'c-title-hero_header-gallery-body3-footer',
421 );
422 if (isset($aliases[$template]) && $this->content_template_exists($aliases[$template])) {
423 return $aliases[$template];
424 }
425
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;
431
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';
434 }
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';
439 return 'c-content';
440 }
441
442 private function detect_template_slots($template_html) {
443 $template_html = (string)$template_html;
444 $slots = array(
445 'content' => array(),
446 'media' => array(),
447 'cols' => 1,
448 'cms' => array(),
449 );
450
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;
454 }
455 }
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;
459 }
460 }
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;
472 }
473 }
474
475 if (strpos($template_html, '{content}') !== false || strpos($template_html, '{content:body}') !== false) {
476 $slots['content']['content'] = 1;
477 }
478
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;
483
484 return $slots;
485 }
486
487 private function parse_content($html, array $slots) {
488 $blocks = $this->html_blocks($html);
489 $segments = $this->segment_content_blocks($blocks);
490
491 $hero_blocks = $segments['hero'];
492 $header_blocks = $segments['header'];
493 $footer_blocks = $segments['footer'];
494 $body_blocks = $segments['content'];
495
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']);
500
501 if (!$has_hero_text_slot) {
502 $body_blocks = array_merge($hero_blocks, $body_blocks);
503 $hero_blocks = array();
504 }
505 if (!$has_header_slot) {
506 $body_blocks = array_merge($header_blocks, $body_blocks);
507 $header_blocks = array();
508 }
509 if (!$has_footer_slot) {
510 $body_blocks = array_merge($body_blocks, $footer_blocks);
511 $footer_blocks = array();
512 }
513
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));
518
519 return array(
520 'h1' => '',
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())),
530 );
531 }
532
533 private function html_blocks($html) {
534 $html = trim((string)$html);
535 if ($html === '') return array();
536
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);
539
540 $blocks = array();
541 $pos = 0;
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);
545
546 $chunk = $match[0];
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';
551
552 $blocks[] = array('type' => $type, 'html' => $chunk);
553 $pos = $match[1] + strlen($chunk);
554 }
555
556 $rest = trim(substr($html, $pos));
557 if ($rest !== '') $blocks[] = array('type' => 'html', 'html' => $rest);
558 return $blocks;
559 }
560
561 private function segment_content_blocks(array $blocks) {
562 $hero_idx = null;
563 $header_idx = null;
564 $footer_idx = null;
565 $footer_hr_idx = null;
566
567 foreach ($blocks as $idx => $block) {
568 if ($hero_idx === null && $this->is_dbx_marker($block, array('hero'))) {
569 $hero_idx = $idx;
570 }
571 if ($header_idx === null && $this->is_dbx_marker($block, array('header', 'teaser', 'thesar'))) {
572 $header_idx = $idx;
573 }
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;
577 }
578 }
579 if ($footer_hr_idx !== null) $footer_idx = $footer_hr_idx;
580
581 $result = array(
582 'hero' => array(),
583 'header' => array(),
584 'content' => array(),
585 'footer' => array(),
586 );
587 $n = count($blocks);
588 $has_hero_marker = $hero_idx !== null;
589
590 $markers = array();
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'];
596 });
597
598 $pos = 0;
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;
604 $pos = $idx + 1;
605 continue;
606 }
607 if ($marker['type'] === 'header') {
608 if ($has_hero_marker && $hero_idx < $idx) {
609 $result['header'] = $gap;
610 } else {
611 $result['content'] = array_merge($result['content'], $gap);
612 }
613 $pos = $idx + 1;
614 continue;
615 }
616 if ($marker['type'] === 'footer') {
617 $result['content'] = array_merge($result['content'], $gap);
618 $result['footer'] = array_slice($blocks, $idx + 1);
619 return $result;
620 }
621 }
622
623 if ($pos < $n) {
624 $result['content'] = array_merge($result['content'], array_slice($blocks, $pos));
625 }
626 return $result;
627 }
628
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());
632 if ($cols === 1) {
633 $result[0] = $blocks;
634 return $result;
635 }
636
637 $marker_split = $this->split_by_column_markers($blocks, $cols);
638 if ($marker_split !== null) return $marker_split;
639
640 return $this->split_by_text_weight($blocks, $cols);
641 }
642
643 private function split_by_column_markers(array $blocks, $cols) {
644 $result = array_fill(0, $cols, array());
645 $current = 0;
646 $has = false;
647 foreach ($blocks as $block) {
648 if ($cols === 2 && $this->is_dbx_marker($block, array('col2', 'split'))) {
649 $has = true;
650 $current = 1;
651 continue;
652 }
653 if ($cols === 3 && $this->is_dbx_marker($block, array('col2', 'col3a'))) {
654 $has = true;
655 $current = 1;
656 continue;
657 }
658 if ($cols === 3 && $this->is_dbx_marker($block, array('col3', 'col3b'))) {
659 $has = true;
660 $current = 2;
661 continue;
662 }
663 $result[$current][] = $block;
664 }
665 return $has ? $result : null;
666 }
667
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;
672
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);
678 }
679 return $result;
680 }
681
682 private function column_units_from_blocks(array $blocks, $cols) {
683 $units = array();
684 $list_index = 0;
685 $paragraph_index = 0;
686
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);
691 $list_index++;
692 if ($list_units !== null) {
693 $units = array_merge($units, $list_units);
694 continue;
695 }
696 }
697 if ($type === 'p') {
698 $paragraph_units = $this->paragraph_units_from_block($block, 'p-' . $paragraph_index, $cols);
699 $paragraph_index++;
700 if ($paragraph_units !== null) {
701 $units = array_merge($units, $paragraph_units);
702 continue;
703 }
704 }
705
706 $units[] = array(
707 'kind' => 'block',
708 'block' => $block,
709 'weight' => $this->estimate_block_weight($block, $cols),
710 );
711 }
712
713 return $units;
714 }
715
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;
721
722 $attrs = (string)$m[1];
723 $chunks = $this->inline_safe_chunks((string)$m[2], $cols);
724 if (count($chunks) < 5) return null;
725
726 $weights = array();
727 $total = 0;
728 foreach ($chunks as $chunk) {
729 $weight = $this->estimate_inline_chunk_weight($chunk, $cols);
730 $weights[] = $weight;
731 $total += $weight;
732 }
733
734 $threshold = max(260, ((int)$cols === 3 ? 240 : 320));
735 if ($total <= $threshold) return null;
736
737 $units = array();
738 foreach ($chunks as $idx => $chunk) {
739 if ($chunk === '') continue;
740 $units[] = array(
741 'kind' => 'paragraph_piece',
742 'paragraph_key' => $paragraph_key,
743 'attrs' => $attrs,
744 'html' => $chunk,
745 'weight' => max(0, (int)($weights[$idx] ?? 0)),
746 );
747 }
748
749 return count($units) > 1 ? $units : null;
750 }
751
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();
755
756 $chunks = 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)) {
764 $chunk = $token;
765 $depth = 1;
766 while ($depth > 0 && $i + 1 < $count) {
767 $i++;
768 $next = $tokens[$i];
769 $chunk .= $next;
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)) {
774 $depth--;
775 } elseif ($this->is_opening_tag($next) && !preg_match('/\/\s*>$/', $next)) {
776 $depth++;
777 }
778 }
779 $chunks[] = $chunk;
780 continue;
781 }
782 $chunks[] = $token;
783 continue;
784 }
785
786 if (preg_match_all('/\S+\s*/u', $token, $words)) {
787 $buffer = '';
788 foreach ($words[0] as $word) {
789 $candidate = $buffer . $word;
790 if ($buffer !== '' && $this->text_length(strip_tags($candidate)) > $chunk_limit) {
791 $chunks[] = $buffer;
792 $buffer = $word;
793 } else {
794 $buffer = $candidate;
795 }
796 }
797 if ($buffer !== '') $chunks[] = $buffer;
798 }
799 }
800
801 return $chunks;
802 }
803
804 private function tag_name($tag) {
805 return preg_match('/^<\s*\/?\s*([a-z0-9]+)/i', (string)$tag, $m) ? strtolower($m[1]) : '';
806 }
807
808 private function is_opening_tag($tag) {
809 return preg_match('/^<\s*[a-z0-9]/i', (string)$tag) === 1;
810 }
811
812 private function is_closing_tag($tag) {
813 return preg_match('/^<\s*\//', (string)$tag) === 1;
814 }
815
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);
818 }
819
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);
827 }
828
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;
832
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;
837
838 $units = array();
839 foreach ($items as $item_html) {
840 $units[] = array(
841 'kind' => 'list_item',
842 'list_key' => $list_key,
843 'tag' => $tag,
844 'attrs' => $attrs,
845 'html' => $item_html,
846 'weight' => $this->estimate_list_item_weight($item_html, $cols),
847 );
848 }
849 return $units;
850 }
851
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)) {
855 return array();
856 }
857
858 $items = array();
859 $depth = 0;
860 $start = null;
861
862 foreach ($matches[0] as $match) {
863 $tag = $match[0];
864 $offset = $match[1];
865 if (preg_match('/^<li\b/i', $tag)) {
866 if ($depth === 0) $start = $offset;
867 $depth++;
868 continue;
869 }
870
871 if ($depth === 0) continue;
872 $depth--;
873 if ($depth === 0 && $start !== null) {
874 $items[] = substr($inner_html, $start, $offset + strlen($tag) - $start);
875 $start = null;
876 }
877 }
878
879 return $depth === 0 ? $items : array();
880 }
881
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));
885 }
886
887 private function partition_column_units(array $units, $cols) {
888 $cols = max(1, min(3, (int)$cols));
889 $n = count($units);
890 if ($cols === 1 || $n === 0) return array($units);
891
892 if ($n < $cols) {
893 $parts = array_fill(0, $cols, array());
894 foreach ($units as $idx => $unit) $parts[$idx][] = $unit;
895 return $parts;
896 }
897
898 $prefix = array(0);
899 foreach ($units as $unit) {
900 $prefix[] = end($prefix) + max(0, (int)($unit['weight'] ?? 0));
901 }
902
903 $total = $prefix[$n];
904 $target = $total > 0 ? $total / $cols : 1;
905 $inf = PHP_INT_MAX;
906 $dp = array();
907 $prev = array();
908
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);
912 }
913
914 for ($i = 1; $i <= $n; $i++) {
915 $dp[1][$i] = $this->column_partition_cost($prefix, 0, $i, $target);
916 $prev[1][$i] = 0;
917 }
918
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]) {
925 $dp[$c][$i] = $cost;
926 $prev[$c][$i] = $j;
927 }
928 }
929 }
930 }
931
932 $parts = array_fill(0, $cols, array());
933 $i = $n;
934 for ($c = $cols; $c >= 1; $c--) {
935 $j = $prev[$c][$i];
936 if ($j < 0) $j = max(0, $i - 1);
937 $parts[$c - 1] = array_slice($units, $j, $i - $j);
938 $i = $j;
939 }
940
941 return $parts;
942 }
943
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]);
948 }
949 }
950 return $parts;
951 }
952
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'] ?? ''));
957 }
958
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;
963 }
964
965 private function column_units_to_blocks(array $units) {
966 $blocks = array();
967 $pending = null;
968
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);
974 $pending = array(
975 'kind' => 'list',
976 'list_key' => $key,
977 'tag' => (string)($unit['tag'] ?? 'ul'),
978 'attrs' => (string)($unit['attrs'] ?? ''),
979 'items' => array(),
980 );
981 }
982 $pending['items'][] = (string)($unit['html'] ?? '');
983 continue;
984 }
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);
989 $pending = array(
990 'kind' => 'paragraph',
991 'paragraph_key' => $key,
992 'attrs' => (string)($unit['attrs'] ?? ''),
993 'pieces' => array(),
994 );
995 }
996 $pending['pieces'][] = (string)($unit['html'] ?? '');
997 continue;
998 }
999 $this->flush_pending_column_group($pending, $blocks);
1000 if (isset($unit['block']) && is_array($unit['block'])) $blocks[] = $unit['block'];
1001 }
1002
1003 $this->flush_pending_column_group($pending, $blocks);
1004 return $blocks;
1005 }
1006
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') {
1015 $tag = 'p';
1016 $html = '<p' . (string)$pending['attrs'] . '>' . implode('', $pending['pieces']) . '</p>';
1017 } else {
1018 $pending = null;
1019 return;
1020 }
1021 $blocks[] = array(
1022 'type' => $tag,
1023 'html' => $html,
1024 );
1025 $pending = null;
1026 }
1027
1028 private function estimate_block_weight(array $block, $cols) {
1029 $html = (string)($block['html'] ?? '');
1030 $type = strtolower((string)($block['type'] ?? 'html'));
1031
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);
1041
1042 return max(60, $this->estimate_text_weight($html, $cols, 1));
1043 }
1044
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);
1053 }
1054
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;
1058 }
1059
1060 $weight = 0;
1061 foreach ($matches[0] as $tag) {
1062 $weight += $this->estimate_media_lines_from_tag($tag, $cols) * 100;
1063 }
1064 return max($this->default_media_lines($cols) * 100, $weight);
1065 }
1066
1067 private function estimate_media_lines_from_tag($tag, $cols) {
1068 $default = $this->default_media_lines($cols);
1069 $width = 0;
1070 $height = 0;
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;
1074
1075 $ratio = $height / $width;
1076 $base = ((int)$cols === 3) ? 14 : 19;
1077 return max(8, min(24, (int)round($ratio * $base)));
1078 }
1079
1080 private function default_media_lines($cols) {
1081 return ((int)$cols === 3) ? 10 : 14;
1082 }
1083
1084 private function column_chars_per_line($cols) {
1085 if ((int)$cols === 3) return 42;
1086 if ((int)$cols === 2) return 58;
1087 return 78;
1088 }
1089
1090 private function text_length($text) {
1091 $text = (string)$text;
1092 return function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text);
1093 }
1094
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('&nbsp;', '', $html);
1101 return trim(html_entity_decode(strip_tags($html), ENT_QUOTES, 'UTF-8')) === '';
1102 }
1103
1104 private function join_blocks(array $blocks) {
1105 $html = '';
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";
1109 continue;
1110 }
1111 if (($block['type'] ?? '') === 'marker') continue;
1112 $html .= ($block['html'] ?? '') . "\n";
1113 }
1114 return trim($html);
1115 }
1116
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);
1121 }
1122
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;
1126 }
1127
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';
1135 return '';
1136 }
1137
1138 private function convert_mod_placeholders($html) {
1139 $html = (string)$html;
1140 if ($html === '' || stripos($html, 'dbx-cms-mod-placeholder') === false) return $html;
1141
1142 return preg_replace_callback(
1143 '/<(p|figure|div)\b([^>]*\bdbx-cms-mod-placeholder\b[^>]*)>([\s\S]*?)<\/\1>/i',
1144 function($m) {
1145 $attrs = (string)($m[2] ?? '');
1146 $inner = (string)($m[3] ?? '');
1147 $modul = '';
1148 $params = '';
1149 $data_dbx = '';
1150 $alt = '';
1151
1152 if (preg_match('/\bdata-cms-mod-modul=["\']([^"\']*)["\']/i', $attrs, $mm)) {
1153 $modul = trim((string)$mm[1]);
1154 }
1155 if (preg_match('/\bdata-cms-mod-params=["\']([^"\']*)["\']/i', $attrs, $mm)) {
1156 $params = trim((string)$mm[1]);
1157 }
1158 if ($modul === '' && preg_match('/\bdata-cms-mod-modul=["\']([^"\']*)["\']/i', $inner, $mm)) {
1159 $modul = trim((string)$mm[1]);
1160 }
1161 if ($params === '' && preg_match('/\bdata-cms-mod-params=["\']([^"\']*)["\']/i', $inner, $mm)) {
1162 $params = trim((string)$mm[1]);
1163 }
1164 if ($modul === '' && preg_match('/<img\b[^>]*\bdata-cms-mod-modul=["\']([^"\']*)["\']/i', $inner, $mm)) {
1165 $modul = trim((string)$mm[1]);
1166 }
1167 if ($params === '' && preg_match('/<img\b[^>]*\bdata-cms-mod-params=["\']([^"\']*)["\']/i', $inner, $mm)) {
1168 $params = trim((string)$mm[1]);
1169 }
1170 if (preg_match('/<img\b[^>]*\bdata-dbx=["\']([^"\']*)["\']/i', $inner, $mm)) {
1171 $data_dbx = trim((string)$mm[1]);
1172 }
1173 if (preg_match('/<img\b[^>]*\balt=["\']([^"\']*)["\']/i', $inner, $mm)) {
1174 $alt = trim((string)$mm[1]);
1175 }
1176
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)) {
1180 require_once $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'] ?? '');
1185 }
1186 if ($params === '') {
1187 $params = (string)($resolved['default_params'] ?? '');
1188 }
1189 }
1190 }
1191 }
1192
1193 $modul = preg_replace('/[^A-Za-z0-9_]+/', '', $modul);
1194 if ($modul === '') return $m[0];
1195
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') . '"' : '';
1200
1201 if ($data_dbx !== '') {
1202 return '<div class="dbx-cms-mod-output" data-dbx="' . $data_dbx . '"' . $alt_attr . '>' . $marker . '</div>';
1203 }
1204 return $marker;
1205 },
1206 $html
1207 );
1208 }
1209
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);
1213 return trim($html);
1214 }
1215
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);
1221 }
1222 if (stripos($html, 'dbx_mid=') !== false || stripos($html, 'data-cms-media-id') !== false) {
1223 $html = $this->render_inline_image_sanitize($db, $html);
1224 }
1225 $parsed[$key] = $html;
1226 }
1227 return $parsed;
1228 }
1229
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] ?? '');
1233 $id = 0;
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];
1238 }
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 '';
1242 return $m[0];
1243 }, (string)$html);
1244 }
1245
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) {
1248 $attrs = $m[1];
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]);
1254 }, (string)$html);
1255 }
1256
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';
1262 return 'file';
1263 }
1264
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 '';
1269
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']);
1277 if ($autoplay) {
1278 $muted = true;
1279 }
1280
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;
1285
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>';
1293 }
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>';
1295 }
1296
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>';
1305 }
1306
1307 private function inline_video_options_from_html($source_attrs, $source_html) {
1308 $style = '';
1309 if (preg_match('/\sstyle=(["\'])(.*?)\1/i', (string)$source_attrs, $m)) {
1310 $style = $this->safe_inline_video_style($m[2]);
1311 }
1312
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');
1317
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]);
1326 }
1327 }
1328
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;
1334
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');
1338 }
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');
1342 }
1343
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');
1347 }
1348
1349 return array(
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,
1354 );
1355 }
1356
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], "\"' ");
1361 return '';
1362 }
1363
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';
1370 return '';
1371 }
1372
1373 private function safe_inline_video_style($style) {
1374 $out = array();
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);
1386 }
1387 $out[$key] = $value;
1388 }
1389 return $this->style_map_to_string($out);
1390 }
1391
1392 private function style_to_map($style) {
1393 $out = array();
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;
1400 }
1401 return $out;
1402 }
1403
1404 private function style_map_to_string(array $style) {
1405 $out = array();
1406 foreach ($style as $key => $value) {
1407 if ((string)$value === '') continue;
1408 $out[] = $key . ': ' . $value;
1409 }
1410 return implode('; ', $out);
1411 }
1412
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';
1420 if ($loop) {
1421 $params['loop'] = '1';
1422 $video_id = $this->youtube_video_id_from_url($url);
1423 if ($video_id !== '') $params['playlist'] = $video_id;
1424 }
1425 foreach ($params as $key => $value) {
1426 if (preg_match('/(?:[?&])' . preg_quote($key, '/') . '=/i', $url)) continue;
1427 $url .= $join . rawurlencode($key) . '=' . rawurlencode($value);
1428 $join = '&';
1429 }
1430 return $url;
1431 }
1432
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];
1438 }
1439 return '';
1440 }
1441
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';
1451 }
1452
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">'
1462 : '';
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 . '">'
1467 . $thumb_html
1468 . '<button type="button" class="dbx-youtube-consent-play" aria-label="Video abspielen">'
1469 . '<i class="bi bi-play-fill"></i></button>'
1470 . '</div>';
1471 }
1472
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(
1481 'dbx-media',
1482 'dbx-media-video',
1483 'dbx-media-external-video',
1484 'dbx-media-hero',
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'),
1488 );
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'),
1492 ));
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>';
1496 }
1497
1498 private function render_media_merge($db, $tpl, $cid, array $slots, array $rec, array $settings = array()) {
1499 $merge = array(
1500 'media:hero' => '',
1501 'media:gallery' => '',
1502 'media:header' => '',
1503 'media:teaser' => '',
1504 'media:footer' => '',
1505 'media_hero' => '',
1506 'media_gallery' => '',
1507 'media_header' => '',
1508 'media_teaser' => '',
1509 'media_footer' => '',
1510 );
1511
1512 $usage_rows = $db->select($this->dd_media_usage, 'content_id = ' . (int)$cid . ' AND active = 1', '*', 'slot,sorter,id', 'ASC', '', 0, 0, 0);
1513 $rows = array();
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);
1526 $rows[] = $row;
1527 }
1528 } else {
1529 $rows = $db->select($this->dd_media, 'content_id = ' . (int)$cid . ' AND active = 1', '*', 'slot,sorter,title,id', 'ASC', '', 0, 0, 0);
1530 }
1531 if (!is_array($rows)) return $merge;
1532
1533 $by_slot = array();
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;
1540 }
1541
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']);
1550 }
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;
1555 }
1556 return $merge;
1557 }
1558
1559 private function one_hero_row($db, array $rows, $hero_id) {
1560 $hero_id = (int)$hero_id;
1561 if ($hero_id > 0) {
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);
1564 }
1565 return isset($rows[0]) ? array($rows[0]) : array();
1566 }
1567
1568 private function render_media_slot($tpl, array $rows, $slot, array $settings) {
1569 if (!$rows) return '';
1570 $html = '';
1571 foreach ($rows as $row) {
1572 $mime = (string)($row['mime'] ?? '');
1573 $type = ((string)($row['media_type'] ?? '') === 'external_video')
1574 ? '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';
1583 }
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);
1587
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);
1593 continue;
1594 }
1595 if ($type === 'external_video') {
1596 $thumb_url = $this->external_video_thumb_url($row);
1597 $poster_url = $thumb_url;
1598 } else {
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') {
1604 $thumb_url = $url;
1605 }
1606 }
1607 $data = array(
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'),
1623 );
1624 $html .= $tpl->get_tpl('dbxContent|media-' . $template, $data);
1625 }
1626 return $html;
1627 }
1628
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';
1634 }
1635 return '';
1636 }
1637
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);
1643 }
1644
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'] ?? '')) !== '';
1648 }
1649 $rel = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
1650 if ($rel === '' || strpos($rel, '..') !== false) return false;
1651
1652 $root = dbx()->os_path(rtrim(dbx()->get_file_dir(), '/\\') . '/');
1653 $file = dbx()->os_path($root . $rel);
1654
1655 $base = realpath($root);
1656 $real = realpath($file);
1657 return $base && $real && strpos($real, $base) === 0 && is_file($real) && is_readable($real);
1658 }
1659
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);
1664 }
1665 }
1666 return $html;
1667 }
1668
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);
1678 return $html;
1679 }
1680
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) !== '';
1692 }
1693
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)) {
1697 return $title;
1698 }
1699 return $this->render_inline($tpl, 'title', $title);
1700 }
1701
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, '-');
1708 }
1709
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);
1716 }
1717
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';
1723 }
1724 return $value;
1725 }
1726
1727 private function hero_text_custom_properties($height) {
1728 $scale = $this->hero_text_scale($height);
1729 return array(
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),
1736 );
1737 }
1738
1739 private function hero_text_scale($height) {
1740 $px = $this->css_length_to_px($height);
1741 if ($px <= 0) return 1.0;
1742
1743 $scale = $px / 420.0;
1744 if ($scale < 0.72) $scale = 0.72;
1745 if ($scale > 1.85) $scale = 1.85;
1746 return $scale;
1747 }
1748
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;
1752
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;
1758
1759 return 0.0;
1760 }
1761
1762 private function css_px($value) {
1763 return $this->css_number($value) . 'px';
1764 }
1765
1766 private function css_number($value) {
1767 $value = (float)$value;
1768 $out = number_format($value, 3, '.', '');
1769 return rtrim(rtrim($out, '0'), '.');
1770 }
1771
1772 private function css_custom_properties(array $props) {
1773 $out = array();
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');
1779 }
1780 return implode(';', $out);
1781 }
1782
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;
1788 }
1789 return '4 / 3';
1790 }
1791
1792 private function gallery_data_dbx_attrs(array $settings, bool $has_slot, bool $has_media): string {
1793 if (!$has_slot || !$has_media) {
1794 return '';
1795 }
1796
1797 $parts = array(
1798 'lib=gallery',
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')),
1804 );
1805
1806 return ' data-dbx="' . htmlspecialchars(implode('|', $parts), ENT_QUOTES, 'UTF-8') . '"';
1807 }
1808
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 '';
1812
1813 $len = function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text);
1814 if ($len <= $max) return $text;
1815
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');
1821 }
1822 } elseif (($cut = strrpos($excerpt, ' ')) !== false && $cut > 40) {
1823 $excerpt = substr($excerpt, 0, $cut);
1824 }
1825
1826 return rtrim($excerpt, ".,;:- \t") . '…';
1827 }
1828
1830 private function seoAbsoluteUrl(string $path): string {
1831 return rtrim((string)dbx()->get_base_url(), '/') . '/' . ltrim($path, '/');
1832 }
1833
1834 private function seoCanonicalUrl($permalink) {
1835 $permalink = trim((string)$permalink);
1836 if ($permalink !== '' && preg_match('/^https?:\/\//i', $permalink)) {
1837 return $permalink;
1838 }
1839 return $this->seoAbsoluteUrl($permalink);
1840 }
1841
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;
1847 }
1848
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);
1853 if ($cid > 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'];
1857 }
1858 }
1859 }
1860 if ($hero_id <= 0) return '';
1861
1862 $row = $db->select1($this->dd_media, $hero_id);
1863 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1 || !$this->media_file_exists($row)) {
1864 return '';
1865 }
1866
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)) {
1870 return '';
1871 }
1872
1873 return $this->seoAbsoluteUrl('index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $hero_id);
1874 }
1875
1876 private function seoOgImageFromMediaId($db, int $media_id) {
1877 $media_id = (int)$media_id;
1878 if ($media_id <= 0) return '';
1879
1880 $row = $db->select1($this->dd_media, $media_id);
1881 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1 || !$this->media_file_exists($row)) {
1882 return '';
1883 }
1884
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)) {
1888 return '';
1889 }
1890
1891 return $this->seoAbsoluteUrl('index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $media_id);
1892 }
1893
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')) {
1897 return '';
1898 }
1899
1900 $lngs = dbx_accessible_lngs();
1901 if (!is_array($lngs) || count($lngs) < 2) {
1902 return '';
1903 }
1904
1905 $alternates = array();
1906 $escapedUid = str_replace("'", "''", $lngUid);
1907
1908 foreach ($lngs as $lng) {
1909 $lng = strtolower(trim((string)$lng));
1910 if ($lng === '' || !preg_match('/^[a-z]{2,3}$/', $lng)) continue;
1911
1912 $permalink = $this->seoHreflangPermalink($db, $lng, $lngUid, $escapedUid, $rec, $currentLng);
1913 if ($permalink === '') continue;
1914
1915 $alternates[] = array(
1916 'lng' => $lng,
1917 'url' => $this->seoCanonicalUrl($permalink),
1918 );
1919 }
1920
1921 if (count($alternates) < 2) {
1922 return '';
1923 }
1924
1925 $lines = array();
1926 $defaultLng = strtolower(trim((string)dbx()->get_config('dbx', 'default_lng', 'de')));
1927 if ($defaultLng === '' || $defaultLng === 'undef') $defaultLng = 'de';
1928
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') . '">';
1931 }
1932
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') . '">';
1936 break;
1937 }
1938 }
1939
1940 return count($lines) ? "\n " . implode("\n ", $lines) : '';
1941 }
1942
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'] ?? ''));
1946 }
1947
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));
1958 }
1959 }
1960
1961 $sibling = $db->select1(
1962 dbxContentLng::ddContent($lng),
1963 "lng_uid = '" . $escapedUid . "' AND activ = 1",
1964 'permalink',
1965 0
1966 );
1967 if (!is_array($sibling)) {
1968 return '';
1969 }
1970
1971 return trim((string)($sibling['permalink'] ?? ''));
1972 }
1973
1974 private function seoJsonLd(array $rec, string $title, string $description, string $canonical, string $lng): string {
1975 if ($title === '' && $description === '' && $canonical === '') {
1976 return '';
1977 }
1978
1979 $data = array(
1980 '@context' => 'https://schema.org',
1981 '@type' => 'WebPage',
1982 'name' => $title,
1983 'description' => $description,
1984 'url' => $canonical,
1985 'inLanguage' => $lng,
1986 );
1987
1988 $modified = trim((string)($rec['update_date'] ?? ''));
1989 if ($modified !== '') {
1990 $ts = strtotime($modified);
1991 if ($ts !== false) {
1992 $data['dateModified'] = gmdate('c', $ts);
1993 }
1994 }
1995
1996 $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1997 if (!is_string($json) || $json === '') {
1998 return '';
1999 }
2000
2001 return '<script type="application/ld+json">' . $json . '</script>';
2002 }
2003
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);
2012 return $html;
2013 }
2014}
2015
2016?>
$index['name']
Definition .dd.php:162
static ddContent(string $lng='')
renderStatic($cid, array $options=array())
applySeoMeta(int $cid, array $rec=null, array $meta=null)
$field
Definition config.dd.php:4
if( $syncRequest)
Definition index.php:520
DBX schema administration.