diff --git a/build-plugin.sh b/build-plugin.sh index cf862e2..bcbf832 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r040" +VERSION="2026.06.23.r041" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r040.tgz b/packages/u-navigator-2026.06.23.r040.tgz deleted file mode 100644 index 299a637..0000000 Binary files a/packages/u-navigator-2026.06.23.r040.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r041.tgz b/packages/u-navigator-2026.06.23.r041.tgz new file mode 100644 index 0000000..4be5f6b Binary files /dev/null and b/packages/u-navigator-2026.06.23.r041.tgz differ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/download.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/download.php new file mode 100644 index 0000000..6d2416b --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/download.php @@ -0,0 +1,122 @@ +open($zipFile, ZipArchive::OVERWRITE) !== true) { + @unlink($zipFile); + unav_download_error(500, 'Could not create ZIP archive'); +} + +$usedNames = []; +foreach ($items as $item) { + $baseName = unav_zip_unique_name(unav_zip_safe_name(basename($item['path'])), $usedNames); + unav_zip_add_path($zip, $item['path'], $baseName); +} +$zip->close(); + +$downloadName = count($items) === 1 ? unav_zip_safe_name(basename($items[0]['path'])) . '.zip' : 'u-navigator-download.zip'; +unav_stream_file($zipFile, $downloadName, true); + +function unav_download_paths(): array { + if (isset($_GET['paths'])) { + $decoded = json_decode((string)$_GET['paths'], true); + if (!is_array($decoded)) { + unav_download_error(400, 'Invalid paths'); + } + $paths = array_values(array_filter($decoded, fn($item) => is_string($item) && $item !== '')); + } else { + $paths = [$_GET['path'] ?? '']; + } + if (!$paths) { + unav_download_error(400, 'Missing path'); + } + return array_slice($paths, 0, 200); +} + +function unav_stream_file(string $path, string $name, bool $deleteAfter = false): void { + if (!is_file($path) || !is_readable($path)) { + if ($deleteAfter) { + @unlink($path); + } + unav_download_error(404, 'Download file not found'); + } + header('Content-Type: application/octet-stream'); + header('Content-Length: ' . filesize($path)); + header('Content-Disposition: attachment; filename="' . addcslashes($name, '"\\') . '"'); + header('X-Content-Type-Options: nosniff'); + readfile($path); + if ($deleteAfter) { + @unlink($path); + } + exit; +} + +function unav_zip_add_path(ZipArchive $zip, string $path, string $zipPath): void { + if (is_link($path)) { + return; + } + $real = realpath($path); + if ($real === false) { + return; + } + unav_root_for_real($real); + + if (is_file($real)) { + $zip->addFile($real, $zipPath); + return; + } + if (!is_dir($real)) { + return; + } + $zip->addEmptyDir($zipPath); + foreach (scandir($real) ?: [] as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + unav_zip_add_path($zip, $real . '/' . $entry, $zipPath . '/' . unav_zip_safe_name($entry)); + } +} + +function unav_zip_safe_name(string $name): string { + $safe = trim(str_replace(["\0", '/', '\\'], '-', $name)); + return $safe === '' || $safe === '.' || $safe === '..' ? 'download' : $safe; +} + +function unav_zip_unique_name(string $name, array &$used): string { + $candidate = $name; + $index = 2; + while (isset($used[$candidate])) { + $candidate = $name . ' ' . $index; + $index++; + } + $used[$candidate] = true; + return $candidate; +} + +function unav_download_error(int $status, string $message): void { + http_response_code($status); + header('Content-Type: text/plain; charset=utf-8'); + echo $message; + exit; +} diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php index f22f4e5..cf81f2a 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php @@ -12,11 +12,22 @@ foreach ($_FILES['files']['name'] ?? [] as $index => $name) { if ($_FILES['files']['error'][$index] !== UPLOAD_ERR_OK) { unav_error(400, 'Upload failed'); } - $safeName = basename($name); + $safeName = unav_upload_safe_relative_path((string)$name); + $relativeParent = dirname($safeName); + if ($relativeParent !== '.') { + $parentCandidate = $target['path'] . '/' . $relativeParent; + unav_root_for_real($target['path']); + if (!is_dir($parentCandidate) && !mkdir($parentCandidate, 0777, true) && !is_dir($parentCandidate)) { + unav_error(500, 'Could not create upload directory'); + } + } $destination = unav_destination_path($target['path'] . '/' . $safeName); if (file_exists($destination['path'])) { unav_error(409, 'Destination already exists'); } + if (!is_dir(dirname($destination['path']))) { + unav_error(500, 'Could not create upload directory'); + } if (!move_uploaded_file($_FILES['files']['tmp_name'][$index], $destination['path'])) { unav_error(500, 'Could not store uploaded file'); } @@ -27,3 +38,18 @@ foreach ($_FILES['files']['name'] ?? [] as $index => $name) { } unav_json(['uploaded' => $uploaded], 201); + +function unav_upload_safe_relative_path(string $name): string { + $parts = []; + foreach (preg_split('#[/\\\\]+#', $name) ?: [] as $part) { + $part = trim($part); + if ($part === '' || $part === '.' || $part === '..') { + continue; + } + $parts[] = str_replace("\0", '', $part); + } + if (!$parts) { + unav_error(400, 'Invalid upload filename'); + } + return implode('/', $parts); +} diff --git a/server/public/app.js b/server/public/app.js index ee89678..fc659e7 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -270,6 +270,10 @@ function renderExplorer(body, win) { + + + +