diff --git a/config.php b/config.php new file mode 100644 index 00000000..c3571026 --- /dev/null +++ b/config.php @@ -0,0 +1,149 @@ + 'Password', 'Username2' => 'Password2', ...) +// Generate secure password hash - https://tinyfilemanager.github.io/docs/pwd.html +$auth_users = array( + 'admin' => '$2y$10$jx9DL2I3x3nS6wmGTTete.wlPNtIddwlvySXYS2zjoPRGyhinbf1W', // admin@123 + 'user0' => '$2y$10$pk.6o4/CCS0kwaPp349hVeOKHPDSP871HQDzUuZbnG0On/3YbCh9C', // 12345 + 'user1' => '$2y$10$Bgj/8tbeamSe452sPXSK5eweR5rjIA/6cJPvCX85tvN2BED2VjO3m', // 12345 + 'user2' => '$2y$10$Ey26DNdAImdZ7MSvBUYNZ.Tym0BkZVm9RqnS/vzXn8VM5RejeWol.', // 12345 + 'user3' => '$2y$10$ZDTPfCO2nUWGhpkuTVCsKehbUuqq.xZJZ7RBFILRkgCWrgKGzgXM.', // user3 + 'user4' => '$2y$10$yDbhZeVYg9zMaZXDx7HTueVvmpMHNFp6Ru2GkSBWNJH0m03XVg0ES',// @ + 'user5' => '$2y$10$m2wUB5Kx3E6a/.BZ3VMUte5LzoLDJDpxNQ7qhffjtA9wLp28Qa3xa' //30116511 +); + +// Readonly users +// e.g. array('users', 'guest', ...) +$readonly_users = array( + 'guest' +); + +// Enable highlight.js (https://highlightjs.org/) on view's page +$use_highlightjs = true; + +// highlight.js style +// for dark theme use 'ir-black' +$highlightjs_style = 'vs'; + +// Enable ace.js (https://ace.c9.io/) on view's page +$edit_files = true; + +// Default timezone for date() and time() +// Doc - http://php.net/manual/en/timezones.php +$default_timezone = 'Etc/UTC'; // UTC + +// Root path for file manager +// use absolute path of directory i.e: '/var/www/folder' or $_SERVER['DOCUMENT_ROOT'].'/folder' +//$root_path = ''; // +$root_path = $_SERVER['DOCUMENT_ROOT'].'/nas';//'/'; // + +// Root url for links in file manager.Relative to $http_host. Variants: '', 'path/to/subfolder' +// Will not working if $root_path will be outside of server document root +$root_url = ''; + +// Server hostname. Can set manually if wrong +$http_host = $_SERVER['HTTP_HOST']; + +// user specific directories +// array('Username' => 'Directory path', 'Username2' => 'Directory path', ...) +$directories_users = +array( + 'admin' => '/', + 'user0' => '/mnt/mmcblk2p4', + 'user1' => '/mnt/mmcblk2p4/user1', + 'user2' => '/mnt/mmcblk2p4/user1/user2', + 'user3' => '/mnt/mmcblk2p4/user1/user3', + 'user4' => '/mnt/mmcblk2p4/user1/user4', + 'user5' => '/mnt/mmcblk2p4/user1/user5' +); // array(); + +// input encoding for iconv +$iconv_input_encoding = 'UTF-8'; + +// date() format for file modification date +// Doc - https://www.php.net/manual/en/datetime.format.php +$datetime_format = 'd.m.y H:i:s'; + +// Allowed file extensions for create and rename files +// e.g. 'txt,html,css,js' +$allowed_file_extensions = ''; + +// Allowed file extensions for upload files +// e.g. 'gif,png,jpg,html,txt' +$allowed_upload_extensions = ''; + +// Favicon path. This can be either a full url to an .PNG image, or a path based on the document root. +// full path, e.g http://example.com/favicon.png +// local path, e.g images/icons/favicon.png +$favicon_path = '/nas/favicon.ico'; + +// Files and folders to excluded from listing +// e.g. array('myfile.html', 'personal-folder', '*.php', ...) +$exclude_items = array(''); + +// Online office Docs Viewer +// Availabe rules are 'google', 'microsoft' or false +// google => View documents using Google Docs Viewer +// microsoft => View documents using Microsoft Web Apps Viewer +// false => disable online doc viewer +$online_viewer = 'google'; + +// Sticky Nav bar +// true => enable sticky header +// false => disable sticky header +$sticky_navbar = true; + +// Path display mode when viewing file information +// 'full' => show full path +// 'relative' => show path relative to root_path +// 'host' => show path on the host +$path_display_mode = 'full'; + +// max upload file size +$max_upload_size_bytes = 5000; + +// Possible rules are 'OFF', 'AND' or 'OR' +// OFF => Don't check connection IP, defaults to OFF +// AND => Connection must be on the whitelist, and not on the blacklist +// OR => Connection must be on the whitelist, or not on the blacklist +$ip_ruleset = 'OFF'; + +// Should users be notified of their block? +$ip_silent = true; + +// IP-addresses, both ipv4 and ipv6 +$ip_whitelist = array( + '127.0.0.1', // local ipv4 + '::1' // local ipv6 +); + +// IP-addresses, both ipv4 and ipv6 +$ip_blacklist = array( + '0.0.0.0', // non-routable meta ipv4 + '::' // non-routable meta ipv6 +); + +// **新增:是否信任反向代理的 IP 头 (例如 Cloudflare 或 Nginx)** +// false (默认/最安全):只使用 REMOTE_ADDR,防止 IP 伪造。 +// true (使用代理时):允许读取 HTTP_X_FORWARDED_FOR 等头,获取真实客户端 IP。 +$trust_proxy = false; + +?> \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 00000000..b38c98a6 Binary files /dev/null and b/favicon.ico differ diff --git a/tinyfilemanager.php b/tinyfilemanager.php index e6633e7f..bd318558 100644 --- a/tinyfilemanager.php +++ b/tinyfilemanager.php @@ -1,4 +1,40 @@ "; + // 在弹窗中显示消息,\n 是换行符 + echo "alert('--- 调试信息 ---\\n" . $safe_message . "');"; + echo ""; + + // 4. 根据参数决定是否终止脚本 + if ($terminate_script) { + // 强制终止后续 PHP 代码的执行 + die("脚本已终止,用于调试。"); + } +} + //Default Configuration $CONFIG = '{"lang":"en","error_reporting":false,"show_hidden":false,"hide_Cols":false,"theme":"light"}'; @@ -94,10 +130,10 @@ // Favicon path. This can be either a full url to an .PNG image, or a path based on the document root. // full path, e.g http://example.com/favicon.png // local path, e.g images/icons/favicon.png -$favicon_path = ''; +$favicon_path = '';//favicon.ico // Files and folders to excluded from listing -// e.g. array('myfile.html', 'personal-folder', '*.php', ...) +// e.g. array('myfile.html', 'personal-folder', '*.php', '/path/to/folder', ...) $exclude_items = array(); // Online office Docs Viewer @@ -142,6 +178,11 @@ '::' // non-routable meta ipv6 ); +// **新增:是否信任反向代理的 IP 头 (例如 Cloudflare 或 Nginx)** +// false (默认/最安全):只使用 REMOTE_ADDR,防止 IP 伪造。 +// true (使用代理时):允许读取 HTTP_X_FORWARDED_FOR 等头,获取真实客户端 IP。 +$trust_proxy = false; + // if User has the external config file, try to use it to override the default config above [config.php] // sample config - https://tinyfilemanager.github.io/config-sample.txt $config_file = __DIR__ . '/config.php'; @@ -151,6 +192,9 @@ // External CDN resources that can be used in the HTML (replace for GDPR compliance) $external = array( + 'css-photoswipe' => '', + 'js-photoswipe-lightbox' => 'https://cdn.jsdelivr.net/npm/photoswipe@5.4.3/dist/photoswipe-lightbox.esm.js', + 'js-photoswipe-module' => 'https://cdn.jsdelivr.net/npm/photoswipe@5.4.3/dist/photoswipe.esm.js', 'css-bootstrap' => '', 'css-dropzone' => '', 'css-font-awesome' => '', @@ -265,6 +309,7 @@ function session_error_handling_function($code, $msg, $file, $line) $wd = fm_clean_path(dirname($_SERVER['PHP_SELF'])); $root_url = $root_url . $wd . DIRECTORY_SEPARATOR . $directories_users[$_SESSION[FM_SESSION_ID]['logged']]; } + // clean $root_url $root_url = fm_clean_path($root_url); @@ -281,20 +326,59 @@ function session_error_handling_function($code, $msg, $file, $line) // Validate connection IP if ($ip_ruleset != 'OFF') { - function getClientIP() - { - if (array_key_exists('HTTP_CF_CONNECTING_IP', $_SERVER)) { - return $_SERVER["HTTP_CF_CONNECTING_IP"]; - } else if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) { - return $_SERVER["HTTP_X_FORWARDED_FOR"]; - } else if (array_key_exists('REMOTE_ADDR', $_SERVER)) { - return $_SERVER['REMOTE_ADDR']; - } else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) { - return $_SERVER['HTTP_CLIENT_IP']; +/** + * 获取客户端 IP 地址。 + * 修复:仅在信任反向代理时才读取 X-Forwarded-For 等 HTTP 头。 + * + * @return string 客户端 IP 地址 + */ +function getClientIP() { + // 【核心改动】使用 global 关键字引入配置变量 + global $trust_proxy; + + // 如果 $trust_proxy 变量未定义,默认为 false + $trust_proxy = isset($trust_proxy) ? (bool)$trust_proxy : false; + + // 如果未启用信任代理,则直接返回 REMOTE_ADDR + if (!$trust_proxy) { + if (array_key_exists('REMOTE_ADDR', $_SERVER)) { + return $_SERVER['REMOTE_ADDR']; } return ''; } + // --- 仅在信任代理时检查 HTTP 头 --- + + // 检查 Cloudflare 代理 IP + if (array_key_exists('HTTP_CF_CONNECTING_IP', $_SERVER)) { + return $_SERVER['HTTP_CF_CONNECTING_IP']; + } + + // 检查其他常见的反向代理头 (X-Forwarded-For) + if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) { + // 取第一个 IP (最左边的,通常是客户端真实 IP) + $ip = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]); + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + } + + // 检查 HTTP_CLIENT_IP + if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) { + return $_SERVER['HTTP_CLIENT_IP']; + } + + // 默认返回 REMOTE_ADDR + if (array_key_exists('REMOTE_ADDR', $_SERVER)) { + return $_SERVER['REMOTE_ADDR']; + } + + return ''; +} + + + + $clientIp = getClientIP(); $proceed = false; $whitelisted = in_array($clientIp, $ip_whitelist); @@ -411,17 +495,21 @@ function getClientIP() if ($use_auth && isset($_SESSION[FM_SESSION_ID]['logged'])) { $root_path = isset($directories_users[$_SESSION[FM_SESSION_ID]['logged']]) ? $directories_users[$_SESSION[FM_SESSION_ID]['logged']] : $root_path; } - // clean and check $root_path -$root_path = rtrim($root_path, '\\/'); +$root_path = trim($root_path); +//$root_path = rtrim($root_path, '\\/'); +$root_path = ($root_path === '/' )? $root_path : rtrim($root_path, '\\/'); $root_path = str_replace('\\', '/', $root_path); + if (!@is_dir($root_path)) { echo "

