diff --git a/module/knowledgecenter_update/Ajax/Ajax.php b/module/knowledgecenter_update/Ajax/Ajax.php new file mode 100644 index 0000000..65a100b --- /dev/null +++ b/module/knowledgecenter_update/Ajax/Ajax.php @@ -0,0 +1,1987 @@ +load(); +} +$common = $mysydeDir . "/common/common_functions.inc.php"; +require_once($common); + +$connEnv = (local_environment()) ? $mysydeDir . "/dc.config.php" : $mysydeDir . "/dc-server.config.php"; + +require_once($connEnv); + +db_connect(); + +// AJAX-Handler: Falls per POST eine Aktion übergeben wurde, diese verarbeiten und das Skript beenden. +if (isset($_POST["action"])) { + header('Content-Type: application/json'); + switch ($_POST["action"]) { + case 'update_categories_sorting': + update_categories_sorting(); + break; + case 'update_category_detail': + update_category_detail(); + break; + case 'update_subcategory_sorting': + update_subcategory_sorting(); + break; + case 'link_subcategory': + link_subcategory(); + break; + case 'unlink_subcategory': + unlink_subcategory(); + break; + case 'update_category_links': + update_category_links(); + break; + case 'delete_category': + delete_category(); + break; + case 'update_post_version': + update_post_version(); + break; + case 'insert_post_version': + insert_post_version(); + break; + case 'create_post_draft': + create_post_draft(); + break; + case 'ensure_post_gallery_folder': + ensure_post_gallery_folder(); + break; + case 'delete_post_version': + delete_post_version(); + break; + case 'get_post_comments': + get_post_comments(); + break; + case 'insert_comment': + insert_comment(); + break; + case 'delete_comment': + delete_comment(); + break; + case 'update_post_categories': + update_post_categories(); + break; + case 'get_post_list': + get_post_list(); + break; + case 'get_post_files': + get_post_files(); + break; + case 'update_post_contacts': + update_post_contacts(); + break; + case 'get_contact_cards': + get_contact_cards(); + break; + case 'search_posts': + search_posts(); + break; + case 'update_post_pdf_inline': + update_post_pdf_inline(); + break; + case 'update_post_colors': + update_post_colors(); + break; + case 'delete_post': + delete_post(); + break; + case 'update_post_mandants': + // Erwarte post_id und einen JSON-codierten Array für mandant IDs + $postId = (int) ($_POST['post_id'] ?? 0); + $mandantIds = json_decode($_POST['mandants'], true) ?: []; + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Post-ID']); + exit; + } + $result = update_post_mandants($postId, $mandantIds); + echo json_encode(is_bool($result) && $result ? ['success' => true] : ['error' => $result]); + exit; + + case 'update_post_departments': + $postId = (int) ($_POST['post_id'] ?? 0); + $departmentIds = json_decode($_POST['departments'], true) ?: []; + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Post-ID']); + exit; + } + $result = update_post_departments($postId, $departmentIds); + echo json_encode(is_bool($result) && $result ? ['success' => true] : ['error' => $result]); + exit; + + case 'update_post_roles': + $postId = (int) ($_POST['post_id'] ?? 0); + $roleIds = json_decode($_POST['roles'], true) ?: []; + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Post-ID']); + exit; + } + $result = update_post_roles($postId, $roleIds); + echo json_encode(is_bool($result) && $result ? ['success' => true] : ['error' => $result]); + exit; + case 'update_post_products': + $postId = (int) ($_POST['post_id'] ?? 0); + $productIds = json_decode($_POST['products'] ?? '[]', true) ?: []; + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Post-ID']); + exit; + } + $result = update_post_products($postId, $productIds); + echo json_encode(is_bool($result) && $result ? ['success' => true] : ['error' => $result]); + exit; + case 'save_post_files': + save_post_files(); + break; + case 'delete_post_file': + delete_post_file(); + break; + case 'update_post_certificate': + update_post_certificate(); + break; + case 'update_post_form': + $postId = (int) ($_POST['post_id'] ?? 0); + $formId = (int) ($_POST['form_id'] ?? 0); + + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Post-ID']); + exit; + } + + $result = update_post_form($postId, $formId); + echo json_encode($result === true ? ['success' => true] : ['error' => $result]); + exit; + default: + echo json_encode(['error' => 'Ungültige Aktion']); + exit; + } + exit; +} + +/** + * Aktualisiert die Sortierung der Kategorien in der Hauptliste. + */ +function update_categories_sorting() +{ + if (!isset($_POST['order']) || !is_array($_POST['order'])) { + echo json_encode(['error' => 'Keine Sortierdaten übergeben']); + exit; + } + + $order = $_POST['order']; + foreach ($order as $index => $categoryId) { + $sorting = $index + 1; // 1-indexiert + $categoryId = (int) $categoryId; + $query = "UPDATE knowledgecenter_categories_update SET sorting = $sorting WHERE id = $categoryId"; + if (!mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['error' => "Datenbankfehler bei Kategorie-ID $categoryId"]); + exit; + } + } + echo json_encode(['success' => true]); + exit; +} + +function update_post_form($postId, $formId) +{ + $checkQuery = "SELECT id FROM knowledgecenter_post_form WHERE post_id = $postId"; + $result = mysqli_query($GLOBALS['mysql_con'], $checkQuery); + + if ($result && mysqli_num_rows($result) > 0) { + $query = "UPDATE knowledgecenter_post_form SET form_id = $formId WHERE post_id = $postId"; + } else { + $query = "INSERT INTO knowledgecenter_post_form (post_id, form_id) VALUES ($postId, $formId)"; + } + + if (mysqli_query($GLOBALS['mysql_con'], $query)) { + return true; + } else { + return 'Fehler beim Speichern der Formular-Zuordnung: ' . mysqli_error($GLOBALS['mysql_con']); + } +} + +/** + * Speichert oder aktualisiert die sprachspezifischen Inhalte (Translation) einer Kategorie. + */ +function update_category_detail() +{ + $is_navigation_item = isset($_POST['is_navigation_item']) ? (int) $_POST['is_navigation_item'] : 0; + $enable_post_color_pickers = isset($_POST['enable_post_color_pickers']) ? (int) $_POST['enable_post_color_pickers'] : 0; + $color_hex = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['color_hex'] ?? ''); + $categoryId = (int) ($_POST['id'] ?? 0); + $languageId = (int) ($_POST['language_id'] ?? 0); + $modified_by = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['modified_by'] ?? ''); + $state = isset($_POST['state']) ? (int) $_POST['state'] : 0; + + if ($languageId <= 0) { + echo json_encode(['error' => 'Ungültige Parameter']); + exit; + } + + $title = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['title'] ?? ''); + $image = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['image'] ?? ''); + + if ($categoryId === 0) { + $insertMain = "INSERT INTO knowledgecenter_categories_update + (modified_by, created_at, state, color_hex, enable_post_color_pickers) + VALUES ('$modified_by', NOW(), $state, '$color_hex', $enable_post_color_pickers)"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertMain)) { + echo json_encode(['error' => 'Fehler beim Erstellen der Hauptkategorie: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + $categoryId = mysqli_insert_id($GLOBALS['mysql_con']); + } + + $checkQuery = "SELECT id FROM knowledgecenter_category_translations + WHERE category_id = $categoryId AND language_id = $languageId LIMIT 1"; + $checkResult = mysqli_query($GLOBALS['mysql_con'], $checkQuery); + + if ($checkResult && mysqli_num_rows($checkResult) > 0) { + $query = "UPDATE knowledgecenter_category_translations + SET title = '$title', image = '$image', modified_at = NOW(), is_navigation_item = $is_navigation_item + WHERE category_id = $categoryId AND language_id = $languageId"; + } else { + $query = "INSERT INTO knowledgecenter_category_translations + (category_id, language_id, title, image, created_at, is_navigation_item) + VALUES ($categoryId, $languageId, '$title', '$image', NOW(), $is_navigation_item)"; + } + + if (!mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['error' => 'Datenbankfehler: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + $mainUpdate = "UPDATE knowledgecenter_categories_update + SET modified_by = '$modified_by', + modified_at = NOW(), + state = $state, + enable_post_color_pickers = $enable_post_color_pickers, + color_hex = '$color_hex' + WHERE id = $categoryId"; + if (!mysqli_query($GLOBALS['mysql_con'], $mainUpdate)) { + echo json_encode(['error' => 'Datenbankfehler beim Update der Hauptkategorie: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + $forwardUrl = "/intranet/de/wiki/?detail=$categoryId"; + if ($is_navigation_item === 1) { + $existsNavQuery = "SELECT id FROM main_navigation + WHERE forward_url = '$forwardUrl' + LIMIT 1"; + $navResult = mysqli_query($GLOBALS['mysql_con'], $existsNavQuery); + if (mysqli_num_rows($navResult) === 0) { + $menuName = mysqli_real_escape_string($GLOBALS['mysql_con'], $title); + $queryNav = "INSERT INTO main_navigation ( + main_site_id, main_language_id, parent_id, sorting, level, code, menu_name, + title_name, active, validity_from, validity_to, modified_date, modified_admin_user_id, + forward_type, forward_navigation_id, forward_page_id, forward_url, + navigation_icon, hidden, is_landing_page, description + ) VALUES ( + 1, $languageId, NULL, 9999, 1, 'kc_$categoryId', '$menuName', + '$menuName', 1, NULL, NULL, NOW(), 0, + 5, 0, 0, '$forwardUrl', + 'knowledgecenter', 0, 0, '' + )"; + mysqli_query($GLOBALS['mysql_con'], $queryNav); + } + } else { + $deleteNavQuery = "DELETE FROM main_navigation WHERE forward_url = '$forwardUrl'"; + mysqli_query($GLOBALS['mysql_con'], $deleteNavQuery); + } + + $queryTimestamp = "SELECT modified_at, modified_by FROM knowledgecenter_categories_update WHERE id = $categoryId"; + $resultTimestamp = mysqli_query($GLOBALS['mysql_con'], $queryTimestamp); + $row = mysqli_fetch_assoc($resultTimestamp); + $fmt = new IntlDateFormatter('de_DE', IntlDateFormatter::LONG, IntlDateFormatter::SHORT); + $timestamp = strtotime($row['modified_at']); + $formattedAt = $fmt->format($timestamp); + + echo json_encode([ + 'success' => true, + 'modified_at' => $formattedAt, + 'modified_by' => $row['modified_by'], + 'category_id' => $categoryId, + 'is_navigation_item' => $is_navigation_item + ]); + exit; +} + +/** + * Aktualisiert die Sortierung der verknüpften Unterkategorien. + */ +function update_subcategory_sorting() +{ + $parentId = (int) ($_POST['parent_id'] ?? 0); + $order = $_POST['order'] ?? []; + + if ($parentId <= 0 || !is_array($order)) { + echo json_encode(['error' => 'Ungültige Parameter']); + exit; + } + + foreach ($order as $index => $childId) { + $childId = (int) $childId; + $sorting = $index + 1; + $query = "UPDATE knowledgecenter_category_links + SET sorting = $sorting + WHERE parent_category_id = $parentId AND child_category_id = $childId"; + if (!mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['error' => "Fehler beim Sortieren: " . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + } + echo json_encode(['success' => true]); + exit; +} + +/** + * Verknüpft eine neue Unterkategorie mit der aktuellen Kategorie. + */ +function link_subcategory() +{ + $parent_id = (int) ($_POST['parent_id'] ?? 0); + $child_id = (int) ($_POST['child_id'] ?? 0); + + if ($parent_id <= 0 || $child_id <= 0) { + echo json_encode(['error' => 'Ungültige Parameter']); + exit; + } + + // Bestimme den nächsten Sortierwert für die neue Verknüpfung + $query = "SELECT IFNULL(MAX(sorting), 0) + 1 AS next_sort + FROM knowledgecenter_category_links + WHERE parent_category_id = $parent_id"; + $result = mysqli_query($GLOBALS['mysql_con'], $query); + $row = mysqli_fetch_assoc($result); + $nextSort = (int) $row['next_sort']; + + $insertQuery = "INSERT INTO knowledgecenter_category_links (parent_category_id, child_category_id, sorting) + VALUES ($parent_id, $child_id, $nextSort)"; + if (mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['error' => 'Datenbankfehler: ' . mysqli_error($GLOBALS['mysql_con'])]); + } + exit; +} + +/** + * Entfernt die Verknüpfung einer Unterkategorie von der aktuellen Kategorie. + */ +function unlink_subcategory() +{ + $parent_id = (int) ($_POST['parent_id'] ?? 0); + $child_id = (int) ($_POST['child_id'] ?? 0); + + if ($parent_id <= 0 || $child_id <= 0) { + echo json_encode(['error' => 'Ungültige Parameter']); + exit; + } + + $query = "DELETE FROM knowledgecenter_category_links WHERE parent_category_id = $parent_id AND child_category_id = $child_id"; + if (mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['error' => 'Datenbankfehler: ' . mysqli_error($GLOBALS['mysql_con'])]); + } + exit; +} + +/** + * Löscht alte Mandanten-Verknüpfungen und fügt neue ein. + */ +function update_category_mandants($categoryId, $mandantIds) +{ + // Alte Verknüpfungen löschen + $deleteQuery = "DELETE FROM knowledgecenter_category_mandant WHERE category_id = $categoryId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + return "Fehler beim Löschen der Mandanten-Verknüpfungen: " . mysqli_error($GLOBALS['mysql_con']); + } + // Neue Verknüpfungen einfügen + foreach ($mandantIds as $mandantId) { + $mandantId = (int) $mandantId; + $insertQuery = "INSERT INTO knowledgecenter_category_mandant (category_id, mandant_id, created_at) + VALUES ($categoryId, $mandantId, NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + return "Fehler beim Einfügen der Mandant-ID $mandantId: " . mysqli_error($GLOBALS['mysql_con']); + } + } + return true; +} + +/** + * Löscht alte Department-Verknüpfungen und fügt neue ein. + */ +function update_category_departments($categoryId, $departmentIds) +{ + $deleteQuery = "DELETE FROM knowledgecenter_category_department WHERE category_id = $categoryId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + return "Fehler beim Löschen der Department-Verknüpfungen: " . mysqli_error($GLOBALS['mysql_con']); + } + foreach ($departmentIds as $departmentId) { + $departmentId = (int) $departmentId; + $insertQuery = "INSERT INTO knowledgecenter_category_department (category_id, department_id, created_at) + VALUES ($categoryId, $departmentId, NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + return "Fehler beim Einfügen der Department-ID $departmentId: " . mysqli_error($GLOBALS['mysql_con']); + } + } + return true; +} + +/** + * Löscht alte Rollen-Verknüpfungen und fügt neue ein. + */ +function update_category_roles($categoryId, $roleIds) +{ + $deleteQuery = "DELETE FROM knowledgecenter_category_role WHERE category_id = $categoryId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + return "Fehler beim Löschen der Rollen-Verknüpfungen: " . mysqli_error($GLOBALS['mysql_con']); + } + foreach ($roleIds as $roleId) { + $roleId = (int) $roleId; + $insertQuery = "INSERT INTO knowledgecenter_category_role (category_id, role_id, created_at) + VALUES ($categoryId, $roleId, NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + return "Fehler beim Einfügen der Rollen-ID $roleId: " . mysqli_error($GLOBALS['mysql_con']); + } + } + return true; +} + +/** + * Kombinierte Funktion, die alle drei Link-Tabellen aktualisiert. + */ +function update_category_links() +{ + $categoryId = (int) ($_POST['id'] ?? 0); + // Die Tagify-Daten werden als JSON-String gesendet + $mandantIds = json_decode($_POST['mandants'], true) ?: []; + $departmentIds = json_decode($_POST['departments'], true) ?: []; + $roleIds = json_decode($_POST['roles'], true) ?: []; + + $errors = []; + + $result = update_category_mandants($categoryId, $mandantIds); + if ($result !== true) { + $errors[] = $result; + } + + $result = update_category_departments($categoryId, $departmentIds); + if ($result !== true) { + $errors[] = $result; + } + + $result = update_category_roles($categoryId, $roleIds); + if ($result !== true) { + $errors[] = $result; + } + + if (empty($errors)) { + // Hier werden für jede Gruppe die vollständigen Datensätze (value und name) abgefragt + $savedMandants = []; + foreach ($mandantIds as $id) { + $id = (int) $id; + $sql = "SELECT id, description FROM main_mandant WHERE id = $id LIMIT 1"; + $res = mysqli_query($GLOBALS['mysql_con'], $sql); + if ($res && mysqli_num_rows($res) > 0) { + $row = mysqli_fetch_assoc($res); + $savedMandants[] = ['value' => $row['id'], 'name' => $row['description']]; + } + } + + $savedDepartments = []; + foreach ($departmentIds as $id) { + $id = (int) $id; + $sql = "SELECT id, description FROM main_department WHERE id = $id LIMIT 1"; + $res = mysqli_query($GLOBALS['mysql_con'], $sql); + if ($res && mysqli_num_rows($res) > 0) { + $row = mysqli_fetch_assoc($res); + $savedDepartments[] = ['value' => $row['id'], 'name' => $row['description']]; + } + } + + $savedRoles = []; + foreach ($roleIds as $id) { + $id = (int) $id; + $sql = "SELECT id, description FROM main_role WHERE id = $id LIMIT 1"; + $res = mysqli_query($GLOBALS['mysql_con'], $sql); + if ($res && mysqli_num_rows($res) > 0) { + $row = mysqli_fetch_assoc($res); + $savedRoles[] = ['value' => $row['id'], 'name' => $row['description']]; + } + } + + echo json_encode([ + 'success' => true, + 'mandants' => $savedMandants, + 'departments' => $savedDepartments, + 'roles' => $savedRoles + ]); + } else { + echo json_encode(['error' => implode("; ", $errors)]); + } + exit; +} + +function delete_category() +{ + $categoryId = (int) ($_POST['id'] ?? 0); + if ($categoryId <= 0) { + echo json_encode(['error' => 'Ungültige Kategorie-ID']); + exit; + } + + // Lösche Navigationseintrag für diese Kategorie, falls vorhanden + $forwardUrl = "/intranet/de/wiki/?detail=" . $categoryId; + $navCheckQuery = "SELECT id FROM main_navigation WHERE forward_url = '$forwardUrl' LIMIT 1"; + $navCheckResult = mysqli_query($GLOBALS['mysql_con'], $navCheckQuery); + if ($navCheckResult && mysqli_num_rows($navCheckResult) > 0) { + mysqli_query($GLOBALS['mysql_con'], "DELETE FROM main_navigation WHERE forward_url = '$forwardUrl'"); + } + + // Bei korrekter Fremdschlüsseldefinition (z. B. ON DELETE CASCADE) reicht oft ein DELETE aus. + $query = "DELETE FROM knowledgecenter_categories_update WHERE id = $categoryId"; + if (mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['error' => 'Fehler beim Löschen: ' . mysqli_error($GLOBALS['mysql_con'])]); + } + exit; +} + +function normalize_hex_color($color, $default = '#FFFFFF') +{ + $color = trim((string) $color); + if (preg_match('/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/', $color)) { + return strtoupper($color); + } + return $default; +} + +/** + * Aktualisiert eine bestehende Post-Version. + */ +function update_post_version() +{ + // Stelle sicher, dass alle Sprachdaten als Array übermittelt wurden + if (!isset($_POST['title']) || !is_array($_POST['title'])) { + echo json_encode(['error' => 'Keine Sprachdaten übermittelt']); + exit; + } + + // Hole die Beitrags-ID aus den POST-Daten (wichtig für den Insert-Fallback) + $postId = (int) ($_POST['post_id'] ?? 0); + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Beitrags-ID']); + exit; + } + $keepModifiedAt = isset($_POST['keep_modified_at']) && (int) $_POST['keep_modified_at'] === 1; + + $backgroundColor = normalize_hex_color($_POST['background_color'] ?? '#FFFFFF', '#FFFFFF'); + $textColor = normalize_hex_color($_POST['text_color'] ?? '#000000', '#000000'); + + $updatePostQuery = "UPDATE knowledgecenter_posts + SET background_color = '$backgroundColor', + text_color = '$textColor'" . ($keepModifiedAt ? "" : ", modified_at = NOW()") . " + WHERE id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $updatePostQuery)) { + echo json_encode(['error' => 'Fehler beim Aktualisieren der Post-Farben: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + // Iteriere über alle übermittelten Sprachfelder + foreach ($_POST['title'] as $langId => $title) { + $langId = (int) $langId; + $title = mysqli_real_escape_string($GLOBALS['mysql_con'], $title); + $image = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['image'][$langId] ?? ''); + $teaser = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['teaser'][$langId] ?? ''); + $content = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['content'][$langId] ?? ''); + $author_id = isset($_POST['author_id'][$langId]) ? (int) $_POST['author_id'][$langId] : 0; + $link = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link'][$langId] ?? ''); + $link_label = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link_label'][$langId] ?? ''); + // State-Rohwert (Default 0 = Inaktiv, falls nichts übergeben) + $stateRaw = $_POST['state'][$langId] ?? '0'; + // In Integer casten + $state = (int) $stateRaw; + // Hier die neuen Felder: + $valid_from = isset($_POST['valid_from'][$langId]) ? mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['valid_from'][$langId]) : 'NULL'; + $valid_until = isset($_POST['valid_until'][$langId]) ? mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['valid_until'][$langId]) : 'NULL'; + + // Wenn ein Datum übergeben wird, müssen wir es als Zeichenkette in SQL einbinden, andernfalls NULL + $valid_from_sql = ($valid_from !== 'NULL' && !empty($valid_from)) ? "'$valid_from'" : "NULL"; + $valid_until_sql = ($valid_until !== 'NULL' && !empty($valid_until)) ? "'$valid_until'" : "NULL"; + + // Prüfe, ob es bereits eine Version gibt… + $versionId = isset($_POST['version_id'][$langId]) ? (int) $_POST['version_id'][$langId] : 0; + if ($versionId <= 0) { + // INSERT: Ermittle die neue Versionnummer… + $query = "SELECT MAX(version_number) AS max_ver + FROM knowledgecenter_post_versions + WHERE post_id = $postId AND language_id = $langId"; + $result = mysqli_query($GLOBALS['mysql_con'], $query); + $row = mysqli_fetch_assoc($result); + $newVersion = ((int) $row['max_ver']) + 1; + + $insertModifiedAtSql = "NOW()"; + if ($keepModifiedAt) { + $prevQuery = "SELECT modified_at FROM knowledgecenter_post_versions + WHERE post_id = $postId AND language_id = $langId + ORDER BY version_number DESC + LIMIT 1"; + $prevResult = mysqli_query($GLOBALS['mysql_con'], $prevQuery); + if ($prevResult && mysqli_num_rows($prevResult) > 0) { + $prevRow = mysqli_fetch_assoc($prevResult); + if (!empty($prevRow['modified_at'])) { + $prevModifiedAt = mysqli_real_escape_string($GLOBALS['mysql_con'], $prevRow['modified_at']); + $insertModifiedAtSql = "'$prevModifiedAt'"; + } + } + } + + $insertQuery = " + INSERT INTO knowledgecenter_post_versions + (post_id, language_id, version_number, + title, image, link, link_label, teaser, content, author_id, state, + modified_at, valid_from, valid_until, background_color, text_color) + VALUES + ($postId, $langId, $newVersion, + '$title', '$image', '$link', '$link_label', '$teaser', '$content', $author_id, $state, + $insertModifiedAtSql, $valid_from_sql, $valid_until_sql, '$backgroundColor', '$textColor') + "; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + echo json_encode(['error' => 'Fehler beim Einfügen für Sprache ' . $langId . ': ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + continue; + } + + // UPDATE: Aktualisiere auch die Datumsfelder + $query = " + UPDATE knowledgecenter_post_versions + SET title = '$title', + image = '$image', + link = '$link', + link_label = '$link_label', + teaser = '$teaser', + content = '$content', + author_id = $author_id, + state = $state, + valid_from = $valid_from_sql, + valid_until = $valid_until_sql, + background_color = '$backgroundColor', + text_color = '$textColor'" . ($keepModifiedAt ? "" : ",\n modified_at = NOW()") . " + WHERE id = $versionId AND language_id = $langId + "; + if (!mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['error' => 'Fehler beim Aktualisieren für Sprache ' . $langId . ': ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + } + echo json_encode(['success' => true]); + exit; +} + +/** + * Erstellt eine neue Post-Version basierend auf den übermittelten Feldern. + */ +function insert_post_version() +{ + // Hole die Beitrags-ID aus den POST-Daten (0 = neuer Beitrag) + $postId = (int) ($_POST['post_id'] ?? 0); + // Initiale Kategorie (falls über Form übergeben) + $initialCategoryId = isset($_POST['initial_category_id']) ? (int) $_POST['initial_category_id'] : 0; + $initialProductId = isset($_POST['InitialProduct']) ? (int) $_POST['InitialProduct'] : 0; + $keepModifiedAt = isset($_POST['keep_modified_at']) && (int) $_POST['keep_modified_at'] === 1; + $backgroundColor = normalize_hex_color($_POST['background_color'] ?? '#FFFFFF', '#FFFFFF'); + $textColor = normalize_hex_color($_POST['text_color'] ?? '#000000', '#000000'); + + // Wenn keine gültige Post-ID vorliegt, erstelle einen neuen Beitrag + if ($postId <= 0) { + $insertPostQuery = "INSERT INTO knowledgecenter_posts (created_at, background_color, text_color) VALUES (NOW(), '$backgroundColor', '$textColor')"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertPostQuery)) { + echo json_encode(['error' => 'Fehler beim Erstellen des neuen Beitrags: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + // Verwende die neu generierte Post-ID für den weiteren Insert + $postId = mysqli_insert_id($GLOBALS['mysql_con']); + } else { + $updatePostQuery = "UPDATE knowledgecenter_posts + SET background_color = '$backgroundColor', + text_color = '$textColor'" . ($keepModifiedAt ? "" : ",\n modified_at = NOW()") . " + WHERE id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $updatePostQuery)) { + echo json_encode(['error' => 'Fehler beim Aktualisieren der Post-Farben: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + } + // Kategorie-Verknüpfung bei neu angelegtem Beitrag + if ($initialCategoryId > 0 && $postId > 0) { + $catInsert = "INSERT INTO knowledgecenter_category_post (post_id, category_id) VALUES ($postId, $initialCategoryId)"; + mysqli_query($GLOBALS['mysql_con'], $catInsert); + } + if ($initialProductId > 0 && $postId > 0) { + $prodInsert = "INSERT INTO product_knowledgecenter_post_link (product_id, post_id) VALUES ($initialProductId, $postId)"; + mysqli_query($GLOBALS['mysql_con'], $prodInsert); + } + + $folderInfo = ensure_post_gallery_dir($postId); + if (!$folderInfo['success']) { + echo json_encode(['error' => 'Fehler beim Erstellen des Beitragsordners: ' . $folderInfo['error']]); + exit; + } + + // Sicherstellen, dass Sprachdaten übermittelt wurden + if (!isset($_POST['title']) || !is_array($_POST['title'])) { + echo json_encode(['error' => 'Keine Sprachdaten übermittelt']); + exit; + } + + $newIds = []; + foreach ($_POST['title'] as $langId => $title) { + $langId = (int) $langId; + $title = trim($title); + $title = mysqli_real_escape_string($GLOBALS['mysql_con'], $title); + $image = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['image'][$langId] ?? ''); + $teaser = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['teaser'][$langId] ?? ''); + $content = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['content'][$langId] ?? ''); + $link = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link'][$langId] ?? ''); + $link_label = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['link_label'][$langId] ?? ''); + $author_id = isset($_POST['author_id'][$langId]) ? (int) $_POST['author_id'][$langId] : 0; + // State-Rohwert (Default 0 = Inaktiv, falls nichts übergeben) + $stateRaw = $_POST['state'][$langId] ?? '0'; + // In Integer casten + $state = (int) $stateRaw; + $valid_from = isset($_POST['valid_from'][$langId]) ? mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['valid_from'][$langId]) : 'NULL'; + $valid_until = isset($_POST['valid_until'][$langId]) ? mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['valid_until'][$langId]) : 'NULL'; + $valid_from_sql = ($valid_from !== 'NULL' && !empty($valid_from)) ? "'$valid_from'" : "NULL"; + $valid_until_sql = ($valid_until !== 'NULL' && !empty($valid_until)) ? "'$valid_until'" : "NULL"; + + $query = "SELECT MAX(version_number) AS max_ver + FROM knowledgecenter_post_versions + WHERE post_id = $postId AND language_id = $langId"; + $result = mysqli_query($GLOBALS['mysql_con'], $query); + $row = mysqli_fetch_assoc($result); + $newVersion = ((int) $row['max_ver']) + 1; + + $insertModifiedAtSql = "NOW()"; + if ($keepModifiedAt) { + $prevQuery = "SELECT modified_at FROM knowledgecenter_post_versions + WHERE post_id = $postId AND language_id = $langId + ORDER BY version_number DESC + LIMIT 1"; + $prevResult = mysqli_query($GLOBALS['mysql_con'], $prevQuery); + if ($prevResult && mysqli_num_rows($prevResult) > 0) { + $prevRow = mysqli_fetch_assoc($prevResult); + if (!empty($prevRow['modified_at'])) { + $prevModifiedAt = mysqli_real_escape_string($GLOBALS['mysql_con'], $prevRow['modified_at']); + $insertModifiedAtSql = "'$prevModifiedAt'"; + } + } + } + + $insertQuery = "INSERT INTO knowledgecenter_post_versions + (post_id, language_id, version_number, title, image, link, link_label, teaser, content, author_id, state, modified_at, valid_from, valid_until, background_color, text_color) + VALUES ($postId, $langId, $newVersion, '$title', '$image', '$link', '$link_label', '$teaser', '$content', $author_id, $state, $insertModifiedAtSql, $valid_from_sql, $valid_until_sql, '$backgroundColor', '$textColor')"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + echo json_encode(['error' => "Fehler beim Einfügen für Sprache $langId: " . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + $newIds[$langId] = mysqli_insert_id($GLOBALS['mysql_con']); + } + + echo json_encode([ + 'success' => true, + 'new_post_id' => $postId, + 'gallery_start_path' => $folderInfo['start_path'], + 'new_ids' => $newIds + ]); + exit; +} + +function get_post_folder_hash($postId) +{ + return substr(sha1('kc_post_' . (int) $postId), 0, 24); +} + +function ensure_post_gallery_dir($postId) +{ + $postId = (int) $postId; + if ($postId <= 0) { + return ['success' => false, 'error' => 'Ungültige Post-ID']; + } + + $hash = get_post_folder_hash($postId); + $galleryRelativePath = 'knowledgecenter_update/posts/' . $hash; + $basePath = $_SERVER['DOCUMENT_ROOT'] . '/userdata/knowledgecenter_update/posts'; + $postPath = $basePath . '/' . $hash; + + if (!is_dir($basePath) && !@mkdir($basePath, 0775, true)) { + return ['success' => false, 'error' => 'Basisordner konnte nicht erstellt werden']; + } + + if (!is_dir($postPath) && !@mkdir($postPath, 0775, true)) { + return ['success' => false, 'error' => 'Post-Ordner konnte nicht erstellt werden']; + } + + // Globalen shared-Symlink idempotent anlegen. Ziel ist relativ, damit der + // Symlink bei Backups/Restores oder Mount-Point-Wechseln stabil bleibt. + // Fehler hier sind nicht-blockierend: ein fehlender shared-Symlink darf die + // Post-Erstellung nicht verhindern. + $sharedLink = $postPath . '/shared'; + if (!is_link($sharedLink) && !file_exists($sharedLink)) { + @symlink('../../shared', $sharedLink); + } + + return [ + 'success' => true, + 'hash' => $hash, + 'start_path' => $galleryRelativePath, + ]; +} + +function delete_post_gallery_dir($postId) +{ + $postId = (int) $postId; + if ($postId <= 0) { + return ['success' => false, 'error' => 'Ungültige Post-ID']; + } + + $userdataRoot = realpath($_SERVER['DOCUMENT_ROOT'] . '/userdata'); + if ($userdataRoot === false) { + return ['success' => false, 'error' => 'userdata-Verzeichnis nicht gefunden']; + } + + $postsBasePath = $userdataRoot . '/knowledgecenter_update/posts'; + $postsBaseReal = realpath($postsBasePath); + if ($postsBaseReal === false) { + return ['success' => true, 'skipped' => true]; + } + + $targetPath = $postsBaseReal . '/' . get_post_folder_hash($postId); + $targetReal = realpath($targetPath); + if ($targetReal === false) { + return ['success' => true, 'skipped' => true]; + } + + if ($targetReal !== $postsBaseReal && strpos($targetReal, $postsBaseReal . DIRECTORY_SEPARATOR) !== 0) { + return ['success' => false, 'error' => 'Ungültiger Zielpfad']; + } + + // Symlinks im Top-Level zuerst entfernen, BEVOR RecursiveDirectoryIterator startet. + // unlink() auf einem Symlink löscht nur den Symlink selbst, nicht das Ziel — so + // bleiben referenzierte globale Ressourcen (z.B. shared/) garantiert unangetastet. + foreach (scandir($targetReal, SCANDIR_SORT_NONE) as $entry) { + if ($entry === '.' || $entry === '..') continue; + $entryPath = $targetReal . '/' . $entry; + if (is_link($entryPath)) { + if (!@unlink($entryPath)) { + return ['success' => false, 'error' => 'Symlink konnte nicht gelöscht werden: ' . $entry]; + } + } + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($targetReal, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $item) { + $itemPath = $item->getPathname(); + // Defense-in-Depth: falls trotz Top-Level-Bereinigung ein Symlink auftaucht + // (z.B. tief verschachtelt), niemals dem Ziel folgen. + if (is_link($itemPath)) { + if (!@unlink($itemPath)) { + return ['success' => false, 'error' => 'Symlink konnte nicht gelöscht werden']; + } + continue; + } + if ($item->isDir()) { + if (!@rmdir($itemPath)) { + return ['success' => false, 'error' => 'Unterordner konnte nicht gelöscht werden']; + } + } else { + if (!@unlink($itemPath)) { + return ['success' => false, 'error' => 'Datei konnte nicht gelöscht werden']; + } + } + } + + if (!@rmdir($targetReal)) { + return ['success' => false, 'error' => 'Post-Ordner konnte nicht gelöscht werden']; + } + + return ['success' => true]; +} + +function create_post_draft() +{ + $insertPostQuery = "INSERT INTO knowledgecenter_posts (created_at) VALUES (NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertPostQuery)) { + echo json_encode(['error' => 'Fehler beim Erstellen des Entwurfs: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + $postId = (int) mysqli_insert_id($GLOBALS['mysql_con']); + $folderInfo = ensure_post_gallery_dir($postId); + if (!$folderInfo['success']) { + echo json_encode(['error' => 'Entwurf erstellt, aber Ordner fehlgeschlagen: ' . $folderInfo['error']]); + exit; + } + + echo json_encode([ + 'success' => true, + 'new_post_id' => $postId, + 'post_hash' => $folderInfo['hash'], + 'gallery_start_path' => $folderInfo['start_path'], + ]); + exit; +} + +function ensure_post_gallery_folder() +{ + $postId = (int) ($_POST['post_id'] ?? 0); + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Post-ID']); + exit; + } + + $folderInfo = ensure_post_gallery_dir($postId); + if (!$folderInfo['success']) { + echo json_encode(['error' => 'Fehler beim Erstellen des Beitragsordners: ' . $folderInfo['error']]); + exit; + } + + echo json_encode([ + 'success' => true, + 'post_id' => $postId, + 'post_hash' => $folderInfo['hash'], + 'gallery_start_path' => $folderInfo['start_path'], + ]); + exit; +} +function delete_post_version() +{ + $postId = (int) ($_POST['post_id'] ?? 0); + $versionNumber = (int) ($_POST['version_number'] ?? 0); + if ($postId <= 0 || $versionNumber <= 0) { + echo json_encode(['error' => 'Ungültige Parameter']); + exit; + } + + $isEmbedded = (int) ($_POST['embedded'] ?? 0); + + // Lösche alle Versionseinträge für diesen Beitrag und diese Version (alle Sprachvarianten) + $query = "DELETE FROM knowledgecenter_post_versions WHERE post_id = $postId AND version_number = $versionNumber"; + if (!mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['error' => 'Fehler beim Löschen der Version: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + // Prüfe, ob noch Versionen zu diesem Beitrag existieren + $queryCount = "SELECT COUNT(*) AS cnt FROM knowledgecenter_post_versions WHERE post_id = $postId"; + $resCount = mysqli_query($GLOBALS['mysql_con'], $queryCount); + if ($resCount && mysqli_num_rows($resCount) > 0) { + $row = mysqli_fetch_assoc($resCount); + $countVersions = (int) $row['cnt']; + } else { + $countVersions = 0; + } + + $response = ['success' => true]; + // Wenn keine Versionen mehr vorhanden sind: Beitrag löschen und Redirect-Pfad zurückgeben + if ($countVersions === 0) { + $deletePostQuery = "DELETE FROM knowledgecenter_posts WHERE id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deletePostQuery)) { + echo json_encode(['error' => 'Fehler beim Löschen des Beitrags: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + $deleteFolderResult = delete_post_gallery_dir($postId); + if (!$deleteFolderResult['success']) { + echo json_encode(['error' => 'Beitrag gelöscht, aber Ordner konnte nicht gelöscht werden: ' . $deleteFolderResult['error']]); + exit; + } + + $action = ($isEmbedded) ? 'PostList' : 'Category'; + $response['redirect'] = '?action=' . $action . '&embedded=' . $isEmbedded; + } + + + echo json_encode($response); + exit; +} + +function get_post_comments() +{ + $postId = (int) ($_POST['post_id'] ?? 0); + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Beitrags-ID']); + exit; + } + + // Achte darauf, dass auch parent_id abgerufen wird. + $query = "SELECT kc.*, mc.name AS author + FROM knowledgecenter_post_comments AS kc + LEFT JOIN main_contact AS mc ON kc.author_id = mc.id + WHERE kc.post_id = $postId + ORDER BY kc.created_at ASC"; + $result = mysqli_query($GLOBALS['mysql_con'], $query); + if (!$result) { + echo json_encode(['error' => 'Datenbankfehler: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + $comments = []; + while ($row = mysqli_fetch_assoc($result)) { + $comments[] = $row; + } + + echo json_encode(['success' => true, 'comments' => $comments]); + exit; +} + + +function insert_comment() +{ + $postId = (int) ($_POST['post_id'] ?? 0); + $text = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['text'] ?? ''); + $author_id = (int) ($_POST['author_id'] ?? 0); + // Neuen Parameter für Antwort (optional) + $parentId = isset($_POST['parent_id']) ? (int) $_POST['parent_id'] : 0; + + if ($postId <= 0 || empty($text) || $author_id <= 0) { + echo json_encode(['error' => 'Ungültige Eingaben: Post-ID, Text und Author-ID sind erforderlich.']); + exit; + } + + // Falls kein Parent vorhanden, setze NULL + $parentValue = ($parentId > 0) ? $parentId : "NULL"; + + $query = "INSERT INTO knowledgecenter_post_comments (post_id, text, author_id, parent_id, created_at) + VALUES ($postId, '$text', $author_id, $parentValue, NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['error' => 'Datenbankfehler: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + $newCommentId = mysqli_insert_id($GLOBALS['mysql_con']); + + echo json_encode(['success' => true, 'id' => $newCommentId]); + exit; +} + +function delete_comment() +{ + // Kommentar-ID aus den POST-Daten + $commentId = (int) ($_POST['comment_id'] ?? 0); + if ($commentId <= 0) { + echo json_encode(['error' => 'Ungültige Kommentar-ID']); + exit; + } + + // Kommentar auslesen + $query = "SELECT author_id FROM knowledgecenter_post_comments WHERE id = $commentId LIMIT 1"; + $result = mysqli_query($GLOBALS['mysql_con'], $query); + if (!$result || mysqli_num_rows($result) == 0) { + echo json_encode(['error' => 'Kommentar nicht gefunden.']); + exit; + } + $row = mysqli_fetch_assoc($result); + $commentAuthorId = (int) $row['author_id']; + + // Den aktuell angemeldeten Benutzer ermitteln (Sicherstellen, dass $GLOBALS['main_contact']['id'] gesetzt ist) + $currentUserId = (int) $_POST['main_contact_id']; + + // Nur eigene Kommentare löschen + if ($commentAuthorId !== $currentUserId) { + echo json_encode(['error' => 'Sie dürfen diesen Kommentar nicht löschen.']); + exit; + } + + // Kommentar löschen + $deleteQuery = "DELETE FROM knowledgecenter_post_comments WHERE id = $commentId"; + if (mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['error' => 'Fehler beim Löschen: ' . mysqli_error($GLOBALS['mysql_con'])]); + } + exit; +} + + +/** + * Löscht alle bestehenden Kategorie-Zuordnungen in der Tabelle knowledgecenter_category_post + * für den gegebenen Post und fügt dann für jede ausgewählte Kategorie einen neuen Eintrag ein. + */ +function update_post_categories() +{ + // post_id und die Kategorien-IDs auslesen (als JSON-codiertes Array) + $postId = (int) ($_POST['post_id'] ?? 0); + $categoriesJson = $_POST['categories'] ?? '[]'; + $categories = json_decode($categoriesJson, true); + + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Beitrags-ID']); + exit; + } + if (!is_array($categories)) { + echo json_encode(['error' => 'Fehlerhafte Kategorien-Daten']); + exit; + } + + // Bestehende Einträge für diesen Beitrag löschen + $deleteQuery = "DELETE FROM knowledgecenter_category_post WHERE post_id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + echo json_encode(['error' => 'Fehler beim Löschen alter Einträge: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + // Für jede ausgewählte Kategorie einen neuen Eintrag einfügen + foreach ($categories as $catId) { + $catId = (int) $catId; + if ($catId > 0) { + $insertQuery = "INSERT INTO knowledgecenter_category_post (post_id, category_id) VALUES ($postId, $catId)"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + echo json_encode(['error' => "Fehler beim Einfügen der Kategorie $catId: " . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + } + } + + echo json_encode(['success' => true]); + exit; +} + +function get_post_list() +{ + $languageId = isset($_POST['language_id']) ? $_POST['language_id'] : '1'; + $languageCode = isset($_POST['language_code']) ? $_POST['language_code'] : 'de'; + $categoryId = isset($_POST['category_id']) + ? (int) $_POST['category_id'] + : 0; + + // Prüfe, ob ein redirectURL-Parameter übergeben wurde, sonst verwende $_SERVER['REQUEST_URI'] + $passedRedirectURL = isset($_POST['redirectURL']) ? $_POST['redirectURL'] : $_SERVER['REQUEST_URI']; + $redirectUrl = urlencode($passedRedirectURL); + + // Lese den Suchbegriff aus (optional) + $search = ''; + if (isset($_POST['query'])) { + $search = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST['query']); + } + + // Erstelle einen zusätzlichen Filter – wenn ein Suchbegriff vorhanden ist, filtere nach Titel ODER Kategorie + if ($search) { + $searchCondition = " + AND ( + pv.title LIKE '%$search%' + OR EXISTS ( + SELECT 1 + FROM knowledgecenter_category_post AS cp + JOIN knowledgecenter_category_translations AS t + ON cp.category_id = t.category_id + AND t.language_id = $languageId + WHERE cp.post_id = kp.id + AND t.title LIKE '%$search%' + ) + OR EXISTS ( + SELECT 1 + FROM knowledgecenter_post_versions AS kpv2 + JOIN main_contact AS sc + ON kpv2.author_id = sc.id + WHERE kpv2.post_id = kp.id + AND sc.name LIKE '%$search%' + ) + )"; + } else { + $searchCondition = ""; + } + + if ($categoryId) { + // nur Posts, die in dieser Kategorie sind + $categoryCondition = " + AND EXISTS ( + SELECT 1 + FROM knowledgecenter_category_post AS cp + WHERE cp.post_id = kp.id + AND cp.category_id = $categoryId + )"; + } else { + $categoryCondition = ""; + } + + + // Beiträge abfragen (nur Post‑ID und Titel der aktuellen Version) + $sqlPosts = " + SELECT + kp.id AS post_id, + pv.title AS current_title + FROM knowledgecenter_posts AS kp + LEFT JOIN knowledgecenter_post_versions AS pv + ON kp.id = pv.post_id + AND pv.language_id = $languageId + /* … Bedingungen für gültige Version und höchste version_number … */ + WHERE 1=1 + $searchCondition + $categoryCondition + GROUP BY kp.id + ORDER BY kp.modified_at DESC + "; + $resPosts = mysqli_query($GLOBALS['mysql_con'], $sqlPosts); + $count = ($resPosts) ? mysqli_num_rows($resPosts) : 0; + $postsHtml = ''; + $fmt = new IntlDateFormatter('de_DE', IntlDateFormatter::LONG, IntlDateFormatter::SHORT); + + if ($resPosts && mysqli_num_rows($resPosts) > 0) { + $postsHtml .= "
Keine Versionen gefunden.
"; + } + + $postsHtml .= "Keine Beiträge gefunden.
'; + } + + echo json_encode([ + 'success' => true, + 'html' => $postsHtml, + 'count' => (int)$count + ]); + exit; +} + +/** + * Löscht alte Mandanten-Verknüpfungen für einen Beitrag und fügt neue ein. + */ +function update_post_mandants($postId, $mandantIds) +{ + // Alte Verknüpfungen löschen + $deleteQuery = "DELETE FROM knowledgecenter_post_mandant WHERE post_id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + return "Fehler beim Löschen der Post-Mandanten-Verknüpfungen: " . mysqli_error($GLOBALS['mysql_con']); + } + // Neue Verknüpfungen einfügen + foreach ($mandantIds as $mandantId) { + $mandantId = (int) $mandantId; + $insertQuery = "INSERT INTO knowledgecenter_post_mandant (post_id, mandant_id, created_at) + VALUES ($postId, $mandantId, NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + return "Fehler beim Einfügen der Mandant-ID $mandantId: " . mysqli_error($GLOBALS['mysql_con']); + } + } + return true; +} + +/** + * Löscht alte Department-Verknüpfungen für einen Beitrag und fügt neue ein. + */ +function update_post_departments($postId, $departmentIds) +{ + $deleteQuery = "DELETE FROM knowledgecenter_post_department WHERE post_id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + return "Fehler beim Löschen der Post-Department-Verknüpfungen: " . mysqli_error($GLOBALS['mysql_con']); + } + foreach ($departmentIds as $departmentId) { + $departmentId = (int) $departmentId; + $insertQuery = "INSERT INTO knowledgecenter_post_department (post_id, department_id, created_at) + VALUES ($postId, $departmentId, NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + return "Fehler beim Einfügen der Department-ID $departmentId: " . mysqli_error($GLOBALS['mysql_con']); + } + } + return true; +} + + +/** + * Löscht alte Rollen-Verknüpfungen für einen Beitrag und fügt neue ein. + */ +function update_post_roles($postId, $roleIds) +{ + $deleteQuery = "DELETE FROM knowledgecenter_post_role WHERE post_id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + return "Fehler beim Löschen der Post-Rollen-Verknüpfungen: " . mysqli_error($GLOBALS['mysql_con']); + } + foreach ($roleIds as $roleId) { + $roleId = (int) $roleId; + $insertQuery = "INSERT INTO knowledgecenter_post_role (post_id, role_id, created_at) + VALUES ($postId, $roleId, NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + return "Fehler beim Einfügen der Rollen-ID $roleId: " . mysqli_error($GLOBALS['mysql_con']); + } + } + return true; +} + +/** + * Löscht alte Produkt-Verknüpfungen und fügt neue in product_knowledgecenter_post_link ein. + */ +function update_post_products($postId, $productIds) +{ + // Alte Verknüpfungen löschen + $deleteQuery = "DELETE FROM product_knowledgecenter_post_link WHERE post_id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + return "Fehler beim Löschen der Produkt-Verknüpfungen: " . mysqli_error($GLOBALS['mysql_con']); + } + // Neue Verknüpfungen einfügen + foreach ($productIds as $productId) { + $productId = (int) $productId; + if ($productId > 0) { + $insertQuery = " + INSERT INTO product_knowledgecenter_post_link + (product_id, post_id) + VALUES + ($productId, $postId) + "; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + return "Fehler beim Einfügen der Produkt-ID $productId: " . mysqli_error($GLOBALS['mysql_con']); + } + } + } + return true; +} + +function save_post_files() +{ + $postId = (int) ($_POST['post_id'] ?? 0); + $filesStr = trim($_POST['post_files'] ?? ''); + + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Post-ID']); + exit; + } + + if (!empty($filesStr)) { + // Zerlege den String in einzelne Pfade (getrennt durch Komma und optional Leerzeichen) + $files = preg_split('/,\s*/', $filesStr); + $errors = []; + foreach ($files as $file) { + $file = trim($file); + if (empty($file)) { + continue; + } + // Setze die Beschreibung standardmäßig auf den Dateinamen, + // der letzte Teil des Pfades (mithilfe von basename) + $description = basename($file); + + // Füge "/userdata/" vor dem Dateipfad hinzu + // Mit ltrim entfernen wir einen eventuell vorhandenen führenden Slash + $fileWithPrefix = '/userdata/' . ltrim($file, '/'); + + // Pfad sicher escapen + $escapedFile = mysqli_real_escape_string($GLOBALS['mysql_con'], $fileWithPrefix); + + $query = "INSERT INTO knowledgecenter_post_files (post_id, description, path, created_at) + VALUES ($postId, '$description', '$escapedFile', NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $query)) { + $errors[] = mysqli_error($GLOBALS['mysql_con']); + } + } + if (!empty($errors)) { + echo json_encode(['error' => implode("; ", $errors)]); + exit; + } + } + + echo json_encode(['success' => true]); + exit; +} + + +function get_post_files() +{ + $postId = (int) ($_POST['post_id'] ?? 0); + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Post-ID']); + exit; + } + $query = "SELECT id, description, path FROM knowledgecenter_post_files WHERE post_id = $postId ORDER BY created_at ASC"; + $result = mysqli_query($GLOBALS['mysql_con'], $query); + if (!$result) { + echo json_encode(['error' => 'Datenbankfehler: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + $files = []; + while ($row = mysqli_fetch_assoc($result)) { + $files[] = $row; + } + echo json_encode(['success' => true, 'files' => $files]); + exit; +} + +function delete_post_file() +{ + $fileId = (int) ($_POST['file_id'] ?? 0); + if ($fileId <= 0) { + echo json_encode(['error' => 'Ungültige Datei-ID']); + exit; + } + + $selectQuery = "SELECT path FROM knowledgecenter_post_files WHERE id = $fileId LIMIT 1"; + $selectResult = mysqli_query($GLOBALS['mysql_con'], $selectQuery); + if (!$selectResult || mysqli_num_rows($selectResult) === 0) { + echo json_encode(['error' => 'Datei-Eintrag nicht gefunden']); + exit; + } + + $row = mysqli_fetch_assoc($selectResult); + $storedPath = trim((string) ($row['path'] ?? '')); + + $userdataRoot = realpath($_SERVER['DOCUMENT_ROOT'] . '/userdata'); + if ($userdataRoot === false) { + echo json_encode(['error' => 'userdata-Verzeichnis nicht gefunden']); + exit; + } + + $relativePath = ltrim(str_replace('\\', '/', $storedPath), '/'); + if (strpos($relativePath, 'userdata/') === 0) { + $relativePath = substr($relativePath, strlen('userdata/')); + } + + if ($relativePath === '' || strpos($relativePath, '..') !== false) { + echo json_encode(['error' => 'Ungültiger Dateipfad']); + exit; + } + + $absolutePath = $userdataRoot . '/' . $relativePath; + $absoluteDir = realpath(dirname($absolutePath)); + if ($absoluteDir === false || strpos($absoluteDir, $userdataRoot) !== 0) { + echo json_encode(['error' => 'Ungültiges Zielverzeichnis']); + exit; + } + + if (is_file($absolutePath)) { + if (!@unlink($absolutePath)) { + echo json_encode(['error' => 'Datei konnte nicht vom Server gelöscht werden']); + exit; + } + } + + $deleteQuery = "DELETE FROM knowledgecenter_post_files WHERE id = $fileId"; + if (mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['error' => 'Fehler beim Löschen des Datenbankeintrags: ' . mysqli_error($GLOBALS['mysql_con'])]); + } + exit; +} + +/** + * Aktualisiert die Kontakte (Ansprechpartner) eines Beitrags in der Tabelle knowledgecenter_post_contacts. + */ +function update_post_contacts() +{ + // Hole die Post-ID und dekodiere den JSON-String für Ansprechpartner + $postId = (int) ($_POST['post_id'] ?? 0); + $contactIds = json_decode($_POST['contacts'] ?? '[]', true) ?: []; + + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Beitrags-ID']); + exit; + } + + // Lösche alle bestehenden Kontakte für diesen Beitrag + $deleteQuery = "DELETE FROM knowledgecenter_post_contacts WHERE post_id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { + echo json_encode(['error' => 'Fehler beim Löschen bestehender Kontakte: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + $errors = []; + // Füge jeden angegebenen Ansprechpartner ein + foreach ($contactIds as $contactId) { + $contactId = (int) $contactId; + if ($contactId > 0) { + $insertQuery = "INSERT INTO knowledgecenter_post_contacts (post_id, main_contact_id, created_at) + VALUES ($postId, $contactId, NOW())"; + if (!mysqli_query($GLOBALS['mysql_con'], $insertQuery)) { + $errors[] = "Fehler beim Einfügen der Kontakt-ID $contactId: " . mysqli_error($GLOBALS['mysql_con']); + } + } + } + + if (!empty($errors)) { + echo json_encode(['error' => implode("; ", $errors)]); + exit; + } + + echo json_encode(['success' => true]); + exit; +} + +/** + * Gibt als HTML-String Visitenkarten der zugeordneten Kontakte zurück. + */ +function get_contact_cards() +{ + $postId = (int) ($_POST['post_id'] ?? 0); + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Beitrags-ID']); + exit; + } + + // Erweiterte Abfrage: Lade zusätzlich email und image + $query = "SELECT mc.id, mc.name, mc.email, mc.image + FROM knowledgecenter_post_contacts AS pc + JOIN main_contact AS mc ON pc.main_contact_id = mc.id + WHERE pc.post_id = $postId"; + $result = mysqli_query($GLOBALS['mysql_con'], $query); + + if (!$result) { + echo json_encode(['error' => 'Datenbankfehler: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + $html = ''; + // Erstelle die HTML-Struktur für jeden Kontakt + while ($row = mysqli_fetch_assoc($result)) { + // Link zum Profil – passe den Pfad an, falls nötig + $profileLink = "/intranet/de/profil/?contact=" . $row['id']; + // Falls kein Bild vorhanden, verwende einen Platzhalter + $image = !empty($row['image']) ? '/userdata/intranet/contact/' . $row['image'] : '/userdata/intranet/contact/default_avatar.webp'; + $name = htmlspecialchars($row['name']); + $email = htmlspecialchars($row['email']); + + $html .= ''; + $html .= '' . $email . '
'; + $html .= 'Keine passenden Beiträge gefunden.
'; + } + + echo json_encode([ + 'success' => true, + 'html' => $html + ]); + exit; +} + + +/** + * Aktualisiert in der Tabelle main_certificate für ein gegebenes Zertifikat den zugeordneten post_id-Wert. + * Falls kein Zertifikat ausgewählt wurde (certificate_id leer oder 0), wird die Zuordnung entfernt, + * indem alle Zertifikate, die aktuell diesen Beitrag zugeordnet haben, auf NULL gesetzt werden. + * + * Erwartete POST-Parameter: + * - post_id: Die ID des Beitrags, der dem Zertifikat zugewiesen werden soll. + * - certificate_id: Die ID des Zertifikats, dessen post_id gesetzt werden soll oder leer/0, um die Zuordnung zu entfernen. + * + * @return void JSON-Antwort mit Erfolgsmeldung oder Fehler + */ +function update_post_certificate() +{ + // Beitrags-ID extrahieren und in Integer umwandeln + $postId = (int) ($_POST['post_id'] ?? 0); + + // certificate_id auswerten: Falls ein leerer Wert oder 0, gilt dies als "kein Zertifikat ausgewählt" + $certificateId = isset($_POST['certificate_id']) && $_POST['certificate_id'] !== "" ? (int) ($_POST['certificate_id']) : 0; + + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Beitrags-ID']); + exit; + } + + // Falls eine gültige certificate_id übergeben wurde (größer als 0) + if ($certificateId > 0) { + // Dem Zertifikat wird der Beitrag zugeordnet + $query = "UPDATE main_certificate SET post_id = $postId WHERE id = $certificateId"; + if (mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['error' => 'Fehler beim Aktualisieren des Zertifikats: ' . mysqli_error($GLOBALS['mysql_con'])]); + } + } else { + // Kein Zertifikat ausgewählt – entferne die Zuordnung zu diesem Beitrag + $query = "UPDATE main_certificate SET post_id = NULL WHERE post_id = $postId"; + if (mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['success' => true, 'message' => 'Zertifikats-Zuordnung entfernt']); + } else { + echo json_encode(['error' => 'Fehler beim Entfernen der Zertifikats-Zuordnung: ' . mysqli_error($GLOBALS['mysql_con'])]); + } + } + exit; +} + +function update_post_pdf_inline() +{ + $postId = (int) ($_POST['post_id'] ?? 0); + // Wenn der Parameter pdf_inline im POST gesetzt ist, wird der Wert verwendet, ansonsten 0 + $pdfInline = isset($_POST['pdf_inline']) ? (int) $_POST['pdf_inline'] : 0; + + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Beitrags-ID']); + exit; + } + + $query = "UPDATE knowledgecenter_posts SET preview_inline = $pdfInline WHERE id = $postId"; + if (mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['error' => 'Fehler beim Aktualisieren der PDF Inline Vorschau: ' . mysqli_error($GLOBALS['mysql_con'])]); + } + exit; +} + +function update_post_colors() +{ + $postId = (int) ($_POST['post_id'] ?? 0); + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Beitrags-ID']); + exit; + } + + $backgroundColor = normalize_hex_color($_POST['background_color'] ?? '#FFFFFF', '#FFFFFF'); + $textColor = normalize_hex_color($_POST['text_color'] ?? '#000000', '#000000'); + + $query = "UPDATE knowledgecenter_posts + SET background_color = '$backgroundColor', + text_color = '$textColor', + modified_at = NOW() + WHERE id = $postId"; + if (mysqli_query($GLOBALS['mysql_con'], $query)) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['error' => 'Fehler beim Aktualisieren der Post-Farben: ' . mysqli_error($GLOBALS['mysql_con'])]); + } + exit; +} + +/** + * Löscht einen kompletten Beitrag (inkl. all seiner Versionen). + */ +function delete_post() +{ + $postId = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0; + if ($postId <= 0) { + echo json_encode(['error' => 'Ungültige Beitrags-ID']); + exit; + } + + // 2) Haupt-Datensatz löschen + $delPostQ = "DELETE FROM knowledgecenter_posts WHERE id = $postId"; + if (!mysqli_query($GLOBALS['mysql_con'], $delPostQ)) { + echo json_encode(['error' => 'Fehler beim Löschen des Beitrags: ' . mysqli_error($GLOBALS['mysql_con'])]); + exit; + } + + $deleteFolderResult = delete_post_gallery_dir($postId); + if (!$deleteFolderResult['success']) { + echo json_encode(['error' => 'Beitrag gelöscht, aber Ordner konnte nicht gelöscht werden: ' . $deleteFolderResult['error']]); + exit; + } + + echo json_encode(['success' => true]); + exit; +} + +?> + + \ No newline at end of file diff --git a/module/knowledgecenter_update/Ajax/endpoints/upload_inline_image.php b/module/knowledgecenter_update/Ajax/endpoints/upload_inline_image.php new file mode 100644 index 0000000..e6ab167 --- /dev/null +++ b/module/knowledgecenter_update/Ajax/endpoints/upload_inline_image.php @@ -0,0 +1,151 @@ + 0 (knowledgecenter_posts.id) + * + * Antwort (JSON): + * { "success": true, "url": "/userdata/knowledgecenter_update/posts/" or "input_[]"
+ $uploadField = $input_key;
+
+ // Normalize $_FILES to array of file hashes
+ $files = [];
+ if (isset($_FILES[$uploadField])) {
+ if (is_array($_FILES[$uploadField]['name'])) {
+ $cnt = count($_FILES[$uploadField]['name']);
+ for ($i = 0; $i < $cnt; $i++) {
+ $files[] = [
+ 'name' => $_FILES[$uploadField]['name'][$i],
+ 'type' => $_FILES[$uploadField]['type'][$i],
+ 'tmp_name' => $_FILES[$uploadField]['tmp_name'][$i],
+ 'error' => $_FILES[$uploadField]['error'][$i],
+ 'size' => $_FILES[$uploadField]['size'][$i],
+ ];
+ }
+ } else {
+ $files[] = $_FILES[$uploadField];
+ }
+ }
+
+ // Mandatory check for files
+ if ((empty($files) || ($files[0]['name'] ?? '') === '') && !empty($field["mandatory"])) {
+ $error = TRUE;
+ $input_value = '';
+ $linked_value = '';
+ break;
+ }
+ if (empty($files) || ($files[0]['name'] ?? '') === '') {
+ // No files and not mandatory
+ $input_value = '';
+ $linked_value = '';
+ break;
+ }
+
+ // Target directory
+ $uploadDir = rtrim(dirname(dirname(dirname(__DIR__))), "/") . '/userdata/mail_attachments/';
+ if (!is_dir($uploadDir)) {
+ @mkdir($uploadDir, 0775, true);
+ }
+
+ // Extension policies
+ $allowed = ['pdf', 'png', 'jpg', 'jpeg', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'zip', 'rar', '7z', 'txt', 'csv', 'mp4', 'mov', 'avi', 'mkv'];
+ $blocked = ['php', 'phtml', 'phar', 'cgi', 'pl', 'exe', 'sh', 'bat', 'cmd', 'com', 'js', 'msi'];
+
+ $uploadedNames = [];
+ foreach ($files as $f) {
+ // Skip unsuccessful items
+ if (!isset($f['error']) || $f['error'] !== UPLOAD_ERR_OK || ($f['name'] ?? '') === '') {
+ continue;
+ }
+
+ $ext = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION));
+ if (in_array($ext, $blocked, true) || ($ext !== '' && !in_array($ext, $allowed, true))) {
+ $filetypeError = TRUE;
+ $error = TRUE;
+ continue;
+ }
+
+ // Optional: size limit (e.g., 25MB)
+ // if (($f['size'] ?? 0) > 25 * 1024 * 1024) { $filetypeError = TRUE; $error = TRUE; continue; }
+
+ // Sanitize filename & ensure uniqueness
+ $base = preg_replace('/[^A-Za-z0-9._-]+/', '_', $f['name']);
+ $dest = $uploadDir . $base;
+ $iUniq = 1;
+ $extSuffix = $ext ? ('.' . $ext) : '';
+ while (file_exists($dest)) {
+ $stem = pathinfo($base, PATHINFO_FILENAME);
+ $dest = $uploadDir . $stem . "_" . $iUniq . $extSuffix;
+ $iUniq++;
+ }
+
+ if (@move_uploaded_file($f['tmp_name'], $dest)) {
+ $attachements[] = basename($dest);
+ $uploadedNames[] = basename($dest);
+ } else {
+ $filetypeError = TRUE;
+ $error = TRUE;
+ }
+ }
+
+ // Mark field value & show list in message
+ $input_value = $uploadedNames ? 'FILE_ATTACHMENT' : '';
+ $linked_value = '';
+ if (!empty($uploadedNames)) {
+ $message .= "" . $field["name"] . ": " . implode(', ', $uploadedNames) . "\n";
+ }
+ break;
+
+ case 9: // Checkbox group (multi-select) — uses field id in POST
+ $vals = $_POST["input_" . $field_id] ?? null;
+ if (is_array($vals) && !empty($vals)) {
+ $lines = implode('
', array_map('trim', $vals));
+ $message .= "" . $field["name"] . ":
" . $lines . "\n";
+ $input_value = implode(', ', $vals);
+ } else {
+ $input_value = '';
+ }
+ $linked_value = '';
+
+ // Linked inputs per chosen option (multistep)
+ if (!empty($contactFormData['is_multistep'])) {
+ $qOpt = 'SELECT * FROM contactform_line_option WHERE line_id = ' . $field_id;
+ $resOpt = @mysqli_query($GLOBALS['mysql_con'], $qOpt);
+ if ($resOpt) {
+ while ($opt = @mysqli_fetch_array($resOpt)) {
+ if (!empty($opt['linked_field'])) {
+ $lk = "inputlinked_" . $field_id . '_' . (int) $opt['id'];
+ if (isset($_POST[$lk]) && $_POST[$lk] !== '') {
+ $message .= $_POST[$lk] . "\n";
+ $linked_value = $_POST[$lk];
+ }
+ }
+ }
+ }
+ }
+ break;
+
+ case 10: // Single choice (radio/select) — uses field id in POST
+ $chosen = $_POST["input_" . $field_id] ?? '';
+ if ($chosen !== '') {
+ $message .= "" . $field["name"] . ": " . $chosen . "\n";
+ }
+ $input_value = $chosen;
+ $linked_value = '';
+
+ // Linked input for selected option (multistep/survey)
+ if (!empty($contactFormData['is_multistep']) || !empty($contactFormData['is_survey'])) {
+ $qOpt = 'SELECT * FROM contactform_line_option WHERE line_id = ' . $field_id;
+ $resOpt = @mysqli_query($GLOBALS['mysql_con'], $qOpt);
+ if ($resOpt) {
+ while ($opt = @mysqli_fetch_array($resOpt)) {
+ if ($opt['option_string'] === $chosen && !empty($opt['linked_field'])) {
+ $lk = "inputlinked_" . $field_id . '_' . (int) $opt['id'];
+ if (isset($_POST[$lk]) && $_POST[$lk] !== '') {
+ $message .= $_POST[$lk] . "\n";
+ $linked_value = $_POST[$lk];
+ }
+ break;
+ }
+ }
+ }
+ }
+ break;
+
+ case 11: // Date
+ $raw = $_POST[$input_key] ?? '';
+ $formatted = '';
+ if ($raw !== '') {
+ try {
+ $dt = new DateTime($raw);
+ $formatted = $dt->format('d.m.Y');
+ } catch (Throwable $e) {
+ $formatted = $raw; // fallback
+ }
+ }
+ $message .= "" . $field["name"] . ": " . $formatted . "\n";
+ $input_value = $raw; // store raw in DB
+ $linked_value = '';
+ break;
+
+ default:
+ // Unknown type — store raw if present
+ $val = $_POST[$input_key] ?? '';
+ if ($val !== '') {
+ $message .= "" . $field["name"] . ": " . $val . "\n";
+ }
+ $input_value = $val;
+ $linked_value = '';
+ break;
+ }
+
+ // Persist a normalized snapshot of the user's input for each field
+ $submission_data[] = [
+ 'field_id' => $field_id,
+ 'value' => is_array($input_value) ? implode(', ', $input_value) : (string) $input_value,
+ 'linked_value' => is_array($linked_value) ? implode(', ', $linked_value) : (string) $linked_value,
+ ];
+ }
+ }
+ if (($_POST["input_captcha1"] <> "") | ((time() - $_POST["input_captcha2"]) < 5)) {
+ $error = TRUE;
+ $requestmassage .= $GLOBALS['tc']['contactform_spam'];
+ }
+
+ if ($captchError == TRUE) {
+ echo $ajaxHelper . "Bitte verwenden Sie das Captcha
" . $ajaxHelper;
+ } else {
+ echo $ajaxHelper . "" . $ajaxHelper;
+ }
+ if ($filetypeError === true) {
+ echo $ajaxHelper . "Upload von PHP Dateien nicht erlaubt" . $ajaxHelper;
+ }
+
+
+ //DUP breadcrumb upload mehrere Dateien update
+ if (!$error) {
+ //fügt Daten ins Array
+ foreach ($attachements as $attachement_line) {
+ $file[] = "/userdata/mail_attachments/" . $attachement_line;
+ }
+
+ // AUTOMATISCHE ANTWORT
+ if ($contactform_header["auto_answer"] != "" && $GLOBALS['main_contact']['email'] != "") {
+ if (
+ mail_create(
+ $contactform_header["subject"],
+ $contactform_header["auto_answer"],
+ $contactform_header["sender_email"],
+ $GLOBALS['main_contact']['email'],
+ $contactform_header["sender_name"],
+ $GLOBALS['main_contact']['email'],
+ FALSE,
+ "",
+ "",
+ "",
+ 0,
+ "",
+ "",
+ $contact_form_id
+ )
+ ) {
+
+ mail_send();
+ clearstatcache();
+ $success = true;
+ $requestmassage .= $GLOBALS['tc']['contactform_success'];
+ foreach ($_POST as $key => $value) {
+ unset($_POST[$key]);
+ }
+ } else {
+ $error = TRUE;
+ $requestmassage .= $translation->get('not_send1');
+ }
+ }
+ if ($sitepart_id == 1) {
+ $mandant_id = $GLOBALS["main_contact"]['master_mandant_id'];
+ $contact_id = $GLOBALS["main_contact"]['id'];
+ // $query = "SELECT * FROM main_contact_link LEFT JOIN main_collection as mc on mc.id = plg.portal_group_id WHERE plg.portal_login_id ='".$login_id."' AND plg.portal_group_id = '".$group_id."' AND plg.email != ''";
+ // $result = @mysqli_query($GLOBALS['mysql_con'], $query);
+ // $res = mysqli_fetch_array($result);
+
+ $contactform_header2["sender_email"] = $contactform_header["sender_email"];
+ $contactform_header2["sender_name"] = $contactform_header["sender_name"];
+ $contactform_header["sender_email"] = $GLOBALS["main_contact"]['email'];
+ $contactform_header["sender_name"] = $GLOBALS["main_contact"]['name'];
+ $contactform_header["subject"] = "Abwesenheit | {$GLOBALS["main_contact"]['name']}";
+ }
+ if ($sitepart_id == 2) {
+ $contactform_header["sender_email"] = $GLOBALS["main_contact"]['email'];
+ $contactform_header["sender_name"] = $GLOBALS["main_contact"]['name'];
+
+ $query = 'SELECT * from main_contact WHERE id = ' . $_REQUEST['input_user'] . ' LIMIT 1';
+ $result = @mysqli_query($GLOBALS['mysql_con'], $query);
+ if (@mysqli_num_rows($result) == 1) {
+ $user = @mysqli_fetch_array($result);
+ if ($user['email'] != NULL && $user['email'] != '') {
+ $contactform_header["recipient_name"] = $user['name'];
+ $contactform_header["recipient_email"] = $user['email'];
+ }
+ }
+ create_notification($_REQUEST['input_user'], $message);
+ }
+
+ // Sende E-Mail an Firmen-Email
+ if ($sitepart_id == 1) {
+ $query = "SELECT * FROM main_contact_link WHERE main_contact_id = '" . $GLOBALS["main_contact"]['id'] . "' AND sorting = 1";
+ $result = @mysqli_query($GLOBALS['mysql_con'], $query);
+
+ $input_arbeitsunfaehigkeitsbescheinigung = $_POST['input_arbeitsunfaehigkeitsbescheinigung'];
+ $input_date2 = new DateTime($_POST['input_date2']);
+ $input_date = new DateTime($_POST['input_date']);
+ $input_besprechung = $_POST['input_besprechung'];
+ $input_ansprechpartner = $_POST['input_ansprechpartner'];
+
+ while ($res = mysqli_fetch_array($result)) {
+ $query = "SELECT * FROM main_mandant WHERE id = " . $res['main_mandant_id'];
+ $result = @mysqli_query($GLOBALS['mysql_con'], $query);
+ $res_mandant = @mysqli_fetch_array($result);
+
+ $contactform_header2['recipient_email'] = $res_mandant['mail_team'];
+ $contactform_header2['recipient_name'] = $res_mandant['description'];
+
+ $message2 = "Liebes Team, \n\n";
+
+ if (isset($_POST['serial_code']) && $_POST['serial_code'] != "") {
+ $randomString = $_POST['serial_code'];
+ } else {
+ $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ $charactersLength = strlen($characters);
+ $randomString = '';
+
+ for ($i = 0; $i < 20; $i++) {
+ $randomString .= $characters[rand(0, $charactersLength - 1)];
+ }
+ }
+
+ if (isset($input_arbeitsunfaehigkeitsbescheinigung) && $input_date2 != "") {
+ $message2 .= $GLOBALS["main_contact"]['name'] . " ist bis einschließlich den " . $input_date2->format('d.m.Y') . " abwesend.\n";
+ $check_query = "SELECT events_type.id, events_type.sick_note FROM events_type WHERE events_type.sick_note = 1";
+ $check_result = mysqli_query($GLOBALS['mysql_con'], $check_query);
+ $check_row = mysqli_fetch_assoc($check_result);
+ $event_type_id = $check_row["id"];
+ $created_date = date(format: "Y-m-d H:i:s");
+ $modifiedDate = date(format: "Y-m-d H:i:s");
+ $date = clone $input_date;
+ while ($date <= $input_date2) {
+ $delete_query = "DELETE FROM events WHERE date_from = '" . $date->format('Y-m-d') . "' AND main_contact_id = '" . $GLOBALS['main_contact']['id'] . "'";
+ @mysqli_query($GLOBALS['mysql_con'], $delete_query);
+ $insert_query = "INSERT INTO events (
+ date_from,
+ date_to,
+ main_contact_id,
+ event_type_id,
+ serial_code,
+ created_date,
+ modified_date,
+ all_day,
+ active
+ ) VALUES (
+ '" . $date->format('Y-m-d') . "',
+ '" . $date->format('Y-m-d') . "',
+ '" . $GLOBALS['main_contact']['id'] . "',
+ '" . $event_type_id . "',
+ '" . $randomString . "',
+ '" . $created_date . "',
+ '" . $modifiedDate . "',
+ 1,
+ 1
+ )";
+ @mysqli_query($GLOBALS['mysql_con'], $insert_query);
+ $date->modify('+1 day');
+ }
+ } else {
+ $message2 .= $GLOBALS["main_contact"]['name'] . " hat sich für den " . $input_date->format('d.m.Y') . " abgemeldet.\n";
+ $delete_query = "DELETE FROM events WHERE date_from = '" . $input_date->format('Y-m-d') . "' AND main_contact_id = '" . $GLOBALS['main_contact']['id'] . "'";
+ @mysqli_query($GLOBALS['mysql_con'], $delete_query);
+ $check_query = "SELECT events_type.id, events_type.sick_note FROM events_type WHERE events_type.sick_note = 1";
+ $check_result = mysqli_query($GLOBALS['mysql_con'], $check_query);
+ $check_row = mysqli_fetch_assoc($check_result);
+ $event_type_id = $check_row["id"];
+ $created_date = date(format: "Y-m-d H:i:s");
+ $modifiedDate = date(format: "Y-m-d H:i:s");
+ $insert_query = "INSERT INTO events (
+ date_from,
+ date_to,
+ main_contact_id,
+ event_type_id,
+ serial_code,
+ created_date,
+ modified_date,
+ all_day,
+ active
+ ) VALUES (
+ '" . $date->format('Y-m-d') . "',
+ '" . $date->format('Y-m-d') . "',
+ '" . $GLOBALS['main_contact']['id'] . "',
+ '" . $event_type_id . "',
+ '" . $randomString . "',
+ '" . $created_date . "',
+ '" . $modifiedDate . "',
+ 1,
+ 1
+ )";
+ @mysqli_query($GLOBALS['mysql_con'], $insert_query);
+ }
+
+ if (isset($input_besprechung) && $input_ansprechpartner != "") {
+ $message2 .= "Die telefonische Besprechung zur Übergabe von Aufgaben und Terminen erfolgte mit " . $input_ansprechpartner . ".\n";
+ }
+
+
+ if (
+ mail_create(
+ $contactform_header["subject"],
+ $message2,
+ $contactform_header2["sender_email"],
+ $contactform_header2["recipient_email"],
+ $contactform_header2["sender_name"],
+ $contactform_header2["recipient_name"],
+ FALSE,
+ $file,
+ "",
+ "",
+ 0,
+ "",
+ ""
+ )
+ ) {
+ mail_send();
+ unset($file);
+ clearstatcache();
+ $success = true;
+ $requestmassage .= $GLOBALS['tc']['contactform_success'];
+ foreach ($_POST as $key => $value) {
+ unset($_POST[$key]);
+ }
+ } else {
+ $error = TRUE;
+ $requestmassage .= $translation->get('not_send2');
+ }
+ }
+ }
+
+ if ($GLOBAL["ws_bestell_result"] == true) {
+ $query = "SELECT main_contact.email FROM main_contact JOIN main_contact_link ON main_contact.id = main_contact_link.main_contact_id AND main_role_id = '3' AND main_mandant_id = " . $GLOBALS["main_contact"]["current_mandant_id"];
+ $result = @mysqli_query($GLOBALS['mysql_con'], $query);
+ while ($email = @mysqli_fetch_array($result)) {
+ $contactform_header["recipient_email"] .= "; " . $email["email"];
+ }
+
+ }
+
+ $bccEmails = [];
+
+ $sender_id = (int) $GLOBALS['main_contact']['id'];
+ $sender_mandant_id = (int) $GLOBALS['main_contact']['current_mandant_id'];
+
+ $responsibleString = $contactform_header['responsible_person'] ?? '';
+ $responsibleIds = array_filter(array_map('intval', explode(',', $responsibleString)));
+
+ if (!empty($responsibleIds) && $sender_mandant_id) {
+ foreach ($responsibleIds as $pid) {
+ $pid = (int) $pid;
+
+ $check = mysqli_query($GLOBALS['mysql_con'], "
+ SELECT 1 FROM main_contact_link
+ WHERE main_contact_id = $pid
+ AND main_mandant_id = $sender_mandant_id
+ AND active_d = 1
+ LIMIT 1
+ ");
+
+ if (mysqli_num_rows($check)) {
+ $emailRes = mysqli_query($GLOBALS['mysql_con'], "
+ SELECT email FROM main_contact
+ WHERE id = $pid AND email != ''
+ LIMIT 1
+ ");
+ if ($emailRow = mysqli_fetch_assoc($emailRes)) {
+ $bccEmails[] = $emailRow['email'];
+ }
+ }
+ }
+ }
+
+// Add mandant leaders to BCC if mandant_leader_in_cc is enabled
+ if ((int)($contactform_header["mandant_leader_in_cc"] ?? 0) === 1 && $sender_mandant_id > 0) {
+ $leadersRes = mysqli_query($GLOBALS['mysql_con'],
+ "SELECT mc.email FROM main_mandant_leader mml
+ INNER JOIN main_contact mc ON mc.id = mml.main_contact_id
+ WHERE mml.main_mandant_id = " . $sender_mandant_id . "
+ AND mc.active = 1 AND mc.email != ''
+ ORDER BY mml.sorting ASC"
+ );
+ while ($leader = mysqli_fetch_assoc($leadersRes)) {
+ $bccEmails[] = $leader['email'];
+ }
+ }
+
+ $bccString = implode(',', array_unique($bccEmails));
+
+ $formName = $contactform_header['description'] ?? '—';
+
+ $applicantName = $GLOBALS['main_contact']['name'] ?? 'Unbekannt';
+
+ $mandantName = '';
+ $mid = (int) $GLOBALS['main_contact']['current_mandant_id'];
+ if ($mid) {
+ $row = mysqli_fetch_assoc(
+ mysqli_query(
+ $GLOBALS['mysql_con'],
+ "SELECT description FROM main_mandant WHERE id = $mid LIMIT 1"
+ )
+ );
+ $mandantName = $row['description'] ?: 'ID ' . $mid;
+ }
+
+ $currentDt = date('d.m.Y H:i');
+
+ $formName = htmlspecialchars($contactform_header['description'] ?? '—', ENT_QUOTES);
+
+ $applicant = htmlspecialchars($GLOBALS['main_contact']['name'] ?? 'Unbekannt', ENT_QUOTES);
+
+ $mandant = '';
+ $mid = (int) $GLOBALS['main_contact']['current_mandant_id'];
+ if ($mid) {
+ $row = mysqli_fetch_assoc(
+ mysqli_query(
+ $GLOBALS['mysql_con'],
+ "SELECT description FROM main_mandant WHERE id = $mid LIMIT 1"
+ )
+ );
+ $mandant = htmlspecialchars($row['description'] ?: 'ID ' . $mid, ENT_QUOTES);
+ }
+
+ $currDt = date('d.m.Y H:i');
+
+ $metaHeader = "
+ Neue Formular-Anfrage
+ Formular: $formName
+ Erstellt von: $applicant
+ Mandant: $mandant
+ Datum: $currDt
+";
+
+ $message = $metaHeader . trim($message);
+ // fa wenn betreff ist leer
+// $contactform_header["subject"] = trim((string)$contactform_header["subject"]);
+// if ($contactform_header["subject"] === '') {
+// $contactform_header["subject"] = 'Kontaktformular';
+// }
+
+ if (
+ mail_create(
+ $contactform_header["subject"],
+ $message,
+ $contactform_header["sender_email"],
+ $contactform_header["recipient_email"],
+ $contactform_header["sender_name"],
+ $contactform_header["recipient_name"],
+ FALSE,
+ $file,
+ $bccString,
+ "",
+ 0,
+ "",
+ $spid
+ )
+ ) {
+ mail_send();
+ unset($file);
+ clearstatcache();
+ $success = true;
+ $requestmassage .= $GLOBALS['tc']['contactform_success'];
+ foreach ($_POST as $key => $value) {
+ unset($_POST[$key]);
+ }
+
+ } else {
+ $error = TRUE;
+ $requestmassage .= $translation->get('not_send3');
+ }
+ } else {
+ $error = TRUE;
+ $requestmassage .= $GLOBALS['tc']['mandatory_fields_error'];
+ if ($filetypeError === TRUE) {
+ $requestmassage .= $GLOBALS['tc']['contactform_error_php'];
+ }
+ }
+
+ $mail_query = mysqli_query($GLOBALS['mysql_con'], "SELECT id FROM main_mail_log ORDER BY id DESC LIMIT 1");
+ $mail_row = mysqli_fetch_assoc($mail_query);
+ $mail_id = $mail_row['id'] ?? 0;
+
+
+ foreach ($submission_data as $entry) {
+ $query_entry = "INSERT INTO contactform_entries (mail_id, line_id, value)
+ VALUES (
+ $mail_id,
+ " . intval($entry['field_id']) . ",
+ '" . mysqli_real_escape_string($GLOBALS['mysql_con'], $entry['value']) . "'
+ )";
+ mysqli_query($GLOBALS['mysql_con'], $query_entry);
+ }
+
+
+ if ($error || $success) {
+ if ($success) {
+ get_requestbox($requestmassage, "", "success");
+ } else {
+ get_requestbox($requestmassage, "");
+ }
+ }
+}
+
+// Helper-Funktion zur Berechtigungsprüfung
+function require_permission($permissionCode)
+{
+ // Hier nehmen wir an, dass $GLOBALS["main_contact"] korrekt gesetzt ist
+ if (
+ get_permission_state(
+ $permissionCode,
+ $GLOBALS['main_contact']['master_mandant_id'],
+ $GLOBALS['main_contact']['id']
+ ) != 1
+ ) {
+ echo "Sie haben nicht die notwendigen Berechtigungen, um diese Seite zu sehen.";
+ exit;
+ }
+}
+
+// Switch-Case für die verschiedenen Aktionen
+$action = $_GET["action"] ?? "List";
+switch ($action) {
+ case "Installation":
+ require_permission('r-wiki');
+ require_once(__DIR__ . '/../Installation/Installation.php');
+ break;
+ case "Categories":
+ if (isset($_GET['detail'])) {
+ require_permission('r-wiki');
+ require_once(__DIR__ . '/../Views/categories_cardform.php');
+ } else {
+ require_permission('r-wiki');
+ require_once(__DIR__ . '/../Views/categories_listform.php');
+ }
+ break;
+ case "PostList":
+ require_permission('r-wiki');
+ require_once(__DIR__ . '/../Views/post_listform.php');
+ break;
+ case "Post":
+ require_once(__DIR__ . '/../Views/post_cardform.php');
+ break;
+ case "Import":
+ require_permission('r-wiki');
+ require_once(__DIR__ . '/../Import/Import.php');
+ break;
+ case "NewPost":
+ require_permission('r-wiki');
+ $_GET['new'] = 1;
+ require_once(__DIR__ . '/../Views/post_cardform.php');
+ break;
+ case "List":
+ default:
+ require_once(__DIR__ . '/../Views/categories_posts_listform.php');
+ break;
+}
+
+require_once(__DIR__ . '/../Script/Script.php');
+require_once(__DIR__ . '/../CSS/CSS.php');
+?>
\ No newline at end of file
diff --git a/module/knowledgecenter_update/Dependencies/Dependencies.php b/module/knowledgecenter_update/Dependencies/Dependencies.php
new file mode 100644
index 0000000..f42bbed
--- /dev/null
+++ b/module/knowledgecenter_update/Dependencies/Dependencies.php
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/module/knowledgecenter_update/Import/Import.php b/module/knowledgecenter_update/Import/Import.php
new file mode 100644
index 0000000..76c73d3
--- /dev/null
+++ b/module/knowledgecenter_update/Import/Import.php
@@ -0,0 +1,264 @@
+
+Import-Dashboard
+
+Tabellen leeren (Reset) ✕
+
+Kategorien importieren ↗
+
+Beiträge importieren ↗
+
+
+$table wurde erfolgreich geleert.
";
+ } else {
+ echo "Fehler beim Leeren der Tabelle $table: " . mysqli_error($GLOBALS['mysql_con']) . "
";
+ }
+ }
+
+ // Foreign-Key-Prüfungen wieder aktivieren
+ mysqli_query($GLOBALS['mysql_con'], "SET FOREIGN_KEY_CHECKS = 1");
+}
+
+/**
+ * Rekursive Funktion zum Importieren von Kategorien.
+ * Für jeden Unterordner wird – sofern noch nicht vorhanden – eine Kategorie in
+ * knowledgecenter_categories_update und eine Übersetzung in knowledgecenter_category_translations (language_id = 1) angelegt.
+ * Bei Unterordnern wird zudem in knowledgecenter_category_links eine Parent-Child-Verknüpfung erstellt.
+ *
+ * @param string $folder Absoluter Pfad des aktuellen Ordners.
+ * @param int|null $parentCategoryId Falls vorhanden, die Kategorie-ID des Elternordners.
+ */
+function import_categories($folder, $parentCategoryId = null)
+{
+ if (!is_dir($folder)) {
+ echo "Ordner '$folder' existiert nicht.
";
+ return;
+ }
+
+ $folderName = basename($folder);
+ $folderNameEscaped = mysqli_real_escape_string($GLOBALS['mysql_con'], $folderName);
+
+ // Prüfen, ob eine Kategorie mit diesem Titel (language_id = 1) bereits existiert
+ $query = "SELECT c.id
+ FROM knowledgecenter_categories_update c
+ JOIN knowledgecenter_category_translations t ON c.id = t.category_id
+ WHERE t.language_id = 1 AND t.title = '$folderNameEscaped' LIMIT 1";
+ $result = mysqli_query($GLOBALS['mysql_con'], $query);
+ if ($result && mysqli_num_rows($result) > 0) {
+ $row = mysqli_fetch_assoc($result);
+ $categoryId = $row['id'];
+ echo "Kategorie '$folderName' existiert bereits (ID: $categoryId).
";
+ } else {
+ $sql_category = "INSERT INTO knowledgecenter_categories_update (sorting, state, modified_at, modified_by, created_at)
+ VALUES (0, 1, NOW(), 'import', NOW())";
+ if (!mysqli_query($GLOBALS['mysql_con'], $sql_category)) {
+ echo "Fehler beim Erstellen der Kategorie '$folderName': " . mysqli_error($GLOBALS['mysql_con']) . "
";
+ return;
+ }
+ $categoryId = mysqli_insert_id($GLOBALS['mysql_con']);
+
+ $sql_trans = "INSERT INTO knowledgecenter_category_translations (category_id, language_id, title, created_at)
+ VALUES ($categoryId, 1, '$folderNameEscaped', NOW())";
+ if (!mysqli_query($GLOBALS['mysql_con'], $sql_trans)) {
+ echo "Fehler beim Einfügen der Kategorie-Übersetzung für '$folderName': " . mysqli_error($GLOBALS['mysql_con']) . "
";
+ }
+ echo "Kategorie '$folderName' wurde erstellt (ID: $categoryId).
";
+ }
+
+ if ($parentCategoryId !== null) {
+ $query_link = "SELECT id FROM knowledgecenter_category_links WHERE parent_category_id = $parentCategoryId AND child_category_id = $categoryId LIMIT 1";
+ $result_link = mysqli_query($GLOBALS['mysql_con'], $query_link);
+ if ($result_link && mysqli_num_rows($result_link) == 0) {
+ $sql_link = "INSERT INTO knowledgecenter_category_links (parent_category_id, child_category_id, sorting, created_at)
+ VALUES ($parentCategoryId, $categoryId, 0, NOW())";
+ if (!mysqli_query($GLOBALS['mysql_con'], $sql_link)) {
+ echo "Fehler beim Verknüpfen der Kategorie '$folderName' mit Eltern ($parentCategoryId): " . mysqli_error($GLOBALS['mysql_con']) . "
";
+ }
+ }
+ }
+
+ // Unterordner verarbeiten
+ $subDirs = glob($folder . "/*", GLOB_ONLYDIR);
+ if ($subDirs !== false) {
+ foreach ($subDirs as $subDir) {
+ import_categories($subDir, $categoryId);
+ }
+ }
+}
+
+/**
+ * Rekursive Funktion zum Importieren von Beiträgen.
+ * In jedem Ordner werden alle Dateien als Beiträge importiert.
+ * Für jede Datei wird der Dateiname (ohne Erweiterung) als Titel verwendet und
+ * als Teaser "Hier finden Sie die Informationen zu [Dateiname]" gesetzt.
+ * Anschließend wird der Beitrag in knowledgecenter_posts und knowledgecenter_post_versions (language_id = 1, Version 1) angelegt
+ * und in knowledgecenter_category_post mit der zugehörigen Kategorie verknüpft.
+ * Zusätzlich wird ein Eintrag in knowledgecenter_post_files für die Datei erstellt.
+ *
+ * @param string $folder Absoluter Pfad des aktuellen Ordners.
+ */
+function import_posts($folder)
+{
+ if (!is_dir($folder)) {
+ return;
+ }
+
+ // Ermitteln der zugehörigen Kategorie anhand des Ordnernamens
+ $folderName = basename($folder);
+ $folderNameEscaped = mysqli_real_escape_string($GLOBALS['mysql_con'], $folderName);
+ $query = "SELECT c.id
+ FROM knowledgecenter_categories_update c
+ JOIN knowledgecenter_category_translations t ON c.id = t.category_id
+ WHERE t.language_id = 1 AND t.title = '$folderNameEscaped' LIMIT 1";
+ $result = mysqli_query($GLOBALS['mysql_con'], $query);
+ if ($result && mysqli_num_rows($result) > 0) {
+ $row = mysqli_fetch_assoc($result);
+ $categoryId = $row['id'];
+ } else {
+ echo "Keine Kategorie für '$folderName' gefunden. Bitte zuerst Kategorien importieren.
";
+ return;
+ }
+
+ // Alle Dateien im aktuellen Ordner als Beiträge importieren
+ $files = glob($folder . "/*");
+ if ($files !== false) {
+ foreach ($files as $item) {
+ if (is_file($item)) {
+ $fileName = basename($item);
+ $fileTitle = pathinfo($fileName, PATHINFO_FILENAME); // Dateiname ohne Erweiterung
+ $titleEscaped = mysqli_real_escape_string($GLOBALS['mysql_con'], $fileTitle);
+ $teaser = "Hier finden Sie die Informationen zu $fileTitle";
+ $teaserEscaped = mysqli_real_escape_string($GLOBALS['mysql_con'], $teaser);
+
+ // Überprüfen, ob bereits ein Beitrag mit diesem Titel existiert
+ $checkQuery = "SELECT pv.id
+ FROM knowledgecenter_post_versions pv
+ JOIN knowledgecenter_posts p ON pv.post_id = p.id
+ WHERE pv.language_id = 1 AND pv.title = '$titleEscaped' LIMIT 1";
+ $checkResult = mysqli_query($GLOBALS['mysql_con'], $checkQuery);
+ if ($checkResult && mysqli_num_rows($checkResult) > 0) {
+ echo "Beitrag '$fileTitle' existiert bereits. Überspringe.
";
+ continue;
+ }
+
+ // Beitrag erstellen
+ $sql_post = "INSERT INTO knowledgecenter_posts (created_at, modified_at) VALUES (NOW(), NOW())";
+ if (!mysqli_query($GLOBALS['mysql_con'], $sql_post)) {
+ echo "Fehler beim Erstellen des Beitrags für Datei '$fileName': " . mysqli_error($GLOBALS['mysql_con']) . "
";
+ continue;
+ }
+ $newPostId = mysqli_insert_id($GLOBALS['mysql_con']);
+
+ // Beitrag-Version anlegen (language_id = 1, Version 1)
+ $sql_post_version = "INSERT INTO knowledgecenter_post_versions
+ (post_id, version_number, language_id, title, teaser, content, author_id, created_at)
+ VALUES ($newPostId, 1, 1, '$titleEscaped', '$teaserEscaped', '', 0, NOW())";
+ if (!mysqli_query($GLOBALS['mysql_con'], $sql_post_version)) {
+ echo "Fehler beim Erstellen der Beitragsversion für Datei '$fileTitle': " . mysqli_error($GLOBALS['mysql_con']) . "
";
+ }
+
+ // Beitrag mit der Kategorie verknüpfen
+ $sql_link_post = "INSERT INTO knowledgecenter_category_post (category_id, post_id) VALUES ($categoryId, $newPostId)";
+ if (!mysqli_query($GLOBALS['mysql_con'], $sql_link_post)) {
+ echo "Fehler beim Verknüpfen des Beitrags $newPostId mit Kategorie $categoryId: " . mysqli_error($GLOBALS['mysql_con']) . "
";
+ }
+
+ // Eintrag in die Files-Tabelle erstellen
+ // Ermitteln des relativen Pfads relativ zum Document Root:
+ $docRoot = realpath($_SERVER['DOCUMENT_ROOT']);
+ $relativePath = str_replace($docRoot, '', realpath($item));
+ // Als Beschreibung verwenden wir den Dateinamen (inklusive Erweiterung)
+ $descEscaped = mysqli_real_escape_string($GLOBALS['mysql_con'], $fileName);
+ $pathEscaped = mysqli_real_escape_string($GLOBALS['mysql_con'], $relativePath);
+ $sql_file = "INSERT INTO knowledgecenter_post_files (post_id, description, path, created_at)
+ VALUES ($newPostId, '$descEscaped', '$pathEscaped', NOW())";
+ if (!mysqli_query($GLOBALS['mysql_con'], $sql_file)) {
+ echo "Fehler beim Einfügen in die Files-Tabelle für Datei '$fileName': " . mysqli_error($GLOBALS['mysql_con']) . "
";
+ }
+
+ echo "Beitrag '$fileTitle' (ID: $newPostId) in Kategorie '$folderName' importiert.
";
+ }
+ }
+ }
+
+ // Unterordner rekursiv verarbeiten
+ $subDirs = glob($folder . "/*", GLOB_ONLYDIR);
+ if ($subDirs !== false) {
+ foreach ($subDirs as $subDir) {
+ import_posts($subDir);
+ }
+ }
+}
+
+
+// Hauptlogik: Prüfe die GET-Parameter zur Steuerung der Aktionen
+// Da im Link auch ?action=Import gesetzt ist, können wir dies ignorieren und nur reset, cat_import, post_import prüfen.
+
+// Reset: Tabellen leeren
+if (isset($_GET['reset_import']) && $_GET['reset_import'] == "1") {
+ echo "Reset Import gestartet
";
+ truncate_import_tables();
+ echo "Reset Import beendet
";
+ exit;
+}
+
+// Kategorien importieren: GET-Parameter "cat_import" verwenden
+if (isset($_GET['cat_import']) && $_GET['cat_import'] == "1") {
+ $importFolder = realpath($_SERVER['DOCUMENT_ROOT'] . '/userdata/knowledgecenter_update');
+ if (!$importFolder) {
+ die("Der Import-Ordner wurde nicht gefunden.");
+ }
+ echo "Kategorien-Import gestartet
";
+ import_categories($importFolder);
+ echo "Kategorien-Import beendet
";
+ exit;
+}
+
+// Beiträge importieren: GET-Parameter "post_import" verwenden
+if (isset($_GET['post_import']) && $_GET['post_import'] == "1") {
+ $importFolder = realpath($_SERVER['DOCUMENT_ROOT'] . '/userdata/knowledgecenter_update');
+ if (!$importFolder) {
+ die("Der Import-Ordner wurde nicht gefunden.");
+ }
+ echo "Beiträge-Import gestartet
";
+ import_posts($importFolder);
+ echo "Beiträge-Import beendet
";
+ exit;
+}
+?>
\ No newline at end of file
diff --git a/module/knowledgecenter_update/Installation/Installation.php b/module/knowledgecenter_update/Installation/Installation.php
new file mode 100644
index 0000000..025f7e1
--- /dev/null
+++ b/module/knowledgecenter_update/Installation/Installation.php
@@ -0,0 +1,444 @@
+✅ $message";
+}
+
+function echoError($message)
+{
+ echo "❌ $message";
+}
+
+// Beispielhafte DB-Verbindung (nur Dummy für Kontext)
+if (!isset($GLOBALS['mysql_con'])) {
+ echoError("Keine Datenbankverbindung gefunden.");
+ exit;
+}
+
+// Tabellen-Erstellung
+function create_tables()
+{
+ $allSuccess = true;
+
+ $queries = [
+ // Tabellenname => SQL
+ "knowledgecenter_posts" => "CREATE TABLE IF NOT EXISTS knowledgecenter_posts (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ modified_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ modified_by VARCHAR(255) COMMENT 'Name des Main Contact',
+ preview_inline TINYINT(1) NOT NULL DEFAULT 1 COMMENT '0 = keine Inline-Vorschau, 1 = Inline-Vorschau',
+ background_color VARCHAR(9) NOT NULL DEFAULT '#FFFFFF',
+ text_color VARCHAR(9) NOT NULL DEFAULT '#000000'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_post_contacts" => "CREATE TABLE IF NOT EXISTS knowledgecenter_post_contacts (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ post_id INT NOT NULL,
+ main_contact_id INT NOT NULL,
+ modified_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (post_id) REFERENCES knowledgecenter_posts(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_post_mandant" => "CREATE TABLE IF NOT EXISTS knowledgecenter_post_mandant (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ post_id INT NOT NULL,
+ mandant_id INT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (post_id) REFERENCES knowledgecenter_posts(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_post_department" => "CREATE TABLE IF NOT EXISTS knowledgecenter_post_department (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ post_id INT NOT NULL,
+ department_id INT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (post_id) REFERENCES knowledgecenter_posts(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_post_files" => "CREATE TABLE IF NOT EXISTS knowledgecenter_post_files (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ post_id INT NOT NULL,
+ description VARCHAR(255),
+ path VARCHAR(500),
+ modified_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (post_id) REFERENCES knowledgecenter_posts(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_post_role" => "CREATE TABLE IF NOT EXISTS knowledgecenter_post_role (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ post_id INT NOT NULL,
+ role_id INT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (post_id) REFERENCES knowledgecenter_posts(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_post_versions" => "CREATE TABLE IF NOT EXISTS knowledgecenter_post_versions (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ post_id INT NOT NULL,
+ version_number INT NOT NULL,
+ language_id INT NOT NULL,
+ title VARCHAR(255),
+ image VARCHAR(255),
+ teaser TEXT,
+ content LONGTEXT,
+ author_id INT,
+ state TINYINT DEFAULT 1,
+ modified_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ valid_from DATETIME NULL,
+ valid_until DATETIME NULL,
+ background_color VARCHAR(9) NOT NULL DEFAULT '#FFFFFF',
+ text_color VARCHAR(9) NOT NULL DEFAULT '#000000',
+ FOREIGN KEY (post_id) REFERENCES knowledgecenter_posts(id)
+ ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_post_version_contacts" => "CREATE TABLE IF NOT EXISTS knowledgecenter_post_version_contacts (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ version_id INT NOT NULL,
+ contact_id INT NOT NULL,
+ FOREIGN KEY (version_id) REFERENCES knowledgecenter_post_versions(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_categories_update" => "CREATE TABLE IF NOT EXISTS knowledgecenter_categories_update (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ sorting INT DEFAULT 0,
+ state TINYINT DEFAULT 1,
+ modified_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ modified_by VARCHAR(255) COMMENT 'Name des Main Contact',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_category_translations" => "CREATE TABLE IF NOT EXISTS knowledgecenter_category_translations (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ category_id INT NOT NULL,
+ language_id INT NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ image VARCHAR(255),
+ modified_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES knowledgecenter_categories_update(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_category_post" => "CREATE TABLE IF NOT EXISTS knowledgecenter_category_post (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ category_id INT NOT NULL,
+ post_id INT NOT NULL,
+ FOREIGN KEY (category_id) REFERENCES knowledgecenter_categories_update(id)
+ ON DELETE CASCADE,
+ FOREIGN KEY (post_id) REFERENCES knowledgecenter_posts(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_category_mandant" => "CREATE TABLE IF NOT EXISTS knowledgecenter_category_mandant (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ category_id INT NOT NULL,
+ mandant_id INT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES knowledgecenter_categories_update(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_category_department" => "CREATE TABLE IF NOT EXISTS knowledgecenter_category_department (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ category_id INT NOT NULL,
+ department_id INT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES knowledgecenter_categories_update(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_category_role" => "CREATE TABLE IF NOT EXISTS knowledgecenter_category_role (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ category_id INT NOT NULL,
+ role_id INT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES knowledgecenter_categories_update(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_post_comments" => "CREATE TABLE IF NOT EXISTS knowledgecenter_post_comments (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ post_id INT NOT NULL,
+ parent_id INT DEFAULT NULL,
+ text TEXT NOT NULL,
+ author_id INT NOT NULL COMMENT 'main_contact_id',
+ modified_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (post_id) REFERENCES knowledgecenter_posts(id) ON DELETE CASCADE,
+ FOREIGN KEY (parent_id) REFERENCES knowledgecenter_post_comments(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
+
+ "knowledgecenter_category_links" => "CREATE TABLE IF NOT EXISTS knowledgecenter_category_links (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ parent_category_id INT NOT NULL,
+ child_category_id INT NOT NULL,
+ sorting INT DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (parent_category_id) REFERENCES knowledgecenter_categories_update(id)
+ ON DELETE CASCADE,
+ FOREIGN KEY (child_category_id) REFERENCES knowledgecenter_categories_update(id)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
+ ];
+
+ foreach ($queries as $name => $sql) {
+ if (mysqli_query($GLOBALS['mysql_con'], $sql)) {
+ echoSuccess("Tabelle $name wurde erfolgreich erstellt (oder existierte bereits).");
+ } else {
+ echoError("Fehler beim Erstellen der Tabelle $name: " . mysqli_error($GLOBALS['mysql_con']));
+ $allSuccess = false;
+ }
+ }
+
+ return $allSuccess;
+}
+
+// Ausführen der Tabellen-Erstellung
+if (create_tables()) {
+ echoSuccess("✅ Alle Tabellen erfolgreich erstellt!");
+} else {
+ echoError("❌ Es gab Fehler beim Erstellen einzelner Tabellen.");
+}
+
+// Jetzt die zusätzlichen Indexe mittels ALTER TABLE setzen
+$indexQueries = [
+ // knowledgecenter_posts
+ "ALTER TABLE knowledgecenter_posts ADD INDEX idx_created_at (created_at);",
+ "ALTER TABLE knowledgecenter_posts ADD INDEX idx_modified_at (modified_at);",
+ "ALTER TABLE knowledgecenter_posts ADD INDEX idx_modified_by (modified_by);",
+
+ // knowledgecenter_post_contacts
+ "ALTER TABLE knowledgecenter_post_contacts ADD INDEX idx_post_id (post_id);",
+ "ALTER TABLE knowledgecenter_post_contacts ADD INDEX idx_main_contact_id (main_contact_id);",
+
+ // knowledgecenter_post_mandant
+ "ALTER TABLE knowledgecenter_post_mandant ADD INDEX idx_post_mandant (post_id, mandant_id);",
+
+ // knowledgecenter_post_department
+ "ALTER TABLE knowledgecenter_post_department ADD INDEX idx_post_department (post_id, department_id);",
+
+ // knowledgecenter_post_files
+ "ALTER TABLE knowledgecenter_post_files ADD INDEX idx_post_id (post_id);",
+
+ // knowledgecenter_post_role
+ "ALTER TABLE knowledgecenter_post_role ADD INDEX idx_post_role (post_id, role_id);",
+
+ // knowledgecenter_post_versions
+ "ALTER TABLE knowledgecenter_post_versions ADD INDEX idx_post_id (post_id);",
+ "ALTER TABLE knowledgecenter_post_versions ADD INDEX idx_post_language (post_id, language_id);",
+
+ // knowledgecenter_post_version_contacts
+ "ALTER TABLE knowledgecenter_post_version_contacts ADD INDEX idx_version_contact (version_id, contact_id);",
+
+ // knowledgecenter_categories_update
+ "ALTER TABLE knowledgecenter_categories_update ADD INDEX idx_state_sorting (state, sorting);",
+ "ALTER TABLE knowledgecenter_categories_update ADD INDEX idx_modified_at (modified_at);",
+ "ALTER TABLE knowledgecenter_categories_update ADD INDEX idx_modified_by (modified_by);",
+
+ // knowledgecenter_category_translations
+ "ALTER TABLE knowledgecenter_category_translations ADD INDEX idx_category_language (category_id, language_id);",
+
+ // knowledgecenter_category_post
+ "ALTER TABLE knowledgecenter_category_post ADD INDEX idx_category_post (category_id, post_id);",
+
+ // knowledgecenter_category_mandant
+ "ALTER TABLE knowledgecenter_category_mandant ADD INDEX idx_category_mandant (category_id, mandant_id);",
+
+ // knowledgecenter_category_department
+ "ALTER TABLE knowledgecenter_category_department ADD INDEX idx_category_department (category_id, department_id);",
+
+ // knowledgecenter_category_role
+ "ALTER TABLE knowledgecenter_category_role ADD INDEX idx_category_role (category_id, role_id);",
+
+ // knowledgecenter_post_comments
+ "ALTER TABLE knowledgecenter_post_comments ADD INDEX idx_post_id (post_id);",
+ "ALTER TABLE knowledgecenter_post_comments ADD INDEX idx_parent_id (parent_id);",
+
+ // knowledgecenter_category_links
+ "ALTER TABLE knowledgecenter_category_links ADD INDEX idx_parent_child (parent_category_id, child_category_id);"
+];
+
+foreach ($indexQueries as $query) {
+ if (mysqli_query($GLOBALS['mysql_con'], $query)) {
+ echoSuccess("Index erfolgreich erstellt: " . htmlspecialchars($query));
+ } else {
+ echoError("Fehler beim Erstellen des Index: " . mysqli_error($GLOBALS['mysql_con']) . " - Query: " . htmlspecialchars($query));
+ }
+}
+
+// Überprüfen, ob die Spalte "post_id" in der Tabelle "main_certificate" existiert
+$checkQuery = "SHOW COLUMNS FROM main_certificate LIKE 'post_id'";
+$checkResult = mysqli_query($GLOBALS['mysql_con'], $checkQuery);
+if ($checkResult && mysqli_num_rows($checkResult) == 0) {
+ // Spalte existiert nicht – hinzufügen
+ $alterQuery = "ALTER TABLE main_certificate ADD COLUMN post_id INT NOT NULL"; // oder NULL, je nach Anforderung
+ if (mysqli_query($GLOBALS['mysql_con'], $alterQuery)) {
+ echoSuccess("Spalte 'post_id' in Tabelle main_certificate erfolgreich hinzugefügt.");
+ } else {
+ echoError("Fehler beim Hinzufügen der Spalte 'post_id': " . mysqli_error($GLOBALS['mysql_con']));
+ }
+} else {
+ echoSuccess("Spalte 'post_id' existiert bereits in main_certificate.");
+}
+
+// Array mit den Permissions, die hinzugefügt werden sollen
+$permissions = [
+ [
+ 'description' => 'Darf Wiki Modul installieren',
+ 'code' => 'install_admin_kc',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Inhalte importieren',
+ 'code' => 'import_admin_kc',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Beitragsliste sehen',
+ 'code' => 'view_admin_kc_post_list',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Kategorie Admin Listen Items löschen',
+ 'code' => 'delete_admin_kc_category_item',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Kategorie Admin Liste sehen',
+ 'code' => 'view_admin_kc_category_list',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Kategorien erstellen',
+ 'code' => 'create_admin_kc_category',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Kategorien bearbeiten',
+ 'code' => 'edit_admin_kc_category',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf neuen Wiki Beitrag erstellen',
+ 'code' => 'create_admin_kc_post',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Beitragsversion ändern',
+ 'code' => 'edit_admin_kc_post_version',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf neue Wiki Beitragsversion erstellen',
+ 'code' => 'create_admin_kc_post_version',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Beitragsversion löschen',
+ 'code' => 'delete_admin_kc_post_version',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Beitragsdateien löschen',
+ 'code' => 'delete_admin_kc_post_file',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Beitragsdateien hinzufügen',
+ 'code' => 'add_admin_kc_post_file',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Beitrags Ansprechpartner verwalten',
+ 'code' => 'manage_admin_kc_post_contacts',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Beitrags Einstellungen sehen',
+ 'code' => 'view_admin_kc_post_settings',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ],
+ [
+ 'description' => 'Darf Wiki Beitrags Einstellungen speichern',
+ 'code' => 'save_admin_kc_post_settings',
+ 'short_text' => '',
+ 'only_main_mandant' => 1,
+ 'default_active' => 0
+ ]
+];
+
+// Iteriere über das Array und füge jede Permission ein:
+foreach ($permissions as $perm) {
+ // Stelle sicher, dass Zeichenketten richtig escaped werden:
+ $description = mysqli_real_escape_string($GLOBALS['mysql_con'], $perm['description']);
+ $code = mysqli_real_escape_string($GLOBALS['mysql_con'], $perm['code']);
+ $short_text = mysqli_real_escape_string($GLOBALS['mysql_con'], $perm['short_text']);
+ $only_main_mandant = (int) $perm['only_main_mandant'];
+ $default_active = (int) $perm['default_active'];
+
+ // Zuerst prüfen, ob eine Permission mit diesem Code bereits existiert.
+ $checkSql = "SELECT id FROM main_permission WHERE code = '$code' LIMIT 1";
+ $checkResult = mysqli_query($GLOBALS['mysql_con'], $checkSql);
+ if ($checkResult && mysqli_num_rows($checkResult) > 0) {
+ // Falls vorhanden, kannst Du eine Erfolgsmeldung anzeigen oder den Eintrag überspringen.
+ echoSuccess("Permission '$description' existiert bereits.");
+ continue; // Weitere Verarbeitung überspringen.
+ }
+
+ // Falls nicht vorhanden, führe das INSERT aus:
+ $insertSql = "INSERT INTO `main_permission` (`id`, `description`, `code`, `short_text`, `only_main_mandant`, `default_active`)
+ VALUES (NULL, '$description', '$code', '$short_text', '$only_main_mandant', '$default_active')";
+ if (mysqli_query($GLOBALS['mysql_con'], $insertSql)) {
+ echoSuccess("Permission '$description' wurde erfolgreich hinzugefügt.");
+ } else {
+ echoError("Fehler beim Hinzufügen der Permission '$description': " . mysqli_error($GLOBALS['mysql_con']));
+ }
+}
+
+
+?>
\ No newline at end of file
diff --git a/module/knowledgecenter_update/Installation/add_shared_symlinks.php b/module/knowledgecenter_update/Installation/add_shared_symlinks.php
new file mode 100644
index 0000000..f51bcfc
--- /dev/null
+++ b/module/knowledgecenter_update/Installation/add_shared_symlinks.php
@@ -0,0 +1,86 @@
+/ einen Symlink "shared"
+// an, der auf ../../shared zeigt.
+//
+// Idempotent (bereits vorhandene Einträge werden übersprungen) und
+// CLI-only — niemals direkt per HTTP aufrufbar.
+//
+// Usage:
+// php Installation/add_shared_symlinks.php --dry-run
+// php Installation/add_shared_symlinks.php
+
+declare(strict_types=1);
+
+if (PHP_SAPI !== 'cli') {
+ http_response_code(403);
+ exit('CLI only');
+}
+
+$dryRun = in_array('--dry-run', $argv, true);
+
+$postsDir = realpath(__DIR__ . '/../../../userdata/knowledgecenter_update/posts');
+$sharedDir = realpath(__DIR__ . '/../../../userdata/knowledgecenter_update/shared');
+
+if ($postsDir === false) {
+ fwrite(STDERR, "FEHLER: posts/-Verzeichnis nicht gefunden\n");
+ exit(2);
+}
+if ($sharedDir === false) {
+ fwrite(STDERR, "FEHLER: shared/-Verzeichnis nicht gefunden — bitte erst anlegen\n");
+ exit(2);
+}
+
+echo "Mode: " . ($dryRun ? 'DRY-RUN (keine Änderungen)' : 'LIVE') . "\n";
+echo "Posts: $postsDir\n";
+echo "Shared: $sharedDir\n\n";
+
+$dirs = glob($postsDir . '/*', GLOB_ONLYDIR);
+if ($dirs === false) {
+ fwrite(STDERR, "FEHLER: glob() lieferte false\n");
+ exit(2);
+}
+
+$created = 0;
+$skipped = 0;
+$collisions = 0;
+$failed = 0;
+
+foreach ($dirs as $dir) {
+ $linkPath = $dir . '/shared';
+ $name = basename($dir);
+
+ if (is_link($linkPath)) {
+ $skipped++;
+ continue;
+ }
+ if (file_exists($linkPath)) {
+ // gleichnamiger echter Ordner/Datei — nicht überschreiben
+ echo "[KOLLISION] $name/shared existiert bereits als Ordner/Datei — übersprungen\n";
+ $collisions++;
+ continue;
+ }
+
+ if ($dryRun) {
+ echo "[DRY-RUN] würde anlegen: $name/shared -> ../../shared\n";
+ $created++;
+ continue;
+ }
+
+ if (@symlink('../../shared', $linkPath)) {
+ $created++;
+ } else {
+ $err = error_get_last();
+ echo "[FEHLER] $name/shared konnte nicht angelegt werden: " . ($err['message'] ?? 'unbekannt') . "\n";
+ $failed++;
+ }
+}
+
+echo "\n=== Zusammenfassung ===\n";
+echo "Beitragsordner gesamt: " . count($dirs) . "\n";
+echo "Symlinks " . ($dryRun ? 'geplant' : 'angelegt') . ": $created\n";
+echo "Bereits vorhanden: $skipped\n";
+echo "Kollisionen: $collisions\n";
+echo "Fehlgeschlagen: $failed\n";
+
+exit($failed > 0 ? 1 : 0);
diff --git a/module/knowledgecenter_update/Middleware/Middleware.php b/module/knowledgecenter_update/Middleware/Middleware.php
new file mode 100644
index 0000000..8a0953e
--- /dev/null
+++ b/module/knowledgecenter_update/Middleware/Middleware.php
@@ -0,0 +1,50 @@
+";
+// echo "Mandanten: " . $allowedMandants . "\n";
+// echo "Abteilungen: " . $allowedDepartments . "\n";
+// echo "Rollen: " . $allowedRoles . "\n";
+// echo "";
diff --git a/module/knowledgecenter_update/Plugins/_files/cache/.gitkeep b/module/knowledgecenter_update/Plugins/_files/cache/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/module/knowledgecenter_update/Plugins/_files/cache/folders/.gitkeep b/module/knowledgecenter_update/Plugins/_files/cache/folders/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/module/knowledgecenter_update/Plugins/_files/cache/images/.gitkeep b/module/knowledgecenter_update/Plugins/_files/cache/images/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/module/knowledgecenter_update/Plugins/_files/cache/menu/.gitkeep b/module/knowledgecenter_update/Plugins/_files/cache/menu/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/module/knowledgecenter_update/Plugins/_files/js/custom.js b/module/knowledgecenter_update/Plugins/_files/js/custom.js
new file mode 100644
index 0000000..ff16c9f
--- /dev/null
+++ b/module/knowledgecenter_update/Plugins/_files/js/custom.js
@@ -0,0 +1,228 @@
+// custom.js – Files Gallery Selection & Popup Script
+
+// Prüfen, ob dieses Fenster ein Popup (d.h. ein Child-Fenster) ist
+if (window.opener) {
+ // URL-Parameter auslesen und in localStorage speichern
+ const urlParams = new URLSearchParams(window.location.search);
+ let input_id = urlParams.get('input_id') || "";
+ let image_id = urlParams.get('image_id') || "";
+ let type = urlParams.get('type') || "";
+ let mode = urlParams.get('mode') || "";
+ let saveajax = urlParams.get('saveajax') || "";
+ let kc_lock_path = urlParams.get('kc_lock_path') || "";
+
+ if (urlParams.has('input_id')) {
+ if (input_id) {
+ localStorage.setItem('input_id', input_id);
+ } else {
+ localStorage.removeItem('input_id');
+ }
+ }
+ if (urlParams.has('image_id')) {
+ if (image_id) {
+ localStorage.setItem('image_id', image_id);
+ } else {
+ localStorage.removeItem('image_id');
+ }
+ }
+ if (urlParams.has('type')) {
+ if (type) {
+ localStorage.setItem('type', type);
+ } else {
+ localStorage.removeItem('type');
+ }
+ }
+ if (urlParams.has('mode')) {
+ if (mode) {
+ localStorage.setItem('mode', mode);
+ } else {
+ localStorage.removeItem('mode');
+ }
+ }
+ if (urlParams.has('saveajax')) {
+ if (saveajax) {
+ localStorage.setItem('saveajax', saveajax);
+ } else {
+ localStorage.removeItem('saveajax');
+ }
+ }
+ if (urlParams.has('kc_lock_path')) {
+ if (kc_lock_path) {
+ localStorage.setItem('kc_lock_path', kc_lock_path);
+ } else {
+ localStorage.removeItem('kc_lock_path');
+ }
+ }
+
+ // Werte aus localStorage wieder auslesen
+ input_id = localStorage.getItem('input_id');
+ image_id = localStorage.getItem('image_id');
+ type = localStorage.getItem('type');
+ mode = localStorage.getItem('mode');
+ saveajax = localStorage.getItem('saveajax');
+ kc_lock_path = localStorage.getItem('kc_lock_path') || "";
+
+ function normalizePath(path) {
+ if (!path) return "";
+
+ const cleaned = String(path).replace(/^\/+/, '');
+ const lockPath = String(kc_lock_path || '').replace(/^\/+|\/+$/g, '');
+
+ if (!lockPath) {
+ return cleaned;
+ }
+
+ if (cleaned === lockPath || cleaned.indexOf(lockPath + '/') === 0) {
+ return cleaned;
+ }
+
+ return lockPath + '/' + cleaned;
+ }
+
+ function normalizePaths(paths) {
+ if (!paths) return "";
+
+ return String(paths)
+ .split(',')
+ .map(part => normalizePath(part.trim()))
+ .filter(Boolean)
+ .join(', ');
+ }
+
+ /**
+ * Übergibt die gewählten Pfade an das Parent-Fenster und schließt das Popup.
+ * @param {string} paths Kommagetrennter Pfad(e)
+ * @param {string} input_id ID des Input-Felds im Parent
+ * @param {string} image_id ID des Bild-Elements im Parent
+ */
+ function sendDataAndClose(paths, input_id, image_id) {
+ const normalizedPaths = normalizePaths(paths);
+ console.log("Übergebe Pfade:", normalizedPaths);
+ if (!window.opener) return;
+
+ // Setze den Pfad oder die Pfade im Input-Feld
+ const input = window.opener.document.getElementById(input_id);
+ if (input) {
+ input.value = normalizedPaths;
+ console.log(`Wert in Input "${input_id}" gesetzt.`);
+ } else {
+ console.error(`Kein Input-Feld mit der ID "${input_id}" gefunden.`);
+ }
+
+ // Bild-Vorschau aktualisieren (sucht erstes Element mit Klasse "preview")
+ const img = window.opener.document.querySelector('.preview');
+ if (img) {
+ img.src = '/userdata/' + normalizedPaths.split(',')[0].trim();
+ img.style.display = "block";
+ console.log('Bild-Element mit Klasse "preview" aktualisiert.');
+ } else {
+ console.warn('Kein Bild-Element mit der Klasse "preview" gefunden.');
+ }
+
+ // Optional: übergebene AJAX-Funktion aufrufen
+ if (saveajax && typeof window.opener[saveajax] === 'function') {
+ console.log(`Rufe AJAX-Funktion "${saveajax}" im Parent auf.`);
+ window.opener[saveajax]();
+ } else if (saveajax) {
+ console.warn(`AJAX-Funktion "${saveajax}" nicht gefunden.`);
+ }
+
+ // Popup schließen
+ console.log("Popup wird geschlossen.");
+ window.close();
+ }
+
+ /**
+ * Fügt den Button "Auswahl speichern" in der Toolbar ein (einmalig).
+ */
+ function addCustomButton() {
+ const topbar = document.querySelector('.topbar-select');
+ if (!topbar) return;
+ const buttonsContainer = topbar.querySelector('.buttons-selected');
+ if (!buttonsContainer) return;
+ if (buttonsContainer.querySelector('.my-custom-button')) return;
+
+ const button = document.createElement('button');
+ button.textContent = 'Auswahl speichern';
+ button.className = 'my-custom-button';
+ Object.assign(button.style, {
+ marginRight: '10px',
+ padding: '10px 20px',
+ backgroundColor: 'rgb(94 188 24)',
+ color: '#fff',
+ border: 'none',
+ borderRadius: '999px',
+ cursor: 'pointer'
+ });
+
+ button.addEventListener('click', () => {
+ const selectedItems = document.querySelectorAll('a.files-a[data-selected="1"]');
+ const selectedPaths = Array.from(selectedItems).map(el => el.dataset.path);
+ const pathString = selectedPaths.join(', ');
+ console.log('Ausgewählte Pfade:', selectedPaths);
+ sendDataAndClose(pathString, input_id, image_id);
+ });
+
+ buttonsContainer.insertBefore(button, buttonsContainer.firstChild);
+ }
+
+ /**
+ * Prüft, ob mindestens ein Element ausgewählt ist, und zeigt ggf. den Button.
+ */
+ function checkSelectionAndAddButton() {
+ const anySelected = document.querySelector('a.files-a[data-selected="1"]');
+ if (anySelected) {
+ addCustomButton();
+ }
+ }
+
+ // Observer für Änderungen an `data-selected`
+ const observer = new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ if (
+ mutation.type === 'attributes' &&
+ mutation.attributeName === 'data-selected'
+ ) {
+ const target = mutation.target;
+ const isSelected = target.getAttribute('data-selected') === '1';
+
+ // Wenn single → andere Auswahl(en) aufheben
+ if (isSelected && mode === 'single') {
+ document.querySelectorAll('a.files-a[data-selected="1"]').forEach(el => {
+ if (el !== target) {
+ el.setAttribute('data-selected', '0');
+ }
+ });
+ }
+
+ checkSelectionAndAddButton();
+ break;
+ }
+ }
+ });
+
+ /**
+ * Beginnt, alle Gallery-Items auf Auswahländerungen zu beobachten.
+ */
+ function observeFiles() {
+ const items = document.querySelectorAll('a.files-a');
+ items.forEach(item => {
+ observer.observe(item, {
+ attributes: true,
+ attributeFilter: ['data-selected']
+ });
+ });
+ }
+
+ // Initialisierung nach DOM ready
+ document.addEventListener('DOMContentLoaded', () => {
+ observeFiles();
+
+ // Falls neue Items nachgeladen werden, erneut beobachten
+ const listObserver = new MutationObserver(() => {
+ observeFiles();
+ });
+ listObserver.observe(document.body, { childList: true, subtree: true });
+ });
+
+}
diff --git a/module/knowledgecenter_update/Plugins/filesgallery.php b/module/knowledgecenter_update/Plugins/filesgallery.php
new file mode 100644
index 0000000..0af04a5
--- /dev/null
+++ b/module/knowledgecenter_update/Plugins/filesgallery.php
@@ -0,0 +1,3542 @@
+ 0,
+ 'path' => '/',
+ 'domain' => $_SERVER['HTTP_HOST'] ?? '',
+ 'secure' => $isSecure,
+ 'httponly' => true,
+ 'samesite' => 'Lax',
+]);
+
+// Startet die PHP-Session mit den oben gesetzten Parametern.
+session_start();
+
+$kcGalleryUserdataRoot = realpath(__DIR__ . '/../../../userdata');
+$kcLockSessionKey = 'kc_fg_lock_root';
+$kcLockedRootPath = '';
+
+if ($kcGalleryUserdataRoot !== false) {
+ if (isset($_REQUEST['kc_lock_path'])) {
+ $requestedLockPath = trim((string) $_REQUEST['kc_lock_path'], '/');
+ if ($requestedLockPath !== '' && preg_match('#^knowledgecenter_update/posts/[a-f0-9]{24}$#', $requestedLockPath)) {
+ $candidateFullPath = $kcGalleryUserdataRoot . '/' . $requestedLockPath;
+ $candidatePath = realpath($candidateFullPath);
+
+ if ($candidatePath === false) {
+ $postsBasePath = $kcGalleryUserdataRoot . '/knowledgecenter_update/posts';
+ if (!is_dir($postsBasePath)) {
+ @mkdir($postsBasePath, 0775, true);
+ }
+ if (!is_dir($candidateFullPath)) {
+ @mkdir($candidateFullPath, 0775, true);
+ }
+ $candidatePath = realpath($candidateFullPath);
+ }
+
+ if ($candidatePath !== false && strpos($candidatePath, $kcGalleryUserdataRoot . DIRECTORY_SEPARATOR) === 0 && is_dir($candidatePath)) {
+ $_SESSION[$kcLockSessionKey] = $candidatePath;
+ }
+ } elseif ($requestedLockPath === '') {
+ unset($_SESSION[$kcLockSessionKey]);
+ }
+ }
+
+ if (!empty($_SESSION[$kcLockSessionKey]) && is_string($_SESSION[$kcLockSessionKey])) {
+ $sessionLockPath = realpath($_SESSION[$kcLockSessionKey]);
+ if ($sessionLockPath !== false && strpos($sessionLockPath, $kcGalleryUserdataRoot . DIRECTORY_SEPARATOR) === 0 && is_dir($sessionLockPath)) {
+ $kcLockedRootPath = $sessionLockPath;
+ } else {
+ unset($_SESSION[$kcLockSessionKey]);
+ }
+ }
+}
+
+if ($kcLockedRootPath !== '' && !empty($_SERVER['QUERY_STRING'])) {
+ $queryParts = explode('&', $_SERVER['QUERY_STRING']);
+ if (!empty($queryParts) && strpos($queryParts[0], '=') === false) {
+ array_shift($queryParts);
+ $_SERVER['QUERY_STRING'] = implode('&', $queryParts);
+ }
+}
+
+$GLOBALS['kc_fg_locked_root'] = $kcLockedRootPath;
+
+// Prüft, ob ein Benutzer eingeloggt ist (login_id existiert und > 0).
+// Wenn nicht, wird sofort zur Startseite weitergeleitet.
+if (empty($_SESSION['login_id']) || $_SESSION['login_id'] == 0) {
+ header('Location: /');
+ exit;
+}
+
+/* Files Gallery 0.12.4
+www.files.gallery | www.files.gallery/docs/ | www.files.gallery/docs/license/
+---
+This PHP file is only 10% of the application, used only to connect with the file system. 90% of the codebase, including app logic, interface, design and layout is managed by the app Javascript and CSS files.
+---
+class Config / load config with static methods to access config options
+class Login / check and manage logins
+class U / static utility functions
+class Path / static functions to convert and validate file paths
+class Json / JSON response functions
+class X3 / helper functions when running Files Gallery alongside X3 www.photo.gallery
+class Tests / outputs PHP, server and config diagnostics by url ?action=tests
+class FileResponse / outputs file, video preview image, resized image and proxies any file by PHP
+class ResizeImage / serves a resized image
+class Dirs / outputs menu json from dir structure
+class Dir / loads data array for a single dir
+class File / returns data array for a single file
+class Iptc / extract IPTC image data from images
+class Exif / extract Exif image data from images
+class Filemanager / functions that handle file operations on server
+class Zipper / create and extract zip files
+class Request / extract parameters for all actions
+class Document / creates the main Files Gallery document response
+*/
+
+// class Config / constructor and static methods to access config options
+class Config {
+
+ // config defaults / https://www.files.gallery/docs/config/
+ // Only edit directly here if it is a temporary installation. Settings here will be lost when updating!
+ // Instead, add options into external config file in your storage_path _files/config/config.php (generated on first run)
+ private static $default = [
+ 'root' => '',
+ 'root_url_path' => null,
+ 'start_path' => false,
+ 'username' => '',
+ 'password' => '',
+ 'load_images' => true,
+ 'load_files_proxy_php' => false,
+ 'load_images_max_filesize' => 1000000,
+ 'image_resize_enabled' => true,
+ 'image_resize_cache' => true,
+ 'image_resize_dimensions' => 320,
+ 'image_resize_dimensions_retina' => 480,
+ 'image_resize_dimensions_allowed' => '',
+ 'image_resize_types' => 'jpeg, png, gif, webp, bmp, avif',
+ 'image_resize_quality' => 85,
+ 'image_resize_function' => 'imagecopyresampled',
+ 'image_resize_sharpen' => true,
+ 'image_resize_memory_limit' => 256,
+ 'image_resize_max_pixels' => 60000000,
+ 'image_resize_min_ratio' => 1.5,
+ 'image_resize_cache_direct' => false,
+ 'folder_preview_image' => true,
+ 'folder_preview_default' => '_filespreview.jpg',
+ 'menu_enabled' => true,
+ 'menu_max_depth' => 5,
+ 'menu_sort' => 'name_asc',
+ 'menu_cache_validate' => true,
+ 'menu_load_all' => false,
+ 'menu_recursive_symlinks' => true,
+ 'layout' => 'rows',
+ 'cache' => true,
+ 'cache_key' => 0,
+ 'storage_path' => '_files',
+ 'files_include' => '',
+ 'files_exclude' => '',
+ 'dirs_include' => '',
+ 'dirs_exclude' => '',
+ 'allow_symlinks' => true,
+ 'get_mime_type' => false,
+ 'license_key' => '',
+ 'download_dir' => 'browser',
+ 'download_dir_cache' => 'dir',
+ 'assets' => '',
+ 'allow_all' => false,
+ 'allow_upload' => false,
+ 'allow_delete' => false,
+ 'allow_rename' => false,
+ 'allow_new_folder' => false,
+ 'allow_new_file' => false,
+ 'allow_duplicate' => false,
+ 'allow_text_edit' => false,
+ 'allow_zip' => false,
+ 'allow_unzip' => false,
+ 'allow_move' => false,
+ 'allow_copy' => false,
+ 'allow_download' => true,
+ 'allow_mass_download' => false,
+ 'allow_mass_copy_links' => false,
+ 'allow_settings' => false,
+ 'allow_check_updates' => false,
+ 'allow_tests' => true,
+ 'allow_tasks' => false,
+ 'demo_mode' => false,
+ 'upload_allowed_file_types' => '',
+ 'upload_max_filesize' => 0,
+ 'upload_exists' => 'increment',
+ 'video_thumbs' => true,
+ 'video_ffmpeg_path' => 'ffmpeg',
+ 'pdf_thumbs' => true,
+ 'imagemagick_path' => 'magick',
+ 'use_google_docs_viewer' => false,
+ 'lang_default' => 'en',
+ 'lang_auto' => true,
+ 'index_cache' => false,
+ ];
+
+ // global application variables created on new Config()
+ public static $version = '0.12.4'; // Files Gallery version
+ public static $config = []; // config array merged from _filesconfig.php, config.php and default config
+ public static $localconfigpath = '_filesconfig.php'; // optional config file in current dir, useful when overriding shared configs
+ public static $localconfig = []; // config array from localconfigpath
+ public static $storagepath; // absolute storage path for cache, config, plugins and more, normally _files dir
+ public static $storageconfigpath; // absolute path to storage config, normally _files/config/config.php
+ public static $storageconfig = []; // config array from storage path, normally _files/config/config.php
+ public static $cachepath; // absolute cache path shortcut
+ public static $__dir__; // absolute __DIR__ path with normalized OS path
+ public static $__file__; // absolute __FILE__ path with normalized OS path
+ public static $root; // absolute root path interpolated from config root option, normally current dir
+ public static $document_root; // absolute server document root with normalized OS path
+ public static $created = []; // checks what dirs and files get created by config on ?action=tests
+
+ // config construct created static app vars and merge configs
+ public function __construct() {
+
+ // get absolute __DIR__ and __FILE__ paths with normalized OS paths
+ self::$__dir__ = Path::realpath(__DIR__);
+ self::$__file__ = Path::realpath(__FILE__);
+
+ // load local config _filesconfig.php if exists
+ self::$localconfig = $this->load(self::$localconfigpath);
+
+ // create initial config array from default and localconfig
+ self::$config = array_replace(self::$default, self::$localconfig);
+ $this->apply_locked_root_config();
+
+ // set absolute storagepath, create storage dirs if required, and load, create or update storage config.php
+ $this->storage();
+ $this->apply_locked_root_config();
+
+ // install.php - allow edit settings and create users from interface temporarily when file is named "install.php"
+ // useful when installing Files Gallery, allows editing settings and creating users without having to modify config.php manually
+ // remember to rename the file back to index.php once you have edited settings and/or created users.
+ if(U::basename(__FILE__) === 'install.php') self::$config['allow_settings'] = true;
+
+ // at this point we must check if login is required or user is already logged in, and then merge user config
+ new Login();
+ $this->apply_locked_root_config();
+
+ // shortcut option `allow_all` allows all file actions (except settings, check_updates, tests, tasks)
+ if(self::get('allow_all')) foreach (['upload', 'delete', 'rename', 'new_folder', 'new_file', 'duplicate', 'text_edit', 'zip', 'unzip', 'move', 'copy', 'download', 'mass_download', 'mass_copy_links'] as $k) self::$config['allow_'.$k] = true;
+
+ // assign public real root path
+ self::$root = Path::realpath(self::get('root'));
+
+ // error if root path does not exist
+ if(!self::$root) U::error('root dir ' . self::get('root') . ' does not exist');
+
+ // storagepath can't be the same as root dir, because storage_path is excluded
+ if(self::$storagepath === self::$root) U::error('storage_path can\'t be the same as root');
+
+ // get server document root with normalized OS path
+ self::$document_root = Path::realpath($_SERVER['DOCUMENT_ROOT']);
+ }
+
+ private function apply_locked_root_config() {
+ if (!empty($GLOBALS['kc_fg_locked_root']) && is_string($GLOBALS['kc_fg_locked_root'])) {
+ self::$config['root'] = $GLOBALS['kc_fg_locked_root'];
+ self::$config['start_path'] = false;
+ self::$config['menu_enabled'] = false;
+ }
+ }
+
+ // public shortcut function to get config option Config::get('option')
+ public static function get($option){
+ return self::$config[$option];
+ }
+
+ // load a config file and trim values / returns empty array if file doesn't exist
+ private function load($path) {
+ if(empty($path) || !file_exists($path)) return [];
+ $config = include $path;
+ if(empty($config) || !is_array($config)) return [];
+ return array_map(function($v){
+ return is_string($v) ? trim($v) : $v;
+ }, $config);
+ }
+
+ // set storagepath from config, create dir if necessary
+ private function storage(){
+
+ // ignore storagepath and disable cache settings if storage_path is specifically set to FALSE
+ if(self::get('storage_path') === false) {
+ foreach (['cache', 'image_resize_cache', 'folder_preview_image'] as $key) self::$config[$key] = false;
+ return;
+ }
+
+ // shortcut to config storage_path
+ $path = rtrim(self::get('storage_path'), '\/');
+
+ // invalid config storage_path can't be empty or non-string
+ if(!$path || !is_string($path)) U::error('Invalid storage_path parameter');
+
+ // get request ?action if any, to determine if we attempt to make dirs and files on config construct
+ $action = U::get('action');
+
+ // if ?action=tests, check what dirs and files will get created, for tests output
+ if($action === 'tests') {
+ foreach (['', '/config', '/config/config.php', '/cache/images', '/cache/folders', '/cache/menu'] as $key) {
+ if(!file_exists($path . $key)) self::$created[] = $path . $key;
+ }
+ }
+
+ // only make dirs and config if main document (no ?action, except action tests)
+ $make = !$action || $action === 'tests';
+
+ // make storage path dir if it doesn't exist or return error
+ if($make) U::mkdir($path);
+
+ // store absolute storagepath
+ self::$storagepath = Path::realpath($path);
+
+ // error in case storagepath still doesn't seem to exist from realpath()
+ if(!self::$storagepath) U::error('storage_path does not exist and can\'t be created');
+
+ // absolute cache path shortcut
+ self::$cachepath = self::$storagepath . '/cache';
+
+ // assign storage config path (normally */_files/config/config.php), from where we load config and save options
+ self::$storageconfigpath = self::$storagepath . '/config/config.php';
+
+ // load storage config (normally _files/config/config.php) or return empty array
+ self::$storageconfig = $this->load(self::$storageconfigpath);
+
+ // if storage config is not empty, update config by merging default, storageconfig and localconfig
+ if(!empty(self::$storageconfig)) self::$config = array_replace(self::$default, self::$storageconfig, self::$localconfig);
+
+ // only make storage dirs and config.php if main document or ?action=tests
+ if(!$make) return;
+
+ // create required storage dirs if they don't exist / error on fail
+ foreach (['config', 'cache/images', 'cache/folders', 'cache/menu'] as $dir) U::mkdir(self::$storagepath . '/' . $dir);
+
+ // create or update config file if older than index.php
+ if(!file_exists(self::$storageconfigpath) || filemtime(self::$storageconfigpath) < filemtime(__FILE__)) self::save();
+ }
+
+ // save to config.php in storagepath (normally _files/config/config.php) or create new config.php if file doesn't exist
+ public static function save($options = []){
+
+ // merge array of parameters with current storageconfig, and intersect with default, to remove invalida/outdated options
+ $save = array_intersect_key(array_replace(self::$storageconfig, $options), self::$default);
+
+ // create exported array string with save values merged into default values, all commented out
+ $export = preg_replace("/ '/", " //'", U::var_export(array_replace(self::$default, $save)));
+
+ // loop save options and un-comment options where values differ from default options (for convenience, only store differences)
+ foreach ($save as $key => $value) if($value !== self::$default[$key]) $export = str_replace("//'" . $key, "'" . $key, $export);
+
+ // write formatted config array to config (normally _files/config/config.php)
+ return @file_put_contents(self::$storageconfigpath, 'set_session_token();
+
+ // detect $_POST login attempt
+ if($this->is_login_attempt()) {
+
+ // on successful login, merge user config and login
+ if($this->is_successful_login()) return $this->login();
+
+ // check if browser is already logged in by session
+ } else if($this->is_logged_in()){
+
+ // ?logout=1 parameter to logout can only apply if user is already logged in
+ if(U::get('logout')) {
+
+ // we can return and serve request without login if default config does not require login
+ // un-comment the below if you want to redirect to non-login version on logout, instead of showing the login form
+ // if(!self::$has_public_login) return $this->clear_session();
+
+ // logout displays login form
+ return $this->form();
+ }
+
+ // merge user config and login
+ return $this->login();
+
+ // if not logged in and default config does not require login (no username or password)
+ } else if(!self::$has_public_login) {
+
+ // ?login=1 displays login form when default config does not require login
+ if(U::get('login')) {
+
+ // remove $_SESSION['username'] just in case user was removed while session remains
+ if(isset($_SESSION['username'])) unset($_SESSION['username']);
+
+ // serve request without login if default config does not require login
+ } else return;
+ }
+
+ // return error if request is an action (don't display login form)
+ if($this->action_request()) return;
+
+ // display form if not logged in or login failed attempt
+ $this->form();
+ }
+
+ // check if _files/users dir exists and return path
+ public static function users_dir(){
+ return Config::$storagepath && file_exists(Config::$storagepath . '/users') ? Config::$storagepath . '/users' : false;
+ }
+
+ // assign CSRF security $_SESSION['token']
+ private function set_session_token(){
+ if(isset($_SESSION['token'])) return; // token already set
+ $_SESSION['token'] = bin2hex(function_exists('random_bytes') ? random_bytes(32) : openssl_random_pseudo_bytes(32));
+ }
+
+ // check if user is already logged in by session
+ private function is_logged_in(){
+
+ // exit if session username or login hash is not set
+ if(!isset($_SESSION['username']) || !isset($_SESSION['login'])) return false;
+
+ // get user config from $_SESSION username
+ $this->user = $this->get_user($_SESSION['username']);
+
+ // logged in if user found login hash matches session login hash
+ // may fail if user is deleted or username/password/IP/user-agent/app-location changes
+ return $this->user && $this->equals($this->login_hash($this->user), $_SESSION['login']);
+ }
+
+ // detect login attempt
+ private function is_login_attempt(){
+
+ // on javascript fetch() from non-login interface, we must populate $_POST from php://input
+ if(U::get('action') === 'login' && empty($_POST)) $_POST = @json_decode(@trim(@file_get_contents('php://input')), true);
+
+ // is login attempt if $_POST['fusername']
+ return !!U::post('fusername');
+ }
+
+ // detect successful login attempt
+ private function is_successful_login(){
+
+ // login attempt if fusername, fpassword and token in $_POST and 'token' exists in $_SESSION
+ if(!U::post('fusername') || !U::post('fpassword') || !U::post('token') || !isset($_SESSION['token'])) return false;
+
+ // make sure $_SESSION token matches $_POST token
+ if(!$this->equals($_SESSION['token'], U::post('token'))) return false;
+
+ // get user config from $_POST username
+ $this->user = $this->get_user($_POST['fusername']);
+
+ // exit if can't find user or password doesn't match
+ if(!$this->user || !$this->passwords_match($this->user['password'], $_POST['fpassword'])) return false;
+
+ // store username in session
+ $_SESSION['username'] = $this->user['username'];
+
+ // store login hash specific to user, must match on active sessions
+ $_SESSION['login'] = $this->login_hash($this->user);
+
+ // successfull login
+ return true;
+ }
+
+ // successfully logged in by session or login attempt
+ private function login(){
+
+ // list of excluded user config options because they should be global or have no function for user or could cause harm
+ // you can add your own options here if you want to prevent some options from being changed per user
+ $user_exclude = [
+ 'image_resize_dimensions', // should not change per user as it invalidates shared image cache
+ 'image_resize_dimensions_retina', // should not change per user as it invalidates shared image cache
+ 'image_resize_dimensions_allowed', // should not change per user as it invalidates shared image cache
+ 'image_resize_quality', // should not change per user as it invalidates shared image cache
+ 'image_resize_function', // should not change per user as it invalidates shared image cache
+ 'image_resize_sharpen', // should not change per user as it invalidates shared image cache
+ 'storage_path', // storage path is always global and must be defined in main config
+ 'video_ffmpeg_path', // should be in global config
+ 'imagemagick_path', // should be in global config
+ 'index_cache', // should be global config / not avaialble for logged in users anyway
+ ];
+
+ // we are hereby logged in
+ self::$is_logged_in = true;
+
+ // merge user config into config object
+ Config::$config = array_replace(Config::$config, array_diff_key($this->user, array_flip($user_exclude)));
+ }
+
+ // clear login-specific session vars, essentially logging out the user
+ private function clear_session(){
+ foreach (['username', 'login'] as $key) unset($_SESSION[$key]);
+ }
+
+ // get user config from login attempt or session
+ private function get_user($username){
+
+ // trim username just in case
+ $username = trim($username);
+
+ // create lowercase username for case-insensitive comparison
+ $lower_username = $this->lower($username);
+
+ // user equals default config user / return username/password array to verify password or session login
+ if($this->lower(Config::get('username')) === $lower_username) {
+ self::$is_default_user = true; // is default config user
+ return [
+ 'username' => Config::get('username'),
+ 'password' => Config::get('password')
+ ];
+ }
+
+ // exit it _files/users dir doesn't exist
+ if(!self::users_dir()) return false;
+
+ // check if user config exists at _files/users/$username/config.php without making case-insensitive lookup
+ // this should apply in most cases when username is input in identical case or from $_SESSION['username']
+ // Mac OS will find user case-insensitive, but that's fine as it doesn't then matter how $_SESSION['username'] is stored
+ $user = $this->get_user_config($username);
+ if($user) return $user;
+
+ // loop user dirs and make case-insensitive username comparison
+ foreach (glob(self::users_dir() . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
+ $arr = explode('/', $dir); // get basename, better than basename() in case of multibyte chars
+ $dirname = end($arr); // get basename, better than basename() in case of multibyte chars
+ // case-insensitive username matches user dir, get user config from $dirname with case in tact (for $_SESSION['username'])
+ if($lower_username === $this->lower($dirname)) return $this->get_user_config($dirname);
+ }
+ }
+
+ // get user config.php file for a specific user $dirname
+ private function get_user_config($dirname){
+ $user = U::uinclude("users/$dirname/config.php"); // return user config array
+ if(!$user) return; // exit if not found
+ // error if the user array does not contain password *required
+ if(empty($user['password'])) return $this->error('User does not have valid password');
+ // return user array merged with username, which is used for $_SESSION['login'] login_hash()
+ return array_replace($user, ['username' => $dirname]);
+ }
+
+ // creates a login hash unique for username/password/IP/user-agent/app-location
+ private function login_hash($user){
+ return md5($user['username'] . $user['password'] . $this->ip() . $this->server('HTTP_USER_AGENT') . __FILE__);
+ }
+
+ // compares strings with more secure hash_equals() function (PHP >= 5.6)
+ private function equals($secret, $user){
+ return function_exists('hash_equals') ? hash_equals($secret, $user) : $secret === $user;
+ }
+
+ // match passwords using password_verify() if password is encrypted else use plain equality matching for non-encrypted passwords
+ private function passwords_match($stored, $posted){
+ if(password_get_info($stored)['algoName'] === 'unknown') return $this->equals($stored, $posted);
+ return password_verify($posted, $stored);
+ }
+
+ // get client IP for login hash matching
+ private function ip(){
+ foreach(['HTTP_CLIENT_IP','HTTP_X_FORWARDED_FOR','HTTP_X_FORWARDED','HTTP_FORWARDED_FOR','HTTP_FORWARDED','REMOTE_ADDR'] as $key){
+ $ip = explode(',', $this->server($key))[0];
+ if($ip && filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
+ }
+ return ''; // return empty string if nothing found
+ }
+
+ // get $_SERVER parameters helpers
+ private function server($str){
+ return isset($_SERVER[$str]) ? $_SERVER[$str] : '';
+ }
+
+ // lowercase username for case-insensitive username validation uses mb_strtolower() if function exists
+ private function lower($str){
+ return function_exists('mb_strtolower') ? mb_strtolower($str) : strtolower($str);
+ }
+
+ // check if request is an action, in which case we return error instead of the form
+ private function action_request(){
+
+ // exit if !action (or action is "tests", which requires login from the form)
+ if(!U::get('action') || U::get('action') === 'tests') return false;
+
+ // return json error if request is POST
+ if($_SERVER['REQUEST_METHOD'] === 'POST') return Json::error('login');
+
+ // login error with login link
+ U::error('Please login to continue', 401);
+ }
+
+ // login page / output form html and exit
+ private function form() {
+
+ // get form alert caused by logout, invalid session or incorrect login, before we destroy sessions vars
+ $alert = $this->get_form_alert();
+
+ // destroy login-specific session vars on logout or if they are invalid / session_unset()
+ $this->clear_session();
+
+ // get login form page header
+ U::html_header('Login', 'page-login');
+
+ // login page html / check language and render form via javascript (blocks simple bots)
+ ?>
+
+