dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
dbxContent_cms.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxContent_admin;
3
4use dbx\dbxContent\dbxContentHome;
5use dbx\dbxContent\dbxContentLng;
6use dbx\dbxContent\dbxContentLngSync;
7use dbx\dbxContent\dbxContentPageCache;
8use dbx\dbxContent\dbxContentPermalinkIndex;
9use dbx\dbxContent\dbxContentRenderer;
10use dbx\dbxContent\dbxContentTranslate;
11use dbx\dbxContent\dbxContent_permalink;
12
13require_once dirname(__DIR__, 2) . '/dbxContent/include/dbxContent_bootstrap_sync.php';
14
15class dbxContent_cms extends \dbxObj {
16
17 private $dd_media = 'dbxMedia';
18 private $dd_media_usage = 'dbxMediaUsage';
19
20 private function cms_json_response(array $data): void {
21 dbx()->json_response($data, true);
22 }
23
24 private function lng_provision_open_flag($db, string $entity, int $id): int {
25 if (!dbxContentLngSync::isMasterLng() || !is_object($db)) {
26 return 0;
27 }
28
29 if (count(dbxContentLngSync::slaveLngs()) <= 0) {
30 return 0;
31 }
32
33 if ($id <= 0) {
34 return 1;
35 }
36
37 return dbxContentLngSync::hasMissingSlaveLng($db, $entity, $id) ? 1 : 0;
38 }
39
40 private function apply_cms_lng_context(): void {
41 $lng = strtolower(trim((string) dbx()->get_request_var('dbx_lng', '')));
42 if ($lng === '') {
43 return;
44 }
45
46 $allowed = dbxContentLngSync::accessibleLngs();
47 if (!in_array($lng, $allowed, true)) {
48 return;
49 }
50
51 dbx()->set_system_var('dbx_lng', $lng);
52 dbx()->set_remember_var('dbx_lng', $lng, 'dbx');
53 }
54
55 private function ini_size_bytes($value) {
56 $value = trim((string)$value);
57 if ($value === '') return 0;
58 $unit = strtolower(substr($value, -1));
59 $num = (float)$value;
60 if ($unit === 'g') $num *= 1024 * 1024 * 1024;
61 elseif ($unit === 'm') $num *= 1024 * 1024;
62 elseif ($unit === 'k') $num *= 1024;
63 return (int)$num;
64 }
65
66 private function upload_max_bytes() {
67 $upload = $this->ini_size_bytes(ini_get('upload_max_filesize'));
68 $post = $this->ini_size_bytes(ini_get('post_max_size'));
69 if ($upload <= 0) return $post;
70 if ($post <= 0) return $upload;
71 return min($upload, $post);
72 }
73
74 private function base_url($action, $params = array()) {
75 $url = $this->app_url() . '?dbx_modul=dbxContent_admin&dbx_run1=' . rawurlencode((string)$action);
76 foreach ($params as $key => $value) {
77 $url .= '&' . rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
78 }
79 return $url;
80 }
81
82 private function app_url(): string {
83 $script = str_replace('\\', '/', (string)($_SERVER['SCRIPT_NAME'] ?? ''));
84 if ($script === '') {
85 return '';
86 }
87
88 $dir = str_replace('\\', '/', dirname($script));
89 if ($dir === '.' || $dir === '/' || $dir === '\\') {
90 return '/';
91 }
92
93 return rtrim($dir, '/') . '/';
94 }
95
96 private function load_page_cache_classes(): void {
97 dbx()->load_content_cache_classes();
98 }
99
100 private function sync_permalink_index($db, int $cid, string $lng = ''): void {
101 $cid = (int) $cid;
102 if ($cid <= 0 || !is_object($db)) {
103 return;
104 }
105
106 $this->load_page_cache_classes();
107 if (!dbxContentPageCache::isConfigEnabled()) {
108 return;
109 }
110
111 $lng = strtolower(trim($lng));
112 $prevLng = null;
113 if ($lng !== '' && $lng !== dbxContentLng::current()) {
114 $prevLng = dbx()->get_system_var('dbx_lng', 'de');
115 dbx()->set_system_var('dbx_lng', $lng);
116 }
117
118 $rec = $db->select1(dbxContentLng::ddContent(), $cid, 'permalink,activ,folder,title,seo_title,description,keywords,meta_robots,seo_image_id,update_date,lng_uid', 0);
119 if (!is_array($rec)) {
120 if ($prevLng !== null) {
121 dbx()->set_system_var('dbx_lng', $prevLng);
122 }
123 return;
124 }
125
126 if ((int)($rec['activ'] ?? 0) !== 1) {
127 dbxContentPermalinkIndex::removeByCid($cid, dbxContentLng::current());
128 if (class_exists('dbx\\dbxContent\\dbxContentSitemap')) {
130 }
131 if ($prevLng !== null) {
132 dbx()->set_system_var('dbx_lng', $prevLng);
133 }
134 return;
135 }
136
137 $renderer = new dbxContentRenderer();
138 $rights = $renderer->getPublicFolderRights((int)($rec['folder'] ?? 0));
139 $permalink = (string)($rec['permalink'] ?? '');
140
141 dbxContentPageCache::writePageMeta($cid, array(
142 'cid' => $cid,
143 'permalink' => $permalink,
144 'rights' => $rights,
145 'activ' => (int)($rec['activ'] ?? 1),
146 'saved_at' => date('c'),
147 'seo' => dbxContentRenderer::seoMetaFromRecord($rec),
148 ));
149
150 if (trim($permalink) !== '') {
151 dbxContentPermalinkIndex::upsertPage(
152 $cid,
153 $permalink,
154 $rights,
155 (int)($rec['activ'] ?? 1),
156 dbxContentLng::current()
157 );
158 }
159
160 dbxContentHome::refreshHomeCache($db, $cid, dbxContentLng::current());
161
162 if (class_exists('dbx\\dbxContent\\dbxContentSitemap')) {
164 }
165
166 if ($prevLng !== null) {
167 dbx()->set_system_var('dbx_lng', $prevLng);
168 }
169 }
170
171 private function flush_lng_sync_cache($db, array $syncResult): void {
172 if (!is_object($db) || !is_array($syncResult)) {
173 return;
174 }
175
176 $updated = is_array($syncResult['updated'] ?? null) ? $syncResult['updated'] : array();
177 if (!count($updated)) {
178 return;
179 }
180
181 $this->load_page_cache_classes();
182 if (!dbxContentPageCache::isConfigEnabled()) {
183 return;
184 }
185
186 $menuFlushed = false;
187 foreach ($updated as $item) {
188 if (!is_array($item)) {
189 continue;
190 }
191
192 $id = (int) ($item['id'] ?? 0);
193 $lng = trim((string) ($item['lng'] ?? ''));
194 $entity = (($item['entity'] ?? 'page') === 'folder') ? 'folder' : 'page';
195 if ($id <= 0) {
196 continue;
197 }
198
199 if ($entity === 'page') {
200 dbxContentPageCache::invalidateContent($id);
201 if ($lng !== '') {
202 $this->sync_permalink_index($db, $id, $lng);
203 }
204 } elseif (!$menuFlushed) {
205 dbxContentPageCache::invalidateAllMenus();
206 $menuFlushed = true;
207 }
208 }
209
210 if (!$menuFlushed) {
211 dbxContentPageCache::invalidateAllMenus();
212 }
213 }
214
215 private function flush_saved_page_cache($db, int $cid): void {
216 $cid = (int) $cid;
217 if ($cid <= 0) {
218 return;
219 }
220
221 $this->load_page_cache_classes();
222 if (!dbxContentPageCache::isConfigEnabled()) {
223 return;
224 }
225 dbxContentPageCache::invalidateContent($cid);
226 dbxContentPageCache::invalidateAllMenus();
227 $this->sync_permalink_index($db, $cid);
228 }
229
230 private function flush_deleted_page_cache(int $cid, string $lng = ''): void {
231 $cid = (int) $cid;
232 if ($cid <= 0) {
233 return;
234 }
235
236 $this->load_page_cache_classes();
237 if (!dbxContentPageCache::isConfigEnabled()) {
238 return;
239 }
240
241 $lng = strtolower(trim($lng));
242 if ($lng === '') {
243 $lng = dbxContentLng::current();
244 }
245
246 dbxContentPageCache::invalidateContent($cid);
247 dbxContentPageCache::invalidateAllMenus();
248 dbxContentPermalinkIndex::removeByCid($cid, $lng);
249 }
250
251 private function copy_page_media_usage($db, int $sourceCid, int $targetCid, int $targetFolderId, bool $replace = true): int {
252 $sourceCid = (int) $sourceCid;
253 $targetCid = (int) $targetCid;
254 $targetFolderId = (int) $targetFolderId;
255 if ($sourceCid <= 0 || $targetCid <= 0 || !is_object($db)) {
256 return 0;
257 }
258
259 $copySlots = array('hero', 'gallery', 'header', 'teaser', 'footer');
260 if ($replace) {
261 foreach ($copySlots as $slot) {
262 $db->update(
263 $this->dd_media_usage,
264 array('active' => 0),
265 "content_id = " . $targetCid . " AND slot = '" . str_replace("'", "''", $slot) . "' AND active = 1",
266 0,
267 1,
268 1,
269 0
270 );
271 }
272 }
273
274 $rows = $db->select(
275 $this->dd_media_usage,
276 "content_id = " . $sourceCid . " AND active = 1 AND slot <> 'inline'",
277 '*',
278 'slot,sorter,id',
279 'ASC',
280 '',
281 0,
282 0,
283 0
284 );
285 if (!is_array($rows)) {
286 return 0;
287 }
288
289 $copied = 0;
290 foreach ($rows as $usage) {
291 if (!is_array($usage)) {
292 continue;
293 }
294 $mediaId = (int) ($usage['media_id'] ?? 0);
295 if ($mediaId <= 0) {
296 continue;
297 }
298 $media = $db->select1($this->dd_media, $mediaId, 'active', 0);
299 if (!is_array($media) || (int) ($media['active'] ?? 0) !== 1) {
300 continue;
301 }
302
303 $slot = $this->valid_media_slot($usage['slot'] ?? 'gallery');
304 if ($slot === 'inline') {
305 continue;
306 }
307
308 $usageId = $this->create_media_usage(
309 $db,
310 $mediaId,
311 $targetCid,
312 $targetFolderId,
313 $slot,
314 (string) ($usage['template'] ?? ''),
315 (string) ($usage['caption'] ?? ''),
316 (string) ($usage['settings'] ?? '')
317 );
318 if ($usageId > 0) {
319 $copied++;
320 }
321 }
322
323 return $copied;
324 }
325
326 private function sync_lng_page_media_from_master($db, int $masterCid, int $slaveCid, string $lng): int {
327 $masterCid = (int) $masterCid;
328 $slaveCid = (int) $slaveCid;
329 $lng = strtolower(trim($lng));
330 if ($masterCid <= 0 || $slaveCid <= 0 || $lng === '' || !is_object($db)) {
331 return 0;
332 }
333
334 $slaveRow = $db->select1(dbxContentLng::ddContent($lng), $slaveCid, 'content,folder', 0);
335 if (!is_array($slaveRow)) {
336 return 0;
337 }
338
339 $slaveFolder = (int) ($slaveRow['folder'] ?? 0);
340 $copied = $this->copy_page_media_usage($db, $masterCid, $slaveCid, $slaveFolder, true);
341 $this->sync_inline_media_usage($db, $slaveCid, (string) ($slaveRow['content'] ?? ''), $slaveFolder);
342 return $copied;
343 }
344
345 private function apply_lng_sync_media($db, int $masterCid, array $syncResult): int {
346 $master = dbxContentLngSync::masterLng();
347 $copied = 0;
348 foreach (($syncResult['updated'] ?? array()) as $item) {
349 if (!is_array($item) || (($item['entity'] ?? 'page') !== 'page')) {
350 continue;
351 }
352 $lng = strtolower(trim((string) ($item['lng'] ?? '')));
353 $slaveId = (int) ($item['id'] ?? 0);
354 if ($slaveId <= 0 || $lng === '' || $lng === $master) {
355 continue;
356 }
357 $copied += $this->sync_lng_page_media_from_master($db, $masterCid, $slaveId, $lng);
358 }
359 return $copied;
360 }
361
362 private function apply_lng_provision_media($db, string $type, int $masterId, array $result): int {
363 if ($type !== 'page' || !is_object($db)) {
364 return 0;
365 }
366
367 $copied = 0;
368 $items = array_merge(
369 is_array($result['created'] ?? null) ? $result['created'] : array(),
370 is_array($result['updated'] ?? null) ? $result['updated'] : array()
371 );
372 foreach ($items as $item) {
373 if (!is_array($item)) {
374 continue;
375 }
376 $lng = strtolower(trim((string) ($item['lng'] ?? '')));
377 $slaveId = (int) ($item['id'] ?? 0);
378 if ($slaveId <= 0 || $lng === '') {
379 continue;
380 }
381 $copied += $this->sync_lng_page_media_from_master($db, $masterId, $slaveId, $lng);
382 }
383 return $copied;
384 }
385
386 private function flush_deleted_folder_cache($db, int $folderId, string $lng = ''): void {
387 $folderId = (int) $folderId;
388 if ($folderId < 0 || !is_object($db)) {
389 return;
390 }
391
392 $this->load_page_cache_classes();
393 if (!dbxContentPageCache::isConfigEnabled()) {
394 return;
395 }
396
397 $lng = strtolower(trim($lng));
398 $prevLng = null;
399 if ($lng !== '' && $lng !== dbxContentLng::current()) {
400 $prevLng = dbx()->get_system_var('dbx_lng', 'de');
401 dbx()->set_system_var('dbx_lng', $lng);
402 }
403
404 dbxContentPageCache::invalidateFolderTree($db, $folderId);
405 dbxContentPageCache::invalidateAllMenus();
406
407 if ($prevLng !== null) {
408 dbx()->set_system_var('dbx_lng', $prevLng);
409 }
410 }
411
412 private function flush_folder_cache($db, int $folderId): void {
413 $folderId = (int) $folderId;
414 if ($folderId < 0 || !is_object($db)) {
415 return;
416 }
417
418 $this->load_page_cache_classes();
419 if (!dbxContentPageCache::isConfigEnabled()) {
420 return;
421 }
422 dbxContentPageCache::invalidateFolderTree($db, $folderId);
423 dbxContentPageCache::invalidateAllMenus();
424 }
425
426 private function flush_menu_cache(): void {
427 $this->load_page_cache_classes();
428 if (!dbxContentPageCache::isConfigEnabled()) {
429 return;
430 }
431 dbxContentPageCache::invalidateAllMenus();
432 }
433
434 private function flush_media_cache($db, int $content_id = 0, int $folder_id = 0): void {
435 if ($content_id <= 0 && $folder_id <= 0) {
436 return;
437 }
438
439 $this->load_page_cache_classes();
440 if (!dbxContentPageCache::isConfigEnabled()) {
441 return;
442 }
443 if ($content_id > 0) {
444 dbxContentPageCache::invalidateContent($content_id);
445 }
446 if ($folder_id > 0 && is_object($db)) {
447 dbxContentPageCache::invalidateFolderTree($db, $folder_id);
448 }
449 dbxContentPageCache::invalidateAllMenus();
450 }
451
452 private function flush_media_by_media_id($db, int $media_id): void {
453 $media_id = (int) $media_id;
454 if ($media_id <= 0 || !is_object($db)) {
455 return;
456 }
457
458 $usage_rows = $db->select($this->dd_media_usage, 'media_id = ' . $media_id . ' AND active = 1', '*', 'id', 'ASC', '', 0, 0, 0);
459 if (!is_array($usage_rows)) {
460 return;
461 }
462
463 foreach ($usage_rows as $usage) {
464 if (!is_array($usage)) {
465 continue;
466 }
467 $this->flush_media_cache(
468 $db,
469 (int)($usage['content_id'] ?? 0),
470 (int)($usage['folder_id'] ?? 0)
471 );
472 }
473 }
474
475 private function request_json() {
476 $data = array();
477
478 $raw = file_get_contents('php://input');
479 if (is_string($raw) && trim($raw) !== '') {
480 $decoded = json_decode($raw, true);
481 if (is_array($decoded)) {
482 $data = $decoded;
483 }
484 }
485
486 if (!count($data) && !empty($_POST) && is_array($_POST)) {
487 $data = $_POST;
488 }
489
490 return $data;
491 }
492
493 private function normalize_content_media_urls($html) {
494 $html = (string)$html;
495 $html = preg_replace_callback('/<figure\b([^>]*dbx-cms-inline-video-block[^>]*)>[\s\S]*?<\/figure>/i', function($m) {
496 $attrs = $m[1];
497 if (!preg_match('/data-cms-media-id=["\']?([0-9]+)/i', $attrs, $id_match)) return $m[0];
498 $id = (int)$id_match[1];
499 $placeholder = $this->inline_media_placeholder_html($id);
500 if ($placeholder === '') return $m[0];
501 $attrs = preg_replace('/\scontenteditable=(["\']).*?\1/i', '', $attrs);
502 if (stripos($attrs, 'data-cms-media-slot=') === false) $attrs .= ' data-cms-media-slot="inline"';
503 return '<figure' . $attrs . '>' . $placeholder . '</figure>';
504 }, $html);
505 $html = preg_replace('/<(video|iframe|source)\b[^>]*data-cms-media-id=["\']?([0-9]+)[^>]*>(?:\s*<\/\1>)?/i', '', $html);
506 $html = preg_replace_callback('/<(img|video|iframe|source)\b([^>]*?)>/i', function($m) {
507 $tag = $m[0];
508 $tag_name = strtolower((string)($m[1] ?? ''));
509 $id = 0;
510 if (preg_match('/dbx_mid=([0-9]+)/i', $tag, $id_match)) {
511 $id = (int)$id_match[1];
512 $tag = preg_replace('/\sdata-cms-media-id\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $tag);
513 $tag = preg_match('/\/\s*>$/', $tag)
514 ? preg_replace('/\s*\/\s*>$/', ' data-cms-media-id="' . $id . '" />', $tag)
515 : preg_replace('/\s*>$/', ' data-cms-media-id="' . $id . '">', $tag);
516 } elseif (preg_match('/data-cms-media-id=["\']?([0-9]+)/i', $tag, $id_match)) {
517 $id = (int)$id_match[1];
518 }
519 if ($tag_name === 'img' && $id > 0 && !$this->inline_media_available($id)) {
520 return $this->inline_media_missing_html($id);
521 }
522 if (stripos($tag, 'data-cms-media-slot=') === false && $id > 0) {
523 $tag = preg_match('/\/\s*>$/', $tag)
524 ? preg_replace('/\s*\/\s*>$/', ' data-cms-media-slot="inline" />', $tag)
525 : preg_replace('/\s*>$/', ' data-cms-media-slot="inline">', $tag);
526 }
527 return $tag;
528 }, $html);
529 $html = preg_replace_callback('/<(p|figure|div)\b([^>]*\bclass\s*=\s*(?:"[^"]*\bdbx-cms-inline-media\b[^"]*"|\'[^\']*\bdbx-cms-inline-media\b[^\']*\')[^>]*)>([\s\S]*?)<\/\1>/i', function($m) {
530 $tag = (string)($m[1] ?? 'p');
531 $attrs = (string)($m[2] ?? '');
532 $inner = (string)($m[3] ?? '');
533 if (!preg_match('/<img\b[^>]*\bsrc\s*=\s*(?:"[^"]*dbx_mid=([0-9]+)[^"]*"|\'[^\']*dbx_mid=([0-9]+)[^\']*\')/i', $inner, $id_match)) {
534 return $m[0];
535 }
536 $id = (int)(($id_match[1] ?? 0) ?: ($id_match[2] ?? 0));
537 if ($id <= 0) return $m[0];
538 $attrs = preg_replace('/\sdata-cms-media-id\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $attrs);
539 if (stripos($attrs, 'data-cms-media-slot=') === false) $attrs .= ' data-cms-media-slot="inline"';
540 $inner = preg_replace_callback('/<img\b([^>]*)>/i', function($img) use ($id) {
541 $img_attrs = (string)($img[1] ?? '');
542 if (!preg_match('/\bsrc\s*=\s*(?:"[^"]*dbx_mid=' . $id . '\b[^"]*"|\'[^\']*dbx_mid=' . $id . '\b[^\']*\')/i', $img_attrs)) {
543 return $img[0];
544 }
545 $img_attrs = preg_replace('/\sdata-cms-media-id\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $img_attrs);
546 if (stripos($img_attrs, 'data-cms-media-slot=') === false) $img_attrs .= ' data-cms-media-slot="inline"';
547 return '<img' . $img_attrs . ' data-cms-media-id="' . $id . '">';
548 }, $inner, 1);
549 return '<' . $tag . $attrs . '>' . $inner . '</' . $tag . '>';
550 }, $html);
551 $html = $this->strip_empty_inline_media_wrappers($html);
552 $html = str_replace(
553 array(
554 '?dbx_modul=dbxContent&amp;dbx_run1=media&amp;dbx_mid=',
555 '?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=',
556 'index.phpindex.php?',
557 ),
558 array(
559 'index.php?dbx_modul=dbxContent&amp;dbx_run1=media&amp;dbx_mid=',
560 'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=',
561 'index.php?',
562 ),
563 $html
564 );
565 return $html;
566 }
567
568 private function sanitize_content_html($html) {
569 $html = (string)$html;
570 if ($html === '') {
571 return '';
572 }
573
574 $html = preg_replace('#<(script|style|object|embed|base|meta|link)\b[^>]*>.*?</\1>#is', '', $html);
575 $html = preg_replace('#<(script|style|object|embed|base|meta|link)\b[^>]*\/?>#is', '', $html);
576
577 if (!class_exists('\DOMDocument')) {
578 $html = preg_replace('/\s+on[a-z]+\s*=\s*(["\']).*?\1/is', '', $html);
579 $html = preg_replace('/\s+style\s*=\s*(["\']).*?\1/is', '', $html);
580 $html = preg_replace('/\s+(href|src)\s*=\s*(["\'])\s*javascript:[^"\']*\2/is', '', $html);
581 return $html;
582 }
583
584 $previous = libxml_use_internal_errors(true);
585 $doc = new \DOMDocument('1.0', 'UTF-8');
586 $wrapped = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><div id="dbx_cms_sanitize_root">' . $html . '</div></body></html>';
587 $doc->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
588 libxml_clear_errors();
589 libxml_use_internal_errors($previous);
590
591 $root = $doc->getElementById('dbx_cms_sanitize_root');
592 if (!$root) {
593 return $html;
594 }
595
596 $walker = function($node) use (&$walker) {
597 if ($node instanceof \DOMElement) {
598 $remove = array();
599 foreach ($node->attributes as $attr) {
600 $name = strtolower($attr->name);
601 $value = trim((string)$attr->value);
602 if (strpos($name, 'on') === 0 || $name === 'style') {
603 $remove[] = $attr->name;
604 continue;
605 }
606 if (($name === 'href' || $name === 'src' || $name === 'xlink:href') && preg_match('/^\s*javascript:/i', $value)) {
607 $remove[] = $attr->name;
608 continue;
609 }
610 if ($name === 'data-dbx') {
611 if (stripos($value, 'lib=openWin') === false || stripos($value, 'javascript:') !== false) {
612 $remove[] = $attr->name;
613 }
614 }
615 }
616 foreach ($remove as $name) {
617 $node->removeAttribute($name);
618 }
619 }
620
621 foreach (iterator_to_array($node->childNodes) as $child) {
622 $walker($child);
623 }
624 };
625 $walker($root);
626
627 $out = '';
628 foreach ($root->childNodes as $child) {
629 $out .= $doc->saveHTML($child);
630 }
631 return $out;
632 }
633
634 private function normalize_and_sanitize_content($html) {
635 return $this->sanitize_content_html($this->normalize_content_media_urls($html));
636 }
637
638 private function inline_media_available($id) {
639 $id = (int)$id;
640 if ($id <= 0) return false;
641 $db = dbx()->get_system_obj('dbxDB');
642 $row = $db->select1($this->dd_media, $id);
643 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1) return false;
644 return $this->media_file_exists($row);
645 }
646
647 private function inline_media_missing_html($id) {
648 $id = (int)$id;
649 if ($id <= 0) return '';
650 $label = 'Mediendatei #' . $id . ' nicht verfuegbar';
651 return '<p class="dbx-cms-inline-media dbx-cms-inline-media-missing-wrap" data-cms-media-id="' . $id . '" data-cms-media-slot="inline" contenteditable="false" tabindex="0" title="Fehlende Mediendatei auswaehlen, Entf zum Loeschen"><span class="dbx-cms-inline-media-missing" aria-hidden="true">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span></p>';
652 }
653
654 private function inline_media_placeholder_html($id) {
655 $id = (int)$id;
656 if ($id <= 0) return '';
657 $db = dbx()->get_system_obj('dbxDB');
658 $row = $db->select1($this->dd_media, $id);
659 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1) return '';
660 $row = $this->normalize_media_row($row);
661 $title = htmlspecialchars((string)($row['title'] ?? $row['alt'] ?? $row['file_name'] ?? 'Video'), ENT_QUOTES, 'UTF-8');
662 $attr = ' data-cms-media-id="' . $id . '" data-cms-media-slot="inline"';
663 if (!in_array($this->media_type($row), array('video', 'external_video'), true)) return '';
664 $thumb = htmlspecialchars((string)($row['thumb_url'] ?? ''), ENT_QUOTES, 'UTF-8');
665 if ($thumb !== '') return '<img class="dbx-cms-inline-video-thumb" src="' . $thumb . '" alt="' . $title . '" title="' . $title . '"' . $attr . '><span class="dbx-cms-inline-video-play" aria-hidden="true"><i class="bi bi-play-fill"></i></span>';
666 return '<span class="dbx-cms-inline-video-empty"' . $attr . '>' . $title . '</span><span class="dbx-cms-inline-video-play" aria-hidden="true"><i class="bi bi-play-fill"></i></span>';
667 }
668
669 private function inline_media_embed_html(array $row) {
670 $id = (int)($row['id'] ?? 0);
671 if ($id <= 0) return '';
672 $row = $this->normalize_media_row($row);
673 $attr = ' data-cms-media-id="' . $id . '" data-cms-media-slot="inline" contenteditable="false" tabindex="0"';
674 if (in_array($this->media_type($row), array('video', 'external_video'), true)) {
675 $inner = $this->inline_media_placeholder_html($id);
676 return $inner !== '' ? '<figure class="dbx-cms-inline-media dbx-cms-inline-video-block"' . $attr . '>' . $inner . '</figure><p></p>' : '';
677 }
678 $url = htmlspecialchars((string)($row['url'] ?? $this->media_url($row)), ENT_QUOTES, 'UTF-8');
679 if ($url === '') return '';
680 $alt = htmlspecialchars((string)($row['alt'] ?? $row['title'] ?? $row['file_name'] ?? ''), ENT_QUOTES, 'UTF-8');
681 $title = htmlspecialchars((string)($row['title'] ?? $row['alt'] ?? $row['file_name'] ?? ''), ENT_QUOTES, 'UTF-8');
682 return '<p class="dbx-cms-inline-media"' . $attr . '><img class="dbx-cms-inline-image" src="' . $url . '" alt="' . $alt . '" title="' . $title . '"' . $attr . '></p><p></p>';
683 }
684
685 private function bool_int($value, $default = 1) {
686 if ($value === '' || $value === null) return $default;
687 return (int)((string)$value === '1' || $value === 1 || $value === true || $value === 'on');
688 }
689
690 private function clean_text($value, $max = 254) {
691 $value = trim((string)$value);
692 if ($max > 0 && strlen($value) > $max) {
693 $value = substr($value, 0, $max);
694 }
695 return $value;
696 }
697
698 private function slug($text) {
699 return dbxContent_permalink::slug($text);
700 }
701
702 private function page_permalink($db, $folder_id, $title, $permalink) {
703 $permalink = dbxContent_permalink::normalize($this->clean_text($permalink, 254));
704 if ($permalink === '') {
705 $permalink = dbxContent_permalink::build($db, dbxContentLng::ddFolder(), $folder_id, $title);
706 }
707 return $permalink;
708 }
709
710 private function page_permalink_exists($db, string $permalink): bool {
711 $permalink = dbxContent_permalink::normalize($permalink);
712 if ($permalink === '' || !is_object($db)) {
713 return false;
714 }
715
716 $escaped = str_replace("'", "''", $permalink);
717 $rows = $db->select(
718 dbxContentLng::ddContent(),
719 "permalink = '" . $escaped . "'",
720 'id',
721 'id',
722 'ASC',
723 '',
724 1,
725 0,
726 0
727 );
728 return is_array($rows) && isset($rows[0]['id']);
729 }
730
731 private function duplicate_page_permalink($db, int $folderId, string $title, string $permalink): string {
732 $base = dbxContent_permalink::normalize($this->clean_text($permalink, 254));
733 if ($base === '') {
734 $base = dbxContent_permalink::build($db, dbxContentLng::ddFolder(), $folderId, $title);
735 }
736 if ($base === '') {
737 $base = 'seite';
738 }
739
740 $copyNumber = 1;
741 do {
742 $suffix = $copyNumber === 1 ? '-kopie' : '-kopie-' . $copyNumber;
743 $maxBaseLength = max(1, 254 - strlen($suffix));
744 $candidateBase = rtrim(substr($base, 0, $maxBaseLength), '/-');
745 if ($candidateBase === '') {
746 $candidateBase = 'seite';
747 }
748 $candidate = dbxContent_permalink::normalize($candidateBase . $suffix);
749 $copyNumber++;
750 } while ($this->page_permalink_exists($db, $candidate));
751
752 return $candidate;
753 }
754
755 private function resolve_cms_page_id(): int {
756 $id = (int)dbx()->get_modul_var('cid', 0, 'int');
757 if ($id <= 0) {
758 $id = (int)dbx()->get_modul_var('dbx_cid', 0, 'int');
759 }
760 if ($id <= 0) {
761 $id = (int)dbx()->get_modul_var('rid', 0, 'int');
762 }
763 if ($id <= 0) {
764 $id = (int)dbx()->get_modul_var('id', 0, 'int');
765 }
766 if ($id > 0) {
767 return $id;
768 }
769
770 $permalink = dbxContent_permalink::normalize((string)dbx()->get_modul_var('permalink', '', 'parameter'));
771 if ($permalink === '') {
772 $permalink = dbxContent_permalink::normalize((string)dbx()->get_request_var('permalink', ''));
773 }
774 if ($permalink === '') {
775 return 0;
776 }
777
778 $db = dbx()->get_system_obj('dbxDB');
779 $rec = $db->select1(dbxContentLng::ddContent(), array('permalink' => $permalink), 'id', 0);
780 return is_array($rec) ? (int)($rec['id'] ?? 0) : 0;
781 }
782
783 private function attach_unreachable_tree_nodes(array $tree): array {
784 $reachablePageIds = array();
785 $reachableFolderIds = array();
786 foreach (is_array($tree['flat'] ?? null) ? $tree['flat'] : array() as $node) {
787 if (!is_array($node)) {
788 continue;
789 }
790 if (($node['_type'] ?? '') === 'page') {
791 $reachablePageIds[(int)($node['_id'] ?? 0)] = true;
792 } elseif (($node['_type'] ?? '') === 'folder') {
793 $reachableFolderIds[(int)($node['_id'] ?? 0)] = true;
794 }
795 }
796
797 $items = is_array($tree['items'] ?? null) ? $tree['items'] : array();
798 $orphanPages = array();
799 foreach ($items as $row) {
800 if (!is_array($row)) {
801 continue;
802 }
803 $id = (int)($row['id'] ?? 0);
804 if ($id > 0 && !isset($reachablePageIds[$id])) {
805 $orphanPages[] = $row;
806 }
807 }
808 if (!count($orphanPages)) {
809 return $tree;
810 }
811
812 $children = array();
813 foreach ($orphanPages as $row) {
814 $id = (int)($row['id'] ?? 0);
815 if ($id <= 0) {
816 continue;
817 }
818 $parent = (int)($row['folder'] ?? 0);
819 $children[] = $row + array(
820 '_node_id' => 'page-' . $id,
821 '_type' => 'page',
822 '_id' => $id,
823 '_parent' => $parent,
824 '_title' => (string)($row['title'] ?? ('Seite ' . $id)),
825 '_rights' => (string)($row['group_read'] ?? ''),
826 '_children' => array(),
827 '_level' => 0,
828 );
829 }
830
831 if (!count($children)) {
832 return $tree;
833 }
834
835 $orphanFolder = array(
836 'id' => -999001,
837 'name' => 'Nicht im Baum',
838 '_node_id' => 'folder--999001',
839 '_type' => 'folder',
840 '_id' => -999001,
841 '_parent' => 0,
842 '_title' => 'Nicht im Baum',
843 '_rights' => '',
844 '_children' => $children,
845 '_level' => 0,
846 );
847
848 $tree['nodes'] = array_merge(is_array($tree['nodes'] ?? null) ? $tree['nodes'] : array(), array($orphanFolder));
849 foreach ($children as $child) {
850 $tree['flat'][] = $child;
851 }
852 $tree['flat'][] = $orphanFolder;
853
854 return $tree;
855 }
856
857 private function cms_tree() {
858 $db = dbx()->get_system_obj('dbxDB');
859 $this->ensure_cms_schema($db);
860 $tree = $db->select_tree(dbxContentLng::ddFolder(), dbxContentLng::ddContent(), array(
861 'folder_order' => 'sorter,name,id',
862 'item_order' => 'sorter,title,id',
863 'verify_access' => 0,
864 ));
865 $tree = $this->attach_unreachable_tree_nodes($tree);
866 return $this->decorate_tree($tree);
867 }
868
869 private function dd_server($dd, $fallback) {
870 $db = dbx()->get_system_obj('dbxDB');
871 if (method_exists($db, 'load_dd')) {
872 $sys = $db->load_dd($dd);
873 $mod = $sys['dd_modul'] ?? 'dbx';
874 $name = $sys['dd_name'] ?? '';
875 if ($name && isset($_SESSION['dbx']['cache']['dd'][$mod][$name]['table']['server'])) {
876 return $_SESSION['dbx']['cache']['dd'][$mod][$name]['table']['server'];
877 }
878 }
879 return $fallback;
880 }
881
882 private function clear_cms_dd_cache() {
883 if (!isset($_SESSION['dbx']['cache']['dd']) || !is_array($_SESSION['dbx']['cache']['dd'])) return;
884 foreach ($_SESSION['dbx']['cache']['dd'] as $modul => $dds) {
885 if (!is_array($dds)) continue;
886 unset($_SESSION['dbx']['cache']['dd'][$modul][dbxContentLng::ddContent()]);
887 unset($_SESSION['dbx']['cache']['dd'][$modul][dbxContentLng::ddFolder()]);
888 unset($_SESSION['dbx']['cache']['dd'][$modul][$this->dd_media]);
889 unset($_SESSION['dbx']['cache']['dd'][$modul][$this->dd_media_usage]);
890 }
891 }
892
893 private function clear_cms_tpl_cache() {
894 if (!isset($_SESSION['dbx']['cache']['tpl']['dbxContent_admin']) || !is_array($_SESSION['dbx']['cache']['tpl']['dbxContent_admin'])) return;
895 foreach (array('cms-admin', 'cms-admin-frame', 'cms-admin-right', 'cms-admin-media-panel', 'cms-admin-settings-panels') as $tpl) {
896 unset($_SESSION['dbx']['cache']['tpl']['dbxContent_admin'][$tpl]);
897 }
898 }
899
900 private function ensure_column($db, $server, $table, $name, $type) {
901 $server = (string)$server;
902 $table = preg_replace('/[^A-Za-z0-9_]+/', '', (string)$table);
903 $name = preg_replace('/[^A-Za-z0-9_]+/', '', (string)$name);
904 if ($server === '' || $table === '' || $name === '') return;
905
906 $cols = $db->select_query($server, 'PRAGMA table_info(' . $table . ')');
907 if (is_array($cols)) {
908 foreach ($cols as $col) {
909 if (isset($col['name']) && strtolower((string)$col['name']) === strtolower($name)) return;
910 }
911 }
912
913 $db->exec($server, 'ALTER TABLE ' . $table . ' ADD COLUMN ' . $name . ' ' . $type);
914 }
915
916 private function ensure_table($db, $server, $table, $create_sql) {
917 $server = (string)$server;
918 $table = preg_replace('/[^A-Za-z0-9_]+/', '', (string)$table);
919 if ($server === '' || $table === '') return;
920 $exists = $db->select_query($server, "SELECT name FROM sqlite_master WHERE type='table' AND name='" . str_replace("'", "''", $table) . "'");
921 if (is_array($exists) && !empty($exists)) return;
922 $db->exec($server, $create_sql);
923 }
924
925 private function ensure_index($db, $server, $name, $sql) {
926 $server = (string)$server;
927 $name = preg_replace('/[^A-Za-z0-9_]+/', '', (string)$name);
928 if ($server === '' || $name === '') return;
929 $exists = $db->select_query($server, "SELECT name FROM sqlite_master WHERE type='index' AND name='" . str_replace("'", "''", $name) . "'");
930 if (is_array($exists) && !empty($exists)) return;
931 $db->exec($server, $sql);
932 }
933
934 public function ensure_schema($db) {
935 $this->ensure_cms_schema($db);
936 }
937
938 private function ensure_cms_schema($db) {
939 static $done = false;
940 if ($done) return;
941 $done = true;
942
943 $this->clear_cms_dd_cache();
944
945 $content_server = $this->dd_server(dbxContentLng::ddContent(), 'dbx|dbxContent.db3');
946 $folder_server = $this->dd_server(dbxContentLng::ddFolder(), 'dbx|dbxContent.db3');
947 $content_table = $db->get_dd_table(dbxContentLng::ddContent());
948 $folder_table = $db->get_dd_table(dbxContentLng::ddFolder());
949 $media_server = $this->dd_server($this->dd_media, 'dbx|dbxMedia.db3');
950 $usage_server = $this->dd_server($this->dd_media_usage, 'dbx|dbxMedia.db3');
951
952 $content_text_cols = array(
953 'hero_template',
954 'hero_image_id',
955 'hero_margin_top',
956 'hero_height',
957 'hero_variant',
958 'hero_sticky',
959 'hero_scroll_layer',
960 'gallery_template',
961 'gallery_visible_count',
962 'gallery_image_size',
963 'gallery_overflow',
964 'gallery_click_behavior',
965 );
966
967 $folder_text_cols = array(
968 'hero_template',
969 'hero_image_id',
970 'hero_margin_top',
971 'hero_height',
972 'hero_variant',
973 'hero_sticky',
974 'hero_scroll_layer',
975 );
976
977 foreach ($content_text_cols as $col) {
978 $this->ensure_column($db, $content_server, $content_table, $col, 'TEXT');
979 }
980 $this->ensure_column($db, $content_server, $content_table, 'gallery_lightbox_width', "TEXT DEFAULT '100vw'");
981 $this->ensure_column($db, $content_server, $content_table, 'meta_robots', "TEXT DEFAULT 'index,follow'");
982 $this->ensure_column($db, $content_server, $content_table, 'seo_title', "TEXT DEFAULT ''");
983 $this->ensure_column($db, $content_server, $content_table, 'seo_image_id', 'INTEGER DEFAULT 0');
984
985 foreach ($folder_text_cols as $col) {
986 $this->ensure_column($db, $folder_server, $folder_table, $col, 'TEXT');
987 }
988 $this->ensure_column($db, $folder_server, $folder_table, 'sorter', "TEXT DEFAULT ''");
989
990 $this->ensure_column($db, $media_server, 'media', 'thumb_file_path', 'TEXT');
991 $this->ensure_column($db, $media_server, 'media', 'thumb_width', 'INTEGER');
992 $this->ensure_column($db, $media_server, 'media', 'thumb_height', 'INTEGER');
993 $this->ensure_column($db, $media_server, 'media', 'media_type', 'TEXT');
994 $this->ensure_column($db, $media_server, 'media', 'storage_type', "TEXT DEFAULT 'local'");
995 $this->ensure_column($db, $media_server, 'media', 'media_folder', "TEXT DEFAULT ''");
996 $this->ensure_column($db, $media_server, 'media', 'provider', "TEXT DEFAULT ''");
997 $this->ensure_column($db, $media_server, 'media', 'provider_id', "TEXT DEFAULT ''");
998 $this->ensure_column($db, $media_server, 'media', 'external_url', "TEXT DEFAULT ''");
999 $this->ensure_column($db, $media_server, 'media', 'embed_url', "TEXT DEFAULT ''");
1000
1001 $this->ensure_table($db, $usage_server, 'media_usage', 'CREATE TABLE media_usage (
1002 id INTEGER PRIMARY KEY AUTOINCREMENT,
1003 create_date TEXT,
1004 create_uid INTEGER DEFAULT 0,
1005 update_date TEXT,
1006 update_uid INTEGER DEFAULT 0,
1007 owner INTEGER,
1008 active INTEGER DEFAULT 1,
1009 media_id INTEGER DEFAULT 0,
1010 content_id INTEGER DEFAULT 0,
1011 folder_id INTEGER DEFAULT 0,
1012 slot TEXT DEFAULT "gallery",
1013 sorter TEXT DEFAULT "",
1014 template TEXT DEFAULT "",
1015 caption TEXT DEFAULT "",
1016 settings TEXT DEFAULT ""
1017 )');
1018 $this->ensure_index($db, $usage_server, 'idx_media_usage_media', 'CREATE INDEX idx_media_usage_media ON media_usage(media_id)');
1019 $this->ensure_index($db, $usage_server, 'idx_media_usage_content_slot', 'CREATE INDEX idx_media_usage_content_slot ON media_usage(content_id,slot)');
1020 $this->ensure_index($db, $usage_server, 'idx_media_usage_folder_slot', 'CREATE INDEX idx_media_usage_folder_slot ON media_usage(folder_id,slot)');
1021
1022 $this->normalize_content_gallery_defaults($db, $content_server, $content_table);
1023 dbxContentLngSync::ensureSchema($db);
1024 }
1025
1026 private function normalize_content_gallery_defaults($db, $server, $table = 'content') {
1027 $table = trim((string) $table);
1028 if ($table === '') {
1029 $table = 'content';
1030 }
1031 $defaults = array(
1032 'gallery_template' => 'image-gallery',
1033 'gallery_visible_count' => '3',
1034 'gallery_image_size' => 'original',
1035 'gallery_lightbox_width' => '100vw',
1036 'gallery_overflow' => 'grid',
1037 'gallery_click_behavior' => 'lightbox',
1038 );
1039
1040 foreach ($defaults as $field => $value) {
1041 $value = str_replace("'", "''", $value);
1042 $db->exec($server, "UPDATE $table SET $field = '$value' WHERE $field IS NULL OR TRIM($field) = '' OR LOWER(TRIM($field)) = 'parent'");
1043 }
1044 }
1045
1046 private function normalize_gallery_row(array $row) {
1047 $defaults = array(
1048 'gallery_template' => 'image-gallery',
1049 'gallery_visible_count' => '3',
1050 'gallery_image_size' => 'original',
1051 'gallery_lightbox_width' => '100vw',
1052 'gallery_overflow' => 'grid',
1053 'gallery_click_behavior' => 'lightbox',
1054 );
1055
1056 foreach ($defaults as $field => $value) {
1057 $current = trim((string)($row[$field] ?? ''));
1058 if ($current === '' || strtolower($current) === 'parent') {
1059 $row[$field] = $value;
1060 }
1061 }
1062
1063 return $row;
1064 }
1065
1066 private function tree_row_html(array $node) {
1067 $tpl = dbx()->get_system_obj('dbxTPL');
1068 $type = ($node['_type'] ?? '') === 'folder' ? 'folder' : 'page';
1069 $id = (int)($node['_id'] ?? 0);
1070 $title = htmlspecialchars((string)($node['_title'] ?? ''), ENT_QUOTES, 'UTF-8');
1071 $rights = htmlspecialchars((string)($node['_rights'] ?? ''), ENT_QUOTES, 'UTF-8');
1072 $data = array(
1073 '_type' => $type,
1074 '_id' => (string)$id,
1075 '_parent' => (string)($node['_parent'] ?? ($type === 'folder' ? $id : 0)),
1076 'icon' => $type === 'folder' ? '<i class="bi bi-folder2-open"></i>' : '<i class="bi bi-file-earmark-text"></i>',
1077 'folder_edit_btn' => $type === 'folder'
1078 ? '<button type="button" class="dbx-cms-tree-edit" data-cms-folder-edit-btn title="Ordner bearbeiten" aria-label="Ordner bearbeiten"><i class="bi bi-pencil-square"></i></button>'
1079 : '',
1080 'folder_id_label' => '',
1081 'title_label' => $type === 'page' ? '<span class="dbx-cms-page-id">(' . $id . ')</span> ' . $title : $title,
1082 'rights_label' => ($type === 'folder' && $rights !== '') ? '<span class="dbx-cms-rights" data-cms-folder-edit title="Ordnerrechte bearbeiten">' . $rights . '</span>' : '',
1083 'lng_badges' => (string)($node['_lng_badges'] ?? ''),
1084 );
1085 return $tpl->get_tpl('dbxContent_admin|cms-tree-row', $data);
1086 }
1087
1088 private function attach_lng_coverage(array $node, $db): array {
1089 $type = ($node['_type'] ?? '') === 'folder' ? 'folder' : 'page';
1090 $id = (int)($node['_id'] ?? 0);
1091 $dd = $type === 'folder' ? dbxContentLng::ddFolder() : dbxContentLng::ddContent();
1092 $lngUid = trim((string)($node['lng_uid'] ?? $node['_lng_uid'] ?? ''));
1093
1094 if ($lngUid === '' && $id > 0) {
1095 $lngUid = dbxContentLngSync::ensureRecordUid($db, $dd, $id, $type === 'folder' ? 'f' : 'p');
1096 }
1097
1098 $node['_lng_uid'] = $lngUid;
1099 if ($lngUid !== '') {
1100 $coverage = dbxContentLngSync::coverageForUid($db, $type, $lngUid);
1101 $node['_lng_coverage'] = $coverage;
1102 $node['_lng_badges'] = dbxContentLngSync::badgesHtml($coverage);
1103 }
1104
1105 return $node;
1106 }
1107
1108 private function decorate_tree_nodes(array $nodes, array &$flat) {
1109 $db = dbx()->get_system_obj('dbxDB');
1110 $out = array();
1111 foreach ($nodes as $node) {
1112 if (!is_array($node)) continue;
1113 if (($node['_type'] ?? '') === 'folder' && (int)($node['_id'] ?? 0) > 0) {
1114 $full = $db->select1(dbxContentLng::ddFolder(), (int)$node['_id'], '*', 0);
1115 if (is_array($full)) {
1116 foreach (array('template','group_read','hero_template','hero_image_id','hero_margin_top','hero_height','hero_variant','hero_sticky','hero_scroll_layer') as $key) {
1117 if (array_key_exists($key, $full)) {
1118 $node[$key] = $full[$key];
1119 $node['_' . $key] = $full[$key];
1120 }
1121 }
1122 $node['_template'] = $full['template'] ?? ($node['_template'] ?? '');
1123 $node['_rights'] = $full['group_read'] ?? ($node['_rights'] ?? '');
1124 $node['lng_uid'] = $full['lng_uid'] ?? '';
1125 }
1126 }
1127 if (($node['_type'] ?? '') === 'page' && (int)($node['_id'] ?? 0) > 0) {
1128 $full = $db->select1(dbxContentLng::ddContent(), (int)$node['_id'], 'lng_uid,lng_sync', 0);
1129 if (is_array($full)) {
1130 $node['lng_uid'] = $full['lng_uid'] ?? '';
1131 }
1132 }
1133 $node = $this->attach_lng_coverage($node, $db);
1134 $node['_row_html'] = $this->tree_row_html($node);
1135 $flat[] = $node;
1136 if (isset($node['_children']) && is_array($node['_children'])) {
1137 $node['_children'] = $this->decorate_tree_nodes($node['_children'], $flat);
1138 }
1139 $out[] = $node;
1140 }
1141 return $out;
1142 }
1143
1144 private function render_tree_report(array $flat) {
1145 $report = dbx()->get_system_obj('dbxReport');
1146 $report->init('cms-content-tree', 'cms-tree-row');
1147 $report->_mode = 'tpl';
1148 $report->_rdata = $flat;
1149 $report->_rcount = count($flat);
1150 $report->_rrows = max(1, count($flat));
1151 $report->_rflds = array();
1152 return $report->run();
1153 }
1154
1155 private function decorate_tree(array $tree) {
1156 $flat = array();
1157 $tree['nodes'] = $this->decorate_tree_nodes(is_array($tree['nodes'] ?? null) ? $tree['nodes'] : array(), $flat);
1158 $tree['flat'] = $flat;
1159 $tree['tree_html'] = $this->render_tree_report($flat);
1160 return $tree;
1161 }
1162
1163 private function media_url($row, $thumb = false) {
1164 $id = (int)($row['id'] ?? 0);
1165 if ($id <= 0) return '';
1166 $url = 'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $id;
1167 if ($thumb && !empty($row['thumb_file_path'])) $url .= '&dbx_thumb=1';
1168 $v = (int)($row['size'] ?? 0) . '-' . (int)($row['width'] ?? 0) . 'x' . (int)($row['height'] ?? 0);
1169 if ($thumb) $v .= '-' . (int)($row['thumb_width'] ?? 0) . 'x' . (int)($row['thumb_height'] ?? 0);
1170 $url .= '&_v=' . rawurlencode($v);
1171 return $url;
1172 }
1173
1174 private function external_video_thumb_url($row) {
1175 $provider = strtolower(trim((string)($row['provider'] ?? '')));
1176 $provider_id = trim((string)($row['provider_id'] ?? ''));
1177 if ($provider === 'youtube' && preg_match('~^[A-Za-z0-9_-]{11}$~', $provider_id)) {
1178 return 'https://img.youtube.com/vi/' . $provider_id . '/hqdefault.jpg';
1179 }
1180 return '';
1181 }
1182
1183 private function external_video_json_meta($file) {
1184 $raw = @file_get_contents((string)$file);
1185 if ($raw === false || $raw === '') return array();
1186 $data = json_decode($raw, true);
1187 if (!is_array($data)) return array();
1188 $provider = strtolower(trim((string)($data['provider'] ?? 'youtube')));
1189 $provider_id = trim((string)($data['provider_id'] ?? pathinfo((string)$file, PATHINFO_FILENAME)));
1190 $title = trim((string)($data['title'] ?? ('YouTube ' . $provider_id)));
1191 $external_url = trim((string)($data['external_url'] ?? ''));
1192 $embed_url = trim((string)($data['embed_url'] ?? ''));
1193 if ($embed_url === '' && preg_match('~^[A-Za-z0-9_-]{11}$~', $provider_id)) {
1194 $embed_url = 'https://www.youtube.com/embed/' . $provider_id;
1195 }
1196 return array(
1197 'provider' => $provider,
1198 'provider_id' => $provider_id,
1199 'title' => $title,
1200 'alt' => $title,
1201 'external_url' => $external_url,
1202 'embed_url' => $embed_url,
1203 );
1204 }
1205
1206 private function media_type($row) {
1207 $stored = strtolower(trim((string)($row['media_type'] ?? '')));
1208 if (in_array($stored, array('image','video','external_video','file'), true)) return $stored;
1209 $storage = strtolower(trim((string)($row['storage_type'] ?? '')));
1210 if ($storage === 'external' || trim((string)($row['provider'] ?? '')) !== '' || trim((string)($row['embed_url'] ?? '')) !== '') return 'external_video';
1211 $mime = strtolower((string)($row['mime'] ?? ''));
1212 $name = strtolower((string)($row['file_name'] ?? $row['file_path'] ?? ''));
1213 if (strpos($mime, 'video/') === 0 || preg_match('/\.(mp4|webm|ogv|ogg|mov|m4v)$/i', $name)) return 'video';
1214 if (strpos($mime, 'image/') === 0 || preg_match('/\.(jpg|jpeg|png|gif|webp|svg)$/i', $name)) return 'image';
1215 return 'file';
1216 }
1217
1218 private function media_file_exists($row) {
1219 $row = is_array($row) ? $row : array();
1220 if (strtolower((string)($row['storage_type'] ?? '')) === 'external') return trim((string)($row['embed_url'] ?? $row['external_url'] ?? '')) !== '';
1221 $rel = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
1222 if ($rel === '' || strpos($rel, '..') !== false) return false;
1223
1224 $root = rtrim(dbx_get_file_dir(), '/\\') . '/';
1225 if (function_exists('dbx_os_path_file')) $root = dbx_os_path_file($root);
1226 $base = realpath($root);
1227 if (!$base) return false;
1228
1229 $file = $root . $rel;
1230 if (function_exists('dbx_os_path_file')) $file = dbx_os_path_file($file);
1231 $real = realpath($file);
1232 return $real && strpos($real, $base) === 0 && is_file($real) && is_readable($real);
1233 }
1234
1235 private function media_thumb_exists($row) {
1236 $row = is_array($row) ? $row : array();
1237 if (strtolower((string)($row['storage_type'] ?? '')) === 'external' && trim((string)($row['thumb_file_path'] ?? '')) === '') return false;
1238 $rel = ltrim(str_replace('\\', '/', (string)($row['thumb_file_path'] ?? '')), '/');
1239 if ($rel === '' || strpos($rel, '..') !== false) return false;
1240
1241 $root = rtrim(dbx_get_file_dir(), '/\\') . '/';
1242 $file = $root . $rel;
1243 if (function_exists('dbx_os_path_file')) {
1245 $file = dbx_os_path_file($file);
1246 }
1247
1248 $base = realpath($root);
1249 $real = realpath($file);
1250 return $base && $real && strpos($real, $base) === 0 && is_file($real) && is_readable($real);
1251 }
1252
1253 private function valid_media_slot($slot, $default = 'gallery') {
1254 $slot = strtolower(trim((string)$slot));
1255 $allowed = array('hero','gallery','inline','header','teaser','footer','shop');
1256 return in_array($slot, $allowed, true) ? $slot : $default;
1257 }
1258
1259 private function media_rel_dir($slot) {
1260 return 'media/' . $this->valid_media_slot($slot) . '/';
1261 }
1262
1263 private function video_media_base_folder() {
1264 $root = rtrim(dbx_get_file_dir(), '/\\') . '/media/';
1265 if (function_exists('dbx_os_path_file')) $root = dbx_os_path_file($root);
1266 if (is_dir($root . 'videos')) return 'videos';
1267 if (is_dir($root . 'video')) return 'video';
1268 return 'videos';
1269 }
1270
1271 private function media_standard_bases() {
1272 return array('img' => 'image', $this->video_media_base_folder() => 'video', 'youtube' => 'external_video', 'file' => 'file');
1273 }
1274
1275 private function canonical_media_folder($folder) {
1276 $folder = strtolower(trim(str_replace('\\', '/', (string)$folder), '/'));
1277 if (preg_match('~^youtube/[a-z0-9_-]{11}$~', $folder)) return 'youtube';
1278 return $folder;
1279 }
1280
1281 private function clean_media_folder($folder, $media_type = 'image') {
1282 $media_type = $this->media_type(array('media_type' => $media_type));
1283 $folder = $this->canonical_media_folder($folder);
1284 $folder = strtolower(str_replace('\\', '/', trim((string)$folder)));
1285 $folder = preg_replace('~/+~', '/', $folder);
1286 $folder = trim($folder, '/');
1287 $parts = array();
1288 foreach (explode('/', $folder) as $part) {
1289 $part = preg_replace('/[^A-Za-z0-9_-]+/', '-', $part);
1290 $part = trim($part, '-_');
1291 if ($part !== '' && $part !== '.' && $part !== '..') $parts[] = $part;
1292 }
1293 $folder = implode('/', $parts);
1294
1295 if ($folder === 'module') {
1296 return 'module';
1297 }
1298
1299 if ($media_type === 'video') {
1300 $base = $this->video_media_base_folder();
1301 if ($folder === '' || $folder === 'video' || $folder === 'videos' || $folder === 'img' || $folder === 'img/video') return $base;
1302 if (substr($folder, 0, 10) === 'img/video/') return $base . '/' . substr($folder, 10);
1303 if (substr($folder, 0, 6) === 'video/') return $base . '/' . substr($folder, 6);
1304 if (substr($folder, 0, 7) === 'videos/') return $base . '/' . substr($folder, 7);
1305 return $base;
1306 }
1307 if ($media_type === 'external_video') {
1308 if ($folder === '' || $folder === 'youtube') return 'youtube';
1309 if (substr($folder, 0, 8) !== 'youtube/') return 'youtube';
1310 return $folder;
1311 }
1312 if ($media_type === 'file') {
1313 if ($folder === '' || $folder === 'file') return 'file/allgemein';
1314 if (substr($folder, 0, 5) !== 'file/') return 'file/allgemein';
1315 return $folder;
1316 }
1317 if ($folder === '' || $folder === 'img' || $folder === 'image') return 'img/allgemein';
1318 if (substr($folder, 0, 4) !== 'img/') return 'img/allgemein';
1319 return $folder;
1320 }
1321
1322 private function media_type_from_folder($folder, $fallback = 'image') {
1323 $folder = $this->canonical_media_folder($folder);
1324 if (strpos($folder, 'img/video') === 0) return 'video';
1325 if (strpos($folder, 'videos/') === 0 || $folder === 'videos') return 'video';
1326 if (strpos($folder, 'video/') === 0 || $folder === 'video') return 'video';
1327 if (strpos($folder, 'youtube/') === 0 || $folder === 'youtube') return 'external_video';
1328 if (strpos($folder, 'file/') === 0 || $folder === 'file') return 'file';
1329 return $fallback;
1330 }
1331
1332 private function media_folder_from_path($rel, $media_type = 'image') {
1333 $rel = ltrim(str_replace('\\', '/', (string)$rel), '/');
1334 if (preg_match('~^media/module/[^/]+$~i', $rel)) {
1335 return 'module';
1336 }
1337 if (preg_match('~^media/youtube/[^/]+\.json$~i', $rel)) {
1338 return 'youtube';
1339 }
1340 if (preg_match('~^media/(img|video|youtube|external|file)/([^/]+)/[^/]+$~i', $rel, $m)) {
1341 return $this->clean_media_folder($m[1] . '/' . $m[2], $media_type);
1342 }
1343 if (preg_match('~^media/(img|video|file)/([^/]+)/?$~i', $rel, $m)) {
1344 return $this->clean_media_folder($m[1] . '/' . $m[2], $media_type);
1345 }
1346 return $this->clean_media_folder('', $media_type);
1347 }
1348
1349 private function media_folder_rel_dir($folder, $media_type = 'image') {
1350 return 'media/' . $this->clean_media_folder($folder, $media_type) . '/';
1351 }
1352
1353 private function media_thumb_rel_dir($slot) {
1354 $slot_text = (string)$slot;
1355 $folder_type = 'image';
1356 if (preg_match('~^(youtube|external)(/|$)~', $slot_text)) $folder_type = 'external_video';
1357 elseif (preg_match('~^(videos|video|img/video)(/|$)~', $slot_text)) $folder_type = 'video';
1358 elseif (preg_match('~^file(/|$)~', $slot_text)) $folder_type = 'file';
1359 $folder = $this->clean_media_folder($slot, $folder_type);
1360 return 'media/_thumbs/' . $folder . '/';
1361 }
1362
1363 private function cms_media_dir($slot = '') {
1364 $slot_text = (string)$slot;
1365 $folder_type = 'image';
1366 if (preg_match('~^(youtube|external)(/|$)~', $slot_text)) $folder_type = 'external_video';
1367 elseif (preg_match('~^(videos|video|img/video)(/|$)~', $slot_text)) $folder_type = 'video';
1368 elseif (preg_match('~^file(/|$)~', $slot_text)) $folder_type = 'file';
1369 $rel = $slot === '' ? 'media/' : $this->media_folder_rel_dir($slot, $folder_type);
1370 $dir = rtrim(dbx_get_file_dir(), '/\\') . '/' . $rel;
1371 return function_exists('dbx_os_path_file') ? dbx_os_path_file($dir) : $dir;
1372 }
1373
1374 private function cms_media_base_dir($base) {
1375 $base = strtolower(trim(str_replace('\\', '/', (string)$base), '/'));
1376 if (!in_array($base, array('img', 'video', 'videos', 'youtube', 'external', 'file', 'module'), true)) return '';
1377 $rel = 'media/' . $base . '/';
1378 $dir = rtrim(dbx_get_file_dir(), '/\\') . '/' . $rel;
1379 return function_exists('dbx_os_path_file') ? dbx_os_path_file($dir) : $dir;
1380 }
1381
1382 private function cms_media_thumb_dir($slot) {
1383 $dir = rtrim(dbx_get_file_dir(), '/\\') . '/' . $this->media_thumb_rel_dir($slot);
1384 return function_exists('dbx_os_path_file') ? dbx_os_path_file($dir) : $dir;
1385 }
1386
1387 private function unique_media_name($name, $prefix = '') {
1388 $name = basename((string)$name);
1389 $name = preg_replace('/[^A-Za-z0-9_.-]+/', '-', $name);
1390 $prefix = $prefix !== '' ? rtrim($prefix, '-') . '-' : '';
1391 return date('YmdHis') . '-' . substr(md5($name . microtime(true)), 0, 8) . '-' . $prefix . $name;
1392 }
1393
1394 private function media_slots() {
1395 return array('hero','gallery','inline','header','teaser','footer');
1396 }
1397
1398 private function media_allowed_extensions() {
1399 return array('jpg','jpeg','png','gif','webp','svg','mp4','webm','ogv','ogg','mov','m4v');
1400 }
1401
1402 private function file_from_media_rel($rel) {
1403 $rel = ltrim(str_replace('\\', '/', (string)$rel), '/');
1404 if ($rel === '' || strpos($rel, '..') !== false) return '';
1405 $file = rtrim(dbx_get_file_dir(), '/\\') . '/' . $rel;
1406 return function_exists('dbx_os_path_file') ? dbx_os_path_file($file) : $file;
1407 }
1408
1409 private function relative_file_exists($rel) {
1410 $file = $this->file_from_media_rel($rel);
1411 return $file !== '' && is_file($file) && is_readable($file);
1412 }
1413
1414 private function detect_media_mime($file, $ext) {
1415 $mime = function_exists('mime_content_type') ? (string)@mime_content_type($file) : '';
1416 if ($mime !== '') return $mime;
1417 $ext = strtolower((string)$ext);
1418 if (in_array($ext, array('mp4','webm','ogv','ogg','mov','m4v'), true)) {
1419 return $ext === 'webm' ? 'video/webm' : ($ext === 'mov' ? 'video/quicktime' : 'video/mp4');
1420 }
1421 return $ext === 'svg' ? 'image/svg+xml' : 'image/' . ($ext === 'jpg' ? 'jpeg' : $ext);
1422 }
1423
1424 private function media_file_meta($file, $name) {
1425 $ext = strtolower(pathinfo((string)$name, PATHINFO_EXTENSION));
1426 $mime = $this->detect_media_mime($file, $ext);
1427 $width = 0;
1428 $height = 0;
1429 $img = @getimagesize($file);
1430 if (is_array($img)) {
1431 $width = (int)($img[0] ?? 0);
1432 $height = (int)($img[1] ?? 0);
1433 }
1434 return array(
1435 'mime' => $mime,
1436 'size' => (int)@filesize($file),
1437 'width' => $width,
1438 'height' => $height,
1439 'media_type' => $this->media_type(array('mime' => $mime, 'file_name' => $name)),
1440 );
1441 }
1442
1443 private function is_excluded_cms_media_folder($folder) {
1444 $folder = $this->canonical_media_folder(strtolower(trim(str_replace('\\', '/', (string)$folder), '/')));
1445 return $folder === 'module' || strpos($folder, 'module/') === 0;
1446 }
1447
1448 private function cms_media_exclude_sql() {
1449 return " AND media_folder <> 'module' AND file_path NOT LIKE 'media/module/%'";
1450 }
1451
1452 private function collect_media_files() {
1453 $files = array();
1454 $allowed = $this->media_allowed_extensions();
1455
1456 foreach (array('img','video','youtube','file') as $base_folder) {
1457 $dir = $this->cms_media_base_dir($base_folder);
1458 if (!is_dir($dir) || !is_readable($dir)) continue;
1459 $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS));
1460 foreach ($it as $file) {
1461 if (!$file->isFile() || !$file->isReadable()) continue;
1462 $name = $file->getFilename();
1463 $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
1464 $is_external_base = $base_folder === 'youtube';
1465 if (!in_array($ext, $allowed, true) && !($is_external_base && $ext === 'json')) continue;
1466 $path = str_replace('\\', '/', $file->getPathname());
1467 $root = str_replace('\\', '/', rtrim(dbx_get_file_dir(), '/\\')) . '/';
1468 if (strpos($path, $root) !== 0) continue;
1469 $rel = substr($path, strlen($root));
1470 $type = $base_folder === 'video' ? 'video' : ($is_external_base ? 'external_video' : ($base_folder === 'file' ? 'file' : 'image'));
1471 if ($ext === 'json' && $is_external_base) $type = 'external_video';
1472 $files[$rel] = array(
1473 'rel' => $rel,
1474 'slot' => 'gallery',
1475 'folder' => $this->media_folder_from_path($rel, $type),
1476 'file' => $file->getPathname(),
1477 'name' => $name,
1478 );
1479 }
1480 }
1481 return $files;
1482 }
1483
1484 private function collect_media_thumb_files() {
1485 $files = array();
1486 foreach ($this->media_slots() as $slot) {
1487 $dir = rtrim(dbx_get_file_dir(), '/\\') . '/media/_thumbs/' . $slot . '/';
1488 if (function_exists('dbx_os_path_file')) $dir = dbx_os_path_file($dir);
1489 if (!is_dir($dir) || !is_readable($dir)) continue;
1490 $it = new \DirectoryIterator($dir);
1491 foreach ($it as $file) {
1492 if (!$file->isFile() || !$file->isReadable()) continue;
1493 $rel = $this->media_thumb_rel_dir($slot) . $file->getFilename();
1494 $files[$rel] = array(
1495 'rel' => $rel,
1496 'file' => $file->getPathname(),
1497 );
1498 }
1499 }
1500 return $files;
1501 }
1502
1503 private function active_media_usage_map($db) {
1504 $map = array();
1505 if (!is_object($db)) return $map;
1506 $rows = $db->select($this->dd_media_usage, 'active = 1', 'media_id,content_id,folder_id,slot', 'id', 'ASC', '', 0, 0, 0);
1507 if (is_array($rows)) {
1508 foreach ($rows as $row) {
1509 if (!is_array($row)) continue;
1510 $media_id = (int)($row['media_id'] ?? 0);
1511 if ($media_id <= 0) continue;
1512 if (!isset($map[$media_id])) $map[$media_id] = array();
1513 $map[$media_id][] = $row;
1514 }
1515 }
1516
1517 $content_rows = $db->select(dbxContentLng::ddContent(), "content LIKE '%dbx_mid=%' OR content LIKE '%data-cms-media-id=%'", 'id,folder,content', 'id');
1518 if (is_array($content_rows)) {
1519 foreach ($content_rows as $row) {
1520 if (!is_array($row)) continue;
1521 $content_id = (int)($row['id'] ?? 0);
1522 foreach ($this->inline_media_ids($row['content'] ?? '') as $media_id) {
1523 $media_id = (int)$media_id;
1524 if ($media_id <= 0) continue;
1525 if (!isset($map[$media_id])) $map[$media_id] = array();
1526 $map[$media_id][] = array(
1527 'media_id' => $media_id,
1528 'content_id' => $content_id,
1529 'folder_id' => (int)($row['folder'] ?? 0),
1530 'slot' => 'inline',
1531 );
1532 }
1533 }
1534 }
1535 return $map;
1536 }
1537
1538 private function find_relocated_media_row(array $existing, string $rel, string $name, array $meta, string $media_folder, array $usage_map = array()) {
1539 $name_l = strtolower(trim($name));
1540 if ($name_l === '') return null;
1541 $size = (int)($meta['size'] ?? 0);
1542 $width = (int)($meta['width'] ?? 0);
1543 $height = (int)($meta['height'] ?? 0);
1544 $media_folder = $this->canonical_media_folder($media_folder);
1545 $best = null;
1546 $best_score = 0;
1547 foreach ($existing as $row) {
1548 if (!is_array($row)) continue;
1549 $id = (int)($row['id'] ?? 0);
1550 $storage = trim((string)($row['storage_type'] ?? ''));
1551 if ($storage === 'external' || (string)($row['media_type'] ?? '') === 'external_video') continue;
1552 if (strtolower((string)($row['file_name'] ?? '')) !== $name_l) continue;
1553 $existing_path = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
1554 if ($existing_path === $rel) continue;
1555 if ($existing_path !== '' && $this->relative_file_exists($existing_path)) continue;
1556 $active = (int)($row['active'] ?? 0) === 1;
1557 $used = $id > 0 && !empty($usage_map[$id]);
1558 if (!$active && !$used) continue;
1559 $score = 1;
1560 if ($used) $score += 10;
1561 if ($active) $score += 3;
1562 if ($size > 0 && (int)($row['size'] ?? 0) === $size) $score += 2;
1563 if ($width > 0 && $height > 0 && (int)($row['width'] ?? 0) === $width && (int)($row['height'] ?? 0) === $height) $score += 2;
1564 $old_folder = $this->canonical_media_folder(trim((string)($row['media_folder'] ?? '')));
1565 if ($old_folder !== '' && $old_folder === $media_folder) $score += 1;
1566 if ($score > $best_score) {
1567 $best_score = $score;
1568 $best = $row;
1569 }
1570 }
1571 return $best;
1572 }
1573
1574 private function sync_cms_media_files($db) {
1575 $existing = $db->select($this->dd_media, '', '*', 'id');
1576 if (!is_array($existing)) $existing = array();
1577 $usage_map = $this->active_media_usage_map($db);
1578
1579 $by_path = array();
1580 foreach ($existing as $row) {
1581 if (!is_array($row)) continue;
1582 $path = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
1583 if ($path !== '') $by_path[$path] = $row;
1584 }
1585
1586 $seen = array();
1587 $relocated_ids = array();
1588 foreach ($this->collect_media_files() as $rel => $file_info) {
1589 $file = (string)($file_info['file'] ?? '');
1590 if ($file === '' || !is_file($file) || !is_readable($file)) continue;
1591 $name = basename($file);
1592 $slot = $this->valid_media_slot($file_info['slot'] ?? $this->slot_from_media_rel($rel));
1593 $seen[$rel] = 1;
1594 $meta = $this->media_file_meta($file, $name);
1595 $media_type = $meta['media_type'];
1596 if ($media_type === 'file' && preg_match('~^media/youtube/~', $rel)) $media_type = 'external_video';
1597 $media_folder = $this->media_folder_from_path($rel, $media_type);
1598 $storage_type = $media_type === 'external_video' ? 'external' : 'local';
1599 $external_meta = $media_type === 'external_video' ? $this->external_video_json_meta($file) : array();
1600
1601 $existing_key = $rel;
1602 if (!isset($by_path[$existing_key])) {
1603 }
1604
1605 $row = isset($by_path[$existing_key]) ? $by_path[$existing_key] : null;
1606 if (!is_array($row)) {
1607 $row = $this->find_relocated_media_row($existing, $rel, $name, $meta, $media_folder, $usage_map);
1608 }
1609 if (is_array($row)) {
1610 $id = (int)($row['id'] ?? 0);
1611 if ($id <= 0) continue;
1612
1613 $needs_thumb = $storage_type === 'local' && (empty($row['thumb_file_path']) || !$this->media_thumb_exists($row));
1614 $thumb = $needs_thumb ? $this->create_media_thumbnail($file, $media_folder, $name, $meta['mime']) : array();
1615 $update = array(
1616 'active' => 1,
1617 'mime' => $meta['mime'],
1618 'size' => $meta['size'],
1619 'width' => $meta['width'],
1620 'height' => $meta['height'],
1621 'media_type' => $media_type,
1622 'storage_type' => $storage_type,
1623 'media_folder' => $media_folder,
1624 'file_name' => $name,
1625 'file_path' => $rel,
1626 );
1627 if ($external_meta) $update = array_merge($update, $external_meta);
1628 if ($thumb) $update = array_merge($update, $thumb);
1629 $db->update($this->dd_media, $update, $id);
1630 if (($row['file_path'] ?? '') !== $rel) $this->flush_media_by_media_id($db, $id);
1631 $by_path[$rel] = array_merge($row, $update);
1632 if (($row['file_path'] ?? '') !== $rel) $relocated_ids[$id] = 1;
1633 continue;
1634 }
1635
1636 $thumb = $storage_type === 'local' ? $this->create_media_thumbnail($file, $media_folder, $name, $meta['mime']) : array();
1637 $title = pathinfo($name, PATHINFO_FILENAME);
1638 if (!empty($external_meta['title'])) $title = (string)$external_meta['title'];
1639 $insert = array(
1640 'active' => 1,
1641 'content_id' => 0,
1642 'folder_id' => 0,
1643 'slot' => $slot,
1644 'usage' => $slot,
1645 'sorter' => $this->next_media_sorter($db, 0, $slot),
1646 'template' => '',
1647 'title' => $title,
1648 'alt' => $title,
1649 'file_name' => $name,
1650 'file_path' => $rel,
1651 'mime' => $meta['mime'],
1652 'size' => $meta['size'],
1653 'width' => $meta['width'],
1654 'height' => $meta['height'],
1655 'media_type' => $media_type,
1656 'storage_type' => $storage_type,
1657 'media_folder' => $media_folder,
1658 );
1659 if ($external_meta) $insert = array_merge($insert, $external_meta);
1660 if ($thumb) $insert = array_merge($insert, $thumb);
1661 $id = ($db->insert($this->dd_media, $insert) === 1) ? $db->get_insert_id() : 0;
1662 }
1663
1664 foreach ($existing as $row) {
1665 if (!is_array($row)) continue;
1666 $id = (int)($row['id'] ?? 0);
1667 $path = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
1668 if ($id <= 0 || (int)($row['active'] ?? 0) !== 1) continue;
1669 if (isset($relocated_ids[$id])) continue;
1670 if (preg_match('~^media/(hero|gallery|inline|header|teaser|footer)/~', $path) && !isset($seen[$path])) {
1671 $db->update($this->dd_media, array('active' => 0), $id);
1672 continue;
1673 }
1674 if (!isset($seen[$path]) && !$this->relative_file_exists($path)) {
1675 $db->update($this->dd_media, array('active' => 0), $id);
1676 }
1677 if ((int)($row['active'] ?? 0) === 1 && ($this->is_excluded_cms_media_folder((string)($row['media_folder'] ?? '')) || preg_match('~^media/module/~', $path))) {
1678 $db->update($this->dd_media, array('active' => 0), $id);
1679 }
1680 }
1681 }
1682
1683 private function normalize_media_row($row) {
1684 $row = is_array($row) ? $row : array();
1685 $file_path = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
1686 $row['url'] = $this->media_url($row);
1687 $row['thumb_url'] = $this->media_url($row, true);
1688 $row['media_type'] = $this->media_type($row);
1689 $row['storage_type'] = trim((string)($row['storage_type'] ?? '')) ?: ($row['media_type'] === 'external_video' ? 'external' : 'local');
1690 $row['media_folder'] = $this->canonical_media_folder(trim((string)($row['media_folder'] ?? '')) ?: $this->media_folder_from_path((string)($row['file_path'] ?? ''), $row['media_type']));
1691 if ($row['media_type'] === 'external_video') {
1692 $row['url'] = (string)($row['embed_url'] ?? $row['external_url'] ?? '');
1693 $row['thumb_url'] = $this->external_video_thumb_url($row);
1694 }
1695 return $row;
1696 }
1697
1698 private function normalize_usage_row($row) {
1699 $row = is_array($row) ? $row : array();
1700 $row['id'] = (int)($row['id'] ?? 0);
1701 $row['media_id'] = (int)($row['media_id'] ?? 0);
1702 $row['content_id'] = (int)($row['content_id'] ?? 0);
1703 $row['folder_id'] = (int)($row['folder_id'] ?? 0);
1704 $row['slot'] = $this->valid_media_slot($row['slot'] ?? 'gallery');
1705 return $row;
1706 }
1707
1708 private function usage_where($media_id = 0, $content_id = 0, $folder_id = 0, $slot = '') {
1709 $where = 'active = 1';
1710 if ((int)$media_id > 0) $where .= ' AND media_id = ' . (int)$media_id;
1711 if ((int)$content_id > 0) $where .= ' AND content_id = ' . (int)$content_id;
1712 if ((int)$folder_id > 0) $where .= ' AND folder_id = ' . (int)$folder_id;
1713 if ((string)$slot !== '') $where .= " AND slot = '" . str_replace("'", "''", $this->valid_media_slot($slot)) . "'";
1714 return $where;
1715 }
1716
1717 private function next_media_usage_sorter($db, $content_id, $folder_id, $slot) {
1718 $where = $this->usage_where(0, $content_id, $folder_id, $slot);
1719 $rows = $db->select($this->dd_media_usage, $where, '*', 'sorter,id', 'DESC', '', 1, 0, 0);
1720 $max = 0;
1721 if (is_array($rows) && isset($rows[0]) && is_array($rows[0])) $max = (int)($rows[0]['sorter'] ?? 0);
1722 return sprintf('%04d', $max + 10);
1723 }
1724
1725 private function create_media_usage($db, $media_id, $content_id, $folder_id, $slot, $template = '', $caption = '', $settings = '') {
1726 $media_id = (int)$media_id;
1727 $content_id = (int)$content_id;
1728 $folder_id = (int)$folder_id;
1729 $slot = $this->valid_media_slot($slot);
1730 if ($media_id <= 0 || ($content_id <= 0 && $folder_id <= 0)) return 0;
1731 if ($slot === 'hero') {
1732 if ($content_id > 0) $db->update($this->dd_media_usage, array('active' => 0), "content_id = " . $content_id . " AND slot = 'hero' AND active = 1", 0, 1, 1, 0);
1733 elseif ($folder_id > 0) $db->update($this->dd_media_usage, array('active' => 0), "folder_id = " . $folder_id . " AND slot = 'hero' AND active = 1", 0, 1, 1, 0);
1734 }
1735 return ($db->insert($this->dd_media_usage, array(
1736 'active' => 1,
1737 'media_id' => $media_id,
1738 'content_id' => $content_id,
1739 'folder_id' => $folder_id,
1740 'slot' => $slot,
1741 'sorter' => $this->next_media_usage_sorter($db, $content_id, $folder_id, $slot),
1742 'template' => $this->clean_text($template, 80),
1743 'caption' => $this->clean_text($caption, 0),
1744 'settings' => $this->clean_text($settings, 0),
1745 )) === 1) ? $db->get_insert_id() : 0;
1746 }
1747
1748 private function get_media_process_state($token) {
1749 $token = preg_replace('/[^A-Za-z0-9_-]+/', '', (string)$token);
1750 if ($token === '') return array();
1751 return $_SESSION['dbx']['dbxContent_admin']['media_process'][$token] ?? array();
1752 }
1753
1754 private function set_media_process_state($token, array $state) {
1755 $token = preg_replace('/[^A-Za-z0-9_-]+/', '', (string)$token);
1756 if ($token === '') return;
1757 if (!isset($_SESSION['dbx']) || !is_array($_SESSION['dbx'])) $_SESSION['dbx'] = array();
1758 if (!isset($_SESSION['dbx']['dbxContent_admin']) || !is_array($_SESSION['dbx']['dbxContent_admin'])) $_SESSION['dbx']['dbxContent_admin'] = array();
1759 if (!isset($_SESSION['dbx']['dbxContent_admin']['media_process']) || !is_array($_SESSION['dbx']['dbxContent_admin']['media_process'])) $_SESSION['dbx']['dbxContent_admin']['media_process'] = array();
1760 $_SESSION['dbx']['dbxContent_admin']['media_process'][$token] = $state;
1761 }
1762
1763 private function slot_from_media_rel($rel, $fallback = 'gallery') {
1764 $rel = ltrim(str_replace('\\', '/', (string)$rel), '/');
1765 if (preg_match('~^media/(hero|gallery|inline|header|teaser|footer)/~', $rel, $m)) {
1766 return $this->valid_media_slot($m[1], $fallback);
1767 }
1768 return $this->valid_media_slot($fallback);
1769 }
1770
1771 private function build_media_process_state($db, $token) {
1772 $rows = $db->select($this->dd_media, '', '*', 'id');
1773 if (!is_array($rows)) $rows = array();
1774
1775 $by_path = array();
1776 $referenced_thumbs = array();
1777 $tasks = array();
1778 $needs_content_cleanup = false;
1779
1780 foreach ($rows as $row) {
1781 if (!is_array($row)) continue;
1782 $id = (int)($row['id'] ?? 0);
1783 $path = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
1784 if ($path !== '') $by_path[$path] = $id;
1785 $thumb = ltrim(str_replace('\\', '/', (string)($row['thumb_file_path'] ?? '')), '/');
1786 if ($thumb !== '') $referenced_thumbs[$thumb] = 1;
1787 if ($id > 0 && preg_match('~^media/(hero|gallery|inline|header|teaser|footer)/~', $path)) {
1788 $tasks[] = array('type' => 'record', 'id' => $id);
1789 if (!$this->media_file_exists($row)) $needs_content_cleanup = true;
1790 }
1791 if ($id > 0
1792 && (int)($row['active'] ?? 0) === 1
1793 && $this->media_file_exists($row)
1794 && preg_match('~^media/(img|video|file|module)/~', $path)
1795 && (empty($row['thumb_file_path']) || !$this->media_thumb_exists($row))) {
1796 $tasks[] = array('type' => 'thumb', 'id' => $id);
1797 }
1798 if ($id > 0 && (int)($row['active'] ?? 0) !== 1) $needs_content_cleanup = true;
1799 }
1800
1801 foreach ($this->collect_media_files() as $rel => $file) {
1802 if (!isset($by_path[$rel])) {
1803 $tasks[] = array('type' => 'import', 'rel' => $rel, 'slot' => $file['slot']);
1804 }
1805 }
1806
1807 foreach ($this->collect_media_thumb_files() as $rel => $file) {
1808 if (!isset($referenced_thumbs[$rel])) {
1809 $tasks[] = array('type' => 'delete_thumb', 'rel' => $rel);
1810 }
1811 }
1812
1813 if ($needs_content_cleanup || $this->content_inline_cleanup_needed($db)) {
1814 $tasks[] = array('type' => 'content_cleanup');
1815 }
1816
1817 if ($this->content_inline_placeholder_repair_needed($db)) {
1818 $tasks[] = array('type' => 'content_placeholder_repair');
1819 }
1820
1821 $cache_content_ids = $this->content_media_reference_ids($db);
1822 if ($cache_content_ids) {
1823 $tasks[] = array('type' => 'content_cache_flush', 'ids' => $cache_content_ids);
1824 }
1825
1826 return array(
1827 'proc_key' => $token,
1828 'proc_type' => 'media_maintenance',
1829 'status' => empty($tasks) ? 'finished' : 'running',
1830 'phase' => 'media_prepare',
1831 'message' => empty($tasks) ? 'Keine Wartung notwendig.' : 'Medienwartung vorbereitet.',
1832 'tasks' => $tasks,
1833 'pos' => 0,
1834 'total' => count($tasks),
1835 'created_thumbs' => 0,
1836 'imported_media' => 0,
1837 'relinked_media' => 0,
1838 'deleted_thumbs' => 0,
1839 'deactivated_media' => 0,
1840 'cleaned_inline_refs' => 0,
1841 'cleaned_content_pages' => 0,
1842 'repaired_inline_refs' => 0,
1843 'flushed_content_pages' => 0,
1844 'errors' => 0,
1845 'percent' => empty($tasks) ? 100 : 0,
1846 'step_percent' => empty($tasks) ? 100 : 0,
1847 'updated_at' => date('H:i:s'),
1848 );
1849 }
1850
1851 private function update_media_process_percent(array &$state) {
1852 $total = max(1, (int)($state['total'] ?? 0));
1853 $pos = max(0, min($total, (int)($state['pos'] ?? 0)));
1854 $percent = (int)floor(($pos / $total) * 100);
1855 if (($state['status'] ?? '') === 'finished') $percent = 100;
1856 $state['percent'] = max(0, min(100, $percent));
1857 $state['step_percent'] = $state['percent'];
1858 }
1859
1860 private function media_process_message(array $state, $current = '') {
1861 $parts = array();
1862 if ((int)($state['created_thumbs'] ?? 0) > 0) $parts[] = 'Thumbs +' . (int)$state['created_thumbs'];
1863 if ((int)($state['imported_media'] ?? 0) > 0) $parts[] = 'Import +' . (int)$state['imported_media'];
1864 if ((int)($state['relinked_media'] ?? 0) > 0) $parts[] = 'Zuordnung repariert ' . (int)$state['relinked_media'];
1865 if ((int)($state['deleted_thumbs'] ?? 0) > 0) $parts[] = 'Thumbs weg ' . (int)$state['deleted_thumbs'];
1866 if ((int)($state['deactivated_media'] ?? 0) > 0) $parts[] = 'Medien aus ' . (int)$state['deactivated_media'];
1867 if ((int)($state['cleaned_inline_refs'] ?? 0) > 0) $parts[] = 'Content bereinigt ' . (int)$state['cleaned_inline_refs'];
1868 if ((int)($state['repaired_inline_refs'] ?? 0) > 0) $parts[] = 'Inline repariert ' . (int)$state['repaired_inline_refs'];
1869 if ((int)($state['flushed_content_pages'] ?? 0) > 0) $parts[] = 'Cache aktualisiert ' . (int)$state['flushed_content_pages'];
1870 if ((int)($state['errors'] ?? 0) > 0) $parts[] = 'Fehler ' . (int)$state['errors'];
1871 $msg = empty($parts) ? 'Keine Aenderungen.' : implode(' | ', $parts);
1872 if ($current !== '') $msg .= ' | ' . $current;
1873 return $msg;
1874 }
1875
1876 private function content_inline_cleanup_needed($db) {
1877 $rows = $db->select(dbxContentLng::ddContent(), "content LIKE '%dbx_mid=%' OR content LIKE '%data-cms-media-id=%'", 'id,content', 'id');
1878 if (!is_array($rows)) return false;
1879 foreach ($rows as $row) {
1880 if (!is_array($row)) continue;
1881 foreach ($this->inline_media_ids($row['content'] ?? '') as $id) {
1882 $media = $db->select1($this->dd_media, $id);
1883 if (!is_array($media) || (int)($media['active'] ?? 0) !== 1 || !$this->media_file_exists($media)) return true;
1884 }
1885 }
1886 return false;
1887 }
1888
1889 private function content_media_reference_ids($db) {
1890 $ids = array();
1891 $rows = $db->select(dbxContentLng::ddContent(), "content LIKE '%dbx_mid=%' OR content LIKE '%data-cms-media-id=%'", 'id,content', 'id');
1892 if (!is_array($rows)) return $ids;
1893 foreach ($rows as $row) {
1894 if (!is_array($row)) continue;
1895 $content_id = (int)($row['id'] ?? 0);
1896 if ($content_id <= 0) continue;
1897 if (!$this->inline_media_ids($row['content'] ?? '')) continue;
1898 $ids[$content_id] = $content_id;
1899 }
1900 return array_values($ids);
1901 }
1902
1903 private function content_inline_placeholder_repair_needed($db) {
1904 foreach (dbxContentLngSync::accessibleLngs() as $lng) {
1905 $rows = $db->select(dbxContentLng::ddContent((string)$lng), "content LIKE '%dbx-cms-inline-media-missing%'", 'id,content', 'id');
1906 if (!is_array($rows)) continue;
1907 foreach ($rows as $row) {
1908 if (!is_array($row)) continue;
1909 if (preg_match('/\bdbx-cms-inline-media-missing\b/i', (string)($row['content'] ?? ''))) return true;
1910 }
1911 }
1912 return false;
1913 }
1914
1915 private function repair_content_inline_placeholders($db) {
1916 $pages = 0;
1917 $refs = 0;
1918 $prev_lng = dbxContentLng::current();
1919
1920 foreach (dbxContentLngSync::accessibleLngs() as $lng) {
1921 $lng = (string)$lng;
1922 dbx()->set_system_var('dbx_lng', $lng);
1923 $dd = dbxContentLng::ddContent($lng);
1924 $rows = $db->select($dd, "content LIKE '%dbx-cms-inline-media-missing%'", 'id,folder,content', 'id');
1925 if (!is_array($rows)) continue;
1926
1927 foreach ($rows as $row) {
1928 if (!is_array($row)) continue;
1929 $content_id = (int)($row['id'] ?? 0);
1930 if ($content_id <= 0) continue;
1931 $html = (string)($row['content'] ?? '');
1932 if (stripos($html, 'dbx-cms-inline-media-missing') === false) continue;
1933
1934 $changed = 0;
1935 $clean = preg_replace_callback(
1936 '/<(p|figure|div)\b([^>]*(?:\bdbx-cms-inline-media-missing-wrap\b|\bdbx-cms-inline-media\b|\bdata-cms-media-id=)[^>]*)>([\s\S]*?\bdbx-cms-inline-media-missing\b[\s\S]*?)<\/\1>/i',
1937 function($m) use ($db, &$changed) {
1938 $block = (string)($m[0] ?? '');
1939 if (!preg_match('/\bdata-cms-media-id=["\']?([0-9]+)/i', $block, $id_match)) return $block;
1940 $id = (int)$id_match[1];
1941 if ($id <= 0) return $block;
1942 $media = $db->select1($this->dd_media, $id);
1943 if (!is_array($media) || (int)($media['active'] ?? 0) !== 1 || !$this->media_file_exists($media)) return $block;
1944 $replacement = $this->inline_media_embed_html($media);
1945 if ($replacement === '') return $block;
1946 $changed++;
1947 return $replacement;
1948 },
1949 $html
1950 );
1951 if ($clean === null || $clean === $html || $changed <= 0) continue;
1952 $ok = $db->update($dd, array('content' => $clean), $content_id, 0, 0, 0, 0);
1953 if ($ok < 0) continue;
1954 $this->sync_inline_media_usage($db, $content_id, $clean, (int)($row['folder'] ?? 0));
1955 $this->flush_saved_page_cache($db, $content_id);
1956 $pages++;
1957 $refs += $changed;
1958 }
1959 }
1960
1961 dbx()->set_system_var('dbx_lng', $prev_lng);
1962
1963 return array('pages' => $pages, 'refs' => $refs);
1964 }
1965
1966 private function remove_inline_media_ids_from_html($html, array $ids, &$removed = 0) {
1967 $html = (string)$html;
1968 $removed = 0;
1969 $ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
1970 if (!$ids || trim($html) === '') return $html;
1971
1972 if (class_exists('\DOMDocument')) {
1973 $doc = new \DOMDocument('1.0', 'UTF-8');
1974 $prev = libxml_use_internal_errors(true);
1975 $wrapped = '<div id="dbx-cms-clean-root">' . $html . '</div>';
1976 $ok = $doc->loadHTML('<?xml encoding="utf-8" ?>' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1977 libxml_clear_errors();
1978 libxml_use_internal_errors($prev);
1979 if ($ok) {
1980 $xpath = new \DOMXPath($doc);
1981 $root_nodes = $xpath->query("//*[@id='dbx-cms-clean-root']");
1982 $root = $root_nodes && $root_nodes->length ? $root_nodes->item(0) : null;
1983 foreach ($ids as $id) {
1984 $query = "//*[@data-cms-media-id='" . $id . "' or contains(@src,'dbx_mid=" . $id . "') or contains(@href,'dbx_mid=" . $id . "')]";
1985 $nodes = $xpath->query($query);
1986 $targets = array();
1987 if ($nodes) {
1988 foreach ($nodes as $node) {
1989 $target = $node;
1990 for ($p = $node; $p && $p !== $root; $p = $p->parentNode) {
1991 if ($p->nodeType !== XML_ELEMENT_NODE) continue;
1992 $tag = strtolower($p->nodeName);
1993 $class = ' ' . (string)$p->getAttribute('class') . ' ';
1994 if ($tag === 'figure' || strpos($class, ' dbx-cms-inline-media ') !== false) {
1995 $target = $p;
1996 }
1997 }
1998 if ($target && $target->parentNode) $targets[spl_object_hash($target)] = $target;
1999 }
2000 }
2001 foreach ($targets as $target) {
2002 if ($target->parentNode) {
2003 $target->parentNode->removeChild($target);
2004 $removed++;
2005 }
2006 }
2007 }
2008 if ($root) {
2009 $out = '';
2010 foreach (iterator_to_array($root->childNodes) as $child) {
2011 $out .= $doc->saveHTML($child);
2012 }
2013 return $out;
2014 }
2015 }
2016 }
2017
2018 foreach ($ids as $id) {
2019 $before = $html;
2020 $id_re = preg_quote((string)$id, '~');
2021 $html = preg_replace('~<(p|figure|div)\b[^>]*(?:data-cms-media-id=["\']?' . $id_re . '\b|class=["\'][^"\']*dbx-cms-inline-media[^"\']*["\'][^>]*>.*?(?:dbx_mid=' . $id_re . '\b|data-cms-media-id=["\']?' . $id_re . '\b))[\s\S]*?</\1>~i', '', $html);
2022 $html = preg_replace('~<(img|video|iframe|source)\b[^>]*(?:dbx_mid=' . $id_re . '\b|data-cms-media-id=["\']?' . $id_re . '\b)[^>]*>(?:</\1>)?~i', '', $html);
2023 if ($html !== $before) $removed++;
2024 }
2025 return $html;
2026 }
2027
2028 private function clean_content_inline_media($db) {
2029 $rows = $db->select(dbxContentLng::ddContent(), "content LIKE '%dbx_mid=%' OR content LIKE '%data-cms-media-id=%'", 'id,folder,content', 'id');
2030 if (!is_array($rows)) return array('pages' => 0, 'refs' => 0);
2031 $pages = 0;
2032 $refs = 0;
2033 foreach ($rows as $row) {
2034 if (!is_array($row)) continue;
2035 $content_id = (int)($row['id'] ?? 0);
2036 if ($content_id <= 0) continue;
2037 $bad = array();
2038 foreach ($this->inline_media_ids($row['content'] ?? '') as $id) {
2039 $media = $db->select1($this->dd_media, $id);
2040 if (!is_array($media) || (int)($media['active'] ?? 0) !== 1 || !$this->media_file_exists($media)) $bad[] = $id;
2041 }
2042 if (!$bad) continue;
2043 $removed = 0;
2044 $clean = $this->remove_inline_media_ids_from_html((string)($row['content'] ?? ''), $bad, $removed);
2045 if ($clean !== (string)($row['content'] ?? '')) {
2046 $db->update(dbxContentLng::ddContent(), array('content' => $clean), $content_id);
2047 $this->sync_inline_media_usage($db, $content_id, $clean, (int)($row['folder'] ?? 0));
2048 $this->flush_saved_page_cache($db, $content_id);
2049 $pages++;
2050 $refs += max($removed, count($bad));
2051 }
2052 }
2053 return array('pages' => $pages, 'refs' => $refs);
2054 }
2055
2056 private function import_media_file($db, $rel, $slot, &$relinked = null) {
2057 $relinked = false;
2058 $rel = ltrim(str_replace('\\', '/', (string)$rel), '/');
2059 $slot = $this->valid_media_slot($slot);
2060 $file = $this->file_from_media_rel($rel);
2061 if ($file === '' || !is_file($file) || !is_readable($file)) return 0;
2062
2063 $name = basename($file);
2064 $meta = $this->media_file_meta($file, $name);
2065 $media_folder = $this->media_folder_from_path($rel, $meta['media_type']);
2066 $existing = $db->select($this->dd_media, '', '*', 'id');
2067 if (!is_array($existing)) $existing = array();
2068 $relocated = $this->find_relocated_media_row($existing, $rel, $name, $meta, $media_folder, $this->active_media_usage_map($db));
2069 if (is_array($relocated)) {
2070 $id = (int)($relocated['id'] ?? 0);
2071 if ($id > 0) {
2072 $thumb = (empty($relocated['thumb_file_path']) || !$this->media_thumb_exists($relocated))
2073 ? $this->create_media_thumbnail($file, $media_folder, $name, $meta['mime'])
2074 : array();
2075 $update = array(
2076 'active' => 1,
2077 'file_path' => $rel,
2078 'file_name' => $name,
2079 'media_folder' => $media_folder,
2080 'mime' => $meta['mime'],
2081 'size' => $meta['size'],
2082 'width' => $meta['width'],
2083 'height' => $meta['height'],
2084 'media_type' => $meta['media_type'],
2085 'storage_type' => 'local',
2086 );
2087 if ($thumb) $update = array_merge($update, $thumb);
2088 $db->update($this->dd_media, $update, $id);
2089 $this->flush_media_by_media_id($db, $id);
2090 $relinked = true;
2091 return $id;
2092 }
2093 }
2094
2095 $title = pathinfo($name, PATHINFO_FILENAME);
2096 $insert = array(
2097 'active' => 1,
2098 'content_id' => 0,
2099 'folder_id' => 0,
2100 'slot' => $slot,
2101 'usage' => $slot,
2102 'sorter' => $this->next_media_sorter($db, 0, $slot),
2103 'template' => '',
2104 'title' => $title,
2105 'alt' => $title,
2106 'file_name' => $name,
2107 'file_path' => $rel,
2108 'mime' => $meta['mime'],
2109 'size' => $meta['size'],
2110 'width' => $meta['width'],
2111 'height' => $meta['height'],
2112 'media_type' => $meta['media_type'],
2113 'media_folder' => $media_folder,
2114 'storage_type' => 'local',
2115 );
2116 $thumb = $this->create_media_thumbnail($file, $media_folder, $name, $meta['mime']);
2117 if ($thumb) $insert = array_merge($insert, $thumb);
2118 return ($db->insert($this->dd_media, $insert) === 1) ? $db->get_insert_id() : 0;
2119 }
2120
2121 private function run_media_process_task($db, array &$state, array $task) {
2122 $type = (string)($task['type'] ?? '');
2123
2124 if ($type === 'delete_thumb') {
2125 $rel = ltrim(str_replace('\\', '/', (string)($task['rel'] ?? '')), '/');
2126 $file = $this->file_from_media_rel($rel);
2127 if ($file !== '' && is_file($file) && @unlink($file)) {
2128 $state['deleted_thumbs'] = (int)($state['deleted_thumbs'] ?? 0) + 1;
2129 }
2130 $state['phase'] = 'media_cleanup';
2131 return basename($rel);
2132 }
2133
2134 if ($type === 'thumb') {
2135 $id = (int)($task['id'] ?? 0);
2136 $row = $id > 0 ? $db->select1($this->dd_media, $id) : array();
2137 if (!is_array($row)) return '#' . $id;
2138 $rel = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
2139 $file = $this->file_from_media_rel($rel);
2140 if ($file === '' || !is_file($file) || !is_readable($file)) {
2141 $state['errors'] = (int)($state['errors'] ?? 0) + 1;
2142 return basename($rel);
2143 }
2144 $name = basename($file);
2145 $meta = $this->media_file_meta($file, $name);
2146 $media_folder = $this->media_folder_from_path($rel, $meta['media_type']);
2147 $thumb = $this->create_media_thumbnail($file, $media_folder, $name, $meta['mime']);
2148 if ($thumb) {
2149 $db->update($this->dd_media, $thumb, $id);
2150 $state['created_thumbs'] = (int)($state['created_thumbs'] ?? 0) + 1;
2151 } else {
2152 $state['errors'] = (int)($state['errors'] ?? 0) + 1;
2153 }
2154 $state['phase'] = 'media_thumbs';
2155 return basename($rel);
2156 }
2157
2158 if ($type === 'import') {
2159 $rel = ltrim(str_replace('\\', '/', (string)($task['rel'] ?? '')), '/');
2160 $slot = $this->valid_media_slot($task['slot'] ?? $this->slot_from_media_rel($rel));
2161 $relinked = false;
2162 $id = $this->import_media_file($db, $rel, $slot, $relinked);
2163 if ($id > 0) {
2164 if ($relinked) {
2165 $state['relinked_media'] = (int)($state['relinked_media'] ?? 0) + 1;
2166 } else {
2167 $state['imported_media'] = (int)($state['imported_media'] ?? 0) + 1;
2168 }
2169 $row = $db->select1($this->dd_media, $id);
2170 if (is_array($row) && !empty($row['thumb_file_path'])) {
2171 $state['created_thumbs'] = (int)($state['created_thumbs'] ?? 0) + 1;
2172 }
2173 } else {
2174 $state['errors'] = (int)($state['errors'] ?? 0) + 1;
2175 }
2176 $state['phase'] = 'media_import';
2177 return basename($rel);
2178 }
2179
2180 if ($type === 'content_cleanup') {
2181 $done = $this->clean_content_inline_media($db);
2182 $state['cleaned_inline_refs'] = (int)($state['cleaned_inline_refs'] ?? 0) + (int)($done['refs'] ?? 0);
2183 $state['cleaned_content_pages'] = (int)($state['cleaned_content_pages'] ?? 0) + (int)($done['pages'] ?? 0);
2184 $state['phase'] = 'content_cleanup';
2185 return 'Content';
2186 }
2187
2188 if ($type === 'content_placeholder_repair') {
2189 $done = $this->repair_content_inline_placeholders($db);
2190 $state['repaired_inline_refs'] = (int)($state['repaired_inline_refs'] ?? 0) + (int)($done['refs'] ?? 0);
2191 $state['cleaned_content_pages'] = (int)($state['cleaned_content_pages'] ?? 0) + (int)($done['pages'] ?? 0);
2192 $state['phase'] = 'content_repair';
2193 return 'Inline';
2194 }
2195
2196 if ($type === 'content_cache_flush') {
2197 $ids = is_array($task['ids'] ?? null) ? $task['ids'] : array();
2198 $count = 0;
2199 foreach ($ids as $content_id) {
2200 $content_id = (int)$content_id;
2201 if ($content_id <= 0) continue;
2202 $this->flush_saved_page_cache($db, $content_id);
2203 $count++;
2204 }
2205 $state['flushed_content_pages'] = (int)($state['flushed_content_pages'] ?? 0) + $count;
2206 $state['phase'] = 'content_cache';
2207 return 'Cache';
2208 }
2209
2210 if ($type === 'record') {
2211 $id = (int)($task['id'] ?? 0);
2212 $row = $id > 0 ? $db->select1($this->dd_media, $id) : array();
2213 if (!is_array($row)) return '#' . $id;
2214
2215 $rel = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
2216 $file = $this->file_from_media_rel($rel);
2217 if ($file === '' || !is_file($file) || !is_readable($file)) {
2218 $thumb = $this->source_thumb_file($row);
2219 if ($thumb !== '' && @unlink($thumb)) {
2220 $state['deleted_thumbs'] = (int)($state['deleted_thumbs'] ?? 0) + 1;
2221 }
2222 if ((int)($row['active'] ?? 0) !== 0 || !empty($row['thumb_file_path'])) {
2223 $db->update($this->dd_media, array(
2224 'active' => 0,
2225 'thumb_file_path' => '',
2226 'thumb_width' => 0,
2227 'thumb_height' => 0,
2228 ), $id);
2229 $state['deactivated_media'] = (int)($state['deactivated_media'] ?? 0) + 1;
2230 }
2231 $state['phase'] = 'media_cleanup';
2232 return basename($rel);
2233 }
2234
2235 $slot = $this->slot_from_media_rel($rel, (string)($row['slot'] ?? 'gallery'));
2236 $name = basename($file);
2237 $meta = $this->media_file_meta($file, $name);
2238 $update = array();
2239 if ((int)($row['active'] ?? 0) !== 1
2240 || (string)($row['slot'] ?? '') !== $slot
2241 || (string)($row['mime'] ?? '') !== $meta['mime']
2242 || (int)($row['size'] ?? 0) !== $meta['size']
2243 || (string)($row['media_type'] ?? '') !== $meta['media_type']) {
2244 $update = array(
2245 'active' => 1,
2246 'slot' => $slot,
2247 'usage' => $slot,
2248 'mime' => $meta['mime'],
2249 'size' => $meta['size'],
2250 'width' => $meta['width'],
2251 'height' => $meta['height'],
2252 'media_type' => $meta['media_type'],
2253 );
2254 }
2255
2256 if (empty($row['thumb_file_path']) || !$this->media_thumb_exists($row)) {
2257 $thumb = $this->create_media_thumbnail($file, $slot, $name, $meta['mime']);
2258 if ($thumb) {
2259 $update = array_merge($update, $thumb);
2260 $state['created_thumbs'] = (int)($state['created_thumbs'] ?? 0) + 1;
2261 }
2262 }
2263
2264 if (!empty($update)) $db->update($this->dd_media, $update, $id);
2265 $state['phase'] = 'media_thumbs';
2266 return basename($rel);
2267 }
2268
2269 return '';
2270 }
2271
2272 private function tick_media_process($db, array &$state) {
2273 if (($state['status'] ?? '') !== 'running') return;
2274 $started = microtime(true);
2275 $limit = 8;
2276 $processed = 0;
2277 $current = '';
2278 $total = (int)($state['total'] ?? 0);
2279
2280 while ((int)($state['pos'] ?? 0) < $total && $processed < $limit && (microtime(true) - $started) < 3.2) {
2281 $idx = (int)$state['pos'];
2282 $task = is_array($state['tasks'][$idx] ?? null) ? $state['tasks'][$idx] : array();
2283 $current = $this->run_media_process_task($db, $state, $task);
2284 $state['pos'] = $idx + 1;
2285 $processed++;
2286 }
2287
2288 if ((int)($state['pos'] ?? 0) >= $total) {
2289 $state['status'] = 'finished';
2290 $state['phase'] = 'media_done';
2291 $state['tasks'] = array();
2292 $current = '';
2293 }
2294
2295 $this->update_media_process_percent($state);
2296 $state['message'] = $this->media_process_message($state, $current);
2297 if (($state['status'] ?? '') === 'finished') $state['message'] = 'Fertig. ' . $state['message'];
2298 $state['updated_at'] = date('H:i:s');
2299 }
2300
2301 private function render_media_process(array $state, $next_url) {
2302 $status = (string)($state['status'] ?? 'running');
2303 $percent = max(0, min(100, (int)($state['percent'] ?? 0)));
2304 $step_percent = max(0, min(100, (int)($state['step_percent'] ?? $percent)));
2305 $status_labels = array(
2306 'running' => 'Laeuft',
2307 'paused' => 'Angehalten',
2308 'canceled' => 'Abgebrochen',
2309 'finished' => 'Fertig',
2310 'error' => 'Fehler',
2311 );
2312 $status_class = $status === 'finished' ? 'bg-success' : ($status === 'error' || $status === 'canceled' ? 'bg-danger' : ($status === 'paused' ? 'bg-warning text-dark' : 'bg-primary'));
2313 $status_icon = $status === 'finished' ? 'bi bi-check-lg' : ($status === 'error' || $status === 'canceled' ? 'bi bi-exclamation-triangle' : ($status === 'paused' ? 'bi bi-pause-fill' : 'bi bi-play-fill'));
2314 $token = (string)($state['proc_key'] ?? '');
2315 $restart_url = $this->append_url_params($next_url, array('reset' => 1, 'proc_cmd' => 'restart'));
2316 $cancel_url = $this->append_url_params($next_url, array('proc_cmd' => 'cancel'));
2317 $resume_url = $this->append_url_params($next_url, array('proc_cmd' => 'resume'));
2318 $pause_url = $this->append_url_params($next_url, array('proc_cmd' => 'pause'));
2319 $autostart = ($status === 'running' && $next_url !== '') ? 1 : 0;
2320 $target_id = 'dbx_cms_media_process_' . substr(md5($token ?: 'media'), 0, 14);
2321
2322 return '<div id="' . dbx()->esc($target_id) . '"'
2323 . ' class="container-fluid dbx-process dbx-cms-media-process"'
2324 . ' data-dbx="lib=process|id=' . dbx()->esc($target_id) . '|url=' . dbx()->esc($next_url) . '|interval=900|autostart=' . $autostart . '"'
2325 . ' data-process-status="' . dbx()->esc($status) . '"'
2326 . ' data-process-percent="' . $percent . '"'
2327 . ' data-process-step-percent="' . $step_percent . '"'
2328 . ' data-process-next-url="' . dbx()->esc($next_url) . '"'
2329 . ' data-process-pause-url="' . dbx()->esc($pause_url) . '"'
2330 . ' data-process-resume-url="' . dbx()->esc($resume_url) . '"'
2331 . ' data-process-cancel-url="' . dbx()->esc($cancel_url) . '"'
2332 . ' data-process-restart-url="' . dbx()->esc($restart_url) . '"'
2333 . ' data-process-autostart="' . $autostart . '"'
2334 . ' data-process-interval="900">'
2335 . '<div class="dbx-process-header">'
2336 . '<h3 class="dbx-process-title">Medienwartung</h3>'
2337 . '<span class="dbx-process-status badge ' . dbx()->esc($status_class) . '"><i class="' . dbx()->esc($status_icon) . '"></i> ' . dbx()->esc($status_labels[$status] ?? $status) . '</span>'
2338 . '</div>'
2339 . '<div class="dbx-process-grid">'
2340 . '<div class="dbx-process-progress"><div class="dbx-process-progress-head"><span class="dbx-process-progress-label">Gesamt</span><span class="dbx-process-progress-value" data-process-percent="overall">' . $percent . '%</span></div><div class="dbx-process-bar" role="progressbar" aria-valuenow="' . $percent . '" aria-valuemin="0" aria-valuemax="100"><div class="dbx-process-bar-fill" data-process-bar="overall" style="width:' . $percent . '%;"></div></div></div>'
2341 . '<div class="dbx-process-progress"><div class="dbx-process-progress-head"><span class="dbx-process-progress-label">Thumbnails und verwaiste Dateien</span><span class="dbx-process-progress-value" data-process-percent="step">' . $step_percent . '%</span></div><div class="dbx-process-bar" role="progressbar" aria-valuenow="' . $step_percent . '" aria-valuemin="0" aria-valuemax="100"><div class="dbx-process-bar-fill" data-process-bar="step" style="width:' . $step_percent . '%;"></div></div></div>'
2342 . '<div class="dbx-process-message" data-process-message>' . dbx()->esc((string)($state['message'] ?? '')) . '</div>'
2343 . '<div class="dbx-process-meta"><span>Eintraege: ' . (int)($state['pos'] ?? 0) . ' / ' . (int)($state['total'] ?? 0) . '</span><span>Aktualisiert: ' . dbx()->esc((string)($state['updated_at'] ?? '')) . '</span></div>'
2344 . '</div>'
2345 . '<div class="dbx-process-actions">'
2346 . '<button type="button" class="btn btn-warning btn-sm" data-process-action="pause" data-process-visible="running" title="Anhalten"><i class="bi bi-pause-fill"></i></button>'
2347 . '<button type="button" class="btn btn-primary btn-sm" data-process-action="resume" data-process-visible="paused" title="Weiter"><i class="bi bi-play-fill"></i></button>'
2348 . '<button type="button" class="btn btn-secondary btn-sm" data-process-action="restart" data-process-visible="paused,canceled,error,finished" title="Neu starten"><i class="bi bi-arrow-clockwise"></i></button>'
2349 . '<button type="button" class="btn btn-danger btn-sm" data-process-action="cancel" data-process-visible="running,paused" title="Abbrechen"><i class="bi bi-x-lg"></i></button>'
2350 . '</div>'
2351 . '</div>';
2352 }
2353
2354 private function append_url_params($url, $params = array()) {
2355 foreach ($params as $key => $value) {
2356 $url .= (strpos($url, '?') === false ? '?' : '&') . rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
2357 }
2358 return $url;
2359 }
2360
2361 private function media_process() {
2362 $db = dbx()->get_system_obj('dbxDB');
2363 $this->ensure_cms_schema($db);
2364
2365 $token = preg_replace('/[^A-Za-z0-9_-]+/', '', (string)($_GET['proc_key'] ?? ''));
2366 if ($token === '') $token = substr(md5(session_id() . microtime(true)), 0, 16);
2367 $reset = (int)($_GET['reset'] ?? 0);
2368 $cmd = strtolower(preg_replace('/[^a-z_]+/', '', (string)($_GET['proc_cmd'] ?? '')));
2369
2370 if ($reset || $cmd === 'restart') {
2371 $state = $this->build_media_process_state($db, $token);
2372 } else {
2373 $state = $this->get_media_process_state($token);
2374 if (empty($state)) $state = $this->build_media_process_state($db, $token);
2375 }
2376
2377 if ($cmd === 'pause' && ($state['status'] ?? '') === 'running') {
2378 $state['status'] = 'paused';
2379 $state['message'] = 'Medienwartung angehalten.';
2380 } elseif ($cmd === 'resume' && ($state['status'] ?? '') === 'paused') {
2381 $state['status'] = 'running';
2382 $state['message'] = 'Medienwartung fortgesetzt.';
2383 } elseif ($cmd === 'cancel') {
2384 $state['status'] = 'canceled';
2385 $state['message'] = 'Medienwartung abgebrochen.';
2386 }
2387
2388 $this->tick_media_process($db, $state);
2389 $this->set_media_process_state($token, $state);
2390
2391 $next = $this->base_url('cms_media_process', array('proc_key' => $token));
2392 return $this->render_media_process($state, $next);
2393 }
2394
2395 private function first_upload_file() {
2396 if (empty($_FILES) || !is_array($_FILES)) return array();
2397
2398 foreach ($_FILES as $file) {
2399 if (!is_array($file)) continue;
2400
2401 if (isset($file['tmp_name']) && !is_array($file['tmp_name'])) {
2402 return $file;
2403 }
2404
2405 if (!isset($file['tmp_name']) || !is_array($file['tmp_name'])) continue;
2406
2407 $idx = array_key_first($file['tmp_name']);
2408 if ($idx === null) continue;
2409
2410 return array(
2411 'name' => is_array($file['name'] ?? null) ? ($file['name'][$idx] ?? '') : ($file['name'] ?? ''),
2412 'type' => is_array($file['type'] ?? null) ? ($file['type'][$idx] ?? '') : ($file['type'] ?? ''),
2413 'tmp_name' => $file['tmp_name'][$idx] ?? '',
2414 'error' => is_array($file['error'] ?? null) ? ($file['error'][$idx] ?? UPLOAD_ERR_NO_FILE) : ($file['error'] ?? UPLOAD_ERR_NO_FILE),
2415 'size' => is_array($file['size'] ?? null) ? ($file['size'][$idx] ?? 0) : ($file['size'] ?? 0),
2416 );
2417 }
2418
2419 return array();
2420 }
2421
2422 private function load_gd_image($file, $mime) {
2423 if (!extension_loaded('gd')) return false;
2424 $mime = strtolower((string)$mime);
2425 if ((strpos($mime, 'jpeg') !== false || preg_match('/\.jpe?g$/i', $file)) && function_exists('imagecreatefromjpeg')) return @imagecreatefromjpeg($file);
2426 if ((strpos($mime, 'png') !== false || preg_match('/\.png$/i', $file)) && function_exists('imagecreatefrompng')) return @imagecreatefrompng($file);
2427 if (strpos($mime, 'webp') !== false || preg_match('/\.webp$/i', $file)) return function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($file) : false;
2428 if ((strpos($mime, 'gif') !== false || preg_match('/\.gif$/i', $file)) && function_exists('imagecreatefromgif')) return @imagecreatefromgif($file);
2429 return false;
2430 }
2431
2432 private function save_gd_image($img, $file, $mime) {
2433 if (!extension_loaded('gd')) return false;
2434 $mime = strtolower((string)$mime);
2435 if (strpos($mime, 'png') !== false || preg_match('/\.png$/i', $file)) {
2436 if (!function_exists('imagepng')) return false;
2437 imagealphablending($img, false);
2438 imagesavealpha($img, true);
2439 return @imagepng($img, $file, 6);
2440 }
2441 if ((strpos($mime, 'webp') !== false || preg_match('/\.webp$/i', $file)) && function_exists('imagewebp')) {
2442 return @imagewebp($img, $file, 86);
2443 }
2444 if (!function_exists('imagejpeg')) return false;
2445 return @imagejpeg($img, $file, 88);
2446 }
2447
2448 private function convert_media_image_to_webp($file, $name, $mime = '') {
2449 if (!extension_loaded('gd') || !function_exists('imagewebp')) return false;
2450 if ($file === '' || !is_file($file) || !is_readable($file)) return false;
2451 if (preg_match('/\.(webp|gif|svg)$/i', (string)$name)) return false;
2452
2453 $mime_l = strtolower((string)$mime);
2454 $ext = strtolower(pathinfo((string)$name, PATHINFO_EXTENSION));
2455 $is_supported = in_array($ext, array('jpg','jpeg','png'), true)
2456 || strpos($mime_l, 'jpeg') !== false
2457 || strpos($mime_l, 'png') !== false;
2458 if (!$is_supported) return false;
2459
2460 $image = $this->load_gd_image($file, $mime);
2461 if (!$image) return false;
2462
2463 imagepalettetotruecolor($image);
2464 imagealphablending($image, true);
2465 imagesavealpha($image, true);
2466
2467 $webp_name = preg_replace('/\.[^.]+$/', '', basename((string)$name)) . '.webp';
2468 $webp_file = rtrim(dirname($file), '/\\') . DIRECTORY_SEPARATOR . $webp_name;
2469 if (is_file($webp_file)) {
2470 $webp_name = $this->unique_media_name($webp_name);
2471 $webp_file = rtrim(dirname($file), '/\\') . DIRECTORY_SEPARATOR . $webp_name;
2472 }
2473
2474 $ok = @imagewebp($image, $webp_file, 86);
2475 imagedestroy($image);
2476 if (!$ok || !is_file($webp_file)) return false;
2477
2478 @unlink($file);
2479 return array(
2480 'file' => $webp_file,
2481 'name' => $webp_name,
2482 'mime' => 'image/webp',
2483 );
2484 }
2485
2486 private function resize_image_to_max($file, $mime, $max_long_side = 2560) {
2487 $max_long_side = max(800, min(6000, (int)$max_long_side));
2488 if ($file === '' || preg_match('/\.(svg|gif)$/i', (string)$file)) return false;
2489
2490 $size = @getimagesize($file);
2491 if (!is_array($size)) return false;
2492
2493 $src_w = (int)($size[0] ?? 0);
2494 $src_h = (int)($size[1] ?? 0);
2495 if ($src_w <= 0 || $src_h <= 0 || max($src_w, $src_h) <= $max_long_side) return false;
2496
2497 $op = array('action' => 'resize');
2498 if ($src_w >= $src_h) {
2499 $op['width'] = $max_long_side;
2500 $op['height'] = 0;
2501 } else {
2502 $op['width'] = 0;
2503 $op['height'] = $max_long_side;
2504 }
2505
2506 return $this->edit_image_file($file, $mime, $op);
2507 }
2508
2509 private function create_media_thumbnail($src, $slot, $name, $mime = '') {
2510 $type = $this->media_type(array('mime' => $mime, 'file_name' => $name));
2511 if ($type === 'video') return $this->create_video_thumbnail($src, $slot, $name);
2512 if ($type !== 'image' || preg_match('/\.svg$/i', (string)$name) || !function_exists('imagecreatetruecolor')) return array();
2513
2514 $image = $this->load_gd_image($src, $mime);
2515 if (!$image) return array();
2516
2517 $src_w = imagesx($image);
2518 $src_h = imagesy($image);
2519 if ($src_w <= 0 || $src_h <= 0) {
2520 imagedestroy($image);
2521 return array();
2522 }
2523
2524 $max_w = 640;
2525 $max_h = 480;
2526 $scale = min($max_w / $src_w, $max_h / $src_h, 1);
2527 $dst_w = max(1, (int)round($src_w * $scale));
2528 $dst_h = max(1, (int)round($src_h * $scale));
2529 $thumb = imagecreatetruecolor($dst_w, $dst_h);
2530 imagealphablending($thumb, false);
2531 imagesavealpha($thumb, true);
2532 imagefill($thumb, 0, 0, imagecolorallocatealpha($thumb, 255, 255, 255, 127));
2533 imagecopyresampled($thumb, $image, 0, 0, 0, 0, $dst_w, $dst_h, $src_w, $src_h);
2534
2535 $dir = $this->cms_media_thumb_dir($slot);
2536 if (!is_dir($dir)) @mkdir($dir, 0777, true);
2537 $thumb_ext = function_exists('imagewebp') ? 'webp' : 'jpg';
2538 $thumb_name = preg_replace('/\.[^.]+$/', '', basename((string)$name)) . '.' . $thumb_ext;
2539 $thumb_name = $this->unique_media_name($thumb_name, 'thumb');
2540 $dst = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR . $thumb_name;
2541 $ok = $thumb_ext === 'webp' ? @imagewebp($thumb, $dst, 82) : @imagejpeg($thumb, $dst, 82);
2542 imagedestroy($thumb);
2543 imagedestroy($image);
2544
2545 if (!$ok) return array();
2546 return array(
2547 'thumb_file_path' => $this->media_thumb_rel_dir($slot) . $thumb_name,
2548 'thumb_width' => $dst_w,
2549 'thumb_height' => $dst_h,
2550 );
2551 }
2552
2553 private function create_video_thumbnail($src, $slot, $name) {
2554 $ffmpeg = dbx()->find_executable_path('ffmpeg');
2555
2556 $dir = $this->cms_media_thumb_dir($slot);
2557 if (!is_dir($dir)) @mkdir($dir, 0777, true);
2558 $thumb_name = $this->unique_media_name(preg_replace('/\.[^.]+$/', '', basename((string)$name)) . '.jpg', 'video-thumb');
2559 $dst = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR . $thumb_name;
2560
2561 if ($ffmpeg !== '') {
2562 $cmd = '"' . $ffmpeg . '" -y -ss 00:00:01 -i "' . $src . '" -frames:v 1 -vf "scale=640:-1" "' . $dst . '" 2>NUL';
2563 @shell_exec($cmd);
2564 }
2565
2566 if (!is_file($dst) || !filesize($dst)) {
2567 $this->create_video_placeholder_thumbnail($dst, $name);
2568 }
2569
2570 if (!is_file($dst) || !filesize($dst)) return array();
2571 $size = @getimagesize($dst);
2572 return array(
2573 'thumb_file_path' => $this->media_thumb_rel_dir($slot) . $thumb_name,
2574 'thumb_width' => is_array($size) ? (int)$size[0] : 0,
2575 'thumb_height' => is_array($size) ? (int)$size[1] : 0,
2576 );
2577 }
2578
2579 private function create_video_placeholder_thumbnail($dst, $name) {
2580 if (!function_exists('imagecreatetruecolor')) return false;
2581 $w = 640;
2582 $h = 360;
2583 $img = imagecreatetruecolor($w, $h);
2584 $bg = imagecolorallocate($img, 30, 43, 60);
2585 $panel = imagecolorallocate($img, 52, 72, 96);
2586 $white = imagecolorallocate($img, 255, 255, 255);
2587 $muted = imagecolorallocate($img, 188, 202, 220);
2588 imagefill($img, 0, 0, $bg);
2589 imagefilledrectangle($img, 20, 20, $w - 20, $h - 20, $panel);
2590 $cx = (int)($w / 2);
2591 $cy = (int)($h / 2) - 12;
2592 imagefilledpolygon($img, array($cx - 38, $cy - 48, $cx - 38, $cy + 48, $cx + 52, $cy), 3, $white);
2593 $label = substr(basename((string)$name), 0, 54);
2594 imagestring($img, 3, 28, $h - 42, $label, $muted);
2595 $ok = @imagejpeg($img, $dst, 82);
2596 imagedestroy($img);
2597 return $ok;
2598 }
2599
2600 private function source_thumb_file($row) {
2601 $rel = ltrim(str_replace('\\', '/', (string)($row['thumb_file_path'] ?? '')), '/');
2602 if ($rel === '' || strpos($rel, '..') !== false) return '';
2603 $file = rtrim(dbx_get_file_dir(), '/\\') . '/' . $rel;
2604 if (function_exists('dbx_os_path_file')) $file = dbx_os_path_file($file);
2605 $base = realpath(rtrim(dbx_get_file_dir(), '/\\'));
2606 $real = realpath($file);
2607 if (!$base || !$real || strpos($real, $base) !== 0 || !is_file($real) || !is_readable($real)) return '';
2608 return $real;
2609 }
2610
2611 private function render_cms() {
2612 $this->clear_cms_tpl_cache();
2613 $oTPL = dbx()->get_system_obj('dbxTPL');
2614 $db = dbx()->get_system_obj('dbxDB');
2615 $this->ensure_cms_schema($db);
2616 $tree = $this->cms_tree();
2617 $page_count = is_array($tree['items'] ?? null) ? count($tree['items']) : 0;
2618 $folder_count = is_array($tree['folders'] ?? null) ? count($tree['folders']) : 0;
2619 $active_count = $db->count(dbxContentLng::ddContent(), 'activ = 1');
2620 if ($active_count < 0) $active_count = 0;
2621
2622 $cms_cid = $this->resolve_cms_page_id();
2623 $cms_fid = (int)dbx()->get_modul_var('fid', 0, 'int');
2624 if ($cms_fid <= 0) {
2625 $cms_fid = (int)dbx()->get_modul_var('dbx_fid', 0, 'int');
2626 }
2627
2628 $data = array(
2629 'i' => dbx()->next_id(),
2630 'cms_cid' => (string)$cms_cid,
2631 'cms_fid' => (string)$cms_fid,
2632 'title' => 'Content CMS',
2633 'subtitle' => 'Seiten, Ordner, Rechte und Medien zentral bearbeiten.',
2634 'count_pages' => (string)$page_count,
2635 'count_folders' => (string)$folder_count,
2636 'count_active' => (string)$active_count,
2637 'tree_url' => dbx()->esc($this->base_url('cms_tree')),
2638 'page_url' => dbx()->esc($this->base_url('cms_page')),
2639 'save_url' => dbx()->esc($this->base_url('cms_save')),
2640 'delete_page_url' => dbx()->esc($this->base_url('cms_delete_page')),
2641 'new_page_url' => dbx()->esc($this->base_url('cms_new_page')),
2642 'duplicate_page_url' => dbx()->esc($this->base_url('cms_duplicate_page')),
2643 'new_folder_url' => dbx()->esc($this->base_url('cms_new_folder')),
2644 'media_url' => dbx()->esc($this->base_url('cms_media')),
2645 'media_process_url' => dbx()->esc($this->base_url('cms_media_process')),
2646 'media_folders_url' => dbx()->esc($this->base_url('cms_media_folders')),
2647 'media_folder_create_url' => dbx()->esc($this->base_url('cms_media_folder_create')),
2648 'media_folder_delete_url' => dbx()->esc($this->base_url('cms_media_folder_delete')),
2649 'media_folder_rename_url' => dbx()->esc($this->base_url('cms_media_folder_rename')),
2650 'media_move_url' => dbx()->esc($this->base_url('cms_media_move')),
2651 'media_unused_url' => dbx()->esc($this->base_url('cms_media_unused')),
2652 'upload_url' => dbx()->esc($this->base_url('cms_upload')),
2653 'external_video_url' => dbx()->esc($this->base_url('cms_external_video')),
2654 'upload_max_bytes' => (string)$this->upload_max_bytes(),
2655 'remove_media_url' => dbx()->esc($this->base_url('cms_remove_media')),
2656 'delete_media_url' => dbx()->esc($this->base_url('cms_delete_media')),
2657 'edit_media_url' => dbx()->esc($this->base_url('cms_edit_media')),
2658 'set_media_slot_url' => dbx()->esc($this->base_url('cms_set_media_slot')),
2659 'assign_media_url' => dbx()->esc($this->base_url('cms_assign_media')),
2660 'sort_media_url' => dbx()->esc($this->base_url('cms_sort_media')),
2661 'mod_catalog_url' => dbx()->esc($this->base_url('cms_mod_catalog')),
2662 'mod_modules_url' => dbx()->esc($this->base_url('cms_mod_modules')),
2663 'folder_save_url' => dbx()->esc($this->base_url('cms_save_folder')),
2664 'folder_delete_url' => dbx()->esc($this->base_url('cms_delete_folder')),
2665 'move_node_url' => dbx()->esc($this->base_url('cms_move_node')),
2666 'lng_coverage_url' => dbx()->esc($this->base_url('cms_lng_coverage')),
2667 'lng_preview_url' => dbx()->esc($this->base_url('cms_lng_preview')),
2668 'lng_provision_url' => dbx()->esc($this->base_url('cms_lng_provision')),
2669 'lng_provision_tree_url' => dbx()->esc($this->base_url('cms_lng_provision_tree')),
2670 'lng_reset_sync_url' => dbx()->esc($this->base_url('cms_lng_reset_sync')),
2671 'lng_delete_preview_url' => dbx()->esc($this->base_url('cms_lng_delete_preview')),
2672 'current_lng' => dbxContentLng::current(),
2673 'master_lng' => dbxContentLngSync::masterLng(),
2674 'lng_bar' => dbxContentLngSync::renderLngBar(),
2675 'template_options' => $this->template_options(),
2676 'rights_options' => $this->rights_options(),
2677 'media_template_options' => $this->media_template_options(),
2678 'hero_template_options' => $this->hero_template_options(),
2679 'hero_variant_options' => $this->hero_variant_options(),
2680 'gallery_template_options' => $this->gallery_template_options(),
2681 'gallery_overflow_options' => $this->gallery_overflow_options(),
2682 'gallery_click_options' => $this->gallery_click_options(),
2683 'page_form' => $this->render_page_form(),
2684 'folder_form' => $this->render_folder_form(),
2685 'settings_form' => $this->render_settings_form(),
2686 'dbx_search' => dbx()->search_html(dbx()->search_defaults(array(
2687 'title' => 'Tree durchsuchen',
2688 'extra_attrs' => 'data-cms-search',
2689 'data_role' => '',
2690 ))),
2691 );
2692
2693 $data = array_merge($data, dbx()->get_include_obj('dbxAdminHelp', 'dbxAdmin')->vars('content'));
2694 $data['bar_class'] = 'dbx-module-bar dbx-cms-head';
2695 $data['bar_title_class'] = 'dbx-module-bar-titleblock';
2696 $data['bar_actions_class'] = 'dbx-module-bar-actions flex-wrap';
2697 if (trim((string)($data['bar_subtitle'] ?? '')) === '' && trim((string)($data['subtitle'] ?? '')) !== '') {
2698 $data['bar_subtitle'] = (string)$data['subtitle'];
2699 }
2700
2701 return $oTPL->get_tpl('dbxContent_admin|cms-admin', $data);
2702 }
2703
2704 private function mod_class_files($modul) {
2705 $modul = preg_replace('/[^A-Za-z0-9_]+/', '', (string)$modul);
2706 if ($modul === '') return array();
2707 $base = dbx_os_path_file(dbx_get_base_dir() . 'dbx/modules/' . $modul . '/');
2708 $files = array();
2709 $main = $base . $modul . '.class.php';
2710 if (is_file($main)) $files[] = $main;
2711 $inc = $base . 'include' . DIRECTORY_SEPARATOR;
2712 if (is_dir($inc)) {
2713 foreach (glob($inc . '*.class.php') ?: array() as $path) {
2714 if (is_file($path)) $files[] = $path;
2715 }
2716 }
2717 return $files;
2718 }
2719
2720 private function mod_scan_runs($modul) {
2721 $run1 = array();
2722 $run2 = array();
2723 $uses_run2 = false;
2724 foreach ($this->mod_class_files($modul) as $file) {
2725 $src = @file_get_contents($file);
2726 if (!is_string($src) || $src === '') continue;
2727 if (preg_match("/get_modul_var\s*\‍(\s*['\"]dbx_run2['\"]/", $src)) {
2728 $uses_run2 = true;
2729 }
2730 if (preg_match_all("/case\s+['\"]([^'\"]+)['\"]\s*:/", $src, $matches)) {
2731 foreach ($matches[1] as $case) {
2732 $case = trim((string)$case);
2733 if ($case === '' || $case === 'default') continue;
2734 $run1[$case] = true;
2735 }
2736 }
2737 if (preg_match_all('/\$(?:run|action|work)\s*===?\s*[\'"]([^\'"]+)[\'"]/', $src, $ifs)) {
2738 foreach ($ifs[1] as $case) {
2739 $case = trim((string)$case);
2740 if ($case === '') continue;
2741 $run1[$case] = true;
2742 }
2743 }
2744 if (preg_match_all("/get_modul_var\s*\‍(\s*['\"]dbx_run1['\"][^)]*['\"]([a-zA-Z0-9_]+)['\"]/", $src, $defaults)) {
2745 foreach ($defaults[1] as $case) {
2746 $case = trim((string)$case);
2747 if ($case === '' || $case === 'parameter') continue;
2748 $run1[$case] = true;
2749 }
2750 }
2751 if (preg_match_all("/get_modul_var\s*\‍(\s*['\"]dbx_run2['\"][^)]*['\"]([^'\"]+)['\"]/", $src, $defaults)) {
2752 foreach ($defaults[1] as $val) {
2753 $val = trim((string)$val);
2754 if ($val !== '') $run2[$val] = true;
2755 }
2756 }
2757 }
2758 $run1_list = array_keys($run1);
2759 usort($run1_list, function($a, $b) {
2760 return strlen($b) <=> strlen($a);
2761 });
2762 return array(
2763 'run1' => array_values(array_keys($run1)),
2764 'run2' => array_values(array_keys($run2)),
2765 'uses_run2' => $uses_run2,
2766 'run1_sorted' => $run1_list,
2767 );
2768 }
2769
2770 private function mod_modules_json() {
2771 $base = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbxAdmin/include/');
2772 require_once $base . 'dbxModuleImages.class.php';
2773 $images = new \dbx\dbxAdmin\dbxModuleImages();
2774
2775 $dir = dbx_os_path_file(dbx_get_base_dir() . 'dbx/modules/');
2776 $items = array();
2777 if (is_dir($dir)) {
2778 foreach (glob($dir . '*', GLOB_ONLYDIR) ?: array() as $path) {
2779 $name = basename(str_replace('\\', '/', $path));
2780 if (!preg_match('/^[A-Za-z][A-Za-z0-9_]*$/', $name)) continue;
2781 $class_file = dbx_os_path_file($path . DIRECTORY_SEPARATOR . $name . '.class.php');
2782 if (!is_file($class_file)) continue;
2783 $scan = $this->mod_scan_runs($name);
2784 $count = $images->imageCount($name);
2785 if ($count <= 0) {
2786 continue;
2787 }
2788 $items[] = array(
2789 'id' => $name,
2790 'label' => $name,
2791 'image_count' => $count,
2792 'uses_run2' => !empty($scan['uses_run2']) ? 1 : 0,
2793 'run1_actions' => array_slice($scan['run1'], 0, 24),
2794 );
2795 }
2796 }
2797 usort($items, function($a, $b) {
2798 return strcmp((string)($a['label'] ?? ''), (string)($b['label'] ?? ''));
2799 });
2800 $this->cms_json_response(array('ok' => 1, 'items' => $items));
2801 }
2802
2803 private function mod_catalog_json() {
2804 $base = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbxAdmin/include/');
2805 require_once $base . 'dbxModuleImages.class.php';
2806 $images = new \dbx\dbxAdmin\dbxModuleImages();
2807
2808 $modul = preg_replace('/[^A-Za-z0-9_]+/', '', (string)dbx()->get_modul_var('modul', '', 'parameter'));
2809 if ($modul === '') {
2810 $this->cms_json_response(array('ok' => 0, 'msg' => 'Modul fehlt.', 'items' => array()));
2811 return;
2812 }
2813 $class_file = dbx_os_path_file(dbx_get_base_dir() . 'dbx/modules/' . $modul . '/' . $modul . '.class.php');
2814 if (!is_file($class_file)) {
2815 $this->cms_json_response(array('ok' => 0, 'msg' => 'Modul nicht gefunden.', 'items' => array()));
2816 return;
2817 }
2818
2819 $out = $images->catalogForModul($modul);
2820 $scan = $this->mod_scan_runs($modul);
2821
2822 $this->cms_json_response(array(
2823 'ok' => 1,
2824 'modul' => $modul,
2825 'uses_run2' => !empty($scan['uses_run2']) ? 1 : 0,
2826 'run1_actions' => $scan['run1'],
2827 'items' => is_array($out) ? $out : array(),
2828 ));
2829 }
2830
2831 private function tree_json() {
2832 $this->cms_json_response(array('ok' => 1) + $this->cms_tree());
2833 }
2834
2835 private function lng_coverage_json() {
2836 $type = trim((string) dbx()->get_modul_var('type', 'page'));
2837 $type = $type === 'folder' ? 'folder' : 'page';
2838 $id = (int) dbx()->get_modul_var('id', 0, 'int');
2839 $lngUid = trim((string) dbx()->get_modul_var('lng_uid', ''));
2840 $db = dbx()->get_system_obj('dbxDB');
2841 $this->ensure_cms_schema($db);
2842
2843 if ($lngUid === '' && $id > 0) {
2844 $dd = $type === 'folder' ? dbxContentLng::ddFolder() : dbxContentLng::ddContent();
2845 $lngUid = dbxContentLngSync::ensureRecordUid($db, $dd, $id, $type === 'folder' ? 'f' : 'p');
2846 }
2847
2848 $coverage = dbxContentLngSync::coverageForUid($db, $type, $lngUid);
2849 $this->cms_json_response(array('ok' => 1, 'coverage' => $coverage));
2850 }
2851
2852 private function lng_preview_json() {
2853 if (!dbxContentLngSync::isMasterLng()) {
2854 $this->cms_json_response(array('ok' => 0, 'msg' => 'Vorschau nur in der Master-Sprache moeglich.'));
2855 }
2856
2857 $payload = $this->request_json();
2858 $type = (($payload['type'] ?? 'page') === 'folder') ? 'folder' : 'page';
2859 $id = (int) ($payload['id'] ?? 0);
2860 $lngs = is_array($payload['lngs'] ?? null) ? $payload['lngs'] : array();
2861 $db = dbx()->get_system_obj('dbxDB');
2862 $this->ensure_cms_schema($db);
2863
2864 dbxContentTranslate::clearWarnings();
2865 $preview = dbxContentLngSync::previewProvision($db, $type, $id, $lngs);
2866 $this->cms_json_response(array(
2867 'ok' => 1,
2868 'preview' => $preview,
2869 'provider' => dbxContentTranslate::provider(),
2870 'translate_warnings' => dbxContentTranslate::warnings(),
2871 ));
2872 }
2873
2874 private function lng_provision_json() {
2875 if (!dbxContentLngSync::isMasterLng()) {
2876 $this->cms_json_response(array('ok' => 0, 'msg' => 'Uebertragung nur in der Master-Sprache moeglich.'));
2877 }
2878
2879 $payload = $this->request_json();
2880 $type = (($payload['type'] ?? 'page') === 'folder') ? 'folder' : 'page';
2881 $id = (int) ($payload['id'] ?? 0);
2882 $items = is_array($payload['items'] ?? null) ? $payload['items'] : array();
2883 $db = dbx()->get_system_obj('dbxDB');
2884 $this->ensure_cms_schema($db);
2885
2886 dbxContentTranslate::clearWarnings();
2887 $result = dbxContentLngSync::provisionFromPreview($db, $type, $id, $items);
2888 if ((int) ($result['ok'] ?? 0) === 1) {
2889 $result['media_copied'] = $this->apply_lng_provision_media($db, $type, $id, $result);
2890 $this->flush_menu_cache();
2891 }
2892 $result['translate_warnings'] = dbxContentTranslate::warnings();
2893 $this->cms_json_response(array('ok' => (int) ($result['ok'] ?? 0), 'result' => $result));
2894 }
2895
2896 private function lng_provision_tree_json() {
2897 if (!dbxContentLngSync::isMasterLng()) {
2898 $this->cms_json_response(array('ok' => 0, 'msg' => 'Unterbaum-Uebertragung nur in der Master-Sprache moeglich.'));
2899 }
2900
2901 $payload = $this->request_json();
2902 $id = (int) ($payload['id'] ?? 0);
2903 $lngs = is_array($payload['lngs'] ?? null) ? $payload['lngs'] : array();
2904 if ($id <= 0) {
2905 $this->cms_json_response(array('ok' => 0, 'msg' => 'Kein Ordner gewaehlt.'));
2906 }
2907
2908 $db = dbx()->get_system_obj('dbxDB');
2909 $this->ensure_cms_schema($db);
2910 dbxContentTranslate::clearWarnings();
2911 $result = dbxContentLngSync::provisionFolderTree($db, $id, $lngs);
2912 if ((int) ($result['ok'] ?? 0) === 1) {
2913 $mediaCopied = 0;
2914 foreach (is_array($result['pages'] ?? null) ? $result['pages'] : array() as $pageItem) {
2915 if (!is_array($pageItem)) {
2916 continue;
2917 }
2918 $masterPageId = (int) ($pageItem['master_id'] ?? 0);
2919 $prov = is_array($pageItem['result'] ?? null) ? $pageItem['result'] : array();
2920 if ($masterPageId > 0) {
2921 $mediaCopied += $this->apply_lng_provision_media($db, 'page', $masterPageId, $prov);
2922 }
2923 }
2924 $result['media_copied'] = $mediaCopied;
2925 $this->flush_menu_cache();
2926 }
2927 $result['translate_warnings'] = dbxContentTranslate::warnings();
2928 $this->cms_json_response(array('ok' => (int) ($result['ok'] ?? 0), 'result' => $result));
2929 }
2930
2931 private function lng_reset_sync_json() {
2932 if (!dbxContentLngSync::isMasterLng()) {
2933 $this->cms_json_response(array('ok' => 0, 'msg' => 'Auto-Sync nur in der Master-Sprache setzbar.'));
2934 }
2935
2936 $payload = $this->request_json();
2937 $type = (($payload['type'] ?? 'page') === 'folder') ? 'folder' : 'page';
2938 $id = (int) ($payload['id'] ?? 0);
2939 $lngs = is_array($payload['lngs'] ?? null) ? $payload['lngs'] : array();
2940 if ($id <= 0) {
2941 $this->cms_json_response(array('ok' => 0, 'msg' => 'Kein Datensatz gewaehlt.'));
2942 }
2943
2944 $db = dbx()->get_system_obj('dbxDB');
2945 $this->ensure_cms_schema($db);
2946 $result = dbxContentLngSync::resetSyncToAuto($db, $type, $id, $lngs);
2947 $this->cms_json_response(array('ok' => count($result['updated'] ?? array()) ? 1 : 0, 'result' => $result));
2948 }
2949
2950 private function lng_delete_preview_json() {
2951 if (!dbxContentLngSync::isMasterLng()) {
2952 $this->cms_json_response(array('ok' => 0, 'msg' => 'Mehrsprachige Loesch-Vorschau nur in der Master-Sprache moeglich.'));
2953 }
2954
2955 $payload = $this->request_json();
2956 $type = (($payload['type'] ?? 'page') === 'folder') ? 'folder' : 'page';
2957 $id = (int) ($payload['id'] ?? 0);
2958 if ($id <= 0) {
2959 $this->cms_json_response(array('ok' => 0, 'msg' => 'Kein Datensatz gewaehlt.'));
2960 }
2961
2962 $db = dbx()->get_system_obj('dbxDB');
2963 $this->ensure_cms_schema($db);
2964 $preview = dbxContentLngSync::previewDelete($db, $type, $id);
2965 $this->cms_json_response(array('ok' => 1, 'preview' => $preview));
2966 }
2967
2968 private function normalize_delete_lngs(array $payload): array {
2969 $current = dbxContentLng::current();
2970 $raw = $payload['delete_lngs'] ?? null;
2971 if (!is_array($raw) || !count($raw)) {
2972 return array($current);
2973 }
2974 if (!dbxContentLngSync::isMasterLng()) {
2975 return array($current);
2976 }
2977
2978 $allowed = dbxContentLngSync::accessibleLngs();
2979 $out = array();
2980 foreach ($raw as $lng) {
2981 $lng = strtolower(trim((string) $lng));
2982 if ($lng !== '' && in_array($lng, $allowed, true) && !in_array($lng, $out, true)) {
2983 $out[] = $lng;
2984 }
2985 }
2986
2987 return count($out) ? $out : array($current);
2988 }
2989
2990 private function delete_page_in_lngs($db, int $id, array $deleteLngs): array {
2991 $targets = dbxContentLngSync::resolveDeleteIds($db, 'page', $id, $deleteLngs);
2992 $deleted = array();
2993 $errors = array();
2994
2995 foreach ($targets as $target) {
2996 if (!is_array($target)) {
2997 continue;
2998 }
2999 $lng = (string) ($target['lng'] ?? '');
3000 $targetId = (int) ($target['id'] ?? 0);
3001 if ($targetId <= 0) {
3002 continue;
3003 }
3004
3005 $targetDd = dbxContentLng::ddContent($lng);
3006 $ok = $db->delete($targetDd, 'id = ' . $targetId, 1, 0);
3007 if ($ok >= 0) {
3008 $db->update($this->dd_media, array('active' => 1, 'content_id' => 0, 'folder_id' => 0), 'content_id = ' . $targetId, 0, 1, 1, 0);
3009 $db->update($this->dd_media_usage, array('active' => 0), 'content_id = ' . $targetId . ' AND active = 1', 0, 1, 1, 0);
3010 $this->flush_deleted_page_cache($targetId, $lng);
3011 $deleted[] = array('lng' => $lng, 'id' => $targetId);
3012 } else {
3013 $errors[] = 'Loeschen in ' . strtoupper($lng) . ' fehlgeschlagen.';
3014 }
3015 }
3016
3017 return array(
3018 'ok' => count($deleted) ? 1 : 0,
3019 'deleted' => $deleted,
3020 'errors' => $errors,
3021 );
3022 }
3023
3024 public function delete_page_record(int $id): array {
3025 if ($id <= 0) {
3026 return array('ok' => 0, 'deleted' => array(), 'errors' => array('Keine Seite gewaehlt.'));
3027 }
3028
3029 $db = dbx()->get_system_obj('dbxDB');
3030 $this->ensure_cms_schema($db);
3031 $result = $this->delete_page_in_lngs($db, $id, array(dbxContentLng::current()));
3032
3033 if ((int)($result['ok'] ?? 0) === 1) {
3034 $this->flush_menu_cache();
3035 }
3036
3037 return $result;
3038 }
3039
3040 public function delete_folder_record(int $id): array {
3041 if ($id <= 0) {
3042 return array('ok' => 0, 'deleted' => array(), 'errors' => array('Kein Ordner gewaehlt.'));
3043 }
3044
3045 $db = dbx()->get_system_obj('dbxDB');
3046 $this->ensure_cms_schema($db);
3047 $result = $this->delete_folder_in_lngs($db, $id, array(dbxContentLng::current()));
3048
3049 if ((int)($result['ok'] ?? 0) === 1) {
3050 $this->flush_menu_cache();
3051 }
3052
3053 return $result;
3054 }
3055
3056 public function delete_media_record(int $id): array {
3057 if ($id <= 0) {
3058 return array('ok' => 0, 'errors' => array('Keine Medien-ID erhalten.'));
3059 }
3060
3061 $db = dbx()->get_system_obj('dbxDB');
3062 $this->ensure_cms_schema($db);
3063 $used = $db->select(
3064 $this->dd_media_usage,
3065 'media_id = ' . $id . ' AND active = 1',
3066 'id',
3067 'id',
3068 'ASC',
3069 '',
3070 1,
3071 0,
3072 0
3073 );
3074 if (is_array($used) && isset($used[0])) {
3075 return array('ok' => 0, 'errors' => array('Medium wird noch verwendet.'));
3076 }
3077
3078 foreach (dbxContentLngSync::accessibleLngs() as $lng) {
3079 $contentDd = dbxContentLng::ddContent((string)$lng);
3080 $folderDd = dbxContentLng::ddFolder((string)$lng);
3081 if ($db->count($contentDd, 'hero_image_id = ' . $id) > 0 || $db->count($folderDd, 'hero_image_id = ' . $id) > 0) {
3082 return array('ok' => 0, 'errors' => array('Medium wird noch verwendet.'));
3083 }
3084 $pages = $db->select($contentDd, '', 'content', 'id', 'ASC', '', 0, 0, 0);
3085 foreach (is_array($pages) ? $pages : array() as $page) {
3086 if (preg_match('/data-cms-media-id=["\']?' . $id . '(?:["\'\s>]|$)/i', (string)($page['content'] ?? ''))) {
3087 return array('ok' => 0, 'errors' => array('Medium wird noch verwendet.'));
3088 }
3089 }
3090 }
3091
3092 $row = $db->select1($this->dd_media, $id);
3093 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1) {
3094 return array('ok' => 0, 'errors' => array('Medium nicht gefunden.'));
3095 }
3096
3097 if (strtolower((string)($row['storage_type'] ?? 'local')) === 'local') {
3098 $file = $this->source_media_file($row);
3099 if ($file !== '') {
3100 @unlink($file);
3101 }
3102 } else {
3103 $file = $this->source_media_file($row);
3104 if ($file !== '' && preg_match('/\.json$/i', $file)) {
3105 @unlink($file);
3106 }
3107 }
3108 $thumb = $this->source_thumb_file($row);
3109 if ($thumb !== '') {
3110 @unlink($thumb);
3111 }
3112
3113 $ok = $db->update($this->dd_media, array('active' => 0), $id);
3114 return array(
3115 'ok' => $ok >= 0 ? 1 : 0,
3116 'errors' => $ok >= 0 ? array() : array('Medium konnte nicht geloescht werden.'),
3117 );
3118 }
3119
3120 private function delete_folder_in_lngs($db, int $id, array $deleteLngs): array {
3121 $targets = dbxContentLngSync::resolveDeleteIds($db, 'folder', $id, $deleteLngs);
3122 $deleted = array();
3123 $errors = array();
3124
3125 foreach ($targets as $target) {
3126 if (!is_array($target)) {
3127 continue;
3128 }
3129 $lng = (string) ($target['lng'] ?? '');
3130 $targetId = (int) ($target['id'] ?? 0);
3131 if ($targetId <= 0) {
3132 continue;
3133 }
3134
3135 $check = dbxContentLngSync::folderDeletable($db, $lng, $targetId);
3136 if ((int) ($check['deletable'] ?? 0) !== 1) {
3137 $reason = trim((string) ($check['reason'] ?? ''));
3138 $errors[] = strtoupper($lng) . ($reason !== '' ? ': ' . $reason : ': Ordner kann nicht geloescht werden.');
3139 continue;
3140 }
3141
3142 $targetDd = dbxContentLng::ddFolder($lng);
3143 $ok = $db->delete($targetDd, 'id = ' . $targetId, 1, 0);
3144 if ($ok >= 0) {
3145 $this->flush_deleted_folder_cache($db, $targetId, $lng);
3146 $deleted[] = array('lng' => $lng, 'id' => $targetId);
3147 } else {
3148 $errors[] = 'Loeschen in ' . strtoupper($lng) . ' fehlgeschlagen.';
3149 }
3150 }
3151
3152 return array(
3153 'ok' => count($deleted) ? 1 : 0,
3154 'deleted' => $deleted,
3155 'errors' => $errors,
3156 );
3157 }
3158
3159 private function page_json() {
3160 $id = (int)dbx()->get_modul_var('id', 0, 'int');
3161 if ($id <= 0) {
3162 $id = $this->resolve_cms_page_id();
3163 }
3164 $db = dbx()->get_system_obj('dbxDB');
3165 $this->ensure_cms_schema($db);
3166 $row = $id > 0 ? $db->select1(dbxContentLng::ddContent(), $id) : $db->empty_record(dbxContentLng::ddContent())[0];
3167 if (is_array($row) && isset($row['content'])) {
3168 $row['content'] = $this->normalize_content_media_urls($row['content']);
3169 }
3170 if (is_array($row)) {
3171 $row = $this->normalize_gallery_row($row);
3172 }
3173 if ($id > 0 && is_array($row)) {
3174 $this->sync_inline_media_usage($db, $id, (string)($row['content'] ?? ''), (int)($row['folder'] ?? 0));
3175 }
3176 $media = $id > 0 ? $this->media_usage_rows_for_context($db, $id, 0) : array();
3177 $this->cms_json_response(array(
3178 'ok' => 1,
3179 'row' => $row,
3180 'media' => $media,
3181 'hero_preview_media' => $this->hero_preview_media($db, $row),
3182 'hero_parent_preview_media' => $this->inherited_hero_preview_media($db, (int)($row['folder'] ?? 0)),
3183 'seo_preview_media' => $this->seo_preview_media($db, $row),
3184 ));
3185 }
3186
3187 private function hero_preview_media($db, $row) {
3188 if (!is_array($row)) return array();
3189 $hero_id = (int)($row['hero_image_id'] ?? 0);
3190 if ($hero_id <= 0) return array();
3191 $hero = $db->select1($this->dd_media, $hero_id);
3192 if (!is_array($hero) || (int)($hero['active'] ?? 0) !== 1) return array();
3193 if (!$this->media_file_exists($hero)) return array();
3194 return $this->normalize_media_row($hero);
3195 }
3196
3197 private function inherited_hero_preview_media($db, $folder_id) {
3198 $folder_id = (int)$folder_id;
3199 $seen = array();
3200 while ($folder_id > 0 && !isset($seen[$folder_id])) {
3201 $seen[$folder_id] = 1;
3202 $folder = $db->select1(dbxContentLng::ddFolder(), $folder_id, '*', 0);
3203 if (!is_array($folder)) return array();
3204 $hero_template = trim((string)($folder['hero_template'] ?? 'parent'));
3205 if ($this->is_no_hero_template($hero_template)) return array();
3206 $hero_value = trim((string)($folder['hero_image_id'] ?? 'parent'));
3207 $hero_id = (int)$hero_value;
3208 if ($hero_id > 0) {
3209 $hero = $db->select1($this->dd_media, $hero_id);
3210 if (!is_array($hero) || (int)($hero['active'] ?? 0) !== 1 || !$this->media_file_exists($hero)) return array();
3211 $hero = $this->normalize_media_row($hero);
3212 $hero['parent_folder_id'] = $folder_id;
3213 $hero['parent_folder_name'] = (string)($folder['name'] ?? '');
3214 return $hero;
3215 }
3216 if ($hero_value === '0' || strtolower($hero_value) === 'none') return array();
3217 $folder_id = (int)($folder['parent_id'] ?? 0);
3218 }
3219 return array();
3220 }
3221
3222 private function seo_preview_media($db, $row) {
3223 if (!is_array($row)) return array();
3224 $seo_id = (int)($row['seo_image_id'] ?? 0);
3225 if ($seo_id <= 0) return array();
3226 $media = $db->select1($this->dd_media, $seo_id);
3227 if (!is_array($media) || (int)($media['active'] ?? 0) !== 1) return array();
3228 if (!$this->media_file_exists($media)) return array();
3229 return $this->normalize_media_row($media);
3230 }
3231
3232 private function save_json() {
3233 $payload = $this->request_json();
3234 $id = (int)($payload['id'] ?? 0);
3235 $db = dbx()->get_system_obj('dbxDB');
3236 $this->ensure_cms_schema($db);
3237
3238 $title = $this->clean_text($payload['title'] ?? '', 254);
3239 $folder = (int)($payload['folder'] ?? 0);
3240 $data = array(
3241 'activ' => $this->bool_int($payload['activ'] ?? 1, 1),
3242 'folder' => $folder,
3243 'title' => $title,
3244 'permalink' => $this->page_permalink($db, $folder, $title, $payload['permalink'] ?? ''),
3245 'description' => $this->clean_text($payload['description'] ?? '', 254),
3246 'template' => $this->clean_text($payload['template'] ?? 'parent', 254),
3247 'hero_template' => $this->clean_text($payload['hero_template'] ?? 'parent', 80),
3248 'hero_image_id' => $this->clean_text($payload['hero_image_id'] ?? 'parent', 32),
3249 'hero_margin_top' => $this->clean_text($payload['hero_margin_top'] ?? 'parent', 32),
3250 'hero_height' => $this->clean_text($payload['hero_height'] ?? 'parent', 32),
3251 'hero_variant' => $this->clean_text($payload['hero_variant'] ?? 'parent', 32),
3252 'hero_sticky' => $this->clean_text($payload['hero_sticky'] ?? 'parent', 32),
3253 'hero_scroll_layer' => $this->clean_text($payload['hero_scroll_layer'] ?? 'parent', 32),
3254 'gallery_template' => 'image-gallery',
3255 'gallery_visible_count' => '3',
3256 'gallery_image_size' => $this->clean_text($payload['gallery_image_size'] ?? 'original', 32),
3257 'gallery_lightbox_width' => $this->clean_text($payload['gallery_lightbox_width'] ?? '100vw', 32),
3258 'gallery_overflow' => $this->clean_text($payload['gallery_overflow'] ?? 'grid', 32),
3259 'gallery_click_behavior' => $this->clean_text($payload['gallery_click_behavior'] ?? 'lightbox', 32),
3260 'content' => $this->normalize_and_sanitize_content($payload['content'] ?? ''),
3261 );
3262 $data = $this->normalize_hero_payload($data);
3263
3264 if ($id > 0) {
3265 $existingSeo = $db->select1(dbxContentLng::ddContent(), $id, 'keywords,meta_robots,seo_title,seo_image_id', 0);
3266 $data['keywords'] = is_array($existingSeo) ? (string)($existingSeo['keywords'] ?? '') : '';
3267 $data['meta_robots'] = is_array($existingSeo) ? (string)($existingSeo['meta_robots'] ?? 'index,follow') : 'index,follow';
3268 $data['seo_title'] = is_array($existingSeo) ? (string)($existingSeo['seo_title'] ?? '') : '';
3269 $data['seo_image_id'] = is_array($existingSeo) ? max(0, (int)($existingSeo['seo_image_id'] ?? 0)) : 0;
3270 } else {
3271 $data['keywords'] = '';
3272 $data['meta_robots'] = 'index,follow';
3273 $data['seo_title'] = '';
3274 $data['seo_image_id'] = 0;
3275 }
3276
3277 if ($id > 0) {
3278 $ok = $db->update(dbxContentLng::ddContent(), $data, $id);
3279 $saved_id = $id;
3280 } else {
3281 $ok = $db->insert(dbxContentLng::ddContent(), $data);
3282 $saved_id = ($ok === 1) ? $db->get_insert_id() : 0;
3283 }
3284
3285 if ($ok === 1) {
3286 $this->sync_saved_hero_media_usage($db, $saved_id, 0, $data['hero_image_id']);
3287 dbxContentLngSync::afterPageSave($db, $saved_id, $id <= 0);
3288 $syncResult = array('updated' => array(), 'skipped' => array(), 'errors' => array());
3289 if ($id > 0 && dbxContentLngSync::isMasterLng()) {
3290 dbxContentTranslate::clearWarnings();
3291 $syncResult = dbxContentLngSync::syncSlavesFromMaster($db, 'page', $saved_id);
3292 $syncResult['media_copied'] = $this->apply_lng_sync_media($db, $saved_id, $syncResult);
3293 $syncResult['translate_warnings'] = dbxContentTranslate::warnings();
3294 $this->flush_lng_sync_cache($db, $syncResult);
3295 }
3296 $inline_provided = array_key_exists('inline_media_ids', $payload) && is_array($payload['inline_media_ids']);
3297 $inline_sync = $this->sync_inline_media_usage(
3298 $db,
3299 $saved_id,
3300 $data['content'],
3301 $data['folder'],
3302 $inline_provided ? $payload['inline_media_ids'] : null,
3303 $inline_provided
3304 );
3305 $this->flush_saved_page_cache($db, $saved_id);
3306 $media = $this->media_usage_rows_for_context($db, $saved_id, 0);
3307 $saved_row = $db->select1(dbxContentLng::ddContent(), $saved_id);
3308 if (is_array($saved_row)) {
3309 if (isset($saved_row['content'])) {
3310 $saved_row['content'] = $this->normalize_content_media_urls($saved_row['content']);
3311 }
3312 $saved_row = $this->normalize_gallery_row($saved_row);
3313 }
3314 $this->cms_json_response(array(
3315 'ok' => 1,
3316 'success' => true,
3317 'id' => $saved_id,
3318 'row' => $saved_row,
3319 'media' => $media,
3320 'inline_media_sync' => $inline_sync,
3321 'hero_preview_media' => $this->hero_preview_media($db, $saved_row),
3322 'hero_parent_preview_media' => $this->inherited_hero_preview_media($db, (int)($saved_row['folder'] ?? 0)),
3323 'seo_preview_media' => $this->seo_preview_media($db, $saved_row),
3324 'lng_forked' => dbxContentLngSync::isMasterLng() ? 0 : 1,
3325 'lng_synced' => count($syncResult['updated'] ?? array()),
3326 'lng_media_copied' => (int) ($syncResult['media_copied'] ?? 0),
3327 'translate_warnings' => is_array($syncResult['translate_warnings'] ?? null) ? $syncResult['translate_warnings'] : array(),
3328 'open_lng_provision' => $this->lng_provision_open_flag($db, 'page', $saved_id),
3329 ));
3330 }
3331
3332 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Seite konnte nicht gespeichert werden.', 'db' => $ok));
3333 }
3334
3335 private function new_page_json() {
3336 $folder = (int)dbx()->get_modul_var('folder', 0, 'int');
3337 $db = dbx()->get_system_obj('dbxDB');
3338 $this->ensure_cms_schema($db);
3339 $title = 'Neue Seite ' . date('H:i');
3340 $sorter = $this->next_content_sorter($db, $folder);
3341 $id = 0;
3342 if ($db->insert(dbxContentLng::ddContent(), array(
3343 'activ' => 1,
3344 'folder' => $folder,
3345 'title' => $title,
3346 'permalink' => dbxContent_permalink::build($db, dbxContentLng::ddFolder(), $folder, $title),
3347 'template' => 'parent',
3348 'hero_template' => 'parent',
3349 'hero_image_id' => 'parent',
3350 'hero_margin_top' => 'parent',
3351 'hero_height' => 'parent',
3352 'hero_variant' => 'parent',
3353 'hero_sticky' => 'parent',
3354 'hero_scroll_layer' => 'parent',
3355 'gallery_template' => 'image-gallery',
3356 'gallery_visible_count' => '3',
3357 'gallery_image_size' => 'original',
3358 'gallery_lightbox_width' => '100vw',
3359 'gallery_overflow' => 'grid',
3360 'gallery_click_behavior' => 'lightbox',
3361 'description' => '',
3362 'keywords' => '',
3363 'meta_robots' => 'index,follow',
3364 'seo_title' => '',
3365 'seo_image_id' => 0,
3366 'sorter' => $sorter,
3367 'content' => '<p>Neue Seite bearbeiten...</p>',
3368 ), 0, 1, 0, 1) === 1) {
3369 $id = $db->get_insert_id();
3370 }
3371 if ($id > 0) {
3372 dbxContentLngSync::afterPageSave($db, $id, true);
3373 $this->flush_menu_cache();
3374 $this->cms_json_response(array(
3375 'ok' => 1,
3376 'success' => true,
3377 'id' => $id,
3378 'row' => $db->select1(dbxContentLng::ddContent(), $id),
3379 'open_lng_provision' => $this->lng_provision_open_flag($db, 'page', $id),
3380 ));
3381 }
3382 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Seite konnte nicht angelegt werden.', 'db' => $id));
3383 }
3384
3385 private function duplicate_page_json() {
3386 $payload = $this->request_json();
3387 $sourceId = (int)($payload['id'] ?? dbx()->get_modul_var('id', 0, 'int'));
3388 if ($sourceId <= 0) {
3389 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Bitte zuerst eine Seite auswaehlen.'));
3390 }
3391
3392 $db = dbx()->get_system_obj('dbxDB');
3393 $this->ensure_cms_schema($db);
3394 $contentDd = dbxContentLng::ddContent();
3395 if ($db->begin($contentDd) !== 1) {
3396 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Seite konnte nicht fuer die Duplizierung gesperrt werden.'));
3397 }
3398
3399 $source = $db->select1($contentDd, $sourceId, '*', 0);
3400 if (!is_array($source) || (int)($source['id'] ?? 0) !== $sourceId) {
3401 $db->rollback($contentDd);
3402 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Die ausgewaehlte Seite wurde nicht gefunden.'));
3403 }
3404
3405 $copy = $source;
3406 foreach (array(
3407 'id', 'create_date', 'create_uid', 'update_date', 'update_uid', 'owner',
3408 'hits', 'xvote', 'vote', 'vote1', 'vote2', 'vote3', 'vote4', 'vote5', 'lastuservote'
3409 ) as $field) {
3410 unset($copy[$field]);
3411 }
3412
3413 $folderId = (int)($source['folder'] ?? 0);
3414 $title = (string)($source['title'] ?? 'Unbenannte Seite');
3415 $copy['sorter'] = $this->next_content_sorter($db, $folderId);
3416 $copy['permalink'] = $this->duplicate_page_permalink(
3417 $db,
3418 $folderId,
3419 $title,
3420 (string)($source['permalink'] ?? '')
3421 );
3422 $copy['lng_uid'] = '';
3423 $copy['lng_sync'] = dbxContentLngSync::isMasterLng() ? 'auto' : 'manual';
3424 $copy['lng_rev'] = 1;
3425 $copy['lng_synced_rev'] = 0;
3426
3427 $inserted = $db->insert($contentDd, $copy, 0, 1, 0, 1);
3428 $newId = $inserted === 1 ? (int)$db->get_insert_id() : 0;
3429 if ($newId <= 0) {
3430 $db->rollback($contentDd);
3431 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Seite konnte nicht dupliziert werden.', 'db' => $inserted));
3432 }
3433
3434 dbxContentLngSync::afterPageSave($db, $newId, true);
3435 if ($db->commit($contentDd) !== 1) {
3436 $db->rollback($contentDd);
3437 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Die duplizierte Seite konnte nicht abgeschlossen werden.'));
3438 }
3439
3440 $mediaCopied = $this->copy_page_media_usage($db, $sourceId, $newId, $folderId, true);
3441 $inlineSync = $this->sync_inline_media_usage(
3442 $db,
3443 $newId,
3444 (string)($source['content'] ?? ''),
3445 $folderId
3446 );
3447 $this->flush_saved_page_cache($db, $newId);
3448
3449 $row = $db->select1($contentDd, $newId);
3450 if (is_array($row) && isset($row['content'])) {
3451 $row['content'] = $this->normalize_content_media_urls($row['content']);
3452 $row = $this->normalize_gallery_row($row);
3453 }
3454
3455 $this->cms_json_response(array(
3456 'ok' => 1,
3457 'success' => true,
3458 'id' => $newId,
3459 'source_id' => $sourceId,
3460 'row' => $row,
3461 'permalink' => (string)($row['permalink'] ?? $copy['permalink']),
3462 'media_copied' => $mediaCopied,
3463 'inline_media_sync' => $inlineSync,
3464 'open_lng_provision' => $this->lng_provision_open_flag($db, 'page', $newId),
3465 'msg' => 'Seite wurde dupliziert.',
3466 ));
3467 }
3468
3469 private function next_content_sorter($db, $folder_id) {
3470 $folder_id = (int)$folder_id;
3471 $rows = $db->select(dbxContentLng::ddContent(), 'folder = ' . $folder_id, '*', 'sorter,id', 'DESC', '', 1, 0, 0);
3472 $max = 0;
3473 if (is_array($rows) && isset($rows[0]) && is_array($rows[0])) {
3474 $max = (int)($rows[0]['sorter'] ?? 0);
3475 }
3476 return sprintf('%04d', $max + 10);
3477 }
3478
3479 private function new_folder_json() {
3480 $parent = (int)dbx()->get_modul_var('parent', 0, 'int');
3481 $db = dbx()->get_system_obj('dbxDB');
3482 $this->ensure_cms_schema($db);
3483 $sorter = $this->next_folder_sorter($db, $parent);
3484 $id = 0;
3485 if ($db->insert(dbxContentLng::ddFolder(), array(
3486 'name' => 'Neuer Ordner ' . date('H:i'),
3487 'parent_id' => $parent,
3488 'sorter' => $sorter,
3489 'group_read' => $parent > 0 ? 'parent' : '*',
3490 'template' => $parent > 0 ? 'parent' : 'c-content',
3491 'hero_template' => $parent > 0 ? 'parent' : 'image-hero',
3492 'hero_image_id' => 'parent',
3493 'hero_margin_top' => 'parent',
3494 'hero_height' => 'parent',
3495 'hero_variant' => 'parent',
3496 'hero_sticky' => 'parent',
3497 'hero_scroll_layer' => 'parent',
3498 )) === 1) {
3499 $id = $db->get_insert_id();
3500 }
3501 if ($id > 0) {
3502 dbxContentLngSync::afterFolderSave($db, $id, true);
3503 $this->flush_menu_cache();
3504 $this->cms_json_response(array(
3505 'ok' => 1,
3506 'success' => true,
3507 'id' => $id,
3508 'row' => $db->select1(dbxContentLng::ddFolder(), $id),
3509 'open_lng_provision' => $this->lng_provision_open_flag($db, 'folder', $id),
3510 ));
3511 }
3512 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner konnte nicht angelegt werden.'));
3513 }
3514
3515 private function save_folder_json() {
3516 $payload = $this->request_json();
3517 $id = (int)($payload['id'] ?? 0);
3518 $db = dbx()->get_system_obj('dbxDB');
3519 $this->ensure_cms_schema($db);
3520
3521 $name = $this->clean_text($payload['name'] ?? '', 120);
3522 $parent_id = (int)($payload['parent_id'] ?? 0);
3523 if ($name === '') {
3524 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Bitte eine Bezeichnung eintragen.'));
3525 }
3526 if ($parent_id < 0) $parent_id = 0;
3527 if ($id > 0 && $parent_id === $id) {
3528 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner kann nicht sein eigener Parent sein.'));
3529 }
3530 if ($id > 0 && $this->folder_is_descendant($db, $parent_id, $id)) {
3531 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner kann nicht unter einen eigenen Unterordner gelegt werden.'));
3532 }
3533
3534 $rights = $this->clean_text($payload['group_read'] ?? '', 512);
3535 if ($rights === '') $rights = $parent_id > 0 ? 'parent' : '*';
3536
3537 $old = $id > 0 ? $db->select1(dbxContentLng::ddFolder(), $id, '*', 0) : array();
3538 $data = array(
3539 'name' => $name,
3540 'parent_id' => $parent_id,
3541 'group_read' => $rights,
3542 'template' => $this->clean_text($payload['template'] ?? 'parent', 254),
3543 'hero_template' => $this->clean_text($payload['hero_template'] ?? 'parent', 80),
3544 'hero_image_id' => $this->clean_text($payload['hero_image_id'] ?? 'parent', 32),
3545 'hero_margin_top' => $this->clean_text($payload['hero_margin_top'] ?? 'parent', 32),
3546 'hero_height' => $this->clean_text($payload['hero_height'] ?? 'parent', 32),
3547 'hero_variant' => $this->clean_text($payload['hero_variant'] ?? 'parent', 32),
3548 'hero_sticky' => $this->clean_text($payload['hero_sticky'] ?? 'parent', 32),
3549 'hero_scroll_layer' => $this->clean_text($payload['hero_scroll_layer'] ?? 'parent', 32),
3550 );
3551 $data = $this->normalize_hero_payload($data);
3552 if ($id <= 0 || (is_array($old) && (int)($old['parent_id'] ?? 0) !== $parent_id)) {
3553 $data['sorter'] = $this->next_folder_sorter($db, $parent_id);
3554 }
3555
3556 if ($id > 0) {
3557 $ok = $db->update(dbxContentLng::ddFolder(), $data, $id);
3558 $saved_id = $id;
3559 } else {
3560 $ok = $db->insert(dbxContentLng::ddFolder(), $data);
3561 $saved_id = ($ok === 1) ? $db->get_insert_id() : 0;
3562 }
3563
3564 if ($ok === 1 && $saved_id > 0) {
3565 $this->sync_saved_hero_media_usage($db, 0, $saved_id, $data['hero_image_id']);
3566 dbxContentLngSync::afterFolderSave($db, $saved_id, $id <= 0);
3567 $syncResult = array('updated' => array(), 'skipped' => array(), 'errors' => array());
3568 if ($id > 0 && dbxContentLngSync::isMasterLng()) {
3569 dbxContentTranslate::clearWarnings();
3570 $syncResult = dbxContentLngSync::syncSlavesFromMaster($db, 'folder', $saved_id);
3571 $syncResult['translate_warnings'] = dbxContentTranslate::warnings();
3572 $this->flush_lng_sync_cache($db, $syncResult);
3573 }
3574 $this->flush_folder_cache($db, $saved_id);
3575 if (is_array($old) && (int)($old['parent_id'] ?? 0) !== $parent_id) {
3576 $this->flush_folder_cache($db, (int)($old['parent_id'] ?? 0));
3577 }
3578 $this->cms_json_response(array(
3579 'ok' => 1,
3580 'success' => true,
3581 'id' => $saved_id,
3582 'row' => $db->select1(dbxContentLng::ddFolder(), $saved_id),
3583 'lng_forked' => dbxContentLngSync::isMasterLng() ? 0 : 1,
3584 'lng_synced' => count($syncResult['updated'] ?? array()),
3585 'translate_warnings' => is_array($syncResult['translate_warnings'] ?? null) ? $syncResult['translate_warnings'] : array(),
3586 'open_lng_provision' => $this->lng_provision_open_flag($db, 'folder', $saved_id),
3587 ));
3588 }
3589 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner konnte nicht gespeichert werden.'));
3590 }
3591
3592 private function delete_folder_json() {
3593 $payload = $this->request_json();
3594 $id = (int)($payload['id'] ?? 0);
3595 if ($id <= 0) {
3596 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Kein Ordner gewaehlt.'));
3597 }
3598
3599 $db = dbx()->get_system_obj('dbxDB');
3600 $deleteLngs = $this->normalize_delete_lngs($payload);
3601 $result = $this->delete_folder_in_lngs($db, $id, $deleteLngs);
3602
3603 if ((int)($result['ok'] ?? 0) === 1) {
3604 $this->cms_json_response(array(
3605 'ok' => 1,
3606 'success' => true,
3607 'id' => $id,
3608 'deleted' => $result['deleted'] ?? array(),
3609 'warnings' => $result['errors'] ?? array(),
3610 ));
3611 }
3612
3613 $msg = count($result['errors'] ?? array()) ? implode(' ', $result['errors']) : 'Ordner konnte nicht geloescht werden.';
3614 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => $msg));
3615 }
3616
3617 private function delete_page_json() {
3618 $payload = $this->request_json();
3619 $id = (int)($payload['id'] ?? dbx()->get_modul_var('id', 0, 'int'));
3620 if ($id <= 0) {
3621 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Keine Seite gewaehlt.'));
3622 }
3623
3624 $db = dbx()->get_system_obj('dbxDB');
3625 $deleteLngs = $this->normalize_delete_lngs($payload);
3626 $result = $this->delete_page_in_lngs($db, $id, $deleteLngs);
3627
3628 if ((int)($result['ok'] ?? 0) === 1) {
3629 $this->flush_menu_cache();
3630 $this->cms_json_response(array(
3631 'ok' => 1,
3632 'success' => true,
3633 'id' => $id,
3634 'deleted' => $result['deleted'] ?? array(),
3635 'warnings' => $result['errors'] ?? array(),
3636 ));
3637 }
3638
3639 $msg = count($result['errors'] ?? array()) ? implode(' ', $result['errors']) : 'Seite konnte nicht geloescht werden.';
3640 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => $msg));
3641 }
3642
3643 private function move_node_json() {
3644 $payload = $this->request_json();
3645 $type = $this->clean_text($payload['type'] ?? '', 16);
3646 $id = (int)($payload['id'] ?? 0);
3647 $target = (int)($payload['target_folder'] ?? 0);
3648 $before_id = (int)($payload['before_id'] ?? 0);
3649 $after_id = (int)($payload['after_id'] ?? 0);
3650
3651 if ($id <= 0 || $target < 0 || !in_array($type, array('folder', 'page'), true)) {
3652 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ungueltige Tree-Verschiebung.'));
3653 }
3654
3655 if ($type === 'folder' && $id === $target) {
3656 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner kann nicht in sich selbst verschoben werden.'));
3657 }
3658
3659 $db = dbx()->get_system_obj('dbxDB');
3660 if ($type === 'folder') {
3661 if ($this->folder_is_descendant($db, $target, $id)) {
3662 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner kann nicht in einen eigenen Unterordner verschoben werden.'));
3663 }
3664 $data = array('parent_id' => $target);
3665 if ($before_id > 0 || $after_id > 0) {
3666 $data['sorter'] = $this->next_folder_sorter($db, $target);
3667 } else {
3668 $current = $db->select1(dbxContentLng::ddFolder(), $id, '*', 0);
3669 if (is_array($current) && (int)($current['parent_id'] ?? 0) !== $target) {
3670 $data['sorter'] = $this->next_folder_sorter($db, $target);
3671 }
3672 }
3673 $ok = $db->update(dbxContentLng::ddFolder(), $data, $id);
3674 if ($ok >= 0 && ($before_id > 0 || $after_id > 0)) {
3675 $this->reorder_folders($db, $target, $id, $before_id, $after_id);
3676 }
3677 } else {
3678 $data = array('folder' => $target);
3679 if ($before_id <= 0 && $after_id <= 0) {
3680 $current = $db->select1(dbxContentLng::ddContent(), $id, '*', 0);
3681 if (is_array($current) && (int)($current['folder'] ?? 0) !== $target) {
3682 $data['sorter'] = $this->next_content_sorter($db, $target);
3683 }
3684 }
3685 $ok = $db->update(dbxContentLng::ddContent(), $data, $id);
3686 if ($ok >= 0 && ($before_id > 0 || $after_id > 0)) {
3687 $this->reorder_content_pages($db, $target, $id, $before_id, $after_id);
3688 }
3689 }
3690
3691 if ($ok >= 0) {
3692 if ($type === 'page') {
3693 $this->flush_saved_page_cache($db, $id);
3694 } else {
3695 $this->flush_folder_cache($db, $id);
3696 $this->flush_folder_cache($db, $target);
3697 }
3698 $this->cms_json_response(array('ok' => 1, 'success' => true, 'id' => $id, 'type' => $type, 'target_folder' => $target));
3699 }
3700
3701 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Tree-Eintrag konnte nicht verschoben werden.'));
3702 }
3703
3704 private function folder_is_descendant($db, $folder_id, $ancestor_id) {
3705 $folder_id = (int)$folder_id;
3706 $ancestor_id = (int)$ancestor_id;
3707 $guard = 0;
3708 while ($folder_id > 0 && $guard < 100) {
3709 if ($folder_id === $ancestor_id) return true;
3710 $row = $db->select1(dbxContentLng::ddFolder(), $folder_id);
3711 if (!is_array($row)) return false;
3712 $folder_id = (int)($row['parent_id'] ?? 0);
3713 $guard++;
3714 }
3715 return false;
3716 }
3717
3718 private function next_folder_sorter($db, $parent_id) {
3719 $parent_id = (int)$parent_id;
3720 $rows = $db->select(dbxContentLng::ddFolder(), 'parent_id = ' . $parent_id, '*', 'sorter,id', 'DESC', '', 1, 0, 0);
3721 $max = 0;
3722 if (is_array($rows) && isset($rows[0]) && is_array($rows[0])) {
3723 $max = (int)($rows[0]['sorter'] ?? 0);
3724 }
3725 return sprintf('%04d', $max + 10);
3726 }
3727
3728 private function reorder_folders($db, $parent_id, $moved_id, $before_id = 0, $after_id = 0) {
3729 $parent_id = (int)$parent_id;
3730 $moved_id = (int)$moved_id;
3731 $rows = $db->select(dbxContentLng::ddFolder(), 'parent_id = ' . $parent_id, '*', 'sorter,name,id', 'ASC');
3732 if (!is_array($rows)) return;
3733
3734 $ordered = array();
3735 $moved = null;
3736 foreach ($rows as $row) {
3737 if (!is_array($row)) continue;
3738 $rid = (int)($row['id'] ?? 0);
3739 if ($rid === $moved_id) {
3740 $moved = $row;
3741 continue;
3742 }
3743 $ordered[] = $row;
3744 }
3745 if (!$moved) return;
3746
3747 $inserted = false;
3748 $out = array();
3749 foreach ($ordered as $row) {
3750 $rid = (int)($row['id'] ?? 0);
3751 if ($before_id > 0 && $rid === $before_id) {
3752 $out[] = $moved;
3753 $inserted = true;
3754 }
3755 $out[] = $row;
3756 if ($after_id > 0 && $rid === $after_id) {
3757 $out[] = $moved;
3758 $inserted = true;
3759 }
3760 }
3761 if (!$inserted) $out[] = $moved;
3762
3763 $sort = 10;
3764 foreach ($out as $row) {
3765 $rid = (int)($row['id'] ?? 0);
3766 if ($rid <= 0) continue;
3767 $db->update(dbxContentLng::ddFolder(), array('sorter' => sprintf('%04d', $sort)), $rid);
3768 $sort += 10;
3769 }
3770 }
3771
3772 private function reorder_content_pages($db, $folder_id, $moved_id, $before_id = 0, $after_id = 0) {
3773 $folder_id = (int)$folder_id;
3774 $moved_id = (int)$moved_id;
3775 $rows = $db->select(dbxContentLng::ddContent(), 'folder = ' . $folder_id, '*', 'sorter,title,id', 'ASC');
3776 if (!is_array($rows)) return;
3777
3778 $ordered = array();
3779 $moved = null;
3780 foreach ($rows as $row) {
3781 if (!is_array($row)) continue;
3782 $rid = (int)($row['id'] ?? 0);
3783 if ($rid === $moved_id) {
3784 $moved = $row;
3785 continue;
3786 }
3787 $ordered[] = $row;
3788 }
3789 if (!$moved) return;
3790
3791 $inserted = false;
3792 $out = array();
3793 foreach ($ordered as $row) {
3794 $rid = (int)($row['id'] ?? 0);
3795 if ($before_id > 0 && $rid === $before_id) {
3796 $out[] = $moved;
3797 $inserted = true;
3798 }
3799 $out[] = $row;
3800 if ($after_id > 0 && $rid === $after_id) {
3801 $out[] = $moved;
3802 $inserted = true;
3803 }
3804 }
3805 if (!$inserted) $out[] = $moved;
3806
3807 $sort = 10;
3808 $changed = array();
3809 foreach ($out as $row) {
3810 $rid = (int)($row['id'] ?? 0);
3811 if ($rid <= 0) continue;
3812 $sorter = sprintf('%04d', $sort);
3813 if ((string)($row['sorter'] ?? '') !== $sorter) {
3814 $db->update(dbxContentLng::ddContent(), array('sorter' => $sorter), $rid);
3815 $changed[] = $rid;
3816 }
3817 $sort += 10;
3818 }
3819 foreach ($changed as $rid) {
3820 dbxContentPageCache::invalidateContent((int)$rid);
3821 }
3822 if (count($changed)) {
3823 dbxContentPageCache::invalidateAllMenus();
3824 }
3825 }
3826
3827 private function media_usage_page_map($db, $usage_rows) {
3828 $map = array();
3829 if (!is_array($usage_rows)) return $map;
3830
3831 $page_cache = array();
3832 $folder_cache = array();
3833
3834 foreach ($usage_rows as $usage) {
3835 if (!is_array($usage)) continue;
3836 $media_id = (int)($usage['media_id'] ?? 0);
3837 $content_id = (int)($usage['content_id'] ?? 0);
3838 if ($media_id <= 0 || $content_id <= 0) continue;
3839
3840 if (!array_key_exists($content_id, $page_cache)) {
3841 $page_cache[$content_id] = $db->select1(dbxContentLng::ddContent(), $content_id, 'id,folder,title', 0);
3842 }
3843 $page = is_array($page_cache[$content_id]) ? $page_cache[$content_id] : array();
3844 $folder_id = (int)($page['folder'] ?? ($usage['folder_id'] ?? 0));
3845
3846 if ($folder_id > 0 && !array_key_exists($folder_id, $folder_cache)) {
3847 $folder_cache[$folder_id] = $db->select1(dbxContentLng::ddFolder(), $folder_id, '*', 0);
3848 }
3849 $folder = ($folder_id > 0 && is_array($folder_cache[$folder_id] ?? null)) ? $folder_cache[$folder_id] : array();
3850
3851 if (!isset($map[$media_id])) $map[$media_id] = array();
3852 if (!isset($map[$media_id][$content_id])) {
3853 $map[$media_id][$content_id] = array(
3854 'id' => $content_id,
3855 'content_id' => $content_id,
3856 'title' => (string)($page['title'] ?? ''),
3857 'folder_id' => $folder_id,
3858 'folder_title' => (string)($folder['name'] ?? $folder['title'] ?? ''),
3859 'slots' => array(),
3860 );
3861 }
3862
3863 $slot = trim((string)($usage['slot'] ?? ''));
3864 if ($slot !== '' && !in_array($slot, $map[$media_id][$content_id]['slots'], true)) {
3865 $map[$media_id][$content_id]['slots'][] = $slot;
3866 }
3867 }
3868
3869 foreach ($map as $media_id => $items) {
3870 ksort($items, SORT_NUMERIC);
3871 $map[$media_id] = array_values($items);
3872 }
3873
3874 return $map;
3875 }
3876
3877 private function media_json() {
3878 $content_id = (int)dbx()->get_modul_var('content_id', 0, 'int');
3879 $folder_id = (int)dbx()->get_modul_var('folder_id', 0, 'int');
3880 $images = (int)dbx()->get_modul_var('images', 0, 'int');
3881 $sync = (int)dbx()->get_modul_var('sync', 0, 'int');
3882 $media_type = strtolower(trim((string)dbx()->get_modul_var('media_type', '', 'varchar')));
3883 $raw_media_folder = trim((string)dbx()->get_modul_var('media_folder', '', 'varchar'));
3884 $media_folder = $raw_media_folder !== '' && strtolower($raw_media_folder) !== 'all'
3885 ? $this->clean_media_folder($raw_media_folder, $media_type ?: $this->media_type_from_folder($raw_media_folder))
3886 : '';
3887 $slot = trim((string)dbx()->get_modul_var('slot', '', 'varchar'));
3888 $query = trim((string)dbx()->get_modul_var('query', '', 'varchar'));
3889 $usage_only = (int)dbx()->get_modul_var('usage', 0, 'int');
3890 $db = dbx()->get_system_obj('dbxDB');
3891 $this->ensure_cms_schema($db);
3892 if ($sync) $this->sync_cms_media_files($db);
3893 $where = 'active = 1' . $this->cms_media_exclude_sql();
3894 if ($images) {
3895 $where .= " AND (mime LIKE 'image/%' OR file_name LIKE '%.jpg' OR file_name LIKE '%.jpeg' OR file_name LIKE '%.png' OR file_name LIKE '%.gif' OR file_name LIKE '%.webp' OR file_name LIKE '%.svg')";
3896 }
3897 if (in_array($media_type, array('image','video','external_video','file'), true)) {
3898 $where .= " AND media_type = '" . str_replace("'", "''", $media_type) . "'";
3899 }
3900 if ($media_folder !== '') {
3901 $where .= " AND media_folder = '" . str_replace("'", "''", $media_folder) . "'";
3902 }
3903 if ($query !== '') {
3904 $q = str_replace("'", "''", $query);
3905 $where .= " AND (title LIKE '%$q%' OR alt LIKE '%$q%' OR caption LIKE '%$q%' OR tags LIKE '%$q%' OR file_name LIKE '%$q%')";
3906 }
3907 $rows = $db->select($this->dd_media, $where, '*', 'media_folder,title,id');
3908 if (!is_array($rows)) $rows = array();
3909 $rows = $this->filter_existing_media($db, $rows);
3910 $usage_rows = $db->select($this->dd_media_usage, 'active = 1', '*', 'media_id,id', 'ASC', '', 0, 0, 0);
3911 $usage_count = array();
3912 $usage_pages = $this->media_usage_page_map($db, $usage_rows);
3913 $current_usage = array();
3914 $current_usage_row = array();
3915 if (is_array($usage_rows)) {
3916 foreach ($usage_rows as $usage) {
3917 if (!is_array($usage)) continue;
3918 $mid = (int)($usage['media_id'] ?? 0);
3919 if ($mid <= 0) continue;
3920 $usage_count[$mid] = ($usage_count[$mid] ?? 0) + 1;
3921 if (($content_id <= 0 || (int)($usage['content_id'] ?? 0) === $content_id)
3922 && ($folder_id <= 0 || (int)($usage['folder_id'] ?? 0) === $folder_id)
3923 && ($slot === '' || (string)($usage['slot'] ?? '') === $slot)) {
3924 $current_usage[$mid] = (int)($usage['id'] ?? 0);
3925 $current_usage_row[$mid] = $usage;
3926 }
3927 }
3928 }
3929 $rows = array_map(function($row) use ($usage_count, $usage_pages, $current_usage, $current_usage_row) {
3930 $row = $this->normalize_media_row($row);
3931 $id = (int)($row['id'] ?? 0);
3932 $row['used_count'] = (int)($usage_count[$id] ?? 0);
3933 $row['usage_pages'] = $usage_pages[$id] ?? array();
3934 $row['current_usage_id'] = (int)($current_usage[$id] ?? 0);
3935 if (!empty($current_usage_row[$id])) {
3936 $usage = $current_usage_row[$id];
3937 $row['usage_id'] = (int)($usage['id'] ?? 0);
3938 $row['slot'] = (string)($usage['slot'] ?? $row['slot'] ?? 'gallery');
3939 $row['sorter'] = (string)($usage['sorter'] ?? $row['sorter'] ?? '');
3940 if (!empty($usage['template'])) $row['template'] = $usage['template'];
3941 if (!empty($usage['caption'])) $row['caption'] = $usage['caption'];
3942 }
3943 return $row;
3944 }, $rows);
3945 if ($usage_only && ($content_id > 0 || $folder_id > 0 || $slot !== '')) {
3946 $rows = array_values(array_filter($rows, function($row) {
3947 return (int)($row['current_usage_id'] ?? 0) > 0;
3948 }));
3949 }
3950 $this->cms_json_response(array('ok' => 1, 'rows' => $rows));
3951 }
3952
3953 private function media_usage_rows_for_context($db, $content_id = 0, $folder_id = 0, $slot = '') {
3954 $content_id = (int)$content_id;
3955 $folder_id = (int)$folder_id;
3956 $where = 'active = 1';
3957 if ($content_id > 0) $where .= ' AND content_id = ' . $content_id;
3958 if ($folder_id > 0) $where .= ' AND folder_id = ' . $folder_id;
3959 if ($slot !== '') $where .= " AND slot = '" . str_replace("'", "''", (string)$slot) . "'";
3960
3961 $usage_rows = $db->select($this->dd_media_usage, $where, '*', 'slot,sorter,id', 'ASC', '', 0, 0, 0);
3962 if (!is_array($usage_rows)) return array();
3963
3964 $rows = array();
3965 foreach ($usage_rows as $usage) {
3966 if (!is_array($usage)) continue;
3967 $media_id = (int)($usage['media_id'] ?? 0);
3968 if ($media_id <= 0) continue;
3969 $row = $db->select1($this->dd_media, $media_id);
3970 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1) continue;
3971 if (!$this->media_file_exists($row)) continue;
3972 $row = $this->normalize_media_row($row);
3973 $row['usage_id'] = (int)($usage['id'] ?? 0);
3974 $row['current_usage_id'] = (int)($usage['id'] ?? 0);
3975 $row['slot'] = (string)($usage['slot'] ?? $row['slot'] ?? 'gallery');
3976 $row['sorter'] = (string)($usage['sorter'] ?? $row['sorter'] ?? '');
3977 if (!empty($usage['template'])) $row['template'] = $usage['template'];
3978 if (!empty($usage['caption'])) $row['caption'] = $usage['caption'];
3979 $rows[] = $row;
3980 }
3981
3982 return $rows;
3983 }
3984
3985 private function inline_media_block_has_content($inner) {
3986 $inner = trim((string)$inner);
3987 if ($inner === '') return false;
3988 if (preg_match('/<(img|video|iframe|source)\b/i', $inner)) return true;
3989 if (preg_match('/\bdbx-cms-inline-video-thumb\b/i', $inner)) return true;
3990 if (preg_match('/\bdbx-cms-inline-video-empty\b/i', $inner)) return true;
3991 if (preg_match('/\bdbx-cms-inline-media-missing\b/i', $inner)) return true;
3992 return false;
3993 }
3994
3995 private function strip_empty_inline_media_wrappers($html) {
3996 $html = (string)$html;
3997 $pattern = '/<(p|figure|div)\b([^>]*(?:\bdbx-cms-inline-media\b|\bdata-cms-media-id=)[^>]*)>([\s\S]*?)<\/\1>/i';
3998 do {
3999 $next = preg_replace_callback(
4000 $pattern,
4001 function($m) {
4002 if ($this->inline_media_block_has_content($m[3] ?? '')) return $m[0];
4003 return '';
4004 },
4005 $html
4006 );
4007 if ($next === null || $next === $html) break;
4008 $html = $next;
4009 } while (true);
4010 return $html;
4011 }
4012
4013 private function inline_media_ids($html) {
4014 $html = preg_replace('/<!--[\s\S]*?-->/', '', (string)$html);
4015 $ids = array();
4016
4017 if (preg_match_all('/<(p|figure|div)\b([^>]*(?:\bdbx-cms-inline-media\b|\bdata-cms-media-id=)[^>]*)>([\s\S]*?)<\/\1>/i', $html, $blocks, PREG_SET_ORDER)) {
4018 foreach ($blocks as $block) {
4019 $inner = (string)($block[3] ?? '');
4020 if (!$this->inline_media_block_has_content($inner)) continue;
4021 if (preg_match_all('/<(?:img|video|source|iframe)\b[^>]*\bsrc=["\'][^"\']*dbx_mid=([0-9]+)/i', $inner, $inner_ids)) {
4022 foreach ($inner_ids[1] as $id) {
4023 $id = (int)$id;
4024 if ($id > 0) $ids[$id] = $id;
4025 }
4026 continue;
4027 }
4028 if (preg_match('/\bdata-cms-media-id=["\']?([0-9]+)/i', (string)($block[0] ?? ''), $id_match)) {
4029 $id = (int)$id_match[1];
4030 if ($id > 0) $ids[$id] = $id;
4031 }
4032 }
4033 }
4034 if (preg_match_all('/<(?:img|video|source|iframe)\b[^>]*\bdata-cms-media-id=["\']?([0-9]+)/i', $html, $m)) {
4035 foreach ($m[1] as $id) {
4036 $id = (int)$id;
4037 if ($id > 0) $ids[$id] = $id;
4038 }
4039 }
4040 if (preg_match_all('/<(?:img|video|source|iframe)\b[^>]*\bsrc=["\'][^"\']*dbx_mid=([0-9]+)/i', $html, $m)) {
4041 foreach ($m[1] as $id) {
4042 $id = (int)$id;
4043 if ($id > 0) $ids[$id] = $id;
4044 }
4045 }
4046 return array_values($ids);
4047 }
4048
4049 private function resolve_inline_media_ids($html, $client_ids = null, $client_ids_provided = false) {
4050 $html_ids = array();
4051 foreach ($this->inline_media_ids($html) as $id) {
4052 $id = (int)$id;
4053 if ($id > 0) $html_ids[$id] = $id;
4054 }
4055 return array_values($html_ids);
4056 }
4057
4058 private function sync_inline_media_usage($db, $content_id, $html, $folder_id = 0, $client_ids = null, $client_ids_provided = false) {
4059 $content_id = (int)$content_id;
4060 $folder_id = (int)$folder_id;
4061 $result = array('added' => 0, 'removed' => 0, 'kept' => 0);
4062 if ($content_id <= 0) return $result;
4063
4064 $wanted = array();
4065 foreach ($this->resolve_inline_media_ids($html, $client_ids, $client_ids_provided) as $id) {
4066 $id = (int)$id;
4067 if ($id > 0) $wanted[$id] = $id;
4068 }
4069
4070 $rows = $db->select($this->dd_media_usage, 'content_id = ' . $content_id . " AND active = 1 AND slot = 'inline'", '*', 'id');
4071 if (is_array($rows)) {
4072 foreach ($rows as $row) {
4073 if (!is_array($row)) continue;
4074 $usage_id = (int)($row['id'] ?? 0);
4075 $media_id = (int)($row['media_id'] ?? 0);
4076 if ($usage_id > 0 && $media_id > 0 && !isset($wanted[$media_id])) {
4077 $db->update($this->dd_media_usage, array('active' => 0), $usage_id, 0, 1, 1, 0);
4078 $result['removed']++;
4079 }
4080 }
4081 }
4082
4083 foreach ($wanted as $id) {
4084 $row = $db->select1($this->dd_media, $id);
4085 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1) continue;
4086
4087 $usages = $db->select(
4088 $this->dd_media_usage,
4089 'media_id = ' . $id . ' AND content_id = ' . $content_id . " AND slot = 'inline'",
4090 '*', 'active DESC, id DESC', '', 0, 0, 0
4091 );
4092
4093 $active_usage = null;
4094 $inactive_usage = null;
4095 if (is_array($usages)) {
4096 foreach ($usages as $usage) {
4097 if (!is_array($usage)) continue;
4098 if ((int)($usage['active'] ?? 0) === 1) {
4099 if (!$active_usage) {
4100 $active_usage = $usage;
4101 } else {
4102 $dup_id = (int)($usage['id'] ?? 0);
4103 if ($dup_id > 0) {
4104 $db->update($this->dd_media_usage, array('active' => 0), $dup_id, 0, 1, 1, 0);
4105 $result['removed']++;
4106 }
4107 }
4108 continue;
4109 }
4110 if (!$inactive_usage) $inactive_usage = $usage;
4111 }
4112 }
4113
4114 if ($active_usage) {
4115 $usage_id = (int)($active_usage['id'] ?? 0);
4116 if ($usage_id > 0 && (int)($active_usage['folder_id'] ?? 0) !== $folder_id) {
4117 $db->update($this->dd_media_usage, array('folder_id' => $folder_id), $usage_id, 0, 1, 1, 0);
4118 }
4119 $result['kept']++;
4120 continue;
4121 }
4122
4123 if ($inactive_usage) {
4124 $usage_id = (int)($inactive_usage['id'] ?? 0);
4125 if ($usage_id > 0) {
4126 $db->update($this->dd_media_usage, array('active' => 1, 'folder_id' => $folder_id), $usage_id, 0, 1, 1, 0);
4127 $result['added']++;
4128 }
4129 continue;
4130 }
4131
4132 if ($this->create_media_usage($db, $id, $content_id, $folder_id, 'inline', 'image-inline', '', '') > 0) {
4133 $result['added']++;
4134 }
4135 }
4136
4137 return $result;
4138 }
4139
4140 private function next_media_sorter($db, $content_id, $slot) {
4141 $content_id = (int)$content_id;
4142 $slot = str_replace("'", "''", (string)$slot);
4143 $rows = $db->select($this->dd_media, "content_id = " . $content_id . " AND slot = '" . $slot . "' AND active = 1", '*', 'sorter,id', 'DESC', '', 1, 0, 0);
4144 $max = 0;
4145 if (is_array($rows) && isset($rows[0]) && is_array($rows[0])) {
4146 $max = (int)($rows[0]['sorter'] ?? 0);
4147 }
4148 return sprintf('%04d', $max + 10);
4149 }
4150
4151 private function deactivate_existing_hero_media($db, $content_id, $folder_id, $except_id = 0) {
4152 $content_id = (int)$content_id;
4153 $folder_id = (int)$folder_id;
4154 $except_id = (int)$except_id;
4155 $except = $except_id > 0 ? ' AND media_id <> ' . $except_id : '';
4156 if ($content_id > 0) {
4157 $db->update($this->dd_media_usage, array('active' => 0), "content_id = " . $content_id . " AND slot = 'hero' AND active = 1" . $except, 0, 1, 1, 0);
4158 } elseif ($folder_id > 0) {
4159 $db->update($this->dd_media_usage, array('active' => 0), "folder_id = " . $folder_id . " AND slot = 'hero' AND active = 1" . $except, 0, 1, 1, 0);
4160 }
4161 }
4162
4163 private function sync_saved_hero_media_usage($db, $content_id, $folder_id, $hero_image_id) {
4164 $content_id = (int)$content_id;
4165 $folder_id = (int)$folder_id;
4166 $hero_id = (int)$hero_image_id;
4167 if ($content_id <= 0 && $folder_id <= 0) return;
4168 if ($hero_id > 0) {
4169 $this->deactivate_existing_hero_media($db, $content_id, $folder_id, $hero_id);
4170 return;
4171 }
4172 $this->deactivate_existing_hero_media($db, $content_id, $folder_id, 0);
4173 }
4174
4175 private function is_no_hero_template($value) {
4176 $value = strtolower(trim((string)$value));
4177 return in_array($value, array('none', 'no-hero', '0', 'off'), true);
4178 }
4179
4180 private function normalize_hero_payload(array $data) {
4181 if ($this->is_no_hero_template($data['hero_template'] ?? '')) {
4182 $data['hero_template'] = 'none';
4183 $data['hero_image_id'] = '0';
4184 }
4185 return $data;
4186 }
4187
4188 private function source_media_file($row) {
4189 $rel = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
4190 if ($rel === '' || strpos($rel, '..') !== false) return '';
4191 $root = rtrim(dbx_get_file_dir(), '/\\') . '/';
4192 if (function_exists('dbx_os_path_file')) $root = dbx_os_path_file($root);
4193 $base = realpath(rtrim($root, '/\\'));
4194 if (!$base) return '';
4195 $file = $root . $rel;
4196 if (function_exists('dbx_os_path_file')) $file = dbx_os_path_file($file);
4197 $real = realpath($file);
4198 return $real && strpos($real, $base) === 0 && is_file($real) && is_readable($real) ? $real : '';
4199 }
4200
4201 private function copy_media_to_slot($db, array $row, $slot) {
4202 return 0;
4203 $slot = $this->valid_media_slot($slot);
4204 $src = $this->source_media_file($row);
4205 if ($src === '') return 0;
4206
4207 $dir = $this->cms_media_dir($slot);
4208 if (!is_dir($dir)) @mkdir($dir, 0777, true);
4209 if (!is_dir($dir)) return 0;
4210
4211 $name = $this->unique_media_name((string)($row['file_name'] ?? basename($src)), $slot);
4212 $dst = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR . $name;
4213 if (!copy($src, $dst)) return 0;
4214
4215 $rel = $this->media_rel_dir($slot) . $name;
4216 $mime = (string)($row['mime'] ?? '');
4217 $width = 0;
4218 $height = 0;
4219 $img = @getimagesize($dst);
4220 if (is_array($img)) {
4221 $width = (int)($img[0] ?? 0);
4222 $height = (int)($img[1] ?? 0);
4223 }
4224
4225 $insert = array(
4226 'active' => 1,
4227 'content_id' => (int)($row['content_id'] ?? 0),
4228 'folder_id' => (int)($row['folder_id'] ?? 0),
4229 'slot' => $slot,
4230 'usage' => $slot,
4231 'sorter' => $this->next_media_sorter($db, (int)($row['content_id'] ?? 0), $slot),
4232 'template' => (string)($row['template'] ?? ''),
4233 'title' => (string)($row['title'] ?? pathinfo($name, PATHINFO_FILENAME)),
4234 'alt' => (string)($row['alt'] ?? $row['title'] ?? ''),
4235 'caption' => (string)($row['caption'] ?? ''),
4236 'file_name' => $name,
4237 'file_path' => $rel,
4238 'mime' => $mime,
4239 'size' => (int)@filesize($dst),
4240 'width' => $width,
4241 'height' => $height,
4242 'tags' => (string)($row['tags'] ?? ''),
4243 'media_type' => $this->media_type(array('mime' => $mime, 'file_name' => $name)),
4244 );
4245 $thumb = $this->create_media_thumbnail($dst, $slot, $name, $mime);
4246 if ($thumb) $insert = array_merge($insert, $thumb);
4247 return ($db->insert($this->dd_media, $insert) === 1) ? $db->get_insert_id() : 0;
4248 }
4249
4250 private function set_media_slot_json() {
4251 $payload = $this->request_json();
4252 $usage_id = (int)($payload['usage_id'] ?? 0);
4253 $id = (int)($payload['media_id'] ?? $payload['id'] ?? 0);
4254 $slot = $this->valid_media_slot($payload['slot'] ?? 'gallery');
4255 if ($usage_id <= 0 && $id <= 0) $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Keine Medien- oder Usage-ID erhalten.'));
4256
4257 $db = dbx()->get_system_obj('dbxDB');
4258 $this->ensure_cms_schema($db);
4259 if ($usage_id <= 0) {
4260 $rows = $db->select($this->dd_media_usage, $this->usage_where($id), '*', 'id', 'DESC', '', 1, 0, 0);
4261 if (is_array($rows) && isset($rows[0])) $usage_id = (int)($rows[0]['id'] ?? 0);
4262 }
4263 if ($usage_id <= 0) {
4264 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Keine vorhandene Zuordnung gefunden.'));
4265 }
4266
4267 $usage = $db->select1($this->dd_media_usage, $usage_id);
4268 if (!is_array($usage) || (int)($usage['active'] ?? 0) !== 1) {
4269 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Zuordnung nicht gefunden.'));
4270 }
4271 if ($slot === 'hero') $this->deactivate_existing_hero_media($db, (int)($usage['content_id'] ?? 0), (int)($usage['folder_id'] ?? 0), (int)($usage['media_id'] ?? 0));
4272 $ok = $db->update($this->dd_media_usage, array('slot' => $slot), $usage_id);
4273 $usage = $this->normalize_usage_row($db->select1($this->dd_media_usage, $usage_id));
4274 $row = $this->normalize_media_row($db->select1($this->dd_media, (int)($usage['media_id'] ?? 0)));
4275 if ($ok >= 0) {
4276 $this->flush_media_cache($db, (int)($usage['content_id'] ?? 0), (int)($usage['folder_id'] ?? 0));
4277 }
4278 $this->cms_json_response(array('ok' => ($ok >= 0 ? 1 : 0), 'success' => ($ok >= 0), 'row' => $row, 'usage' => $usage, 'msg' => 'Zuordnung aktualisiert.'));
4279 }
4280
4281 private function assign_media_json() {
4282 $payload = $this->request_json();
4283 $id = (int)($payload['media_id'] ?? $payload['id'] ?? 0);
4284 $content_id = (int)($payload['content_id'] ?? 0);
4285 $folder_id = (int)($payload['folder_id'] ?? 0);
4286 $slot = $this->valid_media_slot($payload['slot'] ?? 'gallery');
4287 if ($id <= 0 || ($content_id <= 0 && !($folder_id > 0 && $slot === 'hero'))) {
4288 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Keine Zuordnung erhalten.'));
4289 }
4290
4291 $db = dbx()->get_system_obj('dbxDB');
4292 $this->ensure_cms_schema($db);
4293 $row = $db->select1($this->dd_media, $id);
4294 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1) {
4295 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Medium nicht gefunden.'));
4296 }
4297
4298 $usage_id = $this->create_media_usage(
4299 $db,
4300 $id,
4301 $content_id,
4302 $folder_id,
4303 $slot,
4304 (string)($payload['template'] ?? $row['template'] ?? ''),
4305 (string)($payload['caption'] ?? ''),
4306 is_array($payload['settings'] ?? null) ? json_encode($payload['settings'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string)($payload['settings'] ?? '')
4307 );
4308 if ($usage_id <= 0) $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Medium konnte nicht zugeordnet werden.'));
4309 $usage = $this->normalize_usage_row($db->select1($this->dd_media_usage, $usage_id));
4310 $this->flush_media_cache($db, $content_id, $folder_id);
4311 $this->cms_json_response(array('ok' => 1, 'success' => true, 'id' => $id, 'usage_id' => $usage_id, 'row' => $this->normalize_media_row($row), 'usage' => $usage, 'msg' => 'Zuordnung angelegt.'));
4312 }
4313
4314 private function sort_media_json() {
4315 $payload = $this->request_json();
4316 $content_id = (int)($payload['content_id'] ?? 0);
4317 $folder_id = (int)($payload['folder_id'] ?? 0);
4318 $slot = $this->valid_media_slot($payload['slot'] ?? 'gallery');
4319 $ids = $payload['ids'] ?? array();
4320 if (($content_id <= 0 && $folder_id <= 0) || !is_array($ids)) {
4321 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Keine Sortierung erhalten.'));
4322 }
4323
4324 $db = dbx()->get_system_obj('dbxDB');
4325 $this->ensure_cms_schema($db);
4326 $sort = 10;
4327 foreach ($ids as $id) {
4328 $id = (int)$id;
4329 if ($id <= 0) continue;
4330 $where = "active = 1 AND slot = '" . str_replace("'", "''", $slot) . "'";
4331 if ($content_id > 0) $where .= ' AND content_id = ' . $content_id;
4332 if ($folder_id > 0) $where .= ' AND folder_id = ' . $folder_id;
4333 $where .= ' AND (id = ' . $id . ' OR media_id = ' . $id . ')';
4334 $rows = $db->select($this->dd_media_usage, $where, '*', 'id', 'ASC', '', 1, 0, 0);
4335 if (!is_array($rows) || !isset($rows[0])) continue;
4336 $db->update($this->dd_media_usage, array('sorter' => sprintf('%04d', $sort)), (int)$rows[0]['id']);
4337 $sort += 10;
4338 }
4339 $this->flush_media_cache($db, $content_id, $folder_id);
4340 $this->cms_json_response(array('ok' => 1, 'success' => true));
4341 }
4342
4343 private function filter_existing_media($db, array $rows) {
4344 $out = array();
4345 foreach ($rows as $row) {
4346 if (!is_array($row)) continue;
4347 if ($this->media_file_exists($row)) {
4348 $out[] = $row;
4349 continue;
4350 }
4351 $id = (int)($row['id'] ?? 0);
4352 if ($id > 0) {
4353 $db->update($this->dd_media, array('active' => 0), $id);
4354 }
4355 }
4356 return $out;
4357 }
4358
4359 private function upload_json() {
4360 $content_length = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
4361 $max_upload = $this->upload_max_bytes();
4362 if ($content_length > 0 && $max_upload > 0 && $content_length > $max_upload) {
4363 $this->cms_json_response(array(
4364 'ok' => 0,
4365 'success' => false,
4366 'msg' => 'Upload zu gross. Erlaubt sind maximal ' . round($max_upload / 1024 / 1024) . ' MB.'
4367 ));
4368 }
4369
4370 $file = $this->first_upload_file();
4371 if (empty($file) || !is_array($file)) {
4372 $this->cms_json_response(array('ok' => 0, 'msg' => 'Keine Datei erhalten. Bitte Upload-Limit und Dateigroesse pruefen.'));
4373 }
4374
4375 if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
4376 if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
4377 $this->cms_json_response(array('ok' => 0, 'msg' => 'Bitte zuerst eine Datei auswaehlen.'));
4378 }
4379 $this->cms_json_response(array('ok' => 0, 'msg' => 'Upload fehlgeschlagen.'));
4380 }
4381
4382 $name = basename((string)$file['name']);
4383 $name = preg_replace('/[^A-Za-z0-9_.-]+/', '-', $name);
4384 $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
4385 if (!in_array($ext, array('jpg','jpeg','png','gif','webp','svg','pdf','mp4','webm','ogv','ogg','mov','m4v'), true)) {
4386 $this->cms_json_response(array('ok' => 0, 'msg' => 'Dateityp nicht erlaubt.'));
4387 }
4388
4389 $db = dbx()->get_system_obj('dbxDB');
4390 $this->ensure_cms_schema($db);
4391 $content_id = (int)($_POST['content_id'] ?? 0);
4392 $folder_id = (int)($_POST['folder_id'] ?? 0);
4393 $slot = $this->valid_media_slot($_POST['slot'] ?? 'gallery');
4394 $probe_type = $this->media_type(array('file_name' => $name, 'mime' => (string)($file['type'] ?? '')));
4395 $media_folder = $this->clean_media_folder($_POST['media_folder'] ?? '', $probe_type);
4396 $media_rel_dir = $this->media_folder_rel_dir($media_folder, $probe_type);
4397 $dir = $this->cms_media_dir($media_folder);
4398 if (!is_dir($dir)) {
4399 mkdir($dir, 0777, true);
4400 }
4401
4402 $dstName = $this->unique_media_name($name);
4403 $dst = $dir . $dstName;
4404
4405 if (!move_uploaded_file($file['tmp_name'], $dst)) {
4406 $this->cms_json_response(array('ok' => 0, 'msg' => 'Datei konnte nicht gespeichert werden.'));
4407 }
4408
4409 $width = 0;
4410 $height = 0;
4411 $detected_mime = function_exists('mime_content_type') ? (string)@mime_content_type($dst) : '';
4412 $mime = $detected_mime !== '' ? $detected_mime : (string)($file['type'] ?? '');
4413 $img = @getimagesize($dst);
4414 if (is_array($img)) {
4415 $width = (int)($img[0] ?? 0);
4416 $height = (int)($img[1] ?? 0);
4417 }
4418 $max_image_size = max(800, min(6000, (int)($_POST['max_image_size'] ?? 2560)));
4419 if ($width > 0 && $height > 0 && $this->media_type(array('mime' => $mime, 'file_name' => $dstName)) === 'image') {
4420 if ($this->resize_image_to_max($dst, $mime, $max_image_size)) {
4421 clearstatcache(true, $dst);
4422 $img = @getimagesize($dst);
4423 if (is_array($img)) {
4424 $width = (int)($img[0] ?? 0);
4425 $height = (int)($img[1] ?? 0);
4426 }
4427 }
4428
4429 $webp = $this->convert_media_image_to_webp($dst, $dstName, $mime);
4430 if (is_array($webp) && !empty($webp['file']) && !empty($webp['name'])) {
4431 $dst = (string)$webp['file'];
4432 $dstName = (string)$webp['name'];
4433 $mime = (string)($webp['mime'] ?? 'image/webp');
4434 clearstatcache(true, $dst);
4435 $img = @getimagesize($dst);
4436 if (is_array($img)) {
4437 $width = (int)($img[0] ?? 0);
4438 $height = (int)($img[1] ?? 0);
4439 }
4440 }
4441 }
4442
4443 $media_tpl = $this->clean_text($_POST['template'] ?? '', 80);
4444 $file_size = (int)@filesize($dst);
4445 $title = pathinfo($name, PATHINFO_FILENAME);
4446 $media_type = $this->media_type(array('mime' => $mime, 'file_name' => $dstName));
4447 $media_folder = $this->clean_media_folder($media_folder, $media_type);
4448 if ($media_tpl === '') {
4449 $media_tpl = $media_type === 'video' ? ($slot === 'hero' ? 'video-hero' : 'video-gallery') : '';
4450 }
4451
4452 $existing = $db->select(
4453 $this->dd_media,
4454 "active = 1 AND storage_type = 'local' AND media_folder = '" . str_replace("'", "''", $media_folder) . "' AND title = '" . str_replace("'", "''", $title) . "' AND size = " . $file_size,
4455 '*',
4456 'id',
4457 'DESC'
4458 );
4459 if (is_array($existing) && isset($existing[0]) && $this->media_file_exists($existing[0])) {
4460 @unlink($dst);
4461 $row = $this->normalize_media_row($existing[0]);
4462 $usage = array();
4463 if ($content_id > 0 || $folder_id > 0) {
4464 $usage_id = $this->create_media_usage($db, (int)($row['id'] ?? 0), $content_id, $folder_id, $slot, $media_tpl, '', '');
4465 if ($usage_id > 0) $usage = $this->normalize_usage_row($db->select1($this->dd_media_usage, $usage_id));
4466 }
4467 $this->flush_media_cache($db, $content_id, $folder_id);
4468 $this->cms_json_response(array(
4469 'ok' => 1,
4470 'success' => true,
4471 'duplicate' => true,
4472 'row' => $row,
4473 'usage' => $usage,
4474 'files' => array($row['file_name'] ?? ''),
4475 'path' => '',
4476 'baseurl' => '',
4477 ));
4478 }
4479
4480 $insert = array(
4481 'active' => 1,
4482 'content_id' => 0,
4483 'folder_id' => 0,
4484 'slot' => '',
4485 'usage' => '',
4486 'sorter' => '',
4487 'template' => $media_tpl,
4488 'title' => $title,
4489 'alt' => $title,
4490 'file_name' => $dstName,
4491 'file_path' => $media_rel_dir . $dstName,
4492 'mime' => $mime,
4493 'size' => $file_size,
4494 'width' => $width,
4495 'height' => $height,
4496 'media_type' => $media_type,
4497 'storage_type' => 'local',
4498 'media_folder' => $media_folder,
4499 );
4500 $thumb = $this->create_media_thumbnail($dst, $media_folder, $dstName, $mime);
4501 if ($thumb) $insert = array_merge($insert, $thumb);
4502 $id = ($db->insert($this->dd_media, $insert) === 1) ? $db->get_insert_id() : 0;
4503
4504 if ($id > 0) {
4505 $usage = array();
4506 if ($content_id > 0 || $folder_id > 0) {
4507 $usage_id = $this->create_media_usage($db, $id, $content_id, $folder_id, $slot, $media_tpl, '', '');
4508 if ($usage_id > 0) $usage = $this->normalize_usage_row($db->select1($this->dd_media_usage, $usage_id));
4509 }
4510 $row = $this->normalize_media_row($db->select1($this->dd_media, $id));
4511 $baseurl = '';
4512 if (!empty($row['url'])) {
4513 $baseurl = preg_replace('~/[^/]*$~', '/', $row['url']);
4514 }
4515 $this->flush_media_cache($db, $content_id, $folder_id);
4516 $this->cms_json_response(array(
4517 'ok' => 1,
4518 'success' => true,
4519 'row' => $row,
4520 'usage' => $usage,
4521 'files' => array($row['file_name'] ?? $dstName),
4522 'path' => '',
4523 'baseurl' => $baseurl,
4524 ));
4525 }
4526
4527 $this->cms_json_response(array('ok' => 0, 'msg' => 'Mediendatensatz konnte nicht gespeichert werden.'));
4528 }
4529
4530 private function remove_media_json() {
4531 $id = (int)dbx()->get_modul_var('id', 0, 'int');
4532 $usage_id = (int)dbx()->get_modul_var('usage_id', 0, 'int');
4533 $payload = array();
4534 if ($id <= 0 || $usage_id <= 0) {
4535 $payload = $this->request_json();
4536 if ($usage_id <= 0) $usage_id = (int)($payload['usage_id'] ?? 0);
4537 if ($id <= 0) $id = (int)($payload['media_id'] ?? $payload['id'] ?? 0);
4538 }
4539 if ($usage_id <= 0 && $id <= 0) $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Keine Medien- oder Usage-ID erhalten.'));
4540
4541 $db = dbx()->get_system_obj('dbxDB');
4542 $this->ensure_cms_schema($db);
4543 $content_id = (int)($payload['content_id'] ?? dbx()->get_modul_var('content_id', 0, 'int'));
4544 $folder_id = (int)($payload['folder_id'] ?? dbx()->get_modul_var('folder_id', 0, 'int'));
4545 if ($usage_id > 0) {
4546 $usage_row = $db->select1($this->dd_media_usage, $usage_id);
4547 if (is_array($usage_row)) {
4548 if ($content_id <= 0) $content_id = (int)($usage_row['content_id'] ?? 0);
4549 if ($folder_id <= 0) $folder_id = (int)($usage_row['folder_id'] ?? 0);
4550 if ($id <= 0) $id = (int)($usage_row['media_id'] ?? 0);
4551 }
4552 $ok = $db->update($this->dd_media_usage, array('active' => 0), $usage_id);
4553 } else {
4554 $slot = trim((string)($payload['slot'] ?? dbx()->get_modul_var('slot', '', 'varchar')));
4555 if ($content_id <= 0 && $folder_id <= 0) {
4556 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Keine Seiten- oder Ordner-Zuordnung erhalten.'));
4557 }
4558 $where = 'media_id = ' . $id . ' AND active = 1';
4559 if ($content_id > 0) $where .= ' AND content_id = ' . $content_id;
4560 if ($folder_id > 0) $where .= ' AND folder_id = ' . $folder_id;
4561 if ($slot !== '') $where .= " AND slot = '" . str_replace("'", "''", $slot) . "'";
4562 $ok = $db->update($this->dd_media_usage, array('active' => 0), $where, 0, 1, 1, 0);
4563 }
4564 $media = ($content_id > 0 || $folder_id > 0) ? $this->media_usage_rows_for_context($db, $content_id, $content_id > 0 ? 0 : $folder_id) : array();
4565 if ($ok >= 0) {
4566 $this->flush_media_cache($db, $content_id, $folder_id);
4567 }
4568 $this->cms_json_response(array('ok' => ($ok >= 0 ? 1 : 0), 'success' => ($ok >= 0), 'id' => $id, 'usage_id' => $usage_id, 'media' => $media, 'rows' => $media, 'msg' => 'Zuordnung entfernt.'));
4569 }
4570
4571 private function delete_media_json() {
4572 $id = (int)dbx()->get_modul_var('id', 0, 'int');
4573 $force = (int)dbx()->get_modul_var('force', 0, 'int');
4574 if ($id <= 0) {
4575 $payload = $this->request_json();
4576 $id = (int)($payload['media_id'] ?? $payload['id'] ?? 0);
4577 $force = (int)($payload['force'] ?? 0);
4578 }
4579 if ($id <= 0) $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Keine Medien-ID erhalten.'));
4580
4581 $db = dbx()->get_system_obj('dbxDB');
4582 $this->ensure_cms_schema($db);
4583 $used = $db->select($this->dd_media_usage, $this->usage_where($id), '*', 'id', 'ASC', '', 1, 0, 0);
4584 if (is_array($used) && isset($used[0]) && !$force) {
4585 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Medium wird noch verwendet.'));
4586 }
4587 $row = $db->select1($this->dd_media, $id);
4588 if (is_array($row)) {
4589 $this->flush_media_by_media_id($db, $id);
4590 if ($force) $db->update($this->dd_media_usage, array('active' => 0), 'media_id = ' . $id . ' AND active = 1', 0, 1, 1, 0);
4591 if (strtolower((string)($row['storage_type'] ?? 'local')) === 'local') {
4592 $file = $this->source_media_file($row);
4593 if ($file !== '') @unlink($file);
4594 } else {
4595 $file = $this->source_media_file($row);
4596 if ($file !== '' && preg_match('/\.json$/i', $file)) @unlink($file);
4597 }
4598 $thumb = $this->source_thumb_file($row);
4599 if ($thumb !== '') @unlink($thumb);
4600 }
4601 $ok = $db->update($this->dd_media, array('active' => 0), $id);
4602 $this->cms_json_response(array('ok' => ($ok >= 0 ? 1 : 0), 'success' => ($ok >= 0), 'id' => $id, 'msg' => 'Medium geloescht.'));
4603 }
4604
4605 private function youtube_video_id($url) {
4606 $url = trim((string)$url);
4607 if ($url === '') return '';
4608 if (preg_match('~^[A-Za-z0-9_-]{11}$~', $url)) return $url;
4609 $parts = @parse_url($url);
4610 if (!is_array($parts)) return '';
4611 $host = strtolower((string)($parts['host'] ?? ''));
4612 $path = trim((string)($parts['path'] ?? ''), '/');
4613 if (strpos($host, 'youtu.be') !== false && preg_match('~^([A-Za-z0-9_-]{11})~', $path, $m)) return $m[1];
4614 if (strpos($host, 'youtube.com') !== false) {
4615 parse_str((string)($parts['query'] ?? ''), $query);
4616 if (!empty($query['v']) && preg_match('~^[A-Za-z0-9_-]{11}$~', (string)$query['v'])) return (string)$query['v'];
4617 if (preg_match('~^(embed|shorts)/([A-Za-z0-9_-]{11})~', $path, $m)) return $m[2];
4618 }
4619 return '';
4620 }
4621
4622 private function external_video_json() {
4623 $payload = $this->request_json();
4624 $provider = strtolower(trim((string)($payload['provider'] ?? 'youtube')));
4625 $external_url = trim((string)($payload['external_url'] ?? $payload['url'] ?? ''));
4626 if ($provider !== 'youtube') $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Provider nicht unterstuetzt.'));
4627 $video_id = $this->youtube_video_id($external_url);
4628 if ($video_id === '') $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'YouTube-URL nicht erkannt.'));
4629
4630 $db = dbx()->get_system_obj('dbxDB');
4631 $this->ensure_cms_schema($db);
4632 $content_id = (int)($payload['content_id'] ?? 0);
4633 $folder_id = (int)($payload['folder_id'] ?? 0);
4634 $slot = $this->valid_media_slot($payload['slot'] ?? 'gallery');
4635 $media_folder = $this->clean_media_folder($payload['media_folder'] ?? 'youtube', 'external_video');
4636 $title = $this->clean_text($payload['title'] ?? ('YouTube ' . $video_id), 160);
4637 $embed_url = 'https://www.youtube.com/embed/' . $video_id;
4638 $thumb_url = 'https://img.youtube.com/vi/' . $video_id . '/hqdefault.jpg';
4639
4640 $dir = $this->cms_media_dir($media_folder);
4641 if (!is_dir($dir)) @mkdir($dir, 0777, true);
4642 $rel = $this->media_folder_rel_dir($media_folder, 'external_video') . $video_id . '.json';
4643 $file = $this->file_from_media_rel($rel);
4644 if ($file !== '') {
4645 @file_put_contents($file, json_encode(array(
4646 'provider' => 'youtube',
4647 'provider_id' => $video_id,
4648 'external_url' => $external_url,
4649 'embed_url' => $embed_url,
4650 'title' => $title,
4651 'thumb_url' => $thumb_url,
4652 ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
4653 }
4654 $existing = $db->select($this->dd_media, "active = 1 AND provider = 'youtube' AND provider_id = '" . str_replace("'", "''", $video_id) . "'", '*', 'id', 'DESC', '', 1, 0, 0);
4655 if (is_array($existing) && isset($existing[0])) {
4656 $row = $existing[0];
4657 $update = array(
4658 'media_folder' => $media_folder,
4659 'file_name' => $video_id . '.json',
4660 'file_path' => $rel,
4661 'mime' => 'application/json',
4662 'media_type' => 'external_video',
4663 'storage_type' => 'external',
4664 'external_url' => $external_url,
4665 'embed_url' => $embed_url,
4666 'title' => $title,
4667 'size' => $file !== '' && is_file($file) ? (int)@filesize($file) : 0,
4668 );
4669 $db->update($this->dd_media, $update, (int)($row['id'] ?? 0));
4670 $row = array_merge($row, $update);
4671 } else {
4672 $id = 0;
4673 if ($db->insert($this->dd_media, array(
4674 'active' => 1,
4675 'content_id' => 0,
4676 'folder_id' => 0,
4677 'slot' => '',
4678 'usage' => '',
4679 'sorter' => '',
4680 'template' => 'video-gallery',
4681 'title' => $title,
4682 'alt' => $title,
4683 'file_name' => $video_id . '.json',
4684 'file_path' => $rel,
4685 'mime' => 'application/json',
4686 'size' => $file !== '' && is_file($file) ? (int)@filesize($file) : 0,
4687 'width' => 0,
4688 'height' => 0,
4689 'media_type' => 'external_video',
4690 'storage_type' => 'external',
4691 'media_folder' => $media_folder,
4692 'provider' => 'youtube',
4693 'provider_id' => $video_id,
4694 'external_url' => $external_url,
4695 'embed_url' => $embed_url,
4696 )) === 1) {
4697 $id = $db->get_insert_id();
4698 }
4699 $row = $db->select1($this->dd_media, $id);
4700 }
4701
4702 $usage = array();
4703 if (is_array($row) && ((int)($row['id'] ?? 0) > 0) && ($content_id > 0 || $folder_id > 0)) {
4704 $usage_id = $this->create_media_usage($db, (int)$row['id'], $content_id, $folder_id, $slot, 'video-gallery', '', '');
4705 if ($usage_id > 0) $usage = $this->normalize_usage_row($db->select1($this->dd_media_usage, $usage_id));
4706 }
4707 $this->flush_media_cache($db, $content_id, $folder_id);
4708 $this->cms_json_response(array('ok' => 1, 'success' => true, 'row' => $this->normalize_media_row($row), 'usage' => $usage, 'msg' => 'Externes Video gespeichert.'));
4709 }
4710
4711 private function media_dir_has_content($dir) {
4712 if (!is_dir($dir) || !is_readable($dir)) return false;
4713 $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS));
4714 foreach ($it as $item) {
4715 if ($item->isFile()) return true;
4716 }
4717 return false;
4718 }
4719
4720 private function is_reserved_media_folder($folder) {
4721 $folder = $this->canonical_media_folder(strtolower(trim(str_replace('\\', '/', (string)$folder), '/')));
4722 if ($folder === 'module' || strpos($folder, 'module/') === 0) return true;
4723 if (preg_match('~^img/external(/|$)~', $folder)) return true;
4724 if (preg_match('~^img/video(/|$)~', $folder)) return true;
4725 return in_array($folder, $this->media_slots(), true);
4726 }
4727
4728 private function media_folder_dir_is_listable($folder, $path, $base = '') {
4729 return is_dir($path) && is_readable($path);
4730 }
4731
4732 private function media_folder_is_listable($folder) {
4733 $folder = $this->canonical_media_folder($folder);
4734 if ($folder === '' || $folder === 'module') return false;
4735 if ($this->is_reserved_media_folder($folder)) return false;
4736 $dir = $this->cms_media_dir($folder);
4737 return is_dir($dir) && is_readable($dir);
4738 }
4739
4740 private function collect_custom_media_root_folders(array &$folders) {
4741 $mediaRoot = rtrim(dbx_get_file_dir(), '/\\') . '/media/';
4742 if (function_exists('dbx_os_path_file')) $mediaRoot = dbx_os_path_file($mediaRoot);
4743 if (!is_dir($mediaRoot) || !is_readable($mediaRoot)) return;
4744 $skip = array('.', '..', '_thumbs', 'img', 'video', 'videos', 'youtube', 'external', 'file', 'module');
4745 $skip = array_merge($skip, $this->media_slots());
4746 $root_norm = str_replace('\\', '/', rtrim($mediaRoot, '/\\')) . '/';
4747 $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($mediaRoot, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
4748 foreach ($it as $item) {
4749 if (!$item->isDir()) continue;
4750 $path = str_replace('\\', '/', $item->getPathname());
4751 if (strpos($path . '/', $root_norm) !== 0) continue;
4752 $rel = trim(substr($path, strlen($root_norm)), '/');
4753 if ($rel === '') continue;
4754 $top = strtok($rel, '/');
4755 if ($top === false || in_array($top, $skip, true)) continue;
4756 if ($this->is_reserved_media_folder($rel)) continue;
4757 $folders[] = $rel;
4758 }
4759 }
4760
4761 private function media_folders_json() {
4762 $bases = $this->media_standard_bases();
4763 $folders = array();
4764 foreach ($bases as $base => $type) {
4765 $root = $this->cms_media_base_dir($base);
4766 if (!is_dir($root) || !is_readable($root)) continue;
4767 if ($base === 'youtube' || $type === 'video') {
4768 $folders[] = $this->clean_media_folder($base, $type);
4769 }
4770 $root_norm = str_replace('\\', '/', rtrim($root, '/\\')) . '/';
4771 $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
4772 foreach ($it as $item) {
4773 if (!$item->isDir()) continue;
4774 $path = str_replace('\\', '/', $item->getPathname());
4775 if (strpos($path . '/', $root_norm) !== 0) continue;
4776 $rel = trim(substr($path, strlen($root_norm)), '/');
4777 if ($rel === '') continue;
4778 $folder = $this->clean_media_folder($base . '/' . $rel, $type);
4779 if ($this->is_reserved_media_folder($folder)) continue;
4780 if (!$this->media_folder_dir_is_listable($folder, $path, $base)) continue;
4781 $folders[] = $folder;
4782 }
4783 }
4784 $this->collect_custom_media_root_folders($folders);
4785
4786 $folders = array_values(array_unique(array_map(array($this, 'canonical_media_folder'), $folders)));
4787 $verified = array();
4788 foreach ($folders as $folder) {
4789 $folder = $this->canonical_media_folder($folder);
4790 if ($folder === '' || $this->is_excluded_cms_media_folder($folder)) continue;
4791 $dir = $this->cms_media_dir($folder);
4792 if (!is_dir($dir) || !is_readable($dir)) continue;
4793 $verified[] = $folder;
4794 }
4795 $folders = $verified;
4796 sort($folders);
4797 $this->cms_json_response(array('ok' => 1, 'success' => true, 'root' => 'files/media/', 'folders' => $folders));
4798 }
4799
4800 private function media_folder_create_json() {
4801 $payload = $this->request_json();
4802 $type = $this->media_type(array('media_type' => ($payload['media_type'] ?? 'image')));
4803 $folder = $this->clean_media_folder($payload['media_folder'] ?? $payload['folder'] ?? '', $type);
4804 if ($this->is_excluded_cms_media_folder($folder)) {
4805 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Dieser Ordner ist im CMS-Medienbrowser nicht verfuegbar.'));
4806 }
4807 $dir = $this->cms_media_dir($folder);
4808 if (!is_dir($dir) && !@mkdir($dir, 0777, true)) {
4809 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner konnte nicht angelegt werden.'));
4810 }
4811 $this->cms_json_response(array('ok' => 1, 'success' => true, 'folder' => $folder, 'msg' => 'Ordner angelegt.'));
4812 }
4813
4814 private function media_folder_delete_json() {
4815 $payload = $this->request_json();
4816 $raw_folder = $payload['media_folder'] ?? $payload['folder'] ?? '';
4817 $type = $this->media_type(array('media_type' => ($payload['media_type'] ?? $this->media_type_from_folder($raw_folder))));
4818 $folder = $this->clean_media_folder($raw_folder, $type);
4819 if ($this->is_excluded_cms_media_folder($folder)) {
4820 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Dieser Ordner ist im CMS-Medienbrowser nicht verfuegbar.'));
4821 }
4822 $dir = $this->cms_media_dir($folder);
4823 if (!is_dir($dir)) $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner nicht gefunden.'));
4824
4825 $db = dbx()->get_system_obj('dbxDB');
4826 $this->ensure_cms_schema($db);
4827 $used = $db->select($this->dd_media, "active = 1 AND media_folder = '" . str_replace("'", "''", $folder) . "'", '*', 'id', 'ASC', '', 1, 0, 0);
4828 if (is_array($used) && isset($used[0])) $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner enthaelt noch Medien.'));
4829
4830 $items = @scandir($dir);
4831 $items = is_array($items) ? array_diff($items, array('.', '..')) : array();
4832 if (!empty($items)) $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner ist nicht leer.'));
4833 if (!@rmdir($dir)) $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner konnte nicht geloescht werden.'));
4834 $this->cms_json_response(array('ok' => 1, 'success' => true, 'folder' => $folder, 'msg' => 'Ordner geloescht.'));
4835 }
4836
4837 private function media_folder_rename_json() {
4838 $payload = $this->request_json();
4839 $from = $this->clean_media_folder($payload['from_folder'] ?? $payload['media_folder_from'] ?? $payload['from'] ?? '', 'image');
4840 $to_raw = trim((string)($payload['to_folder'] ?? $payload['media_folder_to'] ?? $payload['to'] ?? ''));
4841 if ($to_raw !== '' && strpos($to_raw, '/') === false && $from !== '') {
4842 $base = strtok($from, '/');
4843 $to_raw = ($base !== false && $base !== '' ? $base : 'img') . '/' . $to_raw;
4844 }
4845 $to = $this->clean_media_folder($to_raw, $this->media_type_from_folder($to_raw !== '' ? $to_raw : $from));
4846 if ($from === '' || $to === '' || $from === $to) {
4847 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ungueltiger Ordner.'));
4848 }
4849 if ($this->is_excluded_cms_media_folder($from) || $this->is_excluded_cms_media_folder($to)) {
4850 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Dieser Ordner ist im CMS-Medienbrowser nicht verfuegbar.'));
4851 }
4852 $from_dir = $this->cms_media_dir($from);
4853 $to_dir = $this->cms_media_dir($to);
4854 if (!is_dir($from_dir) || !is_readable($from_dir)) {
4855 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Quellordner nicht gefunden.'));
4856 }
4857 if (is_dir($to_dir)) {
4858 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Zielordner existiert bereits.'));
4859 }
4860 if (!@rename($from_dir, $to_dir)) {
4861 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ordner konnte nicht umbenannt werden.'));
4862 }
4863
4864 $db = dbx()->get_system_obj('dbxDB');
4865 $this->ensure_cms_schema($db);
4866 $from_prefix = 'media/' . $from . '/';
4867 $to_prefix = 'media/' . $to . '/';
4868 $thumb_from = 'media/_thumbs/' . $from . '/';
4869 $thumb_to = 'media/_thumbs/' . $to . '/';
4870 $thumb_from_dir = $this->file_from_media_rel($thumb_from);
4871 $thumb_to_dir = $this->file_from_media_rel($thumb_to);
4872 if ($thumb_from_dir !== '' && is_dir($thumb_from_dir) && $thumb_to_dir !== '' && !is_dir($thumb_to_dir)) {
4873 @rename($thumb_from_dir, $thumb_to_dir);
4874 }
4875
4876 $rows = $db->select($this->dd_media, "active = 1 AND (media_folder = '" . str_replace("'", "''", $from) . "' OR file_path LIKE '" . str_replace("'", "''", $from_prefix) . "%')", '*', 'id');
4877 if (!is_array($rows)) $rows = array();
4878 $updated = 0;
4879 foreach ($rows as $row) {
4880 if (!is_array($row)) continue;
4881 $id = (int)($row['id'] ?? 0);
4882 if ($id <= 0) continue;
4883 $path = ltrim(str_replace('\\', '/', (string)($row['file_path'] ?? '')), '/');
4884 $new_path = $path;
4885 if (strpos($path, $from_prefix) === 0) {
4886 $new_path = $to_prefix . substr($path, strlen($from_prefix));
4887 }
4888 $update = array(
4889 'media_folder' => $to,
4890 'file_path' => $new_path,
4891 );
4892 $thumb = ltrim(str_replace('\\', '/', (string)($row['thumb_file_path'] ?? '')), '/');
4893 if ($thumb !== '' && strpos($thumb, $thumb_from) === 0) {
4894 $update['thumb_file_path'] = $thumb_to . substr($thumb, strlen($thumb_from));
4895 }
4896 $db->update($this->dd_media, $update, $id);
4897 $updated++;
4898 }
4899
4900 $this->cms_json_response(array(
4901 'ok' => 1,
4902 'success' => true,
4903 'from_folder' => $from,
4904 'to_folder' => $to,
4905 'updated' => $updated,
4906 'msg' => 'Ordner umbenannt.',
4907 ));
4908 }
4909
4910 private function media_is_used($db, int $id): bool {
4911 if ($id <= 0) return true;
4912 $used = $db->select(
4913 $this->dd_media_usage,
4914 'media_id = ' . $id . ' AND active = 1',
4915 'id',
4916 'id',
4917 'ASC',
4918 '',
4919 1,
4920 0,
4921 0
4922 );
4923 if (is_array($used) && isset($used[0])) return true;
4924
4925 foreach (dbxContentLngSync::accessibleLngs() as $lng) {
4926 $contentDd = dbxContentLng::ddContent((string)$lng);
4927 $folderDd = dbxContentLng::ddFolder((string)$lng);
4928 if ($db->count($contentDd, 'hero_image_id = ' . $id) > 0 || $db->count($folderDd, 'hero_image_id = ' . $id) > 0) {
4929 return true;
4930 }
4931 $pages = $db->select($contentDd, '', 'content', 'id', 'ASC', '', 0, 0, 0);
4932 foreach (is_array($pages) ? $pages : array() as $page) {
4933 $content = (string)($page['content'] ?? '');
4934 if (preg_match('/data-cms-media-id=["\']?' . $id . '(?:["\'\s>]|$)/i', $content)
4935 || preg_match('/(?:dbx_mid|media_id)=' . $id . '(?:[^0-9]|$)/i', $content)) {
4936 return true;
4937 }
4938 }
4939 }
4940 return false;
4941 }
4942
4943 private function move_media_record_to_folder($db, int $id, string $target_folder): array {
4944 if ($id <= 0) return array('ok' => 0, 'msg' => 'Keine Medien-ID erhalten.');
4945 $row = $db->select1($this->dd_media, $id);
4946 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1) return array('ok' => 0, 'msg' => 'Medium nicht gefunden.');
4947
4948 $media_type = $this->media_type($row);
4949 $folder = $this->clean_media_folder($target_folder, $media_type);
4950 if ($this->is_excluded_cms_media_folder($folder)) {
4951 return array('ok' => 0, 'msg' => 'Dieser Zielordner ist im CMS-Medienbrowser nicht verfuegbar.');
4952 }
4953 $old_file = $this->source_media_file($row);
4954 $name = basename((string)($row['file_name'] ?? $row['file_path'] ?? ''));
4955 if ($name === '') return array('ok' => 0, 'msg' => 'Dateiname fehlt.');
4956 $dir = $this->cms_media_dir($folder);
4957 if (!is_dir($dir)) @mkdir($dir, 0777, true);
4958 if (!is_dir($dir)) return array('ok' => 0, 'msg' => 'Zielordner konnte nicht angelegt werden.');
4959 $new_rel = $this->media_folder_rel_dir($folder, $media_type) . $name;
4960 $new_file = $this->file_from_media_rel($new_rel);
4961 if ($old_file !== '' && $new_file !== '' && $old_file !== $new_file) {
4962 if (is_file($new_file)) {
4963 $name = $this->unique_media_name($name);
4964 $new_rel = $this->media_folder_rel_dir($folder, $media_type) . $name;
4965 $new_file = $this->file_from_media_rel($new_rel);
4966 }
4967 if (!@rename($old_file, $new_file)) return array('ok' => 0, 'msg' => 'Medium konnte nicht verschoben werden.');
4968 }
4969 $old_thumb = $this->source_thumb_file($row);
4970 $mime = (string)($row['mime'] ?? '');
4971 $thumb = $this->create_media_thumbnail($new_file !== '' ? $new_file : $old_file, $folder, $name, $mime);
4972 if ($old_thumb !== '' && is_file($old_thumb) && $old_thumb !== $this->file_from_media_rel((string)($thumb['thumb_file_path'] ?? ''))) {
4973 @unlink($old_thumb);
4974 }
4975 $update = array('active' => 1, 'media_folder' => $folder, 'file_name' => $name, 'file_path' => $new_rel);
4976 if ($thumb) $update = array_merge($update, $thumb);
4977 $ok = $db->update($this->dd_media, $update, $id, 0, 1, 1, 0);
4978 if ($ok < 0) return array('ok' => 0, 'msg' => 'Mediendaten konnten nicht aktualisiert werden.');
4979 $row = $this->normalize_media_row($db->select1($this->dd_media, $id));
4980 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1 || (string)($row['media_folder'] ?? '') !== $folder) {
4981 return array('ok' => 0, 'msg' => 'Mediendaten wurden nicht konsistent gespeichert.');
4982 }
4983 return array('ok' => 1, 'success' => true, 'row' => $row, 'msg' => 'Medium verschoben.');
4984 }
4985
4986 private function media_move_json() {
4987 $payload = $this->request_json();
4988 $id = (int)($payload['media_id'] ?? $payload['id'] ?? 0);
4989 if ($id <= 0) $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Keine Medien-ID erhalten.'));
4990
4991 $db = dbx()->get_system_obj('dbxDB');
4992 $this->ensure_cms_schema($db);
4993 $result = $this->move_media_record_to_folder($db, $id, (string)($payload['media_folder'] ?? $payload['folder'] ?? ''));
4994 $this->cms_json_response($result);
4995 }
4996
4997 private function media_unused_json() {
4998 $payload = $this->request_json();
4999 $action = strtolower(trim((string)($payload['action'] ?? '')));
5000 if (!in_array($action, array('delete', 'move'), true)) {
5001 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Ungueltige Aktion.'));
5002 }
5003
5004 $source_raw = trim((string)($payload['source_folder'] ?? $payload['media_folder'] ?? 'all'));
5005 $source = strtolower($source_raw) === 'all' || $source_raw === ''
5006 ? 'all'
5007 : $this->clean_media_folder($source_raw, $this->media_type_from_folder($source_raw, 'image'));
5008 if ($source !== 'all' && $this->is_excluded_cms_media_folder($source)) {
5009 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Dieser Quellordner ist im CMS-Medienbrowser nicht verfuegbar.'));
5010 }
5011
5012 $target = '';
5013 if ($action === 'move') {
5014 $target_raw = trim((string)($payload['target_folder'] ?? $payload['to_folder'] ?? ''));
5015 if ($target_raw === '') {
5016 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Bitte einen Zielordner waehlen.'));
5017 }
5018 $target = $this->clean_media_folder($target_raw, 'image');
5019 if ($target === '' || $this->is_excluded_cms_media_folder($target)) {
5020 $this->cms_json_response(array('ok' => 0, 'success' => false, 'msg' => 'Bitte einen gueltigen Zielordner waehlen.'));
5021 }
5022 }
5023
5024 $db = dbx()->get_system_obj('dbxDB');
5025 $this->ensure_cms_schema($db);
5026 $this->sync_cms_media_files($db);
5027
5028 $where = "active = 1"
5029 . $this->cms_media_exclude_sql()
5030 . " AND (mime LIKE 'image/%' OR file_name LIKE '%.jpg' OR file_name LIKE '%.jpeg' OR file_name LIKE '%.png' OR file_name LIKE '%.gif' OR file_name LIKE '%.webp' OR file_name LIKE '%.svg')";
5031 if ($source !== 'all') {
5032 $where .= " AND media_folder = '" . str_replace("'", "''", $source) . "'";
5033 }
5034 if ($action === 'move') {
5035 $where .= " AND media_folder <> '" . str_replace("'", "''", $target) . "'";
5036 }
5037
5038 $rows = $db->select($this->dd_media, $where, '*', 'media_folder,title,id', 'ASC', '', 0, 0, 0);
5039 if (!is_array($rows)) $rows = array();
5040 $rows = $this->filter_existing_media($db, $rows);
5041
5042 $checked = 0;
5043 $affected = 0;
5044 $skipped_used = 0;
5045 $errors = array();
5046 $moved_rows = array();
5047
5048 foreach ($rows as $row) {
5049 if (!is_array($row)) continue;
5050 $id = (int)($row['id'] ?? 0);
5051 if ($id <= 0) continue;
5052 $checked++;
5053 if ($this->media_is_used($db, $id)) {
5054 $skipped_used++;
5055 continue;
5056 }
5057 if ($action === 'delete') {
5058 $result = $this->delete_media_record($id);
5059 if ((int)($result['ok'] ?? 0) === 1) {
5060 $affected++;
5061 } else {
5062 $errors[] = '#' . $id . ': ' . implode(' ', (array)($result['errors'] ?? array('Loeschen fehlgeschlagen.')));
5063 }
5064 continue;
5065 }
5066 $result = $this->move_media_record_to_folder($db, $id, $target);
5067 if ((int)($result['ok'] ?? 0) === 1) {
5068 $affected++;
5069 if (!empty($result['row']) && is_array($result['row'])) $moved_rows[] = $result['row'];
5070 } else {
5071 $errors[] = '#' . $id . ': ' . (string)($result['msg'] ?? 'Verschieben fehlgeschlagen.');
5072 }
5073 }
5074
5075 $label = $action === 'delete' ? 'geloescht' : 'verschoben';
5076 $msg = $affected . ' unbenutzte Bilder ' . $label . '.';
5077 if ($skipped_used > 0) $msg .= ' ' . $skipped_used . ' verwendete Bilder uebersprungen.';
5078 if (!empty($errors)) $msg .= ' ' . count($errors) . ' Fehler.';
5079
5080 $this->cms_json_response(array(
5081 'ok' => empty($errors) ? 1 : ($affected > 0 ? 1 : 0),
5082 'success' => empty($errors) || $affected > 0,
5083 'action' => $action,
5084 'source_folder' => $source,
5085 'target_folder' => $target,
5086 'checked' => $checked,
5087 'affected' => $affected,
5088 'skipped_used' => $skipped_used,
5089 'errors' => $errors,
5090 'rows' => $moved_rows,
5091 'msg' => $msg,
5092 ));
5093 }
5094
5095 private function edit_image_file($file, $mime, array $op) {
5096 if (!extension_loaded('gd') || !function_exists('imagecreatetruecolor')) return false;
5097 $img = $this->load_gd_image($file, $mime);
5098 if (!$img) return false;
5099
5100 $src_w = imagesx($img);
5101 $src_h = imagesy($img);
5102 if ($src_w <= 0 || $src_h <= 0) {
5103 imagedestroy($img);
5104 return false;
5105 }
5106
5107 $action = strtolower((string)($op['action'] ?? 'resize'));
5108 if ($action === 'crop') {
5109 $x = max(0, min($src_w - 1, (int)($op['x'] ?? 0)));
5110 $y = max(0, min($src_h - 1, (int)($op['y'] ?? 0)));
5111 $w = max(1, min($src_w - $x, (int)($op['width'] ?? $src_w)));
5112 $h = max(1, min($src_h - $y, (int)($op['height'] ?? $src_h)));
5113 $dst = imagecreatetruecolor($w, $h);
5114 imagefill($dst, 0, 0, imagecolorallocate($dst, 255, 255, 255));
5115 imagecopyresampled($dst, $img, 0, 0, $x, $y, $w, $h, $w, $h);
5116 } else {
5117 $w = (int)($op['width'] ?? 0);
5118 $h = (int)($op['height'] ?? 0);
5119 $ratio = !empty($op['ratio']);
5120 if ($w <= 0 && $h <= 0) {
5121 imagedestroy($img);
5122 return false;
5123 }
5124 if ($w <= 0) $w = max(1, (int)round($src_w * ($h / $src_h)));
5125 if ($h <= 0) $h = max(1, (int)round($src_h * ($w / $src_w)));
5126 if ($ratio && $w > 0 && $h > 0) {
5127 $scale = min($w / $src_w, $h / $src_h);
5128 $w = max(1, (int)round($src_w * $scale));
5129 $h = max(1, (int)round($src_h * $scale));
5130 }
5131 $dst = imagecreatetruecolor($w, $h);
5132 imagefill($dst, 0, 0, imagecolorallocate($dst, 255, 255, 255));
5133 imagecopyresampled($dst, $img, 0, 0, 0, 0, $w, $h, $src_w, $src_h);
5134 }
5135
5136 $ok = $this->save_gd_image($dst, $file, $mime);
5137 imagedestroy($dst);
5138 imagedestroy($img);
5139 return $ok;
5140 }
5141
5142 private function edit_media_json() {
5143 try {
5144 $payload = $this->request_json();
5145 $ids = array();
5146 if (isset($payload['ids']) && is_array($payload['ids'])) {
5147 foreach ($payload['ids'] as $id) {
5148 $id = (int)$id;
5149 if ($id > 0) $ids[$id] = $id;
5150 }
5151 } else {
5152 $id = (int)($payload['id'] ?? 0);
5153 if ($id > 0) $ids[$id] = $id;
5154 }
5155 if (!$ids) $this->cms_json_response(array('ok' => 0, 'msg' => 'Keine Medien ausgewaehlt.'));
5156
5157 $action = strtolower((string)($payload['action'] ?? 'resize'));
5158 if (!in_array($action, array('resize', 'crop'), true)) $action = 'resize';
5159 if ($action === 'crop' && count($ids) > 1) $this->cms_json_response(array('ok' => 0, 'msg' => 'Zuschneiden ist nur fuer ein Bild moeglich.'));
5160
5161 $db = dbx()->get_system_obj('dbxDB');
5162 $this->ensure_cms_schema($db);
5163 $done = array();
5164 foreach ($ids as $id) {
5165 $row = $db->select1($this->dd_media, $id);
5166 if (!is_array($row) || (int)($row['active'] ?? 0) !== 1 || $this->media_type($row) !== 'image') continue;
5167 if (preg_match('/\.svg$/i', (string)($row['file_name'] ?? ''))) continue;
5168 $file = $this->source_media_file($row);
5169 if ($file === '') continue;
5170 $mime = (string)($row['mime'] ?? '');
5171 if (!$this->edit_image_file($file, $mime, array(
5172 'action' => $action,
5173 'x' => (int)($payload['x'] ?? 0),
5174 'y' => (int)($payload['y'] ?? 0),
5175 'width' => (int)($payload['width'] ?? 0),
5176 'height' => (int)($payload['height'] ?? 0),
5177 'ratio' => !empty($payload['ratio']),
5178 ))) continue;
5179
5180 $old_thumb = $this->source_thumb_file($row);
5181 if ($old_thumb !== '') @unlink($old_thumb);
5182 $size = @getimagesize($file);
5183 $update = array(
5184 'size' => (int)@filesize($file),
5185 'width' => is_array($size) ? (int)$size[0] : 0,
5186 'height' => is_array($size) ? (int)$size[1] : 0,
5187 'media_type' => 'image',
5188 'thumb_file_path' => '',
5189 'thumb_width' => 0,
5190 'thumb_height' => 0,
5191 );
5192 $media_folder = $this->canonical_media_folder(trim((string)($row['media_folder'] ?? '')) ?: $this->media_folder_from_path((string)($row['file_path'] ?? ''), $this->media_type($row)));
5193 $thumb = $this->create_media_thumbnail($file, $media_folder, (string)($row['file_name'] ?? basename($file)), $mime);
5194 if ($thumb) $update = array_merge($update, $thumb);
5195 $db->update($this->dd_media, $update, $id);
5196 $done[] = $this->normalize_media_row($db->select1($this->dd_media, $id));
5197 }
5198
5199 if (!$done) $this->cms_json_response(array('ok' => 0, 'msg' => 'Keine bearbeitbaren Bilder gefunden.'));
5200 foreach ($ids as $id) {
5201 $this->flush_media_by_media_id($db, (int)$id);
5202 }
5203 $this->cms_json_response(array('ok' => 1, 'success' => true, 'rows' => $done));
5204 } catch (\Throwable $e) {
5205 $this->cms_json_response(array(
5206 'ok' => 0,
5207 'success' => false,
5208 'msg' => 'Medienbearbeitung fehlgeschlagen: ' . $e->getMessage()
5209 ));
5210 }
5211 }
5212
5213 private function template_options() {
5214 $dir = dbx()->get_system_var('dbx_dir', '') . '/modules/dbxContent/tpl/htm/';
5215 if (!is_dir($dir)) $dir = dirname(__DIR__, 2) . '/dbxContent/tpl/htm/';
5216 $files = glob($dir . 'c-*.htm');
5217 $html = '';
5218 if (is_array($files)) {
5219 sort($files);
5220 foreach ($files as $file) {
5221 $name = basename($file, '.htm');
5222 $html .= '<option value="' . dbx()->esc($name) . '">' . dbx()->esc($name) . '</option>';
5223 }
5224 }
5225 return $html ?: '<option value="c-content">c-content</option>';
5226 }
5227
5228 private function rights_options() {
5229 $values = $this->rights_values();
5230 $html = '';
5231 foreach ($values as $value => $label) {
5232 $html .= '<option value="' . dbx()->esc($value) . '">' . dbx()->esc($label) . '</option>';
5233 }
5234 return $html;
5235 }
5236
5237 private function rights_values() {
5238 $values = array(
5239 'parent' => 'parent - Rechte vom Parent',
5240 '*' => '*Alle*',
5241 );
5242 $db = dbx()->get_system_obj('dbxDB');
5243 $rows = $db->select('dbxUser_groups', '', '*', 'name');
5244 if (is_array($rows)) {
5245 foreach ($rows as $row) {
5246 if (!is_array($row)) continue;
5247 $name = trim((string)($row['name'] ?? ''));
5248 if ($name !== '') $values[$name] = $name;
5249 }
5250 }
5251 return $values;
5252 }
5253
5254 private function media_template_options() {
5255 $values = array(
5256 'image-hero' => 'Bild Hero',
5257 'image-gallery' => 'Bild Galerie',
5258 'image-teaser' => 'Bild Header',
5259 'video-hero' => 'Video Hero',
5260 'video-gallery' => 'Video Galerie',
5261 'file-gallery' => 'Datei Download',
5262 );
5263 $html = '';
5264 foreach ($values as $value => $label) {
5265 $html .= '<option value="' . dbx()->esc($value) . '">' . dbx()->esc($label) . '</option>';
5266 }
5267 return $html;
5268 }
5269
5270 private function options_html(array $values, $selected = '') {
5271 $html = '';
5272 foreach ($values as $value => $label) {
5273 $sel = ((string)$value === (string)$selected) ? ' selected' : '';
5274 $html .= '<option value="' . dbx()->esc($value) . '"' . $sel . '>' . dbx()->esc($label) . '</option>';
5275 }
5276 return $html;
5277 }
5278
5279 private function content_template_options($with_parent = true) {
5280 $html = $with_parent ? '<option value="parent">parent - vom Parent</option>' : '';
5281 $html .= $this->template_options();
5282 return $html;
5283 }
5284
5285 private function hero_template_values() {
5286 return array(
5287 'parent' => 'parent - vom Parent',
5288 'none' => 'Ohne Hero',
5289 'image-hero' => 'Bild Hero',
5290 'video-hero' => 'Video Hero',
5291 );
5292 }
5293
5294 private function hero_template_options() {
5295 return $this->options_html($this->hero_template_values());
5296 }
5297
5298 private function hero_variant_values() {
5299 return array(
5300 'parent' => 'parent - vom Parent',
5301 'original' => 'Original',
5302 'yellow' => 'gelblich',
5303 'green' => 'gruenlich',
5304 'blue' => 'blaeulich',
5305 'red' => 'roetlich',
5306 'light' => 'hell',
5307 'dark' => 'dunkel',
5308 'blackwhite' => 'schwarz/weiss',
5309 'monochrome' => 'monochrom',
5310 );
5311 }
5312
5313 private function hero_variant_options() {
5314 return $this->options_html($this->hero_variant_values());
5315 }
5316
5317 private function hero_sticky_values() {
5318 return array(
5319 'parent' => 'parent - vom Parent',
5320 '0' => 'Nicht sticky',
5321 '1' => 'Sticky',
5322 );
5323 }
5324
5325 private function hero_scroll_layer_values() {
5326 return array(
5327 'parent' => 'parent - vom Parent',
5328 'under' => 'Scroll laeuft drunter',
5329 'over' => 'Scroll laeuft drueber',
5330 );
5331 }
5332
5333 private function gallery_template_values() {
5334 return array(
5335 'image-gallery' => 'Bildergalerie',
5336 'video-gallery' => 'Video Galerie',
5337 'carousel3' => 'Carousel 3',
5338 'cols3' => '3 Spalten',
5339 );
5340 }
5341
5342 private function gallery_template_options() {
5343 return $this->options_html($this->gallery_template_values());
5344 }
5345
5346 private function gallery_image_size_values() {
5347 return array(
5348 '800x600' => '4:3 - Standard (800 x 600)',
5349 '1200x900' => '4:3 - gross (1200 x 900)',
5350 '1024x768' => '4:3 - klassisch (1024 x 768)',
5351 '1600x1200' => '4:3 - hochaufloesend (1600 x 1200)',
5352 '1280x720' => '16:9 - breit (1280 x 720)',
5353 '1920x1080' => '16:9 - Full HD (1920 x 1080)',
5354 '1200x675' => '16:9 - Web (1200 x 675)',
5355 '1080x1080' => '1:1 - Quadrat (1080 x 1080)',
5356 '1200x1200' => '1:1 - Quadrat gross (1200 x 1200)',
5357 '1080x1350' => '4:5 - Portrait (1080 x 1350)',
5358 '900x1200' => '3:4 - Portrait (900 x 1200)',
5359 '1600x900' => '16:9 - gross (1600 x 900)',
5360 '2560x1440' => '16:9 - QHD (2560 x 1440)',
5361 );
5362 }
5363
5364 private function gallery_lightbox_width_values() {
5365 return array(
5366 '60%' => '60% - kompakt',
5367 '70%' => '70% - mittel',
5368 '80%' => '80% - Standard',
5369 '90%' => '90% - breit',
5370 '95%' => '95% - sehr breit',
5371 '70vw' => '70vw - Viewport mittel',
5372 '80vw' => '80vw - Viewport Standard',
5373 '90vw' => '90vw - Viewport breit',
5374 '960px' => '960px - kleine Desktopbreite',
5375 '1200px' => '1200px - Desktop',
5376 '1440px' => '1440px - grosser Desktop',
5377 );
5378 }
5379
5380 private function gallery_overflow_values() {
5381 return array(
5382 'grid' => 'Grid',
5383 'scroll' => 'scroll',
5384 'laufband' => 'laufband',
5385 'slider' => 'slider',
5386 'tutorial' => 'Tutorial Slideshow',
5387 );
5388 }
5389
5390 private function gallery_overflow_options() {
5391 return $this->options_html($this->gallery_overflow_values());
5392 }
5393
5394 private function gallery_click_values() {
5395 return array(
5396 'lightbox' => 'Lightbox',
5397 'swiper-coverflow' => 'Swiper Coverflow',
5398 'swiper-cube' => 'Swiper Cube',
5399 'swiper-cards' => 'Swiper Cards',
5400 'swiper-3d' => 'Swiper 3D-Slider',
5401 'viewerjs' => 'Viewer.js',
5402 'blueimp' => 'blueimp Gallery',
5403 'photoswipe' => 'PhotoSwipe',
5404 'deepzoom' => 'Deep-Zoom-Viewer',
5405 'link' => 'Link',
5406 'newtab' => 'Neues Fenster',
5407 'none' => 'Kein Klick',
5408 );
5409 }
5410
5411 private function gallery_click_options() {
5412 return $this->options_html($this->gallery_click_values());
5413 }
5414
5415 private function meta_robots_values() {
5416 return array(
5417 'index,follow' => 'index, follow (Standard)',
5418 'noindex,follow' => 'noindex, follow',
5419 'index,nofollow' => 'index, nofollow',
5420 'noindex,nofollow' => 'noindex, nofollow',
5421 );
5422 }
5423
5424 private function render_page_form() {
5425 $form = dbx()->get_system_obj('dbxForm');
5426 $form->init('cms-page', 'cms-admin-page-form');
5427 $form->_dd = dbxContentLng::ddContent();
5428 $form->_data = array(
5429 'activ' => 1,
5430 'template' => 'parent',
5431 'hero_template' => 'parent',
5432 'hero_image_id' => 'parent',
5433 'hero_margin_top' => 'parent',
5434 'hero_height' => 'parent',
5435 'hero_variant' => 'parent',
5436 'hero_sticky' => 'parent',
5437 'hero_scroll_layer' => 'parent',
5438 'gallery_template' => 'image-gallery',
5439 'gallery_visible_count' => '3',
5440 'gallery_image_size' => 'original',
5441 'gallery_lightbox_width' => '100vw',
5442 'gallery_overflow' => 'grid',
5443 'gallery_click_behavior' => 'lightbox',
5444 'meta_robots' => 'index,follow',
5445 'seo_title' => '',
5446 'seo_image_id' => 0,
5447 );
5448 $form->add_fld('id', 'dbxContent_admin|cms-field-hidden', data: array('cms_field' => 'id'));
5449 $form->add_fld('folder', 'dbxContent_admin|cms-field-hidden', data: array('cms_field' => 'folder'));
5450 $form->add_fld('title', 'dbxContent_admin|cms-field-text', label: 'Titel', rules: 'varchar|min=1', data: array('cms_field' => 'title'));
5451 $form->add_fld('permalink', 'dbxContent_admin|cms-field-text', label: 'Permalink', rules: 'parameter+/', data: array('cms_field' => 'permalink'));
5452 $form->add_fld('activ', 'dbxContent_admin|cms-field-select', label: 'Status', rules: 'int', data: array('cms_field' => 'activ'), options: array('1' => 'Aktiv', '0' => 'Inaktiv'));
5453 $form->add_fld('template', 'dbxContent_admin|cms-field-select', label: 'Content-Template', rules: 'varchar', data: array('cms_field' => 'template'), options: $this->content_template_values());
5454 $form->add_fld('description', 'dbxContent_admin|cms-field-textarea', label: 'Beschreibung', rules: 'varchar', data: array('cms_field' => 'description'), placeholder: 'Kurzbeschreibung der Seite, max. 254 Zeichen.');
5455 $form->add_fld('content', 'dbxContent_admin|cms-field-textarea-hidden', label: 'Inhalt', rules: 'text', data: array('cms_field' => 'content'));
5456 return $form->run();
5457 }
5458
5459 private function content_template_values() {
5460 $values = array('parent' => 'parent - vom Parent');
5461 $dir = dbx()->get_system_var('dbx_dir', '') . '/modules/dbxContent/tpl/htm/';
5462 if (!is_dir($dir)) $dir = dirname(__DIR__, 2) . '/dbxContent/tpl/htm/';
5463 $files = glob($dir . 'c-*.htm');
5464 if (is_array($files)) {
5465 sort($files);
5466 foreach ($files as $file) {
5467 $name = basename($file, '.htm');
5468 $values[$name] = $name;
5469 }
5470 }
5471 if (count($values) === 1) $values['c-content'] = 'c-content';
5472 return $values;
5473 }
5474
5475 private function render_folder_form() {
5476 $form = dbx()->get_system_obj('dbxForm');
5477 $form->init('cms-folder', 'cms-admin-folder-form');
5478 $form->_dd = dbxContentLng::ddFolder();
5479 $form->_data = array(
5480 'parent_id' => 0,
5481 'group_read' => 'parent',
5482 'template' => 'parent',
5483 'hero_template' => 'parent',
5484 'hero_image_id' => 'parent',
5485 'hero_margin_top' => 'parent',
5486 'hero_height' => 'parent',
5487 'hero_variant' => 'parent',
5488 'hero_sticky' => 'parent',
5489 'hero_scroll_layer' => 'parent',
5490 );
5491 $form->add_fld('id', 'dbxContent_admin|cms-field-folder-hidden', data: array('cms_field' => 'id'));
5492 $form->add_fld('name', 'dbxContent_admin|cms-field-folder-text', label: 'Name', rules: 'varchar|min=1', data: array('cms_field' => 'name'));
5493 $form->add_fld('parent_id', 'dbxContent_admin|cms-field-folder-select', label: 'Zuordnung', rules: 'int', data: array('cms_field' => 'parent_id'), options: array('0' => 'Root / erste Ebene'));
5494 $form->add_fld('template', 'dbxContent_admin|cms-field-folder-select', label: 'Content-Template', rules: 'varchar', data: array('cms_field' => 'template'), options: $this->content_template_values());
5495 $form->add_fld('group_read', 'dbxContent_admin|cms-field-folder-rights', label: 'Leserechte', rules: 'varchar', data: array('cms_field' => 'group_read'), options: $this->rights_values());
5496 return $form->run();
5497 }
5498
5499 private function render_settings_form() {
5500 $form = dbx()->get_system_obj('dbxForm');
5501 $form->init('cms-settings', 'cms-admin-settings-panels');
5502 $form->_dd = dbxContentLng::ddContent();
5503 $form->_data = array(
5504 'hero_template' => 'parent',
5505 'hero_image_id' => 'parent',
5506 'hero_margin_top' => 'parent',
5507 'hero_height' => 'parent',
5508 'hero_variant' => 'parent',
5509 'hero_sticky' => 'parent',
5510 'hero_scroll_layer' => 'parent',
5511 'gallery_template' => 'image-gallery',
5512 'gallery_visible_count' => '3',
5513 'gallery_image_size' => 'original',
5514 'gallery_lightbox_width' => '100vw',
5515 'gallery_overflow' => 'grid',
5516 'gallery_click_behavior' => 'lightbox',
5517 );
5518 $form->add_fld('hero_template', 'dbxContent_admin|cms-field-select', label: 'Template', rules: 'varchar', data: array('cms_field' => 'hero_template'), options: $this->hero_template_values());
5519 $form->add_fld('hero_image_id', 'dbxContent_admin|cms-field-hidden', rules: 'varchar', data: array('cms_field' => 'hero_image_id'));
5520 $form->add_fld('hero_margin_top', 'dbxContent_admin|cms-field-text', label: 'Abstand oben', rules: 'varchar', data: array('cms_field' => 'hero_margin_top'));
5521 $form->add_fld('hero_height', 'dbxContent_admin|cms-field-text', label: 'Hoehe', rules: 'varchar', data: array('cms_field' => 'hero_height'));
5522 $form->add_fld('hero_variant', 'dbxContent_admin|cms-field-select', label: 'Variante', rules: 'varchar', data: array('cms_field' => 'hero_variant'), options: $this->hero_variant_values());
5523 $form->add_fld('hero_sticky', 'dbxContent_admin|cms-field-select', label: 'Hero sticky', rules: 'varchar', data: array('cms_field' => 'hero_sticky'), options: $this->hero_sticky_values());
5524 $form->add_fld('hero_scroll_layer', 'dbxContent_admin|cms-field-select', label: 'Scroll-Ebene', rules: 'varchar', data: array('cms_field' => 'hero_scroll_layer'), options: $this->hero_scroll_layer_values());
5525 $form->add_fld('gallery_image_size', 'dbxContent_admin|cms-field-select', label: 'Bildgroesse', rules: 'varchar', data: array('cms_field' => 'gallery_image_size'), options: $this->gallery_image_size_values());
5526 $form->add_fld('gallery_lightbox_width', 'dbxContent_admin|cms-field-select', label: 'Bildbreite Lightbox', rules: 'varchar', data: array('cms_field' => 'gallery_lightbox_width'), options: $this->gallery_lightbox_width_values());
5527 $form->add_fld('gallery_overflow', 'dbxContent_admin|cms-field-select', label: 'Overflow', rules: 'varchar', data: array('cms_field' => 'gallery_overflow'), options: $this->gallery_overflow_values());
5528 $form->add_fld('gallery_click_behavior', 'dbxContent_admin|cms-field-select', label: 'Klick', rules: 'varchar', data: array('cms_field' => 'gallery_click_behavior'), options: $this->gallery_click_values());
5529 return $form->run();
5530 }
5531
5532 public function run($action = 'cms') {
5533 $this->apply_cms_lng_context();
5534 switch ($action) {
5535 case 'cms_tree':
5536 return $this->tree_json();
5537 case 'cms_lng_coverage':
5538 return $this->lng_coverage_json();
5539 case 'cms_lng_preview':
5540 return $this->lng_preview_json();
5541 case 'cms_lng_provision':
5542 return $this->lng_provision_json();
5543 case 'cms_lng_provision_tree':
5544 return $this->lng_provision_tree_json();
5545 case 'cms_lng_reset_sync':
5546 return $this->lng_reset_sync_json();
5547 case 'cms_lng_delete_preview':
5548 return $this->lng_delete_preview_json();
5549 case 'cms_page':
5550 return $this->page_json();
5551 case 'cms_save':
5552 return $this->save_json();
5553 case 'cms_new_page':
5554 return $this->new_page_json();
5555 case 'cms_duplicate_page':
5556 return $this->duplicate_page_json();
5557 case 'cms_new_folder':
5558 return $this->new_folder_json();
5559 case 'cms_save_folder':
5560 return $this->save_folder_json();
5561 case 'cms_delete_folder':
5562 return $this->delete_folder_json();
5563 case 'cms_delete_page':
5564 return $this->delete_page_json();
5565 case 'cms_move_node':
5566 return $this->move_node_json();
5567 case 'cms_media':
5568 return $this->media_json();
5569 case 'cms_media_process':
5570 return $this->media_process();
5571 case 'cms_upload':
5572 return $this->upload_json();
5573 case 'cms_external_video':
5574 return $this->external_video_json();
5575 case 'cms_media_folders':
5576 return $this->media_folders_json();
5577 case 'cms_media_folder_create':
5578 return $this->media_folder_create_json();
5579 case 'cms_media_folder_delete':
5580 return $this->media_folder_delete_json();
5581 case 'cms_media_folder_rename':
5582 return $this->media_folder_rename_json();
5583 case 'cms_media_move':
5584 return $this->media_move_json();
5585 case 'cms_media_unused':
5586 return $this->media_unused_json();
5587 case 'cms_remove_media':
5588 return $this->remove_media_json();
5589 case 'cms_delete_media':
5590 return $this->delete_media_json();
5591 case 'cms_edit_media':
5592 return $this->edit_media_json();
5593 case 'cms_set_media_slot':
5594 return $this->set_media_slot_json();
5595 case 'cms_assign_media':
5596 return $this->assign_media_json();
5597 case 'cms_sort_media':
5598 return $this->sort_media_json();
5599 case 'cms_mod_catalog':
5600 return $this->mod_catalog_json();
5601 case 'cms_mod_modules':
5602 return $this->mod_modules_json();
5603 case 'cms':
5604 default:
5605 return $this->render_cms();
5606 }
5607 }
5608}
5609
5610?>
$table['server']
Definition .dd.php:6
static ddContent(string $lng='')
static resolveDeleteIds($db, string $entity, int $id, array $deleteLngs)
Gemeinsame Basisklasse fuer dbXapp-System-, Modul- und Include-Objekte.
Definition dbxKernel.php:63
$field
Definition config.dd.php:4
mod_class_files($modulesDir, $modul)
mod_scan_runs($modulesDir, $modul)
dbx_os_path_file($path_file)
Definition index.php:164
dbx_get_file_dir()
Definition index.php:265
if( $syncRequest)
Definition index.php:520
dbx_get_base_dir($cut_Data=0)
Definition index.php:157
DBX schema administration.
if(! $db->connect_db_server($server)) $result
$_SERVER['REQUEST_URI']