Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions emhttp/plugins/dynamix/Browse.page
Original file line number Diff line number Diff line change
Expand Up @@ -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/<share> -> /mnt/<pool|disk>/<share>,
// 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/<disk> 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));
Expand Down
19 changes: 17 additions & 2 deletions emhttp/plugins/dynamix/include/Browse.php
Original file line number Diff line number Diff line change
Expand Up @@ -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/<share> -> /mnt/<pool|disk>/<share>,
// 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/<disk> 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) {
Expand Down
2 changes: 1 addition & 1 deletion emhttp/plugins/dynamix/include/DiskList.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function sharesOnly($disk) {
case 2: $luks = "<a class='info' onclick='return false'><i class='padlock fa fa-unlock-alt orange-text'></i><span>"._('Some or all files unencrypted')."</span></a>"; break;
default: $luks = "<a class='info' onclick='return false'><i class='padlock fa fa-lock red-text'></i><span>"._('Unknown encryption state')."</span></a>"; break;
} else $luks = "";
echo "<tr><td><a class='view' href=\"/$path/Browse?dir=/mnt/$name\"><i class=\"icon-u-tab\" title=\"",_('Browse')," /mnt/$name\"></i></a>";
echo "<tr><td><a class='view info' href=\"/$path/Browse?dir=/mnt/",rawurlencode($name),"\"><i class=\"fa fa-folder-o\"></i><span>",_('Open in File Manager'),"</span></a>";
echo "<a class='info nohand' onclick='return false'><i class='fa fa-$orb orb $color-orb'></i><span style='left:18px'>$help</span></a>$luks<a href=\"/$path/Disk?name=$name\" onclick=\"$.cookie('one','tab1')\">$name</a></td>";
echo "<td>",htmlspecialchars(_var($disk,'comment')),"</td>";
echo "<td>",disk_share_settings(_var($var,'shareSMBEnabled'), $sec[$name]),"</td>";
Expand Down
26 changes: 24 additions & 2 deletions emhttp/plugins/dynamix/include/FileTree.php
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pool>.
$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);
Expand All @@ -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";
Expand All @@ -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'];
Expand Down
2 changes: 1 addition & 1 deletion emhttp/plugins/dynamix/include/ShareList.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ function shareInclude($name) {
}
}

echo "<tr><td><a class='view' href=\"/$path/Browse?dir=/mnt/user/", htmlspecialchars($name), "\"><i class=\"icon-u-tab\" title=\"", _('Browse'), " /mnt/user/" . htmlspecialchars($name), "\"></i></a>";
echo "<tr><td><a class='view info' href=\"/$path/Browse?dir=/mnt/user/", rawurlencode($name), "\"><i class=\"fa fa-folder-o\"></i><span>", _('Open in File Manager'), "</span></a>";
echo "<a class='info nohand' onclick='return false'><i class='fa fa-$orb orb $color-orb'></i><span style='left:18px'>$help</span></a>$luks<a href=\"/$path/Share?name=";
echo rawurlencode($name), "\" onclick=\"$.cookie('one','tab1')\">$name</a></td>";
echo "<td>", htmlspecialchars(_var($share,'comment')), "</td>";
Expand Down
22 changes: 13 additions & 9 deletions emhttp/plugins/dynamix/nchan/device_list
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,18 @@ function pool_status_html(string $poolName, array $poolstatusData, array $disks)
$displayText = $hasErrors ? _('ONLINE') : _($status_text);
$deviceUrl = "/Main/Device?name=".urlencode($poolName)."#poolsummary";

// 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 = "<a class='info pool-info' href='$deviceUrl'><i class='fa fa-info-circle'></i><span>"._('View pool details')."</span></a>";
$warn = "<a class='info' onclick='return false'><i class='fa fa-warning fa-fw orange-text'></i><span>"._('Check pool status')."</span></a>";
if (!$isOnline) {
return " <a href='$deviceUrl' title='"._('View pool details')."'>("._($status_text).")</a><span title='"._('Check pool status')."'><i class='fa fa-warning fa-fw orange-text'></i></span>";
return " <span class='pool-state'>("._($status_text).")</span> $warn$details";
}
if ($hasErrors) {
return " <a href='$deviceUrl' title='"._('View pool details')."'>($displayText)</a><span title='"._('Check pool status')."'><i class='fa fa-warning fa-fw orange-text'></i></span>";
return " <span class='pool-state'>($displayText)</span> $warn$details";
}

return " <a href='$deviceUrl' title='"._('View pool details')."'>($displayText)</a>";
return " <span class='pool-state'>($displayText)</span> $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
Expand Down Expand Up @@ -503,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 = "<a class='view'></a>";
} else {
$view = "<a class='view' href=\"/Main/Browse?dir=".htmlspecialchars($dir)."\"><i class=\"icon-u-tab\" title=\""._('Browse')." $dir\"></i></a>";
$view = "<a class='view info' href=\"/Main/Browse?dir=".rawurlencode($dir)."\"><i class=\"fa fa-folder-o\"></i><span>"._('Open in File Manager')."</span></a>";
}
if (!$showView) $view = $showViewPlaceholder ? "<a class='view'></a>" : "";
if ($showView && _var($options,'forceView',false) && _var($disk,'fsStatus')=='Mounted' && $dir !== '') {
$view = "<a class='view' href=\"/Main/Browse?dir=".htmlspecialchars($dir)."\"><i class=\"icon-u-tab\" title=\""._('Browse')." $dir\"></i></a>";
$view = "<a class='view info' href=\"/Main/Browse?dir=".rawurlencode($dir)."\"><i class=\"fa fa-folder-o\"></i><span>"._('Open in File Manager')."</span></a>";
}
if ($indent) $view = "<span style='padding-left:{$indent}px'></span>".$view;
$realName = _var($disk,'name');
Expand Down Expand Up @@ -1389,18 +1393,18 @@ 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
? "<a class='view'></a>"
: "<a class='view' href=\"$bootBrowseUrl\"><i class=\"icon-u-tab\" title=\""._('Browse')." $bootDir\"></i></a>";
: "<a class='view info' href=\"$bootBrowseUrl\"><i class=\"fa fa-folder-o\"></i><span>"._('Open in File Manager')."</span></a>";
$bootLink = "<a href=\"$bootDeviceUrl\">"._($bootLabel,3)."</a>";
$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
? "<a class='view'></a>"
: "<a class='view' href=\"$poolBrowseUrl\"><i class=\"icon-u-tab\" title=\""._('Browse')." $poolDir\"></i></a>";
: "<a class='view info' href=\"$poolBrowseUrl\"><i class=\"fa fa-folder-o\"></i><span>"._('Open in File Manager')."</span></a>";
$poolLink = "<a href=\"$poolDeviceUrl\">"._($poolLabel,3)."</a>";
$flashWarn = flash_smb_warning_html(_var($bootDisk,'name',''));
$bootSummaryName = $bootIcon.$bootLink.$flashWarn;
Expand Down
27 changes: 27 additions & 0 deletions emhttp/plugins/dynamix/styles/default-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ 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);
}
/* 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 */
}
/* 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-info {
color: var(--alt-text-color);
margin-left: 4px;
}
a.pool-info:hover {
color: var(--blue-800);
}
i.spacing {
margin-left: -6px;
Expand Down
Loading