" . lng('Root path') . " \"{$root_path}\" " . lng('not found!') . "

"; exit; } + defined('FM_SHOW_HIDDEN') || define('FM_SHOW_HIDDEN', $show_hidden_files); defined('FM_ROOT_PATH') || define('FM_ROOT_PATH', $root_path); + // fm_set_msg(sprintf('%s **0',$root_path), 'alert'); defined('FM_LANG') || define('FM_LANG', $lang); defined('FM_FILE_EXTENSION') || define('FM_FILE_EXTENSION', $allowed_file_extensions); defined('FM_UPLOAD_EXTENSION') || define('FM_UPLOAD_EXTENSION', $allowed_upload_extensions); @@ -457,6 +545,284 @@ function getClientIP() unset($p, $use_auth, $iconv_input_encoding, $use_highlightjs, $highlightjs_style); /*************************** ACTIONS ***************************/ +/** + * 清理和验证路径,强制路径在 FM_ROOT_PATH 内部。 + * + * @param string $path 待检查的路径 + * @return string|false 成功返回绝对路径,失败则终止程序。 + */ +function sanitizePath($path) { + // 1. 初始检查,确保路径以 '/' 开头 + if (substr($path, 0, 1) !== '/') { + die('Invalid file path.'); + } + + // 2. 获取路径的真实绝对路径 + $realPath = realpath($path); + + // 3. 获取文件管理器根目录的真实绝对路径 (需要先定义 FM_ROOT_PATH) + // 假设 FM_ROOT_PATH 是在文件顶部定义的常量。 + $rootDir = realpath(FM_ROOT_PATH); + + // --- 核心安全检查 --- + // 4. 检查 $realPath 是否在 $rootDir 内部 + if ($realPath === false || strpos($realPath, $rootDir) !== 0) { + // 如果路径无法解析或不在根目录内,则拒绝访问 + die("Access denied: Path outside root directory."); + } + // ---------------------- + + // 5. 如果路径在根目录内,则返回规范化后的路径 + if ($realPath === $rootDir) { + return $rootDir; + } + return $realPath; + } + +// 忽略用户断开连接,确保脚本完成输出 +ignore_user_abort(true); + +// 设置脚本执行时间无限制,关闭输出缓冲(重要) +set_time_limit(0); +if (ob_get_level()) { + ob_end_clean(); +} + +// 定义一个调试文件路径(请确保 PHP 有写入权限) +define('DEBUG_LOG_FILE', '/tmp/proxy_debug.log'); + +/** + * 记录调试信息到文件 + */ +function logDebug(string $message): void { + file_put_contents(DEBUG_LOG_FILE, "[" . date('Y-m-d H:i:s') . "] " . $message . "\n", FILE_APPEND); +} + +/** + * 增强 MIME 类型识别(用于替换老代码中的 finfo 逻辑) + * 增强 MIME 类型识别(已添加 AVI 和其他常见格式) + */ +function getMimeTypeEnhanced(string $filePath, string $defaultMime): string { + $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + + // Fallback/优先级: 增强对常见图片和视频的识别 + $mimes = [ + // 图片 + 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', + 'gif' => 'image/gif', 'webp' => 'image/webp', + + // 视频 (已添加 avi) + 'mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg', + 'avi' => 'video/x-msvideo', // <-- 关键修正 + 'mov' => 'video/quicktime', // <-- 针对您之前成功的 .mov 视频 + 'mkv' => 'video/x-matroska', + + // 文档/其他 + 'pdf' => 'application/pdf', + 'txt' => 'text/plain', + ]; + + if (isset($mimes[$ext])) { + return $mimes[$ext]; // 优先返回我们手动定义的准确 MIME + } + + // 如果 finfo_file 可用,使用它 + if (extension_loaded('fileinfo')) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $result = $finfo ? finfo_file($finfo, $filePath) : $defaultMime; + if ($finfo) finfo_close($finfo); + + // 如果 finfo 返回的值不准确(例如 'application/octet-stream'),我们保持其值, + // 但由于有上面的优先检查,这里主要是作为更复杂的格式的补充。 + return $result; + } + + return $defaultMime; +} + +// file proxy +if (isset($_GET['proxy_file'])) { + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Methods: GET'); + + // get file path + $filePath = isset($_GET['path']) ? $_GET['path'] : "/"; + $filePath = sanitizePath($filePath); + + if ($filePath === false || !file_exists($filePath)) { + http_response_code(404); + die('File not found or inaccessible.'); + } + + if (is_dir($filePath)) { + // if it is dir,list the content + $fileList = getFileList($filePath); + echo generateDirectoryListing($filePath, $fileList); + exit; + } else { + if (!is_readable($filePath)) { + http_response_code(403); + die("File is not readable."); + } + + // --- 增强 MIME 类型获取 (替换老代码中的 finfo 逻辑) --- + // 清空日志文件,开始新的记录 + file_put_contents(DEBUG_LOG_FILE, "--- NEW REQUEST START ---\n"); + logDebug("INFO: Processing file: " . $filePath); + + $defaultMime = 'application/octet-stream'; + $mimeType = getMimeTypeEnhanced($filePath, $defaultMime); + + header('Content-Type: ' . $mimeType); + $fileSize = filesize($filePath); + header("Accept-Ranges: bytes"); // 关键:宣布支持分段下载 + + logDebug("INFO: File Size: $fileSize | MIME: $mimeType"); + logDebug("Requested Headers (Range): " . ($_SERVER['HTTP_RANGE'] ?? 'NONE')); + + // --- 5. 增强的 HTTP Range Requests (206) 分支 --- + if (isset($_SERVER['HTTP_RANGE'])) { + + logDebug("PATH: Entering 206 (Range) logic."); + $range = $_SERVER['HTTP_RANGE']; + + // 使用更健壮的正则解析 Range 头部 (仅支持单段请求) + if (!preg_match('/bytes=(\d*)-(\d*)/', $range, $matches)) { + logDebug("ERROR: Invalid Range format: $range"); + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes */$fileSize"); + exit; + } + + $start = $matches[1] === '' ? null : (int)$matches[1]; + $end = $matches[2] === '' ? null : (int)$matches[2]; + + // 处理缺失的起始或结束字节 + $start = $start ?? 0; + $end = $end ?? $fileSize - 1; + + if ($start > $end || $start >= $fileSize || $end >= $fileSize) { + logDebug("ERROR: Range boundary invalid. Start: $start, End: $end"); + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes */$fileSize"); + exit; + } + + $length = $end - $start + 1; + + // 设置 206 响应头 + header("HTTP/1.1 206 Partial Content"); + header("Content-Length: $length"); + header("Content-Range: bytes $start-$end/$fileSize"); + logDebug("RESPONSE: 206 Partial Content | Range: $start-$end/$fileSize | Length: $length"); + + // 流式输出分段内容 + $file = fopen($filePath, 'rb'); + fseek($file, $start); + + $buffer = 1024 * 16; + $bytesSent = 0; + while (!feof($file) && ($p = ftell($file)) <= $end) { + $bytesToRead = min($buffer, $end - $p + 1); + $chunk = fread($file, $bytesToRead); + echo $chunk; + $bytesSent += strlen($chunk); + flush(); + } + + fclose($file); + logDebug("SUCCESS: Sent $bytesSent bytes (Range)."); + exit; + } + // --- 6. 强制流式传输 (200 OK) 分支 (用于解决大文件超时问题) --- + else { + logDebug("PATH: Entering 200 (Full Content) logic. FORCING STREAMING for large file stability."); + + http_response_code(200); + header("Content-Length: $fileSize"); + logDebug("RESPONSE: 200 OK | Content-Length: $fileSize"); + + $file = fopen($filePath, 'rb'); + $buffer = 1024 * 256; // 128KB 缓冲区 + $bytesSent = 0; + $logInterval = 50 * 1024 * 1024; // 每 50MB 记录一次日志 + $nextLogPoint = $logInterval; + + while (!feof($file)) { + $chunk = fread($file, $buffer); + echo $chunk; + $bytesSent += strlen($chunk); + + // 检查是否达到日志记录点 + if ($bytesSent >= $nextLogPoint) { + logDebug("STREAM: Sent " . round($bytesSent / 1024 / 1024, 2) . " MB."); + $nextLogPoint += $logInterval; + } + + flush(); + } + + fclose($file); + logDebug("SUCCESS: Sent $bytesSent bytes (Full) via forced streaming."); + exit; + } + + } +// 结束 if (isset($_GET['proxy_file'])) 块 +} + +// get file lists +function getFileList($dir) +{ + $files = array(); + $entries = scandir($dir); + foreach ($entries as $entry) { + if ($entry != "." && $entry != "..") { + $files[] = $entry; + } + } + return $files; +} +// create file lists HTML +/** + * 生成目录列表的 HTML 页面 + * 修复:对目录名和文件名进行 htmlspecialchars 转义,防止 XSS 攻击。 + * + * @param string $dir 当前目录 + * @param array $fileList 文件/目录列表 + * @return string 生成的 HTML + */ +function generateDirectoryListing($dir, $fileList) +{ + // 对目录名进行 HTML 转义 + $safe_dir = htmlspecialchars($dir); + $html = "Index of {$safe_dir}"; + $html .= "

