diff --git a/README.md b/README.md index cc22bc8..a804175 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,8 @@ USB-RAID-Array/ │ └── ... # původní technická dokumentace a podklady ├── tests/ │ ├── workspace_test.php -│ └── serverstats_test.php +│ ├── serverstats_test.php +│ └── remotepaths_test.php └── Web/ ├── index.php # přihlášení ├── config/config.php # databáze, SFTP a SSH @@ -127,6 +128,7 @@ Z kořene repozitáře: ```sh php tests/workspace_test.php php tests/serverstats_test.php +php tests/remotepaths_test.php node --check Web/js/theme.js node --check Web/js/ui.js node --check Web/content/ftp/js/index.js diff --git a/Web/config/config.php b/Web/config/config.php index e8bdd42..b71264f 100644 --- a/Web/config/config.php +++ b/Web/config/config.php @@ -3,6 +3,7 @@ // Autoload dependencies (phpseclib) require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__ . '/../includes/remotepaths.php'; use phpseclib3\Net\SFTP; use phpseclib3\Net\SSH2; @@ -56,17 +57,4 @@ function getSSHConnection(): SSH2 { return $ssh; } -/** Utility: Normalise a path */ -function normalizePath(string $path): string { - $parts = array_filter(explode('/', $path), fn($part) => $part !== '' && $part !== '.'); - $stack = []; - foreach ($parts as $part) { - if ($part === '..') { - array_pop($stack); - } else { - $stack[] = $part; - } - } - return '/' . implode('/', $stack); -} ?> diff --git a/Web/content/ftp/delete.php b/Web/content/ftp/delete.php index 73c02c4..c5ce30f 100644 --- a/Web/content/ftp/delete.php +++ b/Web/content/ftp/delete.php @@ -5,7 +5,7 @@ $sftp = initializeSFTP(); if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) { $itemToDelete = $_POST['delete']; - $parentDir = dirname($itemToDelete); + $parentDir = remoteDirname($itemToDelete); if ($parentDir === '/' || $parentDir === '.') { $parentDir = $defaultPath; diff --git a/Web/content/ftp/download.php b/Web/content/ftp/download.php index edcaeef..539ec52 100644 --- a/Web/content/ftp/download.php +++ b/Web/content/ftp/download.php @@ -41,7 +41,7 @@ try { $fullRemotePath = rtrim($currentPath, '/') . '/' . $file; $baseDirName = basename($remoteBasePath); - $relPathFromBase = substr($fullRemotePath, strlen(dirname($remoteBasePath)) + 1); + $relPathFromBase = substr($fullRemotePath, strlen(rtrim(remoteDirname($remoteBasePath), '/')) + 1); file_put_contents($logFile, "Processing: $fullRemotePath (relative: $relPathFromBase)\n", FILE_APPEND); $isDir = $sftp->is_dir($fullRemotePath); @@ -214,4 +214,4 @@ try { header("HTTP/1.1 500 Internal Server Error"); echo $errorMessage; } -?> \ No newline at end of file +?> diff --git a/Web/content/ftp/extract.php b/Web/content/ftp/extract.php index 689f087..f577316 100644 --- a/Web/content/ftp/extract.php +++ b/Web/content/ftp/extract.php @@ -24,7 +24,7 @@ $sftp = initializeSFTP(); // Process extraction request if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['archive'])) { $archivePath = $_POST['archive']; - $dirPath = dirname($archivePath); + $dirPath = remoteDirname($archivePath); $archiveName = basename($archivePath); $extension = pathinfo($archiveName, PATHINFO_EXTENSION); diff --git a/Web/content/ftp/index.php b/Web/content/ftp/index.php index 7c65be8..6f548e5 100644 --- a/Web/content/ftp/index.php +++ b/Web/content/ftp/index.php @@ -13,11 +13,12 @@ error_reporting(E_ALL); require_once __DIR__ . '/../../config/config.php'; -require_once __DIR__ . '/../../includes/workspace.php'; + require_once __DIR__ . '/../../includes/workspace.php'; $sftp = initializeSFTP(); + $defaultPath = normalizePath($defaultPath); $currentPath = isset($_GET['path']) ? normalizePath($_GET['path']) : $defaultPath; - if (strpos($currentPath, $defaultPath) !== 0) { + if ($currentPath !== $defaultPath && strpos($currentPath, rtrim($defaultPath, '/') . '/') !== 0) { $currentPath = $defaultPath; } @@ -188,30 +189,30 @@ require_once __DIR__ . '/../../includes/workspace.php'; if ($currentPath !== $defaultPath) : ?> - ↑ Parent folder + ↑ Parent folder - — + —
- +
- +
- + @@ -235,7 +236,7 @@ require_once __DIR__ . '/../../includes/workspace.php'; - + @@ -246,12 +247,12 @@ require_once __DIR__ . '/../../includes/workspace.php'; Audio - + - + PDF @@ -261,28 +262,28 @@ require_once __DIR__ . '/../../includes/workspace.php'; - stat($currentPath . '/' . $file)['size']) ?> + stat(rtrim($currentPath, '/') . '/' . $file)['size']) ?>
- +
- +
- +
- +
diff --git a/Web/content/ftp/open.php b/Web/content/ftp/open.php index 469a5d7..4a2fe58 100644 --- a/Web/content/ftp/open.php +++ b/Web/content/ftp/open.php @@ -52,7 +52,7 @@ if (!empty($filePath) && $sftp->file_exists($filePath) && !$sftp->is_dir($filePa $showLineNumbers = in_array(strtolower($extension), ['php', 'js', 'html', 'css', 'py', 'java', 'c', 'cpp', 'h', 'hpp', 'rb', 'sh', 'xml', 'json']); -$parentDir = dirname($filePath); +$parentDir = remoteDirname($filePath); ?> diff --git a/Web/content/ftp/pdf.php b/Web/content/ftp/pdf.php index 07d6c14..2e256e2 100644 --- a/Web/content/ftp/pdf.php +++ b/Web/content/ftp/pdf.php @@ -134,7 +134,7 @@ if (isset($_GET['stream'])) { 'title' => $fileName, 'description' => 'Browse your document without leaving the workspace.', 'connected' => true, - 'back' => 'index.php?path=' . urlencode(dirname($filePath)), + 'back' => 'index.php?path=' . urlencode(remoteDirname($filePath)), 'class' => 'pdf-page', ]); ?> diff --git a/Web/content/ftp/rename.php b/Web/content/ftp/rename.php index a385cbe..b44d93f 100644 --- a/Web/content/ftp/rename.php +++ b/Web/content/ftp/rename.php @@ -26,7 +26,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { die("Missing required information for renaming."); } - $dirPath = dirname($oldPath); + $dirPath = remoteDirname($oldPath); $oldName = basename($oldPath); $newPath = $dirPath . '/' . $newName; diff --git a/Web/content/ftp/upload.php b/Web/content/ftp/upload.php index 11d6ce0..a8d37f4 100644 --- a/Web/content/ftp/upload.php +++ b/Web/content/ftp/upload.php @@ -20,7 +20,7 @@ function createDirectoryRecursive($sftp, $path) { return true; } - $parent = dirname($path); + $parent = remoteDirname($path); if ($parent != '/' && !$sftp->is_dir($parent)) { createDirectoryRecursive($sftp, $parent); } @@ -91,7 +91,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!empty($relativePath)) { $fileName = basename($relativePath); - $dirPart = dirname($relativePath); + $dirPart = remoteDirname($relativePath); if ($dirPart !== '.' && $dirPart !== '') { $remoteDirPath = $currentPath . '/' . $dirPart; diff --git a/Web/content/ftp/view.php b/Web/content/ftp/view.php index b0c6a8f..bb4cc3f 100644 --- a/Web/content/ftp/view.php +++ b/Web/content/ftp/view.php @@ -38,7 +38,7 @@ if (isset($_POST['createShare'])) { if ($stmt->execute()) { $shareUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" . - dirname($_SERVER['PHP_SELF']) . "/shared.php?token=$token"; + remoteDirname($_SERVER['PHP_SELF']) . "/shared.php?token=$token"; $_SESSION['shareCreated'] = true; $_SESSION['shareUrl'] = $shareUrl; @@ -274,7 +274,7 @@ if (isset($_GET['stream'])) { 'title' => $fileName, 'description' => 'Preview your media, download a copy or create a shared link.', 'connected' => true, - 'back' => 'index.php?path=' . urlencode(dirname($filePath)), + 'back' => 'index.php?path=' . urlencode(remoteDirname($filePath)), 'class' => 'media-page', ]); ?> diff --git a/Web/includes/remotepaths.php b/Web/includes/remotepaths.php new file mode 100644 index 0000000..38f8cf8 --- /dev/null +++ b/Web/includes/remotepaths.php @@ -0,0 +1,24 @@ + $part !== '' && $part !== '.'); + $stack = []; + foreach ($parts as $part) { + if ($part === '..') { + array_pop($stack); + } else { + $stack[] = $part; + } + } + return '/' . implode('/', $stack); +} + +/** Like dirname(), but only '/' is a separator and the root stays '/'. */ +function remoteDirname(string $path): string { + $path = rtrim($path, '/'); + if ($path === '') return '/'; + $separator = strrpos($path, '/'); + if ($separator === false) return '.'; + $parent = rtrim(substr($path, 0, $separator), '/'); + return $parent === '' ? '/' : $parent; +} diff --git a/tests/remotepaths_test.php b/tests/remotepaths_test.php new file mode 100644 index 0000000..4ab0811 --- /dev/null +++ b/tests/remotepaths_test.php @@ -0,0 +1,31 @@ + '/', + '/etc' => '/', + '/etc/' => '/', + '/etc/file.txt' => '/etc', + '//etc//file.txt' => '//etc', + '/folder with spaces/item.txt' => '/folder with spaces', + 'folder/file.txt' => 'folder', + 'file.txt' => '.', + '/a\\b/file.txt' => '/a\\b', // Backslash is a valid POSIX filename character. +]; +foreach ($parents as $path => $expected) { + if (remoteDirname($path) !== $expected) throw new RuntimeException('Incorrect parent for ' . $path); +} +foreach (['/' => '/', '//etc/../' => '/', '/etc/../../' => '/', '/etc/./ssh/' => '/etc/ssh'] as $path => $expected) { + if (normalizePath($path) !== $expected) throw new RuntimeException('Incorrect normalized path for ' . $path); +} +// Round trips must remain correct on both Windows/XAMPP and Linux. +foreach (['/etc', '/mnt/raid', '/folder with spaces'] as $directory) { + $parent = normalizePath(remoteDirname($directory)); + if ($parent !== ($directory === '/mnt/raid' ? '/mnt' : '/')) throw new RuntimeException('Invalid navigation round trip'); +} +// ZIP paths keep the top-level directory name when it is directly under root. +foreach (['/etc' => '/etc/config.txt', '/mnt/raid' => '/mnt/raid/config.txt'] as $base => $file) { + $relative = substr($file, strlen(rtrim(remoteDirname($base), '/')) + 1); + if ($relative !== ($base === '/etc' ? 'etc/config.txt' : 'raid/config.txt')) throw new RuntimeException('Incorrect ZIP member path'); +} +echo "Remote paths: POSIX parents, normalization, root navigation and ZIP paths passed.\n"; diff --git a/tests/workspace_test.php b/tests/workspace_test.php index 7daa995..32b93c5 100644 --- a/tests/workspace_test.php +++ b/tests/workspace_test.php @@ -1,5 +1,6 @@ 'michal', 'admin' => $admin, 'upPer' => true, 'downPer' => true, 'delPer' => true]; - $defaultPath = '/mnt/raid'; $currentPath = '/mnt/raid/Workspace'; $filePath = $currentPath . '/Example.pdf'; $fileName = basename($filePath); $parentDir = $currentPath; $fileExtension = 'png'; $fileSize = 245760; $mimeType = 'image/png'; $isImage = true; $isVideo = false; $isAudio = false; @@ -75,6 +75,12 @@ foreach ($pages as $name) { $member = renderWorkspaceFixture('index', false); expectWorkspace(strpos($member, 'User management') === false && strpos($member, '> Server status') === false, 'Member navigation omits admin pages'); expectWorkspace(strpos($member, 'Account settings') !== false, 'Members can access account settings'); +$rootChild = renderWorkspaceFixture('index', true, '/', '/etc'); +expectWorkspace(strpos($rootChild, 'href="?path=%2F"') !== false, 'Parent link from a root child uses a POSIX slash'); +expectWorkspace(strpos($rootChild, '%5C') === false, 'No Windows separator in remote links'); +$root = renderWorkspaceFixture('index', true, '/', '/'); +expectWorkspace(strpos($root, 'Parent folder') === false, 'Root has no parent-folder link'); +expectWorkspace(strpos($root, 'href="?path=%2FDocuments"') !== false, 'Root children use a single leading slash'); if ($preview) { // One-page PDF fixture for viewer navigation and fullscreen checks. $stream = "BT /F1 24 Tf 60 700 Td (USB RAID workspace preview) Tj ET";