From f86397e0e5aef385c7e12f98cf81d1dcc0701fbd Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 23 Jun 2026 18:37:27 -0400 Subject: [PATCH 1/7] fix(file-manager): keep exclusive shares in /mnt/user space Opening an exclusive share in the File Manager silently switched the whole view - and every folder/row link - from /mnt/user/ to the raw /mnt// path. Cause: validdir() ran the requested dir through realpath(), which follows the symlink that backs an exclusive share (/mnt/user/ -> /mnt//). validdir() now still validates with realpath() (the resolved path must live under /mnt or /boot, so traversal is still blocked), but re-anchors to a lexically-canonicalized /mnt/(user0?|rootshare) path when that is what was requested. FUSE is bypassed for exclusive shares, so this is the same data at native speed - just shown under the path the user clicked. Applied to both Browse.page and include/Browse.php. Also swap the open-in-File-Manager launcher icon from icon-u-tab (a square-with-arrow / open-in-tab glyph that doesn't say what it does) to fa fa-folder-open across DiskList, ShareList and device_list, matching the folder-open icon already used elsewhere. Co-Authored-By: Claude Opus 4.8 --- emhttp/plugins/dynamix/Browse.page | 19 +++++++++++++++++-- emhttp/plugins/dynamix/include/Browse.php | 19 +++++++++++++++++-- emhttp/plugins/dynamix/include/DiskList.php | 2 +- emhttp/plugins/dynamix/include/ShareList.php | 2 +- emhttp/plugins/dynamix/nchan/device_list | 8 ++++---- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index d08a06f9db..dbdf3a9996 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -24,8 +24,23 @@ function dfm_array($array) { } function validdir($dir) { - $path = realpath($dir); - return in_array(explode('/', $path)[1] ?? '', ['mnt','boot']) ? $path : ''; + $real = realpath($dir); + // Validate against the real (symlink-resolved) path: it must live under /mnt or /boot. + if ($real === false || !in_array(explode('/', $real)[1] ?? '', ['mnt','boot'])) return ''; + // An exclusive share is exposed as a symlink /mnt/user/ -> /mnt//, + // so realpath() would kick the File Manager out of user-share space and rewrite the whole + // view (and every folder link) to the raw /mnt/ path. Keep the user-share path the + // caller asked for - FUSE is bypassed for exclusive shares, so it is the same data at native + // speed. Canonicalize lexically (no filesystem) so '..' can never escape /mnt/user. + $parts = []; + foreach (explode('/', $dir) as $p) { + if ($p === '' || $p === '.') continue; + if ($p === '..') { array_pop($parts); continue; } + $parts[] = $p; + } + $canon = '/'.implode('/', $parts); + if (preg_match('#^/mnt/(user0?|rootshare)(/|$)#', $canon) && is_dir($canon)) return $canon; + return $real; } $dir = validdir(rawurldecode($dir)); diff --git a/emhttp/plugins/dynamix/include/Browse.php b/emhttp/plugins/dynamix/include/Browse.php index fefbac066c..bed980dfe1 100644 --- a/emhttp/plugins/dynamix/include/Browse.php +++ b/emhttp/plugins/dynamix/include/Browse.php @@ -24,8 +24,23 @@ function write(&$rows) { } function validdir($dir) { - $path = realpath($dir); - return in_array(explode('/', $path)[1] ?? '', ['mnt','boot']) ? $path : ''; + $real = realpath($dir); + // Validate against the real (symlink-resolved) path: it must live under /mnt or /boot. + if ($real === false || !in_array(explode('/', $real)[1] ?? '', ['mnt','boot'])) return ''; + // An exclusive share is exposed as a symlink /mnt/user/ -> /mnt//, + // so realpath() would kick the File Manager out of user-share space and rewrite the whole + // listing (every row's path and LOCATION) to the raw /mnt/ path. Keep the user-share + // path the caller asked for - FUSE is bypassed for exclusive shares, so it is the same data + // at native speed. Canonicalize lexically (no filesystem) so '..' can never escape /mnt/user. + $parts = []; + foreach (explode('/', $dir) as $p) { + if ($p === '' || $p === '.') continue; + if ($p === '..') { array_pop($parts); continue; } + $parts[] = $p; + } + $canon = '/'.implode('/', $parts); + if (preg_match('#^/mnt/(user0?|rootshare)(/|$)#', $canon) && is_dir($canon)) return $canon; + return $real; } function escapeQuote($data) { diff --git a/emhttp/plugins/dynamix/include/DiskList.php b/emhttp/plugins/dynamix/include/DiskList.php index cb103c61cf..3ecef09f94 100644 --- a/emhttp/plugins/dynamix/include/DiskList.php +++ b/emhttp/plugins/dynamix/include/DiskList.php @@ -110,7 +110,7 @@ function sharesOnly($disk) { case 2: $luks = ""._('Some or all files unencrypted').""; break; default: $luks = ""._('Unknown encryption state').""; break; } else $luks = ""; - echo ""; + echo ""; echo "$help$luks$name"; echo "",htmlspecialchars(_var($disk,'comment')),""; echo "",disk_share_settings(_var($var,'shareSMBEnabled'), $sec[$name]),""; diff --git a/emhttp/plugins/dynamix/include/ShareList.php b/emhttp/plugins/dynamix/include/ShareList.php index c3688d56a7..eea6eef6c2 100644 --- a/emhttp/plugins/dynamix/include/ShareList.php +++ b/emhttp/plugins/dynamix/include/ShareList.php @@ -266,7 +266,7 @@ function shareInclude($name) { } } - echo ""; + echo ""; echo "$help$luks$name"; echo "", htmlspecialchars(_var($share,'comment')), ""; diff --git a/emhttp/plugins/dynamix/nchan/device_list b/emhttp/plugins/dynamix/nchan/device_list index 6dbb43b363..0fb6444587 100755 --- a/emhttp/plugins/dynamix/nchan/device_list +++ b/emhttp/plugins/dynamix/nchan/device_list @@ -503,11 +503,11 @@ function device_info(&$disk,$online,$poolName='',$poolstatusData=[], $options=[] if (!$online || _var($disk,'fsStatus')!='Mounted' || (in_array(_var($disk,'type'),['Parity','Cache']) && (!in_array(_var($disk,'name'),$pools) || isSubpool(_var($disk,'name'))))) { $view = ""; } else { - $view = ""; + $view = ""; } if (!$showView) $view = $showViewPlaceholder ? "" : ""; if ($showView && _var($options,'forceView',false) && _var($disk,'fsStatus')=='Mounted' && $dir !== '') { - $view = ""; + $view = ""; } if ($indent) $view = "".$view; $realName = _var($disk,'name'); @@ -1392,7 +1392,7 @@ while (true) { $bootBrowseUrl = $bootDir !== '' ? "/Main/Browse?dir=".htmlspecialchars($bootDir) : $bootDeviceUrl; $bootIcon = $bootBrowseUrl === $bootDeviceUrl ? "" - : ""; + : ""; $bootLink = ""._($bootLabel,3).""; $poolLabel = ucfirst($pool); $poolDeviceUrl = "/Main/Device?name=".urlencode($pool); @@ -1400,7 +1400,7 @@ while (true) { $poolBrowseUrl = $poolDir !== '' ? "/Main/Browse?dir=".htmlspecialchars($poolDir) : $poolDeviceUrl; $poolIcon = $poolBrowseUrl === $poolDeviceUrl ? "" - : ""; + : ""; $poolLink = ""._($poolLabel,3).""; $flashWarn = flash_smb_warning_html(_var($bootDisk,'name','')); $bootSummaryName = $bootIcon.$bootLink.$flashWarn; From 96dcc0290a24416264344aeaff0c303bea5df0eb Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 23 Jun 2026 18:59:13 -0400 Subject: [PATCH 2/7] style(file-manager): visually separate the browse icon from the device link The folder-open browse icon sits immediately left of the device/share name link and inherited the same link blue, so the two read as a single element - you couldn't tell the icon was a separate clickable action. Mute the browse icon to --alt-text-color (brightening to the link color on hover to keep the clickable affordance) and add a right-margin gap before the name. Applied once on the shared a.view rule, so DiskList, ShareList and the device list all get it; the empty .view placeholders take the same slot, so column alignment is unchanged. Co-Authored-By: Claude Opus 4.8 --- emhttp/plugins/dynamix/styles/default-base.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/emhttp/plugins/dynamix/styles/default-base.css b/emhttp/plugins/dynamix/styles/default-base.css index 6dfa65cea8..9b5f9f0b0b 100755 --- a/emhttp/plugins/dynamix/styles/default-base.css +++ b/emhttp/plugins/dynamix/styles/default-base.css @@ -131,6 +131,16 @@ a.static { a.view { display: inline-block; width: 20px; + /* Gap so the browse icon doesn't visually merge with the device/share link beside it */ + margin-right: 8px; +} +/* Mute the browse icon so it reads as a separate action, not part of the blue link + next to it; brighten to the link color on hover to signal it is clickable. */ +a.view i { + color: var(--alt-text-color); +} +a.view:hover i { + color: var(--blue-800); } i.spacing { margin-left: -6px; From b8ca44954a9ace7e712799e12822130beeed16c2 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 23 Jun 2026 19:03:26 -0400 Subject: [PATCH 3/7] style(file-manager): closed browse folder that opens on hover Use a closed folder (fa-folder-o) at rest for the browse links and swap to the open variant on hover, so the icon visibly previews the "open this location" action. Done via a single a.view:hover content override in CSS; the icon class changes to fa-folder-o in DiskList, ShareList and the device list. Co-Authored-By: Claude Opus 4.8 --- emhttp/plugins/dynamix/include/DiskList.php | 2 +- emhttp/plugins/dynamix/include/ShareList.php | 2 +- emhttp/plugins/dynamix/nchan/device_list | 8 ++++---- emhttp/plugins/dynamix/styles/default-base.css | 4 ++++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/emhttp/plugins/dynamix/include/DiskList.php b/emhttp/plugins/dynamix/include/DiskList.php index 3ecef09f94..54a1fa5a19 100644 --- a/emhttp/plugins/dynamix/include/DiskList.php +++ b/emhttp/plugins/dynamix/include/DiskList.php @@ -110,7 +110,7 @@ function sharesOnly($disk) { case 2: $luks = ""._('Some or all files unencrypted').""; break; default: $luks = ""._('Unknown encryption state').""; break; } else $luks = ""; - echo ""; + echo ""; echo "$help$luks$name"; echo "",htmlspecialchars(_var($disk,'comment')),""; echo "",disk_share_settings(_var($var,'shareSMBEnabled'), $sec[$name]),""; diff --git a/emhttp/plugins/dynamix/include/ShareList.php b/emhttp/plugins/dynamix/include/ShareList.php index eea6eef6c2..737fb7cca6 100644 --- a/emhttp/plugins/dynamix/include/ShareList.php +++ b/emhttp/plugins/dynamix/include/ShareList.php @@ -266,7 +266,7 @@ function shareInclude($name) { } } - echo ""; + echo ""; echo "$help$luks$name"; echo "", htmlspecialchars(_var($share,'comment')), ""; diff --git a/emhttp/plugins/dynamix/nchan/device_list b/emhttp/plugins/dynamix/nchan/device_list index 0fb6444587..09b8ae68a8 100755 --- a/emhttp/plugins/dynamix/nchan/device_list +++ b/emhttp/plugins/dynamix/nchan/device_list @@ -503,11 +503,11 @@ function device_info(&$disk,$online,$poolName='',$poolstatusData=[], $options=[] if (!$online || _var($disk,'fsStatus')!='Mounted' || (in_array(_var($disk,'type'),['Parity','Cache']) && (!in_array(_var($disk,'name'),$pools) || isSubpool(_var($disk,'name'))))) { $view = ""; } else { - $view = ""; + $view = ""; } if (!$showView) $view = $showViewPlaceholder ? "" : ""; if ($showView && _var($options,'forceView',false) && _var($disk,'fsStatus')=='Mounted' && $dir !== '') { - $view = ""; + $view = ""; } if ($indent) $view = "".$view; $realName = _var($disk,'name'); @@ -1392,7 +1392,7 @@ while (true) { $bootBrowseUrl = $bootDir !== '' ? "/Main/Browse?dir=".htmlspecialchars($bootDir) : $bootDeviceUrl; $bootIcon = $bootBrowseUrl === $bootDeviceUrl ? "" - : ""; + : ""; $bootLink = ""._($bootLabel,3).""; $poolLabel = ucfirst($pool); $poolDeviceUrl = "/Main/Device?name=".urlencode($pool); @@ -1400,7 +1400,7 @@ while (true) { $poolBrowseUrl = $poolDir !== '' ? "/Main/Browse?dir=".htmlspecialchars($poolDir) : $poolDeviceUrl; $poolIcon = $poolBrowseUrl === $poolDeviceUrl ? "" - : ""; + : ""; $poolLink = ""._($poolLabel,3).""; $flashWarn = flash_smb_warning_html(_var($bootDisk,'name','')); $bootSummaryName = $bootIcon.$bootLink.$flashWarn; diff --git a/emhttp/plugins/dynamix/styles/default-base.css b/emhttp/plugins/dynamix/styles/default-base.css index 9b5f9f0b0b..81d40e5313 100755 --- a/emhttp/plugins/dynamix/styles/default-base.css +++ b/emhttp/plugins/dynamix/styles/default-base.css @@ -142,6 +142,10 @@ a.view i { a.view:hover i { color: var(--blue-800); } +/* Closed folder at rest, opening on hover, to signal "click to browse here" */ +a.view:hover i.fa-folder-o:before { + content: "\f115"; /* fa-folder-open-o */ +} i.spacing { margin-left: -6px; } From 482367b05b45f51535397e0754329b66af76366b Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 23 Jun 2026 19:06:08 -0400 Subject: [PATCH 4/7] style(device-list): make the pool (ONLINE) status read as a details link The ZFS/btrfs pool health text linked to the pool's details section but looked like a stray blue label - same color as the device-name link next to it - so it wasn't clear it was clickable or what it did. Give it a dedicated .pool-status style (muted text, brightens + underlines on hover) and a trailing chevron to mark it navigable, keeping the existing "View pool details" tooltip. Applies to the healthy and problem states. Co-Authored-By: Claude Opus 4.8 --- emhttp/plugins/dynamix/nchan/device_list | 9 ++++++--- emhttp/plugins/dynamix/styles/default-base.css | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/emhttp/plugins/dynamix/nchan/device_list b/emhttp/plugins/dynamix/nchan/device_list index 09b8ae68a8..d1c7368619 100755 --- a/emhttp/plugins/dynamix/nchan/device_list +++ b/emhttp/plugins/dynamix/nchan/device_list @@ -344,14 +344,17 @@ function pool_status_html(string $poolName, array $poolstatusData, array $disks) $displayText = $hasErrors ? _('ONLINE') : _($status_text); $deviceUrl = "/Main/Device?name=".urlencode($poolName)."#poolsummary"; + // The pool-status class + trailing chevron mark this as a "view pool details" + // link rather than a stray blue label (the device name beside it is also a link). + $chevron = ""; if (!$isOnline) { - return " ("._($status_text).")"; + return " ("._($status_text).") $chevron"; } if ($hasErrors) { - return " ($displayText)"; + return " ($displayText) $chevron"; } - return " ($displayText)"; + return " ($displayText) $chevron"; } function pool_function_row(string $label, ?array $poolDisk, ?array $firstMember, array $metrics, string $poolName='', array $poolstatusData=[], bool $showSummary=false, array $fsOverride=[], bool $showPoolStatusInTitle=true, string $statusPoolName='', string $summaryDisplayName='', bool $summaryShowView=true, string $summaryRowId=''): string diff --git a/emhttp/plugins/dynamix/styles/default-base.css b/emhttp/plugins/dynamix/styles/default-base.css index 81d40e5313..30d2757424 100755 --- a/emhttp/plugins/dynamix/styles/default-base.css +++ b/emhttp/plugins/dynamix/styles/default-base.css @@ -146,6 +146,21 @@ a.view:hover i { a.view:hover i.fa-folder-o:before { content: "\f115"; /* fa-folder-open-o */ } +/* Pool health indicator: a "view pool details" link, not a generic blue label. + Muted so it reads as secondary status text; a small chevron marks it navigable, + and it brightens + underlines on hover. */ +a.pool-status { + color: var(--alt-text-color); +} +a.pool-status:hover { + color: var(--blue-800); + text-decoration: underline; +} +i.pool-status-go { + font-size: 0.85em; + margin-left: 3px; + opacity: 0.7; +} i.spacing { margin-left: -6px; } From 9bbd88d284c8339f6edd6beda6c8ca48a8514e33 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 23 Jun 2026 19:20:20 -0400 Subject: [PATCH 5/7] style(file-manager): label the browse icon and de-link the pool status Address the affordance gaps with comprehension cues, not just styling: - Browse folder links now carry Unraid's standard a.info tooltip ("Open in File Manager") instead of the slow native title=, so hovering says what the icon does. Combined with the closed->open swap and hover color, the action is now self-explanatory. Applied on DiskList, ShareList and the device list. - Pool health (ONLINE) is no longer a hyperlink - a clickable status word is an odd pattern. It's now plain status text, with a small info-circle beside it as the only clickable element (a.info tooltip "View pool details") linking to the pool summary. The existing warning icon for DEGRADED/ERRORS states gets the same styled tooltip. Co-Authored-By: Claude Opus 4.8 --- emhttp/plugins/dynamix/include/DiskList.php | 2 +- emhttp/plugins/dynamix/include/ShareList.php | 2 +- emhttp/plugins/dynamix/nchan/device_list | 21 ++++++++++--------- .../plugins/dynamix/styles/default-base.css | 20 ++++++++---------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/emhttp/plugins/dynamix/include/DiskList.php b/emhttp/plugins/dynamix/include/DiskList.php index 54a1fa5a19..1c288d763b 100644 --- a/emhttp/plugins/dynamix/include/DiskList.php +++ b/emhttp/plugins/dynamix/include/DiskList.php @@ -110,7 +110,7 @@ function sharesOnly($disk) { case 2: $luks = ""._('Some or all files unencrypted').""; break; default: $luks = ""._('Unknown encryption state').""; break; } else $luks = ""; - echo ""; + echo "",_('Open in File Manager'),""; echo "$help$luks$name"; echo "",htmlspecialchars(_var($disk,'comment')),""; echo "",disk_share_settings(_var($var,'shareSMBEnabled'), $sec[$name]),""; diff --git a/emhttp/plugins/dynamix/include/ShareList.php b/emhttp/plugins/dynamix/include/ShareList.php index 737fb7cca6..9adcdc23b5 100644 --- a/emhttp/plugins/dynamix/include/ShareList.php +++ b/emhttp/plugins/dynamix/include/ShareList.php @@ -266,7 +266,7 @@ function shareInclude($name) { } } - echo ""; + echo "", _('Open in File Manager'), ""; echo "$help$luks$name"; echo "", htmlspecialchars(_var($share,'comment')), ""; diff --git a/emhttp/plugins/dynamix/nchan/device_list b/emhttp/plugins/dynamix/nchan/device_list index d1c7368619..a2819a8d99 100755 --- a/emhttp/plugins/dynamix/nchan/device_list +++ b/emhttp/plugins/dynamix/nchan/device_list @@ -344,17 +344,18 @@ function pool_status_html(string $poolName, array $poolstatusData, array $disks) $displayText = $hasErrors ? _('ONLINE') : _($status_text); $deviceUrl = "/Main/Device?name=".urlencode($poolName)."#poolsummary"; - // The pool-status class + trailing chevron mark this as a "view pool details" - // link rather than a stray blue label (the device name beside it is also a link). - $chevron = ""; + // Health stays plain status text; the only clickable bit is a small info icon + // (with the standard a.info tooltip) that jumps to the pool details section. + $details = ""._('View pool details').""; + $warn = ""._('Check pool status').""; if (!$isOnline) { - return " ("._($status_text).") $chevron"; + return " ("._($status_text).") $warn$details"; } if ($hasErrors) { - return " ($displayText) $chevron"; + return " ($displayText) $warn$details"; } - return " ($displayText) $chevron"; + return " ($displayText) $details"; } function pool_function_row(string $label, ?array $poolDisk, ?array $firstMember, array $metrics, string $poolName='', array $poolstatusData=[], bool $showSummary=false, array $fsOverride=[], bool $showPoolStatusInTitle=true, string $statusPoolName='', string $summaryDisplayName='', bool $summaryShowView=true, string $summaryRowId=''): string @@ -506,11 +507,11 @@ function device_info(&$disk,$online,$poolName='',$poolstatusData=[], $options=[] if (!$online || _var($disk,'fsStatus')!='Mounted' || (in_array(_var($disk,'type'),['Parity','Cache']) && (!in_array(_var($disk,'name'),$pools) || isSubpool(_var($disk,'name'))))) { $view = ""; } else { - $view = ""; + $view = ""._('Open in File Manager').""; } if (!$showView) $view = $showViewPlaceholder ? "" : ""; if ($showView && _var($options,'forceView',false) && _var($disk,'fsStatus')=='Mounted' && $dir !== '') { - $view = ""; + $view = ""._('Open in File Manager').""; } if ($indent) $view = "".$view; $realName = _var($disk,'name'); @@ -1395,7 +1396,7 @@ while (true) { $bootBrowseUrl = $bootDir !== '' ? "/Main/Browse?dir=".htmlspecialchars($bootDir) : $bootDeviceUrl; $bootIcon = $bootBrowseUrl === $bootDeviceUrl ? "" - : ""; + : ""._('Open in File Manager').""; $bootLink = ""._($bootLabel,3).""; $poolLabel = ucfirst($pool); $poolDeviceUrl = "/Main/Device?name=".urlencode($pool); @@ -1403,7 +1404,7 @@ while (true) { $poolBrowseUrl = $poolDir !== '' ? "/Main/Browse?dir=".htmlspecialchars($poolDir) : $poolDeviceUrl; $poolIcon = $poolBrowseUrl === $poolDeviceUrl ? "" - : ""; + : ""._('Open in File Manager').""; $poolLink = ""._($poolLabel,3).""; $flashWarn = flash_smb_warning_html(_var($bootDisk,'name','')); $bootSummaryName = $bootIcon.$bootLink.$flashWarn; diff --git a/emhttp/plugins/dynamix/styles/default-base.css b/emhttp/plugins/dynamix/styles/default-base.css index 30d2757424..17f5015393 100755 --- a/emhttp/plugins/dynamix/styles/default-base.css +++ b/emhttp/plugins/dynamix/styles/default-base.css @@ -146,20 +146,18 @@ a.view:hover i { a.view:hover i.fa-folder-o:before { content: "\f115"; /* fa-folder-open-o */ } -/* Pool health indicator: a "view pool details" link, not a generic blue label. - Muted so it reads as secondary status text; a small chevron marks it navigable, - and it brightens + underlines on hover. */ -a.pool-status { +/* Pool health is plain status text, not a link. The separate info icon beside it + is the only clickable bit; it's muted and brightens on hover, with the standard + a.info tooltip explaining where it goes. */ +span.pool-state { color: var(--alt-text-color); } -a.pool-status:hover { - color: var(--blue-800); - text-decoration: underline; +a.pool-info { + color: var(--alt-text-color); + margin-left: 4px; } -i.pool-status-go { - font-size: 0.85em; - margin-left: 3px; - opacity: 0.7; +a.pool-info:hover { + color: var(--blue-800); } i.spacing { margin-left: -6px; From 5c20006bfc25d5fc894996cf52705e2bd4dc2791 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 14 Jul 2026 16:28:26 -0400 Subject: [PATCH 6/7] fix(file-manager): preserve exclusive share picker paths --- emhttp/plugins/dynamix/include/FileTree.php | 26 +++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/emhttp/plugins/dynamix/include/FileTree.php b/emhttp/plugins/dynamix/include/FileTree.php index 613214d1c7..a247599368 100644 --- a/emhttp/plugins/dynamix/include/FileTree.php +++ b/emhttp/plugins/dynamix/include/FileTree.php @@ -31,6 +31,25 @@ function path($dir) { return mb_substr($dir,-1) == '/' ? $dir : $dir.'/'; } +function tree_dir($dir) { + $real = realpath($dir); + if ($real === false) return false; + + // Exclusive shares are symlinks into their backing pool. Keep the requested + // user-share path so selecting a nested destination does not switch the + // File Manager target from /mnt/user to /mnt/. + $parts = []; + foreach (explode('/', $dir) as $part) { + if ($part === '' || $part === '.') continue; + if ($part === '..') { array_pop($parts); continue; } + $parts[] = $part; + } + $canonical = '/'.implode('/', $parts); + if (preg_match('#^/mnt/(user0?|rootshare)(/|$)#', $canonical) && is_dir($canonical)) return $canonical; + + return $real; +} + function is_top($dir) { global $fileTreeRoot; return mb_strlen($dir) > mb_strlen($fileTreeRoot); @@ -45,8 +64,9 @@ function my_dir($name) { return ($rootdir === $userdir && in_array($name, $UDincluded)) ? $topdir : $rootdir; } -$fileTreeRoot = path(realpath($_POST['root'])); +$fileTreeRoot = tree_dir($_POST['root']); if (!$fileTreeRoot) exit("ERROR: Root filesystem directory not set in jqueryFileTree.php"); +$fileTreeRoot = path($fileTreeRoot); $docroot = '/usr/local/emhttp'; require_once "$docroot/webGui/include/Secure.php"; @@ -56,7 +76,9 @@ function my_dir($name) { $mntdir = '/mnt/'; $userdir = '/mnt/user/'; -$rootdir = path(realpath($_POST['dir'])); +$rootdir = tree_dir($_POST['dir']); +if (!$rootdir) exit("ERROR: Filesystem directory not set in jqueryFileTree.php"); +$rootdir = path($rootdir); $topdir = str_replace($userdir, $mntdir, $rootdir); $filters = (array)$_POST['filter']; $match = $_POST['match']; From b418d1c1abd472bf6d7b3689e17063a150431983 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 14 Jul 2026 16:35:38 -0400 Subject: [PATCH 7/7] fix(file-manager): encode browse link paths --- emhttp/plugins/dynamix/include/DiskList.php | 2 +- emhttp/plugins/dynamix/include/ShareList.php | 2 +- emhttp/plugins/dynamix/nchan/device_list | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/emhttp/plugins/dynamix/include/DiskList.php b/emhttp/plugins/dynamix/include/DiskList.php index 1c288d763b..a55fb7b56b 100644 --- a/emhttp/plugins/dynamix/include/DiskList.php +++ b/emhttp/plugins/dynamix/include/DiskList.php @@ -110,7 +110,7 @@ function sharesOnly($disk) { case 2: $luks = ""._('Some or all files unencrypted').""; break; default: $luks = ""._('Unknown encryption state').""; break; } else $luks = ""; - echo "",_('Open in File Manager'),""; + echo "",_('Open in File Manager'),""; echo "$help$luks$name"; echo "",htmlspecialchars(_var($disk,'comment')),""; echo "",disk_share_settings(_var($var,'shareSMBEnabled'), $sec[$name]),""; diff --git a/emhttp/plugins/dynamix/include/ShareList.php b/emhttp/plugins/dynamix/include/ShareList.php index 9adcdc23b5..d54629edaa 100644 --- a/emhttp/plugins/dynamix/include/ShareList.php +++ b/emhttp/plugins/dynamix/include/ShareList.php @@ -266,7 +266,7 @@ function shareInclude($name) { } } - echo "", _('Open in File Manager'), ""; + echo "", _('Open in File Manager'), ""; echo "$help$luks$name"; echo "", htmlspecialchars(_var($share,'comment')), ""; diff --git a/emhttp/plugins/dynamix/nchan/device_list b/emhttp/plugins/dynamix/nchan/device_list index a2819a8d99..b399ccde43 100755 --- a/emhttp/plugins/dynamix/nchan/device_list +++ b/emhttp/plugins/dynamix/nchan/device_list @@ -507,11 +507,11 @@ function device_info(&$disk,$online,$poolName='',$poolstatusData=[], $options=[] if (!$online || _var($disk,'fsStatus')!='Mounted' || (in_array(_var($disk,'type'),['Parity','Cache']) && (!in_array(_var($disk,'name'),$pools) || isSubpool(_var($disk,'name'))))) { $view = ""; } else { - $view = ""._('Open in File Manager').""; + $view = ""._('Open in File Manager').""; } if (!$showView) $view = $showViewPlaceholder ? "" : ""; if ($showView && _var($options,'forceView',false) && _var($disk,'fsStatus')=='Mounted' && $dir !== '') { - $view = ""._('Open in File Manager').""; + $view = ""._('Open in File Manager').""; } if ($indent) $view = "".$view; $realName = _var($disk,'name'); @@ -1393,7 +1393,7 @@ while (true) { $bootLabel = _($bootPoolMode === 'dedicated' ? 'Internal Boot(Dedicated)' : 'Internal Boot'); $bootDeviceUrl = "/Main/Boot?name=".urlencode($bootPoolName); $bootDir = _var($bootDisk,'fsMountpoint','/boot'); - $bootBrowseUrl = $bootDir !== '' ? "/Main/Browse?dir=".htmlspecialchars($bootDir) : $bootDeviceUrl; + $bootBrowseUrl = $bootDir !== '' ? "/Main/Browse?dir=".rawurlencode($bootDir) : $bootDeviceUrl; $bootIcon = $bootBrowseUrl === $bootDeviceUrl ? "" : ""._('Open in File Manager').""; @@ -1401,7 +1401,7 @@ while (true) { $poolLabel = ucfirst($pool); $poolDeviceUrl = "/Main/Device?name=".urlencode($pool); $poolDir = _var($poolDisk,'fsMountpoint',''); - $poolBrowseUrl = $poolDir !== '' ? "/Main/Browse?dir=".htmlspecialchars($poolDir) : $poolDeviceUrl; + $poolBrowseUrl = $poolDir !== '' ? "/Main/Browse?dir=".rawurlencode($poolDir) : $poolDeviceUrl; $poolIcon = $poolBrowseUrl === $poolDeviceUrl ? "" : ""._('Open in File Manager')."";