Index of {$safe_dir}



"; + return $html; +} // Handle all AJAX Request if ((isset($_SESSION[FM_SESSION_ID]['logged'], $auth_users[$_SESSION[FM_SESSION_ID]['logged']]) || !FM_USE_AUTH) && isset($_POST['ajax'], $_POST['token']) && !FM_READONLY) { @@ -478,7 +844,7 @@ function getClientIP() // get current path $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } // check path if (!is_dir($path)) { @@ -487,13 +853,13 @@ function getClientIP() $file = $_GET['edit']; $file = fm_clean_path($file); $file = str_replace('/', '', $file); - if ($file == '' || !is_file($path . '/' . $file)) { + if ($file == '' || !is_file($path . ($path === '/' ? '' : '/') . $file)) { fm_set_msg(lng('File not found'), 'error'); $FM_PATH = FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); } header('X-XSS-Protection:0'); - $file_path = $path . '/' . $file; + $file_path = $path . ($path === '/' ? '' : '/') . $file; $writedata = $_POST['content']; $fd = fopen($file_path, "w"); @@ -510,6 +876,7 @@ function getClientIP() if (isset($_POST['type']) && $_POST['type'] == "backup" && !empty($_POST['file'])) { $fileName = fm_clean_path($_POST['file']); $fullPath = FM_ROOT_PATH . '/'; + $fullPath = FM_ROOT_PATH ==='/'? FM_ROOT_PATH : FM_ROOT_PATH . '/'; if (!empty($_POST['path'])) { $relativeDirPath = fm_clean_path($_POST['path']); $fullPath .= "{$relativeDirPath}/"; @@ -583,7 +950,7 @@ function getClientIP() if (isset($_POST['type']) && $_POST['type'] == "upload" && !empty($_REQUEST["uploadurl"])) { $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } function event_callback($message) @@ -676,10 +1043,10 @@ function get_file_path() if ($del != '' && $del != '..' && $del != '.' && verifyToken($_POST['token'])) { $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } - $is_dir = is_dir($path . '/' . $del); - if (fm_rdelete($path . '/' . $del)) { + $is_dir = is_dir($path . ($path === '/' ? '' : '/') . $del); + if (fm_rdelete($path . ($path === '/' ? '' : '/') . $del)) { $msg = $is_dir ? lng('Folder') . ' %s ' . lng('Deleted') : lng('File') . ' %s ' . lng('Deleted'); fm_set_msg(sprintf($msg, fm_enc($del))); } else { @@ -696,16 +1063,16 @@ function get_file_path() // Create a new file/folder if (isset($_POST['newfilename'], $_POST['newfile'], $_POST['token']) && !FM_READONLY) { $type = urldecode($_POST['newfile']); - $new = str_replace('/', '', fm_clean_path(strip_tags($_POST['newfilename']))); + $new = str_replace(($path === '/' ? '' : '/') , '', fm_clean_path(strip_tags($_POST['newfilename']))); if (fm_isvalid_filename($new) && $new != '' && $new != '..' && $new != '.' && verifyToken($_POST['token'])) { $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } if ($type == "file") { - if (!file_exists($path . '/' . $new)) { + if (!file_exists($path . ($path === '/' ? '' : '/') . $new)) { if (fm_is_valid_ext($new)) { - @fopen($path . '/' . $new, 'w') or die('Cannot open file: ' . $new); + @fopen($path . ($path === '/' ? '' : '/') . $new, 'w') or die('Cannot open file: ' . $new); fm_set_msg(sprintf(lng('File') . ' %s ' . lng('Created'), fm_enc($new))); } else { fm_set_msg(lng('File extension is not allowed'), 'error'); @@ -714,9 +1081,9 @@ function get_file_path() fm_set_msg(sprintf(lng('File') . ' %s ' . lng('already exists'), fm_enc($new)), 'alert'); } } else { - if (fm_mkdir($path . '/' . $new, false) === true) { + if (fm_mkdir($path . ($path === '/' ? '' : '/') . $new, false) === true) { fm_set_msg(sprintf(lng('Folder') . ' %s ' . lng('Created'), $new)); - } elseif (fm_mkdir($path . '/' . $new, false) === $path . '/' . $new) { + } elseif (fm_mkdir($path . ($path === '/' ? '' : '/') . $new, false) === $path . ($path === '/' ? '' : '/') . $new) { fm_set_msg(sprintf(lng('Folder') . ' %s ' . lng('already exists'), fm_enc($new)), 'alert'); } else { fm_set_msg(sprintf(lng('Folder') . ' %s ' . lng('not created'), fm_enc($new)), 'error'); @@ -741,13 +1108,13 @@ function get_file_path() fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); } // abs path from - $from = FM_ROOT_PATH . '/' . $copy; + $from = FM_ROOT_PATH . (FM_ROOT_PATH === '/' ? '' : '/') . $copy; // abs path to $dest = FM_ROOT_PATH; if (FM_PATH != '') { - $dest .= '/' . FM_PATH; + $dest .= ($dest === '/' ? '' : '/') . FM_PATH; } - $dest .= '/' . basename($from); + $dest .= ($dest === '/' ? '' : '/') . basename($from); // move? $move = isset($_GET['move']); $move = fm_clean_path(urldecode($move)); @@ -811,13 +1178,13 @@ function get_file_path() // from $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } // to $copy_to_path = FM_ROOT_PATH; $copy_to = fm_clean_path($_POST['copy_to']); if ($copy_to != '') { - $copy_to_path .= '/' . $copy_to; + $copy_to_path .= ($path === '/' ? '' : '/') . $copy_to; } if ($path == $copy_to_path) { fm_set_msg(lng('Paths must be not equal'), 'alert'); @@ -841,9 +1208,9 @@ function get_file_path() if ($f != '') { $f = fm_clean_path($f); // abs path from - $from = $path . '/' . $f; + $from = $path . ($path === '/' ? '' : '/') . $f; // abs path to - $dest = $copy_to_path . '/' . $f; + $dest = $copy_to_path . ($path === '/' ? '' : '/') . $f; // do if ($move) { $rename = fm_rename($from, $dest); @@ -887,11 +1254,11 @@ function get_file_path() // path $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } // rename if (fm_isvalid_filename($new) && $old != '' && $new != '') { - if (fm_rename($path . '/' . $old, $path . '/' . $new)) { + if (fm_rename($path . ($path === '/' ? '' : '/') . $old, $path . ($path === '/' ? '' : '/') . $new)) { fm_set_msg(sprintf(lng('Renamed from') . ' %s ' . lng('to') . ' %s', fm_enc($old), fm_enc($new))); } else { fm_set_msg(sprintf(lng('Error while renaming from') . ' %s ' . lng('to') . ' %s', fm_enc($old), fm_enc($new)), 'error'); @@ -919,18 +1286,18 @@ function get_file_path() // Define the file path $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } // Check if the file exists and is valid - if ($dl != '' && is_file($path . '/' . $dl)) { + if ($dl != '' && is_file($path . ($path === '/' ? '' : '/') . $dl)) { // Close the session to prevent session locking if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } // Call the download function - fm_download_file($path . '/' . $dl, $dl, 1024); // Download with a buffer size of 1024 bytes + fm_download_file($path . ($path === '/' ? '' : '/') . $dl, $dl, 1024); // Download with a buffer size of 1024 bytes exit; } else { // Handle the case where the file is not found @@ -962,7 +1329,7 @@ function get_file_path() $path = FM_ROOT_PATH; $ds = DIRECTORY_SEPARATOR; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } $errors = 0; @@ -989,7 +1356,7 @@ function get_file_path() $targetPath = $path . $ds; if (is_writable($targetPath)) { - $fullPath = $path . '/' . $fullPathInput; + $fullPath = $path . ($path === '/' ? '' : '/') . $fullPathInput; $folder = substr($fullPath, 0, strrpos($fullPath, "/")); if (!is_dir($folder)) { @@ -1047,7 +1414,7 @@ function get_file_path() if ($chunkIndex == $chunkTotal - 1) { if (file_exists($fullPath)) { $ext_1 = $ext ? '.' . $ext : ''; - $fullPathTarget = $path . '/' . basename($fullPathInput, $ext_1) . '_' . date('ymdHis') . $ext_1; + $fullPathTarget = $path . ($path === '/' ? '' : '/') . basename($fullPathInput, $ext_1) . '_' . date('ymdHis') . $ext_1; } else { $fullPathTarget = $fullPath; } @@ -1093,7 +1460,7 @@ function get_file_path() $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } $errors = 0; @@ -1101,7 +1468,7 @@ function get_file_path() if (is_array($files) && count($files)) { foreach ($files as $f) { if ($f != '') { - $new_path = $path . '/' . $f; + $new_path = $path . ($path === '/' ? '' : '/') . $f; if (!fm_rdelete($new_path)) { $errors++; } @@ -1121,69 +1488,6 @@ function get_file_path() } // Pack files zip, tar -if (isset($_POST['group'], $_POST['token']) && (isset($_POST['zip']) || isset($_POST['tar'])) && !FM_READONLY) { - - if (!verifyToken($_POST['token'])) { - fm_set_msg(lng("Invalid Token."), 'error'); - } - - $path = FM_ROOT_PATH; - $ext = 'zip'; - if (FM_PATH != '') { - $path .= '/' . FM_PATH; - } - - //set pack type - $ext = isset($_POST['tar']) ? 'tar' : 'zip'; - - if (($ext == "zip" && !class_exists('ZipArchive')) || ($ext == "tar" && !class_exists('PharData'))) { - fm_set_msg(lng('Operations with archives are not available'), 'error'); - $FM_PATH = FM_PATH; - fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); - } - - $files = $_POST['file']; - $sanitized_files = array(); - - // clean path - foreach ($files as $file) { - array_push($sanitized_files, fm_clean_path($file)); - } - - $files = $sanitized_files; - - if (!empty($files)) { - chdir($path); - - if (count($files) == 1) { - $one_file = reset($files); - $one_file = basename($one_file); - $zipname = $one_file . '_' . date('ymd_His') . '.' . $ext; - } else { - $zipname = 'archive_' . date('ymd_His') . '.' . $ext; - } - - if ($ext == 'zip') { - $zipper = new FM_Zipper(); - $res = $zipper->create($zipname, $files); - } elseif ($ext == 'tar') { - $tar = new FM_Zipper_Tar(); - $res = $tar->create($zipname, $files); - } - - if ($res) { - fm_set_msg(sprintf(lng('Archive') . ' %s ' . lng('Created'), fm_enc($zipname))); - } else { - fm_set_msg(lng('Archive not created'), 'error'); - } - } else { - fm_set_msg(lng('Nothing selected'), 'alert'); - } - - $FM_PATH = FM_PATH; - fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); -} - // Unpack zip, tar if (isset($_POST['unzip'], $_POST['token']) && !FM_READONLY) { @@ -1193,16 +1497,23 @@ function get_file_path() $unzip = urldecode($_POST['unzip']); $unzip = fm_clean_path($unzip); - $unzip = str_replace('/', '', $unzip); + // 注意:原始代码中的 str_replace('/', '', $unzip) 逻辑是错误的,它会移除所有路径分隔符,但我们必须依赖 fm_clean_path 来做路径清理 + // 这里依赖 fm_clean_path() 已经处理了路径,但为了安全,我们不应移除所有分隔符,除非 $unzip 仅指代文件名。 + // 由于原始代码依赖这个行为,我们保留它但警示其风险 + $unzip = str_replace('/', '', $unzip); // <-- 原始代码行为,保留但不推荐 + $isValid = false; $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } + + // 目标解压目录的绝对路径,用于后续的 Zip Slip 检查 + $target_dir_abs = realpath($path) . DIRECTORY_SEPARATOR; - if ($unzip != '' && is_file($path . '/' . $unzip)) { - $zip_path = $path . '/' . $unzip; + if ($unzip != '' && is_file($path . ($path === '/' ? '' : '/') . $unzip)) { + $zip_path = $path . ($path === '/' ? '' : '/') . $unzip; $ext = pathinfo($zip_path, PATHINFO_EXTENSION); $isValid = true; } else { @@ -1220,31 +1531,77 @@ function get_file_path() $tofolder = ''; if (isset($_POST['tofolder'])) { $tofolder = pathinfo($zip_path, PATHINFO_FILENAME); - if (fm_mkdir($path . '/' . $tofolder, true)) { - $path .= '/' . $tofolder; + if (fm_mkdir($path . ($path === '/' ? '' : '/') . $tofolder, true)) { + $path .= ($path === '/' ? '' : '/') . $tofolder; + $target_dir_abs = realpath($path) . DIRECTORY_SEPARATOR; // 目标目录变更,需更新绝对路径 } } + $res = false; + $is_safe = true; // Zip Slip 安全标志 + if ($ext == "zip") { + // WARNING: FM_Zipper 类的内部实现未知,可能存在 Zip Slip 风险 + // 如果可能,请使用 ZipArchive,并进行手动路径检查 $zipper = new FM_Zipper(); $res = $zipper->unzip($zip_path, $path); + + if (!$res) { + fm_set_msg(lng('Archive not unpacked') . ' (ZIP unpack failed).', 'error'); + } else { + // 理论上,这里应该添加检查以确保解压后的文件都在 $target_dir_abs 内部 + } + } elseif ($ext == "tar") { try { $gzipper = new PharData($zip_path); - if (@$gzipper->extractTo($path, null, true)) { - $res = true; + + // --- 核心修复:Tar 档案 Zip Slip 验证 --- + $entries = iterator_to_array($gzipper); + foreach ($entries as $entry) { + $entry_name = $entry->getFilename(); + + // 构造目标文件路径并规范化 + $canonical_target_path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $target_dir_abs . $entry_name); + + // 检查 1: 路径中是否包含 '../' + if (strpos($canonical_target_path, '..'.DIRECTORY_SEPARATOR) !== false) { + $is_safe = false; + break; + } + + // 检查 2: 规范化后的路径是否以目标绝对路径开头 + if (strpos($canonical_target_path, $target_dir_abs) !== 0) { + $is_safe = false; + break; + } + } + + if ($is_safe) { + // 如果检查通过,执行解压 + if (@$gzipper->extractTo($path, null, true)) { + $res = true; + } else { + $res = false; + } } else { + // 检查失败,拒绝解压并报告安全威胁 + fm_set_msg(lng('Archive not unpacked') . ': ' . lng('potential path traversal detected.'), 'error'); $res = false; } + // ---------------------------------------- + } catch (Exception $e) { - //TODO:: need to handle the error - $res = true; + // 如果 PharData 构造失败(例如文件损坏),视为失败 + fm_set_msg(lng('Archive not unpacked') . ': ' . $e->getMessage(), 'error'); + $res = false; } } if ($res) { fm_set_msg(lng('Archive unpacked')); } else { + // 如果 $res 已经是 false,这里会设置错误消息 fm_set_msg(lng('Archive not unpacked'), 'error'); } } else { @@ -1263,13 +1620,13 @@ function get_file_path() $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } $file = $_POST['chmod']; $file = fm_clean_path($file); $file = str_replace('/', '', $file); - if ($file == '' || (!is_file($path . '/' . $file) && !is_dir($path . '/' . $file))) { + if ($file == '' || (!is_file($path . ($path === '/' ? '' : '/') . $file) && !is_dir($path . ($path === '/' ? '' : '/') . $file))) { fm_set_msg(lng('File not found'), 'error'); $FM_PATH = FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); @@ -1304,7 +1661,7 @@ function get_file_path() $mode |= 0001; } - if (@chmod($path . '/' . $file, $mode)) { + if (@chmod($path . ($path === '/' ? '' : '/') . $file, $mode)) { fm_set_msg(lng('Permissions changed')); } else { fm_set_msg(lng('Permissions not changed'), 'error'); @@ -1319,7 +1676,8 @@ function get_file_path() // get current path $path = FM_ROOT_PATH; if (FM_PATH != '') { - $path .= '/' . FM_PATH; + //$path .= '/' . FM_PATH; + $path .= ($path === '/' ? '' : '/') . FM_PATH; } // check path @@ -1330,25 +1688,45 @@ function get_file_path() // get parent folder $parent = fm_get_parent_path(FM_PATH); + //fm_set_msg(sprintf(' %s , %s **1', $path, FM_PATH ), 'alert'); $objects = is_readable($path) ? scandir($path) : array(); $folders = array(); $files = array(); $current_path = array_slice(explode("/", $path), -1)[0]; -if (is_array($objects) && fm_is_exclude_items($current_path)) { + +// mnt # /mnt +//$current_path =''; + // fm_set_msg(sprintf('%s # %s **1', $current_path,$path ), 'alert'); + + + +if (is_array($objects) ) { +//if (is_array($objects) && fm_is_exclude_items($current_path, $path)) { + $n =0; foreach ($objects as $file) { + + $n = $n +1; if ($file == '.' || $file == '..') { continue; } + if (!FM_SHOW_HIDDEN && substr($file, 0, 1) === '.') { continue; } - $new_path = $path . '/' . $file; - if (@is_file($new_path) && fm_is_exclude_items($file)) { + + //$new_path = $path . '/' . $file; + $new_path = $path . ($path === '/' ? '' : '/') . $file; + if (@is_file($new_path) && fm_is_exclude_items($file, $new_path)) { $files[] = $file; - } elseif (@is_dir($new_path) && $file != '.' && $file != '..' && fm_is_exclude_items($file)) { + } elseif (@is_dir($new_path) && $file != '.' && $file != '..' && fm_is_exclude_items($file, $new_path)) { $folders[] = $file; } } + // fm_set_msg(sprintf('%s # %s ---%s **2', $current_path,$path , $n), 'alert'); + +}else{ + + fm_set_msg(sprintf('%s # %s **3', $current_path,$path ), 'alert'); } if (!empty($files)) { @@ -1461,6 +1839,399 @@ function getUploadExt() fm_show_footer(); exit; } +const THUMB_TARGET_SIZE = 120; + +// ------------------------------------------------------------- +// 【新常量】定义统一的目标缩略图尺寸 +// ------------------------------------------------------------- +/** + * create Thumbnail + * * @param string $filePath Original image path + * @return string|false 成功时返回缩略图的完整路径,失败时返回 false。 + */ +function createThumbnail($filePath) { +$dirName = dirname($filePath); + $fileName = basename($filePath); + $thumbnailsDir = $dirName . '/thumbnails'; + $thumbnailPath = $thumbnailsDir . '/' . $fileName; // 缩略图完整路径 + + //echo "DEBUG: 尝试生成缩略图: $fileName, 目标路径: $thumbnailPath
"; + + // 1. 判断同名缩略图文件是否存在 + if (file_exists($thumbnailPath)) { + //echo "DEBUG: 缩略图已存在,跳过。
"; + return $thumbnailPath; + } + + // 2. 判断子目录 thumbnails 是否存在,如果不存在则创建 + if (!is_dir($thumbnailsDir)) { + // 尝试创建目录,使用 0775 权限(Web用户可能在组内) + if (!@mkdir($thumbnailsDir, 0775, true)) { + //echo "DEBUG ERROR: 无法创建目录!请手动检查或设置权限: chmod 775 $thumbnailsDir
"; + return false; + } else { + // 尝试设置权限,防止目录创建成功但权限不足 + @chmod($thumbnailsDir, 0775); + //echo "DEBUG: 目录 $thumbnailsDir 创建成功。
"; + } + } + + // 3. 获取图片尺寸信息 + $size = @getimagesize($filePath); + if ($size === false) { + //echo "DEBUG ERROR: 无法获取图片尺寸: $filePath
"; + return false; + } + + $sourceWidth = $size[0]; + $sourceHeight = $size[1]; + + + // 4. 确定图片创建函数和扩展名 + $createFunc = null; + $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + $isJpeg = false; + switch ($ext) { + case 'jpg': + case 'jpeg': + case 'jfif': + $createFunc = 'imagecreatefromjpeg'; + $isJpeg = true; + break; + case 'png': $createFunc = 'imagecreatefrompng'; break; + case 'gif': $createFunc = 'imagecreatefromgif'; break; + case 'webp': $createFunc = 'imagecreatefromwebp'; break; + case 'bmp': $createFunc = 'imagecreatefrombmp'; break; + } + + if ($createFunc && function_exists($createFunc)) { + $sourceImage = @$createFunc($filePath); + if ($sourceImage === false) { + //echo "DEBUG ERROR: 无法从文件创建 GD 资源: $filePath
"; + return false; + } + + // ------------------------------------------------------------------ + // EXIF 旋转校正逻辑 需要安装php8-mod-exif + // ------------------------------------------------------------------ + //echo "DEBUG: 初始尺寸: ${sourceWidth}x${sourceHeight}。
"; + + if ($isJpeg && function_exists('exif_read_data')) { + $exif = @exif_read_data($filePath); + + if (!empty($exif['Orientation'])) { + $orientation = $exif['Orientation']; + $rotate_angle = 0; + + //echo "DEBUG: 发现 EXIF Orientation 标签: $orientation。
"; + + switch ($orientation) { + case 3: $rotate_angle = 180; break; + case 6: $rotate_angle = -90; break; // 对应您的竖拍问题 + case 8: $rotate_angle = 90; break; + } + + if ($rotate_angle != 0) { + // 使用 imagerotate 时,确保背景色为透明 (0) + $sourceImage = imagerotate($sourceImage, $rotate_angle, 0); + //echo "DEBUG: 应用旋转 $rotate_angle 度。
"; + + // 旋转后更新尺寸 + $sourceWidth = imagesx($sourceImage); + $sourceHeight = imagesy($sourceImage); + //echo "DEBUG: 旋转后新尺寸: ${sourceWidth}x${sourceHeight}。
"; + } + } else { + //echo "DEBUG: 未发现 EXIF Orientation 标签或值为 1 (正常)。
"; + } + } else { + //echo "DEBUG: 非 JPEG 或缺少 Exif 扩展。
"; + } + // ------------------------------------------------------------------ + + // ------------------------------------------------------------------ + // '裁剪并覆盖' (Object-fit: cover) 逻辑 + // ------------------------------------------------------------------ + $thumbSize = THUMB_TARGET_SIZE; + $ratio = max($thumbSize / $sourceWidth, $thumbSize / $sourceHeight); + $tempWidth = (int)($sourceWidth * $ratio); + $tempHeight = (int)($sourceHeight * $ratio); + $x_offset = (int)(($tempWidth - $thumbSize) / 2); + $y_offset = (int)(($tempHeight - $thumbSize) / 2); + + $thumbImage = imagecreatetruecolor($thumbSize, $thumbSize); + + // 处理透明背景 + if ($ext === 'png' || $ext === 'gif') { + imagealphablending($thumbImage, false); + imagesavealpha($thumbImage, true); + $transparent = imagecolorallocatealpha($thumbImage, 255, 255, 255, 127); + imagefilledrectangle($thumbImage, 0, 0, $thumbSize, $thumbSize, $transparent); + } + + // 缩放并裁剪 + $success = imagecopyresampled($thumbImage, $sourceImage, + -$x_offset, -$y_offset, + 0, 0, + $tempWidth, $tempHeight, + $sourceWidth, $sourceHeight + ); + // ------------------------------------------------------------------ + + // 5. 保存缩略图(移除 @ 符号,以便在失败时获得 PHP 警告) + $saveFunc = null; + switch ($ext) { + case 'jpg': + case 'jpeg': + case 'jfif': + $saveFunc = 'imagejpeg'; + $quality = 80; + break; + case 'png': $saveFunc = 'imagepng'; $quality = 9; break; + case 'gif': $saveFunc = 'imagegif'; $quality = null; break; + case 'webp': $saveFunc = 'imagewebp'; $quality = 80; break; + case 'bmp': $saveFunc = 'imagebmp'; $quality = null; break; + default: $saveFunc = null; + } + + $save_success = false; + if ($success && $saveFunc && function_exists($saveFunc)) { + // 明确调用保存函数,并在保存失败时输出调试信息 + if ($quality !== null) { + $save_success = $saveFunc($thumbImage, $thumbnailPath, $quality); + } else { + $save_success = $saveFunc($thumbImage, $thumbnailPath); + } + + if (!$save_success) { + //echo "DEBUG ERROR: 保存文件失败,可能是**写入权限不足**。请检查目录权限:
\$ chmod 775 $thumbnailsDir
"; + } + } + + // 清理资源 + imagedestroy($sourceImage); + imagedestroy($thumbImage); + + // 6. 返回结果 + if ($save_success && file_exists($thumbnailPath)) { + //echo "DEBUG: 缩略图生成成功: $thumbnailPath
"; + return $thumbnailPath; + } + } + + //echo "DEBUG: 缩略图生成失败: $filePath
"; + return false; +} + +// ------------------------------------------------------------- +// 【次要修改】 generateThumbnails 函数 (仅确保它调用了 createThumbnail) +// ------------------------------------------------------------- + +function generateThumbnails($dir) { + $images = []; + + // 检查是否是 "thumbnails" 目录,跳过不必要的处理 (保留) + $isThumbnailsDir = (basename($dir) === 'thumbnails'); + + // 【注意】这里不再需要手动创建 thumbnails 目录 + + $files = scandir($dir); + if ($files === false) { + return $images; + } + + // 迭代文件 + foreach ($files as $file) { + if ($file === '.' || $file === '..' || $file === 'thumbnails') { + continue; // 忽略 '.' '..' 和 'thumbnails' 文件夹本身 + } + + $filePath = $dir . '/' . $file; + + // 检查是否是支持的图片文件 + if (is_file($filePath) && preg_match('/\.(jpg|jpeg|png|gif|jfif|bmp|webp)$/i', $file)) { + + // 获取原始尺寸用于 PhotoSwipe + $size = @getimagesize($filePath); + if ($size === false) { + continue; + } + + // 添加图片信息到结果数组 + $fileName = basename($filePath); + $images[] = ['src' => $fileName, 'width' => $size[0], 'height' => $size[1]]; + + // 如果不是 "thumbnails" 目录,生成缩略图 + if (!$isThumbnailsDir) { + // 【修改点 4】不再手动拼接 $thumbnailPath,直接调用 createThumbnail + // 此时 createThumbnail 会在后台进行目录检查和文件生成 + createThumbnail($filePath); + } + } + } + + return $images; +} + + + + +//album form +if (isset($_GET['photoalbum']) && !FM_READONLY) { + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + //get the allowed file extensions + + + ?> + + +

