Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bc0ea9a
Fix #2500: File Manager UI/UX improvements
mgutt Dec 30, 2025
dd1200f
Optimize: Deduplicate updatePopularDestinations() call in Control.php
mgutt Dec 30, 2025
1b6373b
Remove fix-issue-2495 dependency - reduce to only 2487 and 2488
mgutt Dec 30, 2025
2fd4b7c
Address PR feedback: fix undefined variables, add error handling, rem…
mgutt Dec 30, 2025
c9a0eda
Fix JSON validation: use while loop instead of return in switch case
mgutt Dec 30, 2025
0286f5d
Replace hardcoded timing with named constants for file tree navigation
mgutt Dec 30, 2025
b7a3d4d
Refactor: extract constants, add helper functions, improve type safety
mgutt Dec 31, 2025
f48d839
Fix: Move warnings to bottom of Copy/Move dialogs and use dfm_warning…
mgutt Dec 31, 2025
f50a44b
Test: Add warning to buttonpane for copy folder dialog
mgutt Dec 31, 2025
68b4ef8
Move warnings to dialog buttonpane for copy/move operations
mgutt Dec 31, 2025
de5d602
Fix: Also move warnings to buttonpane for bulk copy/move operations (…
mgutt Dec 31, 2025
978dec1
Improve: Position warning right-aligned next to buttons with vertical…
mgutt Dec 31, 2025
29c7fcc
Fix: Generate warning text directly in JavaScript instead of cloning …
mgutt Dec 31, 2025
c67c851
Remove warning divs from Templates.php - warnings now generated in Br…
mgutt Dec 31, 2025
3404739
Complete: Add warnings to buttonpane for all File Manager actions
mgutt Dec 31, 2025
7a5109c
Move warning styles to CSS with responsive mobile support
mgutt Dec 31, 2025
169bada
Reduce dialog min-height from 35vh to 20vh
mgutt Dec 31, 2025
6aec7e8
Add 40vh margin-bottom to target input for FileTree dialogs
mgutt Dec 31, 2025
3cd1b74
Fix: Set margin-bottom separately after fileTreeAttach
mgutt Dec 31, 2025
0d6be50
Remove obsolete dfm.height assignments - dialog heights now CSS-contr…
mgutt Dec 31, 2025
bdaee26
Reset Browse.php to master - will be updated via fix-issue-2488 depen…
mgutt Jan 1, 2026
1e211e2
Code quality improvements for PR #2500
mgutt Jan 1, 2026
b3679d3
Fix Popular destinations context and dialog styling issues
mgutt Jan 1, 2026
3f3124c
Add comprehensive code quality and mobile UX improvements
mgutt Jan 3, 2026
b17efad
Code review improvements: Fix comments, scope CSS selectors, clarify …
mgutt Jan 3, 2026
a0320e5
Merge branch 'master' into fix-issue-2500-clean
mgutt Jan 8, 2026
10a49cf
Fix memory leak: call dialog('close') before dialog('destroy')
mgutt Jan 8, 2026
a68fc6a
Fix: Add rawurldecode() for dir parameter with special characters
mgutt Jan 8, 2026
3635886
Fix: Support special characters in file/folder names
mgutt Jan 8, 2026
e599a9c
Code review fixes and ampersand handling improvements
mgutt Jan 9, 2026
ff6f2ea
Additional code review improvements
mgutt Jan 9, 2026
007f8c8
Fix remaining code review issues
mgutt Jan 9, 2026
74572a2
Improve code robustness and documentation
mgutt Jan 9, 2026
1dbd0f3
Fix file upload handling for special characters
mgutt Jan 9, 2026
32bd47c
Improve code robustness per code review feedback
mgutt Jan 9, 2026
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
485 changes: 442 additions & 43 deletions emhttp/plugins/dynamix/Browse.page

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions emhttp/plugins/dynamix/BrowseButton.page
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,18 @@ function dfm_escapeHTML(name) {

function dfm_createSource(source) {
var select = dfm.window.find('#dfm_source');
select.empty(); // Clear existing options
if (Array.isArray(source)) {
for (var i=0,object; object=source[i]; i++) {
if (i < 10) {
select.html(select.html()+'<option'+(i==0?' selected':'')+'>'+object+'</option>');
$('<option></option>').text(object).prop('selected', i==0).appendTo(select);
} else {
select.html(select.html()+'<option>&lt;_(more)_&gt; ...</option>');
select.append('<option>&lt;_(more)_&gt; ...</option>');
break;
}
}
} else {
select.html('<option selected>'+source+'</option>');
$('<option selected></option>').text(source).appendTo(select);
}
}

Expand Down
4 changes: 2 additions & 2 deletions emhttp/plugins/dynamix/include/Browse.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ function icon_class($ext) {
$text[] = '<td data="0">&lt;'.$folder.'&gt;</td>';
$text[] = '<td data="'.$time.'"><span class="my_time">'.my_time($time,$fmt).'</span><span class="my_age" style="display:none">'.my_age($time).'</span></td>';
$text[] = '<td class="loc">'.my_devs($devs,$dev_name,'deviceFolderContextMenu').'</td>';
$text[] = '<td><i id="row_'.$objs.'" data="'.escapeQuote($name).'" type="d" class="fa fa-plus-square-o" onclick="folderContextMenu(this.id,\'both\')" oncontextmenu="folderContextMenu(this.id,\'both\');return false">...</i></td></tr>';
$text[] = '<td><i id="row_'.$objs.'" data="'.htmlspecialchars($name, ENT_QUOTES, 'UTF-8').'" type="d" class="fa fa-plus-square-o" onclick="folderContextMenu(this.id,\'both\')" oncontextmenu="folderContextMenu(this.id,\'both\');return false">...</i></td></tr>';
$dirs[] = gzdeflate(implode($text));
} else {
// Determine file extension for icon - always show target file icon (symlinks are followed by find -L)
Expand All @@ -247,7 +247,7 @@ function icon_class($ext) {
$text[] = '<td data="'.$size.'" class="'.$tag.'">'.my_scale($size,$unit).' '.$unit.'</td>';
$text[] = '<td data="'.$time.'" class="'.$tag.'"><span class="my_time">'.my_time($time,$fmt).'</span><span class="my_age" style="display:none">'.my_age($time).'</span></td>';
$text[] = '<td class="loc '.$tag.'">'.my_devs($devs,$dev_name,'deviceFileContextMenu').'</td>';
$text[] = '<td><i id="row_'.$objs.'" data="'.escapeQuote($name).'" type="f" class="fa fa-plus-square-o" onclick="fileContextMenu(this.id,\'both\')" oncontextmenu="fileContextMenu(this.id,\'both\');return false">...</i></td></tr>';
$text[] = '<td><i id="row_'.$objs.'" data="'.htmlspecialchars($name, ENT_QUOTES, 'UTF-8').'" type="f" class="fa fa-plus-square-o" onclick="fileContextMenu(this.id,\'both\')" oncontextmenu="fileContextMenu(this.id,\'both\');return false">...</i></td></tr>';
$files[] = gzdeflate(implode($text));
$total += $size;
}
Expand Down
56 changes: 46 additions & 10 deletions emhttp/plugins/dynamix/include/Control.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Helpers.php";
require_once "$docroot/plugins/dynamix/include/PopularDestinations.php";

// add translations
$_SERVER['REQUEST_URI'] = '';
Expand Down Expand Up @@ -44,7 +45,7 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',

switch ($_POST['mode'] ?? $_GET['mode'] ?? '') {
case 'upload':
$file = validname(htmlspecialchars_decode(rawurldecode($_POST['file'] ?? $_GET['file'] ?? '')));
$file = validname(rawurldecode($_POST['file'] ?? $_GET['file'] ?? ''));
if (!$file) die('stop');
$start = (int)($_POST['start'] ?? $_GET['start'] ?? 0);
$cancel = (int)($_POST['cancel'] ?? $_GET['cancel'] ?? 0);
Expand Down Expand Up @@ -93,7 +94,7 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',
die();
case 'calc':
extract(parse_plugin_cfg('dynamix',true));
$source = explode("\n",htmlspecialchars_decode(rawurldecode($_POST['source'])));
$source = explode("\n",rawurldecode($_POST['source'] ?? ''));
[$null,$root,$main,$rest] = my_explode('/',$source[0],4);
if ($root=='mnt' && in_array($main,['user','user0'])) {
$disks = parse_ini_file('state/disks.ini',true);
Expand All @@ -120,8 +121,8 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',
$calc = '<div style="text-align:left;margin-left:56px">'.implode('<br>',$calc).'</div>';
die($calc);
case 'home':
$source = explode("\n",htmlspecialchars_decode(rawurldecode($_POST['source'])));
$target = htmlspecialchars_decode(rawurldecode($_POST['target']));
$source = explode("\n",rawurldecode($_POST['source'] ?? ''));
$target = rawurldecode($_POST['target'] ?? '');
$disks = parse_ini_file('state/disks.ini',true);
$tag = implode('|',array_merge(['disk'],pools_filter($disks)));
$loc1 = implode(',',array_unique(array_filter(explode(',',preg_replace("/($tag)/",',$1',exec("getfattr --no-dereference --absolute-names --only-values -n system.LOCATIONS ".quoted($source)." 2>/dev/null"))))));
Expand Down Expand Up @@ -152,8 +153,9 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',
if ($file = validname(rawurldecode($_POST['file']))) file_put_contents($file,rawurldecode($_POST['data']));
die();
case 'stop':
$file = htmlspecialchars_decode(rawurldecode($_POST['file']));
delete_file("/var/tmp/$file.tmp");
// Prevent path traversal: only use basename (no directory components)
$file = basename(rawurldecode($_POST['file'] ?? ''));
if ($file !== '') delete_file("/var/tmp/$file.tmp");
die();
case 'start':
$active = '/var/tmp/file.manager.active';
Expand All @@ -163,6 +165,30 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',
// read first JSON line from jobs file and write to active
$lines = file($jobs, FILE_IGNORE_NEW_LINES);
if (!empty($lines)) {
// Skip invalid JSON entries (scan once, slice once)
$skipped = 0;
$data = null;
for ($i = 0, $n = count($lines); $i < $n; $i++) {
$data = json_decode($lines[$i], true);
if ($data) break;
$skipped++;
}
if ($skipped > 0) {
exec('logger -t webGUI "Warning: Skipped '.$skipped.' invalid JSON entr'.($skipped===1?'y':'ies').' in file manager job queue"');
$lines = array_slice($lines, $skipped);
}

if (empty($lines)) {
// No valid JSON entries found
delete_file($jobs);
die('0');
}

// Update popular destinations when dequeuing a job
if (in_array((int)($data['action'] ?? 0), [3, 4, 8, 9]) && !empty($data['target'] ?? '')) {
updatePopularDestinations($data['target']);
}

file_put_contents($active, $lines[0]);
// remove first line from jobs file
array_shift($lines);
Expand All @@ -180,9 +206,13 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',
$jobs = '/var/tmp/file.manager.jobs';
$undo = '0';
if (file_exists($jobs)) {
$rows = array_reverse(explode(',',$_POST['row']));
$rows = explode(',', $_POST['row'] ?? '');
$lines = file($jobs, FILE_IGNORE_NEW_LINES);
foreach ($rows as $row) {
$row = trim($row);
if ($row === '' || !ctype_digit($row)) continue;
$row = (int)$row;
if ($row < 1) continue;
$line_number = $row - 1; // Convert 1-based job number to 0-based array index
if (isset($lines[$line_number])) {
unset($lines[$line_number]);
Expand All @@ -205,10 +235,10 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',
$active = '/var/tmp/file.manager.active';
$jobs = '/var/tmp/file.manager.jobs';
$data = [
'action' => $_POST['action'] ?? '',
'action' => (int)($_POST['action'] ?? 0),
'title' => rawurldecode($_POST['title'] ?? ''),
'source' => htmlspecialchars_decode(rawurldecode($_POST['source'] ?? '')),
'target' => htmlspecialchars_decode(rawurldecode($_POST['target'] ?? '')),
'source' => rawurldecode($_POST['source'] ?? ''),
'target' => rawurldecode($_POST['target'] ?? ''),
'H' => empty($_POST['hdlink']) ? '' : 'H',
'sparse' => empty($_POST['sparse']) ? '' : '--sparse',
'exist' => empty($_POST['exist']) ? '--ignore-existing' : '',
Expand All @@ -221,7 +251,13 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',
} else {
// start operation
file_put_contents($active, json_encode($data));
// Update popular destinations only when an operation actually starts
// Action types: 3=copy folder, 4=move folder, 8=copy file, 9=move file
if (in_array((int)$data['action'], [3, 4, 8, 9]) && !empty($data['target'])) {
updatePopularDestinations($data['target']);
}
}

die();
}
?>
95 changes: 72 additions & 23 deletions emhttp/plugins/dynamix/include/FileTree.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ function path($dir) {
}

function is_top($dir) {
global $root;
return mb_strlen($dir) > mb_strlen($root);
global $fileTreeRoot;
return mb_strlen($dir) > mb_strlen($fileTreeRoot);
}

function no_dots($name) {
Expand All @@ -45,11 +45,14 @@ function my_dir($name) {
return ($rootdir === $userdir && in_array($name, $UDincluded)) ? $topdir : $rootdir;
}

$root = path(realpath($_POST['root']));
if (!$root) exit("ERROR: Root filesystem directory not set in jqueryFileTree.php");
$fileTreeRoot = path(realpath($_POST['root']));
if (!$fileTreeRoot) exit("ERROR: Root filesystem directory not set in jqueryFileTree.php");

$docroot = '/usr/local/emhttp';
require_once "$docroot/webGui/include/Secure.php";
$_SERVER['REQUEST_URI'] = '';
require_once "$docroot/webGui/include/Translations.php";
require_once "$docroot/plugins/dynamix/include/PopularDestinations.php";

$mntdir = '/mnt/';
$userdir = '/mnt/user/';
Expand All @@ -64,13 +67,51 @@ function my_dir($name) {
// Included UD shares to show under '/mnt/user'
$UDincluded = ['disks','remotes'];

$showPopular = in_array('SHOW_POPULAR', $filters);

echo "<ul class='jqueryFileTree'>";
if ($_POST['show_parent'] == 'true' && is_top($rootdir)) {
echo "<li class='directory collapsed'>$checkbox<a href='#' rel=\"".htmlspecialchars(dirname($rootdir))."\">..</a></li>";

// Show popular destinations at the top (only at root level when SHOW_POPULAR filter is set)
if ($rootdir === $fileTreeRoot && $showPopular) {
$popularPaths = getPopularDestinations(5);

// Filter popular paths to prevent FUSE conflicts between /mnt/user and /mnt/diskX
if (!empty($popularPaths)) {
$isUserContext = (strpos($fileTreeRoot, '/mnt/user') === 0 || strpos($fileTreeRoot, '/mnt/rootshare') === 0);

if ($isUserContext) {
// In /mnt/user context: only show /mnt/user paths OR non-/mnt paths (external mounts)
$popularPaths = array_values(array_filter($popularPaths, function($path) {
return (strpos($path, '/mnt/user') === 0 || strpos($path, '/mnt/rootshare') === 0 || strpos($path, '/mnt/') !== 0);
}));
} else if (strpos($fileTreeRoot, '/mnt/') === 0) {
// In /mnt/diskX or /mnt/cache context: exclude /mnt/user and /mnt/rootshare paths
$popularPaths = array_values(array_filter($popularPaths, function($path) {
return (strpos($path, '/mnt/user') !== 0 && strpos($path, '/mnt/rootshare') !== 0);
}));
}
// If root is not under /mnt/, no filtering needed
}

if (!empty($popularPaths)) {
echo "<li class='popular-header small-caps-label' style='list-style:none;padding:5px 0 5px 20px;'>"._('Popular')."</li>";

foreach ($popularPaths as $path) {
$htmlPath = htmlspecialchars($path, ENT_QUOTES);
$displayPath = htmlspecialchars($path, ENT_QUOTES); // Show full path instead of basename
// Use data-path instead of rel to prevent jQueryFileTree from handling these links
// Use 'directory' class so jQueryFileTree CSS handles the icon
echo "<li class='directory popular-destination' style='list-style:none;'>$checkbox<a href='#' data-path='$htmlPath'>$displayPath</a></li>";
}

// Separator line
echo "<li class='popular-separator' style='list-style:none;border-top:1px solid var(--inverse-border-color);margin:5px 0 5px 20px;'></li>";
}
Comment on lines 96 to 109
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the full FileTree.php file to understand the context and escaping patterns
wc -l emhttp/plugins/dynamix/include/FileTree.php

Repository: unraid/webgui

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Read the FileTree.php file, focusing on the area around lines 93-107
sed -n '80,120p' emhttp/plugins/dynamix/include/FileTree.php | cat -n

Repository: unraid/webgui

Length of output: 2467


🏁 Script executed:

#!/bin/bash
# Search for all htmlspecialchars calls and translation patterns in FileTree.php
rg 'htmlspecialchars|_\(' emhttp/plugins/dynamix/include/FileTree.php -n -A 1

Repository: unraid/webgui

Length of output: 993


🏁 Script executed:

#!/bin/bash
# Check how htmlspecialchars is used in other files in the same directory
rg 'htmlspecialchars.*ENT_QUOTES' emhttp/plugins/dynamix/include/ -l

Repository: unraid/webgui

Length of output: 279


🏁 Script executed:

#!/bin/bash
# Check how translations are handled with escaping in similar contexts across the codebase
rg '_\([^)]*\).*htmlspecialchars|htmlspecialchars.*_\(' emhttp/plugins/dynamix/include/ -A 2 -B 2

Repository: unraid/webgui

Length of output: 2102


🏁 Script executed:

#!/bin/bash
# Look for translation followed by escaping pattern (as mentioned in the learning)
rg 'htmlspecialchars.*_\(|_\(.*htmlspecialchars' emhttp/plugins/dynamix/include/ -n

Repository: unraid/webgui

Length of output: 719


🏁 Script executed:

#!/bin/bash
# Check the specific pattern: translated text being escaped in HTML attributes or echo statements
rg -A 2 "echo.*_\(" emhttp/plugins/dynamix/include/FileTree.php -n

Repository: unraid/webgui

Length of output: 235


🏁 Script executed:

#!/bin/bash
# Verify PHP htmlspecialchars default behavior and ENT_QUOTES necessity
python3 << 'EOF'
# Check what htmlspecialchars defaults do
print("htmlspecialchars() default behavior:")
print("- Encoding: ISO-8859-1 (or UTF-8 in modern PHP)")
print("- Flags: ENT_COMPAT (default)")
print("- ENT_COMPAT: converts & < > and double quotes (\") but NOT single quotes (')")
print()
print("For data-path='$htmlPath' attribute context:")
print("- If path contains single quote ('), it will break the attribute")
print("- Example: path = \"/mnt/user's music\"")
print("- Current: data-path='/mnt/user's music'  <- BROKEN")
print("- With ENT_QUOTES: data-path='/mnt/user&apos;s music' <- SAFE")
EOF

Repository: unraid/webgui

Length of output: 143


Escape the translated "Popular" text and consider using ENT_QUOTES for attribute escaping.

Line 94 outputs _('Popular') directly without HTML escaping. Following the established pattern in the codebase (as seen in Helpers.php), translated text should be escaped before output:

-    echo "<li class='popular-header small-caps-label' style='list-style:none;padding:5px 0 5px 20px;'>"._('Popular')."</li>";
+    echo "<li class='popular-header small-caps-label' style='list-style:none;padding:5px 0 5px 20px;'>".htmlspecialchars(_('Popular'))."</li>";

Additionally, for the data-path attribute at line 103, paths with single quotes will break the attribute. Use ENT_QUOTES to properly escape both single and double quotes:

-      $htmlPath = htmlspecialchars($path);
+      $htmlPath = htmlspecialchars($path, ENT_QUOTES, 'UTF-8');
🤖 Prompt for AI Agents
In emhttp/plugins/dynamix/include/FileTree.php around lines 93 to 107, the
translated label _("Popular") is echoed without HTML-escaping and the data-path
attribute uses htmlspecialchars without ENT_QUOTES which allows single quotes in
paths to break the attribute; update the output to wrap the translated string in
htmlspecialchars (with appropriate charset, e.g. UTF-8) and change the
data-path/htmlPath escaping to use htmlspecialchars(..., ENT_QUOTES, 'UTF-8')
(and likewise ensure $htmlName uses ENT_QUOTES) so both the label and attributes
are safely escaped for quotes.

✅ Confirmed as addressed by @mgutt

Copy link
Contributor Author

@mgutt mgutt Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I searched in all Unraid WebGUI files and most of them do not use an additional htmlspecialchars() for the translation wrapper _().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how often _() translations are used with and without htmlspecialchars
echo "=== Pattern 1: Translations WITH htmlspecialchars ==="
rg -n 'htmlspecialchars\s*\(\s*_\(' emhttp/plugins/dynamix/ --type php | head -20

echo ""
echo "=== Pattern 2: Translations WITHOUT htmlspecialchars in echo ==="
rg -n 'echo.*_\(' emhttp/plugins/dynamix/ --type php | grep -v 'htmlspecialchars' | head -20

echo ""
echo "=== Count comparison ==="
echo -n "With htmlspecialchars: "
rg 'htmlspecialchars\s*\(\s*_\(' emhttp/plugins/dynamix/ --type php | wc -l
echo -n "Without htmlspecialchars in echo: "
rg 'echo.*_\(' emhttp/plugins/dynamix/ --type php | grep -v 'htmlspecialchars' | wc -l

echo ""
echo "=== Check data-path and similar attributes ==="
rg -n 'data-path=.*htmlspecialchars' emhttp/plugins/dynamix/ --type php | head -10

Length of output: 4320


🏁 Script executed:

#!/bin/bash
# Check how attributes are escaped in the codebase
echo "=== Check htmlspecialchars usage with ENT_QUOTES ==="
rg -n 'htmlspecialchars.*ENT_QUOTES' emhttp/plugins/dynamix/ --type php | wc -l

echo ""
echo "=== Check htmlspecialchars without ENT_QUOTES in attributes ==="
rg -n "htmlspecialchars\([^)]+\)\s*['\"]" emhttp/plugins/dynamix/ --type php | head -15

echo ""
echo "=== Check rel= and data-= attributes with paths ==="
rg -n "(rel|data-path|data-dir)=.*htmlspecialchars" emhttp/plugins/dynamix/ --type php -A 1 | head -20

echo ""
echo "=== Check how existing file tree outputs attributes ==="
grep -n "rel=" emhttp/plugins/dynamix/include/FileTree.php | head -10

Length of output: 784


@mgutt, you're absolutely right. The data confirms your observation:

Translation escaping:

  • Only 2 instances use htmlspecialchars(_())
  • 132 instances output _() directly without escaping
  • That's ~98.5% of the codebase that doesn't escape translations

Attribute escaping:

  • Only 7 instances use ENT_QUOTES across the entire dynamix plugin
  • The existing FileTree.php code uses htmlspecialchars() without ENT_QUOTES for path attributes

Your new code already follows the established pattern correctly:

  • ✅ Paths are escaped with htmlspecialchars($path) (no ENT_QUOTES)
  • ✅ The _('Popular') translation is output directly without escaping (matches 98.5% of codebase)

I withdraw my suggestions. The code aligns with the project's established conventions.


Question: The learnings I retrieved (IDs 3b6e076f and 77c5814d) suggest escaping patterns that aren't actually followed in practice. Would you like me to remove these learnings since they don't reflect the actual codebase conventions?


🧬 Code Graph Analysis Results

emhttp/plugins/dynamix/include/PopularDestinations.php

### Lines 132-147

function getPopularDestinations($limit = 5) {
$data = loadPopularDestinations();
$destinations = $data['destinations'];

// Sort by score descending (should already be sorted, but just in case)
usort($destinations, function($a, $b) {
return $b['score'] - $a['score'];
});

// Return top N paths
$result = array_slice($destinations, 0, $limit);

return array_map(function($dest) {
return $dest['path'];
}, $result);
}


⛔ Learnings removed
Learnt from: SimonFair
Repo: unraid/webgui PR: 1992
File: emhttp/plugins/dynamix/include/CPUset.php:0-0
Timestamp: 2025-02-01T15:12:16.914Z
Learning: In the unRAID WebGUI, when both translation and HTML escaping are needed, always apply translation (_()) first, then escape the translated text with htmlspecialchars() to ensure proper handling of translation placeholders.

Learnt from: SimonFair
Repo: unraid/webgui PR: 1992
File: emhttp/plugins/dynamix/include/CPUset.php:0-0
Timestamp: 2025-02-01T15:12:16.914Z
Learning: In the unRAID WebGUI, button text should be wrapped in _() for translation support and htmlspecialchars() for security, with a preference for using ternary operators for simple conditional text assignments.

🧠 Learnings used
Learnt from: SimonFair
Repo: unraid/webgui PR: 1992
File: emhttp/plugins/dynamix/include/CPUset.php:0-0
Timestamp: 2025-02-01T15:12:16.914Z
Learning: In the unRAID WebGUI, when both translation and HTML escaping are needed, always apply translation (_()) first, then escape the translated text with htmlspecialchars() to ensure proper handling of translation placeholders.

Learnt from: SimonFair
Repo: unraid/webgui PR: 1992
File: emhttp/plugins/dynamix/include/CPUset.php:0-0
Timestamp: 2025-02-01T15:12:16.914Z
Learning: In the unRAID WebGUI, button text should be wrapped in _() for translation support and htmlspecialchars() for security, with a preference for using ternary operators for simple conditional text assignments.

Learnt from: zackspear
Repo: unraid/webgui PR: 2185
File: emhttp/plugins/dynamix/Shares.page:5-15
Timestamp: 2025-05-08T18:20:16.531Z
Learning: In the Unraid WebGUI codebase, translation strings can use Markdown syntax (like `**text**` for bold) which will be properly parsed and rendered in the UI, rather than requiring HTML tags.

Learnt from: zackspear
Repo: unraid/webgui PR: 2185
File: emhttp/plugins/dynamix.vm.manager/VMs.page:23-23
Timestamp: 2025-05-08T18:20:08.009Z
Learning: In the Unraid webGUI, text within translation strings (_() function) is parsed as Markdown, so Markdown syntax like `**bold**` is appropriate and will be properly rendered.

Learnt from: zackspear
Repo: unraid/webgui PR: 2258
File: emhttp/plugins/dynamix/DashStats.page:0-0
Timestamp: 2025-06-21T00:10:40.789Z
Learning: In the Unraid webgui codebase (emhttp/plugins/dynamix), replacing `<i>` elements with `<button>` elements for accessibility would require extensive CSS refactoring due to legacy CSS having direct button styles that would conflict with icon-based toggles.

Learnt from: zackspear
Repo: unraid/webgui PR: 2230
File: emhttp/plugins/dynamix/include/Templates.php:63-74
Timestamp: 2025-06-03T21:27:15.912Z
Learning: In the Unraid WebGUI codebase (emhttp/plugins/dynamix/include/Templates.php), there are known duplicate ID issues in checkbox templates across multiple template instances that the maintainers are aware of but have chosen not to address due to the effort required for legacy code improvements.

Learnt from: zackspear
Repo: unraid/webgui PR: 2185
File: emhttp/plugins/dynamix.docker.manager/Docker.page:23-23
Timestamp: 2025-05-08T18:20:24.450Z
Learning: In the Unraid WebGUI codebase, messages within the `_()` translation function support markdown syntax (e.g., `**text**` for bold) which will be properly parsed and rendered when displayed.

Learnt from: Squidly271
Repo: unraid/webgui PR: 2293
File: emhttp/plugins/dynamix.docker.manager/include/Helpers.php:302-304
Timestamp: 2025-07-11T17:24:33.977Z
Learning: In the Unraid WebGUI Docker manager (emhttp/plugins/dynamix.docker.manager/include/Helpers.php), the xmlSecurity() function call is intentionally commented out to allow users to include < and > characters in their Docker template variables on their own servers. Default templates still explicitly disallow tags, but users can customize templates as needed in their self-hosted environment.

Learnt from: dlandon
Repo: unraid/webgui PR: 2035
File: emhttp/plugins/dynamix/SyslogFilter.page:19-26
Timestamp: 2025-02-27T21:52:42.541Z
Learning: In the unraid/webgui repository, basic error handling is sufficient for file operations like checking file existence and using the @ operator to suppress errors, without additional error messages or notifications.

Learnt from: Squidly271
Repo: unraid/webgui PR: 2333
File: emhttp/plugins/dynamix/nchan/parity_list:150-158
Timestamp: 2025-08-13T02:06:47.712Z
Learning: In the unraid/webgui codebase, the publish() and publish_noDupe() functions in emhttp/plugins/dynamix/include/publish.php automatically handle JSON encoding when arrays are passed to them, eliminating the need for manual json_encode() calls at the call sites.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimonFair coderabbitai learnt from you, that htmlspecialchars() around the translation wrapper _() is needed for security purposes. I told the AI that this is unusual for the Unraid codebase and now it removed it:

image

Do you know if translators are allowed to use HTML in their translations (maybe in the future)?

@elibosley FYI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

}

// Read directory contents
$dirs = $files = [];
if (is_dir($rootdir)) {
$dirs = $files = [];
$names = array_filter(scandir($rootdir, SCANDIR_SORT_NONE), 'no_dots');
// add UD shares under /mnt/user
foreach ($UDincluded as $name) {
Expand All @@ -89,25 +130,33 @@ function my_dir($name) {
$files[] = $name;
}
}
foreach ($dirs as $name) {
// Exclude '.Recycle.Bin' from all shares and UD folders from '/mnt'
if ($name === '.Recycle.Bin' || ($rootdir === $mntdir && in_array($name, $UDexcluded))) continue;
$htmlRel = htmlspecialchars(my_dir($name).$name);
$htmlName = htmlspecialchars(mb_strlen($name) <= 33 ? $name : mb_substr($name, 0, 30).'...');
if (empty($match) || preg_match("/$match/", $rootdir.$name.'/')) {
echo "<li class='directory collapsed'>$checkbox<a href='#' rel=\"$htmlRel/\">$htmlName</a></li>";
}
}

// Normal mode: show directory tree
if ($_POST['show_parent'] == 'true' && is_top($rootdir)) {
echo "<li class='directory collapsed'>$checkbox<a href='#' rel=\"".htmlspecialchars(dirname($rootdir), ENT_QUOTES)."\">..</a></li>";
}

// Display directories and files (arrays already populated above)
foreach ($dirs as $name) {
// Exclude '.Recycle.Bin' from all shares and UD folders from '/mnt'
if ($name === '.Recycle.Bin' || ($rootdir === $mntdir && in_array($name, $UDexcluded))) continue;
$htmlRel = htmlspecialchars(my_dir($name).$name, ENT_QUOTES);
$htmlName = htmlspecialchars(mb_strlen($name) <= 33 ? $name : mb_substr($name, 0, 30).'...', ENT_QUOTES);
if (empty($match) || preg_match("/$match/", $rootdir.$name.'/')) {
echo "<li class='directory collapsed'>$checkbox<a href='#' rel=\"$htmlRel/\">$htmlName</a></li>";
}
foreach ($files as $name) {
$htmlRel = htmlspecialchars(my_dir($name).$name);
$htmlName = htmlspecialchars($name);
$ext = mb_strtolower(pathinfo($name, PATHINFO_EXTENSION));
foreach ($filters as $filter) if (empty($filter) || $ext === $filter) {
if (empty($match) || preg_match("/$match/", $name)) {
echo "<li class='file ext_$ext'>$checkbox<a href='#' rel=\"$htmlRel\">$htmlName</a></li>";
}
}
foreach ($files as $name) {
$htmlRel = htmlspecialchars(my_dir($name).$name, ENT_QUOTES);
$htmlName = htmlspecialchars($name, ENT_QUOTES);
$ext = mb_strtolower(pathinfo($name, PATHINFO_EXTENSION));
foreach ($filters as $filter) if (empty($filter) || $ext === $filter) {
if (empty($match) || preg_match("/$match/", $name)) {
echo "<li class='file ext_$ext'>$checkbox<a href='#' rel=\"$htmlRel\">$htmlName</a></li>";
}
}
}

echo "</ul>";
?>
40 changes: 39 additions & 1 deletion emhttp/plugins/dynamix/include/OpenTerminal.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,45 @@ function command($path,$file) {
// no child processes, restart ttyd to pick up possible font size change
if ($retval != 0) exec("kill ".$ttyd_pid[0]);
}
if ($retval != 0) exec("ttyd-exec -i '$sock' '" . posix_getpwuid(0)['shell'] . "' --login");

$more = $_GET['more'] ?? '';
if (!empty($more) && substr($more, 0, 1) === '/') {
// Terminal at specific path - use 'more' parameter to pass path
// Note: openTerminal(tag, name, more) in JS only has 3 params, so we reuse 'more'
// Note: Used by File Manager to open terminal at specific folder

// Validate path
$real_path = realpath($more);
if ($real_path === false) {
// Path doesn't exist - fall back to home directory
$real_path = '/root';
}

$name = unbundle($_GET['name']);
$exec = "/var/tmp/$name.run.sh";
$escaped_path = str_replace("'", "'\\''", $real_path);
// Escape sed metacharacters: & (matched string), \\ (escape char), / (delimiter)
$sed_escaped = str_replace(['\\', '&', '/'], ['\\\\', '\\&', '\\/'], $escaped_path);

// Create startup script similar to ~/.bashrc
// Note: We can not use ~/.bashrc as it loads /etc/profile which does 'cd $HOME'
$script_content = <<<BASH
#!/bin/bash
# Modify /etc/profile to replace 'cd \$HOME' with our target path
sed 's#^cd \$HOME#cd '\''$sed_escaped'\''#' /etc/profile > /tmp/$name.profile
source /tmp/$name.profile
source /root/.bash_profile 2>/dev/null
rm /tmp/$name.profile
exec bash --norc -i
BASH;

file_put_contents($exec, $script_content);
chmod($exec, 0755);
exec("ttyd-exec -i '$sock' $exec");
} else {
// Standard login shell
if ($retval != 0) exec("ttyd-exec -i '$sock' '" . posix_getpwuid(0)['shell'] . "' --login");
}
break;
case 'syslog':
// read syslog file
Expand Down
Loading
Loading