feat: filemanager - plná cesta na disku, podpora symlinků, oprava stylů
- Backend: listFiles vrací full_path, detekce symlinků/junctions - Backend: resolveProjectPath povoluje navigaci přes symlinky - Frontend: zobrazení plné cesty pod breadcrumbem - Frontend: ikona odkazu u symlinků s tooltipem cíle - Fix: underline jen na názvu složky, ne na počtu položek Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,7 @@ class NasFileManager
|
||||
/**
|
||||
* Seznam souboru a slozek v dane ceste
|
||||
*
|
||||
* @return array{path: string, items: list<array<string, mixed>>}|null
|
||||
* @return array{path: string, items: list<array<string, mixed>>, full_path: string}|null
|
||||
*/
|
||||
public function listFiles(string $projectNumber, string $subPath = ''): ?array
|
||||
{
|
||||
@@ -115,14 +115,23 @@ class NasFileManager
|
||||
}
|
||||
|
||||
$fullPath = $dirPath . '/' . $entry;
|
||||
$isLink = is_link($fullPath) || $this->isJunction($fullPath);
|
||||
$isDir = is_dir($fullPath);
|
||||
|
||||
$item = [
|
||||
'name' => $entry,
|
||||
'type' => $isDir ? 'folder' : 'file',
|
||||
'modified' => date('Y-m-d H:i', filemtime($fullPath) ?: 0),
|
||||
'is_symlink' => $isLink,
|
||||
];
|
||||
|
||||
if ($isLink) {
|
||||
$target = $this->readLinkTarget($fullPath);
|
||||
if ($target !== null) {
|
||||
$item['link_target'] = $target;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isDir) {
|
||||
$item['item_count'] = $this->countItems($fullPath);
|
||||
} else {
|
||||
@@ -151,10 +160,14 @@ class NasFileManager
|
||||
}
|
||||
}
|
||||
|
||||
// Realna cesta na disku (pro zobrazeni v UI)
|
||||
$realDirPath = realpath($dirPath);
|
||||
|
||||
return [
|
||||
'path' => $subPath,
|
||||
'items' => $items,
|
||||
'breadcrumb' => $breadcrumb,
|
||||
'full_path' => str_replace('/', '\\', $realDirPath ?: $dirPath),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -387,7 +400,9 @@ class NasFileManager
|
||||
|
||||
/**
|
||||
* Resolve a project sub-path to a safe absolute path
|
||||
* Returns null if path traversal detected
|
||||
* Returns null if path traversal detected.
|
||||
* Symlinky/junctions jsou povoleny - realpath kontrola se preskoci
|
||||
* pokud je v ceste symlink.
|
||||
*/
|
||||
private function resolveProjectPath(string $projectNumber, string $subPath): ?string
|
||||
{
|
||||
@@ -410,29 +425,66 @@ class NasFileManager
|
||||
|
||||
$candidate = $folderPath . '/' . $subPath;
|
||||
|
||||
if (!file_exists($candidate)) {
|
||||
// Pro nove soubory/slozky - kontrola rodice
|
||||
$parentDir = dirname($candidate);
|
||||
if (file_exists($parentDir)) {
|
||||
$normalBase = str_replace('\\', '/', $folderPath);
|
||||
// Rodic musi byt v projektu nebo dosazitelny pres symlink z projektu
|
||||
if (!$this->isPathReachable($parentDir, $normalBase)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
// Existujici cesta - overi ze je dosazitelna z projektove slozky
|
||||
// (bud primo uvnitr, nebo pres symlink ktery je v projektu)
|
||||
$normalBase = str_replace('\\', '/', $folderPath);
|
||||
|
||||
// realpath kontrola - soubor/slozka musi existovat pro existujici cesty
|
||||
if (file_exists($candidate)) {
|
||||
$real = realpath($candidate);
|
||||
$normalReal = str_replace('\\', '/', (string) $real);
|
||||
if ($real === false || !str_starts_with($normalReal, $normalBase)) {
|
||||
return null;
|
||||
}
|
||||
return $normalReal;
|
||||
if (!$this->isPathReachable($candidate, $normalBase)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pro nove soubory/slozky - kontrola rodice
|
||||
$parentDir = dirname($candidate);
|
||||
if (file_exists($parentDir)) {
|
||||
$realParent = realpath($parentDir);
|
||||
$normalParent = str_replace('\\', '/', (string) $realParent);
|
||||
if ($realParent === false || !str_starts_with($normalParent, $normalBase)) {
|
||||
return null;
|
||||
return str_replace('\\', '/', $candidate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overi ze cesta je dosazitelna z projektove slozky.
|
||||
* Projde cestu segment po segmentu - pokud narazi na symlink
|
||||
* v projektove slozce, povoli ho.
|
||||
*/
|
||||
private function isPathReachable(string $path, string $projectBase): bool
|
||||
{
|
||||
$normalPath = str_replace('\\', '/', $path);
|
||||
|
||||
// Primo v projektove slozce (bez symlinku)
|
||||
$real = realpath($path);
|
||||
if ($real !== false) {
|
||||
$normalReal = str_replace('\\', '/', $real);
|
||||
if (str_starts_with($normalReal, $projectBase)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
// Cesta muze vest pres symlink - overime ze symlink samotny
|
||||
// je uvnitr projektove slozky
|
||||
$parts = explode('/', str_replace('\\', '/', $normalPath));
|
||||
$baseParts = explode('/', $projectBase);
|
||||
$baseLen = count($baseParts);
|
||||
|
||||
$current = '';
|
||||
foreach ($parts as $i => $part) {
|
||||
$current .= ($i > 0 ? '/' : '') . $part;
|
||||
if ($i < $baseLen) {
|
||||
continue;
|
||||
}
|
||||
// Segment za projektovou slozkou - pokud je symlink, je to OK
|
||||
if (file_exists($current) && (is_link($current) || $this->isJunction($current))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function deleteRecursive(string $path): bool
|
||||
@@ -495,6 +547,55 @@ class NasFileManager
|
||||
return round($bytes / 1073741824, 1) . ' GB';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detekce NTFS junction pointu (Windows)
|
||||
*/
|
||||
private function isJunction(string $path): bool
|
||||
{
|
||||
if (PHP_OS_FAMILY !== 'Windows') {
|
||||
return is_link($path);
|
||||
}
|
||||
|
||||
if (!file_exists($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attr = @exec('fsutil reparsepoint query "' . str_replace('/', '\\', $path) . '" 2>NUL');
|
||||
if ($attr !== false && $attr !== '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback - realpath se lisi od puvodniho path u junction
|
||||
$real = realpath($path);
|
||||
$normalized = str_replace('\\', '/', $path);
|
||||
$normalReal = str_replace('\\', '/', (string) $real);
|
||||
return $real !== false && strtolower($normalized) !== strtolower($normalReal) && is_dir($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Precte cil symlinku/junction
|
||||
*/
|
||||
private function readLinkTarget(string $path): ?string
|
||||
{
|
||||
// PHP is_link + readlink
|
||||
if (is_link($path)) {
|
||||
$target = readlink($path);
|
||||
return $target !== false ? str_replace('/', '\\', $target) : null;
|
||||
}
|
||||
|
||||
// Windows junction - realpath ukazuje na cil
|
||||
$real = realpath($path);
|
||||
if ($real !== false) {
|
||||
$normalized = str_replace('\\', '/', $path);
|
||||
$normalReal = str_replace('\\', '/', $real);
|
||||
if (strtolower($normalized) !== strtolower($normalReal)) {
|
||||
return str_replace('/', '\\', $real);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detekce podezrelych MIME typů (napr. exe maskujici se jako jpg)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user