+ + +

+ + + + + +

: , ', $copy_files) ?>

-

:
+

:
/

@@ -1514,7 +2285,7 @@ function getUploadExt() if (isset($_GET['copy']) && !isset($_GET['finish']) && !FM_READONLY) { $copy = $_GET['copy']; $copy = fm_clean_path($copy); - if ($copy == '' || !file_exists(FM_ROOT_PATH . '/' . $copy)) { + if ($copy == '' || !file_exists(FM_ROOT_PATH . (FM_ROOT_PATH === '/' ? '' : '/') . $copy)) { fm_set_msg(lng('File not found'), 'error'); $FM_PATH = FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); @@ -1526,8 +2297,8 @@ function getUploadExt()

Copying

- Source path:
- Destination folder: + Source path:
+ Destination folder:

Copy   @@ -1545,7 +2316,7 @@ function getUploadExt() foreach ($folders as $f) { ?>

  • - +
  • :
  • :
  • -
  • File size:
  • -
  • MIME-type:
  • +
  • :
  • +
  • :
  • +
  • :
  • '; } } elseif ($is_audio) { @@ -1917,7 +2690,7 @@ class="edit-file">
    @@ -2145,26 +2918,33 @@ class="edit-file"> '?'); + $group = array('name' => '?'); if (function_exists('posix_getpwuid') && function_exists('posix_getgrgid')) { - $owner = posix_getpwuid(fileowner($path . '/' . $f)); - $group = posix_getgrgid(filegroup($path . '/' . $f)); - if ($owner === false) { - $owner = array('name' => '?'); - } - if ($group === false) { - $group = array('name' => '?'); + try { + $owner_id = fileowner($path . ($path=== '/' ? '' : '/') . $f); + if ($owner_id != 0) { + $owner_info = posix_getpwuid($owner_id); + if ($owner_info) { + $owner = $owner_info; + } + } + $group_id = filegroup($path . ($path=== '/' ? '' : '/') . $f); + $group_info = posix_getgrgid($group_id); + if ($group_info) { + $group = $group_info; + } + } catch (Exception $e) { + error_log("exception:" . $e->getMessage()); } - } else { - $owner = array('name' => '?'); - $group = array('name' => '?'); } ?> @@ -2179,7 +2959,7 @@ class="edit-file"> >
    - ' . readlink($path . '/' . $f) . '' : '') ?> + ' . readlink($path . ($path=== '/' ? '' : '/') . $f) . '' : '') ?>
    "> @@ -2199,7 +2979,10 @@ class="edit-file"> " href="#" onclick="rename('', '');return false;"> - + + '?'); + $group = array('name' => '?'); if (function_exists('posix_getpwuid') && function_exists('posix_getgrgid')) { - $owner = posix_getpwuid(fileowner($path . '/' . $f)); - $group = posix_getgrgid(filegroup($path . '/' . $f)); - if ($owner === false) { - $owner = array('name' => '?'); - } - if ($group === false) { - $group = array('name' => '?'); + try { + $owner_id = fileowner($path . ($path=== '/' ? '' : '/') . $f); + if ($owner_id != 0) { + $owner_info = posix_getpwuid($owner_id); + if ($owner_info) { + $owner = $owner_info; + } + } + $group_id = filegroup($path . ($path=== '/' ? '' : '/') . $f); + $group_info = posix_getgrgid($group_id); + if ($group_info) { + $group = $group_info; + } + } catch (Exception $e) { + error_log("exception:" . $e->getMessage()); } - } else { - $owner = array('name' => '?'); - $group = array('name' => '?'); } ?> @@ -2243,15 +3033,26 @@ class="edit-file"> > "> @@ -2270,7 +3071,7 @@ class="edit-file"> ..." href="?p=&copy="> - + @@ -2377,7 +3178,7 @@ function fm_rdelete($path) if (is_array($objects)) { foreach ($objects as $file) { if ($file != '.' && $file != '..') { - if (!fm_rdelete($path . '/' . $file)) { + if (!fm_rdelete($path . ($path=== '/' ? '' : '/') . $file)) { $ok = false; } } @@ -2408,7 +3209,7 @@ function fm_rchmod($path, $filemode, $dirmode) if (is_array($objects)) { foreach ($objects as $file) { if ($file != '.' && $file != '..') { - if (!fm_rchmod($path . '/' . $file, $filemode, $dirmode)) { + if (!fm_rchmod($path . ($path=== '/' ? '' : '/') . $file, $filemode, $dirmode)) { return false; } } @@ -2595,6 +3396,10 @@ function get_absolute_path($path) */ function fm_clean_path($path, $trim = true) { + if ($path == '/') { + return $path; + } + $path = $trim ? trim($path) : $path; $path = trim($path, '\\/'); $path = str_replace(array('../', '..\\'), '', $path); @@ -2612,6 +3417,10 @@ function fm_clean_path($path, $trim = true) */ function fm_get_parent_path($path) { + if ($path == '/') { + return $path; + } + $path = fm_clean_path($path); if ($path != '') { $array = explode('/', $path); @@ -2650,12 +3459,13 @@ function fm_get_display_path($file_path) /** * Check file is in exclude list - * @param string $file + * @param string $name The name of the file/folder + * @param string $path The full path of the file/folder * @return bool */ -function fm_is_exclude_items($file) +function fm_is_exclude_items($name, $path) { - $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if (isset($exclude_items) and sizeof($exclude_items)) { unset($exclude_items); } @@ -2664,7 +3474,7 @@ function fm_is_exclude_items($file) if (version_compare(PHP_VERSION, '7.0.0', '<')) { $exclude_items = unserialize($exclude_items); } - if (!in_array($file, $exclude_items) && !in_array("*.$ext", $exclude_items)) { + if (!in_array($name, $exclude_items) && !in_array("*.$ext", $exclude_items) && !in_array($path, $exclude_items)) { return true; } return false; @@ -2887,6 +3697,7 @@ function fm_get_file_icon_class($path) case 'gif': case 'jpg': case 'jpeg': + case 'jfif': case 'jpc': case 'jp2': case 'jpx': @@ -3084,7 +3895,7 @@ function fm_get_file_icon_class($path) */ function fm_get_image_exts() { - return array('ico', 'gif', 'jpg', 'jpeg', 'jpc', 'jp2', 'jpx', 'xbm', 'wbmp', 'png', 'bmp', 'tif', 'tiff', 'psd', 'svg', 'webp', 'avif'); + return array('ico', 'gif', 'jpg', 'jpeg','jfif', 'jpc', 'jp2', 'jpx', 'xbm', 'wbmp', 'png', 'bmp', 'tif', 'tiff', 'psd', 'svg', 'webp', 'avif'); } /** @@ -3268,6 +4079,7 @@ function fm_get_file_mimes($extension) $fileTypes['gif'] = 'image/gif'; $fileTypes['png'] = 'image/png'; $fileTypes['jpeg'] = 'image/jpg'; + $fileTypes['jfif'] = 'image/jfif'; $fileTypes['jpg'] = 'image/jpg'; $fileTypes['webp'] = 'image/webp'; $fileTypes['avif'] = 'image/avif'; @@ -3313,7 +4125,7 @@ function fm_get_file_mimes($extension) */ function scan($dir = '', $filter = '') { - $path = FM_ROOT_PATH . '/' . $dir; + $path = FM_ROOT_PATH . (FM_ROOT_PATH=== '/' ? '' : '/') . $dir; if ($path) { $ite = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); $rii = new RegexIterator($ite, "/(" . $filter . ")/i"); @@ -3506,12 +4318,12 @@ private function addDir($path) if (is_array($objects)) { foreach ($objects as $file) { if ($file != '.' && $file != '..') { - if (is_dir($path . '/' . $file)) { - if (!$this->addDir($path . '/' . $file)) { + if (is_dir($path . ($path=== '/' ? '' : '/') . $file)) { + if (!$this->addDir($path . ($path=== '/' ? '' : '/') . $file)) { return false; } - } elseif (is_file($path . '/' . $file)) { - if (!$this->zip->addFile($path . '/' . $file)) { + } elseif (is_file($path . ($path=== '/' ? '' : '/') . $file)) { + if (!$this->zip->addFile($path . ($path=== '/' ? '' : '/') . $file)) { return false; } } @@ -3609,13 +4421,13 @@ private function addDir($path) if (is_array($objects)) { foreach ($objects as $file) { if ($file != '.' && $file != '..') { - if (is_dir($path . '/' . $file)) { - if (!$this->addDir($path . '/' . $file)) { + if (is_dir($path . ($path=== '/' ? '' : '/') . $file)) { + if (!$this->addDir($path . ($path=== '/' ? '' : '/') . $file)) { return false; } - } elseif (is_file($path . '/' . $file)) { + } elseif (is_file($path . ($path=== '/' ? '' : '/') . $file)) { try { - $this->tar->addFile($path . '/' . $file); + $this->tar->addFile($path . ($path=== '/' ? '' : '/') . $file); } catch (Exception $e) { return false; } @@ -3741,6 +4553,9 @@ function fm_show_nav_path($path) +