19 private const ACTION_TOKEN_SCOPE =
'dbxContent_admin.actions';
21 private $dd_media =
'dbxMedia';
22 private $dd_media_usage =
'dbxMediaUsage';
23 private $cms_form_security = array();
24 private $cmsTexts =
null;
25 private $contentTemplateNames =
null;
34 private function cms_texts() {
35 if ($this->cmsTexts)
return $this->cmsTexts;
36 dbx()->get_system_obj(
'dbxForm',
'use');
38 $texts->init(
'cms-page-texts');
39 $texts->_fd =
'dbxContent_admin|cms-page';
40 $texts->load_fd_messages();
41 $texts->set_form_help_enabled(
false);
43 return $this->cmsTexts;
46 private function cms_js_messages():
string {
47 $texts = $this->cms_texts();
49 'page_select_first',
'page_loading',
'page_load_error',
50 'page_saved',
'page_save_error',
'page_deleted',
'page_delete_error',
51 'page_duplicating',
'page_duplicated',
'page_duplicate_error',
52 'folder_selected',
'folder_edit',
'folder_saved',
'folder_save_error',
53 'folder_delete_error',
'folder_select_first',
'folder_name_required',
54 'unsaved',
'saved',
'unsaved_title',
'saved_title',
55 'page_save_title',
'folder_save_title',
'page_delete_title',
56 'folder_delete_title',
'duplicate_title',
'duplicate_select_title',
57 'selection_label',
'tree_show',
'tree_hide',
'right_show',
'right_hide',
58 'editor_marker_tooltip',
'editor_media_tooltip',
'editor_module_tooltip',
59 'editor_bootstrap_tooltip',
'editor_text_format',
'editor_horizontal_rule',
60 'editor_columns_two',
'editor_columns_three',
61 'editor_columns_first',
'editor_columns_second',
'editor_columns_third',
62 'editor_columns_new',
'editor_columns_stacked',
'editor_columns_responsive',
63 'editor_column_add',
'editor_columns_dissolve',
64 'editor_context_menu',
'editor_context_undo',
'editor_context_redo',
65 'editor_context_select_all',
'editor_context_block_up',
'editor_context_block_down',
66 'editor_context_module',
'editor_context_video',
'editor_image_edit',
'editor_image_remove',
67 'editor_context_copy',
68 'editor_context_cut',
'editor_context_paste',
'editor_context_delete',
69 'editor_save_all',
'editor_bold',
'editor_italic',
'editor_underline',
70 'editor_strike',
'editor_align_left',
'editor_align_center',
71 'editor_align_right',
'editor_align_justify',
'editor_print_break',
73 'media_inline_empty',
'media_inline_focus',
'media_inline_edit',
74 'media_inline_remove',
'media_inline_removed',
'media_gallery_empty',
'media_hero_empty',
75 'media_area_empty',
'media_all_folders',
'media_folders_title',
76 'media_no_folders',
'media_folder_empty',
'media_back',
77 'media_folder_instruction',
'media_folder_label',
'media_count',
78 'media_view',
'media_view_small',
'media_view_medium',
'media_view_large',
82 return json_encode(
$messages, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
85 private function cms_json_response(array $data):
void {
86 if ($this->cms_form_security) {
87 $data[
'form_security'] = $this->cms_form_security;
89 dbx()->json_response($data,
true);
96 return dbx()->get_include_obj(
'dbxContentMediaForms',
'dbxContent_admin');
106 private function verify_media_form(
string $kind):
bool {
107 $action = $this->base_url($kind ===
'external' ?
'cms_external_video' :
'cms_upload');
108 $state = $this->media_forms()->verify($kind, $action);
109 $this->cms_form_security = is_array(
$state[
'security'] ??
null) ?
$state[
'security'] : array();
110 return !empty(
$state[
'submitted']);
113 private function lng_provision_open_flag(
$db,
string $entity,
int $id):
int {
114 if (!dbxContentLngSync::isMasterLng() || !is_object(
$db)) {
118 if (count(dbxContentLngSync::slaveLngs()) <= 0) {
126 return dbxContentLngSync::hasMissingSlaveLng(
$db, $entity, $id) ? 1 : 0;
129 private function apply_cms_lng_context():
void {
130 $lng = strtolower(trim((
string)
dbx()->get_request_var(
'dbx_lng',
'')));
135 $allowed = dbxContentLngSync::accessibleLngs();
140 dbx()->set_system_var(
'dbx_lng',
$lng);
141 dbx()->set_remember_var(
'dbx_lng',
$lng,
'dbx');
144 private function ini_size_bytes(
$value) {
146 if (
$value ===
'')
return 0;
147 $unit = strtolower(substr(
$value, -1));
149 if ($unit ===
'g') $num *= 1024 * 1024 * 1024;
150 elseif ($unit ===
'm') $num *= 1024 * 1024;
151 elseif ($unit ===
'k') $num *= 1024;
155 private function upload_max_bytes() {
156 $upload = $this->ini_size_bytes(ini_get(
'upload_max_filesize'));
157 $post = $this->ini_size_bytes(ini_get(
'post_max_size'));
158 if ($upload <= 0)
return $post;
159 if ($post <= 0)
return $upload;
160 return min($upload, $post);
163 private function base_url($action, $params = array()) {
164 $url = $this->app_url() .
'?dbx_modul=dbxContent_admin&dbx_run1=' . rawurlencode((
string)$action);
165 foreach ($params as $key =>
$value) {
166 $url .=
'&' . rawurlencode((
string)$key) .
'=' . rawurlencode((
string)
$value);
168 if ($this->tokenized_action((
string)$action)) {
181 private function tokenized_action(
string $action):
bool {
182 return in_array($action, array(
184 'cms_lng_provision_tree',
185 'cms_lng_reset_sync',
188 'cms_duplicate_page',
197 'cms_external_video',
198 'cms_media_folder_create',
199 'cms_media_folder_delete',
200 'cms_media_folder_rename',
205 'cms_set_media_slot',
211 private function action_requires_token(
string $action):
bool {
212 if ($action ===
'cms_media') {
213 return (
int)
dbx()->get_modul_var(
'sync', 0,
'int') === 1;
215 return $this->tokenized_action($action);
218 private function check_action_token(
string $action):
bool {
219 $token = (string)
dbx()->get_modul_var(
'dbx_token',
'',
'varchar|max=128');
220 if (
dbx()->check_action_token(self::ACTION_TOKEN_SCOPE,
$token)) {
228 'CMS-Aktion ohne gueltigen Token abgewiesen',
229 'ip=' . (
string)(
$_SERVER[
'REMOTE_ADDR'] ??
'')
234 private function reject_action_token(
string $action) {
235 http_response_code(403);
236 $message =
'Die Aktion wurde aus Sicherheitsgruenden abgewiesen. Bitte laden Sie das CMS neu.';
237 if ($action ===
'cms_media_process') {
238 return '<div class="alert alert-danger m-3">' .
dbx()->esc($message) .
'</div>';
240 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' => $message));
244 private function app_url():
string {
245 $script = str_replace(
'\\',
'/', (
string)(
$_SERVER[
'SCRIPT_NAME'] ??
''));
246 if ($script ===
'') {
250 $dir = str_replace(
'\\',
'/', dirname($script));
255 return rtrim(
$dir,
'/') .
'/';
258 private function load_page_cache_classes():
void {
259 dbx()->load_content_cache_classes();
262 private function sync_permalink_index(
$db,
int $cid,
string $lng =
''):
void {
264 if ($cid <= 0 || !is_object(
$db)) {
268 $this->load_page_cache_classes();
272 if (
$lng !==
'' &&
$lng !== dbxContentLng::current()) {
273 $prevLng =
dbx()->get_system_var(
'dbx_lng',
'de');
274 dbx()->set_system_var(
'dbx_lng',
$lng);
277 $rec =
$db->select1(dbxContentLng::ddContent(), $cid,
'permalink,activ,folder,title,seo_title,description,keywords,meta_robots,seo_image_id,update_date,lng_uid', 0);
278 if (!is_array($rec)) {
279 if ($prevLng !==
null) {
280 dbx()->set_system_var(
'dbx_lng', $prevLng);
285 if ((
int)($rec[
'activ'] ?? 0) !== 1) {
286 dbxContentPermalinkIndex::removeByCid($cid, dbxContentLng::current());
287 if (class_exists(
'dbx\\dbxContent\\dbxContentSitemap')) {
290 if ($prevLng !==
null) {
291 dbx()->set_system_var(
'dbx_lng', $prevLng);
297 $rights =
$renderer->getPublicFolderRights((
int)($rec[
'folder'] ?? 0));
298 $permalink = (string)($rec[
'permalink'] ??
'');
300 dbxContentPageCache::writePageMeta($cid, array(
302 'permalink' => $permalink,
304 'activ' => (
int)($rec[
'activ'] ?? 1),
305 'saved_at' => date(
'c'),
306 'seo' => dbxContentRenderer::seoMetaFromRecord($rec),
309 if (trim($permalink) !==
'') {
310 dbxContentPermalinkIndex::upsertPage(
314 (
int)($rec[
'activ'] ?? 1),
315 dbxContentLng::current()
319 dbxContentHome::refreshHomeCache(
$db, $cid, dbxContentLng::current());
321 if (class_exists(
'dbx\\dbxContent\\dbxContentSitemap')) {
325 if ($prevLng !==
null) {
326 dbx()->set_system_var(
'dbx_lng', $prevLng);
330 private function flush_lng_sync_cache(
$db, array $syncResult):
void {
331 if (!is_object(
$db) || !is_array($syncResult)) {
335 $updated = is_array($syncResult[
'updated'] ??
null) ? $syncResult[
'updated'] : array();
340 $this->load_page_cache_classes();
342 $menuFlushed =
false;
344 if (!is_array($item)) {
348 $id = (int) ($item[
'id'] ?? 0);
349 $lng = trim((
string) ($item[
'lng'] ??
''));
350 $entity = (($item[
'entity'] ??
'page') ===
'folder') ?
'folder' :
'page';
355 if ($entity ===
'page') {
356 dbxContentPageCache::invalidateContent($id);
358 $this->sync_permalink_index(
$db, $id,
$lng);
360 } elseif (!$menuFlushed) {
361 dbxContentPageCache::invalidateAllMenus();
367 dbxContentPageCache::invalidateAllMenus();
371 private function flush_saved_page_cache(
$db,
int $cid):
void {
377 $this->load_page_cache_classes();
378 dbxContentPageCache::invalidateContent($cid);
379 dbxContentPageCache::invalidateAllMenus();
380 $this->sync_permalink_index(
$db, $cid);
383 private function flush_deleted_page_cache(
int $cid,
string $lng =
''):
void {
389 $this->load_page_cache_classes();
393 $lng = dbxContentLng::current();
396 dbxContentPageCache::invalidateContent($cid);
397 dbxContentPageCache::invalidateAllMenus();
398 dbxContentPermalinkIndex::removeByCid($cid,
$lng);
401 private function copy_page_media_usage(
406 bool $replace =
true,
407 string $sourceLng =
'',
408 string $targetLng =
''
410 $sourceCid = (int) $sourceCid;
411 $targetCid = (int) $targetCid;
412 $targetFolderId = (int) $targetFolderId;
413 $sourceLng = dbxContentMediaUsageScope::language($sourceLng);
414 $targetLng = dbxContentMediaUsageScope::language($targetLng);
415 if ($sourceCid <= 0 || $targetCid <= 0 || !is_object(
$db)) {
419 $copySlots = array(
'hero',
'gallery',
'header',
'teaser',
'footer');
421 foreach ($copySlots as $slot) {
423 $this->dd_media_usage,
424 array(
'active' => 0),
425 dbxContentMediaUsageScope::withLanguage(
426 "content_id = " . $targetCid .
" AND slot = '" . str_replace(
"'",
"''", $slot) .
"' AND active = 1",
438 $this->dd_media_usage,
439 dbxContentMediaUsageScope::withLanguage(
440 "content_id = " . $sourceCid .
" AND active = 1 AND slot IN ('hero','gallery','header','teaser','footer')",
451 if (!is_array(
$rows)) {
456 foreach (
$rows as $usage) {
457 if (!is_array($usage)) {
460 $mediaId = (int) ($usage[
'media_id'] ?? 0);
465 if (!is_array(
$media) || (
int) (
$media[
'active'] ?? 0) !== 1) {
469 $slot = $this->valid_media_slot($usage[
'slot'] ??
'gallery');
470 if ($slot ===
'inline') {
474 $usageId = $this->create_media_usage(
480 (
string) ($usage[
'template'] ??
''),
481 (
string) ($usage[
'caption'] ??
''),
482 (
string) ($usage[
'settings'] ??
''),
493 private function sync_lng_page_media_from_master(
$db,
int $masterCid,
int $slaveCid,
string $lng):
int {
494 $masterCid = (int) $masterCid;
495 $slaveCid = (int) $slaveCid;
497 if ($masterCid <= 0 || $slaveCid <= 0 ||
$lng ===
'' || !is_object(
$db)) {
501 $slaveRow =
$db->select1(dbxContentLng::ddContent(
$lng), $slaveCid,
'content,folder', 0);
502 if (!is_array($slaveRow)) {
506 $slaveFolder = (int) ($slaveRow[
'folder'] ?? 0);
507 $copied = $this->copy_page_media_usage(
513 dbxContentLngSync::masterLng(),
516 $this->sync_inline_media_usage(
$db, $slaveCid, (
string) ($slaveRow[
'content'] ??
''), $slaveFolder,
null,
false,
$lng);
520 private function apply_lng_sync_media(
$db,
int $masterCid, array $syncResult):
int {
521 $master = dbxContentLngSync::masterLng();
523 foreach (($syncResult[
'updated'] ?? array()) as $item) {
524 if (!is_array($item) || (($item[
'entity'] ??
'page') !==
'page')) {
527 $lng = strtolower(trim((
string) ($item[
'lng'] ??
'')));
528 $slaveId = (int) ($item[
'id'] ?? 0);
529 if ($slaveId <= 0 ||
$lng ===
'' ||
$lng === $master) {
532 $copied += $this->sync_lng_page_media_from_master(
$db, $masterCid, $slaveId,
$lng);
537 private function apply_lng_provision_media(
$db,
string $type,
int $masterId, array
$result):
int {
538 if ($type !==
'page' || !is_object(
$db)) {
544 is_array(
$result[
'created'] ??
null) ?
$result[
'created'] : array(),
545 is_array(
$result[
'updated'] ??
null) ?
$result[
'updated'] : array()
547 foreach (
$items as $item) {
548 if (!is_array($item)) {
551 $lng = strtolower(trim((
string) ($item[
'lng'] ??
'')));
552 $slaveId = (int) ($item[
'id'] ?? 0);
553 if ($slaveId <= 0 ||
$lng ===
'') {
556 $copied += $this->sync_lng_page_media_from_master(
$db, $masterId, $slaveId,
$lng);
561 private function flush_deleted_folder_cache(
$db,
int $folderId,
string $lng =
''):
void {
562 $folderId = (int) $folderId;
563 if ($folderId < 0 || !is_object(
$db)) {
567 $this->load_page_cache_classes();
571 if (
$lng !==
'' &&
$lng !== dbxContentLng::current()) {
572 $prevLng =
dbx()->get_system_var(
'dbx_lng',
'de');
573 dbx()->set_system_var(
'dbx_lng',
$lng);
576 dbxContentPageCache::invalidateFolderTree(
$db, $folderId);
577 dbxContentPageCache::invalidateAllMenus();
579 if ($prevLng !==
null) {
580 dbx()->set_system_var(
'dbx_lng', $prevLng);
584 private function flush_folder_cache(
$db,
int $folderId):
void {
585 $folderId = (int) $folderId;
586 if ($folderId < 0 || !is_object(
$db)) {
590 $this->load_page_cache_classes();
591 dbxContentPageCache::invalidateFolderTree(
$db, $folderId);
592 dbxContentPageCache::invalidateAllMenus();
595 private function flush_menu_cache():
void {
596 $this->load_page_cache_classes();
597 dbxContentPageCache::invalidateAllMenus();
600 private function flush_media_cache(
$db,
int $content_id = 0,
int $folder_id = 0):
void {
601 if ($content_id <= 0 && $folder_id <= 0) {
605 $this->load_page_cache_classes();
606 if ($content_id > 0) {
607 dbxContentPageCache::invalidateContent($content_id);
609 if ($folder_id > 0 && is_object(
$db)) {
610 dbxContentPageCache::invalidateFolderTree(
$db, $folder_id);
612 dbxContentPageCache::invalidateAllMenus();
615 private function flush_media_by_media_id(
$db,
int $media_id):
void {
616 $media_id = (int) $media_id;
617 if ($media_id <= 0 || !is_object(
$db)) {
621 $usage_rows =
$db->select($this->dd_media_usage,
'media_id = ' . $media_id .
' AND active = 1',
'*',
'id',
'ASC',
'', 0, 0, 0);
622 if (!is_array($usage_rows)) {
626 foreach ($usage_rows as $usage) {
627 if (!is_array($usage)) {
630 $this->flush_media_cache(
632 (
int)($usage[
'content_id'] ?? 0),
633 (
int)($usage[
'folder_id'] ?? 0)
638 private function request_json() {
641 $raw = file_get_contents(
'php://input');
642 if (is_string($raw) && trim($raw) !==
'') {
643 $decoded = json_decode($raw,
true);
644 if (is_array($decoded)) {
649 if (!count($data) && !empty(
$_POST) && is_array(
$_POST)) {
656 private function normalize_content_media_urls(
$html) {
658 $html = preg_replace_callback(
'/<figure\b([^>]*dbx-cms-inline-video-block[^>]*)>[\s\S]*?<\/figure>/i',
function($m) {
660 if (!preg_match(
'/data-cms-media-id=["\']?([0-9]+)/i', $attrs, $id_match)
661 && !preg_match(
'/data-cms-media-id=["\']?([0-9]+)/i', $m[0], $id_match)
662 && !preg_match(
'/dbx_mid=([0-9]+)/i', $m[0], $id_match))
return $m[0];
663 $id = (int)$id_match[1];
664 $placeholder = $this->inline_media_placeholder_html($id);
665 if ($placeholder ===
'')
return $m[0];
666 $attrs = preg_replace(
'/\scontenteditable=(["\']).*?\1/i',
'', $attrs);
667 $attrs = preg_replace(
'/\sdata-cms-media-id\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i',
'', $attrs);
668 $attrs .=
' data-cms-media-id="' . $id .
'"';
669 if (stripos($attrs,
'data-cms-media-slot=') ===
false) $attrs .=
' data-cms-media-slot="inline"';
670 return '<figure' . $attrs .
'>' . $placeholder .
'</figure>';
678 $html = preg_replace_callback(
'/<video\b([^>]*)>([\s\S]*?)<\/video>/i',
function($m) {
679 $attrs = (string)($m[1] ??
'');
680 $inner = (string)($m[2] ??
'');
681 $source = $attrs .
' ' . $inner;
683 if (preg_match(
'/data-cms-media-id=["\']?([0-9]+)/i',
$source, $id_match)
684 || preg_match(
'/dbx_mid=([0-9]+)/i',
$source, $id_match)) {
685 $id = (int)($id_match[1] ?? 0);
687 if ($id <= 0)
return $m[0];
689 $placeholder = $this->inline_media_placeholder_html($id);
690 if ($placeholder ===
'')
return $m[0];
692 $video_attrs =
' class="dbx-cms-inline-media dbx-cms-inline-video-block"'
693 .
' data-cms-media-id="' . $id .
'" data-cms-media-slot="inline"';
694 foreach (array(
'autoplay',
'loop',
'muted') as $option) {
695 $enabled = preg_match(
'/(?:^|\s)' . $option .
'(?:\s|=|$)/i', $attrs) === 1;
696 $video_attrs .=
' data-cms-video-' . $option .
'="' . ($enabled ?
'1' :
'0') .
'"';
698 return '<figure' . $video_attrs .
'>' . $placeholder .
'</figure>';
700 $html = preg_replace(
'/<(video|iframe|source)\b[^>]*data-cms-media-id=["\']?([0-9]+)[^>]*>(?:\s*<\/\1>)?/i',
'',
$html);
701 $html = preg_replace_callback(
'/<(img|video|iframe|source)\b([^>]*?)>/i',
function($m) {
703 $tag_name = strtolower((
string)($m[1] ??
''));
705 if (preg_match(
'/dbx_mid=([0-9]+)/i', $tag, $id_match)) {
706 $id = (int)$id_match[1];
707 $tag = preg_replace(
'/\sdata-cms-media-id\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i',
'', $tag);
708 $tag = preg_match(
'/\/\s*>$/', $tag)
709 ? preg_replace(
'/\s*\/\s*>$/',
' data-cms-media-id="' . $id .
'" />', $tag)
710 : preg_replace(
'/\s*>$/',
' data-cms-media-id="' . $id .
'">', $tag);
711 } elseif (preg_match(
'/data-cms-media-id=["\']?([0-9]+)/i', $tag, $id_match)) {
712 $id = (int)$id_match[1];
714 if ($tag_name ===
'img' && $id > 0 && !$this->inline_media_available($id)) {
715 return $this->inline_media_missing_html($id);
717 if (stripos($tag,
'data-cms-media-slot=') ===
false && $id > 0) {
718 $tag = preg_match(
'/\/\s*>$/', $tag)
719 ? preg_replace(
'/\s*\/\s*>$/',
' data-cms-media-slot="inline" />', $tag)
720 : preg_replace(
'/\s*>$/',
' data-cms-media-slot="inline">', $tag);
724 $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) {
725 $tag = (string)($m[1] ??
'p');
726 $attrs = (string)($m[2] ??
'');
727 $inner = (string)($m[3] ??
'');
728 if (stripos($attrs,
'dbx-cms-inline-video-block') !==
false) {
731 if (!preg_match(
'/<img\b[^>]*\bsrc\s*=\s*(?:"[^"]*dbx_mid=([0-9]+)[^"]*"|\'[^\']*dbx_mid=([0-9]+)[^\']*\')/i', $inner, $id_match)) {
734 $id = (int)(($id_match[1] ?? 0) ?: ($id_match[2] ?? 0));
735 if ($id <= 0)
return $m[0];
736 $attrs = preg_replace(
'/\sdata-cms-media-id\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i',
'', $attrs);
737 if (stripos($attrs,
'data-cms-media-slot=') ===
false) $attrs .=
' data-cms-media-slot="inline"';
738 $inner = preg_replace_callback(
'/<img\b([^>]*)>/i',
function($img) use ($id) {
739 $img_attrs = (string)($img[1] ??
'');
740 if (!preg_match(
'/\bsrc\s*=\s*(?:"[^"]*dbx_mid=' . $id .
'\b[^"]*"|\'[^\']*dbx_mid=' . $id .
'\b[^\']*\')/i', $img_attrs)) {
743 $img_attrs = preg_replace(
'/\sdata-cms-media-id\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i',
'', $img_attrs);
744 if (stripos($img_attrs,
'data-cms-media-slot=') ===
false) $img_attrs .=
' data-cms-media-slot="inline"';
745 return '<img' . $img_attrs .
' data-cms-media-id="' . $id .
'">';
747 return '<' . $tag . $attrs .
'>' . $inner .
'</' . $tag .
'>';
749 $html = $this->strip_empty_inline_media_wrappers(
$html);
752 '?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=',
753 '?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=',
754 'index.phpindex.php?',
757 'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=',
758 'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=',
766 private function content_node_has_visible_value($node):
bool {
767 foreach ($node->childNodes as $child) {
768 if ($child instanceof \DOMText) {
769 $value = preg_replace(
'/[\s\x{00A0}\x{200B}\x{FEFF}]+/u',
'', (
string)$child->nodeValue);
770 if (
$value !==
'')
return true;
774 if (!($child instanceof \DOMElement))
continue;
775 $tag = strtolower($child->tagName);
776 if (in_array($tag, array(
'br',
'wbr'),
true))
continue;
777 if (in_array($tag, array(
'img',
'video',
'audio',
'iframe',
'object',
'embed',
'table',
'hr',
'svg',
'canvas',
'input',
'textarea',
'select',
'button'),
true)) {
780 if ($this->content_node_has_visible_value($child))
return true;
785 private function strip_empty_content_paragraphs_fallback(
string $html):
string {
786 $pattern =
'#<p\b[^>]*>(?:\s| |�*160;|�*a0;|<br\b[^>]*>|<wbr\b[^>]*>|<!--[\s\S]*?-->)*</p>#iu';
788 $clean = preg_replace($pattern,
'',
$html);
789 if ($clean ===
null || $clean ===
$html)
break;
795 private function safe_content_style(
string $style,
string $tag,
string $className =
''):
string {
796 $tag = strtolower(trim($tag));
797 $className = strtolower(trim($className));
798 $isMediaLayout = in_array($tag, array(
'img',
'video',
'iframe',
'figure'),
true)
799 || preg_match(
'/(?:^|\s)dbx-cms-inline-(?:media|image|video|video-block)(?:\s|$)/', $className);
802 foreach (explode(
';',
$style) as $declaration) {
803 if (strpos($declaration,
':') ===
false)
continue;
804 list($property,
$value) = array_map(
'trim', explode(
':', $declaration, 2));
805 $property = strtolower($property);
807 if ($property ===
'' ||
$value ===
'')
continue;
809 if ($property ===
'text-align' && in_array(
$value, array(
'left',
'right',
'center',
'justify',
'start',
'end'),
true)) {
810 $safe[$property] =
$value;
814 if (!$isMediaLayout)
continue;
815 if ($property ===
'float' && in_array(
$value, array(
'left',
'right',
'none'),
true)) {
816 $safe[$property] =
$value;
819 if ($property ===
'display' && in_array(
$value, array(
'block',
'inline-block'),
true)) {
820 $safe[$property] =
$value;
823 if (in_array($property, array(
'width',
'height',
'max-width',
'max-height'),
true)
824 && preg_match(
'/^(?:auto|0|(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?:px|%|vw|vh|rem|em))$/',
$value)) {
825 $safe[$property] =
$value;
828 if (in_array($property, array(
'margin-left',
'margin-right'),
true)
829 && preg_match(
'/^(?:auto|0|(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?:px|%|rem|em))$/',
$value)) {
830 $safe[$property] =
$value;
835 foreach ($safe as $property =>
$value) {
841 private function sanitize_content_html(
$html) {
847 $html = preg_replace(
'#<(script|style|object|embed|base|meta|link)\b[^>]*>.*?</\1>#is',
'',
$html);
848 $html = preg_replace(
'#<(script|style|object|embed|base|meta|link)\b[^>]*\/?>#is',
'',
$html);
850 if (!class_exists(
'\DOMDocument')) {
851 $html = preg_replace(
'/\s+on[a-z]+\s*=\s*(["\']).*?\1/is',
'',
$html);
852 $html = preg_replace(
'/\s+style\s*=\s*(["\']).*?\1/is',
'',
$html);
853 $html = preg_replace(
'/\s+(href|src)\s*=\s*(["\'])\s*javascript:[^"\']*\2/is',
'',
$html);
854 return $this->strip_empty_content_paragraphs_fallback(
$html);
857 $previous = libxml_use_internal_errors(
true);
858 $doc = new \DOMDocument(
'1.0',
'UTF-8');
859 $wrapped =
'<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><div id="dbx_cms_sanitize_root">' .
$html .
'</div></body></html>';
860 $doc->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
861 libxml_clear_errors();
862 libxml_use_internal_errors($previous);
864 $root = $doc->getElementById(
'dbx_cms_sanitize_root');
869 $walker =
function($node) use (&$walker) {
870 if ($node instanceof \DOMElement) {
873 foreach ($node->attributes as $attr) {
874 $name = strtolower($attr->name);
875 $value = trim((
string)$attr->value);
876 if ($name ===
'style') {
877 $safeStyle = $this->safe_content_style(
879 (
string)$node->tagName,
880 (
string)$node->getAttribute(
'class')
885 if (strpos($name,
'on') === 0) {
889 if (($name ===
'href' || $name ===
'src' || $name ===
'xlink:href') && preg_match(
'/^\s*javascript:/i',
$value)) {
893 if ($name ===
'data-dbx') {
894 if (stripos(
$value,
'lib=openWin') ===
false || stripos(
$value,
'javascript:') !==
false) {
900 $node->removeAttribute($name);
902 if ($safeStyle !==
'') {
903 $node->setAttribute(
'style', $safeStyle);
907 foreach (iterator_to_array($node->childNodes) as $child) {
913 $emptyParagraphs = array();
914 foreach (
$root->getElementsByTagName(
'p') as $paragraph) {
915 if (!$this->content_node_has_visible_value($paragraph)) {
916 $emptyParagraphs[] = $paragraph;
919 foreach ($emptyParagraphs as $paragraph) {
920 if ($paragraph->parentNode) $paragraph->parentNode->removeChild($paragraph);
924 foreach (
$root->childNodes as $child) {
925 $out .= $doc->saveHTML($child);
930 private function normalize_and_sanitize_content(
$html) {
931 return $this->sanitize_content_html($this->normalize_content_media_urls(
$html));
934 private function inline_media_available($id) {
936 if ($id <= 0)
return false;
937 $db =
dbx()->get_system_obj(
'dbxDB');
938 $row =
$db->select1($this->dd_media, $id);
939 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1)
return false;
940 return $this->media_file_exists($row);
943 private function inline_media_missing_html($id) {
945 if ($id <= 0)
return '';
946 $label =
'Mediendatei #' . $id .
' nicht verfuegbar';
947 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 auswählen, Entf zum Löschen"><span class="dbx-cms-inline-media-missing" aria-hidden="true">' . htmlspecialchars($label, ENT_QUOTES,
'UTF-8') .
'</span></p>';
950 private function inline_media_placeholder_html($id) {
952 if ($id <= 0)
return '';
953 $db =
dbx()->get_system_obj(
'dbxDB');
954 $row =
$db->select1($this->dd_media, $id);
955 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1)
return '';
956 $row = $this->normalize_media_row($row);
957 $title = htmlspecialchars((
string)($row[
'title'] ?? $row[
'alt'] ?? $row[
'file_name'] ??
'Video'), ENT_QUOTES,
'UTF-8');
958 $attr =
' data-cms-media-id="' . $id .
'" data-cms-media-slot="inline"';
959 if (!in_array($this->media_type($row), array(
'video',
'external_video'),
true))
return '';
960 $thumb = htmlspecialchars((
string)($row[
'thumb_url'] ??
''), ENT_QUOTES,
'UTF-8');
961 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>';
962 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>';
965 private function inline_media_embed_html(array $row) {
966 $id = (int)($row[
'id'] ?? 0);
967 if ($id <= 0)
return '';
968 $row = $this->normalize_media_row($row);
969 $attr =
' data-cms-media-id="' . $id .
'" data-cms-media-slot="inline" contenteditable="false" tabindex="0"';
970 if (in_array($this->media_type($row), array(
'video',
'external_video'),
true)) {
971 $inner = $this->inline_media_placeholder_html($id);
972 return $inner !==
'' ?
'<figure class="dbx-cms-inline-media dbx-cms-inline-video-block"' . $attr .
'>' . $inner .
'</figure><p></p>' :
'';
974 $url = htmlspecialchars((
string)($row[
'url'] ?? $this->media_url($row)), ENT_QUOTES,
'UTF-8');
975 if (
$url ===
'')
return '';
976 $alt = htmlspecialchars((
string)($row[
'alt'] ?? $row[
'title'] ?? $row[
'file_name'] ??
''), ENT_QUOTES,
'UTF-8');
977 $title = htmlspecialchars((
string)($row[
'title'] ?? $row[
'alt'] ?? $row[
'file_name'] ??
''), ENT_QUOTES,
'UTF-8');
978 return '<p class="dbx-cms-inline-media"' . $attr .
'><img class="dbx-cms-inline-image" src="' .
$url .
'" alt="' . $alt .
'" title="' . $title .
'"' . $attr .
'></p><p></p>';
981 private function bool_int(
$value, $default = 1) {
986 private function clean_text(
$value, $max = 254) {
988 if ($max > 0 && strlen(
$value) > $max) {
994 private function slug($text) {
995 return dbxContent_permalink::slug($text);
998 private function page_permalink(
$db, $folder_id, $title, $permalink,
int $excludeId = 0) {
999 $permalink = trim($this->clean_text($permalink, 254));
1000 if ($permalink ===
'') {
1001 return dbxContent_permalink::build(
$db, dbxContentLng::ddFolder(), $folder_id, $title, $excludeId);
1003 if (!dbxContent_permalink::isValid($permalink)) {
1004 throw new \InvalidArgumentException(
'Permalink: nur Kleinbuchstaben, Zahlen und einzelne Bindestriche sind erlaubt.');
1006 if (dbxContent_permalink::exists(
$db, dbxContentLng::ddContent(), $permalink, $excludeId)) {
1007 throw new \InvalidArgumentException(
'Dieser Permalink wird bereits von einer anderen Seite verwendet.');
1012 private function page_permalink_exists(
$db,
string $permalink,
int $excludeId = 0):
bool {
1013 $permalink = dbxContent_permalink::normalize($permalink);
1014 return dbxContent_permalink::exists(
$db, dbxContentLng::ddContent(), $permalink, $excludeId);
1017 private function duplicate_page_permalink(
$db,
int $folderId,
string $title,
string $permalink):
string {
1018 $base = dbxContent_permalink::normalize($this->clean_text($permalink, 254));
1020 $base = dbxContent_permalink::build(
$db, dbxContentLng::ddFolder(), $folderId, $title);
1028 $suffix = $copyNumber === 1 ?
'-kopie' :
'-kopie-' . $copyNumber;
1029 $maxBaseLength = max(1, 254 - strlen($suffix));
1030 $candidateBase = rtrim(substr(
$base, 0, $maxBaseLength),
'/-');
1031 if ($candidateBase ===
'') {
1032 $candidateBase =
'seite';
1034 $candidate = dbxContent_permalink::normalize($candidateBase . $suffix);
1036 }
while ($this->page_permalink_exists(
$db, $candidate));
1041 private function resolve_cms_page_id():
int {
1042 $id = (int)
dbx()->get_modul_var(
'cid', 0,
'int');
1044 $id = (int)
dbx()->get_modul_var(
'dbx_cid', 0,
'int');
1047 $id = (int)
dbx()->get_modul_var(
'rid', 0,
'int');
1050 $id = (int)
dbx()->get_modul_var(
'id', 0,
'int');
1056 $rawPermalink = trim((
string)
dbx()->get_request_var(
'permalink',
''));
1057 $permalink = dbxContent_permalink::isValid($rawPermalink)
1059 : dbxContent_permalink::canonicalFromLegacy($rawPermalink);
1060 $db =
dbx()->get_system_obj(
'dbxDB');
1061 if ($permalink !==
'') {
1062 $rec =
$db->select1(dbxContentLng::ddContent(), array(
'permalink' => $permalink),
'id', 0);
1063 $permalinkId = is_array($rec) ? (int)($rec[
'id'] ?? 0) : 0;
1064 if ($permalinkId > 0)
return $permalinkId;
1070 dbxContentLng::ddContent(),
1080 return is_array(
$rows) && isset(
$rows[0]) ? (int)(
$rows[0][
'id'] ?? 0) : 0;
1083 private function attach_unreachable_tree_nodes(array $tree): array {
1084 $reachablePageIds = array();
1085 $reachableFolderIds = array();
1086 foreach (is_array($tree[
'flat'] ??
null) ? $tree[
'flat'] : array() as $node) {
1087 if (!is_array($node)) {
1090 if (($node[
'_type'] ??
'') ===
'page') {
1091 $reachablePageIds[(int)($node[
'_id'] ?? 0)] =
true;
1092 } elseif (($node[
'_type'] ??
'') ===
'folder') {
1093 $reachableFolderIds[(int)($node[
'_id'] ?? 0)] =
true;
1097 $items = is_array($tree[
'items'] ??
null) ? $tree[
'items'] : array();
1098 $orphanPages = array();
1099 foreach (
$items as $row) {
1100 if (!is_array($row)) {
1103 $id = (int)($row[
'id'] ?? 0);
1104 if ($id > 0 && !isset($reachablePageIds[$id])) {
1105 $orphanPages[] = $row;
1108 if (!count($orphanPages)) {
1112 $children = array();
1113 foreach ($orphanPages as $row) {
1114 $id = (int)($row[
'id'] ?? 0);
1118 $parent = (int)($row[
'folder'] ?? 0);
1119 $children[] = $row + array(
1120 '_node_id' =>
'page-' . $id,
1123 '_parent' => $parent,
1124 '_title' => (
string)($row[
'title'] ?? (
'Seite ' . $id)),
1125 '_rights' => (
string)($row[
'group_read'] ??
''),
1126 '_children' => array(),
1131 if (!count($children)) {
1135 $orphanFolder = array(
1137 'name' =>
'Nicht im Baum',
1138 '_node_id' =>
'folder--999001',
1139 '_type' =>
'folder',
1142 '_title' =>
'Nicht im Baum',
1144 '_children' => $children,
1148 $tree[
'nodes'] = array_merge(is_array($tree[
'nodes'] ??
null) ? $tree[
'nodes'] : array(), array($orphanFolder));
1149 foreach ($children as $child) {
1150 $tree[
'flat'][] = $child;
1152 $tree[
'flat'][] = $orphanFolder;
1157 private function cms_tree() {
1158 $db =
dbx()->get_system_obj(
'dbxDB');
1159 $this->ensure_cms_schema(
$db);
1160 $tree =
$db->select_tree(dbxContentLng::ddFolder(), dbxContentLng::ddContent(), array(
1161 'folder_order' =>
'sorter,name,id',
1162 'item_order' =>
'sorter,title,id',
1163 'verify_access' => 0,
1165 $tree = $this->attach_unreachable_tree_nodes($tree);
1166 return $this->decorate_tree($tree);
1169 private function clear_cms_tpl_cache() {
1170 if (isset(
$_SESSION[
'dbx'][
'cache'][
'tpl'][
'dbxContent_admin']) && is_array(
$_SESSION[
'dbx'][
'cache'][
'tpl'][
'dbxContent_admin'])) {
1171 foreach (array(
'cms-admin',
'cms-admin-frame',
'cms-admin-header',
'cms-admin-left',
'cms-admin-middle',
'cms-admin-right',
'cms-admin-media-panel',
'cms-admin-page-form',
'cms-admin-folder-form',
'cms-admin-settings-panels') as
$tpl) {
1172 unset(
$_SESSION[
'dbx'][
'cache'][
'tpl'][
'dbxContent_admin'][
$tpl]);
1175 if (isset(
$_SESSION[
'dbx'][
'cache'][
'tpl'][
'dbx']) && is_array(
$_SESSION[
'dbx'][
'cache'][
'tpl'][
'dbx'])) {
1176 foreach (array(
'module-bar-cms',
'bar-cms',
'bar-title-cms',
'bar-middle-cms',
'bar-actions-cms') as
$tpl) {
1193 private function ensure_cms_schema(
$db) {
1197 private function normalize_gallery_row(array $row) {
1199 'gallery_template' =>
'image-gallery',
1200 'gallery_visible_count' =>
'3',
1201 'gallery_image_size' =>
'original',
1202 'gallery_lightbox_width' =>
'100vw',
1203 'gallery_overflow' =>
'grid',
1204 'gallery_click_behavior' =>
'lightbox',
1208 $current = trim((
string)($row[
$field] ??
''));
1209 if ($current ===
'' || strtolower($current) ===
'parent') {
1217 private function tree_records_by_id(
$db,
string $dd,
string $fields): array {
1220 foreach (is_array(
$rows) ?
$rows : array() as $row) {
1221 $id = (int)($row[
'id'] ?? 0);
1222 if ($id > 0)
$out[$id] = $row;
1227 private function tree_lng_coverage_rows(
$db): array {
1228 $out = array(
'folder' => array(),
'page' => array());
1229 foreach (dbxContentLngSync::accessibleLngs() as
$lng) {
1231 'folder' => array(dbxContentLng::ddFolder(
$lng),
'id,lng_uid,lng_sync,name',
'name'),
1232 'page' => array(dbxContentLng::ddContent(
$lng),
'id,lng_uid,lng_sync,title',
'title'),
1236 foreach (is_array(
$rows) ?
$rows : array() as $row) {
1237 $lngUid = trim((
string)($row[
'lng_uid'] ??
''));
1238 if ($lngUid ===
'')
continue;
1239 $out[$entity][$lngUid][
$lng] = array(
1240 'id' => (
int)($row[
'id'] ?? 0),
1241 'title' => (
string)($row[$titleField] ??
''),
1242 'lng_sync' => strtolower(trim((
string)($row[
'lng_sync'] ??
'auto'))) ?:
'auto',
1250 private function tree_lng_coverage(
string $type,
string $lngUid, array $coverageRows): array {
1251 $master = dbxContentLngSync::masterLng();
1253 'lng_uid' => $lngUid,
1255 'master_lng' => $master,
1256 'current_lng' => dbxContentLng::current(),
1257 'languages' => array(),
1259 foreach (dbxContentLngSync::accessibleLngs() as
$lng) {
1260 $row = $coverageRows[$type][$lngUid][
$lng] ??
null;
1261 $sync = is_array($row) ? (string)($row[
'lng_sync'] ??
'auto') :
'';
1262 $coverage[
'languages'][
$lng] = array(
1264 'status' => is_array($row) ? (
$lng === $master ?
'master' : ($sync ?:
'auto')) :
'missing',
1265 'id' => is_array($row) ? (
int)($row[
'id'] ?? 0) : 0,
1266 'title' => is_array($row) ? (
string)($row[
'title'] ??
'') :
'',
1267 'lng_sync' => $sync,
1268 'is_master' =>
$lng === $master ? 1 : 0,
1274 private function attach_lng_coverage(array $node,
$db, array &$coverageRows): array {
1275 $type = ($node[
'_type'] ??
'') ===
'folder' ?
'folder' :
'page';
1276 $id = (int)($node[
'_id'] ?? 0);
1277 $dd = $type ===
'folder' ? dbxContentLng::ddFolder() : dbxContentLng::ddContent();
1278 $lngUid = trim((
string)($node[
'lng_uid'] ?? $node[
'_lng_uid'] ??
''));
1280 if ($lngUid ===
'' && $id > 0) {
1281 $lngUid = dbxContentLngSync::ensureRecordUid(
$db,
$dd, $id, $type ===
'folder' ?
'f' :
'p');
1284 $node[
'_lng_uid'] = $lngUid;
1285 if ($lngUid !==
'') {
1286 $currentLng = dbxContentLng::current();
1287 if (!isset($coverageRows[$type][$lngUid][$currentLng])) {
1288 $coverageRows[$type][$lngUid][$currentLng] = array(
1290 'title' => (
string)($node[
'_title'] ??
''),
1291 'lng_sync' => strtolower(trim((
string)($node[
'lng_sync'] ??
'auto'))) ?:
'auto',
1294 $coverage = $this->tree_lng_coverage($type, $lngUid, $coverageRows);
1295 $node[
'_lng_coverage'] = $coverage;
1296 $node[
'_lng_badges'] = dbxContentLngSync::badgesHtml($coverage);
1302 private function decorate_tree_nodes(array $nodes, array &$flat,
$db, array $folderRows, array $pageRows, array &$coverageRows) {
1304 foreach ($nodes as $node) {
1305 if (!is_array($node))
continue;
1306 if (($node[
'_type'] ??
'') ===
'folder' && (
int)($node[
'_id'] ?? 0) > 0) {
1307 $full = $folderRows[(int)$node[
'_id']] ??
null;
1308 if (is_array($full)) {
1309 foreach (array(
'template',
'group_read',
'hero_template',
'hero_image_id',
'hero_margin_top',
'hero_height',
'hero_variant',
'hero_sticky',
'hero_scroll_layer') as $key) {
1310 if (array_key_exists($key, $full)) {
1311 $node[$key] = $full[$key];
1312 $node[
'_' . $key] = $full[$key];
1315 $node[
'_template'] = $full[
'template'] ?? ($node[
'_template'] ??
'');
1316 $node[
'_rights'] = $full[
'group_read'] ?? ($node[
'_rights'] ??
'');
1317 $node[
'lng_uid'] = $full[
'lng_uid'] ??
'';
1318 $node[
'lng_sync'] = $full[
'lng_sync'] ??
'auto';
1321 if (($node[
'_type'] ??
'') ===
'page' && (
int)($node[
'_id'] ?? 0) > 0) {
1322 $full = $pageRows[(int)$node[
'_id']] ??
null;
1323 if (is_array($full)) {
1324 $node[
'lng_uid'] = $full[
'lng_uid'] ??
'';
1325 $node[
'lng_sync'] = $full[
'lng_sync'] ??
'auto';
1328 $node = $this->attach_lng_coverage($node,
$db, $coverageRows);
1330 if (isset($node[
'_children']) && is_array($node[
'_children'])) {
1331 $node[
'_children'] = $this->decorate_tree_nodes($node[
'_children'], $flat,
$db, $folderRows, $pageRows, $coverageRows);
1338 private function decorate_tree(array $tree) {
1339 $db =
dbx()->get_system_obj(
'dbxDB');
1340 $folderRows = $this->tree_records_by_id(
1342 dbxContentLng::ddFolder(),
1343 'id,template,group_read,hero_template,hero_image_id,hero_margin_top,hero_height,hero_variant,hero_sticky,hero_scroll_layer,lng_uid,lng_sync'
1345 $pageRows = $this->tree_records_by_id(
$db, dbxContentLng::ddContent(),
'id,lng_uid,lng_sync');
1346 $coverageRows = $this->tree_lng_coverage_rows(
$db);
1348 $tree[
'nodes'] = $this->decorate_tree_nodes(
1349 is_array($tree[
'nodes'] ??
null) ? $tree[
'nodes'] : array(),
1356 $tree[
'flat'] = $flat;
1360 private function media_url($row, $thumb =
false) {
1361 $id = (int)($row[
'id'] ?? 0);
1362 if ($id <= 0)
return '';
1363 $url =
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $id;
1364 if ($thumb && !empty($row[
'thumb_file_path']))
$url .=
'&dbx_thumb=1';
1365 $v = (int)($row[
'size'] ?? 0) .
'-' . (int)($row[
'width'] ?? 0) .
'x' . (int)($row[
'height'] ?? 0);
1366 if ($thumb) $v .=
'-' . (int)($row[
'thumb_width'] ?? 0) .
'x' . (int)($row[
'thumb_height'] ?? 0);
1367 $url .=
'&_v=' . rawurlencode($v);
1371 private function external_video_thumb_url($row) {
1372 $provider = strtolower(trim((
string)($row[
'provider'] ??
'')));
1373 $provider_id = trim((
string)($row[
'provider_id'] ??
''));
1374 if ($provider ===
'youtube' && preg_match(
'~^[A-Za-z0-9_-]{11}$~', $provider_id)) {
1375 return 'https://img.youtube.com/vi/' . $provider_id .
'/hqdefault.jpg';
1380 private function external_video_json_meta(
$file) {
1381 $raw = @file_get_contents((
string)
$file);
1382 if ($raw ===
false || $raw ===
'')
return array();
1383 $data = json_decode($raw,
true);
1384 if (!is_array($data))
return array();
1385 $provider = strtolower(trim((
string)($data[
'provider'] ??
'youtube')));
1386 $provider_id = trim((
string)($data[
'provider_id'] ?? pathinfo((
string)
$file, PATHINFO_FILENAME)));
1387 $title = trim((
string)($data[
'title'] ?? (
'YouTube ' . $provider_id)));
1388 $external_url = trim((
string)($data[
'external_url'] ??
''));
1389 $embed_url = trim((
string)($data[
'embed_url'] ??
''));
1390 if ($embed_url ===
'' && preg_match(
'~^[A-Za-z0-9_-]{11}$~', $provider_id)) {
1391 $embed_url =
'https://www.youtube.com/embed/' . $provider_id;
1394 'provider' => $provider,
1395 'provider_id' => $provider_id,
1398 'external_url' => $external_url,
1399 'embed_url' => $embed_url,
1403 private function media_type($row) {
1404 $stored = strtolower(trim((
string)($row[
'media_type'] ??
'')));
1405 if (in_array(
$stored, array(
'image',
'video',
'external_video',
'file'),
true))
return $stored;
1406 $storage = strtolower(trim((
string)($row[
'storage_type'] ??
'')));
1407 if ($storage ===
'external' || trim((
string)($row[
'provider'] ??
'')) !==
'' || trim((
string)($row[
'embed_url'] ??
'')) !==
'')
return 'external_video';
1408 $mime = strtolower((
string)($row[
'mime'] ??
''));
1409 $name = strtolower((
string)($row[
'file_name'] ?? $row[
'file_path'] ??
''));
1410 if (strpos($mime,
'video/') === 0 || preg_match(
'/\.(mp4|webm|ogv|ogg|mov|m4v)$/i', $name))
return 'video';
1411 if (strpos($mime,
'image/') === 0 || preg_match(
'/\.(jpg|jpeg|png|gif|webp|svg)$/i', $name))
return 'image';
1415 private function media_file_exists($row) {
1416 $row = is_array($row) ? $row : array();
1417 if (strtolower((
string)($row[
'storage_type'] ??
'')) ===
'external')
return trim((
string)($row[
'embed_url'] ?? $row[
'external_url'] ??
'')) !==
'';
1418 $rel = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
1419 if ($rel ===
'' || strpos($rel,
'..') !==
false)
return false;
1424 if (!
$base)
return false;
1428 $real = realpath(
$file);
1429 return $real && strpos($real,
$base) === 0 && is_file($real) && is_readable($real);
1432 private function media_thumb_exists($row) {
1433 $row = is_array($row) ? $row : array();
1434 if (strtolower((
string)($row[
'storage_type'] ??
'')) ===
'external' && trim((
string)($row[
'thumb_file_path'] ??
'')) ===
'')
return false;
1435 $rel = ltrim(str_replace(
'\\',
'/', (
string)($row[
'thumb_file_path'] ??
'')),
'/');
1436 if ($rel ===
'' || strpos($rel,
'..') !==
false)
return false;
1444 $real = realpath(
$file);
1445 return $base && $real && strpos($real,
$base) === 0 && is_file($real) && is_readable($real);
1448 private function media_thumbnail_supported($row): bool {
1449 $row = is_array($row) ? $row : array();
1450 $type = $this->media_type($row);
1451 if ($type ===
'video')
return true;
1452 if ($type !==
'image' || !function_exists(
'imagecreatetruecolor'))
return false;
1453 $name = (string)($row[
'file_name'] ?? $row[
'file_path'] ??
'');
1454 $mime = strtolower(trim((
string)($row[
'mime'] ??
'')));
1457 return $mime !==
'image/svg+xml' && !preg_match(
'/\.svg$/i', $name);
1466 private function media_thumbnail_is_current($row): bool {
1467 $row = is_array($row) ? $row : array();
1468 if (!$this->media_thumb_exists($row))
return false;
1469 if ($this->media_type($row) !==
'image')
return true;
1470 if (!$this->media_thumbnail_supported($row))
return true;
1472 $source = $this->source_media_file($row);
1473 $thumb = $this->source_thumb_file($row);
1474 if (
$source ===
'' || $thumb ===
'')
return false;
1476 $source_time = @filemtime(
$source);
1477 $thumb_time = @filemtime($thumb);
1478 if ($source_time !==
false && $thumb_time !==
false && $source_time > $thumb_time)
return false;
1480 $source_name = (string)($row[
'file_name'] ?? basename(
$source));
1481 $source_stem = strtolower((
string)pathinfo($source_name, PATHINFO_FILENAME));
1482 $thumb_stem = strtolower((
string)pathinfo(basename($thumb), PATHINFO_FILENAME));
1483 if ($source_stem !==
'' && strpos($thumb_stem, $source_stem) ===
false)
return false;
1485 $source_size = @getimagesize(
$source);
1486 $thumb_size = @getimagesize($thumb);
1487 if (!is_array($source_size) || !is_array($thumb_size))
return false;
1488 $source_w = (int)($source_size[0] ?? 0);
1489 $source_h = (int)($source_size[1] ?? 0);
1490 if ($source_w <= 0 || $source_h <= 0)
return false;
1491 $scale = min(640 / $source_w, 480 / $source_h, 1);
1492 $expected_w = max(1, (
int)round($source_w * $scale));
1493 $expected_h = max(1, (
int)round($source_h * $scale));
1494 return abs((
int)($thumb_size[0] ?? 0) - $expected_w) <= 1
1495 && abs((
int)($thumb_size[1] ?? 0) - $expected_h) <= 1;
1498 private function valid_media_slot($slot, $default =
'gallery') {
1499 $slot = strtolower(trim((
string)$slot));
1500 $allowed = array(
'hero',
'gallery',
'inline',
'header',
'teaser',
'footer',
'shop');
1501 return in_array($slot,
$allowed,
true) ? $slot : $default;
1504 private function media_rel_dir($slot) {
1505 return 'media/' . $this->valid_media_slot($slot) .
'/';
1508 private function video_media_base_folder() {
1511 if (is_dir(
$root .
'videos'))
return 'videos';
1512 if (is_dir(
$root .
'video'))
return 'video';
1516 private function media_standard_bases() {
1517 return array(
'img' =>
'image', $this->video_media_base_folder() =>
'video',
'youtube' =>
'external_video',
'file' =>
'file');
1520 private function canonical_media_folder($folder) {
1521 $folder = strtolower(trim(str_replace(
'\\',
'/', (
string)$folder),
'/'));
1522 if (preg_match(
'~^youtube/[a-z0-9_-]{11}$~', $folder))
return 'youtube';
1526 private function clean_media_folder($folder, $media_type =
'image') {
1527 $media_type = $this->media_type(array(
'media_type' => $media_type));
1528 $folder = $this->canonical_media_folder($folder);
1529 $folder = strtolower(str_replace(
'\\',
'/', trim((
string)$folder)));
1530 $folder = preg_replace(
'~/+~',
'/', $folder);
1531 $folder = trim($folder,
'/');
1533 foreach (explode(
'/', $folder) as $part) {
1534 $part = preg_replace(
'/[^A-Za-z0-9_-]+/',
'-', $part);
1535 $part = trim($part,
'-_');
1536 if ($part !==
'' && $part !==
'.' && $part !==
'..') $parts[] = $part;
1538 $folder = implode(
'/', $parts);
1540 if ($folder ===
'module') {
1544 if ($media_type ===
'video') {
1545 $base = $this->video_media_base_folder();
1546 if ($folder ===
'' || $folder ===
'video' || $folder ===
'videos' || $folder ===
'img' || $folder ===
'img/video')
return $base;
1547 if (substr($folder, 0, 10) ===
'img/video/')
return $base .
'/' . substr($folder, 10);
1548 if (substr($folder, 0, 6) ===
'video/')
return $base .
'/' . substr($folder, 6);
1549 if (substr($folder, 0, 7) ===
'videos/')
return $base .
'/' . substr($folder, 7);
1552 if ($media_type ===
'external_video') {
1553 if ($folder ===
'' || $folder ===
'youtube')
return 'youtube';
1554 if (substr($folder, 0, 8) !==
'youtube/')
return 'youtube';
1557 if ($media_type ===
'file') {
1558 if ($folder ===
'' || $folder ===
'file')
return 'file/allgemein';
1559 if (substr($folder, 0, 5) !==
'file/')
return 'file/allgemein';
1562 if ($folder ===
'' || $folder ===
'img' || $folder ===
'image')
return 'img/allgemein';
1563 if (substr($folder, 0, 4) !==
'img/')
return 'img/allgemein';
1567 private function media_type_from_folder($folder,
$fallback =
'image') {
1568 $folder = $this->canonical_media_folder($folder);
1569 if (strpos($folder,
'img/video') === 0)
return 'video';
1570 if (strpos($folder,
'videos/') === 0 || $folder ===
'videos')
return 'video';
1571 if (strpos($folder,
'video/') === 0 || $folder ===
'video')
return 'video';
1572 if (strpos($folder,
'youtube/') === 0 || $folder ===
'youtube')
return 'external_video';
1573 if (strpos($folder,
'file/') === 0 || $folder ===
'file')
return 'file';
1577 private function media_folder_from_path($rel, $media_type =
'image') {
1578 $rel = ltrim(str_replace(
'\\',
'/', (
string)$rel),
'/');
1579 if (preg_match(
'~^media/module/[^/]+$~i', $rel)) {
1582 if (preg_match(
'~^media/youtube/[^/]+\.json$~i', $rel)) {
1585 if (preg_match(
'~^media/(img|video|youtube|external|file)/([^/]+)/[^/]+$~i', $rel, $m)) {
1586 return $this->clean_media_folder($m[1] .
'/' . $m[2], $media_type);
1588 if (preg_match(
'~^media/(img|video|file)/([^/]+)/?$~i', $rel, $m)) {
1589 return $this->clean_media_folder($m[1] .
'/' . $m[2], $media_type);
1591 return $this->clean_media_folder(
'', $media_type);
1594 private function media_folder_rel_dir($folder, $media_type =
'image') {
1595 return 'media/' . $this->clean_media_folder($folder, $media_type) .
'/';
1598 private function media_thumb_rel_dir($slot) {
1599 $slot_text = (string)$slot;
1600 $folder_type =
'image';
1601 if (preg_match(
'~^(youtube|external)(/|$)~', $slot_text)) $folder_type =
'external_video';
1602 elseif (preg_match(
'~^(videos|video|img/video)(/|$)~', $slot_text)) $folder_type =
'video';
1603 elseif (preg_match(
'~^file(/|$)~', $slot_text)) $folder_type =
'file';
1604 $folder = $this->clean_media_folder($slot, $folder_type);
1605 return 'media/_thumbs/' . $folder .
'/';
1608 private function cms_media_dir($slot =
'') {
1609 $slot_text = (string)$slot;
1610 $folder_type =
'image';
1611 if (preg_match(
'~^(youtube|external)(/|$)~', $slot_text)) $folder_type =
'external_video';
1612 elseif (preg_match(
'~^(videos|video|img/video)(/|$)~', $slot_text)) $folder_type =
'video';
1613 elseif (preg_match(
'~^file(/|$)~', $slot_text)) $folder_type =
'file';
1614 $rel = $slot ===
'' ?
'media/' : $this->media_folder_rel_dir($slot, $folder_type);
1619 private function cms_media_base_dir(
$base) {
1620 $base = strtolower(trim(str_replace(
'\\',
'/', (
string)
$base),
'/'));
1621 if (!in_array(
$base, array(
'img',
'video',
'videos',
'youtube',
'external',
'file',
'module'),
true))
return '';
1622 $rel =
'media/' .
$base .
'/';
1627 private function cms_media_thumb_dir($slot) {
1632 private function unique_media_name($name, $prefix =
'') {
1633 $name = basename((
string)$name);
1634 $name = preg_replace(
'/[^A-Za-z0-9_.-]+/',
'-', $name);
1635 $prefix = $prefix !==
'' ? rtrim($prefix,
'-') .
'-' :
'';
1636 return date(
'YmdHis') .
'-' . substr(md5($name . microtime(
true)), 0, 8) .
'-' . $prefix . $name;
1639 private function media_slots() {
1640 return array(
'hero',
'gallery',
'inline',
'header',
'teaser',
'footer');
1643 private function media_allowed_extensions() {
1644 return array(
'jpg',
'jpeg',
'png',
'gif',
'webp',
'svg',
'mp4',
'webm',
'ogv',
'ogg',
'mov',
'm4v');
1647 private function file_from_media_rel($rel) {
1648 $rel = ltrim(str_replace(
'\\',
'/', (
string)$rel),
'/');
1649 if ($rel ===
'' || strpos($rel,
'..') !==
false)
return '';
1654 private function relative_file_exists($rel) {
1655 $file = $this->file_from_media_rel($rel);
1659 private function detect_media_mime(
$file, $ext) {
1660 $mime = function_exists(
'mime_content_type') ? (string)@mime_content_type(
$file) :
'';
1661 if ($mime !==
'')
return $mime;
1662 $ext = strtolower((
string)$ext);
1663 if (in_array($ext, array(
'mp4',
'webm',
'ogv',
'ogg',
'mov',
'm4v'),
true)) {
1664 return $ext ===
'webm' ?
'video/webm' : ($ext ===
'mov' ?
'video/quicktime' :
'video/mp4');
1666 return $ext ===
'svg' ?
'image/svg+xml' :
'image/' . ($ext ===
'jpg' ?
'jpeg' : $ext);
1669 private function media_file_meta(
$file, $name) {
1670 $ext = strtolower(pathinfo((
string)$name, PATHINFO_EXTENSION));
1671 $mime = $this->detect_media_mime(
$file, $ext);
1674 $img = @getimagesize(
$file);
1675 if (is_array($img)) {
1676 $width = (int)($img[0] ?? 0);
1677 $height = (int)($img[1] ?? 0);
1681 'size' => (
int)@filesize(
$file),
1683 'height' => $height,
1684 'media_type' => $this->media_type(array(
'mime' => $mime,
'file_name' => $name)),
1688 private function is_excluded_cms_media_folder($folder) {
1689 $folder = $this->canonical_media_folder(strtolower(trim(str_replace(
'\\',
'/', (
string)$folder),
'/')));
1690 return $folder ===
'module' || strpos($folder,
'module/') === 0;
1693 private function cms_media_exclude_sql() {
1694 return " AND media_folder <> 'module' AND file_path NOT LIKE 'media/module/%'";
1697 private function collect_media_files() {
1699 $allowed = $this->media_allowed_extensions();
1701 foreach (array(
'img',
'video',
'youtube',
'file') as $base_folder) {
1702 $dir = $this->cms_media_base_dir($base_folder);
1703 if (!is_dir(
$dir) || !is_readable(
$dir))
continue;
1704 $it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$dir, \FilesystemIterator::SKIP_DOTS));
1706 if (!
$file->isFile() || !
$file->isReadable())
continue;
1707 $name =
$file->getFilename();
1708 $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
1709 $is_external_base = $base_folder ===
'youtube';
1710 if (!in_array($ext,
$allowed,
true) && !($is_external_base && $ext ===
'json'))
continue;
1711 $path = str_replace(
'\\',
'/',
$file->getPathname());
1713 if (strpos($path,
$root) !== 0)
continue;
1714 $rel = substr($path, strlen(
$root));
1715 $type = $base_folder ===
'video' ?
'video' : ($is_external_base ?
'external_video' : ($base_folder ===
'file' ?
'file' :
'image'));
1716 if ($ext ===
'json' && $is_external_base) $type =
'external_video';
1719 'slot' =>
'gallery',
1720 'folder' => $this->media_folder_from_path($rel, $type),
1721 'file' =>
$file->getPathname(),
1729 private function collect_media_thumb_files() {
1733 $rootNormalized = str_replace(
'\\',
'/', rtrim(
$root,
'/\\')) .
'/';
1734 $it = new \RecursiveIteratorIterator(
1735 new \RecursiveDirectoryIterator(
$root, \FilesystemIterator::SKIP_DOTS),
1736 \RecursiveIteratorIterator::LEAVES_ONLY
1739 if (!
$file->isFile() || !
$file->isReadable())
continue;
1740 $path = str_replace(
'\\',
'/',
$file->getPathname());
1741 if (strpos($path, $rootNormalized) !== 0)
continue;
1742 $rel =
'media/_thumbs/' . ltrim(substr($path, strlen($rootNormalized)),
'/');
1743 $files[$rel] = array(
'rel' => $rel,
'file' =>
$file->getPathname());
1748 private function active_media_usage_map(
$db) {
1750 if (!is_object(
$db))
return $map;
1751 $rows =
$db->select($this->dd_media_usage,
'active = 1',
'media_id,content_id,folder_id,content_lng,slot',
'id',
'ASC',
'', 0, 0, 0);
1752 if (is_array(
$rows)) {
1753 foreach (
$rows as $row) {
1754 if (!is_array($row))
continue;
1755 $media_id = (int)($row[
'media_id'] ?? 0);
1756 if ($media_id <= 0)
continue;
1757 if (!isset($map[$media_id])) $map[$media_id] = array();
1758 $map[$media_id][] = $row;
1762 foreach ($this->ordered_media_languages() as
$lng) {
1763 $content_rows =
$db->select(dbxContentLng::ddContent(
$lng),
'',
'id,folder,hero_image_id,seo_image_id,content',
'id');
1764 foreach (is_array($content_rows) ? $content_rows : array() as $row) {
1765 if (!is_array($row))
continue;
1766 $content_id = (int)($row[
'id'] ?? 0);
1767 $folder_id = (int)($row[
'folder'] ?? 0);
1769 'hero' => (
int)($row[
'hero_image_id'] ?? 0),
1770 'seo' => (
int)($row[
'seo_image_id'] ?? 0),
1772 foreach ($direct as $slot => $media_id) {
1773 if ($media_id <= 0)
continue;
1774 if (!isset($map[$media_id])) $map[$media_id] = array();
1775 $map[$media_id][] = array(
'media_id' => $media_id,
'content_id' => $content_id,
'folder_id' => $folder_id,
'content_lng' =>
$lng,
'slot' => $slot);
1777 foreach ($this->inline_media_ids($row[
'content'] ??
'') as $media_id) {
1778 $media_id = (int)$media_id;
1779 if ($media_id <= 0)
continue;
1780 if (!isset($map[$media_id])) $map[$media_id] = array();
1781 $map[$media_id][] = array(
'media_id' => $media_id,
'content_id' => $content_id,
'folder_id' => $folder_id,
'content_lng' =>
$lng,
'slot' =>
'inline');
1784 $folder_rows =
$db->select(dbxContentLng::ddFolder(
$lng),
'',
'id,hero_image_id',
'id');
1785 foreach (is_array($folder_rows) ? $folder_rows : array() as $row) {
1786 $media_id = is_array($row) ? (int)($row[
'hero_image_id'] ?? 0) : 0;
1787 $folder_id = is_array($row) ? (int)($row[
'id'] ?? 0) : 0;
1788 if ($media_id <= 0 || $folder_id <= 0)
continue;
1789 if (!isset($map[$media_id])) $map[$media_id] = array();
1790 $map[$media_id][] = array(
'media_id' => $media_id,
'content_id' => 0,
'folder_id' => $folder_id,
'content_lng' =>
$lng,
'slot' =>
'hero');
1803 private function orphan_media_usage_rows(
$db): array {
1804 if (!is_object(
$db)) return array();
1806 $media_ids = array();
1807 $media_rows =
$db->select($this->dd_media,
'',
'id',
'id',
'ASC',
'', 0, 0, 0);
1808 if (is_array($media_rows)) {
1809 foreach ($media_rows as $row) {
1810 $id = is_array($row) ? (int)($row[
'id'] ?? 0) : 0;
1811 if ($id > 0) $media_ids[$id] = 1;
1815 $content_ids = array();
1816 $folder_ids = array();
1817 foreach (dbxContentLngSync::accessibleLngs() as
$lng) {
1819 $content_rows =
$db->select(dbxContentLng::ddContent((
string)
$lng),
'',
'id',
'id',
'ASC',
'', 0, 0, 0);
1820 if (is_array($content_rows)) {
1821 foreach ($content_rows as $row) {
1822 $id = is_array($row) ? (int)($row[
'id'] ?? 0) : 0;
1823 if ($id > 0) $content_ids[
$lng .
':' . $id] = 1;
1827 $folder_rows =
$db->select(dbxContentLng::ddFolder((
string)
$lng),
'',
'id',
'id',
'ASC',
'', 0, 0, 0);
1828 if (is_array($folder_rows)) {
1829 foreach ($folder_rows as $row) {
1830 $id = is_array($row) ? (int)($row[
'id'] ?? 0) : 0;
1831 if ($id > 0) $folder_ids[
$lng .
':' . $id] = 1;
1834 }
catch (\Throwable $e) {
1835 dbx()->debug(
'dbxContent media usage check skipped lng=' . (
string)
$lng, $e->getMessage());
1840 $usage_rows =
$db->select($this->dd_media_usage,
'',
'*',
'id',
'ASC',
'', 0, 0, 0);
1841 if (!is_array($usage_rows))
return $orphans;
1843 foreach ($usage_rows as $row) {
1844 if (!is_array($row))
continue;
1845 $id = (int)($row[
'id'] ?? 0);
1846 if ($id <= 0)
continue;
1848 $media_id = (int)($row[
'media_id'] ?? 0);
1849 $content_id = (int)($row[
'content_id'] ?? 0);
1850 $folder_id = (int)($row[
'folder_id'] ?? 0);
1851 $content_lng = dbxContentMediaUsageScope::language((
string)($row[
'content_lng'] ??
''));
1854 if ($media_id <= 0 || !isset($media_ids[$media_id])) {
1855 $reason =
'media_missing';
1856 } elseif ($content_id > 0) {
1857 if (!isset($content_ids[$content_lng .
':' . $content_id])) $reason =
'content_missing';
1858 } elseif ($folder_id > 0) {
1859 if (!isset($folder_ids[$content_lng .
':' . $folder_id])) $reason =
'folder_missing';
1861 $reason =
'target_missing';
1864 if ($reason !==
'') {
1865 $orphans[$id] = array(
'row' => $row,
'reason' => $reason);
1872 private function cleanup_orphan_media_usage(
$db): array {
1873 $orphans = $this->orphan_media_usage_rows(
$db);
1876 $cache_targets = array();
1878 foreach ($orphans as $id => $item) {
1879 $row = is_array($item[
'row'] ??
null) ? $item[
'row'] : array();
1880 $reason = (string)($item[
'reason'] ??
'unknown');
1881 $content_id = (int)($row[
'content_id'] ?? 0);
1882 $folder_id = (int)($row[
'folder_id'] ?? 0);
1883 $cache_targets[$content_id .
':' . $folder_id] = array($content_id, $folder_id);
1887 if ((
int)
$db->delete($this->dd_media_usage, (
int)$id, 1, 0) === 1) {
1889 $reasons[$reason] = (int)($reasons[$reason] ?? 0) + 1;
1893 foreach ($cache_targets as
$target) {
1898 'found' => count($orphans),
1899 'removed' => $removed,
1900 'reasons' => $reasons,
1904 private function ordered_media_languages(): array {
1905 $languages = array_values(array_unique(array_map(
'strval', dbxContentLngSync::accessibleLngs())));
1906 $current = strtolower(trim((
string)
dbx()->get_system_var(
'dbx_lng', dbxContentLngSync::masterLng())));
1908 if ($current !==
'' && in_array($current,
$languages,
true)) $ordered[] = $current;
1910 $language = strtolower(trim($language));
1911 if ($language !==
'' && !in_array($language, $ordered,
true)) $ordered[] = $language;
1916 private function add_expected_media_usage(array &
$expected,
int $media_id,
int $content_id,
int $folder_id,
string $slot,
string $template =
'',
string $content_lng =
'',
string $caption =
'',
string $settings =
''): void {
1917 if ($media_id <= 0 || ($content_id <= 0 && $folder_id <= 0)) return;
1918 $content_lng = dbxContentMediaUsageScope::language($content_lng);
1919 $key = dbxContentMediaUsageMaintenance::usageKey($media_id, $content_id, $folder_id, $slot, $content_lng);
1922 'media_id' => $media_id,
1923 'content_id' => $content_id,
1924 'folder_id' => $folder_id,
1926 'content_lng' => $content_lng,
1928 'caption' => $caption,
1929 'settings' => $settings,
1930 'valid_folders' => array(),
1933 if ($content_id > 0 && $folder_id > 0) {
1934 $expected[$key][
'valid_folders'][$folder_id] = 1;
1943 private function media_usage_snapshot(
$db): array {
1944 $media_rows =
$db->select($this->dd_media,
'',
'*',
'id',
'ASC',
'', 0, 0, 0);
1945 if (!is_array($media_rows)) $media_rows = array();
1946 $valid_media_ids = array();
1947 foreach ($media_rows as $row) {
1948 if (!is_array($row))
continue;
1949 $id = (int)($row[
'id'] ?? 0);
1950 if ($id > 0 && (
int)($row[
'active'] ?? 0) === 1 && $this->media_file_exists($row)) {
1951 $valid_media_ids[$id] = 1;
1956 $content_folders = array();
1957 $folder_ids = array();
1958 $seo_media_ids = array();
1959 $seo_references = 0;
1960 foreach ($this->ordered_media_languages() as
$lng) {
1963 dbxContentLng::ddContent(
$lng),
1965 'id,folder,hero_image_id,seo_image_id,content',
1974 if (!is_array(
$page))
continue;
1975 $content_id = (int)(
$page[
'id'] ?? 0);
1976 $folder_id = (int)(
$page[
'folder'] ?? 0);
1977 if ($content_id <= 0)
continue;
1978 $content_key =
$lng .
':' . $content_id;
1979 if (!isset($content_folders[$content_key])) $content_folders[$content_key] = array();
1980 if ($folder_id > 0) $content_folders[$content_key][$folder_id] = 1;
1982 $hero_id = (int)(
$page[
'hero_image_id'] ?? 0);
1983 if (isset($valid_media_ids[$hero_id])) {
1984 $this->add_expected_media_usage(
$expected, $hero_id, $content_id, $folder_id,
'hero',
'image-hero',
$lng);
1986 $seo_id = (int)(
$page[
'seo_image_id'] ?? 0);
1987 if (isset($valid_media_ids[$seo_id])) {
1988 $seo_media_ids[$seo_id] = 1;
1991 foreach ($this->inline_media_ids((
string)(
$page[
'content'] ??
'')) as $media_id) {
1992 $media_id = (int)$media_id;
1993 if (isset($valid_media_ids[$media_id])) {
1994 $this->add_expected_media_usage(
$expected, $media_id, $content_id, $folder_id,
'inline',
'image-inline',
$lng);
1999 $folders =
$db->select(
2000 dbxContentLng::ddFolder(
$lng),
2010 foreach (is_array($folders) ? $folders : array() as $folder) {
2011 if (!is_array($folder))
continue;
2012 $folder_id = (int)($folder[
'id'] ?? 0);
2013 if ($folder_id <= 0)
continue;
2014 $folder_ids[
$lng .
':' . $folder_id] = 1;
2015 $hero_id = (int)($folder[
'hero_image_id'] ?? 0);
2016 if (isset($valid_media_ids[$hero_id])) {
2017 $this->add_expected_media_usage(
$expected, $hero_id, 0, $folder_id,
'hero',
'image-hero',
$lng);
2020 }
catch (\Throwable $e) {
2021 dbx()->debug(
'dbxContent media usage snapshot skipped lng=' .
$lng, $e->getMessage());
2025 $controlled_slots = array(
'hero',
'inline');
2027 $master_lng = dbxContentMediaUsageScope::language(dbxContentLngSync::masterLng());
2028 $shop_page =
$db->select1(
2029 dbxContentLng::ddContent($master_lng),
2030 array(
'permalink' =>
'shop-medienverwendung'),
2034 if (!is_array($shop_page)) {
2035 $shop_page =
$db->select1(
2036 dbxContentLng::ddContent($master_lng),
2037 array(
'permalink' =>
'outside/shop-media-usage'),
2042 $shop_content_id = is_array($shop_page) ? (int)($shop_page[
'id'] ?? 0) : 0;
2043 $shop_folder_id = is_array($shop_page) ? (int)($shop_page[
'folder'] ?? 0) : 0;
2044 $shop_rows =
$db->select(
2045 'dbxShop|shopProductImage',
2046 'active = 1 AND trash = 0 AND media_id > 0',
2047 'media_id,title,product_id,group_id',
2055 if ($shop_content_id > 0 && is_array($shop_rows)) {
2056 $content_key = $master_lng .
':' . $shop_content_id;
2057 if (!isset($content_folders[$content_key])) $content_folders[$content_key] = array();
2058 if ($shop_folder_id > 0) $content_folders[$content_key][$shop_folder_id] = 1;
2059 $shop_by_media = array();
2060 foreach ($shop_rows as $shop_row) {
2061 if (!is_array($shop_row))
continue;
2062 $media_id = (int)($shop_row[
'media_id'] ?? 0);
2063 if (!isset($valid_media_ids[$media_id]))
continue;
2064 if (!isset($shop_by_media[$media_id])) {
2065 $shop_by_media[$media_id] = array(
2066 'title' => (
string)($shop_row[
'title'] ??
''),
2067 'product_ids' => array(),
2068 'group_ids' => array(),
2071 $product_id = (int)($shop_row[
'product_id'] ?? 0);
2072 $group_id = (int)($shop_row[
'group_id'] ?? 0);
2073 if ($product_id > 0) $shop_by_media[$media_id][
'product_ids'][$product_id] = $product_id;
2074 if ($group_id > 0) $shop_by_media[$media_id][
'group_ids'][$group_id] = $group_id;
2076 foreach ($shop_by_media as $media_id => $shop_info) {
2077 $settings = json_encode(array(
2078 'source' =>
'dbxShop',
2079 'product_ids' => array_values($shop_info[
'product_ids']),
2080 'group_ids' => array_values($shop_info[
'group_ids']),
2081 ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
2082 $this->add_expected_media_usage(
2090 (
string)$shop_info[
'title'],
2091 $settings ?:
'{"source":"dbxShop"}'
2094 $controlled_slots[] =
'shop';
2096 }
catch (\Throwable $e) {
2097 dbx()->debug(
'dbxContent shop media usage snapshot skipped', $e->getMessage());
2101 'media_rows' => $media_rows,
2102 'valid_media_ids' => $valid_media_ids,
2104 'content_folders' => $content_folders,
2105 'folder_ids' => $folder_ids,
2106 'seo_media_ids' => $seo_media_ids,
2107 'seo_references' => $seo_references,
2108 'controlled_slots' => $controlled_slots,
2112 private function physical_delete_ids(
$db,
string $dd, array $ids): int {
2113 $ids = array_values(array_unique(array_filter(array_map(
'intval', $ids))));
2115 foreach (array_chunk($ids, 250) as $chunk) {
2116 $where =
'id IN (' . implode(
',', $chunk) .
')';
2117 $before = (int)
$db->count(
$dd, $where);
2118 if ($before <= 0)
continue;
2119 $db->delete(
$dd, $where, 1, 0);
2121 $removed += max(0, $before -
$after);
2126 private function reconcile_media_usage(
$db): array {
2127 $snapshot = $this->media_usage_snapshot(
$db);
2128 $usage_rows =
$db->select($this->dd_media_usage,
'',
'*',
'id',
'ASC',
'', 0, 0, 0);
2129 if (!is_array($usage_rows)) $usage_rows = array();
2130 $plan = dbxContentMediaUsageMaintenance::plan(
2132 $snapshot[
'valid_media_ids'],
2133 $snapshot[
'expected'],
2134 $snapshot[
'content_folders'],
2135 $snapshot[
'folder_ids'],
2136 array(
'hero',
'gallery',
'inline',
'header',
'teaser',
'footer',
'shop'),
2137 $snapshot[
'controlled_slots']
2145 $sorter_max = array();
2146 foreach ($usage_rows as $row) {
2147 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1)
continue;
2148 $sorter_key = (int)($row[
'content_id'] ?? 0) .
':'
2149 . (int)($row[
'folder_id'] ?? 0) .
':'
2150 . dbxContentMediaUsageScope::language((
string)($row[
'content_lng'] ??
'')) .
':'
2151 . $this->valid_media_slot($row[
'slot'] ??
'gallery');
2152 $sorter_max[$sorter_key] = max(
2153 (
int)($sorter_max[$sorter_key] ?? 0),
2154 (
int)($row[
'sorter'] ?? 0)
2158 $cache_targets = array();
2159 foreach ($usage_rows as $row) {
2160 $id = is_array($row) ? (int)($row[
'id'] ?? 0) : 0;
2161 if ($id <= 0 || !isset(
$plan[
'delete'][$id]))
continue;
2162 $content_id = (int)($row[
'content_id'] ?? 0);
2163 $folder_id = (int)($row[
'folder_id'] ?? 0);
2164 $cache_targets[$content_id .
':' . $folder_id] = array($content_id, $folder_id);
2169 $transaction_started = (int)
$db->begin($this->dd_media_usage) === 1;
2171 $removed = $this->physical_delete_ids(
$db, $this->dd_media_usage, array_keys(
$plan[
'delete']));
2173 foreach (
$plan[
'update'] as $id => $data) {
2174 if ((
int)
$db->update($this->dd_media_usage, $data, (
int)$id, 0, 1, 1, 0) >= 0)
$updated++;
2178 $content_id = (int)(
$reference[
'content_id'] ?? 0);
2179 $folder_id = (int)(
$reference[
'folder_id'] ?? 0);
2180 $slot = $this->valid_media_slot(
$reference[
'slot'] ??
'gallery');
2181 $content_lng = dbxContentMediaUsageScope::language((
string)(
$reference[
'content_lng'] ??
''));
2182 $sorter_key = $content_id .
':' . $folder_id .
':' . $content_lng .
':' . $slot;
2183 $sorter_max[$sorter_key] = (int)($sorter_max[$sorter_key] ?? 0) + 10;
2186 'media_id' => (
int)(
$reference[
'media_id'] ?? 0),
2187 'content_id' => $content_id,
2188 'folder_id' => $folder_id,
2189 'content_lng' => $content_lng,
2191 'sorter' => sprintf(
'%04d', $sorter_max[$sorter_key]),
2192 'template' => (
string)(
$reference[
'template'] ??
''),
2193 'caption' => (
string)(
$reference[
'caption'] ??
''),
2194 'settings' => (
string)(
$reference[
'settings'] ??
''),
2196 if (
$db->insert($this->dd_media_usage, $insert, 0, 1, 1, 0) === 1) {
2198 $cache_targets[$content_id .
':' . $folder_id] = array($content_id, $folder_id);
2202 if ($transaction_started && (
int)
$db->commit($this->dd_media_usage) !== 1) {
2203 throw new \RuntimeException(
'media_usage_commit_failed');
2205 }
catch (\Throwable $e) {
2206 if ($transaction_started)
$db->rollback($this->dd_media_usage);
2210 foreach ($cache_targets as
$target) {
2215 'analyzed' => (
int)
$plan[
'analyzed'],
2216 'actual_references' => (
int)
$plan[
'kept'] + $added + (
int)($snapshot[
'seo_references'] ?? 0),
2217 'kept' => (
int)
$plan[
'kept'],
2220 'removed' => $removed,
2221 'planned_removed' => count(
$plan[
'delete']),
2222 'reasons' =>
$plan[
'reasons'],
2226 private function cleanup_invalid_structured_media_references(
$db): array {
2227 $snapshot = $this->media_usage_snapshot(
$db);
2228 $valid = $snapshot[
'valid_media_ids'];
2232 $previous_lng = (string)
dbx()->get_system_var(
'dbx_lng', dbxContentLngSync::masterLng());
2235 foreach ($this->ordered_media_languages() as
$lng) {
2236 dbx()->set_system_var(
'dbx_lng',
$lng);
2237 $content_dd = dbxContentLng::ddContent(
$lng);
2238 $page_rows =
$db->select($content_dd,
'',
'id,hero_image_id,seo_image_id',
'id',
'ASC',
'', 0, 0, 0);
2239 foreach (is_array($page_rows) ? $page_rows : array() as $row) {
2240 if (!is_array($row))
continue;
2241 $id = (int)($row[
'id'] ?? 0);
2242 if ($id <= 0)
continue;
2244 $hero_id = (int)($row[
'hero_image_id'] ?? 0);
2245 $seo_id = (int)($row[
'seo_image_id'] ?? 0);
2246 if ($hero_id > 0 && !isset(
$valid[$hero_id])) $update[
'hero_image_id'] =
'0';
2247 if ($seo_id > 0 && !isset(
$valid[$seo_id])) $update[
'seo_image_id'] = 0;
2248 if (!$update)
continue;
2249 if ((
int)
$db->update($content_dd, $update, $id, 0, 1, 1, 0) >= 0) {
2250 $refs += count($update);
2252 $this->flush_saved_page_cache(
$db, $id);
2256 $folder_dd = dbxContentLng::ddFolder(
$lng);
2257 $folder_rows =
$db->select($folder_dd,
'',
'id,hero_image_id',
'id',
'ASC',
'', 0, 0, 0);
2258 foreach (is_array($folder_rows) ? $folder_rows : array() as $row) {
2259 if (!is_array($row))
continue;
2260 $id = (int)($row[
'id'] ?? 0);
2261 $hero_id = (int)($row[
'hero_image_id'] ?? 0);
2262 if ($id <= 0 || $hero_id <= 0 || isset(
$valid[$hero_id]))
continue;
2263 if ((
int)
$db->update($folder_dd, array(
'hero_image_id' =>
'parent'), $id, 0, 1, 1, 0) >= 0) {
2270 dbx()->set_system_var(
'dbx_lng', $previous_lng);
2273 return array(
'refs' => $refs,
'pages' =>
$pages,
'folders' => $folders_count);
2276 private function purge_invalid_media_records(
$db): array {
2277 $rows =
$db->select($this->dd_media,
'active <> 1',
'*',
'id',
'ASC',
'', 0, 0, 0);
2280 $deleted_thumbs = 0;
2281 foreach (
$rows as $row) {
2282 if (!is_array($row))
continue;
2283 $id = (int)($row[
'id'] ?? 0);
2284 $path = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
2285 if ($id <= 0 || $this->is_excluded_cms_media_folder((
string)($row[
'media_folder'] ??
'')) || strpos($path,
'media/module/') === 0)
continue;
2287 $thumb = $this->source_thumb_file($row);
2288 if ($thumb !==
'' && is_file($thumb) && @unlink($thumb)) $deleted_thumbs++;
2291 'found' => count($ids),
2292 'removed' => $this->physical_delete_ids(
$db, $this->dd_media, $ids),
2293 'deleted_thumbs' => $deleted_thumbs,
2297 private function find_relocated_media_row(array $existing,
string $rel,
string $name, array $meta,
string $media_folder, array $usage_map = array()) {
2298 $name_l = strtolower(trim($name));
2299 if ($name_l ===
'')
return null;
2300 $size = (int)($meta[
'size'] ?? 0);
2301 $width = (int)($meta[
'width'] ?? 0);
2302 $height = (int)($meta[
'height'] ?? 0);
2303 $media_folder = $this->canonical_media_folder($media_folder);
2306 foreach ($existing as $row) {
2307 if (!is_array($row))
continue;
2308 $id = (int)($row[
'id'] ?? 0);
2309 $storage = trim((
string)($row[
'storage_type'] ??
''));
2310 if ($storage ===
'external' || (
string)($row[
'media_type'] ??
'') ===
'external_video')
continue;
2311 if (strtolower((
string)($row[
'file_name'] ??
'')) !== $name_l)
continue;
2312 $existing_path = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
2313 if ($existing_path === $rel)
continue;
2314 if ($existing_path !==
'' && $this->relative_file_exists($existing_path))
continue;
2315 $active = (int)($row[
'active'] ?? 0) === 1;
2316 $used = $id > 0 && !empty($usage_map[$id]);
2317 if (!$active && !$used)
continue;
2319 if ($used) $score += 10;
2320 if ($active) $score += 3;
2321 if ($size > 0 && (
int)($row[
'size'] ?? 0) === $size) $score += 2;
2322 if ($width > 0 && $height > 0 && (
int)($row[
'width'] ?? 0) === $width && (
int)($row[
'height'] ?? 0) === $height) $score += 2;
2323 $old_folder = $this->canonical_media_folder(trim((
string)($row[
'media_folder'] ??
'')));
2324 if ($old_folder !==
'' && $old_folder === $media_folder) $score += 1;
2325 if ($score > $best_score) {
2326 $best_score = $score;
2333 private function sync_cms_media_files(
$db) {
2334 $existing =
$db->select($this->dd_media,
'',
'*',
'id');
2335 if (!is_array($existing)) $existing = array();
2336 $usage_map = $this->active_media_usage_map(
$db);
2339 foreach ($existing as $row) {
2340 if (!is_array($row))
continue;
2341 $path = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
2342 if ($path !==
'') $by_path[$path] = $row;
2346 $relocated_ids = array();
2347 foreach ($this->collect_media_files() as $rel => $file_info) {
2348 $file = (string)($file_info[
'file'] ??
'');
2350 $name = basename(
$file);
2351 $slot = $this->valid_media_slot($file_info[
'slot'] ?? $this->slot_from_media_rel($rel));
2353 $meta = $this->media_file_meta(
$file, $name);
2354 $media_type = $meta[
'media_type'];
2355 if ($media_type ===
'file' && preg_match(
'~^media/youtube/~', $rel)) $media_type =
'external_video';
2356 $media_folder = $this->media_folder_from_path($rel, $media_type);
2357 $storage_type = $media_type ===
'external_video' ?
'external' :
'local';
2358 $external_meta = $media_type ===
'external_video' ? $this->external_video_json_meta(
$file) : array();
2360 $existing_key = $rel;
2361 if (!isset($by_path[$existing_key])) {
2364 $row = isset($by_path[$existing_key]) ? $by_path[$existing_key] :
null;
2365 if (!is_array($row)) {
2366 $row = $this->find_relocated_media_row($existing, $rel, $name, $meta, $media_folder, $usage_map);
2368 if (is_array($row)) {
2369 $id = (int)($row[
'id'] ?? 0);
2370 if ($id <= 0)
continue;
2372 $needs_thumb = $storage_type ===
'local' && (empty($row[
'thumb_file_path']) || !$this->media_thumb_exists($row));
2373 $thumb = $needs_thumb ? $this->create_media_thumbnail(
$file, $media_folder, $name, $meta[
'mime']) : array();
2376 'mime' => $meta[
'mime'],
2377 'size' => $meta[
'size'],
2378 'width' => $meta[
'width'],
2379 'height' => $meta[
'height'],
2380 'media_type' => $media_type,
2381 'storage_type' => $storage_type,
2382 'media_folder' => $media_folder,
2383 'file_name' => $name,
2384 'file_path' => $rel,
2386 if ($external_meta) $update = array_merge($update, $external_meta);
2387 if ($thumb) $update = array_merge($update, $thumb);
2388 $db->update($this->dd_media, $update, $id);
2389 if (($row[
'file_path'] ??
'') !== $rel) $this->flush_media_by_media_id(
$db, $id);
2390 $by_path[$rel] = array_merge($row, $update);
2391 if (($row[
'file_path'] ??
'') !== $rel) $relocated_ids[$id] = 1;
2395 $thumb = $storage_type ===
'local' ? $this->create_media_thumbnail(
$file, $media_folder, $name, $meta[
'mime']) : array();
2396 $title = pathinfo($name, PATHINFO_FILENAME);
2397 if (!empty($external_meta[
'title'])) $title = (string)$external_meta[
'title'];
2404 'sorter' => $this->next_media_sorter(
$db, 0, $slot),
2408 'file_name' => $name,
2409 'file_path' => $rel,
2410 'mime' => $meta[
'mime'],
2411 'size' => $meta[
'size'],
2412 'width' => $meta[
'width'],
2413 'height' => $meta[
'height'],
2414 'media_type' => $media_type,
2415 'storage_type' => $storage_type,
2416 'media_folder' => $media_folder,
2418 if ($external_meta) $insert = array_merge($insert, $external_meta);
2419 if ($thumb) $insert = array_merge($insert, $thumb);
2420 $id = (
$db->insert($this->dd_media, $insert) === 1) ?
$db->get_insert_id() : 0;
2423 foreach ($existing as $row) {
2424 if (!is_array($row))
continue;
2425 $id = (int)($row[
'id'] ?? 0);
2426 $path = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
2427 if ($id <= 0 || (
int)($row[
'active'] ?? 0) !== 1)
continue;
2428 if (isset($relocated_ids[$id]))
continue;
2429 if (preg_match(
'~^media/(hero|gallery|inline|header|teaser|footer)/~', $path) && !isset($seen[$path])) {
2430 $db->update($this->dd_media, array(
'active' => 0), $id);
2433 if (!isset($seen[$path]) && !$this->relative_file_exists($path)) {
2434 $db->update($this->dd_media, array(
'active' => 0), $id);
2436 if ((
int)($row[
'active'] ?? 0) === 1 && ($this->is_excluded_cms_media_folder((
string)($row[
'media_folder'] ??
'')) || preg_match(
'~^media/module/~', $path))) {
2437 $db->update($this->dd_media, array(
'active' => 0), $id);
2442 private function normalize_media_row($row) {
2443 $row = is_array($row) ? $row : array();
2444 $file_path = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
2445 $row[
'url'] = $this->media_url($row);
2446 $row[
'thumb_url'] = $this->media_url($row,
true);
2447 $row[
'media_type'] = $this->media_type($row);
2448 $row[
'storage_type'] = trim((
string)($row[
'storage_type'] ??
'')) ?: ($row[
'media_type'] ===
'external_video' ?
'external' :
'local');
2449 $row[
'media_folder'] = $this->canonical_media_folder(trim((
string)($row[
'media_folder'] ??
'')) ?: $this->media_folder_from_path((
string)($row[
'file_path'] ??
''), $row[
'media_type']));
2450 if ($row[
'media_type'] ===
'external_video') {
2451 $row[
'url'] = (string)($row[
'embed_url'] ?? $row[
'external_url'] ??
'');
2452 $row[
'thumb_url'] = $this->external_video_thumb_url($row);
2457 private function normalize_usage_row($row) {
2458 $row = is_array($row) ? $row : array();
2459 $row[
'id'] = (int)($row[
'id'] ?? 0);
2460 $row[
'media_id'] = (int)($row[
'media_id'] ?? 0);
2461 $row[
'content_id'] = (int)($row[
'content_id'] ?? 0);
2462 $row[
'folder_id'] = (int)($row[
'folder_id'] ?? 0);
2463 $row[
'content_lng'] = dbxContentMediaUsageScope::language((
string)($row[
'content_lng'] ??
''));
2464 $row[
'slot'] = $this->valid_media_slot($row[
'slot'] ??
'gallery');
2468 private function usage_where($media_id = 0, $content_id = 0, $folder_id = 0, $slot =
'', $content_lng =
'') {
2469 $where =
'active = 1';
2470 if ((
int)$media_id > 0) $where .=
' AND media_id = ' . (int)$media_id;
2471 if ((
int)$content_id > 0) $where .=
' AND content_id = ' . (int)$content_id;
2472 if ((
int)$folder_id > 0) $where .=
' AND folder_id = ' . (int)$folder_id;
2473 if ((
string)$slot !==
'') $where .=
" AND slot = '" . str_replace(
"'",
"''", $this->valid_media_slot($slot)) .
"'";
2474 if ((
int)$content_id > 0 || (
int)$folder_id > 0) {
2475 $where = dbxContentMediaUsageScope::withLanguage($where, $content_lng);
2480 private function next_media_usage_sorter(
$db, $content_id, $folder_id, $slot, $content_lng =
'') {
2481 $where = $this->usage_where(0, $content_id, $folder_id, $slot, $content_lng);
2482 $rows =
$db->select($this->dd_media_usage, $where,
'*',
'sorter,id',
'DESC',
'', 1, 0, 0);
2484 if (is_array(
$rows) && isset(
$rows[0]) && is_array(
$rows[0])) $max = (int)(
$rows[0][
'sorter'] ?? 0);
2485 return sprintf(
'%04d', $max + 10);
2488 private function create_media_usage(
$db, $media_id, $content_id, $folder_id, $slot,
$template =
'', $caption =
'', $settings =
'', $content_lng =
'') {
2489 $media_id = (int)$media_id;
2490 $content_id = (int)$content_id;
2491 $folder_id = (int)$folder_id;
2492 $slot = $this->valid_media_slot($slot);
2493 $content_lng = dbxContentMediaUsageScope::language($content_lng);
2494 if ($media_id <= 0 || ($content_id <= 0 && $folder_id <= 0))
return 0;
2495 $existingWhere =
'media_id = ' . $media_id
2496 .
' AND content_id = ' . $content_id
2497 .
' AND folder_id = ' . $folder_id
2498 .
" AND slot = '" . str_replace(
"'",
"''", $slot) .
"'";
2499 $existingWhere = dbxContentMediaUsageScope::withLanguage($existingWhere, $content_lng);
2500 $existing =
$db->select($this->dd_media_usage, $existingWhere,
'*',
'active DESC,id DESC',
'ASC',
'', 1, 0, 0);
2501 if (is_array($existing) && isset($existing[0][
'id']) && (
int)($existing[0][
'active'] ?? 0) === 1) {
2502 return (
int)$existing[0][
'id'];
2504 if ($slot ===
'hero') {
2505 if ($content_id > 0)
$db->update($this->dd_media_usage, array(
'active' => 0), dbxContentMediaUsageScope::withLanguage(
"content_id = " . $content_id .
" AND slot = 'hero' AND active = 1", $content_lng), 0, 1, 1, 0);
2506 elseif ($folder_id > 0)
$db->update($this->dd_media_usage, array('active' => 0), dbxContentMediaUsageScope::withLanguage(
"folder_id = " . $folder_id .
" AND slot = 'hero' AND active = 1", $content_lng), 0, 1, 1, 0);
2508 if (is_array($existing) && isset($existing[0]['
id'])) {
2509 $existingId = (int)$existing[0][
'id'];
2510 $db->update($this->dd_media_usage, array(
2512 'template' => $this->clean_text(
$template, 80),
2513 'caption' => $this->clean_text($caption, 0),
2514 'settings' => $this->clean_text($settings, 0),
2515 ), $existingId, 0, 1, 1, 0);
2518 return (
$db->insert($this->dd_media_usage, array(
2520 'media_id' => $media_id,
2521 'content_id' => $content_id,
2522 'folder_id' => $folder_id,
2523 'content_lng' => $content_lng,
2525 'sorter' => $this->next_media_usage_sorter(
$db, $content_id, $folder_id, $slot, $content_lng),
2526 'template' => $this->clean_text(
$template, 80),
2527 'caption' => $this->clean_text($caption, 0),
2528 'settings' => $this->clean_text($settings, 0),
2529 )) === 1) ?
$db->get_insert_id() : 0;
2532 private function get_media_process_state(
$token) {
2533 $token = preg_replace(
'/[^A-Za-z0-9_-]+/',
'', (
string)
$token);
2534 if (
$token ===
'')
return array();
2535 return $_SESSION[
'dbx'][
'dbxContent_admin'][
'media_process'][
$token] ?? array();
2538 private function set_media_process_state(
$token, array
$state) {
2539 $token = preg_replace(
'/[^A-Za-z0-9_-]+/',
'', (
string)
$token);
2540 if (
$token ===
'')
return;
2542 if (!isset(
$_SESSION[
'dbx'][
'dbxContent_admin']) || !is_array(
$_SESSION[
'dbx'][
'dbxContent_admin']))
$_SESSION[
'dbx'][
'dbxContent_admin'] = array();
2543 if (!isset(
$_SESSION[
'dbx'][
'dbxContent_admin'][
'media_process']) || !is_array(
$_SESSION[
'dbx'][
'dbxContent_admin'][
'media_process']))
$_SESSION[
'dbx'][
'dbxContent_admin'][
'media_process'] = array();
2547 private function slot_from_media_rel($rel,
$fallback =
'gallery') {
2548 $rel = ltrim(str_replace(
'\\',
'/', (
string)$rel),
'/');
2549 if (preg_match(
'~^media/(hero|gallery|inline|header|teaser|footer)/~', $rel, $m)) {
2550 return $this->valid_media_slot($m[1],
$fallback);
2552 return $this->valid_media_slot(
$fallback);
2555 private function build_media_process_state(
$db,
$token) {
2556 $rows =
$db->select($this->dd_media,
'',
'*',
'id');
2560 $referenced_thumbs = array();
2562 $needs_content_cleanup =
false;
2564 foreach (
$rows as $row) {
2565 if (!is_array($row))
continue;
2566 $id = (int)($row[
'id'] ?? 0);
2567 $path = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
2568 if ($path !==
'') $by_path[$path] = $id;
2569 $thumb = ltrim(str_replace(
'\\',
'/', (
string)($row[
'thumb_file_path'] ??
'')),
'/');
2570 if ($thumb !==
'') $referenced_thumbs[$thumb] = 1;
2571 $is_external = strtolower((
string)($row[
'storage_type'] ??
'')) ===
'external'
2572 || $this->media_type($row) ===
'external_video';
2573 $is_excluded = $this->is_excluded_cms_media_folder((
string)($row[
'media_folder'] ??
''))
2574 || strpos($path,
'media/module/') === 0;
2575 $is_legacy_path = (bool)preg_match(
'~^media/(hero|gallery|inline|header|teaser|footer)/~', $path);
2576 $file_exists = $this->media_file_exists($row);
2577 $needs_record_check = $is_legacy_path
2578 || (!$is_external && !$is_excluded && (((int)($row[
'active'] ?? 0) !== 1 && $file_exists) || ((int)($row[
'active'] ?? 0) === 1 && !$file_exists)))
2579 || ($is_external && (
int)($row[
'active'] ?? 0) === 1 && !$file_exists);
2580 if ($id > 0 && $needs_record_check) {
2581 $tasks[] = array(
'type' =>
'record',
'id' => $id);
2582 if (!$file_exists) $needs_content_cleanup =
true;
2585 && !$needs_record_check
2586 && (
int)($row[
'active'] ?? 0) === 1
2587 && $this->media_file_exists($row)
2588 && $this->media_thumbnail_supported($row)
2589 && preg_match(
'~^media/(img|video|file|module)/~', $path)
2590 && !$this->media_thumbnail_is_current($row)) {
2591 $tasks[] = array(
'type' =>
'thumb',
'id' => $id);
2593 if ($id > 0 && (
int)($row[
'active'] ?? 0) !== 1) $needs_content_cleanup =
true;
2596 foreach ($this->collect_media_files() as $rel =>
$file) {
2597 if (!isset($by_path[$rel])) {
2598 $tasks[] = array(
'type' =>
'import',
'rel' => $rel,
'slot' =>
$file[
'slot']);
2602 foreach ($this->collect_media_thumb_files() as $rel =>
$file) {
2603 if (!isset($referenced_thumbs[$rel])) {
2604 $tasks[] = array(
'type' =>
'delete_thumb',
'rel' => $rel);
2608 if ($needs_content_cleanup || $this->content_inline_cleanup_needed(
$db)) {
2609 $tasks[] = array(
'type' =>
'content_cleanup');
2612 if ($this->content_inline_placeholder_repair_needed(
$db)) {
2613 $tasks[] = array(
'type' =>
'content_placeholder_repair');
2616 $tasks[] = array(
'type' =>
'structured_reference_cleanup');
2617 $tasks[] = array(
'type' =>
'usage_reconcile');
2618 $tasks[] = array(
'type' =>
'media_record_purge');
2620 $cache_content_ids = $this->content_media_reference_ids(
$db);
2621 if ($cache_content_ids) {
2622 $tasks[] = array(
'type' =>
'content_cache_flush',
'ids' => $cache_content_ids);
2627 $tasks[] = array(
'type' =>
'database_optimize');
2631 'proc_type' =>
'media_maintenance',
2632 'status' => empty($tasks) ?
'finished' :
'running',
2633 'phase' =>
'media_prepare',
2634 'message' => empty($tasks) ?
'Keine Wartung notwendig.' :
'Medienwartung vorbereitet.',
2637 'total' => count($tasks),
2638 'created_thumbs' => 0,
2639 'imported_media' => 0,
2640 'relinked_media' => 0,
2641 'deleted_thumbs' => 0,
2642 'deactivated_media' => 0,
2643 'cleaned_inline_refs' => 0,
2644 'cleaned_content_pages' => 0,
2645 'repaired_inline_refs' => 0,
2646 'flushed_content_pages' => 0,
2647 'removed_orphan_usage' => 0,
2648 'usage_rows_analyzed' => 0,
2649 'actual_media_references' => 0,
2650 'usage_rows_added' => 0,
2651 'usage_rows_updated' => 0,
2652 'usage_rows_removed' => 0,
2653 'usage_inactive_removed' => 0,
2654 'usage_stale_removed' => 0,
2655 'usage_duplicate_removed' => 0,
2656 'cleaned_structured_refs' => 0,
2657 'purged_media_records' => 0,
2658 'database_optimized' => 0,
2660 'percent' => empty($tasks) ? 100 : 0,
2661 'step_percent' => empty($tasks) ? 100 : 0,
2662 'updated_at' => date(
'H:i:s'),
2666 private function update_media_process_percent(array &
$state) {
2667 $total = max(1, (
int)(
$state[
'total'] ?? 0));
2668 $pos = max(0, min($total, (
int)(
$state[
'pos'] ?? 0)));
2669 $percent = (int)floor(($pos / $total) * 100);
2670 if ((
$state[
'status'] ??
'') ===
'finished') $percent = 100;
2671 $state[
'percent'] = max(0, min(100, $percent));
2675 private function media_process_message(array
$state, $current =
'') {
2677 if ((
int)(
$state[
'created_thumbs'] ?? 0) > 0) $parts[] =
'Thumbs +' . (int)
$state[
'created_thumbs'];
2678 if ((
int)(
$state[
'imported_media'] ?? 0) > 0) $parts[] =
'Import +' . (int)
$state[
'imported_media'];
2679 if ((
int)(
$state[
'relinked_media'] ?? 0) > 0) $parts[] =
'Zuordnung repariert ' . (int)
$state[
'relinked_media'];
2680 if ((
int)(
$state[
'deleted_thumbs'] ?? 0) > 0) $parts[] =
'Thumbs weg ' . (int)
$state[
'deleted_thumbs'];
2681 if ((
int)(
$state[
'deactivated_media'] ?? 0) > 0) $parts[] =
'Medien aus ' . (int)
$state[
'deactivated_media'];
2682 if ((
int)(
$state[
'cleaned_inline_refs'] ?? 0) > 0) $parts[] =
'Content bereinigt ' . (int)
$state[
'cleaned_inline_refs'];
2683 if ((
int)(
$state[
'repaired_inline_refs'] ?? 0) > 0) $parts[] =
'Inline repariert ' . (int)
$state[
'repaired_inline_refs'];
2684 if ((
int)(
$state[
'flushed_content_pages'] ?? 0) > 0) $parts[] =
'Cache aktualisiert ' . (int)
$state[
'flushed_content_pages'];
2685 if ((
int)(
$state[
'usage_rows_added'] ?? 0) > 0) $parts[] =
'Nutzung ergaenzt ' . (int)
$state[
'usage_rows_added'];
2686 if ((
int)(
$state[
'usage_rows_updated'] ?? 0) > 0) $parts[] =
'Nutzung korrigiert ' . (int)
$state[
'usage_rows_updated'];
2687 if ((
int)(
$state[
'usage_rows_removed'] ?? 0) > 0) $parts[] =
'Ungueltige Zuordnungen entfernt ' . (int)
$state[
'usage_rows_removed'];
2688 if ((
int)(
$state[
'cleaned_structured_refs'] ?? 0) > 0) $parts[] =
'Defekte Inhaltsverweise bereinigt ' . (int)
$state[
'cleaned_structured_refs'];
2689 if ((
int)(
$state[
'purged_media_records'] ?? 0) > 0) $parts[] =
'Ungueltige Medien-Datensaetze entfernt ' . (int)
$state[
'purged_media_records'];
2690 if ((
int)(
$state[
'database_optimized'] ?? 0) > 0) $parts[] =
'Datenbanken optimiert ' . (int)
$state[
'database_optimized'];
2691 if ((
int)(
$state[
'errors'] ?? 0) > 0) $parts[] =
'Fehler ' . (int)
$state[
'errors'];
2692 $msg = empty($parts) ?
'Keine Aenderungen.' : implode(
' | ', $parts);
2693 if ($current !==
'') $msg .=
' | ' . $current;
2697 private function content_inline_cleanup_needed(
$db) {
2698 foreach ($this->ordered_media_languages() as
$lng) {
2699 $rows =
$db->select(dbxContentLng::ddContent(
$lng),
"content LIKE '%dbx_mid=%' OR content LIKE '%data-cms-media-id=%'",
'id,content',
'id');
2700 if (!is_array(
$rows))
continue;
2701 foreach (
$rows as $row) {
2702 if (!is_array($row))
continue;
2703 foreach ($this->inline_media_ids($row[
'content'] ??
'') as $id) {
2704 $media =
$db->select1($this->dd_media, $id);
2705 if (!is_array(
$media) || (
int)(
$media[
'active'] ?? 0) !== 1 || !$this->media_file_exists(
$media))
return true;
2712 private function content_media_reference_ids(
$db) {
2714 $rows =
$db->select(dbxContentLng::ddContent(),
"content LIKE '%dbx_mid=%' OR content LIKE '%data-cms-media-id=%'",
'id,content',
'id');
2715 if (!is_array(
$rows))
return $ids;
2716 foreach (
$rows as $row) {
2717 if (!is_array($row))
continue;
2718 $content_id = (int)($row[
'id'] ?? 0);
2719 if ($content_id <= 0)
continue;
2720 if (!$this->inline_media_ids($row[
'content'] ??
''))
continue;
2721 $ids[$content_id] = $content_id;
2723 return array_values($ids);
2726 private function content_inline_placeholder_repair_needed(
$db) {
2727 foreach (dbxContentLngSync::accessibleLngs() as
$lng) {
2728 $rows =
$db->select(dbxContentLng::ddContent((
string)
$lng),
"content LIKE '%dbx-cms-inline-media-missing%'",
'id,content',
'id');
2729 if (!is_array(
$rows))
continue;
2730 foreach (
$rows as $row) {
2731 if (!is_array($row))
continue;
2732 if (preg_match(
'/\bdbx-cms-inline-media-missing\b/i', (
string)($row[
'content'] ??
'')))
return true;
2738 private function repair_content_inline_placeholders(
$db) {
2741 $prev_lng = dbxContentLng::current();
2743 foreach (dbxContentLngSync::accessibleLngs() as
$lng) {
2745 dbx()->set_system_var(
'dbx_lng',
$lng);
2746 $dd = dbxContentLng::ddContent(
$lng);
2747 $rows =
$db->select(
$dd,
"content LIKE '%dbx-cms-inline-media-missing%'",
'id,folder,content',
'id');
2748 if (!is_array(
$rows))
continue;
2750 foreach (
$rows as $row) {
2751 if (!is_array($row))
continue;
2752 $content_id = (int)($row[
'id'] ?? 0);
2753 if ($content_id <= 0)
continue;
2754 $html = (string)($row[
'content'] ??
'');
2755 if (stripos(
$html,
'dbx-cms-inline-media-missing') ===
false)
continue;
2758 $clean = preg_replace_callback(
2759 '/<(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',
2760 function($m) use (
$db, &$changed) {
2761 $block = (string)($m[0] ??
'');
2762 if (!preg_match(
'/\bdata-cms-media-id=["\']?([0-9]+)/i', $block, $id_match))
return $block;
2763 $id = (int)$id_match[1];
2764 if ($id <= 0)
return $block;
2765 $media =
$db->select1($this->dd_media, $id);
2766 if (!is_array(
$media) || (
int)(
$media[
'active'] ?? 0) !== 1 || !$this->media_file_exists(
$media))
return $block;
2767 $replacement = $this->inline_media_embed_html(
$media);
2768 if ($replacement ===
'')
return $block;
2770 return $replacement;
2774 if ($clean ===
null || $clean ===
$html || $changed <= 0)
continue;
2775 $ok =
$db->update(
$dd, array(
'content' => $clean), $content_id, 0, 0, 0, 0);
2776 if (
$ok < 0)
continue;
2777 $this->sync_inline_media_usage(
$db, $content_id, $clean, (
int)($row[
'folder'] ?? 0));
2778 $this->flush_saved_page_cache(
$db, $content_id);
2784 dbx()->set_system_var(
'dbx_lng', $prev_lng);
2786 return array(
'pages' =>
$pages,
'refs' => $refs);
2789 private function remove_inline_media_ids_from_html(
$html, array $ids, &$removed = 0) {
2792 $ids = array_values(array_unique(array_filter(array_map(
'intval', $ids))));
2793 if (!$ids || trim(
$html) ===
'')
return $html;
2795 if (class_exists(
'\DOMDocument')) {
2796 $doc = new \DOMDocument(
'1.0',
'UTF-8');
2797 $prev = libxml_use_internal_errors(
true);
2798 $wrapped =
'<div id="dbx-cms-clean-root">' .
$html .
'</div>';
2799 $ok = $doc->loadHTML(
'<?xml encoding="utf-8" ?>' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
2800 libxml_clear_errors();
2801 libxml_use_internal_errors($prev);
2803 $xpath = new \DOMXPath($doc);
2804 $root_nodes = $xpath->query(
"//*[@id='dbx-cms-clean-root']");
2805 $root = $root_nodes && $root_nodes->length ? $root_nodes->item(0) :
null;
2806 foreach ($ids as $id) {
2807 $query =
"//*[@data-cms-media-id='" . $id .
"' or contains(@src,'dbx_mid=" . $id .
"') or contains(@href,'dbx_mid=" . $id .
"')]";
2808 $nodes = $xpath->query($query);
2811 foreach ($nodes as $node) {
2813 for ($p = $node; $p && $p !==
$root; $p = $p->parentNode) {
2814 if ($p->nodeType !== XML_ELEMENT_NODE)
continue;
2815 $tag = strtolower($p->nodeName);
2816 $class =
' ' . (string)$p->getAttribute(
'class') .
' ';
2817 if ($tag ===
'figure' || strpos(
$class,
' dbx-cms-inline-media ') !==
false) {
2833 foreach (iterator_to_array(
$root->childNodes) as $child) {
2834 $out .= $doc->saveHTML($child);
2841 foreach ($ids as $id) {
2843 $id_re = preg_quote((
string)$id,
'~');
2844 $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);
2845 $html = preg_replace(
'~<(img|video|iframe|source)\b[^>]*(?:dbx_mid=' . $id_re .
'\b|data-cms-media-id=["\']?' . $id_re .
'\b)[^>]*>(?:</\1>)?~i',
'',
$html);
2846 if (
$html !== $before) $removed++;
2851 private function clean_content_inline_media(
$db) {
2854 $previous_lng = (string)
dbx()->get_system_var(
'dbx_lng', dbxContentLngSync::masterLng());
2856 foreach ($this->ordered_media_languages() as
$lng) {
2857 dbx()->set_system_var(
'dbx_lng',
$lng);
2858 $content_dd = dbxContentLng::ddContent(
$lng);
2859 $rows =
$db->select($content_dd,
"content LIKE '%dbx_mid=%' OR content LIKE '%data-cms-media-id=%'",
'id,folder,content',
'id');
2860 if (!is_array(
$rows))
continue;
2861 foreach (
$rows as $row) {
2862 if (!is_array($row))
continue;
2863 $content_id = (int)($row[
'id'] ?? 0);
2864 if ($content_id <= 0)
continue;
2866 foreach ($this->inline_media_ids($row[
'content'] ??
'') as $id) {
2867 $media =
$db->select1($this->dd_media, $id);
2868 if (!is_array(
$media) || (
int)(
$media[
'active'] ?? 0) !== 1 || !$this->media_file_exists(
$media)) $bad[] = $id;
2870 if (!$bad)
continue;
2872 $clean = $this->remove_inline_media_ids_from_html((
string)($row[
'content'] ??
''), $bad, $removed);
2873 if ($clean !== (
string)($row[
'content'] ??
'')) {
2874 $db->update($content_dd, array(
'content' => $clean), $content_id);
2875 $this->sync_inline_media_usage(
$db, $content_id, $clean, (
int)($row[
'folder'] ?? 0));
2876 $this->flush_saved_page_cache(
$db, $content_id);
2878 $refs += max($removed, count($bad));
2883 dbx()->set_system_var(
'dbx_lng', $previous_lng);
2885 return array(
'pages' =>
$pages,
'refs' => $refs);
2888 private function import_media_file(
$db, $rel, $slot, &$relinked =
null) {
2890 $rel = ltrim(str_replace(
'\\',
'/', (
string)$rel),
'/');
2891 $slot = $this->valid_media_slot($slot);
2892 $file = $this->file_from_media_rel($rel);
2895 $name = basename(
$file);
2896 $meta = $this->media_file_meta(
$file, $name);
2897 $media_folder = $this->media_folder_from_path($rel, $meta[
'media_type']);
2898 $existing =
$db->select($this->dd_media,
'',
'*',
'id');
2899 if (!is_array($existing)) $existing = array();
2900 $relocated = $this->find_relocated_media_row($existing, $rel, $name, $meta, $media_folder, $this->active_media_usage_map(
$db));
2901 if (is_array($relocated)) {
2902 $id = (int)($relocated[
'id'] ?? 0);
2904 $thumb = (empty($relocated[
'thumb_file_path']) || !$this->media_thumb_exists($relocated))
2905 ? $this->create_media_thumbnail(
$file, $media_folder, $name, $meta[
'mime'])
2909 'file_path' => $rel,
2910 'file_name' => $name,
2911 'media_folder' => $media_folder,
2912 'mime' => $meta[
'mime'],
2913 'size' => $meta[
'size'],
2914 'width' => $meta[
'width'],
2915 'height' => $meta[
'height'],
2916 'media_type' => $meta[
'media_type'],
2917 'storage_type' =>
'local',
2919 if ($thumb) $update = array_merge($update, $thumb);
2920 $db->update($this->dd_media, $update, $id);
2921 $this->flush_media_by_media_id(
$db, $id);
2927 $title = pathinfo($name, PATHINFO_FILENAME);
2934 'sorter' => $this->next_media_sorter(
$db, 0, $slot),
2938 'file_name' => $name,
2939 'file_path' => $rel,
2940 'mime' => $meta[
'mime'],
2941 'size' => $meta[
'size'],
2942 'width' => $meta[
'width'],
2943 'height' => $meta[
'height'],
2944 'media_type' => $meta[
'media_type'],
2945 'media_folder' => $media_folder,
2946 'storage_type' =>
'local',
2948 $thumb = $this->create_media_thumbnail(
$file, $media_folder, $name, $meta[
'mime']);
2949 if ($thumb) $insert = array_merge($insert, $thumb);
2950 return (
$db->insert($this->dd_media, $insert) === 1) ?
$db->get_insert_id() : 0;
2953 private function run_media_process_task(
$db, array &
$state, array $task) {
2954 $type = (string)($task[
'type'] ??
'');
2956 if ($type ===
'delete_thumb') {
2957 $rel = ltrim(str_replace(
'\\',
'/', (
string)($task[
'rel'] ??
'')),
'/');
2958 $file = $this->file_from_media_rel($rel);
2960 $state[
'deleted_thumbs'] = (int)(
$state[
'deleted_thumbs'] ?? 0) + 1;
2962 $state[
'phase'] =
'media_cleanup';
2963 return basename($rel);
2966 if ($type ===
'thumb') {
2967 $id = (int)($task[
'id'] ?? 0);
2968 $row = $id > 0 ?
$db->select1($this->dd_media, $id) : array();
2969 if (!is_array($row))
return '#' . $id;
2970 $rel = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
2971 if (!$this->media_thumbnail_supported($row)) {
2972 $state[
'phase'] =
'media_thumbs';
2973 return basename($rel);
2975 $file = $this->file_from_media_rel($rel);
2978 return basename($rel);
2980 $name = basename(
$file);
2981 $meta = $this->media_file_meta(
$file, $name);
2982 $media_folder = $this->media_folder_from_path($rel, $meta[
'media_type']);
2983 $old_thumb = $this->source_thumb_file($row);
2984 $thumb = $this->create_media_thumbnail(
$file, $media_folder, $name, $meta[
'mime']);
2986 $db->update($this->dd_media, $thumb, $id);
2987 $state[
'created_thumbs'] = (int)(
$state[
'created_thumbs'] ?? 0) + 1;
2988 $new_thumb = $this->file_from_media_rel((
string)($thumb[
'thumb_file_path'] ??
''));
2989 if ($old_thumb !==
'' && $old_thumb !== $new_thumb && is_file($old_thumb) && @unlink($old_thumb)) {
2990 $state[
'deleted_thumbs'] = (int)(
$state[
'deleted_thumbs'] ?? 0) + 1;
2995 $state[
'phase'] =
'media_thumbs';
2996 return basename($rel);
2999 if ($type ===
'import') {
3000 $rel = ltrim(str_replace(
'\\',
'/', (
string)($task[
'rel'] ??
'')),
'/');
3001 $slot = $this->valid_media_slot($task[
'slot'] ?? $this->slot_from_media_rel($rel));
3003 $id = $this->import_media_file(
$db, $rel, $slot, $relinked);
3006 $state[
'relinked_media'] = (int)(
$state[
'relinked_media'] ?? 0) + 1;
3008 $state[
'imported_media'] = (int)(
$state[
'imported_media'] ?? 0) + 1;
3010 $row =
$db->select1($this->dd_media, $id);
3011 if (is_array($row) && !empty($row[
'thumb_file_path'])) {
3012 $state[
'created_thumbs'] = (int)(
$state[
'created_thumbs'] ?? 0) + 1;
3017 $state[
'phase'] =
'media_import';
3018 return basename($rel);
3021 if ($type ===
'content_cleanup') {
3022 $done = $this->clean_content_inline_media(
$db);
3023 $state[
'cleaned_inline_refs'] = (int)(
$state[
'cleaned_inline_refs'] ?? 0) + (int)($done[
'refs'] ?? 0);
3024 $state[
'cleaned_content_pages'] = (int)(
$state[
'cleaned_content_pages'] ?? 0) + (int)($done[
'pages'] ?? 0);
3025 $state[
'phase'] =
'content_cleanup';
3029 if ($type ===
'content_placeholder_repair') {
3030 $done = $this->repair_content_inline_placeholders(
$db);
3031 $state[
'repaired_inline_refs'] = (int)(
$state[
'repaired_inline_refs'] ?? 0) + (int)($done[
'refs'] ?? 0);
3032 $state[
'cleaned_content_pages'] = (int)(
$state[
'cleaned_content_pages'] ?? 0) + (int)($done[
'pages'] ?? 0);
3033 $state[
'phase'] =
'content_repair';
3037 if ($type ===
'content_cache_flush') {
3038 $ids = is_array($task[
'ids'] ??
null) ? $task[
'ids'] : array();
3040 foreach ($ids as $content_id) {
3041 $content_id = (int)$content_id;
3042 if ($content_id <= 0)
continue;
3043 $this->flush_saved_page_cache(
$db, $content_id);
3046 $state[
'flushed_content_pages'] = (int)(
$state[
'flushed_content_pages'] ?? 0) +
$count;
3047 $state[
'phase'] =
'content_cache';
3051 if ($type ===
'structured_reference_cleanup') {
3052 $done = $this->cleanup_invalid_structured_media_references(
$db);
3053 $state[
'cleaned_structured_refs'] = (int)(
$state[
'cleaned_structured_refs'] ?? 0) + (int)($done[
'refs'] ?? 0);
3054 $state[
'cleaned_content_pages'] = (int)(
$state[
'cleaned_content_pages'] ?? 0) + (int)($done[
'pages'] ?? 0);
3055 $state[
'phase'] =
'media_reference_cleanup';
3056 return 'Inhaltsverweise';
3059 if ($type ===
'usage_reconcile') {
3060 $done = $this->reconcile_media_usage(
$db);
3061 $state[
'usage_rows_analyzed'] = (int)(
$state[
'usage_rows_analyzed'] ?? 0) + (int)($done[
'analyzed'] ?? 0);
3062 $state[
'actual_media_references'] = (int)($done[
'actual_references'] ?? 0);
3063 $state[
'usage_rows_added'] = (int)(
$state[
'usage_rows_added'] ?? 0) + (int)($done[
'added'] ?? 0);
3064 $state[
'usage_rows_updated'] = (int)(
$state[
'usage_rows_updated'] ?? 0) + (int)($done[
'updated'] ?? 0);
3065 $state[
'usage_rows_removed'] = (int)(
$state[
'usage_rows_removed'] ?? 0) + (int)($done[
'removed'] ?? 0);
3066 $state[
'removed_orphan_usage'] = (int)(
$state[
'removed_orphan_usage'] ?? 0) + (int)($done[
'removed'] ?? 0);
3067 $reasons = is_array($done[
'reasons'] ??
null) ? $done[
'reasons'] : array();
3068 $state[
'usage_inactive_removed'] = (int)(
$state[
'usage_inactive_removed'] ?? 0) + (int)($reasons[
'inactive'] ?? 0);
3069 $state[
'usage_duplicate_removed'] = (int)(
$state[
'usage_duplicate_removed'] ?? 0) + (int)($reasons[
'duplicate'] ?? 0);
3070 $state[
'usage_stale_removed'] = (int)(
$state[
'usage_stale_removed'] ?? 0)
3071 + max(0, (
int)($done[
'removed'] ?? 0) - (
int)($reasons[
'inactive'] ?? 0) - (
int)($reasons[
'duplicate'] ?? 0));
3072 if ((
int)($done[
'removed'] ?? 0) < (
int)($done[
'planned_removed'] ?? 0)) {
3073 $state[
'errors'] = (int)(
$state[
'errors'] ?? 0) + ((int)$done[
'planned_removed'] - (int)$done[
'removed']);
3075 $state[
'phase'] =
'media_usage_reconcile';
3076 return 'Nutzungsanalyse';
3079 if ($type ===
'media_record_purge') {
3080 $done = $this->purge_invalid_media_records(
$db);
3081 $state[
'purged_media_records'] = (int)(
$state[
'purged_media_records'] ?? 0) + (int)($done[
'removed'] ?? 0);
3082 $state[
'deleted_thumbs'] = (int)(
$state[
'deleted_thumbs'] ?? 0) + (int)($done[
'deleted_thumbs'] ?? 0);
3083 if ((
int)($done[
'removed'] ?? 0) < (
int)($done[
'found'] ?? 0)) {
3084 $state[
'errors'] = (int)(
$state[
'errors'] ?? 0) + ((int)$done[
'found'] - (int)$done[
'removed']);
3086 $state[
'phase'] =
'media_record_purge';
3087 return 'Medien-Datenbank';
3090 if ($type ===
'usage_cleanup') {
3091 $done = $this->cleanup_orphan_media_usage(
$db);
3092 $state[
'removed_orphan_usage'] = (int)(
$state[
'removed_orphan_usage'] ?? 0) + (int)($done[
'removed'] ?? 0);
3093 if ((
int)($done[
'removed'] ?? 0) < (
int)($done[
'found'] ?? 0)) {
3094 $state[
'errors'] = (int)(
$state[
'errors'] ?? 0) + ((int)$done[
'found'] - (int)$done[
'removed']);
3096 $state[
'phase'] =
'media_usage_cleanup';
3097 return 'Zuordnungen';
3100 if ($type ===
'database_optimize') {
3102 foreach (array($this->dd_media_usage, dbxContentLng::ddContent()) as
$dd) {
3103 if ((
int)
$db->optimize_tab(
$dd) === 1) $optimized++;
3104 else $state[
'errors'] = (int)(
$state[
'errors'] ?? 0) + 1;
3106 $state[
'database_optimized'] = $optimized;
3107 $state[
'phase'] =
'database_optimize';
3108 return 'Datenbanken';
3111 if ($type ===
'record') {
3112 $id = (int)($task[
'id'] ?? 0);
3113 $row = $id > 0 ?
$db->select1($this->dd_media, $id) : array();
3114 if (!is_array($row))
return '#' . $id;
3116 if (strtolower((
string)($row[
'storage_type'] ??
'')) ===
'external' || $this->media_type($row) ===
'external_video') {
3117 if (!$this->media_file_exists($row) && (
int)($row[
'active'] ?? 0) !== 0) {
3118 $db->update($this->dd_media, array(
'active' => 0), $id);
3119 $state[
'deactivated_media'] = (int)(
$state[
'deactivated_media'] ?? 0) + 1;
3121 $state[
'phase'] =
'media_cleanup';
3122 return (
string)($row[
'title'] ?? (
'#' . $id));
3125 $rel = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
3126 $file = $this->file_from_media_rel($rel);
3128 $thumb = $this->source_thumb_file($row);
3129 if ($thumb !==
'' && @unlink($thumb)) {
3130 $state[
'deleted_thumbs'] = (int)(
$state[
'deleted_thumbs'] ?? 0) + 1;
3132 if ((
int)($row[
'active'] ?? 0) !== 0 || !empty($row[
'thumb_file_path'])) {
3133 $db->update($this->dd_media, array(
3135 'thumb_file_path' =>
'',
3137 'thumb_height' => 0,
3139 $state[
'deactivated_media'] = (int)(
$state[
'deactivated_media'] ?? 0) + 1;
3141 $state[
'phase'] =
'media_cleanup';
3142 return basename($rel);
3145 $slot = $this->slot_from_media_rel($rel, (
string)($row[
'slot'] ??
'gallery'));
3146 $name = basename(
$file);
3147 $meta = $this->media_file_meta(
$file, $name);
3149 if ((
int)($row[
'active'] ?? 0) !== 1
3150 || (
string)($row[
'slot'] ??
'') !== $slot
3151 || (
string)($row[
'mime'] ??
'') !== $meta[
'mime']
3152 || (
int)($row[
'size'] ?? 0) !== $meta[
'size']
3153 || (
string)($row[
'media_type'] ??
'') !== $meta[
'media_type']) {
3158 'mime' => $meta[
'mime'],
3159 'size' => $meta[
'size'],
3160 'width' => $meta[
'width'],
3161 'height' => $meta[
'height'],
3162 'media_type' => $meta[
'media_type'],
3166 if (!$this->media_thumbnail_is_current($row)) {
3167 $old_thumb = $this->source_thumb_file($row);
3168 $thumb = $this->create_media_thumbnail(
$file, $slot, $name, $meta[
'mime']);
3170 $update = array_merge($update, $thumb);
3171 $state[
'created_thumbs'] = (int)(
$state[
'created_thumbs'] ?? 0) + 1;
3172 $new_thumb = $this->file_from_media_rel((
string)($thumb[
'thumb_file_path'] ??
''));
3173 if ($old_thumb !==
'' && $old_thumb !== $new_thumb && is_file($old_thumb) && @unlink($old_thumb)) {
3174 $state[
'deleted_thumbs'] = (int)(
$state[
'deleted_thumbs'] ?? 0) + 1;
3179 if (!empty($update))
$db->update($this->dd_media, $update, $id);
3180 $state[
'phase'] =
'media_thumbs';
3181 return basename($rel);
3187 private function tick_media_process(
$db, array &
$state) {
3188 if ((
$state[
'status'] ??
'') !==
'running')
return;
3189 $started = microtime(
true);
3193 $total = (int)(
$state[
'total'] ?? 0);
3195 while ((
int)(
$state[
'pos'] ?? 0) < $total && $processed < $limit && (microtime(
true) - $started) < 3.2) {
3196 $idx = (int)
$state[
'pos'];
3197 $task = is_array(
$state[
'tasks'][$idx] ??
null) ?
$state[
'tasks'][$idx] : array();
3198 $current = $this->run_media_process_task(
$db,
$state, $task);
3199 $state[
'pos'] = $idx + 1;
3203 if ((
int)(
$state[
'pos'] ?? 0) >= $total) {
3204 $state[
'status'] =
'finished';
3205 $state[
'phase'] =
'media_done';
3206 $state[
'tasks'] = array();
3210 $this->update_media_process_percent(
$state);
3211 $state[
'message'] = $this->media_process_message(
$state, $current);
3212 if ((
$state[
'status'] ??
'') ===
'finished')
$state[
'message'] =
'Fertig. ' .
$state[
'message'];
3213 $state[
'updated_at'] = date(
'H:i:s');
3216 private function render_media_process(array
$state, $next_url) {
3217 $status = (string)(
$state[
'status'] ??
'running');
3218 $percent = max(0, min(100, (
int)(
$state[
'percent'] ?? 0)));
3219 $step_percent = max(0, min(100, (
int)(
$state[
'step_percent'] ?? $percent)));
3220 $status_labels = array(
3221 'running' =>
'Laeuft',
3222 'paused' =>
'Angehalten',
3223 'canceled' =>
'Abgebrochen',
3224 'finished' =>
'Fertig',
3225 'error' =>
'Fehler',
3227 $status_class = $status ===
'finished' ?
'bg-success' : ($status ===
'error' || $status ===
'canceled' ?
'bg-danger' : ($status ===
'paused' ?
'bg-warning text-dark' :
'bg-primary'));
3228 $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'));
3230 $restart_url = $this->append_url_params($next_url, array(
'reset' => 1,
'proc_cmd' =>
'restart'));
3231 $cancel_url = $this->append_url_params($next_url, array(
'proc_cmd' =>
'cancel'));
3232 $resume_url = $this->append_url_params($next_url, array(
'proc_cmd' =>
'resume'));
3233 $pause_url = $this->append_url_params($next_url, array(
'proc_cmd' =>
'pause'));
3234 $autostart = ($status ===
'running' && $next_url !==
'') ? 1 : 0;
3235 $target_id =
'dbx_cms_media_process_' . substr(md5(
$token ?:
'media'), 0, 14);
3236 $report =
'<div class="dbx-cms-media-maintenance-report" aria-label="Ergebnis der Medienwartung">'
3237 .
'<div><strong>' . (int)(
$state[
'usage_rows_analyzed'] ?? 0) .
'</strong><span>DB-Zuordnungen geprueft</span></div>'
3238 .
'<div><strong>' . (int)(
$state[
'actual_media_references'] ?? 0) .
'</strong><span>Echte Verwendungen erkannt</span></div>'
3239 .
'<div><strong>' . (int)(
$state[
'usage_rows_added'] ?? 0) .
'</strong><span>Zuordnungen ergaenzt</span></div>'
3240 .
'<div><strong>' . (int)(
$state[
'usage_rows_updated'] ?? 0) .
'</strong><span>Zuordnungen korrigiert</span></div>'
3241 .
'<div><strong>' . (int)(
$state[
'usage_rows_removed'] ?? 0) .
'</strong><span>Alt-/Fehleintraege entfernt</span></div>'
3242 .
'<div><strong>' . (int)(
$state[
'purged_media_records'] ?? 0) .
'</strong><span>Medien-Datensaetze entfernt</span></div>'
3243 .
'<div><strong>' . (int)(
$state[
'cleaned_structured_refs'] ?? 0) .
'</strong><span>Defekte Inhaltsverweise entfernt</span></div>'
3244 .
'<div><strong>' . (int)(
$state[
'database_optimized'] ?? 0) .
'</strong><span>Datenbanken optimiert</span></div>'
3247 return '<div id="' .
dbx()->esc($target_id) .
'"'
3248 .
' class="container-fluid dbx-process dbx-cms-media-process"'
3249 .
' data-dbx="lib=process|id=' .
dbx()->esc($target_id) .
'|url=' .
dbx()->esc($next_url) .
'|interval=900|autostart=' . $autostart .
'"'
3250 .
' data-process-status="' .
dbx()->esc($status) .
'"'
3251 .
' data-process-percent="' . $percent .
'"'
3252 .
' data-process-step-percent="' . $step_percent .
'"'
3253 .
' data-process-next-url="' .
dbx()->esc($next_url) .
'"'
3254 .
' data-process-pause-url="' .
dbx()->esc($pause_url) .
'"'
3255 .
' data-process-resume-url="' .
dbx()->esc($resume_url) .
'"'
3256 .
' data-process-cancel-url="' .
dbx()->esc($cancel_url) .
'"'
3257 .
' data-process-restart-url="' .
dbx()->esc($restart_url) .
'"'
3258 .
' data-process-autostart="' . $autostart .
'"'
3259 .
' data-process-interval="900">'
3260 .
'<div class="dbx-process-header">'
3261 .
'<h3 class="dbx-process-title">Medienwartung</h3>'
3262 .
'<span class="dbx-process-status badge ' .
dbx()->esc($status_class) .
'"><i class="' .
dbx()->esc($status_icon) .
'"></i> ' .
dbx()->esc($status_labels[$status] ?? $status) .
'</span>'
3264 .
'<div class="dbx-process-grid">'
3265 .
'<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>'
3266 .
'<div class="dbx-process-progress"><div class="dbx-process-progress-head"><span class="dbx-process-progress-label">Medien, Zuordnungen und Datenbank</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>'
3267 .
'<div class="dbx-process-message" data-process-message>' .
dbx()->esc((
string)(
$state[
'message'] ??
'')) .
'</div>'
3268 .
'<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>'
3271 .
'<div class="dbx-process-actions">'
3272 .
'<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>'
3273 .
'<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>'
3274 .
'<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>'
3275 .
'<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>'
3280 private function append_url_params(
$url, $params = array()) {
3281 foreach ($params as $key =>
$value) {
3282 $url .= (strpos(
$url,
'?') ===
false ?
'?' :
'&') . rawurlencode((
string)$key) .
'=' . rawurlencode((
string)
$value);
3287 private function media_process() {
3288 $db =
dbx()->get_system_obj(
'dbxDB');
3289 $this->ensure_cms_schema(
$db);
3291 $token = preg_replace(
'/[^A-Za-z0-9_-]+/',
'', (
string)(
$_GET[
'proc_key'] ??
''));
3292 if (
$token ===
'')
$token = substr(md5(session_id() . microtime(
true)), 0, 16);
3293 $reset = (int)(
$_GET[
'reset'] ?? 0);
3294 $cmd = strtolower(preg_replace(
'/[^a-z_]+/',
'', (
string)(
$_GET[
'proc_cmd'] ??
'')));
3296 if ($reset || $cmd ===
'restart') {
3303 if ($cmd ===
'pause' && (
$state[
'status'] ??
'') ===
'running') {
3304 $state[
'status'] =
'paused';
3305 $state[
'message'] =
'Medienwartung angehalten.';
3306 } elseif ($cmd ===
'resume' && (
$state[
'status'] ??
'') ===
'paused') {
3307 $state[
'status'] =
'running';
3308 $state[
'message'] =
'Medienwartung fortgesetzt.';
3309 } elseif ($cmd ===
'cancel') {
3310 $state[
'status'] =
'canceled';
3311 $state[
'message'] =
'Medienwartung abgebrochen.';
3317 $next = $this->base_url(
'cms_media_process', array(
'proc_key' =>
$token));
3318 return $this->render_media_process(
$state, $next);
3321 private function first_upload_file() {
3322 if (empty($_FILES) || !is_array($_FILES))
return array();
3324 foreach ($_FILES as
$file) {
3325 if (!is_array(
$file))
continue;
3327 if (isset(
$file[
'tmp_name']) && !is_array(
$file[
'tmp_name'])) {
3331 if (!isset(
$file[
'tmp_name']) || !is_array(
$file[
'tmp_name']))
continue;
3333 $idx = array_key_first(
$file[
'tmp_name']);
3334 if ($idx ===
null)
continue;
3337 'name' => is_array(
$file[
'name'] ??
null) ? (
$file[
'name'][$idx] ??
'') : (
$file[
'name'] ??
''),
3338 'type' => is_array(
$file[
'type'] ??
null) ? (
$file[
'type'][$idx] ??
'') : (
$file[
'type'] ??
''),
3339 'tmp_name' =>
$file[
'tmp_name'][$idx] ??
'',
3340 'error' => is_array(
$file[
'error'] ??
null) ? (
$file[
'error'][$idx] ?? UPLOAD_ERR_NO_FILE) : (
$file[
'error'] ?? UPLOAD_ERR_NO_FILE),
3341 'size' => is_array(
$file[
'size'] ??
null) ? (
$file[
'size'][$idx] ?? 0) : (
$file[
'size'] ?? 0),
3348 private function load_gd_image(
$file, $mime) {
3349 if (!extension_loaded(
'gd'))
return false;
3350 $mime = strtolower((
string)$mime);
3351 if ((strpos($mime,
'jpeg') !==
false || preg_match(
'/\.jpe?g$/i',
$file)) && function_exists(
'imagecreatefromjpeg'))
return @imagecreatefromjpeg(
$file);
3352 if ((strpos($mime,
'png') !==
false || preg_match(
'/\.png$/i',
$file)) && function_exists(
'imagecreatefrompng'))
return @imagecreatefrompng(
$file);
3353 if (strpos($mime,
'webp') !==
false || preg_match(
'/\.webp$/i',
$file))
return function_exists(
'imagecreatefromwebp') ? @imagecreatefromwebp(
$file) : false;
3354 if ((strpos($mime,
'gif') !==
false || preg_match(
'/\.gif$/i',
$file)) && function_exists(
'imagecreatefromgif'))
return @imagecreatefromgif(
$file);
3358 private function save_gd_image($img,
$file, $mime) {
3359 if (!extension_loaded(
'gd'))
return false;
3360 $mime = strtolower((
string)$mime);
3361 if (strpos($mime,
'png') !==
false || preg_match(
'/\.png$/i',
$file)) {
3362 if (!function_exists(
'imagepng'))
return false;
3363 imagealphablending($img,
false);
3364 imagesavealpha($img,
true);
3365 return @imagepng($img,
$file, 6);
3367 if ((strpos($mime,
'webp') !==
false || preg_match(
'/\.webp$/i',
$file)) && function_exists(
'imagewebp')) {
3368 return @imagewebp($img,
$file, 86);
3370 if (!function_exists(
'imagejpeg'))
return false;
3371 return @imagejpeg($img,
$file, 88);
3374 private function convert_media_image_to_webp(
$file, $name, $mime =
'') {
3375 if (!extension_loaded(
'gd') || !function_exists(
'imagewebp'))
return false;
3376 if (
$file ===
'' || !is_file(
$file) || !is_readable(
$file))
return false;
3377 if (preg_match(
'/\.(webp|gif|svg)$/i', (
string)$name))
return false;
3379 $mime_l = strtolower((
string)$mime);
3380 $ext = strtolower(pathinfo((
string)$name, PATHINFO_EXTENSION));
3381 $is_supported = in_array($ext, array(
'jpg',
'jpeg',
'png'),
true)
3382 || strpos($mime_l,
'jpeg') !==
false
3383 || strpos($mime_l,
'png') !==
false;
3384 if (!$is_supported)
return false;
3386 $image = $this->load_gd_image(
$file, $mime);
3387 if (!$image)
return false;
3389 imagepalettetotruecolor($image);
3390 imagealphablending($image,
true);
3391 imagesavealpha($image,
true);
3393 $webp_name = preg_replace(
'/\.[^.]+$/',
'', basename((
string)$name)) .
'.webp';
3394 $webp_file = rtrim(dirname(
$file),
'/\\') . DIRECTORY_SEPARATOR . $webp_name;
3395 if (is_file($webp_file)) {
3396 $webp_name = $this->unique_media_name($webp_name);
3397 $webp_file = rtrim(dirname(
$file),
'/\\') . DIRECTORY_SEPARATOR . $webp_name;
3400 $ok = @imagewebp($image, $webp_file, 86);
3401 imagedestroy($image);
3402 if (!
$ok || !is_file($webp_file))
return false;
3406 'file' => $webp_file,
3407 'name' => $webp_name,
3408 'mime' =>
'image/webp',
3412 private function resize_image_to_max(
$file, $mime, $max_long_side = 2560) {
3413 $max_long_side = max(800, min(6000, (
int)$max_long_side));
3414 if (
$file ===
'' || preg_match(
'/\.(svg|gif)$/i', (
string)
$file))
return false;
3416 $size = @getimagesize(
$file);
3417 if (!is_array($size))
return false;
3419 $src_w = (int)($size[0] ?? 0);
3420 $src_h = (int)($size[1] ?? 0);
3421 if ($src_w <= 0 || $src_h <= 0 || max($src_w, $src_h) <= $max_long_side)
return false;
3423 $op = array(
'action' =>
'resize');
3424 if ($src_w >= $src_h) {
3425 $op[
'width'] = $max_long_side;
3429 $op[
'height'] = $max_long_side;
3432 return $this->edit_image_file(
$file, $mime, $op);
3435 private function create_media_thumbnail($src, $slot, $name, $mime =
'') {
3436 $type = $this->media_type(array(
'mime' => $mime,
'file_name' => $name));
3437 if ($type ===
'video')
return $this->create_video_thumbnail($src, $slot, $name);
3438 if ($type !==
'image' || preg_match(
'/\.svg$/i', (
string)$name) || !function_exists(
'imagecreatetruecolor'))
return array();
3440 $image = $this->load_gd_image($src, $mime);
3441 if (!$image)
return array();
3443 $src_w = imagesx($image);
3444 $src_h = imagesy($image);
3445 if ($src_w <= 0 || $src_h <= 0) {
3446 imagedestroy($image);
3452 $scale = min($max_w / $src_w, $max_h / $src_h, 1);
3453 $dst_w = max(1, (
int)round($src_w * $scale));
3454 $dst_h = max(1, (
int)round($src_h * $scale));
3455 $thumb = imagecreatetruecolor($dst_w, $dst_h);
3456 imagealphablending($thumb,
false);
3457 imagesavealpha($thumb,
true);
3458 imagefill($thumb, 0, 0, imagecolorallocatealpha($thumb, 255, 255, 255, 127));
3459 imagecopyresampled($thumb, $image, 0, 0, 0, 0, $dst_w, $dst_h, $src_w, $src_h);
3461 $dir = $this->cms_media_thumb_dir($slot);
3462 if (!is_dir(
$dir)) @mkdir(
$dir, 0777,
true);
3463 $thumb_ext = function_exists(
'imagewebp') ?
'webp' :
'jpg';
3464 $thumb_name = preg_replace(
'/\.[^.]+$/',
'', basename((
string)$name)) .
'.' . $thumb_ext;
3465 $thumb_name = $this->unique_media_name($thumb_name,
'thumb');
3466 $dst = rtrim(
$dir,
'/\\') . DIRECTORY_SEPARATOR . $thumb_name;
3467 $ok = $thumb_ext ===
'webp' ? @imagewebp($thumb, $dst, 82) : @imagejpeg($thumb, $dst, 82);
3468 imagedestroy($thumb);
3469 imagedestroy($image);
3471 if (!
$ok)
return array();
3473 'thumb_file_path' => $this->media_thumb_rel_dir($slot) . $thumb_name,
3474 'thumb_width' => $dst_w,
3475 'thumb_height' => $dst_h,
3479 private function find_executable_path(
string $command): string {
3480 static $cache = array();
3482 $command = trim($command);
3483 if ($command ===
'' || !preg_match(
'/^[a-zA-Z0-9_.-]+$/', $command)) {
3487 if (array_key_exists($command, $cache)) {
3488 return $cache[$command];
3491 $lookup = stripos(PHP_OS_FAMILY,
'Windows') === 0
3492 ?
'where ' . $command .
' 2>NUL'
3493 :
'command -v ' . $command .
' 2>/dev/null';
3494 $result = trim((
string)@shell_exec($lookup));
3496 $cache[$command] = is_string($path) ? trim($path) :
'';
3498 return $cache[$command];
3501 private function create_video_thumbnail($src, $slot, $name) {
3502 $ffmpeg = $this->find_executable_path(
'ffmpeg');
3504 $dir = $this->cms_media_thumb_dir($slot);
3505 if (!is_dir(
$dir)) @mkdir(
$dir, 0777,
true);
3506 $thumb_name = $this->unique_media_name(preg_replace(
'/\.[^.]+$/',
'', basename((
string)$name)) .
'.jpg',
'video-thumb');
3507 $dst = rtrim(
$dir,
'/\\') . DIRECTORY_SEPARATOR . $thumb_name;
3509 if ($ffmpeg !==
'') {
3510 $cmd =
'"' . $ffmpeg .
'" -y -ss 00:00:01 -i "' . $src .
'" -frames:v 1 -vf "scale=640:-1" "' . $dst .
'" 2>NUL';
3514 if (!is_file($dst) || !filesize($dst)) {
3515 $this->create_video_placeholder_thumbnail($dst, $name);
3518 if (!is_file($dst) || !filesize($dst))
return array();
3519 $size = @getimagesize($dst);
3521 'thumb_file_path' => $this->media_thumb_rel_dir($slot) . $thumb_name,
3522 'thumb_width' => is_array($size) ? (
int)$size[0] : 0,
3523 'thumb_height' => is_array($size) ? (
int)$size[1] : 0,
3527 private function create_video_placeholder_thumbnail($dst, $name) {
3528 if (!function_exists(
'imagecreatetruecolor'))
return false;
3531 $img = imagecreatetruecolor($w, $h);
3532 $bg = imagecolorallocate($img, 30, 43, 60);
3533 $panel = imagecolorallocate($img, 52, 72, 96);
3534 $white = imagecolorallocate($img, 255, 255, 255);
3535 $muted = imagecolorallocate($img, 188, 202, 220);
3536 imagefill($img, 0, 0, $bg);
3537 imagefilledrectangle($img, 20, 20, $w - 20, $h - 20, $panel);
3538 $cx = (int)($w / 2);
3539 $cy = (int)($h / 2) - 12;
3540 imagefilledpolygon($img, array($cx - 38, $cy - 48, $cx - 38, $cy + 48, $cx + 52, $cy), 3, $white);
3541 $label = substr(basename((
string)$name), 0, 54);
3542 imagestring($img, 3, 28, $h - 42, $label, $muted);
3543 $ok = @imagejpeg($img, $dst, 82);
3548 private function source_thumb_file($row) {
3549 $rel = ltrim(str_replace(
'\\',
'/', (
string)($row[
'thumb_file_path'] ??
'')),
'/');
3550 if ($rel ===
'' || strpos($rel,
'..') !==
false)
return '';
3554 $real = realpath(
$file);
3555 if (!
$base || !$real || strpos($real,
$base) !== 0 || !is_file($real) || !is_readable($real))
return '';
3559 private function render_cms() {
3560 $this->clear_cms_tpl_cache();
3561 $oTPL =
dbx()->get_system_obj(
'dbxTPL');
3562 $db =
dbx()->get_system_obj(
'dbxDB');
3563 $this->ensure_cms_schema(
$db);
3564 $texts = $this->cms_texts();
3567 $page_count = (int)
$db->count(dbxContentLng::ddContent(),
'');
3568 $folder_count = (int)
$db->count(dbxContentLng::ddFolder(),
'');
3569 $active_count =
$db->count(dbxContentLng::ddContent(),
'activ = 1');
3570 if ($page_count < 0) $page_count = 0;
3571 if ($folder_count < 0) $folder_count = 0;
3572 if ($active_count < 0) $active_count = 0;
3574 $cms_cid = $this->resolve_cms_page_id();
3575 $cms_fid = (int)
dbx()->get_modul_var(
'fid', 0,
'int');
3576 if ($cms_fid <= 0) {
3577 $cms_fid = (int)
dbx()->get_modul_var(
'dbx_fid', 0,
'int');
3582 'cms_cid' => (
string)$cms_cid,
3583 'cms_fid' => (
string)$cms_fid,
3584 'title' =>
$texts->get_fd_message(
'bar_title'),
3585 'subtitle' =>
$texts->get_fd_message(
'bar_subtitle'),
3586 'bar_title' =>
$texts->get_fd_message(
'bar_title'),
3587 'bar_subtitle' =>
$texts->get_fd_message(
'bar_subtitle'),
3588 'right_panel_hint' =>
$texts->get_fd_message(
'right_panel_hint'),
3589 'inline_media_title' =>
$texts->get_fd_message(
'inline_media_title'),
3590 'inline_media_assign_title' =>
$texts->get_fd_message(
'inline_media_assign_title'),
3591 'media_inline_empty' =>
$texts->get_fd_message(
'media_inline_empty'),
3592 'selection_label' =>
$texts->get_fd_message(
'selection_label'),
3593 'right_show' =>
$texts->get_fd_message(
'right_show'),
3594 'right_hide' =>
$texts->get_fd_message(
'right_hide'),
3595 'cms_messages' =>
dbx()->
esc($this->cms_js_messages()),
3596 'count_pages' => (
string)$page_count,
3597 'count_folders' => (
string)$folder_count,
3598 'count_active' => (
string)$active_count,
3599 'tree_url' =>
dbx()->
esc($this->base_url(
'cms_tree')),
3600 'page_url' =>
dbx()->
esc($this->base_url(
'cms_page')),
3601 'save_url' =>
dbx()->
esc($this->base_url(
'cms_save')),
3602 'delete_page_url' =>
dbx()->
esc($this->base_url(
'cms_delete_page')),
3603 'new_page_url' =>
dbx()->
esc($this->base_url(
'cms_new_page')),
3604 'duplicate_page_url' =>
dbx()->
esc($this->base_url(
'cms_duplicate_page')),
3605 'new_folder_url' =>
dbx()->
esc($this->base_url(
'cms_new_folder')),
3606 'media_url' =>
dbx()->
esc($this->base_url(
'cms_media')),
3607 'media_process_url' =>
dbx()->
esc($this->base_url(
'cms_media_process')),
3608 'media_folders_url' =>
dbx()->
esc($this->base_url(
'cms_media_folders')),
3609 'media_folder_create_url' =>
dbx()->
esc($this->base_url(
'cms_media_folder_create')),
3610 'media_folder_delete_url' =>
dbx()->
esc($this->base_url(
'cms_media_folder_delete')),
3611 'media_folder_rename_url' =>
dbx()->
esc($this->base_url(
'cms_media_folder_rename')),
3612 'media_move_url' =>
dbx()->
esc($this->base_url(
'cms_media_move')),
3613 'media_unused_url' =>
dbx()->
esc($this->base_url(
'cms_media_unused')),
3614 'upload_url' =>
dbx()->
esc($this->base_url(
'cms_upload')),
3615 'external_video_url' =>
dbx()->
esc($this->base_url(
'cms_external_video')),
3616 'upload_max_bytes' => (
string)$this->upload_max_bytes(),
3617 'remove_media_url' =>
dbx()->
esc($this->base_url(
'cms_remove_media')),
3618 'delete_media_url' =>
dbx()->
esc($this->base_url(
'cms_delete_media')),
3619 'edit_media_url' =>
dbx()->
esc($this->base_url(
'cms_edit_media')),
3620 'set_media_slot_url' =>
dbx()->
esc($this->base_url(
'cms_set_media_slot')),
3621 'assign_media_url' =>
dbx()->
esc($this->base_url(
'cms_assign_media')),
3622 'sort_media_url' =>
dbx()->
esc($this->base_url(
'cms_sort_media')),
3623 'mod_catalog_url' =>
dbx()->
esc($this->base_url(
'cms_mod_catalog')),
3624 'mod_modules_url' =>
dbx()->
esc($this->base_url(
'cms_mod_modules')),
3625 'folder_save_url' =>
dbx()->
esc($this->base_url(
'cms_save_folder')),
3626 'folder_delete_url' =>
dbx()->
esc($this->base_url(
'cms_delete_folder')),
3627 'move_node_url' =>
dbx()->
esc($this->base_url(
'cms_move_node')),
3628 'lng_coverage_url' =>
dbx()->
esc($this->base_url(
'cms_lng_coverage')),
3629 'lng_preview_url' =>
dbx()->
esc($this->base_url(
'cms_lng_preview')),
3630 'lng_provision_url' =>
dbx()->
esc($this->base_url(
'cms_lng_provision')),
3631 'lng_provision_tree_url' =>
dbx()->
esc($this->base_url(
'cms_lng_provision_tree')),
3632 'lng_reset_sync_url' =>
dbx()->
esc($this->base_url(
'cms_lng_reset_sync')),
3633 'lng_delete_preview_url' =>
dbx()->
esc($this->base_url(
'cms_lng_delete_preview')),
3634 'current_lng' => dbxContentLng::current(),
3635 'master_lng' => dbxContentLngSync::masterLng(),
3636 'lng_bar' => dbxContentLngSync::renderLngBar(),
3637 'template_options' => $this->template_options(),
3638 'rights_options' => $this->rights_options(),
3639 'media_template_options' => $this->media_template_options(),
3640 'hero_template_options' => $this->hero_template_options(),
3641 'hero_variant_options' => $this->hero_variant_options(),
3642 'gallery_template_options' => $this->gallery_template_options(),
3643 'gallery_overflow_options' => $this->gallery_overflow_options(),
3644 'gallery_click_options' => $this->gallery_click_options(),
3645 'page_form' => $this->render_page_form(),
3646 'folder_form' => $this->render_folder_form(),
3647 'settings_form' => $this->render_settings_form(),
3648 'media_browser_forms' => $this->media_forms()->renderTemplates(
3649 $this->base_url(
'cms_upload'),
3651 $this->base_url(
'cms_external_video'),
3652 'cms-external-video'
3655 'title' =>
$texts->get_fd_message(
'tree_search'),
3656 'extra_attrs' =>
'data-cms-search',
3661 $data = array_merge(
3662 dbx()->get_include_obj(
'dbxAdminHelp',
'dbxAdmin')->vars(
'content'),
3665 $data[
'bar_class'] =
'dbx-module-bar dbx-cms-head';
3666 $data[
'bar_title_class'] =
'dbx-module-bar-titleblock';
3667 $data[
'bar_actions_class'] =
'dbx-module-bar-actions flex-wrap';
3668 if (trim((
string)($data[
'bar_subtitle'] ??
'')) ===
'' && trim((
string)($data[
'subtitle'] ??
'')) !==
'') {
3669 $data[
'bar_subtitle'] = (string)$data[
'subtitle'];
3672 return $oTPL->get_tpl(
'dbxContent_admin|cms-admin', $data);
3676 $modul = preg_replace(
'/[^A-Za-z0-9_]+/',
'', (
string)$modul);
3677 if ($modul ===
'')
return array();
3682 $inc =
$base .
'include' . DIRECTORY_SEPARATOR;
3684 foreach (glob($inc .
'*.class.php') ?: array() as $path) {
3685 if (is_file($path))
$files[] = $path;
3696 $src = @file_get_contents(
$file);
3697 if (!is_string($src) || $src ===
'')
continue;
3698 if (preg_match(
"/get_modul_var\s*\(\s*['\"]dbx_run2['\"]/", $src)) {
3701 if (preg_match_all(
"/case\s+['\"]([^'\"]+)['\"]\s*:/", $src, $matches)) {
3702 foreach ($matches[1] as $case) {
3703 $case = trim((
string)$case);
3704 if ($case ===
'' || $case ===
'default')
continue;
3705 $run1[$case] =
true;
3708 if (preg_match_all(
'/\$(?:run|action|work)\s*===?\s*[\'"]([^\'"]+)[\'"]/', $src, $ifs)) {
3709 foreach ($ifs[1] as $case) {
3710 $case = trim((
string)$case);
3711 if ($case ===
'')
continue;
3712 $run1[$case] =
true;
3715 if (preg_match_all(
"/get_modul_var\s*\(\s*['\"]dbx_run1['\"][^)]*['\"]([a-zA-Z0-9_]+)['\"]/", $src, $defaults)) {
3716 foreach ($defaults[1] as $case) {
3717 $case = trim((
string)$case);
3718 if ($case ===
'' || $case ===
'parameter')
continue;
3719 $run1[$case] =
true;
3722 if (preg_match_all(
"/get_modul_var\s*\(\s*['\"]dbx_run2['\"][^)]*['\"]([^'\"]+)['\"]/", $src, $defaults)) {
3723 foreach ($defaults[1] as $val) {
3724 $val = trim((
string)$val);
3725 if ($val !==
'') $run2[$val] =
true;
3729 $run1_list = array_keys($run1);
3730 usort($run1_list,
function($a, $b) {
3731 return strlen($b) <=> strlen($a);
3734 'run1' => array_values(array_keys($run1)),
3735 'run2' => array_values(array_keys($run2)),
3736 'uses_run2' => $uses_run2,
3737 'run1_sorted' => $run1_list,
3741 private function mod_modules_json() {
3743 require_once
$base .
'dbxModuleImages.class.php';
3744 $images = new \dbx\dbxAdmin\dbxModuleImages();
3749 foreach (glob(
$dir .
'*', GLOB_ONLYDIR) ?: array() as $path) {
3750 $name = basename(str_replace(
'\\',
'/', $path));
3751 if (!preg_match(
'/^[A-Za-z][A-Za-z0-9_]*$/', $name))
continue;
3752 $class_file =
dbx()->os_path($path . DIRECTORY_SEPARATOR . $name .
'.class.php');
3753 if (!is_file($class_file))
continue;
3755 $count = $images->imageCount($name);
3763 'uses_run2' => !empty($scan[
'uses_run2']) ? 1 : 0,
3764 'run1_actions' => array_slice($scan[
'run1'], 0, 24),
3768 usort(
$items,
function($a, $b) {
3769 return strcmp((
string)($a[
'label'] ??
''), (
string)($b[
'label'] ??
''));
3771 $this->cms_json_response(array(
'ok' => 1,
'items' =>
$items));
3774 private function mod_catalog_json() {
3776 require_once
$base .
'dbxModuleImages.class.php';
3777 $images = new \dbx\dbxAdmin\dbxModuleImages();
3779 $modul = preg_replace(
'/[^A-Za-z0-9_]+/',
'', (
string)
dbx()->get_modul_var(
'modul',
'',
'parameter'));
3780 if ($modul ===
'') {
3781 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Modul fehlt.',
'items' => array()));
3784 $class_file =
dbx()->os_path(
dbx()->
get_base_dir() .
'dbx/modules/' . $modul .
'/' . $modul .
'.class.php');
3785 if (!is_file($class_file)) {
3786 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Modul nicht gefunden.',
'items' => array()));
3790 $out = $images->catalogForModul($modul);
3793 $this->cms_json_response(array(
3796 'uses_run2' => !empty($scan[
'uses_run2']) ? 1 : 0,
3797 'run1_actions' => $scan[
'run1'],
3798 'items' => is_array(
$out) ?
$out : array(),
3802 private function tree_json() {
3803 $this->cms_json_response(array(
'ok' => 1) + $this->cms_tree());
3806 private function lng_coverage_json() {
3807 $type = trim((
string)
dbx()->get_modul_var(
'type',
'page'));
3808 $type = $type ===
'folder' ?
'folder' :
'page';
3809 $id = (int)
dbx()->get_modul_var(
'id', 0,
'int');
3810 $lngUid = trim((
string)
dbx()->get_modul_var(
'lng_uid',
''));
3811 $db =
dbx()->get_system_obj(
'dbxDB');
3812 $this->ensure_cms_schema(
$db);
3814 if ($lngUid ===
'' && $id > 0) {
3815 $dd = $type ===
'folder' ? dbxContentLng::ddFolder() : dbxContentLng::ddContent();
3816 $lngUid = dbxContentLngSync::ensureRecordUid(
$db,
$dd, $id, $type ===
'folder' ?
'f' :
'p');
3819 $coverage = dbxContentLngSync::coverageForUid(
$db, $type, $lngUid);
3820 $this->cms_json_response(array(
'ok' => 1,
'coverage' => $coverage));
3823 private function lng_preview_json() {
3824 if (!dbxContentLngSync::isMasterLng()) {
3825 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Vorschau nur in der Master-Sprache moeglich.'));
3828 $payload = $this->request_json();
3829 $type = (($payload[
'type'] ??
'page') ===
'folder') ?
'folder' :
'page';
3830 $id = (int) ($payload[
'id'] ?? 0);
3831 $lngs = is_array($payload[
'lngs'] ??
null) ? $payload[
'lngs'] : array();
3832 $db =
dbx()->get_system_obj(
'dbxDB');
3833 $this->ensure_cms_schema(
$db);
3835 dbxContentTranslate::clearWarnings();
3836 $preview = dbxContentLngSync::previewProvision(
$db, $type, $id, $lngs);
3837 $this->cms_json_response(array(
3839 'preview' => $preview,
3840 'provider' => dbxContentTranslate::provider(),
3841 'translate_warnings' => dbxContentTranslate::warnings(),
3845 private function lng_provision_json() {
3846 if (!dbxContentLngSync::isMasterLng()) {
3847 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Uebertragung nur in der Master-Sprache moeglich.'));
3850 $payload = $this->request_json();
3851 $type = (($payload[
'type'] ??
'page') ===
'folder') ?
'folder' :
'page';
3852 $id = (int) ($payload[
'id'] ?? 0);
3853 $items = is_array($payload[
'items'] ??
null) ? $payload[
'items'] : array();
3854 $db =
dbx()->get_system_obj(
'dbxDB');
3855 $this->ensure_cms_schema(
$db);
3857 dbxContentTranslate::clearWarnings();
3858 $result = dbxContentLngSync::provisionFromPreview(
$db, $type, $id,
$items);
3859 if ((
int) (
$result[
'ok'] ?? 0) === 1) {
3860 $result[
'media_copied'] = $this->apply_lng_provision_media(
$db, $type, $id,
$result);
3861 $this->flush_menu_cache();
3863 $result[
'translate_warnings'] = dbxContentTranslate::warnings();
3864 $this->cms_json_response(array(
'ok' => (
int) (
$result[
'ok'] ?? 0),
'result' =>
$result));
3867 private function lng_provision_tree_json() {
3868 if (!dbxContentLngSync::isMasterLng()) {
3869 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Unterbaum-Uebertragung nur in der Master-Sprache moeglich.'));
3872 $payload = $this->request_json();
3873 $id = (int) ($payload[
'id'] ?? 0);
3874 $lngs = is_array($payload[
'lngs'] ??
null) ? $payload[
'lngs'] : array();
3876 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Kein Ordner gewaehlt.'));
3879 $db =
dbx()->get_system_obj(
'dbxDB');
3880 $this->ensure_cms_schema(
$db);
3881 dbxContentTranslate::clearWarnings();
3882 $result = dbxContentLngSync::provisionFolderTree(
$db, $id, $lngs);
3883 if ((
int) (
$result[
'ok'] ?? 0) === 1) {
3885 foreach (is_array(
$result[
'pages'] ??
null) ?
$result[
'pages'] : array() as $pageItem) {
3886 if (!is_array($pageItem)) {
3889 $masterPageId = (int) ($pageItem[
'master_id'] ?? 0);
3890 $prov = is_array($pageItem[
'result'] ??
null) ? $pageItem[
'result'] : array();
3891 if ($masterPageId > 0) {
3892 $mediaCopied += $this->apply_lng_provision_media(
$db,
'page', $masterPageId, $prov);
3895 $result[
'media_copied'] = $mediaCopied;
3896 $this->flush_menu_cache();
3898 $result[
'translate_warnings'] = dbxContentTranslate::warnings();
3899 $this->cms_json_response(array(
'ok' => (
int) (
$result[
'ok'] ?? 0),
'result' =>
$result));
3902 private function lng_reset_sync_json() {
3903 if (!dbxContentLngSync::isMasterLng()) {
3904 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Auto-Sync nur in der Master-Sprache setzbar.'));
3907 $payload = $this->request_json();
3908 $type = (($payload[
'type'] ??
'page') ===
'folder') ?
'folder' :
'page';
3909 $id = (int) ($payload[
'id'] ?? 0);
3910 $lngs = is_array($payload[
'lngs'] ??
null) ? $payload[
'lngs'] : array();
3912 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Kein Datensatz gewaehlt.'));
3915 $db =
dbx()->get_system_obj(
'dbxDB');
3916 $this->ensure_cms_schema(
$db);
3917 $result = dbxContentLngSync::resetSyncToAuto(
$db, $type, $id, $lngs);
3918 $this->cms_json_response(array(
'ok' => count(
$result[
'updated'] ?? array()) ? 1 : 0,
'result' =>
$result));
3921 private function lng_delete_preview_json() {
3922 if (!dbxContentLngSync::isMasterLng()) {
3923 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Mehrsprachige Loesch-Vorschau nur in der Master-Sprache moeglich.'));
3926 $payload = $this->request_json();
3927 $type = (($payload[
'type'] ??
'page') ===
'folder') ?
'folder' :
'page';
3928 $id = (int) ($payload[
'id'] ?? 0);
3930 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Kein Datensatz gewaehlt.'));
3933 $db =
dbx()->get_system_obj(
'dbxDB');
3934 $this->ensure_cms_schema(
$db);
3935 $preview = dbxContentLngSync::previewDelete(
$db, $type, $id);
3936 $this->cms_json_response(array(
'ok' => 1,
'preview' => $preview));
3939 private function normalize_delete_lngs(array $payload): array {
3940 $current = dbxContentLng::current();
3941 $raw = $payload[
'delete_lngs'] ??
null;
3942 if (!is_array($raw) || !count($raw)) {
3943 return array($current);
3945 if (!dbxContentLngSync::isMasterLng()) {
3946 return array($current);
3949 $allowed = dbxContentLngSync::accessibleLngs();
3951 foreach ($raw as
$lng) {
3952 $lng = strtolower(trim((
string)
$lng));
3958 return count(
$out) ?
$out : array($current);
3961 private function delete_page_in_lngs(
$db,
int $id, array $deleteLngs): array {
3962 $targets = dbxContentLngSync::resolveDeleteIds(
$db,
'page', $id, $deleteLngs);
3971 $targetId = (int) (
$target[
'id'] ?? 0);
3972 if ($targetId <= 0) {
3976 $targetDd = dbxContentLng::ddContent(
$lng);
3977 $ok =
$db->delete($targetDd,
'id = ' . $targetId, 1, 0);
3979 $db->update($this->dd_media, array(
'active' => 1,
'content_id' => 0,
'folder_id' => 0),
'content_id = ' . $targetId, 0, 1, 1, 0);
3981 $this->dd_media_usage,
3982 array(
'active' => 0),
3983 dbxContentMediaUsageScope::withLanguage(
'content_id = ' . $targetId .
' AND active = 1',
$lng),
3989 $this->flush_deleted_page_cache($targetId,
$lng);
3990 $deleted[] = array(
'lng' =>
$lng,
'id' => $targetId);
3992 $errors[] =
'Loeschen in ' . strtoupper(
$lng) .
' fehlgeschlagen.';
3997 'ok' => count($deleted) ? 1 : 0,
3998 'deleted' => $deleted,
4005 return array(
'ok' => 0,
'deleted' => array(),
'errors' => array(
'Keine Seite gewaehlt.'));
4008 $db =
dbx()->get_system_obj(
'dbxDB');
4009 $this->ensure_cms_schema(
$db);
4010 $result = $this->delete_page_in_lngs(
$db, $id, array(dbxContentLng::current()));
4012 if ((
int)(
$result[
'ok'] ?? 0) === 1) {
4013 $this->flush_menu_cache();
4021 return array(
'ok' => 0,
'deleted' => array(),
'errors' => array(
'Kein Ordner gewaehlt.'));
4024 $db =
dbx()->get_system_obj(
'dbxDB');
4025 $this->ensure_cms_schema(
$db);
4026 $result = $this->delete_folder_in_lngs(
$db, $id, array(dbxContentLng::current()));
4028 if ((
int)(
$result[
'ok'] ?? 0) === 1) {
4029 $this->flush_menu_cache();
4037 return array(
'ok' => 0,
'errors' => array(
'Keine Medien-ID erhalten.'));
4040 $db =
dbx()->get_system_obj(
'dbxDB');
4041 $this->ensure_cms_schema(
$db);
4042 $used =
$db->select(
4043 $this->dd_media_usage,
4044 'media_id = ' . $id .
' AND active = 1',
4053 if (is_array($used) && isset($used[0])) {
4054 return array(
'ok' => 0,
'errors' => array(
'Medium wird noch verwendet.'));
4057 foreach (dbxContentLngSync::accessibleLngs() as
$lng) {
4061 return array(
'ok' => 0,
'errors' => array(
'Medium wird noch verwendet.'));
4065 if (preg_match(
'/data-cms-media-id=["\']?' . $id .
'(?:["\'\s>]|$)/i', (
string)(
$page[
'content'] ??
''))) {
4066 return array(
'ok' => 0,
'errors' => array(
'Medium wird noch verwendet.'));
4071 $row =
$db->select1($this->dd_media, $id);
4072 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1) {
4073 return array(
'ok' => 0,
'errors' => array(
'Medium nicht gefunden.'));
4076 if (strtolower((
string)($row[
'storage_type'] ??
'local')) ===
'local') {
4077 $file = $this->source_media_file($row);
4082 $file = $this->source_media_file($row);
4083 if (
$file !==
'' && preg_match(
'/\.json$/i',
$file)) {
4087 $thumb = $this->source_thumb_file($row);
4088 if ($thumb !==
'') {
4092 $ok =
$db->update($this->dd_media, array(
'active' => 0), $id);
4094 'ok' =>
$ok >= 0 ? 1 : 0,
4095 'errors' =>
$ok >= 0 ? array() : array(
'Medium konnte nicht geloescht werden.'),
4099 private function delete_folder_in_lngs(
$db,
int $id, array $deleteLngs): array {
4100 $targets = dbxContentLngSync::resolveDeleteIds(
$db,
'folder', $id, $deleteLngs);
4109 $targetId = (int) (
$target[
'id'] ?? 0);
4110 if ($targetId <= 0) {
4114 $check = dbxContentLngSync::folderDeletable(
$db,
$lng, $targetId);
4115 if ((
int) (
$check[
'deletable'] ?? 0) !== 1) {
4116 $reason = trim((
string) (
$check[
'reason'] ??
''));
4117 $errors[] = strtoupper(
$lng) . ($reason !==
'' ?
': ' . $reason :
': Ordner kann nicht geloescht werden.');
4121 $targetDd = dbxContentLng::ddFolder(
$lng);
4122 $ok =
$db->delete($targetDd,
'id = ' . $targetId, 1, 0);
4125 $this->dd_media_usage,
4126 array(
'active' => 0),
4127 dbxContentMediaUsageScope::withLanguage(
'folder_id = ' . $targetId .
' AND content_id = 0 AND active = 1',
$lng),
4133 $this->flush_deleted_folder_cache(
$db, $targetId,
$lng);
4134 $deleted[] = array(
'lng' =>
$lng,
'id' => $targetId);
4136 $errors[] =
'Loeschen in ' . strtoupper(
$lng) .
' fehlgeschlagen.';
4141 'ok' => count($deleted) ? 1 : 0,
4142 'deleted' => $deleted,
4147 private function page_json() {
4148 $id = (int)
dbx()->get_modul_var(
'id', 0,
'int');
4150 $id = $this->resolve_cms_page_id();
4152 $db =
dbx()->get_system_obj(
'dbxDB');
4153 $this->ensure_cms_schema(
$db);
4154 $row = $id > 0 ?
$db->select1(dbxContentLng::ddContent(), $id) :
$db->empty_record(dbxContentLng::ddContent())[0];
4155 if (is_array($row) && isset($row[
'content'])) {
4156 $row[
'content'] = $this->normalize_content_media_urls($row[
'content']);
4158 if (is_array($row)) {
4159 $row = $this->normalize_gallery_row($row);
4161 if ($id > 0 && is_array($row)) {
4162 $this->sync_inline_media_usage(
$db, $id, (
string)($row[
'content'] ??
''), (
int)($row[
'folder'] ?? 0));
4164 $media = $id > 0 ? $this->media_usage_rows_for_context(
$db, $id, 0) : array();
4165 $this->cms_json_response(array(
4169 'hero_preview_media' => $this->hero_preview_media(
$db, $row),
4170 'hero_parent_preview_media' => $this->inherited_hero_preview_media(
$db, (
int)($row[
'folder'] ?? 0)),
4171 'seo_preview_media' => $this->seo_preview_media(
$db, $row),
4175 private function hero_preview_media(
$db, $row) {
4176 if (!is_array($row))
return array();
4177 $hero_id = (int)($row[
'hero_image_id'] ?? 0);
4178 if ($hero_id <= 0)
return array();
4179 $hero =
$db->select1($this->dd_media, $hero_id);
4180 if (!is_array(
$hero) || (
int)(
$hero[
'active'] ?? 0) !== 1)
return array();
4181 if (!$this->media_file_exists(
$hero))
return array();
4182 return $this->normalize_media_row(
$hero);
4185 private function inherited_hero_preview_media(
$db, $folder_id) {
4186 $folder_id = (int)$folder_id;
4188 while ($folder_id > 0 && !isset($seen[$folder_id])) {
4189 $seen[$folder_id] = 1;
4190 $folder =
$db->select1(dbxContentLng::ddFolder(), $folder_id,
'*', 0);
4191 if (!is_array($folder))
return array();
4192 $hero_template = trim((
string)($folder[
'hero_template'] ??
'parent'));
4193 if ($this->is_no_hero_template($hero_template))
return array();
4194 $hero_value = trim((
string)($folder[
'hero_image_id'] ??
'parent'));
4195 $hero_id = (int)$hero_value;
4197 $hero =
$db->select1($this->dd_media, $hero_id);
4198 if (!is_array(
$hero) || (
int)(
$hero[
'active'] ?? 0) !== 1 || !$this->media_file_exists(
$hero))
return array();
4200 $hero[
'parent_folder_id'] = $folder_id;
4201 $hero[
'parent_folder_name'] = (string)($folder[
'name'] ??
'');
4204 if ($hero_value ===
'0' || strtolower($hero_value) ===
'none')
return array();
4205 $folder_id = (int)($folder[
'parent_id'] ?? 0);
4210 private function seo_preview_media(
$db, $row) {
4211 if (!is_array($row))
return array();
4212 $seo_id = (int)($row[
'seo_image_id'] ?? 0);
4213 if ($seo_id <= 0)
return array();
4214 $media =
$db->select1($this->dd_media, $seo_id);
4215 if (!is_array(
$media) || (
int)(
$media[
'active'] ?? 0) !== 1)
return array();
4216 if (!$this->media_file_exists(
$media))
return array();
4217 return $this->normalize_media_row(
$media);
4220 private function save_json() {
4221 $texts = $this->cms_texts();
4222 $payload = $this->request_json();
4223 $id = (int)($payload[
'id'] ?? 0);
4224 $db =
dbx()->get_system_obj(
'dbxDB');
4225 $this->ensure_cms_schema(
$db);
4227 $title = $this->clean_text($payload[
'title'] ??
'', 254);
4228 $folder = (int)($payload[
'folder'] ?? 0);
4229 $keywordsProvided = array_key_exists(
'keywords', $payload);
4231 $permalink = $this->page_permalink(
$db, $folder, $title, $payload[
'permalink'] ??
'', $id);
4232 }
catch (\InvalidArgumentException $e) {
4233 $this->cms_json_response(array(
4236 'field' =>
'permalink',
4237 'msg' => $e->getMessage(),
4241 'activ' => $this->bool_int($payload[
'activ'] ?? 1, 1),
4242 'folder' => $folder,
4244 'menu_title' => $this->clean_text($payload[
'menu_title'] ??
'', 96),
4245 'permalink' => $permalink,
4246 'description' => $this->clean_text($payload[
'description'] ??
'', 254),
4247 'template' => $this->clean_text($payload[
'template'] ??
'parent', 254),
4248 'hero_template' => $this->clean_text($payload[
'hero_template'] ??
'parent', 80),
4249 'hero_image_id' => $this->clean_text($payload[
'hero_image_id'] ??
'parent', 32),
4250 'hero_margin_top' => $this->clean_text($payload[
'hero_margin_top'] ??
'parent', 32),
4251 'hero_height' => $this->clean_text($payload[
'hero_height'] ??
'parent', 32),
4252 'hero_variant' => $this->clean_text($payload[
'hero_variant'] ??
'parent', 32),
4253 'hero_sticky' => $this->clean_text($payload[
'hero_sticky'] ??
'parent', 32),
4254 'hero_scroll_layer' => $this->clean_text($payload[
'hero_scroll_layer'] ??
'parent', 32),
4255 'gallery_template' =>
'image-gallery',
4256 'gallery_visible_count' =>
'3',
4257 'gallery_image_size' => $this->clean_text($payload[
'gallery_image_size'] ??
'original', 32),
4258 'gallery_lightbox_width' => $this->clean_text($payload[
'gallery_lightbox_width'] ??
'100vw', 32),
4259 'gallery_overflow' => $this->clean_text($payload[
'gallery_overflow'] ??
'grid', 32),
4260 'gallery_click_behavior' => $this->clean_text($payload[
'gallery_click_behavior'] ??
'lightbox', 32),
4261 'content' => $this->normalize_and_sanitize_content($payload[
'content'] ??
''),
4263 $data = $this->normalize_hero_payload($data);
4266 $existingSeo =
$db->select1(dbxContentLng::ddContent(), $id,
'keywords,meta_robots,seo_title,seo_image_id', 0);
4267 $data[
'keywords'] = $keywordsProvided
4268 ? $this->clean_text($payload[
'keywords'] ??
'', 254)
4269 : (is_array($existingSeo) ? (string)($existingSeo[
'keywords'] ??
'') :
'');
4270 $data[
'meta_robots'] = is_array($existingSeo) ? (string)($existingSeo[
'meta_robots'] ??
'index,follow') :
'index,follow';
4271 $data[
'seo_title'] = is_array($existingSeo) ? (string)($existingSeo[
'seo_title'] ??
'') :
'';
4272 $data[
'seo_image_id'] = is_array($existingSeo) ? max(0, (
int)($existingSeo[
'seo_image_id'] ?? 0)) : 0;
4274 $data[
'keywords'] = $keywordsProvided ? $this->clean_text($payload[
'keywords'] ??
'', 254) :
'';
4275 $data[
'meta_robots'] =
'index,follow';
4276 $data[
'seo_title'] =
'';
4277 $data[
'seo_image_id'] = 0;
4281 $ok =
$db->update(dbxContentLng::ddContent(), $data, $id);
4284 $ok =
$db->insert(dbxContentLng::ddContent(), $data);
4285 $saved_id = (
$ok === 1) ?
$db->get_insert_id() : 0;
4289 $this->sync_saved_hero_media_usage(
$db, $saved_id, 0, $data[
'hero_image_id']);
4290 dbxContentLngSync::afterPageSave(
$db, $saved_id, $id <= 0);
4291 $syncResult = array(
'updated' => array(),
'skipped' => array(),
'errors' => array());
4292 if ($id > 0 && dbxContentLngSync::isMasterLng()) {
4293 dbxContentTranslate::clearWarnings();
4294 $syncResult = dbxContentLngSync::syncSlavesFromMaster(
$db,
'page', $saved_id);
4295 $syncResult[
'media_copied'] = $this->apply_lng_sync_media(
$db, $saved_id, $syncResult);
4296 $syncResult[
'translate_warnings'] = dbxContentTranslate::warnings();
4297 $this->flush_lng_sync_cache(
$db, $syncResult);
4299 $inline_provided = array_key_exists(
'inline_media_ids', $payload) && is_array($payload[
'inline_media_ids']);
4300 $inline_sync = $this->sync_inline_media_usage(
4305 $inline_provided ? $payload[
'inline_media_ids'] :
null,
4308 $this->flush_saved_page_cache(
$db, $saved_id);
4309 $media = $this->media_usage_rows_for_context(
$db, $saved_id, 0);
4310 $saved_row =
$db->select1(dbxContentLng::ddContent(), $saved_id);
4311 if (is_array($saved_row)) {
4312 if (isset($saved_row[
'content'])) {
4313 $saved_row[
'content'] = $this->normalize_content_media_urls($saved_row[
'content']);
4315 $saved_row = $this->normalize_gallery_row($saved_row);
4317 $this->cms_json_response(array(
4321 'row' => $saved_row,
4323 'inline_media_sync' => $inline_sync,
4324 'hero_preview_media' => $this->hero_preview_media(
$db, $saved_row),
4325 'hero_parent_preview_media' => $this->inherited_hero_preview_media(
$db, (
int)($saved_row[
'folder'] ?? 0)),
4326 'seo_preview_media' => $this->seo_preview_media(
$db, $saved_row),
4327 'lng_forked' => dbxContentLngSync::isMasterLng() ? 0 : 1,
4328 'lng_synced' => count($syncResult[
'updated'] ?? array()),
4329 'lng_media_copied' => (
int) ($syncResult[
'media_copied'] ?? 0),
4330 'lng_translate_provider' => dbxContentTranslate::provider(),
4331 'lng_sync_targets' => dbxContentLngSync::isMasterLng() ? dbxContentLngSync::slaveLngs() : array(),
4332 'lng_sync_updated' => is_array($syncResult[
'updated'] ??
null) ? $syncResult[
'updated'] : array(),
4333 'lng_sync_skipped' => is_array($syncResult[
'skipped'] ??
null) ? $syncResult[
'skipped'] : array(),
4334 'lng_sync_errors' => is_array($syncResult[
'errors'] ??
null) ? $syncResult[
'errors'] : array(),
4335 'translate_warnings' => is_array($syncResult[
'translate_warnings'] ??
null) ? $syncResult[
'translate_warnings'] : array(),
4336 'open_lng_provision' => $this->lng_provision_open_flag(
$db,
'page', $saved_id),
4340 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'page_save_error'),
'db' =>
$ok));
4343 private function new_page_json() {
4344 $texts = $this->cms_texts();
4345 $folder = (int)
dbx()->get_modul_var(
'folder', 0,
'int');
4346 $db =
dbx()->get_system_obj(
'dbxDB');
4347 $this->ensure_cms_schema(
$db);
4348 $title =
$texts->format_fd_message(
'new_page_title', array(
'time' => date(
'H:i')));
4349 $sorter = $this->next_content_sorter(
$db, $folder);
4351 if (
$db->insert(dbxContentLng::ddContent(), array(
4353 'folder' => $folder,
4356 'permalink' => dbxContent_permalink::build(
$db, dbxContentLng::ddFolder(), $folder, $title),
4357 'template' =>
'parent',
4358 'hero_template' =>
'parent',
4359 'hero_image_id' =>
'parent',
4360 'hero_margin_top' =>
'parent',
4361 'hero_height' =>
'parent',
4362 'hero_variant' =>
'parent',
4363 'hero_sticky' =>
'parent',
4364 'hero_scroll_layer' =>
'parent',
4365 'gallery_template' =>
'image-gallery',
4366 'gallery_visible_count' =>
'3',
4367 'gallery_image_size' =>
'original',
4368 'gallery_lightbox_width' =>
'100vw',
4369 'gallery_overflow' =>
'grid',
4370 'gallery_click_behavior' =>
'lightbox',
4371 'description' =>
'',
4373 'meta_robots' =>
'index,follow',
4375 'seo_image_id' => 0,
4376 'sorter' => $sorter,
4377 'content' =>
'<p>' .
$texts->get_fd_message(
'new_page_content') .
'</p>',
4378 ), 0, 1, 0, 1) === 1) {
4379 $id =
$db->get_insert_id();
4382 dbxContentLngSync::afterPageSave(
$db, $id,
true);
4383 $this->flush_menu_cache();
4384 $this->cms_json_response(array(
4388 'row' =>
$db->select1(dbxContentLng::ddContent(), $id),
4389 'open_lng_provision' => $this->lng_provision_open_flag(
$db,
'page', $id),
4392 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'page_create_error'),
'db' => $id));
4395 private function duplicate_page_json() {
4396 $texts = $this->cms_texts();
4397 $payload = $this->request_json();
4398 $sourceId = (int)($payload[
'id'] ??
dbx()->get_modul_var(
'id', 0,
'int'));
4399 if ($sourceId <= 0) {
4400 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'page_select_first')));
4403 $db =
dbx()->get_system_obj(
'dbxDB');
4404 $this->ensure_cms_schema(
$db);
4407 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'page_duplicate_lock_error')));
4411 if (!is_array(
$source) || (
int)(
$source[
'id'] ?? 0) !== $sourceId) {
4413 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'page_not_found')));
4418 'id',
'create_date',
'create_uid',
'update_date',
'update_uid',
'owner',
4419 'hits',
'xvote',
'vote',
'vote1',
'vote2',
'vote3',
'vote4',
'vote5',
'lastuservote'
4424 $folderId = (int)(
$source[
'folder'] ?? 0);
4425 $title = (string)(
$source[
'title'] ??
'Unbenannte Seite');
4426 $copy[
'sorter'] = $this->next_content_sorter(
$db, $folderId);
4427 $copy[
'permalink'] = $this->duplicate_page_permalink(
4431 (
string)(
$source[
'permalink'] ??
'')
4433 $copy[
'lng_uid'] =
'';
4434 $copy[
'lng_sync'] = dbxContentLngSync::isMasterLng() ?
'auto' :
'manual';
4435 $copy[
'lng_rev'] = 1;
4436 $copy[
'lng_synced_rev'] = 0;
4439 $newId =
$inserted === 1 ? (int)
$db->get_insert_id() : 0;
4442 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'page_duplicate_error'),
'db' =>
$inserted));
4445 dbxContentLngSync::afterPageSave(
$db, $newId,
true);
4448 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'page_duplicate_commit_error')));
4451 $mediaCopied = $this->copy_page_media_usage(
4457 dbxContentLng::current(),
4458 dbxContentLng::current()
4460 $inlineSync = $this->sync_inline_media_usage(
4463 (
string)(
$source[
'content'] ??
''),
4466 $this->flush_saved_page_cache(
$db, $newId);
4469 if (is_array($row) && isset($row[
'content'])) {
4470 $row[
'content'] = $this->normalize_content_media_urls($row[
'content']);
4471 $row = $this->normalize_gallery_row($row);
4474 $this->cms_json_response(array(
4478 'source_id' => $sourceId,
4480 'permalink' => (
string)($row[
'permalink'] ?? $copy[
'permalink']),
4481 'media_copied' => $mediaCopied,
4482 'inline_media_sync' => $inlineSync,
4483 'open_lng_provision' => $this->lng_provision_open_flag(
$db,
'page', $newId),
4484 'msg' =>
$texts->get_fd_message(
'page_duplicated'),
4488 private function next_content_sorter(
$db, $folder_id) {
4489 $folder_id = (int)$folder_id;
4490 $rows =
$db->select(dbxContentLng::ddContent(),
'folder = ' . $folder_id,
'*',
'sorter,id',
'DESC',
'', 1, 0, 0);
4493 $max = (int)(
$rows[0][
'sorter'] ?? 0);
4495 return sprintf(
'%04d', $max + 10);
4498 private function new_folder_json() {
4499 $texts = $this->cms_texts();
4500 $parent = (int)
dbx()->get_modul_var(
'parent', 0,
'int');
4501 $db =
dbx()->get_system_obj(
'dbxDB');
4502 $this->ensure_cms_schema(
$db);
4503 $sorter = $this->next_folder_sorter(
$db, $parent);
4505 if (
$db->insert(dbxContentLng::ddFolder(), array(
4506 'name' =>
$texts->format_fd_message(
'new_folder_title', array(
'time' => date(
'H:i'))),
4507 'parent_id' => $parent,
4508 'sorter' => $sorter,
4509 'group_read' => $parent > 0 ?
'parent' :
'*',
4510 'template' => $parent > 0 ?
'parent' :
'c-content',
4511 'hero_template' => $parent > 0 ?
'parent' :
'image-hero',
4512 'hero_image_id' =>
'parent',
4513 'hero_margin_top' =>
'parent',
4514 'hero_height' =>
'parent',
4515 'hero_variant' =>
'parent',
4516 'hero_sticky' =>
'parent',
4517 'hero_scroll_layer' =>
'parent',
4519 $id =
$db->get_insert_id();
4522 dbxContentLngSync::afterFolderSave(
$db, $id,
true);
4523 $this->flush_menu_cache();
4524 $this->cms_json_response(array(
4528 'row' =>
$db->select1(dbxContentLng::ddFolder(), $id),
4529 'open_lng_provision' => $this->lng_provision_open_flag(
$db,
'folder', $id),
4532 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'folder_create_error')));
4535 private function save_folder_json() {
4536 $texts = $this->cms_texts();
4537 $payload = $this->request_json();
4538 $id = (int)($payload[
'id'] ?? 0);
4539 $db =
dbx()->get_system_obj(
'dbxDB');
4540 $this->ensure_cms_schema(
$db);
4542 $name = $this->clean_text($payload[
'name'] ??
'', 120);
4543 $parent_id = (int)($payload[
'parent_id'] ?? 0);
4545 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'folder_name_required')));
4547 if ($parent_id < 0) $parent_id = 0;
4548 if ($id > 0 && $parent_id === $id) {
4549 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'folder_self_parent')));
4551 if ($id > 0 && $this->folder_is_descendant(
$db, $parent_id, $id)) {
4552 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'folder_descendant_parent')));
4555 $rights = $this->clean_text($payload[
'group_read'] ??
'', 512);
4556 if ($rights ===
'') $rights = $parent_id > 0 ?
'parent' :
'*';
4558 $old = $id > 0 ?
$db->select1(dbxContentLng::ddFolder(), $id,
'*', 0) : array();
4561 'parent_id' => $parent_id,
4562 'group_read' => $rights,
4563 'template' => $this->clean_text($payload[
'template'] ??
'parent', 254),
4564 'hero_template' => $this->clean_text($payload[
'hero_template'] ??
'parent', 80),
4565 'hero_image_id' => $this->clean_text($payload[
'hero_image_id'] ??
'parent', 32),
4566 'hero_margin_top' => $this->clean_text($payload[
'hero_margin_top'] ??
'parent', 32),
4567 'hero_height' => $this->clean_text($payload[
'hero_height'] ??
'parent', 32),
4568 'hero_variant' => $this->clean_text($payload[
'hero_variant'] ??
'parent', 32),
4569 'hero_sticky' => $this->clean_text($payload[
'hero_sticky'] ??
'parent', 32),
4570 'hero_scroll_layer' => $this->clean_text($payload[
'hero_scroll_layer'] ??
'parent', 32),
4572 $data = $this->normalize_hero_payload($data);
4573 if ($id <= 0 || (is_array($old) && (
int)($old[
'parent_id'] ?? 0) !== $parent_id)) {
4574 $data[
'sorter'] = $this->next_folder_sorter(
$db, $parent_id);
4578 $ok =
$db->update(dbxContentLng::ddFolder(), $data, $id);
4581 $ok =
$db->insert(dbxContentLng::ddFolder(), $data);
4582 $saved_id = (
$ok === 1) ?
$db->get_insert_id() : 0;
4585 if (
$ok === 1 && $saved_id > 0) {
4586 $this->sync_saved_hero_media_usage(
$db, 0, $saved_id, $data[
'hero_image_id']);
4587 dbxContentLngSync::afterFolderSave(
$db, $saved_id, $id <= 0);
4588 $syncResult = array(
'updated' => array(),
'skipped' => array(),
'errors' => array());
4589 if ($id > 0 && dbxContentLngSync::isMasterLng()) {
4590 dbxContentTranslate::clearWarnings();
4591 $syncResult = dbxContentLngSync::syncSlavesFromMaster(
$db,
'folder', $saved_id);
4592 $syncResult[
'translate_warnings'] = dbxContentTranslate::warnings();
4593 $this->flush_lng_sync_cache(
$db, $syncResult);
4595 $this->flush_folder_cache(
$db, $saved_id);
4596 if (is_array($old) && (
int)($old[
'parent_id'] ?? 0) !== $parent_id) {
4597 $this->flush_folder_cache(
$db, (
int)($old[
'parent_id'] ?? 0));
4599 $this->cms_json_response(array(
4603 'row' =>
$db->select1(dbxContentLng::ddFolder(), $saved_id),
4604 'lng_forked' => dbxContentLngSync::isMasterLng() ? 0 : 1,
4605 'lng_synced' => count($syncResult[
'updated'] ?? array()),
4606 'lng_translate_provider' => dbxContentTranslate::provider(),
4607 'lng_sync_targets' => dbxContentLngSync::isMasterLng() ? dbxContentLngSync::slaveLngs() : array(),
4608 'lng_sync_updated' => is_array($syncResult[
'updated'] ??
null) ? $syncResult[
'updated'] : array(),
4609 'lng_sync_skipped' => is_array($syncResult[
'skipped'] ??
null) ? $syncResult[
'skipped'] : array(),
4610 'lng_sync_errors' => is_array($syncResult[
'errors'] ??
null) ? $syncResult[
'errors'] : array(),
4611 'translate_warnings' => is_array($syncResult[
'translate_warnings'] ??
null) ? $syncResult[
'translate_warnings'] : array(),
4612 'open_lng_provision' => $this->lng_provision_open_flag(
$db,
'folder', $saved_id),
4615 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'folder_save_error')));
4618 private function delete_folder_json() {
4619 $texts = $this->cms_texts();
4620 $payload = $this->request_json();
4621 $id = (int)($payload[
'id'] ?? 0);
4623 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'folder_none_selected')));
4626 $db =
dbx()->get_system_obj(
'dbxDB');
4627 $deleteLngs = $this->normalize_delete_lngs($payload);
4628 $result = $this->delete_folder_in_lngs(
$db, $id, $deleteLngs);
4630 if ((
int)(
$result[
'ok'] ?? 0) === 1) {
4631 $this->cms_json_response(array(
4635 'deleted' =>
$result[
'deleted'] ?? array(),
4636 'warnings' =>
$result[
'errors'] ?? array(),
4640 $msg = count(
$result[
'errors'] ?? array()) ? implode(
' ',
$result[
'errors']) :
$texts->get_fd_message(
'folder_delete_error');
4641 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' => $msg));
4644 private function delete_page_json() {
4645 $texts = $this->cms_texts();
4646 $payload = $this->request_json();
4647 $id = (int)($payload[
'id'] ??
dbx()->get_modul_var(
'id', 0,
'int'));
4649 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
$texts->get_fd_message(
'page_none_selected')));
4652 $db =
dbx()->get_system_obj(
'dbxDB');
4653 $deleteLngs = $this->normalize_delete_lngs($payload);
4654 $result = $this->delete_page_in_lngs(
$db, $id, $deleteLngs);
4656 if ((
int)(
$result[
'ok'] ?? 0) === 1) {
4657 $this->flush_menu_cache();
4658 $this->cms_json_response(array(
4662 'deleted' =>
$result[
'deleted'] ?? array(),
4663 'warnings' =>
$result[
'errors'] ?? array(),
4667 $msg = count(
$result[
'errors'] ?? array()) ? implode(
' ',
$result[
'errors']) :
$texts->get_fd_message(
'page_delete_error');
4668 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' => $msg));
4671 private function move_node_json() {
4672 $payload = $this->request_json();
4673 $type = $this->clean_text($payload[
'type'] ??
'', 16);
4674 $id = (int)($payload[
'id'] ?? 0);
4675 $target = (int)($payload[
'target_folder'] ?? 0);
4676 $before_id = (int)($payload[
'before_id'] ?? 0);
4677 $after_id = (int)($payload[
'after_id'] ?? 0);
4679 if ($id <= 0 ||
$target < 0 || !in_array($type, array(
'folder',
'page'),
true)) {
4680 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ungueltige Tree-Verschiebung.'));
4683 if ($type ===
'folder' && $id ===
$target) {
4684 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ordner kann nicht in sich selbst verschoben werden.'));
4687 $db =
dbx()->get_system_obj(
'dbxDB');
4688 if ($type ===
'folder') {
4689 if ($this->folder_is_descendant(
$db,
$target, $id)) {
4690 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ordner kann nicht in einen eigenen Unterordner verschoben werden.'));
4692 $data = array(
'parent_id' =>
$target);
4693 if ($before_id > 0 || $after_id > 0) {
4694 $data[
'sorter'] = $this->next_folder_sorter(
$db,
$target);
4696 $current =
$db->select1(dbxContentLng::ddFolder(), $id,
'*', 0);
4697 if (is_array($current) && (
int)($current[
'parent_id'] ?? 0) !==
$target) {
4698 $data[
'sorter'] = $this->next_folder_sorter(
$db,
$target);
4701 $ok =
$db->update(dbxContentLng::ddFolder(), $data, $id);
4702 if (
$ok >= 0 && ($before_id > 0 || $after_id > 0)) {
4703 $this->reorder_folders(
$db,
$target, $id, $before_id, $after_id);
4706 $data = array(
'folder' =>
$target);
4707 if ($before_id <= 0 && $after_id <= 0) {
4708 $current =
$db->select1(dbxContentLng::ddContent(), $id,
'*', 0);
4709 if (is_array($current) && (
int)($current[
'folder'] ?? 0) !==
$target) {
4710 $data[
'sorter'] = $this->next_content_sorter(
$db,
$target);
4713 $ok =
$db->update(dbxContentLng::ddContent(), $data, $id);
4714 if (
$ok >= 0 && ($before_id > 0 || $after_id > 0)) {
4715 $this->reorder_content_pages(
$db,
$target, $id, $before_id, $after_id);
4720 if ($type ===
'page') {
4721 $this->flush_saved_page_cache(
$db, $id);
4723 $this->flush_folder_cache(
$db, $id);
4726 $this->cms_json_response(array(
'ok' => 1,
'success' =>
true,
'id' => $id,
'type' => $type,
'target_folder' =>
$target));
4729 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Tree-Eintrag konnte nicht verschoben werden.'));
4732 private function folder_is_descendant(
$db, $folder_id, $ancestor_id) {
4733 $folder_id = (int)$folder_id;
4734 $ancestor_id = (int)$ancestor_id;
4736 while ($folder_id > 0 && $guard < 100) {
4737 if ($folder_id === $ancestor_id)
return true;
4738 $row =
$db->select1(dbxContentLng::ddFolder(), $folder_id);
4739 if (!is_array($row))
return false;
4740 $folder_id = (int)($row[
'parent_id'] ?? 0);
4746 private function next_folder_sorter(
$db, $parent_id) {
4747 $parent_id = (int)$parent_id;
4748 $rows =
$db->select(dbxContentLng::ddFolder(),
'parent_id = ' . $parent_id,
'*',
'sorter,id',
'DESC',
'', 1, 0, 0);
4751 $max = (int)(
$rows[0][
'sorter'] ?? 0);
4753 return sprintf(
'%04d', $max + 10);
4756 private function reorder_folders(
$db, $parent_id, $moved_id, $before_id = 0, $after_id = 0) {
4757 $parent_id = (int)$parent_id;
4758 $moved_id = (int)$moved_id;
4759 $rows =
$db->select(dbxContentLng::ddFolder(),
'parent_id = ' . $parent_id,
'*',
'sorter,name,id',
'ASC');
4760 if (!is_array(
$rows))
return;
4764 foreach (
$rows as $row) {
4765 if (!is_array($row))
continue;
4766 $rid = (int)($row[
'id'] ?? 0);
4767 if ($rid === $moved_id) {
4773 if (!$moved)
return;
4777 foreach ($ordered as $row) {
4778 $rid = (int)($row[
'id'] ?? 0);
4779 if ($before_id > 0 && $rid === $before_id) {
4784 if ($after_id > 0 && $rid === $after_id) {
4792 foreach (
$out as $row) {
4793 $rid = (int)($row[
'id'] ?? 0);
4794 if ($rid <= 0)
continue;
4795 $db->update(dbxContentLng::ddFolder(), array(
'sorter' => sprintf(
'%04d', $sort)), $rid);
4800 private function reorder_content_pages(
$db, $folder_id, $moved_id, $before_id = 0, $after_id = 0) {
4801 $folder_id = (int)$folder_id;
4802 $moved_id = (int)$moved_id;
4803 $rows =
$db->select(dbxContentLng::ddContent(),
'folder = ' . $folder_id,
'*',
'sorter,title,id',
'ASC');
4804 if (!is_array(
$rows))
return;
4808 foreach (
$rows as $row) {
4809 if (!is_array($row))
continue;
4810 $rid = (int)($row[
'id'] ?? 0);
4811 if ($rid === $moved_id) {
4817 if (!$moved)
return;
4821 foreach ($ordered as $row) {
4822 $rid = (int)($row[
'id'] ?? 0);
4823 if ($before_id > 0 && $rid === $before_id) {
4828 if ($after_id > 0 && $rid === $after_id) {
4837 foreach (
$out as $row) {
4838 $rid = (int)($row[
'id'] ?? 0);
4839 if ($rid <= 0)
continue;
4840 $sorter = sprintf(
'%04d', $sort);
4841 if ((
string)($row[
'sorter'] ??
'') !== $sorter) {
4842 $db->update(dbxContentLng::ddContent(), array(
'sorter' => $sorter), $rid);
4847 foreach ($changed as $rid) {
4848 dbxContentPageCache::invalidateContent((
int)$rid);
4850 if (count($changed)) {
4851 dbxContentPageCache::invalidateAllMenus();
4855 private function media_usage_page_map(
$db, $usage_rows) {
4857 if (!is_array($usage_rows))
return $map;
4859 $page_cache = array();
4860 $folder_cache = array();
4862 foreach ($usage_rows as $usage) {
4863 if (!is_array($usage))
continue;
4864 $media_id = (int)($usage[
'media_id'] ?? 0);
4865 $content_id = (int)($usage[
'content_id'] ?? 0);
4866 $content_lng = dbxContentMediaUsageScope::language((
string)($usage[
'content_lng'] ??
''));
4867 if ($media_id <= 0 || $content_id <= 0)
continue;
4869 $page_key = $content_lng .
':' . $content_id;
4870 if (!array_key_exists($page_key, $page_cache)) {
4871 $page_cache[$page_key] =
$db->select1(dbxContentLng::ddContent($content_lng), $content_id,
'id,folder,title', 0);
4873 $page = is_array($page_cache[$page_key]) ? $page_cache[$page_key] : array();
4874 $folder_id = (int)(
$page[
'folder'] ?? ($usage[
'folder_id'] ?? 0));
4876 $folder_key = $content_lng .
':' . $folder_id;
4877 if ($folder_id > 0 && !array_key_exists($folder_key, $folder_cache)) {
4878 $folder_cache[$folder_key] =
$db->select1(dbxContentLng::ddFolder($content_lng), $folder_id,
'*', 0);
4880 $folder = ($folder_id > 0 && is_array($folder_cache[$folder_key] ??
null)) ? $folder_cache[$folder_key] : array();
4882 if (!isset($map[$media_id])) $map[$media_id] = array();
4883 if (!isset($map[$media_id][$page_key])) {
4884 $map[$media_id][$page_key] = array(
4885 'id' => $content_id,
4886 'content_id' => $content_id,
4887 'content_lng' => $content_lng,
4888 'title' => (
string)(
$page[
'title'] ??
''),
4889 'folder_id' => $folder_id,
4890 'folder_title' => (
string)($folder[
'name'] ?? $folder[
'title'] ??
''),
4895 $slot = trim((
string)($usage[
'slot'] ??
''));
4896 if ($slot !==
'' && !in_array($slot, $map[$media_id][$page_key][
'slots'],
true)) {
4897 $map[$media_id][$page_key][
'slots'][] = $slot;
4901 foreach ($map as $media_id =>
$items) {
4902 ksort(
$items, SORT_NUMERIC);
4903 $map[$media_id] = array_values(
$items);
4909 private function media_json() {
4910 $content_id = (int)
dbx()->get_modul_var(
'content_id', 0,
'int');
4911 $folder_id = (int)
dbx()->get_modul_var(
'folder_id', 0,
'int');
4912 $images = (int)
dbx()->get_modul_var(
'images', 0,
'int');
4913 $sync = (int)
dbx()->get_modul_var(
'sync', 0,
'int');
4914 $media_type = strtolower(trim((
string)
dbx()->get_modul_var(
'media_type',
'',
'varchar')));
4915 $raw_media_folder = trim((
string)
dbx()->get_modul_var(
'media_folder',
'',
'varchar'));
4916 $media_folder = $raw_media_folder !==
'' && strtolower($raw_media_folder) !==
'all'
4917 ? $this->clean_media_folder($raw_media_folder, $media_type ?: $this->media_type_from_folder($raw_media_folder))
4919 $slot = trim((
string)
dbx()->get_modul_var(
'slot',
'',
'varchar'));
4920 $query = trim((
string)
dbx()->get_modul_var(
'query',
'',
'varchar'));
4921 $usage_only = (int)
dbx()->get_modul_var(
'usage', 0,
'int');
4922 $limit = max(0, min(200, (
int)
dbx()->get_modul_var(
'limit', 0,
'int')));
4923 $offset = max(0, (
int)
dbx()->get_modul_var(
'offset', 0,
'int'));
4924 $db =
dbx()->get_system_obj(
'dbxDB');
4925 $this->ensure_cms_schema(
$db);
4926 if ($sync) $this->sync_cms_media_files(
$db);
4927 $where =
'active = 1' . $this->cms_media_exclude_sql();
4929 $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')";
4931 if (in_array($media_type, array(
'image',
'video',
'external_video',
'file'),
true)) {
4932 $where .=
" AND media_type = '" . str_replace(
"'",
"''", $media_type) .
"'";
4934 if ($media_folder !==
'') {
4935 $where .=
" AND media_folder = '" . str_replace(
"'",
"''", $media_folder) .
"'";
4937 if ($query !==
'') {
4938 $q = str_replace(
"'",
"''", $query);
4939 $where .=
" AND (title LIKE '%$q%' OR alt LIKE '%$q%' OR caption LIKE '%$q%' OR tags LIKE '%$q%' OR file_name LIKE '%$q%')";
4941 $select_limit = $limit > 0 ? $limit + 1 : 0;
4942 $rows =
$db->select($this->dd_media, $where,
'*',
'media_folder,title,id',
'ASC',
'', $select_limit, $offset, 0);
4944 $has_more = $limit > 0 && count(
$rows) > $limit;
4945 if ($has_more)
$rows = array_slice(
$rows, 0, $limit);
4946 $page_row_count = count(
$rows);
4947 if ($limit <= 0)
$rows = $this->filter_existing_media(
$db,
$rows);
4949 $usage_where =
'active = 1';
4951 $page_media_ids = array_values(array_filter(array_map(
function($row) {
4952 return (
int)($row[
'id'] ?? 0);
4954 $usage_where .= $page_media_ids
4955 ?
' AND media_id IN (' . implode(
',', $page_media_ids) .
')'
4958 $usage_rows =
$db->select($this->dd_media_usage, $usage_where,
'*',
'media_id,id',
'ASC',
'', 0, 0, 0);
4959 $usage_count = array();
4960 $usage_pages = $this->media_usage_page_map(
$db, $usage_rows);
4961 $current_usage = array();
4962 $current_usage_row = array();
4963 $has_usage_context = $content_id > 0 || $folder_id > 0 || $slot !==
'';
4964 if (is_array($usage_rows)) {
4965 foreach ($usage_rows as $usage) {
4966 if (!is_array($usage))
continue;
4967 $mid = (int)($usage[
'media_id'] ?? 0);
4968 if ($mid <= 0)
continue;
4969 $usage_count[$mid] = ($usage_count[$mid] ?? 0) + 1;
4970 if ($has_usage_context
4971 && dbxContentMediaUsageScope::language((
string)($usage[
'content_lng'] ??
'')) === dbxContentLng::current()
4972 && ($content_id <= 0 || (
int)($usage[
'content_id'] ?? 0) === $content_id)
4973 && ($folder_id <= 0 || (
int)($usage[
'folder_id'] ?? 0) === $folder_id)
4974 && ($slot ===
'' || (
string)($usage[
'slot'] ??
'') === $slot)) {
4975 $current_usage[$mid] = (int)($usage[
'id'] ?? 0);
4976 $current_usage_row[$mid] = $usage;
4980 $rows = array_map(
function($row) use ($usage_count, $usage_pages, $current_usage, $current_usage_row) {
4981 $row = $this->normalize_media_row($row);
4982 $id = (int)($row[
'id'] ?? 0);
4983 $row[
'used_count'] = (int)($usage_count[$id] ?? 0);
4984 $row[
'usage_pages'] = $usage_pages[$id] ?? array();
4985 $row[
'current_usage_id'] = (int)($current_usage[$id] ?? 0);
4986 if (!empty($current_usage_row[$id])) {
4987 $usage = $current_usage_row[$id];
4988 $row[
'usage_id'] = (int)($usage[
'id'] ?? 0);
4989 $row[
'slot'] = (string)($usage[
'slot'] ?? $row[
'slot'] ??
'gallery');
4990 $row[
'sorter'] = (string)($usage[
'sorter'] ?? $row[
'sorter'] ??
'');
4991 if (!empty($usage[
'template'])) $row[
'template'] = $usage[
'template'];
4992 if (!empty($usage[
'caption'])) $row[
'caption'] = $usage[
'caption'];
4996 if ($usage_only && ($content_id > 0 || $folder_id > 0 || $slot !==
'')) {
4997 $rows = array_values(array_filter(
$rows,
function($row) {
4998 return (
int)($row[
'current_usage_id'] ?? 0) > 0;
5001 $this->cms_json_response(array(
5004 'has_more' => $has_more ? 1 : 0,
5005 'next_offset' => $limit > 0 ? $offset + $page_row_count : 0,
5009 private function media_usage_rows_for_context(
$db, $content_id = 0, $folder_id = 0, $slot =
'', $content_lng =
'') {
5010 $content_id = (int)$content_id;
5011 $folder_id = (int)$folder_id;
5012 $where = dbxContentMediaUsageScope::withLanguage(
'active = 1', $content_lng);
5013 if ($content_id > 0) $where .=
' AND content_id = ' . $content_id;
5014 if ($folder_id > 0) $where .=
' AND folder_id = ' . $folder_id;
5015 if ($slot !==
'') $where .=
" AND slot = '" . str_replace(
"'",
"''", (
string)$slot) .
"'";
5017 $usage_rows =
$db->select($this->dd_media_usage, $where,
'*',
'slot,sorter,id',
'ASC',
'', 0, 0, 0);
5018 if (!is_array($usage_rows))
return array();
5021 foreach ($usage_rows as $usage) {
5022 if (!is_array($usage))
continue;
5023 $media_id = (int)($usage[
'media_id'] ?? 0);
5024 if ($media_id <= 0)
continue;
5025 $row =
$db->select1($this->dd_media, $media_id);
5026 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1)
continue;
5027 if (!$this->media_file_exists($row))
continue;
5028 $row = $this->normalize_media_row($row);
5029 $row[
'usage_id'] = (int)($usage[
'id'] ?? 0);
5030 $row[
'current_usage_id'] = (int)($usage[
'id'] ?? 0);
5031 $row[
'slot'] = (string)($usage[
'slot'] ?? $row[
'slot'] ??
'gallery');
5032 $row[
'sorter'] = (string)($usage[
'sorter'] ?? $row[
'sorter'] ??
'');
5033 if (!empty($usage[
'template'])) $row[
'template'] = $usage[
'template'];
5034 if (!empty($usage[
'caption'])) $row[
'caption'] = $usage[
'caption'];
5041 private function inline_media_block_has_content($inner) {
5042 $inner = trim((
string)$inner);
5043 if ($inner ===
'')
return false;
5044 if (preg_match(
'/<(img|video|iframe|source)\b/i', $inner))
return true;
5045 if (preg_match(
'/\bdbx-cms-inline-video-thumb\b/i', $inner))
return true;
5046 if (preg_match(
'/\bdbx-cms-inline-video-empty\b/i', $inner))
return true;
5047 if (preg_match(
'/\bdbx-cms-inline-media-missing\b/i', $inner))
return true;
5051 private function strip_empty_inline_media_wrappers(
$html) {
5053 $pattern =
'/<(p|figure|div)\b([^>]*(?:\bdbx-cms-inline-media\b|\bdata-cms-media-id=)[^>]*)>([\s\S]*?)<\/\1>/i';
5055 $next = preg_replace_callback(
5058 if ($this->inline_media_block_has_content($m[3] ??
''))
return $m[0];
5063 if ($next ===
null || $next ===
$html)
break;
5069 private function inline_media_ids(
$html) {
5070 $html = preg_replace(
'/<!--[\s\S]*?-->/',
'', (
string)
$html);
5073 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)) {
5074 foreach ($blocks as $block) {
5075 $inner = (string)($block[3] ??
'');
5076 if (!$this->inline_media_block_has_content($inner))
continue;
5077 if (preg_match_all(
'/<(?:img|video|source|iframe)\b[^>]*\bsrc=["\'][^"\']*dbx_mid=([0-9]+)/i', $inner, $inner_ids)) {
5078 foreach ($inner_ids[1] as $id) {
5080 if ($id > 0) $ids[$id] = $id;
5084 if (preg_match(
'/\bdata-cms-media-id=["\']?([0-9]+)/i', (
string)($block[0] ??
''), $id_match)) {
5085 $id = (int)$id_match[1];
5086 if ($id > 0) $ids[$id] = $id;
5090 if (preg_match_all(
'/<(?:img|video|source|iframe)\b[^>]*\bdata-cms-media-id=["\']?([0-9]+)/i',
$html, $m)) {
5091 foreach ($m[1] as $id) {
5093 if ($id > 0) $ids[$id] = $id;
5096 if (preg_match_all(
'/<(?:img|video|source|iframe)\b[^>]*\bsrc=["\'][^"\']*dbx_mid=([0-9]+)/i',
$html, $m)) {
5097 foreach ($m[1] as $id) {
5099 if ($id > 0) $ids[$id] = $id;
5102 return array_values($ids);
5105 private function resolve_inline_media_ids(
$html, $client_ids =
null, $client_ids_provided =
false) {
5106 $html_ids = array();
5107 foreach ($this->inline_media_ids(
$html) as $id) {
5109 if ($id > 0) $html_ids[$id] = $id;
5111 return array_values($html_ids);
5114 private function sync_inline_media_usage(
$db, $content_id,
$html, $folder_id = 0, $client_ids =
null, $client_ids_provided =
false, $content_lng =
'') {
5115 $content_id = (int)$content_id;
5116 $folder_id = (int)$folder_id;
5117 $content_lng = dbxContentMediaUsageScope::language((
string)$content_lng);
5118 $result = array(
'added' => 0,
'removed' => 0,
'kept' => 0);
5119 if ($content_id <= 0)
return $result;
5122 foreach ($this->resolve_inline_media_ids(
$html, $client_ids, $client_ids_provided) as $id) {
5124 if ($id > 0) $wanted[$id] = $id;
5128 $this->dd_media_usage,
5129 dbxContentMediaUsageScope::withLanguage(
'content_id = ' . $content_id .
" AND active = 1 AND slot = 'inline'", $content_lng),
5133 if (is_array(
$rows)) {
5134 foreach (
$rows as $row) {
5135 if (!is_array($row))
continue;
5136 $usage_id = (int)($row[
'id'] ?? 0);
5137 $media_id = (int)($row[
'media_id'] ?? 0);
5138 if ($usage_id > 0 && $media_id > 0 && !isset($wanted[$media_id])) {
5139 $db->update($this->dd_media_usage, array(
'active' => 0), $usage_id, 0, 1, 1, 0);
5145 foreach ($wanted as $id) {
5146 $row =
$db->select1($this->dd_media, $id);
5147 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1)
continue;
5150 $this->dd_media_usage,
5151 dbxContentMediaUsageScope::withLanguage(
'media_id = ' . $id .
' AND content_id = ' . $content_id .
" AND slot = 'inline'", $content_lng),
5152 '*',
'active DESC, id DESC',
'', 0, 0, 0
5155 $active_usage =
null;
5156 $inactive_usage =
null;
5159 if (!is_array($usage))
continue;
5160 if ((
int)($usage[
'active'] ?? 0) === 1) {
5161 if (!$active_usage) {
5162 $active_usage = $usage;
5164 $dup_id = (int)($usage[
'id'] ?? 0);
5166 $db->update($this->dd_media_usage, array(
'active' => 0), $dup_id, 0, 1, 1, 0);
5172 if (!$inactive_usage) $inactive_usage = $usage;
5176 if ($active_usage) {
5177 $usage_id = (int)($active_usage[
'id'] ?? 0);
5178 if ($usage_id > 0 && (
int)($active_usage[
'folder_id'] ?? 0) !== $folder_id) {
5179 $db->update($this->dd_media_usage, array(
'folder_id' => $folder_id), $usage_id, 0, 1, 1, 0);
5185 if ($inactive_usage) {
5186 $usage_id = (int)($inactive_usage[
'id'] ?? 0);
5187 if ($usage_id > 0) {
5188 $db->update($this->dd_media_usage, array(
'active' => 1,
'folder_id' => $folder_id), $usage_id, 0, 1, 1, 0);
5194 if ($this->create_media_usage(
$db, $id, $content_id, $folder_id,
'inline',
'image-inline',
'',
'', $content_lng) > 0) {
5202 private function next_media_sorter(
$db, $content_id, $slot) {
5203 $content_id = (int)$content_id;
5204 $slot = str_replace(
"'",
"''", (
string)$slot);
5205 $rows =
$db->select($this->dd_media,
"content_id = " . $content_id .
" AND slot = '" . $slot .
"' AND active = 1",
'*',
'sorter,id',
'DESC',
'', 1, 0, 0);
5208 $max = (int)(
$rows[0][
'sorter'] ?? 0);
5210 return sprintf(
'%04d', $max + 10);
5213 private function deactivate_existing_hero_media(
$db, $content_id, $folder_id, $except_id = 0, $content_lng =
'') {
5214 $content_id = (int)$content_id;
5215 $folder_id = (int)$folder_id;
5216 $except_id = (int)$except_id;
5217 $content_lng = dbxContentMediaUsageScope::language((
string)$content_lng);
5218 $except = $except_id > 0 ?
' AND media_id <> ' . $except_id :
'';
5219 if ($content_id > 0) {
5220 $db->update($this->dd_media_usage, array(
'active' => 0), dbxContentMediaUsageScope::withLanguage(
"content_id = " . $content_id .
" AND slot = 'hero' AND active = 1" . $except, $content_lng), 0, 1, 1, 0);
5221 } elseif ($folder_id > 0) {
5222 $db->update($this->dd_media_usage, array(
'active' => 0), dbxContentMediaUsageScope::withLanguage(
"folder_id = " . $folder_id .
" AND slot = 'hero' AND active = 1" . $except, $content_lng), 0, 1, 1, 0);
5226 private function sync_saved_hero_media_usage(
$db, $content_id, $folder_id, $hero_image_id) {
5227 $content_id = (int)$content_id;
5228 $folder_id = (int)$folder_id;
5229 $hero_id = (int)$hero_image_id;
5230 if ($content_id <= 0 && $folder_id <= 0)
return;
5232 $this->deactivate_existing_hero_media(
$db, $content_id, $folder_id, $hero_id);
5235 $this->deactivate_existing_hero_media(
$db, $content_id, $folder_id, 0);
5238 private function is_no_hero_template(
$value) {
5240 return in_array(
$value, array(
'none',
'no-hero',
'0',
'off'),
true);
5243 private function normalize_hero_payload(array $data) {
5244 if ($this->is_no_hero_template($data[
'hero_template'] ??
'')) {
5245 $data[
'hero_template'] =
'none';
5246 $data[
'hero_image_id'] =
'0';
5251 private function source_media_file($row) {
5252 $rel = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
5253 if ($rel ===
'' || strpos($rel,
'..') !==
false)
return '';
5257 if (!
$base)
return '';
5260 $real = realpath(
$file);
5261 return $real && strpos($real,
$base) === 0 && is_file($real) && is_readable($real) ? $real :
'';
5264 private function copy_media_to_slot(
$db, array $row, $slot) {
5266 $slot = $this->valid_media_slot($slot);
5267 $src = $this->source_media_file($row);
5268 if ($src ===
'')
return 0;
5270 $dir = $this->cms_media_dir($slot);
5271 if (!is_dir(
$dir)) @mkdir(
$dir, 0777,
true);
5272 if (!is_dir(
$dir))
return 0;
5274 $name = $this->unique_media_name((
string)($row[
'file_name'] ?? basename($src)), $slot);
5275 $dst = rtrim(
$dir,
'/\\') . DIRECTORY_SEPARATOR . $name;
5276 if (!copy($src, $dst))
return 0;
5278 $rel = $this->media_rel_dir($slot) . $name;
5279 $mime = (string)($row[
'mime'] ??
'');
5282 $img = @getimagesize($dst);
5283 if (is_array($img)) {
5284 $width = (int)($img[0] ?? 0);
5285 $height = (int)($img[1] ?? 0);
5290 'content_id' => (
int)($row[
'content_id'] ?? 0),
5291 'folder_id' => (
int)($row[
'folder_id'] ?? 0),
5294 'sorter' => $this->next_media_sorter(
$db, (
int)($row[
'content_id'] ?? 0), $slot),
5295 'template' => (
string)($row[
'template'] ??
''),
5296 'title' => (
string)($row[
'title'] ?? pathinfo($name, PATHINFO_FILENAME)),
5297 'alt' => (
string)($row[
'alt'] ?? $row[
'title'] ??
''),
5298 'caption' => (
string)($row[
'caption'] ??
''),
5299 'file_name' => $name,
5300 'file_path' => $rel,
5302 'size' => (
int)@filesize($dst),
5304 'height' => $height,
5305 'tags' => (
string)($row[
'tags'] ??
''),
5306 'media_type' => $this->media_type(array(
'mime' => $mime,
'file_name' => $name)),
5308 $thumb = $this->create_media_thumbnail($dst, $slot, $name, $mime);
5309 if ($thumb) $insert = array_merge($insert, $thumb);
5310 return (
$db->insert($this->dd_media, $insert) === 1) ?
$db->get_insert_id() : 0;
5313 private function set_media_slot_json() {
5314 $payload = $this->request_json();
5315 $usage_id = (int)($payload[
'usage_id'] ?? 0);
5316 $id = (int)($payload[
'media_id'] ?? $payload[
'id'] ?? 0);
5317 $slot = $this->valid_media_slot($payload[
'slot'] ??
'gallery');
5318 if ($usage_id <= 0 && $id <= 0) $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Keine Medien- oder Usage-ID erhalten.'));
5320 $db =
dbx()->get_system_obj(
'dbxDB');
5321 $this->ensure_cms_schema(
$db);
5322 if ($usage_id <= 0) {
5323 $rows =
$db->select($this->dd_media_usage, $this->usage_where($id),
'*',
'id',
'DESC',
'', 1, 0, 0);
5324 if (is_array(
$rows) && isset(
$rows[0])) $usage_id = (int)(
$rows[0][
'id'] ?? 0);
5326 if ($usage_id <= 0) {
5327 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Keine vorhandene Zuordnung gefunden.'));
5330 $usage =
$db->select1($this->dd_media_usage, $usage_id);
5331 if (!is_array($usage) || (
int)($usage[
'active'] ?? 0) !== 1) {
5332 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Zuordnung nicht gefunden.'));
5334 if ($slot ===
'hero') $this->deactivate_existing_hero_media(
$db, (
int)($usage[
'content_id'] ?? 0), (
int)($usage[
'folder_id'] ?? 0), (
int)($usage[
'media_id'] ?? 0));
5335 $ok =
$db->update($this->dd_media_usage, array(
'slot' => $slot), $usage_id);
5336 $usage = $this->normalize_usage_row(
$db->select1($this->dd_media_usage, $usage_id));
5337 $row = $this->normalize_media_row(
$db->select1($this->dd_media, (
int)($usage[
'media_id'] ?? 0)));
5339 $this->flush_media_cache(
$db, (
int)($usage[
'content_id'] ?? 0), (
int)($usage[
'folder_id'] ?? 0));
5341 $this->cms_json_response(array(
'ok' => (
$ok >= 0 ? 1 : 0),
'success' => (
$ok >= 0),
'row' => $row,
'usage' => $usage,
'msg' =>
'Zuordnung aktualisiert.'));
5344 private function assign_media_json() {
5345 $payload = $this->request_json();
5346 $id = (int)($payload[
'media_id'] ?? $payload[
'id'] ?? 0);
5347 $content_id = (int)($payload[
'content_id'] ?? 0);
5348 $folder_id = (int)($payload[
'folder_id'] ?? 0);
5349 $slot = $this->valid_media_slot($payload[
'slot'] ??
'gallery');
5350 if ($id <= 0 || ($content_id <= 0 && !($folder_id > 0 && $slot ===
'hero'))) {
5351 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Keine Zuordnung erhalten.'));
5354 $db =
dbx()->get_system_obj(
'dbxDB');
5355 $this->ensure_cms_schema(
$db);
5356 $row =
$db->select1($this->dd_media, $id);
5357 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1) {
5358 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Medium nicht gefunden.'));
5361 $usage_id = $this->create_media_usage(
5367 (
string)($payload[
'template'] ?? $row[
'template'] ??
''),
5368 (
string)($payload[
'caption'] ??
''),
5369 is_array($payload[
'settings'] ??
null) ? json_encode($payload[
'settings'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (
string)($payload[
'settings'] ??
'')
5371 if ($usage_id <= 0) $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Medium konnte nicht zugeordnet werden.'));
5372 $usage = $this->normalize_usage_row(
$db->select1($this->dd_media_usage, $usage_id));
5373 $this->flush_media_cache(
$db, $content_id, $folder_id);
5374 $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.'));
5377 private function sort_media_json() {
5378 $payload = $this->request_json();
5379 $content_id = (int)($payload[
'content_id'] ?? 0);
5380 $folder_id = (int)($payload[
'folder_id'] ?? 0);
5381 $slot = $this->valid_media_slot($payload[
'slot'] ??
'gallery');
5382 $ids = $payload[
'ids'] ?? array();
5383 if (($content_id <= 0 && $folder_id <= 0) || !is_array($ids)) {
5384 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Keine Sortierung erhalten.'));
5387 $db =
dbx()->get_system_obj(
'dbxDB');
5388 $this->ensure_cms_schema(
$db);
5390 foreach ($ids as $id) {
5392 if ($id <= 0)
continue;
5393 $where = dbxContentMediaUsageScope::withLanguage(
5394 "active = 1 AND slot = '" . str_replace(
"'",
"''", $slot) .
"'",
5395 dbxContentLng::current()
5397 if ($content_id > 0) $where .=
' AND content_id = ' . $content_id;
5398 if ($folder_id > 0) $where .=
' AND folder_id = ' . $folder_id;
5399 $where .=
' AND (id = ' . $id .
' OR media_id = ' . $id .
')';
5400 $rows =
$db->select($this->dd_media_usage, $where,
'*',
'id',
'ASC',
'', 1, 0, 0);
5401 if (!is_array(
$rows) || !isset(
$rows[0]))
continue;
5402 $db->update($this->dd_media_usage, array(
'sorter' => sprintf(
'%04d', $sort)), (
int)
$rows[0][
'id']);
5405 $this->flush_media_cache(
$db, $content_id, $folder_id);
5406 $this->cms_json_response(array(
'ok' => 1,
'success' =>
true));
5409 private function filter_existing_media(
$db, array
$rows) {
5411 foreach (
$rows as $row) {
5412 if (!is_array($row))
continue;
5413 if ($this->media_file_exists($row)) {
5417 $id = (int)($row[
'id'] ?? 0);
5419 $db->update($this->dd_media, array(
'active' => 0), $id);
5425 private function upload_json() {
5426 $content_length = (int)(
$_SERVER[
'CONTENT_LENGTH'] ?? 0);
5427 $max_upload = $this->upload_max_bytes();
5428 if ($content_length > 0 && $max_upload > 0 && $content_length > $max_upload) {
5429 $this->cms_json_response(array(
5432 'msg' =>
'Upload zu gross. Erlaubt sind maximal ' . round($max_upload / 1024 / 1024) .
' MB.'
5435 if (!$this->verify_media_form(
'upload')) {
5436 $this->cms_json_response(array(
5439 'msg' =>
'Ungueltiger oder abgelaufener Formular-Token.'
5443 $file = $this->first_upload_file();
5445 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Keine Datei erhalten. Bitte Upload-Limit und Dateigroesse pruefen.'));
5448 if ((
$file[
'error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
5449 if ((
$file[
'error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
5450 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Bitte zuerst eine Datei auswaehlen.'));
5452 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Upload fehlgeschlagen.'));
5455 $name = basename((
string)
$file[
'name']);
5456 $name = preg_replace(
'/[^A-Za-z0-9_.-]+/',
'-', $name);
5457 $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
5458 if (!in_array($ext, array(
'jpg',
'jpeg',
'png',
'gif',
'webp',
'svg',
'pdf',
'mp4',
'webm',
'ogv',
'ogg',
'mov',
'm4v'),
true)) {
5459 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Dateityp nicht erlaubt.'));
5462 $db =
dbx()->get_system_obj(
'dbxDB');
5463 $this->ensure_cms_schema(
$db);
5464 $content_id = (int)(
$_POST[
'content_id'] ?? 0);
5465 $folder_id = (int)(
$_POST[
'folder_id'] ?? 0);
5466 $slot = $this->valid_media_slot(
$_POST[
'slot'] ??
'gallery');
5467 $probe_type = $this->media_type(array(
'file_name' => $name,
'mime' => (
string)(
$file[
'type'] ??
'')));
5468 $media_folder = $this->clean_media_folder(
$_POST[
'media_folder'] ??
'', $probe_type);
5469 $media_rel_dir = $this->media_folder_rel_dir($media_folder, $probe_type);
5470 $dir = $this->cms_media_dir($media_folder);
5471 if (!is_dir(
$dir)) {
5472 mkdir(
$dir, 0777,
true);
5475 $dstName = $this->unique_media_name($name);
5476 $dst =
$dir . $dstName;
5478 if (!move_uploaded_file(
$file[
'tmp_name'], $dst)) {
5479 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Datei konnte nicht gespeichert werden.'));
5484 $detected_mime = function_exists(
'mime_content_type') ? (string)@mime_content_type($dst) :
'';
5485 $mime = $detected_mime !==
'' ? $detected_mime : (string)(
$file[
'type'] ??
'');
5486 $img = @getimagesize($dst);
5487 if (is_array($img)) {
5488 $width = (int)($img[0] ?? 0);
5489 $height = (int)($img[1] ?? 0);
5491 $max_image_size = max(800, min(6000, (
int)(
$_POST[
'max_image_size'] ?? 2560)));
5492 if ($width > 0 && $height > 0 && $this->media_type(array(
'mime' => $mime,
'file_name' => $dstName)) ===
'image') {
5493 if ($this->resize_image_to_max($dst, $mime, $max_image_size)) {
5494 clearstatcache(
true, $dst);
5495 $img = @getimagesize($dst);
5496 if (is_array($img)) {
5497 $width = (int)($img[0] ?? 0);
5498 $height = (int)($img[1] ?? 0);
5502 $webp = $this->convert_media_image_to_webp($dst, $dstName, $mime);
5503 if (is_array($webp) && !empty($webp[
'file']) && !empty($webp[
'name'])) {
5504 $dst = (string)$webp[
'file'];
5505 $dstName = (string)$webp[
'name'];
5506 $mime = (string)($webp[
'mime'] ??
'image/webp');
5507 clearstatcache(
true, $dst);
5508 $img = @getimagesize($dst);
5509 if (is_array($img)) {
5510 $width = (int)($img[0] ?? 0);
5511 $height = (int)($img[1] ?? 0);
5516 $media_tpl = $this->clean_text(
$_POST[
'template'] ??
'', 80);
5517 $file_size = (int)@filesize($dst);
5518 $title = pathinfo($name, PATHINFO_FILENAME);
5519 $media_type = $this->media_type(array(
'mime' => $mime,
'file_name' => $dstName));
5520 $media_folder = $this->clean_media_folder($media_folder, $media_type);
5521 if ($media_tpl ===
'') {
5522 $media_tpl = $media_type ===
'video' ? ($slot ===
'hero' ?
'video-hero' :
'video-gallery') :
'';
5525 $existing =
$db->select(
5527 "active = 1 AND storage_type = 'local' AND media_folder = '" . str_replace(
"'",
"''", $media_folder) .
"' AND title = '" . str_replace(
"'",
"''", $title) .
"' AND size = " . $file_size,
5532 if (is_array($existing) && isset($existing[0]) && $this->media_file_exists($existing[0])) {
5534 $row = $this->normalize_media_row($existing[0]);
5536 if ($content_id > 0 || $folder_id > 0) {
5537 $usage_id = $this->create_media_usage(
$db, (
int)($row[
'id'] ?? 0), $content_id, $folder_id, $slot, $media_tpl,
'',
'');
5538 if ($usage_id > 0) $usage = $this->normalize_usage_row(
$db->select1($this->dd_media_usage, $usage_id));
5540 $this->flush_media_cache(
$db, $content_id, $folder_id);
5541 $this->cms_json_response(array(
5544 'duplicate' =>
true,
5547 'files' => array($row[
'file_name'] ??
''),
5560 'template' => $media_tpl,
5563 'file_name' => $dstName,
5564 'file_path' => $media_rel_dir . $dstName,
5566 'size' => $file_size,
5568 'height' => $height,
5569 'media_type' => $media_type,
5570 'storage_type' =>
'local',
5571 'media_folder' => $media_folder,
5573 $thumb = $this->create_media_thumbnail($dst, $media_folder, $dstName, $mime);
5574 if ($thumb) $insert = array_merge($insert, $thumb);
5575 $id = (
$db->insert($this->dd_media, $insert) === 1) ?
$db->get_insert_id() : 0;
5579 if ($content_id > 0 || $folder_id > 0) {
5580 $usage_id = $this->create_media_usage(
$db, $id, $content_id, $folder_id, $slot, $media_tpl,
'',
'');
5581 if ($usage_id > 0) $usage = $this->normalize_usage_row(
$db->select1($this->dd_media_usage, $usage_id));
5583 $row = $this->normalize_media_row(
$db->select1($this->dd_media, $id));
5585 if (!empty($row[
'url'])) {
5586 $baseurl = preg_replace(
'~/[^/]*$~',
'/', $row[
'url']);
5588 $this->flush_media_cache(
$db, $content_id, $folder_id);
5589 $this->cms_json_response(array(
5594 'files' => array($row[
'file_name'] ?? $dstName),
5596 'baseurl' => $baseurl,
5600 $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Mediendatensatz konnte nicht gespeichert werden.'));
5603 private function remove_media_json() {
5604 $id = (int)
dbx()->get_modul_var(
'id', 0,
'int');
5605 $usage_id = (int)
dbx()->get_modul_var(
'usage_id', 0,
'int');
5607 if ($id <= 0 || $usage_id <= 0) {
5608 $payload = $this->request_json();
5609 if ($usage_id <= 0) $usage_id = (int)($payload[
'usage_id'] ?? 0);
5610 if ($id <= 0) $id = (int)($payload[
'media_id'] ?? $payload[
'id'] ?? 0);
5612 if ($usage_id <= 0 && $id <= 0) $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Keine Medien- oder Usage-ID erhalten.'));
5614 $db =
dbx()->get_system_obj(
'dbxDB');
5615 $this->ensure_cms_schema(
$db);
5616 $content_id = (int)($payload[
'content_id'] ??
dbx()->get_modul_var(
'content_id', 0,
'int'));
5617 $folder_id = (int)($payload[
'folder_id'] ??
dbx()->get_modul_var(
'folder_id', 0,
'int'));
5618 if ($usage_id > 0) {
5619 $usage_row =
$db->select1($this->dd_media_usage, $usage_id);
5620 if (is_array($usage_row)) {
5621 if ($content_id <= 0) $content_id = (int)($usage_row[
'content_id'] ?? 0);
5622 if ($folder_id <= 0) $folder_id = (int)($usage_row[
'folder_id'] ?? 0);
5623 if ($id <= 0) $id = (int)($usage_row[
'media_id'] ?? 0);
5625 $ok =
$db->update($this->dd_media_usage, array(
'active' => 0), $usage_id);
5627 $slot = trim((
string)($payload[
'slot'] ??
dbx()->get_modul_var(
'slot',
'',
'varchar')));
5628 if ($content_id <= 0 && $folder_id <= 0) {
5629 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Keine Seiten- oder Ordner-Zuordnung erhalten.'));
5631 $where =
'media_id = ' . $id .
' AND active = 1';
5632 if ($content_id > 0) $where .=
' AND content_id = ' . $content_id;
5633 if ($folder_id > 0) $where .=
' AND folder_id = ' . $folder_id;
5634 if ($slot !==
'') $where .=
" AND slot = '" . str_replace(
"'",
"''", $slot) .
"'";
5635 $where = dbxContentMediaUsageScope::withLanguage($where, dbxContentLng::current());
5636 $ok =
$db->update($this->dd_media_usage, array(
'active' => 0), $where, 0, 1, 1, 0);
5638 $media = ($content_id > 0 || $folder_id > 0) ? $this->media_usage_rows_for_context(
$db, $content_id, $content_id > 0 ? 0 : $folder_id) : array();
5640 $this->flush_media_cache(
$db, $content_id, $folder_id);
5642 $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.'));
5645 private function delete_media_json() {
5646 $id = (int)
dbx()->get_modul_var(
'id', 0,
'int');
5647 $force = (int)
dbx()->get_modul_var(
'force', 0,
'int');
5649 $payload = $this->request_json();
5650 $id = (int)($payload[
'media_id'] ?? $payload[
'id'] ?? 0);
5651 $force = (int)($payload[
'force'] ?? 0);
5653 if ($id <= 0) $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Keine Medien-ID erhalten.'));
5655 $db =
dbx()->get_system_obj(
'dbxDB');
5656 $this->ensure_cms_schema(
$db);
5657 $used =
$db->select($this->dd_media_usage, $this->usage_where($id),
'*',
'id',
'ASC',
'', 1, 0, 0);
5658 if (is_array($used) && isset($used[0]) && !
$force) {
5659 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Medium wird noch verwendet.'));
5661 $row =
$db->select1($this->dd_media, $id);
5662 if (is_array($row)) {
5663 $this->flush_media_by_media_id(
$db, $id);
5664 if (
$force)
$db->update($this->dd_media_usage, array(
'active' => 0),
'media_id = ' . $id .
' AND active = 1', 0, 1, 1, 0);
5665 if (strtolower((
string)($row[
'storage_type'] ??
'local')) ===
'local') {
5666 $file = $this->source_media_file($row);
5669 $file = $this->source_media_file($row);
5672 $thumb = $this->source_thumb_file($row);
5673 if ($thumb !==
'') @unlink($thumb);
5675 $ok =
$db->update($this->dd_media, array(
'active' => 0), $id);
5676 $this->cms_json_response(array(
'ok' => (
$ok >= 0 ? 1 : 0),
'success' => (
$ok >= 0),
'id' => $id,
'msg' =>
'Medium geloescht.'));
5679 private function youtube_video_id(
$url) {
5681 if (
$url ===
'')
return '';
5682 if (preg_match(
'~^[A-Za-z0-9_-]{11}$~',
$url))
return $url;
5684 if (!is_array($parts))
return '';
5685 $host = strtolower((
string)($parts[
'host'] ??
''));
5686 $path = trim((
string)($parts[
'path'] ??
''),
'/');
5687 if (strpos($host,
'youtu.be') !==
false && preg_match(
'~^([A-Za-z0-9_-]{11})~', $path, $m))
return $m[1];
5688 if (strpos($host,
'youtube.com') !==
false) {
5689 parse_str((
string)($parts[
'query'] ??
''), $query);
5690 if (!empty($query[
'v']) && preg_match(
'~^[A-Za-z0-9_-]{11}$~', (
string)$query[
'v']))
return (
string)$query[
'v'];
5691 if (preg_match(
'~^(embed|shorts)/([A-Za-z0-9_-]{11})~', $path, $m))
return $m[2];
5696 private function external_video_json() {
5697 if (!$this->verify_media_form(
'external')) {
5698 $this->cms_json_response(array(
5701 'msg' =>
'Ungueltiger oder abgelaufener Formular-Token.'
5704 $payload = $this->request_json();
5705 $provider = strtolower(trim((
string)($payload[
'provider'] ??
'youtube')));
5706 $external_url = trim((
string)($payload[
'external_url'] ?? $payload[
'url'] ??
''));
5707 if ($provider !==
'youtube') $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Provider nicht unterstuetzt.'));
5708 $video_id = $this->youtube_video_id($external_url);
5709 if ($video_id ===
'') $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'YouTube-URL nicht erkannt.'));
5711 $db =
dbx()->get_system_obj(
'dbxDB');
5712 $this->ensure_cms_schema(
$db);
5713 $content_id = (int)($payload[
'content_id'] ?? 0);
5714 $folder_id = (int)($payload[
'folder_id'] ?? 0);
5715 $slot = $this->valid_media_slot($payload[
'slot'] ??
'gallery');
5716 $media_folder = $this->clean_media_folder($payload[
'media_folder'] ??
'youtube',
'external_video');
5717 $title = $this->clean_text($payload[
'title'] ?? (
'YouTube ' . $video_id), 160);
5718 $embed_url =
'https://www.youtube.com/embed/' . $video_id;
5719 $thumb_url =
'https://img.youtube.com/vi/' . $video_id .
'/hqdefault.jpg';
5721 $dir = $this->cms_media_dir($media_folder);
5722 if (!is_dir(
$dir)) @mkdir(
$dir, 0777,
true);
5723 $rel = $this->media_folder_rel_dir($media_folder,
'external_video') . $video_id .
'.json';
5724 $file = $this->file_from_media_rel($rel);
5726 @file_put_contents(
$file, json_encode(array(
5727 'provider' =>
'youtube',
5728 'provider_id' => $video_id,
5729 'external_url' => $external_url,
5730 'embed_url' => $embed_url,
5732 'thumb_url' => $thumb_url,
5733 ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
5735 $existing =
$db->select($this->dd_media,
"active = 1 AND provider = 'youtube' AND provider_id = '" . str_replace(
"'",
"''", $video_id) .
"'",
'*',
'id',
'DESC',
'', 1, 0, 0);
5736 if (is_array($existing) && isset($existing[0])) {
5737 $row = $existing[0];
5739 'media_folder' => $media_folder,
5740 'file_name' => $video_id .
'.json',
5741 'file_path' => $rel,
5742 'mime' =>
'application/json',
5743 'media_type' =>
'external_video',
5744 'storage_type' =>
'external',
5745 'external_url' => $external_url,
5746 'embed_url' => $embed_url,
5750 $db->update($this->dd_media, $update, (
int)($row[
'id'] ?? 0));
5751 $row = array_merge($row, $update);
5754 if (
$db->insert($this->dd_media, array(
5761 'template' =>
'video-gallery',
5764 'file_name' => $video_id .
'.json',
5765 'file_path' => $rel,
5766 'mime' =>
'application/json',
5770 'media_type' =>
'external_video',
5771 'storage_type' =>
'external',
5772 'media_folder' => $media_folder,
5773 'provider' =>
'youtube',
5774 'provider_id' => $video_id,
5775 'external_url' => $external_url,
5776 'embed_url' => $embed_url,
5778 $id =
$db->get_insert_id();
5780 $row =
$db->select1($this->dd_media, $id);
5784 if (is_array($row) && ((
int)($row[
'id'] ?? 0) > 0) && ($content_id > 0 || $folder_id > 0)) {
5785 $usage_id = $this->create_media_usage(
$db, (
int)$row[
'id'], $content_id, $folder_id, $slot,
'video-gallery',
'',
'');
5786 if ($usage_id > 0) $usage = $this->normalize_usage_row(
$db->select1($this->dd_media_usage, $usage_id));
5788 $this->flush_media_cache(
$db, $content_id, $folder_id);
5789 $this->cms_json_response(array(
'ok' => 1,
'success' =>
true,
'row' => $this->normalize_media_row($row),
'usage' => $usage,
'msg' =>
'Externes Video gespeichert.'));
5792 private function media_dir_has_content(
$dir) {
5793 if (!is_dir(
$dir) || !is_readable(
$dir))
return false;
5794 $it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$dir, \FilesystemIterator::SKIP_DOTS));
5795 foreach (
$it as $item) {
5796 if ($item->isFile())
return true;
5801 private function is_reserved_media_folder($folder) {
5802 $folder = $this->canonical_media_folder(strtolower(trim(str_replace(
'\\',
'/', (
string)$folder),
'/')));
5803 if ($folder ===
'module' || strpos($folder,
'module/') === 0)
return true;
5804 if (preg_match(
'~^img/external(/|$)~', $folder))
return true;
5805 if (preg_match(
'~^img/video(/|$)~', $folder))
return true;
5806 return in_array($folder, $this->media_slots(),
true);
5809 private function media_folder_dir_is_listable($folder, $path,
$base =
'') {
5810 return is_dir($path) && is_readable($path);
5813 private function media_folder_is_listable($folder) {
5814 $folder = $this->canonical_media_folder($folder);
5815 if ($folder ===
'' || $folder ===
'module')
return false;
5816 if ($this->is_reserved_media_folder($folder))
return false;
5817 $dir = $this->cms_media_dir($folder);
5818 return is_dir(
$dir) && is_readable(
$dir);
5821 private function collect_custom_media_root_folders(array &$folders) {
5823 $mediaRoot =
dbx()->os_path($mediaRoot);
5824 if (!is_dir($mediaRoot) || !is_readable($mediaRoot))
return;
5825 $skip = array(
'.',
'..',
'_thumbs',
'img',
'video',
'videos',
'youtube',
'external',
'file',
'module');
5826 $skip = array_merge($skip, $this->media_slots());
5827 $root_norm = str_replace(
'\\',
'/', rtrim($mediaRoot,
'/\\')) .
'/';
5828 $it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($mediaRoot, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
5829 foreach (
$it as $item) {
5830 if (!$item->isDir())
continue;
5831 $path = str_replace(
'\\',
'/', $item->getPathname());
5832 if (strpos($path .
'/', $root_norm) !== 0)
continue;
5833 $rel = trim(substr($path, strlen($root_norm)),
'/');
5834 if ($rel ===
'')
continue;
5835 $top = strtok($rel,
'/');
5836 if ($top ===
false || in_array($top, $skip,
true))
continue;
5837 if ($this->is_reserved_media_folder($rel))
continue;
5842 private function media_folders_json() {
5843 $bases = $this->media_standard_bases();
5845 foreach ($bases as
$base => $type) {
5847 if (!is_dir(
$root) || !is_readable(
$root))
continue;
5848 if (
$base ===
'youtube' || $type ===
'video') {
5849 $folders[] = $this->clean_media_folder(
$base, $type);
5851 $root_norm = str_replace(
'\\',
'/', rtrim(
$root,
'/\\')) .
'/';
5852 $it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$root, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
5853 foreach (
$it as $item) {
5854 if (!$item->isDir())
continue;
5855 $path = str_replace(
'\\',
'/', $item->getPathname());
5856 if (strpos($path .
'/', $root_norm) !== 0)
continue;
5857 $rel = trim(substr($path, strlen($root_norm)),
'/');
5858 if ($rel ===
'')
continue;
5859 $folder = $this->clean_media_folder(
$base .
'/' . $rel, $type);
5860 if ($this->is_reserved_media_folder($folder))
continue;
5861 if (!$this->media_folder_dir_is_listable($folder, $path,
$base))
continue;
5862 $folders[] = $folder;
5865 $this->collect_custom_media_root_folders($folders);
5867 $folders = array_values(array_unique(array_map(array($this,
'canonical_media_folder'), $folders)));
5868 $verified = array();
5869 foreach ($folders as $folder) {
5870 $folder = $this->canonical_media_folder($folder);
5871 if ($folder ===
'' || $this->is_excluded_cms_media_folder($folder))
continue;
5872 $dir = $this->cms_media_dir($folder);
5873 if (!is_dir(
$dir) || !is_readable(
$dir))
continue;
5874 $verified[] = $folder;
5876 $folders = $verified;
5878 $this->cms_json_response(array(
'ok' => 1,
'success' =>
true,
'root' =>
'files/media/',
'folders' => $folders));
5881 private function media_folder_create_json() {
5882 $payload = $this->request_json();
5883 $type = $this->media_type(array(
'media_type' => ($payload[
'media_type'] ??
'image')));
5884 $folder = $this->clean_media_folder($payload[
'media_folder'] ?? $payload[
'folder'] ??
'', $type);
5885 if ($this->is_excluded_cms_media_folder($folder)) {
5886 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Dieser Ordner ist im CMS-Medienbrowser nicht verfuegbar.'));
5888 $dir = $this->cms_media_dir($folder);
5889 if (!is_dir(
$dir) && !@mkdir(
$dir, 0777,
true)) {
5890 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ordner konnte nicht angelegt werden.'));
5892 $this->cms_json_response(array(
'ok' => 1,
'success' =>
true,
'folder' => $folder,
'msg' =>
'Ordner angelegt.'));
5895 private function media_folder_delete_json() {
5896 $payload = $this->request_json();
5897 $raw_folder = $payload[
'media_folder'] ?? $payload[
'folder'] ??
'';
5898 $type = $this->media_type(array(
'media_type' => ($payload[
'media_type'] ?? $this->media_type_from_folder($raw_folder))));
5899 $folder = $this->clean_media_folder($raw_folder, $type);
5900 if ($this->is_excluded_cms_media_folder($folder)) {
5901 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Dieser Ordner ist im CMS-Medienbrowser nicht verfuegbar.'));
5903 $dir = $this->cms_media_dir($folder);
5904 if (!is_dir(
$dir)) $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ordner nicht gefunden.'));
5906 $db =
dbx()->get_system_obj(
'dbxDB');
5907 $this->ensure_cms_schema(
$db);
5908 $used =
$db->select($this->dd_media,
"active = 1 AND media_folder = '" . str_replace(
"'",
"''", $folder) .
"'",
'*',
'id',
'ASC',
'', 1, 0, 0);
5909 if (is_array($used) && isset($used[0])) $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ordner enthaelt noch Medien.'));
5913 if (!empty(
$items)) $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ordner ist nicht leer.'));
5914 if (!@rmdir(
$dir)) $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ordner konnte nicht geloescht werden.'));
5915 $this->cms_json_response(array(
'ok' => 1,
'success' =>
true,
'folder' => $folder,
'msg' =>
'Ordner geloescht.'));
5918 private function media_folder_rename_json() {
5919 $payload = $this->request_json();
5920 $from = $this->clean_media_folder($payload[
'from_folder'] ?? $payload[
'media_folder_from'] ?? $payload[
'from'] ??
'',
'image');
5921 $to_raw = trim((
string)($payload[
'to_folder'] ?? $payload[
'media_folder_to'] ?? $payload[
'to'] ??
''));
5922 if ($to_raw !==
'' && strpos($to_raw,
'/') ===
false && $from !==
'') {
5923 $base = strtok($from,
'/');
5924 $to_raw = (
$base !==
false &&
$base !==
'' ?
$base :
'img') .
'/' . $to_raw;
5926 $to = $this->clean_media_folder($to_raw, $this->media_type_from_folder($to_raw !==
'' ? $to_raw : $from));
5927 if ($from ===
'' || $to ===
'' || $from === $to) {
5928 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ungueltiger Ordner.'));
5930 if ($this->is_excluded_cms_media_folder($from) || $this->is_excluded_cms_media_folder($to)) {
5931 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Dieser Ordner ist im CMS-Medienbrowser nicht verfuegbar.'));
5933 $from_dir = $this->cms_media_dir($from);
5934 $to_dir = $this->cms_media_dir($to);
5935 if (!is_dir($from_dir) || !is_readable($from_dir)) {
5936 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Quellordner nicht gefunden.'));
5938 if (is_dir($to_dir)) {
5939 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Zielordner existiert bereits.'));
5941 if (!@rename($from_dir, $to_dir)) {
5942 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ordner konnte nicht umbenannt werden.'));
5945 $db =
dbx()->get_system_obj(
'dbxDB');
5946 $this->ensure_cms_schema(
$db);
5947 $from_prefix =
'media/' . $from .
'/';
5948 $to_prefix =
'media/' . $to .
'/';
5949 $thumb_from =
'media/_thumbs/' . $from .
'/';
5950 $thumb_to =
'media/_thumbs/' . $to .
'/';
5951 $thumb_from_dir = $this->file_from_media_rel($thumb_from);
5952 $thumb_to_dir = $this->file_from_media_rel($thumb_to);
5953 if ($thumb_from_dir !==
'' && is_dir($thumb_from_dir) && $thumb_to_dir !==
'' && !is_dir($thumb_to_dir)) {
5954 @rename($thumb_from_dir, $thumb_to_dir);
5957 $rows =
$db->select($this->dd_media,
"active = 1 AND (media_folder = '" . str_replace(
"'",
"''", $from) .
"' OR file_path LIKE '" . str_replace(
"'",
"''", $from_prefix) .
"%')",
'*',
'id');
5960 foreach (
$rows as $row) {
5961 if (!is_array($row))
continue;
5962 $id = (int)($row[
'id'] ?? 0);
5963 if ($id <= 0)
continue;
5964 $path = ltrim(str_replace(
'\\',
'/', (
string)($row[
'file_path'] ??
'')),
'/');
5966 if (strpos($path, $from_prefix) === 0) {
5967 $new_path = $to_prefix . substr($path, strlen($from_prefix));
5970 'media_folder' => $to,
5971 'file_path' => $new_path,
5973 $thumb = ltrim(str_replace(
'\\',
'/', (
string)($row[
'thumb_file_path'] ??
'')),
'/');
5974 if ($thumb !==
'' && strpos($thumb, $thumb_from) === 0) {
5975 $update[
'thumb_file_path'] = $thumb_to . substr($thumb, strlen($thumb_from));
5977 $db->update($this->dd_media, $update, $id);
5981 $this->cms_json_response(array(
5984 'from_folder' => $from,
5987 'msg' =>
'Ordner umbenannt.',
5991 private function media_is_used(
$db,
int $id): bool {
5992 if ($id <= 0) return true;
5993 $used =
$db->select(
5994 $this->dd_media_usage,
5995 'media_id = ' . $id .
' AND active = 1',
6004 if (is_array($used) && isset($used[0]))
return true;
6006 foreach (dbxContentLngSync::accessibleLngs() as
$lng) {
6009 if (
$db->count(
$contentDd,
'hero_image_id = ' . $id .
' OR seo_image_id = ' . $id) > 0 ||
$db->count(
$folderDd,
'hero_image_id = ' . $id) > 0) {
6015 if (preg_match(
'/data-cms-media-id=["\']?' . $id .
'(?:["\'\s>]|$)/i',
$content)
6016 || preg_match(
'/(?:dbx_mid|media_id)=' . $id .
'(?:[^0-9]|$)/i',
$content)) {
6024 private function move_media_record_to_folder(
$db,
int $id,
string $target_folder): array {
6025 if ($id <= 0) return array(
'ok' => 0,
'msg' =>
'Keine Medien-ID erhalten.');
6026 $row =
$db->select1($this->dd_media, $id);
6027 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1)
return array(
'ok' => 0,
'msg' =>
'Medium nicht gefunden.');
6029 $media_type = $this->media_type($row);
6030 $folder = $this->clean_media_folder($target_folder, $media_type);
6031 if ($this->is_excluded_cms_media_folder($folder)) {
6032 return array(
'ok' => 0,
'msg' =>
'Dieser Zielordner ist im CMS-Medienbrowser nicht verfuegbar.');
6034 $old_file = $this->source_media_file($row);
6035 $name = basename((
string)($row[
'file_name'] ?? $row[
'file_path'] ??
''));
6036 if ($name ===
'')
return array(
'ok' => 0,
'msg' =>
'Dateiname fehlt.');
6037 $dir = $this->cms_media_dir($folder);
6038 if (!is_dir(
$dir)) @mkdir(
$dir, 0777,
true);
6039 if (!is_dir(
$dir))
return array(
'ok' => 0,
'msg' =>
'Zielordner konnte nicht angelegt werden.');
6040 $new_rel = $this->media_folder_rel_dir($folder, $media_type) . $name;
6041 $new_file = $this->file_from_media_rel($new_rel);
6042 if ($old_file !==
'' && $new_file !==
'' && $old_file !== $new_file) {
6043 if (is_file($new_file)) {
6044 $name = $this->unique_media_name($name);
6045 $new_rel = $this->media_folder_rel_dir($folder, $media_type) . $name;
6046 $new_file = $this->file_from_media_rel($new_rel);
6048 if (!@rename($old_file, $new_file))
return array(
'ok' => 0,
'msg' =>
'Medium konnte nicht verschoben werden.');
6050 $old_thumb = $this->source_thumb_file($row);
6051 $mime = (string)($row[
'mime'] ??
'');
6052 $thumb = $this->create_media_thumbnail($new_file !==
'' ? $new_file : $old_file, $folder, $name, $mime);
6053 if ($old_thumb !==
'' && is_file($old_thumb) && $old_thumb !== $this->file_from_media_rel((
string)($thumb[
'thumb_file_path'] ??
''))) {
6054 @unlink($old_thumb);
6056 $update = array(
'active' => 1,
'media_folder' => $folder,
'file_name' => $name,
'file_path' => $new_rel);
6057 if ($thumb) $update = array_merge($update, $thumb);
6058 $ok =
$db->update($this->dd_media, $update, $id, 0, 1, 1, 0);
6059 if (
$ok < 0)
return array(
'ok' => 0,
'msg' =>
'Mediendaten konnten nicht aktualisiert werden.');
6060 $row = $this->normalize_media_row(
$db->select1($this->dd_media, $id));
6061 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || (
string)($row[
'media_folder'] ??
'') !== $folder) {
6062 return array(
'ok' => 0,
'msg' =>
'Mediendaten wurden nicht konsistent gespeichert.');
6064 return array(
'ok' => 1,
'success' =>
true,
'row' => $row,
'msg' =>
'Medium verschoben.');
6067 private function media_move_json() {
6068 $payload = $this->request_json();
6069 $id = (int)($payload[
'media_id'] ?? $payload[
'id'] ?? 0);
6070 if ($id <= 0) $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Keine Medien-ID erhalten.'));
6072 $db =
dbx()->get_system_obj(
'dbxDB');
6073 $this->ensure_cms_schema(
$db);
6074 $result = $this->move_media_record_to_folder(
$db, $id, (
string)($payload[
'media_folder'] ?? $payload[
'folder'] ??
''));
6075 $this->cms_json_response(
$result);
6078 private function media_unused_json() {
6079 $payload = $this->request_json();
6080 $action = strtolower(trim((
string)($payload[
'action'] ??
'')));
6081 if (!in_array($action, array(
'delete',
'move'),
true)) {
6082 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Ungueltige Aktion.'));
6085 $source_raw = trim((
string)($payload[
'source_folder'] ?? $payload[
'media_folder'] ??
'all'));
6086 $source = strtolower($source_raw) ===
'all' || $source_raw ===
''
6088 : $this->clean_media_folder($source_raw, $this->media_type_from_folder($source_raw,
'image'));
6089 if (
$source !==
'all' && $this->is_excluded_cms_media_folder(
$source)) {
6090 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Dieser Quellordner ist im CMS-Medienbrowser nicht verfuegbar.'));
6094 if ($action ===
'move') {
6095 $target_raw = trim((
string)($payload[
'target_folder'] ?? $payload[
'to_folder'] ??
''));
6096 if ($target_raw ===
'') {
6097 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Bitte einen Zielordner waehlen.'));
6099 $target = $this->clean_media_folder($target_raw,
'image');
6100 if (
$target ===
'' || $this->is_excluded_cms_media_folder(
$target)) {
6101 $this->cms_json_response(array(
'ok' => 0,
'success' =>
false,
'msg' =>
'Bitte einen gueltigen Zielordner waehlen.'));
6105 $db =
dbx()->get_system_obj(
'dbxDB');
6106 $this->ensure_cms_schema(
$db);
6107 $this->sync_cms_media_files(
$db);
6109 $where =
"active = 1"
6110 . $this->cms_media_exclude_sql()
6111 .
" 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')";
6113 $where .=
" AND media_folder = '" . str_replace(
"'",
"''",
$source) .
"'";
6115 if ($action ===
'move') {
6116 $where .=
" AND media_folder <> '" . str_replace(
"'",
"''",
$target) .
"'";
6119 $rows =
$db->select($this->dd_media, $where,
'*',
'media_folder,title,id',
'ASC',
'', 0, 0, 0);
6127 $moved_rows = array();
6129 foreach (
$rows as $row) {
6130 if (!is_array($row))
continue;
6131 $id = (int)($row[
'id'] ?? 0);
6132 if ($id <= 0)
continue;
6134 if ($this->media_is_used(
$db, $id)) {
6138 if ($action ===
'delete') {
6139 $result = $this->delete_media_record($id);
6140 if ((
int)(
$result[
'ok'] ?? 0) === 1) {
6143 $errors[] =
'#' . $id .
': ' . implode(
' ', (array)(
$result[
'errors'] ?? array(
'Loeschen fehlgeschlagen.')));
6148 if ((
int)(
$result[
'ok'] ?? 0) === 1) {
6152 $errors[] =
'#' . $id .
': ' . (string)(
$result[
'msg'] ??
'Verschieben fehlgeschlagen.');
6156 $label = $action ===
'delete' ?
'geloescht' :
'verschoben';
6157 $msg = $affected .
' unbenutzte Bilder ' . $label .
'.';
6158 if ($skipped_used > 0) $msg .=
' ' . $skipped_used .
' verwendete Bilder uebersprungen.';
6161 $this->cms_json_response(array(
6162 'ok' => empty(
$errors) ? 1 : ($affected > 0 ? 1 : 0),
6163 'success' => empty(
$errors) || $affected > 0,
6164 'action' => $action,
6168 'affected' => $affected,
6169 'skipped_used' => $skipped_used,
6171 'rows' => $moved_rows,
6176 private function edit_image_file(
$file, $mime, array $op) {
6177 if (!extension_loaded(
'gd') || !function_exists(
'imagecreatetruecolor'))
return false;
6178 $img = $this->load_gd_image(
$file, $mime);
6179 if (!$img)
return false;
6181 $src_w = imagesx($img);
6182 $src_h = imagesy($img);
6183 if ($src_w <= 0 || $src_h <= 0) {
6188 $action = strtolower((
string)($op[
'action'] ??
'resize'));
6189 if ($action ===
'crop') {
6190 $x = max(0, min($src_w - 1, (
int)($op[
'x'] ?? 0)));
6191 $y = max(0, min($src_h - 1, (
int)($op[
'y'] ?? 0)));
6192 $w = max(1, min($src_w - $x, (
int)($op[
'width'] ?? $src_w)));
6193 $h = max(1, min($src_h - $y, (
int)($op[
'height'] ?? $src_h)));
6194 $dst = imagecreatetruecolor($w, $h);
6195 imagefill($dst, 0, 0, imagecolorallocate($dst, 255, 255, 255));
6196 imagecopyresampled($dst, $img, 0, 0, $x, $y, $w, $h, $w, $h);
6198 $w = (int)($op[
'width'] ?? 0);
6199 $h = (int)($op[
'height'] ?? 0);
6200 $ratio = !empty($op[
'ratio']);
6201 if ($w <= 0 && $h <= 0) {
6205 if ($w <= 0) $w = max(1, (
int)round($src_w * ($h / $src_h)));
6206 if ($h <= 0) $h = max(1, (
int)round($src_h * ($w / $src_w)));
6207 if ($ratio && $w > 0 && $h > 0) {
6208 $scale = min($w / $src_w, $h / $src_h);
6209 $w = max(1, (
int)round($src_w * $scale));
6210 $h = max(1, (
int)round($src_h * $scale));
6212 $dst = imagecreatetruecolor($w, $h);
6213 imagefill($dst, 0, 0, imagecolorallocate($dst, 255, 255, 255));
6214 imagecopyresampled($dst, $img, 0, 0, 0, 0, $w, $h, $src_w, $src_h);
6217 $ok = $this->save_gd_image($dst,
$file, $mime);
6223 private function edit_media_json() {
6225 $payload = $this->request_json();
6227 if (isset($payload[
'ids']) && is_array($payload[
'ids'])) {
6228 foreach ($payload[
'ids'] as $id) {
6230 if ($id > 0) $ids[$id] = $id;
6233 $id = (int)($payload[
'id'] ?? 0);
6234 if ($id > 0) $ids[$id] = $id;
6236 if (!$ids) $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Keine Medien ausgewaehlt.'));
6238 $action = strtolower((
string)($payload[
'action'] ??
'resize'));
6239 if (!in_array($action, array(
'resize',
'crop'),
true)) $action =
'resize';
6240 if ($action ===
'crop' && count($ids) > 1) $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Zuschneiden ist nur fuer ein Bild moeglich.'));
6242 $db =
dbx()->get_system_obj(
'dbxDB');
6243 $this->ensure_cms_schema(
$db);
6245 foreach ($ids as $id) {
6246 $row =
$db->select1($this->dd_media, $id);
6247 if (!is_array($row) || (
int)($row[
'active'] ?? 0) !== 1 || $this->media_type($row) !==
'image')
continue;
6248 if (preg_match(
'/\.svg$/i', (
string)($row[
'file_name'] ??
'')))
continue;
6249 $file = $this->source_media_file($row);
6250 if (
$file ===
'')
continue;
6251 $mime = (string)($row[
'mime'] ??
'');
6252 if (!$this->edit_image_file(
$file, $mime, array(
6253 'action' => $action,
6254 'x' => (
int)($payload[
'x'] ?? 0),
6255 'y' => (
int)($payload[
'y'] ?? 0),
6256 'width' => (
int)($payload[
'width'] ?? 0),
6257 'height' => (
int)($payload[
'height'] ?? 0),
6258 'ratio' => !empty($payload[
'ratio']),
6261 $old_thumb = $this->source_thumb_file($row);
6262 if ($old_thumb !==
'') @unlink($old_thumb);
6263 $size = @getimagesize(
$file);
6265 'size' => (
int)@filesize(
$file),
6266 'width' => is_array($size) ? (
int)$size[0] : 0,
6267 'height' => is_array($size) ? (
int)$size[1] : 0,
6268 'media_type' =>
'image',
6269 'thumb_file_path' =>
'',
6271 'thumb_height' => 0,
6273 $media_folder = $this->canonical_media_folder(trim((
string)($row[
'media_folder'] ??
'')) ?: $this->media_folder_from_path((
string)($row[
'file_path'] ??
''), $this->media_type($row)));
6274 $thumb = $this->create_media_thumbnail(
$file, $media_folder, (
string)($row[
'file_name'] ?? basename(
$file)), $mime);
6275 if ($thumb) $update = array_merge($update, $thumb);
6276 $db->update($this->dd_media, $update, $id);
6277 $done[] = $this->normalize_media_row(
$db->select1($this->dd_media, $id));
6280 if (!$done) $this->cms_json_response(array(
'ok' => 0,
'msg' =>
'Keine bearbeitbaren Bilder gefunden.'));
6281 foreach ($ids as $id) {
6282 $this->flush_media_by_media_id(
$db, (
int)$id);
6284 $this->cms_json_response(array(
'ok' => 1,
'success' =>
true,
'rows' => $done));
6285 }
catch (\Throwable $e) {
6286 $this->cms_json_response(array(
6289 'msg' =>
'Medienbearbeitung fehlgeschlagen: ' . $e->getMessage()
6304 private function content_template_names(): array {
6305 if (is_array($this->contentTemplateNames)) {
6306 return $this->contentTemplateNames;
6312 $files = is_dir(
$dir) ? glob(rtrim(
$dir,
'/\\') . DIRECTORY_SEPARATOR .
'c-*.htm') : array();
6317 if (!is_file(
$file))
continue;
6318 $names[] = basename(
$file,
'.htm');
6322 $names = array_values(array_unique($names));
6323 sort($names, SORT_NATURAL | SORT_FLAG_CASE);
6325 $this->contentTemplateNames = $names ?: array(
'c-content');
6326 return $this->contentTemplateNames;
6329 private function template_options() {
6331 foreach ($this->content_template_names() as $name) {
6332 $html .=
'<option value="' .
dbx()->esc($name) .
'">' .
dbx()->esc($name) .
'</option>';
6338 private function rights_options() {
6339 $values = $this->rights_values();
6341 foreach ($values as
$value => $label) {
6342 $html .=
'<option value="' .
dbx()->esc(
$value) .
'">' .
dbx()->esc($label) .
'</option>';
6347 private function rights_values() {
6348 $texts = $this->cms_texts();
6350 'parent' =>
$texts->get_fd_message(
'option_parent'),
6353 $db =
dbx()->get_system_obj(
'dbxDB');
6354 $rows =
$db->select(
'dbxUser_groups',
'',
'*',
'name');
6355 if (is_array(
$rows)) {
6356 foreach (
$rows as $row) {
6357 if (!is_array($row))
continue;
6358 $name = trim((
string)($row[
'name'] ??
''));
6359 if ($name !==
'') $values[$name] = $name;
6365 private function media_template_options() {
6367 'image-hero' =>
'Bild Hero',
6368 'image-gallery' =>
'Bild Galerie',
6369 'image-teaser' =>
'Bild Header',
6370 'video-hero' =>
'Video Hero',
6371 'video-gallery' =>
'Video Galerie',
6372 'file-gallery' =>
'Datei Download',
6375 foreach ($values as
$value => $label) {
6376 $html .=
'<option value="' .
dbx()->esc(
$value) .
'">' .
dbx()->esc($label) .
'</option>';
6381 private function options_html(array $values, $selected =
'') {
6383 foreach ($values as
$value => $label) {
6384 $sel = ((string)
$value === (string)$selected) ?
' selected' :
'';
6385 $html .=
'<option value="' .
dbx()->esc(
$value) .
'"' . $sel .
'>' .
dbx()->esc($label) .
'</option>';
6390 private function content_template_options($with_parent =
true) {
6391 $html = $with_parent ?
'<option value="parent">parent - vom Parent</option>' :
'';
6392 $html .= $this->template_options();
6396 private function hero_template_values() {
6397 $texts = $this->cms_texts();
6399 'parent' =>
$texts->get_fd_message(
'option_parent'),
6400 'none' =>
$texts->get_fd_message(
'option_no_hero'),
6401 'image-hero' =>
'Bild Hero',
6402 'video-hero' =>
'Video Hero',
6406 private function hero_template_options() {
6407 return $this->options_html($this->hero_template_values());
6410 private function hero_variant_values() {
6411 $texts = $this->cms_texts();
6413 'parent' =>
$texts->get_fd_message(
'option_parent'),
6414 'original' =>
'Original',
6415 'yellow' =>
'gelblich',
6416 'green' =>
'gruenlich',
6417 'blue' =>
'blaeulich',
6418 'red' =>
'roetlich',
6421 'blackwhite' =>
'schwarz/weiss',
6422 'monochrome' =>
'monochrom',
6426 private function hero_variant_options() {
6427 return $this->options_html($this->hero_variant_values());
6430 private function hero_sticky_values() {
6431 $texts = $this->cms_texts();
6433 'parent' =>
$texts->get_fd_message(
'option_parent'),
6434 '0' =>
$texts->get_fd_message(
'option_not_sticky'),
6435 '1' =>
$texts->get_fd_message(
'option_sticky'),
6439 private function hero_scroll_layer_values() {
6440 $texts = $this->cms_texts();
6442 'parent' =>
$texts->get_fd_message(
'option_parent'),
6443 'under' =>
$texts->get_fd_message(
'option_scroll_under'),
6444 'over' =>
$texts->get_fd_message(
'option_scroll_over'),
6448 private function gallery_template_values() {
6450 'image-gallery' =>
'Bildergalerie',
6451 'video-gallery' =>
'Video Galerie',
6452 'carousel3' =>
'Carousel 3',
6453 'cols3' =>
'3 Spalten',
6457 private function gallery_template_options() {
6458 return $this->options_html($this->gallery_template_values());
6461 private function gallery_image_size_values() {
6463 '800x600' =>
'4:3 - Standard (800 x 600)',
6464 '1200x900' =>
'4:3 - gross (1200 x 900)',
6465 '1024x768' =>
'4:3 - klassisch (1024 x 768)',
6466 '1600x1200' =>
'4:3 - hochaufloesend (1600 x 1200)',
6467 '1280x720' =>
'16:9 - breit (1280 x 720)',
6468 '1920x1080' =>
'16:9 - Full HD (1920 x 1080)',
6469 '1200x675' =>
'16:9 - Web (1200 x 675)',
6470 '1080x1080' =>
'1:1 - Quadrat (1080 x 1080)',
6471 '1200x1200' =>
'1:1 - Quadrat gross (1200 x 1200)',
6472 '1080x1350' =>
'4:5 - Portrait (1080 x 1350)',
6473 '900x1200' =>
'3:4 - Portrait (900 x 1200)',
6474 '1600x900' =>
'16:9 - gross (1600 x 900)',
6475 '2560x1440' =>
'16:9 - QHD (2560 x 1440)',
6479 private function gallery_lightbox_width_values() {
6481 '60%' =>
'60% - kompakt',
6482 '70%' =>
'70% - mittel',
6483 '80%' =>
'80% - Standard',
6484 '90%' =>
'90% - breit',
6485 '95%' =>
'95% - sehr breit',
6486 '70vw' =>
'70vw - Viewport mittel',
6487 '80vw' =>
'80vw - Viewport Standard',
6488 '90vw' =>
'90vw - Viewport breit',
6489 '960px' =>
'960px - kleine Desktopbreite',
6490 '1200px' =>
'1200px - Desktop',
6491 '1440px' =>
'1440px - grosser Desktop',
6495 private function gallery_overflow_values() {
6498 'scroll' =>
'scroll',
6499 'laufband' =>
'laufband',
6500 'slider' =>
'slider',
6501 'tutorial' =>
'Tutorial Slideshow',
6505 private function gallery_overflow_options() {
6506 return $this->options_html($this->gallery_overflow_values());
6509 private function gallery_click_values() {
6511 'lightbox' =>
'Lightbox',
6512 'swiper-coverflow' =>
'Swiper Coverflow',
6513 'swiper-cube' =>
'Swiper Cube',
6514 'swiper-cards' =>
'Swiper Cards',
6515 'swiper-3d' =>
'Swiper 3D-Slider',
6516 'viewerjs' =>
'Viewer.js',
6517 'blueimp' =>
'blueimp Gallery',
6518 'photoswipe' =>
'PhotoSwipe',
6519 'deepzoom' =>
'Deep-Zoom-Viewer',
6521 'newtab' =>
'Neues Fenster',
6522 'none' =>
'Kein Klick',
6526 private function gallery_click_options() {
6527 return $this->options_html($this->gallery_click_values());
6530 private function meta_robots_values() {
6532 'index,follow' =>
'index, follow (Standard)',
6533 'noindex,follow' =>
'noindex, follow',
6534 'index,nofollow' =>
'index, nofollow',
6535 'noindex,nofollow' =>
'noindex, nofollow',
6539 private function render_page_form() {
6540 $texts = $this->cms_texts();
6541 $form =
dbx()->get_system_obj(
'dbxForm');
6542 $form->init(
'cms-page',
'cms-admin-page-form');
6543 $form->_dd = dbxContentLng::ddContent();
6544 $form->_fd =
'dbxContent_admin|cms-page';
6545 $form->load_fd_messages();
6546 $form->set_form_help_enabled(
false);
6547 $form->_data = array(
6549 'template' =>
'parent',
6550 'hero_template' =>
'parent',
6551 'hero_image_id' =>
'parent',
6552 'hero_margin_top' =>
'parent',
6553 'hero_height' =>
'parent',
6554 'hero_variant' =>
'parent',
6555 'hero_sticky' =>
'parent',
6556 'hero_scroll_layer' =>
'parent',
6557 'gallery_template' =>
'image-gallery',
6558 'gallery_visible_count' =>
'3',
6559 'gallery_image_size' =>
'original',
6560 'gallery_lightbox_width' =>
'100vw',
6561 'gallery_overflow' =>
'grid',
6562 'gallery_click_behavior' =>
'lightbox',
6564 'meta_robots' =>
'index,follow',
6566 'seo_image_id' => 0,
6569 $form->add_fld(
'id',
'dbxContent_admin|cms-field-hidden', data: array(
'cms_field' =>
'id'));
6570 $form->add_fld(
'folder',
'dbxContent_admin|cms-field-hidden', data: array(
'cms_field' =>
'folder'));
6571 $form->add_fld(
'title',
'dbxContent_admin|cms-field-text', label:
$texts->get_fd_message(
'label_title'), rules:
'varchar|min=1', data: array(
'cms_field' =>
'title'));
6572 $form->add_fld(
'menu_title',
'dbxContent_admin|cms-field-text', label:
$texts->get_fd_message(
'label_menu_title'), rules:
'varchar|max=96', data: array(
'cms_field' =>
'menu_title'), placeholder:
$texts->get_fd_message(
'placeholder_menu_title'));
6573 $form->add_fld(
'permalink',
'dbxContent_admin|cms-field-text', label:
$texts->get_fd_message(
'label_permalink'), rules:
'permalink|max=254', data: array(
'cms_field' =>
'permalink'), tooltip:
$texts->get_fd_message(
'tooltip_permalink'));
6574 $form->add_fld(
'activ',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_status'), rules:
'int', data: array(
'cms_field' =>
'activ'), options: array(
'1' =>
$texts->get_fd_message(
'option_active'),
'0' =>
$texts->get_fd_message(
'option_inactive')));
6575 $form->add_fld(
'template',
'dbxContent_admin|cms-field-content-template-select', label:
$texts->get_fd_message(
'label_template'), rules:
'varchar', data: array(
'cms_field' =>
'template'), options: $this->content_template_values());
6576 $form->add_fld(
'description',
'dbxContent_admin|cms-field-textarea', label:
$texts->get_fd_message(
'label_description'), rules:
'varchar', data: array(
'cms_field' =>
'description'), placeholder:
$texts->get_fd_message(
'placeholder_description'));
6577 $form->add_fld(
'keywords',
'dbxContent_admin|cms-field-text', label:
$texts->get_fd_message(
'label_keywords'), rules:
'varchar', data: array(
'cms_field' =>
'keywords'), placeholder:
$texts->get_fd_message(
'placeholder_keywords'));
6578 $form->add_fld(
'content',
'dbxContent_admin|cms-field-textarea-hidden', label:
$texts->get_fd_message(
'label_content'), rules:
'text', data: array(
'cms_field' =>
'content'));
6579 return $form->run();
6582 private function content_template_values() {
6583 $values = array(
'parent' => $this->cms_texts()->get_fd_message(
'option_parent'));
6584 foreach ($this->content_template_names() as $name) {
6585 $values[$name] = $name;
6591 private function render_folder_form() {
6592 $texts = $this->cms_texts();
6593 $form =
dbx()->get_system_obj(
'dbxForm');
6594 $form->init(
'cms-folder',
'cms-admin-folder-form');
6595 $form->_dd = dbxContentLng::ddFolder();
6596 $form->_fd =
'dbxContent_admin|cms-page';
6597 $form->load_fd_messages();
6598 $form->set_form_help_enabled(
false);
6599 $form->_data = array(
6601 'group_read' =>
'parent',
6602 'template' =>
'parent',
6603 'hero_template' =>
'parent',
6604 'hero_image_id' =>
'parent',
6605 'hero_margin_top' =>
'parent',
6606 'hero_height' =>
'parent',
6607 'hero_variant' =>
'parent',
6608 'hero_sticky' =>
'parent',
6609 'hero_scroll_layer' =>
'parent',
6611 $form->add_fld(
'id',
'dbxContent_admin|cms-field-folder-hidden', data: array(
'cms_field' =>
'id'));
6612 $form->add_fld(
'name',
'dbxContent_admin|cms-field-folder-text', label:
$texts->get_fd_message(
'label_name'), rules:
'varchar|min=1', data: array(
'cms_field' =>
'name'));
6613 $form->add_fld(
'parent_id',
'dbxContent_admin|cms-field-folder-select', label:
$texts->get_fd_message(
'label_assignment'), rules:
'int', data: array(
'cms_field' =>
'parent_id'), options: array(
'0' =>
$texts->get_fd_message(
'option_root')));
6614 $form->add_fld(
'template',
'dbxContent_admin|cms-field-folder-content-template-select', label:
$texts->get_fd_message(
'label_template'), rules:
'varchar', data: array(
'cms_field' =>
'template'), options: $this->content_template_values());
6615 $form->add_fld(
'group_read',
'dbxContent_admin|cms-field-folder-rights', label:
$texts->get_fd_message(
'label_read_rights'), rules:
'varchar', data: array(
'cms_field' =>
'group_read'), options: $this->rights_values());
6616 return $form->run();
6619 private function render_settings_form() {
6620 $texts = $this->cms_texts();
6621 $form =
dbx()->get_system_obj(
'dbxForm');
6622 $form->init(
'cms-settings',
'cms-admin-settings-panels');
6623 $form->_dd = dbxContentLng::ddContent();
6624 $form->_fd =
'dbxContent_admin|cms-page';
6625 $form->load_fd_messages();
6626 $form->set_form_help_enabled(
false);
6627 $form->_data = array(
6628 'hero_template' =>
'parent',
6629 'hero_image_id' =>
'parent',
6630 'hero_margin_top' =>
'parent',
6631 'hero_height' =>
'parent',
6632 'hero_variant' =>
'parent',
6633 'hero_sticky' =>
'parent',
6634 'hero_scroll_layer' =>
'parent',
6635 'gallery_template' =>
'image-gallery',
6636 'gallery_visible_count' =>
'3',
6637 'gallery_image_size' =>
'original',
6638 'gallery_lightbox_width' =>
'100vw',
6639 'gallery_overflow' =>
'grid',
6640 'gallery_click_behavior' =>
'lightbox',
6642 $form->add_fld(
'hero_template',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_hero_template'), rules:
'varchar', data: array(
'cms_field' =>
'hero_template'), options: $this->hero_template_values());
6643 $form->add_fld(
'hero_image_id',
'dbxContent_admin|cms-field-hidden', rules:
'varchar', data: array(
'cms_field' =>
'hero_image_id'));
6644 $form->add_fld(
'hero_margin_top',
'dbxContent_admin|cms-field-text', label:
$texts->get_fd_message(
'label_hero_margin_top'), rules:
'varchar', data: array(
'cms_field' =>
'hero_margin_top'));
6645 $form->add_fld(
'hero_height',
'dbxContent_admin|cms-field-text', label:
$texts->get_fd_message(
'label_hero_height'), rules:
'varchar', data: array(
'cms_field' =>
'hero_height'));
6646 $form->add_fld(
'hero_variant',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_hero_variant'), rules:
'varchar', data: array(
'cms_field' =>
'hero_variant'), options: $this->hero_variant_values());
6647 $form->add_fld(
'hero_sticky',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_hero_sticky'), rules:
'varchar', data: array(
'cms_field' =>
'hero_sticky'), options: $this->hero_sticky_values());
6648 $form->add_fld(
'hero_scroll_layer',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_hero_scroll_layer'), rules:
'varchar', data: array(
'cms_field' =>
'hero_scroll_layer'), options: $this->hero_scroll_layer_values());
6649 $form->add_fld(
'gallery_image_size',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_gallery_image_size'), rules:
'varchar', data: array(
'cms_field' =>
'gallery_image_size'), options: $this->gallery_image_size_values());
6650 $form->add_fld(
'gallery_lightbox_width',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_gallery_lightbox_width'), rules:
'varchar', data: array(
'cms_field' =>
'gallery_lightbox_width'), options: $this->gallery_lightbox_width_values());
6651 $form->add_fld(
'gallery_overflow',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_gallery_overflow'), rules:
'varchar', data: array(
'cms_field' =>
'gallery_overflow'), options: $this->gallery_overflow_values());
6652 $form->add_fld(
'gallery_click_behavior',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_gallery_click'), rules:
'varchar', data: array(
'cms_field' =>
'gallery_click_behavior'), options: $this->gallery_click_values());
6653 return $form->run();
6656 public function run($action =
'cms') {
6657 $this->apply_cms_lng_context();
6658 $action = (string)$action;
6660 return $this->reject_action_token($action);
6664 return $this->tree_json();
6665 case 'cms_lng_coverage':
6666 return $this->lng_coverage_json();
6667 case 'cms_lng_preview':
6668 return $this->lng_preview_json();
6669 case 'cms_lng_provision':
6670 return $this->lng_provision_json();
6671 case 'cms_lng_provision_tree':
6672 return $this->lng_provision_tree_json();
6673 case 'cms_lng_reset_sync':
6674 return $this->lng_reset_sync_json();
6675 case 'cms_lng_delete_preview':
6676 return $this->lng_delete_preview_json();
6678 return $this->page_json();
6680 return $this->save_json();
6681 case 'cms_new_page':
6682 return $this->new_page_json();
6683 case 'cms_duplicate_page':
6684 return $this->duplicate_page_json();
6685 case 'cms_new_folder':
6686 return $this->new_folder_json();
6687 case 'cms_save_folder':
6688 return $this->save_folder_json();
6689 case 'cms_delete_folder':
6690 return $this->delete_folder_json();
6691 case 'cms_delete_page':
6692 return $this->delete_page_json();
6693 case 'cms_move_node':
6694 return $this->move_node_json();
6696 return $this->media_json();
6697 case 'cms_media_process':
6698 return $this->media_process();
6700 return $this->upload_json();
6701 case 'cms_external_video':
6702 return $this->external_video_json();
6703 case 'cms_media_folders':
6704 return $this->media_folders_json();
6705 case 'cms_media_folder_create':
6706 return $this->media_folder_create_json();
6707 case 'cms_media_folder_delete':
6708 return $this->media_folder_delete_json();
6709 case 'cms_media_folder_rename':
6710 return $this->media_folder_rename_json();
6711 case 'cms_media_move':
6712 return $this->media_move_json();
6713 case 'cms_media_unused':
6714 return $this->media_unused_json();
6715 case 'cms_remove_media':
6716 return $this->remove_media_json();
6717 case 'cms_delete_media':
6718 return $this->delete_media_json();
6719 case 'cms_edit_media':
6720 return $this->edit_media_json();
6721 case 'cms_set_media_slot':
6722 return $this->set_media_slot_json();
6723 case 'cms_assign_media':
6724 return $this->assign_media_json();
6725 case 'cms_sort_media':
6726 return $this->sort_media_json();
6727 case 'cms_mod_catalog':
6728 return $this->mod_catalog_json();
6729 case 'cms_mod_modules':
6730 return $this->mod_modules_json();
6733 return $this->render_cms();