Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Authorization\CapabilityChecker;
/**
* Deckt den Weg zu einem zugänglichen Themengebiet ab.
*
* Liegt ein lesbares Themengebiet tief im Baum, müssen die übergeordneten
* Gebiete als Weg sichtbar sein – ohne dass ihre eigenen Seiten zugänglich
* werden. Deshalb steht der Weg bewusst neben der Liste der zugänglichen
* Gebiete, an der Suche, Export und KI-Kontext hängen.
*/
return static function (): void {
/**
* Datenbank-Doppel für einen Benutzer mit einer standortgebundenen Rolle.
*
* @param array<int,array{path:string,locs:int[]}> $themen
* @param int[] $zugewieseneStandorte
*/
$db = function (array $themen, array $zugewieseneStandorte): object {
return new class ($themen, $zugewieseneStandorte) extends \OpenNIT\Core\Database {
public function __construct(private array $themen, private array $standorte)
{
// Verbindung bewusst nicht aufbauen – die Abfragen werden bedient.
}
public function fetchColumn(string $sql, array $params = [], int $column = 0): mixed
{
if (str_contains($sql, 'INFORMATION_SCHEMA')) {
return 1; // scope_type ist vorhanden
}
if (str_contains($sql, 'SELECT is_active FROM users')) {
return 1;
}
if (str_contains($sql, 'SELECT is_admin FROM users')) {
return 0; // kein globaler Admin
}
return 0; // keine globale/themengebundene Zuweisung
}
public function fetchAll(string $sql, array $params = []): array
{
if (str_contains($sql, 'urt.location_id FROM user_role_topics')) {
return array_map(static fn($id) => ['location_id' => $id], $this->standorte);
}
if (str_contains($sql, 'user_role_topics')) {
return []; // keine themengebundene Zuweisung
}
if (str_contains($sql, 'FROM ticket_locations')) {
return [['id' => 1, 'parent_id' => null], ['id' => 2, 'parent_id' => null]];
}
if (str_contains($sql, 'FROM topic_locations')) {
$rows = [];
foreach ($this->themen as $id => $t) {
foreach ($t['locs'] as $loc) {
$rows[] = ['topic_id' => $id, 'location_id' => $loc];
}
}
return $rows;
}
if (str_contains($sql, 'FROM topics')) {
$rows = [];
foreach ($this->themen as $id => $t) {
$rows[] = [
'id' => $id,
'name' => 'Thema ' . $id,
'path' => $t['path'],
'location_id' => $t['locs'][0] ?? null,
];
}
// Die Abfrage mit IN-Liste liefert nur die angefragten Gebiete.
if (str_contains($sql, 'id IN (')) {
$erlaubt = array_fill_keys(array_map('intval', $params), true);
return array_values(array_filter($rows, static fn($r) => isset($erlaubt[$r['id']])));
}
return $rows;
}
return [];
}
};
};
// Kunde Müller (Standort 1) mit tief liegender gemeinsamer Anleitung, die
// auch Kunde Schulze (Standort 2) zugeordnet ist.
$themen = [
10 => ['path' => '/10/', 'locs' => [1]],
11 => ['path' => '/10/11/', 'locs' => []],
12 => ['path' => '/10/11/12/', 'locs' => [1, 2]],
20 => ['path' => '/20/', 'locs' => [2]],
];
test('Der Weg zu einem zugänglichen Themengebiet ist sichtbar', function () use ($db, $themen) {
$checker = new CapabilityChecker($db($themen, [2]));
$zugaenglich = array_column($checker->getTopicsWithCapability(7, 'read_page'), 'id');
sort($zugaenglich);
assertSame([12, 20], $zugaenglich, 'Die gemeinsame Anleitung und der eigene Bereich');
$weg = $checker->getPathTopicIds(7, 'read_page');
sort($weg);
assertSame([10, 11], $weg, 'Beide übergeordneten Gebiete führen zur Anleitung');
});
test('Der Weg enthält nie ein ohnehin zugängliches Themengebiet', function () use ($db, $themen) {
$checker = new CapabilityChecker($db($themen, [1]));
$zugaenglich = array_column($checker->getTopicsWithCapability(7, 'read_page'), 'id');
sort($zugaenglich);
assertSame([10, 11, 12], $zugaenglich, 'Kunde Müller sieht seinen ganzen Zweig');
assertSame([], $checker->getPathTopicIds(7, 'read_page'), 'Kein Weg nötig, alles ist zugänglich');
});
test('Ohne zugängliches Themengebiet gibt es auch keinen Weg', function () use ($db, $themen) {
$checker = new CapabilityChecker($db($themen, [99]));
assertSame([], $checker->getTopicsWithCapability(7, 'read_page'));
assertSame([], $checker->getPathTopicIds(7, 'read_page'));
});
};