feat(wiki): Kategorienvererbung für betriebsrat_editable, Ajax-Absicherung

- is_category_betriebsrat_editable(): läuft den Kategoriebaum nach oben durch
  (via knowledgecenter_category_links), sodass Unterkategorien einer
  freigegebenen Kategorie automatisch ebenfalls als editierbar gelten
- ajax_is_category_editable_for_br(): gleiche Logik für den Ajax-Kontext
- ajax_can_edit_post(): nutzt jetzt Vererbung statt direkter Kategorienprüfung
- create_post_draft(): ajax_has_wiki_edit()-Check hinzugefügt
- update_post_version(): Permission-Check mit Sonderfall neue Drafts (noch
  keine Kategorien → Prüfung auf has_wiki_edit begrenzt)
- update_post_categories(): Betriebsrat darf nur freigegebene Kategorien
  (inkl. Vererbung) zuweisen
- upload_inline_image.php: Bild-Upload prüft ebenfalls den Kategoriebaum

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 08:08:59 +02:00
parent a0698d621e
commit 4d6cc807c9
3 changed files with 140 additions and 17 deletions

View File

@@ -33,26 +33,56 @@ function ajax_has_wiki_edit(): bool
|| get_permission_state('r-wiki-br', $mid, $uid) == 1; || get_permission_state('r-wiki-br', $mid, $uid) == 1;
} }
// Betriebsrat darf einen Post nur bearbeiten, wenn er in einer freigegebenen Kategorie liegt. // Prüft ob eine Kategorie (oder ein Vorfahre) betriebsrat_editable=1 hat.
// Gibt false zurück wenn Bearbeitung verboten, true wenn erlaubt. function ajax_is_category_editable_for_br(int $categoryId): bool
{
$visited = [];
$queue = [$categoryId];
while (!empty($queue)) {
$current = array_shift($queue);
if (isset($visited[$current])) {
continue;
}
$visited[$current] = true;
$res = mysqli_query($GLOBALS['mysql_con'],
"SELECT betriebsrat_editable FROM knowledgecenter_categories_update WHERE id = $current LIMIT 1");
$row = mysqli_fetch_assoc($res);
if ($row && (int)$row['betriebsrat_editable'] === 1) {
return true;
}
$pRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT parent_category_id FROM knowledgecenter_category_links WHERE child_category_id = $current");
while ($pRow = mysqli_fetch_assoc($pRes)) {
$pid = (int)$pRow['parent_category_id'];
if (!isset($visited[$pid])) {
$queue[] = $pid;
}
}
}
return false;
}
// Betriebsrat darf einen Post nur bearbeiten, wenn er in einer freigegebenen
// Kategorie (oder Unterkategorie einer freigegebenen Kategorie) liegt.
function ajax_can_edit_post(int $postId): bool function ajax_can_edit_post(int $postId): bool
{ {
$mid = $GLOBALS['main_contact']['master_mandant_id'] ?? 0; $mid = $GLOBALS['main_contact']['master_mandant_id'] ?? 0;
$uid = $GLOBALS['main_contact']['id'] ?? 0; $uid = $GLOBALS['main_contact']['id'] ?? 0;
if (get_permission_state('r-wiki', $mid, $uid) == 1) { if (get_permission_state('r-wiki', $mid, $uid) == 1) {
return true; // Redakteur darf immer return true;
} }
if (get_permission_state('r-wiki-br', $mid, $uid) != 1) { if (get_permission_state('r-wiki-br', $mid, $uid) != 1) {
return false; // Kein Wiki-Recht return false;
} }
// Betriebsrat: Post muss in betriebsrat_editable Kategorie liegen // Betriebsrat: prüfe alle dem Post zugeordneten Kategorien inkl. Vorfahren
$res = mysqli_query($GLOBALS['mysql_con'], $res = mysqli_query($GLOBALS['mysql_con'],
"SELECT COUNT(*) AS cnt "SELECT category_id FROM knowledgecenter_category_post WHERE post_id = $postId");
FROM knowledgecenter_category_post kcp while ($row = mysqli_fetch_assoc($res)) {
JOIN knowledgecenter_categories_update kcu ON kcu.id = kcp.category_id if (ajax_is_category_editable_for_br((int)$row['category_id'])) {
WHERE kcp.post_id = $postId AND kcu.betriebsrat_editable = 1"); return true;
$row = mysqli_fetch_assoc($res); }
return $row && (int)$row['cnt'] > 0; }
return false;
} }
// AJAX-Handler: Falls per POST eine Aktion übergeben wurde, diese verarbeiten und das Skript beenden. // AJAX-Handler: Falls per POST eine Aktion übergeben wurde, diese verarbeiten und das Skript beenden.
@@ -719,6 +749,22 @@ function update_post_version()
echo json_encode(['error' => 'Ungültige Beitrags-ID']); echo json_encode(['error' => 'Ungültige Beitrags-ID']);
exit; exit;
} }
if (!ajax_has_wiki_edit()) {
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
// Betriebsrat darf bestehende Posts nur bearbeiten wenn der Post in einer freigegebenen Kategorie liegt.
// Neue Drafts haben noch keine Kategorien → Prüfung überspringen (Zugang wurde schon bei create_post_draft gesichert).
if ($postId > 0) {
$catCountRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT COUNT(*) AS cnt FROM knowledgecenter_category_post WHERE post_id = $postId");
$catCountRow = mysqli_fetch_assoc($catCountRes);
if ((int)$catCountRow['cnt'] > 0 && !ajax_can_edit_post($postId)) {
echo json_encode(['error' => 'Keine Berechtigung für diesen Beitrag']);
exit;
}
}
$keepModifiedAt = isset($_POST['keep_modified_at']) && (int) $_POST['keep_modified_at'] === 1; $keepModifiedAt = isset($_POST['keep_modified_at']) && (int) $_POST['keep_modified_at'] === 1;
$backgroundColor = normalize_hex_color($_POST['background_color'] ?? '#FFFFFF', '#FFFFFF'); $backgroundColor = normalize_hex_color($_POST['background_color'] ?? '#FFFFFF', '#FFFFFF');
@@ -1062,6 +1108,10 @@ function delete_post_gallery_dir($postId)
function create_post_draft() function create_post_draft()
{ {
if (!ajax_has_wiki_edit()) {
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
$insertPostQuery = "INSERT INTO knowledgecenter_posts (created_at) VALUES (NOW())"; $insertPostQuery = "INSERT INTO knowledgecenter_posts (created_at) VALUES (NOW())";
if (!mysqli_query($GLOBALS['mysql_con'], $insertPostQuery)) { if (!mysqli_query($GLOBALS['mysql_con'], $insertPostQuery)) {
echo json_encode(['error' => 'Fehler beim Erstellen des Entwurfs: ' . mysqli_error($GLOBALS['mysql_con'])]); echo json_encode(['error' => 'Fehler beim Erstellen des Entwurfs: ' . mysqli_error($GLOBALS['mysql_con'])]);
@@ -1276,6 +1326,27 @@ function update_post_categories()
exit; exit;
} }
$mid = $GLOBALS['main_contact']['master_mandant_id'] ?? 0;
$uid = $GLOBALS['main_contact']['id'] ?? 0;
$isRedakteur = get_permission_state('r-wiki', $mid, $uid) == 1;
$isBetriebsrat = get_permission_state('r-wiki-br', $mid, $uid) == 1;
if (!$isRedakteur && !$isBetriebsrat) {
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
// Betriebsrat darf nur Kategorien zuweisen, die für ihn freigegeben sind (inkl. Vererbung)
if (!$isRedakteur && $isBetriebsrat) {
foreach ($categories as $catId) {
$catId = (int)$catId;
if ($catId > 0 && !ajax_is_category_editable_for_br($catId)) {
echo json_encode(['error' => "Kategorie $catId ist nicht für den Betriebsrat freigegeben"]);
exit;
}
}
}
// Bestehende Einträge für diesen Beitrag löschen // Bestehende Einträge für diesen Beitrag löschen
$deleteQuery = "DELETE FROM knowledgecenter_category_post WHERE post_id = $postId"; $deleteQuery = "DELETE FROM knowledgecenter_category_post WHERE post_id = $postId";
if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) { if (!mysqli_query($GLOBALS['mysql_con'], $deleteQuery)) {

View File

@@ -74,14 +74,36 @@ if (!$verifyQ || mysqli_num_rows($verifyQ) === 0) {
respond_error('Beitrag existiert nicht.', 404); respond_error('Beitrag existiert nicht.', 404);
} }
// Betriebsrat darf Bilder nur in freigegebene Kategorien hochladen // Betriebsrat darf Bilder nur für Posts in freigegebenen Kategorien hochladen (inkl. Vererbung)
if (!$hasRwiki && $hasRwikiBr) { if (!$hasRwiki && $hasRwikiBr) {
$brAllowed = false;
$brRes = mysqli_query($GLOBALS['mysql_con'], $brRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT COUNT(*) AS cnt FROM knowledgecenter_category_post kcp "SELECT category_id FROM knowledgecenter_category_post WHERE post_id = $postId");
JOIN knowledgecenter_categories_update kcu ON kcu.id = kcp.category_id while ($brRow = mysqli_fetch_assoc($brRes)) {
WHERE kcp.post_id = $postId AND kcu.betriebsrat_editable = 1"); $catId = (int)$brRow['category_id'];
$brRow = mysqli_fetch_assoc($brRes); // Baum nach oben absuchen
if (!$brRow || (int)$brRow['cnt'] === 0) { $visited = [];
$queue = [$catId];
while (!empty($queue)) {
$current = array_shift($queue);
if (isset($visited[$current])) continue;
$visited[$current] = true;
$chkRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT betriebsrat_editable FROM knowledgecenter_categories_update WHERE id = $current LIMIT 1");
$chkRow = mysqli_fetch_assoc($chkRes);
if ($chkRow && (int)$chkRow['betriebsrat_editable'] === 1) {
$brAllowed = true;
break 2;
}
$pRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT parent_category_id FROM knowledgecenter_category_links WHERE child_category_id = $current");
while ($pRow = mysqli_fetch_assoc($pRes)) {
$pid = (int)$pRow['parent_category_id'];
if (!isset($visited[$pid])) $queue[] = $pid;
}
}
}
if (!$brAllowed) {
respond_error('Keine Berechtigung für diesen Beitrag.', 403); respond_error('Keine Berechtigung für diesen Beitrag.', 403);
} }
} }

View File

@@ -867,6 +867,36 @@ function require_wiki_edit_permission(): void
} }
} }
// Prüft ob eine Kategorie (oder irgendein Vorfahre über knowledgecenter_category_links)
// betriebsrat_editable=1 hat. Läuft den Baum nach oben durch.
function is_category_betriebsrat_editable(int $categoryId): bool
{
$visited = [];
$queue = [$categoryId];
while (!empty($queue)) {
$current = array_shift($queue);
if (isset($visited[$current])) {
continue;
}
$visited[$current] = true;
$res = mysqli_query($GLOBALS['mysql_con'],
"SELECT betriebsrat_editable FROM knowledgecenter_categories_update WHERE id = $current LIMIT 1");
$row = mysqli_fetch_assoc($res);
if ($row && (int)$row['betriebsrat_editable'] === 1) {
return true;
}
$pRes = mysqli_query($GLOBALS['mysql_con'],
"SELECT parent_category_id FROM knowledgecenter_category_links WHERE child_category_id = $current");
while ($pRow = mysqli_fetch_assoc($pRes)) {
$pid = (int)$pRow['parent_category_id'];
if (!isset($visited[$pid])) {
$queue[] = $pid;
}
}
}
return false;
}
// Switch-Case für die verschiedenen Aktionen // Switch-Case für die verschiedenen Aktionen
$action = $_GET["action"] ?? "List"; $action = $_GET["action"] ?? "List";
switch ($action) { switch ($action) {