Skip to content

Conversation

@mgutt
Copy link
Contributor

@mgutt mgutt commented Dec 26, 2025

  • Properly escape special chars like backslashes, quotes, etc. by using JSON instead of INI files (Control.php, file_manager)
  • Change shell command (find) to support null-terminated output (Browse.php)
  • Decode getfattr octal escapes for LOCATION column lookup
  • Add nl2br() for newline display in filename cells

Fixes #2488

New features:

  • support of linebreaks in filenames (do not break the WebGUI anymore)
  • broken symlinks are highlighted through icon + editor is disabled
  • symlinks are hightlighted through icon
  • hover / press on symlink icon shows tooltip with target path

Example:
image

Summary by CodeRabbit

  • Bug Fixes

    • More robust directory listings: preserves special chars/newlines, decodes attributes correctly, detects and marks broken symlinks (new icon/tooltip) and disables actions for broken links.
    • Improved device/location reporting: LOCATION shows resolved device names and flags multi-device situations.
  • Refactor

    • Job queue now uses JSON-per-line for start/undo/read operations; undo/delete now sends direct row identifiers.
  • Chores

    • Hardened parsing and decoding for filenames and job data.

✏️ Tip: You can customize this high-level summary in your review settings.

- Properly escape backslashes, quotes, and ${ in INI files (Control.php)
- Use find -exec stat --printf with null-terminated output (Browse.php)
- Decode getfattr octal escapes for LOCATION column lookup
- Add nl2br() for newline display in filename cells

Fixes unraid#2488
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 26, 2025

Walkthrough

Replaces stat-based listing with a NULL-delimited find/getfattr pipeline, decodes octal escapes, enumerates and flags broken symlinks, resolves device names from deviceIDs, preserves newlines in names, and migrates file-manager job storage/parsing from INI blocks to JSON-per-line.

Changes

Cohort / File(s) Summary
File listing & rendering
emhttp/plugins/dynamix/include/Browse.php
Replaces line-oriented stat parsing with a NULL-delimited find -L + getfattr stream (8-field records). Adds octal-unescape decoding, nl2br name rendering, symlink enumeration, broken-symlink detection/tooltips, and updates icon mapping (adds broken-symlink). LOCATION rendering now uses resolved device names.
Device / location resolution
emhttp/plugins/dynamix/include/Browse.php
Builds deviceID→dev_name mapping for /mnt/*; derives device(s) for user-shares from path/xattrs or from current disk locks; constructs devs_value supporting multi-device and warning tagging.
Job storage & control logic
emhttp/plugins/dynamix/include/Control.php, emhttp/plugins/dynamix/nchan/file_manager
Switches job queue format from INI-style blocks to JSON-per-line. Parsers use json_decode/json_encode; Start/Undo/Read handlers now manipulate JSON lines (array operations) instead of sed/INI parsing. Consumers guard decoded JSON with is_array before extract.
UI row-id handling
emhttp/plugins/dynamix/Browse.page
Delete action for scheduled jobs now sends the direct row id fragment (id.split('_')[1]) instead of the prior offset-calculated index, altering which queued entries are targeted for undo/delete.
Miscellaneous & comments
emhttp/plugins/dynamix/include/Browse.php, emhttp/plugins/dynamix/include/Control.php
Removes legacy stat/pclose and sed-based edits, adds bounds checks for missing getfattr lines, updates inline comments documenting the NULL-delimited pipeline and JSON job format. No public function signatures changed.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Browser as Client (Browser UI)
  participant Web as emhttp PHP
  participant FS as System (find / getfattr)
  participant Jobs as Jobs file (JSON lines)

  Note over Browser,Web: Directory listing request
  Browser->>Web: GET listing for path
  Web->>FS: run `find -L ... -print0` + `getfattr` (NULL-delimited 8-field stream)
  FS-->>Web: NULL-delimited stream (type,linktype,owner,perms,size,ts,name,devID)
  Web->>Web: parse 8-field groups, unescape octal, map symlinks, detect broken links, resolve devID→dev_name
  Web-->>Browser: Render rows (icons incl. broken, nl2br names, symlink tooltips, LOCATION)

  Note over Browser,Jobs: Job queue operations
  Browser->>Web: POST start / undo (row id or JSON)
  Web->>Jobs: read/modify/write JSON lines (consume/append/remove)
  Jobs-->>Web: updated queue file
  Web-->>Browser: operation result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nibble NULLs, unescape each ghostly name,
I map red chains that once were only shame.
JSON lines march tidy, jobs no longer stray,
Broken links wear badges in the light of day.
Hop — a rabbit's cheer for tidy files and play.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing handling of special characters in filenames by switching to safer encoding mechanisms.
Linked Issues check ✅ Passed The PR addresses all requirements from issue #2488: safe handling of special characters (quotes, backslashes, newlines) and symlink improvements are implemented.
Out of Scope Changes check ✅ Passed All changes directly address the stated objectives: safer filename encoding, null-terminated output, symlink handling, and special character support.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73ea9c2 and 31d59e5.

📒 Files selected for processing (1)
  • emhttp/plugins/dynamix/include/Browse.php
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-30T20:27:59.455Z
Learnt from: mgutt
Repo: unraid/webgui PR: 2501
File: emhttp/plugins/dynamix/Browse.page:1282-1339
Timestamp: 2025-12-30T20:27:59.455Z
Learning: In emhttp/plugins/dynamix/Browse.page, the xlink() function's clipboard functionality is intentionally designed to silently fail on HTTP connections without showing warnings. The path text is pre-selected to allow manual copying, which is preferred over showing repetitive error messages that would annoy HTTP users.

Applied to files:

  • emhttp/plugins/dynamix/include/Browse.php
📚 Learning: 2025-02-01T15:12:16.914Z
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.

Applied to files:

  • emhttp/plugins/dynamix/include/Browse.php
📚 Learning: 2025-12-28T15:54:58.673Z
Learnt from: mgutt
Repo: unraid/webgui PR: 2496
File: emhttp/plugins/dynamix/Browse.page:901-906
Timestamp: 2025-12-28T15:54:58.673Z
Learning: In the unraid/webgui codebase, CSRF validation is centralized in the global auto_prepend_file (local_prepend.php) which runs before every PHP request. Do not add per-script CSRF checks in individual files like Browse.page or Control.php. If a script relies on global validation, ensure it does not duplicate CSRF logic; otherwise extend the central preface to cover the needed checks.

Applied to files:

  • emhttp/plugins/dynamix/include/Browse.php
🧬 Code graph analysis (1)
emhttp/plugins/dynamix/include/Browse.php (4)
emhttp/plugins/dynamix/include/FileTree.php (1)
  • path (30-32)
emhttp/plugins/dynamix/include/SmartInfo.php (1)
  • size (30-32)
emhttp/plugins/dynamix/include/Helpers.php (1)
  • my_scale (19-39)
emhttp/plugins/dynamix/include/Control.php (1)
  • my_age (25-35)
🔇 Additional comments (8)
emhttp/plugins/dynamix/include/Browse.php (8)

85-86: LGTM! Broken symlink icon is appropriate.

The red broken chain icon correctly represents a broken symlink visually.


152-165: LGTM! Octal escape decoding and bounds checking are correct.

The implementation properly handles getfattr output with:

  • Octal escape decoding to match actual filenames
  • Bounds checking to skip incomplete/malformed records
  • Graceful handling of missing attribute lines

168-179: LGTM! Symlink enumeration logic is correct.

The separate enumeration of symlinks (without -L) correctly identifies all symlinks including broken ones. The tab-delimited parsing with bounds checking is safe.


180-197: LGTM! NULL-delimited parsing correctly handles newlines in filenames.

The implementation correctly:

  • Uses NULL delimiters to support newlines and special characters
  • Processes entries in groups of 8 fields
  • Has proper loop bounds checking (addresses previous review comment)

The loop condition $i + 8 <= count($fields_array) correctly handles the trailing empty string from the final \0 in find's output.


198-216: LGTM! Symlink detection and device resolution logic is correct.

The implementation properly:

  • Identifies broken symlinks using link_type == 'N' from find's %Y format
  • Checks symlink map built from the earlier enumeration
  • Resolves device names for both user shares (via xattrs) and disk paths
  • Flags multi-device locations with warning tag

220-232: LGTM! Folder rendering correctly handles special characters and symlinks.

The implementation:

  • Correctly uses rawurlencode($name) in href (URL encoding is HTML-safe, no need for double-encoding)
  • Properly displays newlines using nl2br(htmlspecialchars(basename($name)))
  • Shows symlink tooltip with target path when applicable

233-253: LGTM! File rendering correctly handles broken symlinks and special characters.

The implementation:

  • Disables the editor for broken symlinks (correct safety measure)
  • Uses broken-symlink extension to show red broken chain icon
  • Properly encodes filenames in HTML attributes using escapeQuote()
  • Displays newlines correctly with nl2br(htmlspecialchars())
  • Shows symlink tooltip with target path

58-58: LGTM! Parent link encoding is correct.

Uses rawurlencode() consistently with other directory links. URL encoding is HTML-safe, so no additional escaping is needed.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Dec 26, 2025

🔧 PR Test Plugin Available

A test plugin has been generated for this PR that includes the modified files.

Version: 2026.01.01.1957
Build: View Workflow Run

📥 Installation Instructions:

Install via Unraid Web UI:

  1. Go to Plugins → Install Plugin
  2. Copy and paste this URL:
https://preview.dl.unraid.net/pr-plugins/pr-2491/webgui-pr-2491.plg
  1. Click Install

Alternative: Direct Download

⚠️ Important Notes:

  • Testing only: This plugin is for testing PR changes
  • Backup included: Original files are automatically backed up
  • Easy removal: Files are restored when plugin is removed
  • Conflicts: Remove this plugin before installing production updates

📝 Modified Files:

Click to expand file list
emhttp/plugins/dynamix/Browse.page
emhttp/plugins/dynamix/include/Browse.php
emhttp/plugins/dynamix/include/Control.php
emhttp/plugins/dynamix/nchan/file_manager

🔄 To Remove:

Navigate to Plugins → Installed Plugins and remove webgui-pr-2491, or run:

plugin remove webgui-pr-2491

🤖 This comment is automatically generated and will be updated with each new push to this PR.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 987fa42 and 0ac63e9.

📒 Files selected for processing (2)
  • emhttp/plugins/dynamix/include/Browse.php
  • emhttp/plugins/dynamix/include/Control.php
🔇 Additional comments (5)
emhttp/plugins/dynamix/include/Control.php (1)

167-169: Escaping logic looks correct.

The escape order (backslashes first, then quotes, then ${) is correct to prevent double-escaping and INI constant interpolation. Good defensive handling for special characters in filenames.

emhttp/plugins/dynamix/include/Browse.php (4)

152-158: Good fix for octal escape decoding.

The preg_replace_callback correctly decodes octal escapes (e.g., \012 for newline) from getfattr output to match the actual filenames returned by find. This ensures the lookup in $set works correctly for filenames containing special characters.


161-166: Robust approach using NULL-delimited output.

Using find -exec stat --printf ... '\0' with stream_get_line(..., "\0") correctly handles filenames containing newlines. The explode('|', $row, 6) with limit 6 ensures any | characters within the filename (in %n) are preserved in the $name variable.


174-175: Correct escaping order for XSS safety.

Applying htmlspecialchars() before nl2br() ensures HTML entities are escaped before <br> tags are inserted. This safely renders filenames with embedded newlines.


188-188: Consistent with folder handling.

Same safe pattern applied for file rows.

- Added htmlspecialchars_decode() to target (PR feedback)
- Optimized: replaced stat with find -printf only
- Use \0 as single delimiter, process in 7-field groups
- Improved code comments for better readability
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
emhttp/plugins/dynamix/include/Control.php (1)

172-176: Critical: Apply the same newline/CR escaping fix here.

The $dst handling has the same missing newline and carriage return escaping as $src. Apply the fix described in the previous comment to lines 173-175.

🧹 Nitpick comments (1)
emhttp/plugins/dynamix/include/Browse.php (1)

178-178: Consider clearer loop condition.

The loop condition $i < count($fields_array) - 7 is correct but $i + 7 <= count($fields_array) would more clearly express the intent of ensuring 7 fields are available.

🔎 Alternative formulation
-for ($i = 0; $i < count($fields_array) - 7; $i += 7) {
+for ($i = 0; $i + 7 <= count($fields_array); $i += 7) {
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac63e9 and 7662df5.

📒 Files selected for processing (2)
  • emhttp/plugins/dynamix/include/Browse.php
  • emhttp/plugins/dynamix/include/Control.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-03-27T22:04:00.594Z
Learnt from: zackspear
Repo: unraid/webgui PR: 2099
File: emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php:58-74
Timestamp: 2025-03-27T22:04:00.594Z
Learning: The file `emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php` is synced from a different repository, and modifications should not be suggested in this repository's context. Changes should be proposed in the source repository instead.

Applied to files:

  • emhttp/plugins/dynamix/include/Control.php
🧬 Code graph analysis (1)
emhttp/plugins/dynamix/include/Browse.php (1)
emhttp/plugins/dynamix/include/Helpers.php (1)
  • my_scale (19-39)
🔇 Additional comments (7)
emhttp/plugins/dynamix/include/Browse.php (5)

164-170: Clever two-pass approach for handling broken symlinks.

The separate find commands for working entries and broken symlinks ensure that all entries are captured with proper metadata. The NULL-separated format correctly handles filenames containing newlines and other special characters.


173-176: LGTM: Correct handling of NULL-delimited output.

Reading the entire output and splitting by NULL bytes is the appropriate approach for handling filenames with special characters including newlines.


187-221: Verify device resolution for absolute symlinks in user shares.

For absolute symlinks in user shares (lines 205-206), the code uses $set[basename($name)] which retrieves the xattr from the symlink's source location. This shows where the symlink is stored rather than where it points. Confirm this is the intended behavior for the LOCATION column.

If the LOCATION column should instead show where an absolute symlink points to, consider using the target's device for user shares as well:

if ($user) {
    // For absolute symlinks in user shares, consider using target device
    if ($target && $target[0] == '/') {
        $devs_value = $dev_name; // dev_name from target path (lines 190-192)
    } else {
        $devs_value = $set[basename($name)] ?? $shares[$dev_name]['cachePool'] ?? '';
    }
}

229-229: Excellent use of nl2br() for preserving newlines.

Using nl2br(htmlspecialchars(basename($name))) correctly displays filenames containing newlines while maintaining XSS protection. This addresses the PR objective of handling special characters safely.

Also applies to: 243-243


240-246: Broken symlink handling addresses previous review concern.

The explicit broken symlink detection (type == 'broken') and visual indicator (red chain icon) properly address the past review comment about broken symlink handling. The two-pass find approach ensures broken symlinks are captured with a distinct type marker, eliminating the risk of malformed records.

emhttp/plugins/dynamix/include/Control.php (2)

172-172: Previous inconsistency resolved.

The addition of htmlspecialchars_decode() to $dst makes it consistent with $src (line 166) and other target path handling in this file (e.g., line 100). This addresses the concern raised in the previous review.


169-169: The \${ escaping correctly prevents INI constant substitution in parse_ini_string().

The code at lines 169 and 175 properly escapes ${ to \${ using str_replace(), which is the standard PHP approach documented in the official PHP manual. This escaping works as intended with the default INI_SCANNER_NORMAL mode used at line 113. The implementation is correct and the defensive comment about PHP version behavior is reasonable, though documented behavior is consistent across supported versions.

…rovements

- Switch from INI to JSON format for file.manager.active/jobs communication
  - Handles all special characters (newlines, CR, quotes, backslashes, ${) natively
  - Simplifies code by removing manual escaping logic
  - Updates Control.php and file_manager to use json_encode/json_decode

- Add bounds checking for getfattr output parsing
  - Prevents undefined index errors if getfattr fails for individual files
  - Safely handles incomplete output with isset() check

- Move broken symlink icon to icon_class() function
  - Better code semantics with dedicated 'broken-symlink' extension type
  - Removes hardcoded icon logic from table generation

- Disable onclick for broken symlinks
  - Prevents text editor from opening when clicking broken symlinks
  - Avoids accidentally creating target files through the editor
  - Maintains context menu functionality for operations like delete

Fixes unraid#2488
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7662df5 and 3895e97.

📒 Files selected for processing (3)
  • emhttp/plugins/dynamix/include/Browse.php
  • emhttp/plugins/dynamix/include/Control.php
  • emhttp/plugins/dynamix/nchan/file_manager
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-03-27T22:04:00.594Z
Learnt from: zackspear
Repo: unraid/webgui PR: 2099
File: emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php:58-74
Timestamp: 2025-03-27T22:04:00.594Z
Learning: The file `emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php` is synced from a different repository, and modifications should not be suggested in this repository's context. Changes should be proposed in the source repository instead.

Applied to files:

  • emhttp/plugins/dynamix/nchan/file_manager
  • emhttp/plugins/dynamix/include/Control.php
📚 Learning: 2025-03-27T22:04:34.550Z
Learnt from: zackspear
Repo: unraid/webgui PR: 2099
File: emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php:13-19
Timestamp: 2025-03-27T22:04:34.550Z
Learning: The file emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php is synced from another repository and should not be modified directly in the webgui repository.

Applied to files:

  • emhttp/plugins/dynamix/nchan/file_manager
🧬 Code graph analysis (1)
emhttp/plugins/dynamix/include/Browse.php (4)
emhttp/plugins/dynamix/include/SmartInfo.php (1)
  • size (30-32)
emhttp/plugins/dynamix/include/FileTree.php (1)
  • path (30-32)
emhttp/plugins/dynamix/include/Helpers.php (2)
  • my_time (47-51)
  • my_scale (19-39)
emhttp/plugins/dynamix/include/Control.php (1)
  • my_age (25-35)
🔇 Additional comments (11)
emhttp/plugins/dynamix/include/Control.php (5)

112-121: LGTM: JSON parsing with proper validation.

The code correctly validates the json_decode() result before accessing the data, and uses null coalescing operators for optional fields.


139-153: LGTM: Clean implementation of JSON-based job queue.

The first-line extraction and queue management logic is correct and handles the transition from INI to JSON format appropriately.


185-202: LGTM: JSON encoding properly handles special characters.

The switch to json_encode() eliminates the need for manual escaping and natively handles special characters (newlines, quotes, backslashes, ${}), addressing the core issue in the PR objectives.


159-176: The INI-to-JSON row conversion formula is correct and properly documented. The code explicitly states that the INI format had 9 lines per job entry, and the formula intdiv($row - 1, 9) correctly converts INI row numbers (1, 10, 19, ...) to JSON line indices (0, 1, 2, ...). No changes needed.


178-181: No consumers of the 'read' endpoint were found in the codebase. The only modes used in the file manager are 'edit' and 'jobs'. This endpoint exists but appears to be unused or dead code. Verification of JSON format handling by consumers is not applicable since no callers exist.

emhttp/plugins/dynamix/include/Browse.php (6)

154-161: LGTM: Proper bounds checking and octal decoding.

The added bounds check prevents undefined index errors when getfattr output is incomplete, and the octal escape decoding correctly handles special characters in filenames.


165-179: LGTM: Robust NULL-delimited pipeline for special character handling.

The two-pass find approach with NULL-delimited output correctly handles filenames containing newlines and distinguishes broken symlinks. The 7-field format is clearly documented.


85-86: LGTM: Clear visual indication for broken symlinks.

The broken chain icon provides an appropriate visual cue for broken symlinks, consistent with the detection logic in the pipeline.


191-225: LGTM: Correct device resolution for symlinks.

The logic properly distinguishes between absolute and relative symlinks when determining the LOCATION column value, showing the target's device for absolute symlinks as expected.


233-233: LGTM: Proper newline handling in displayed names.

The use of nl2br() correctly preserves newlines in directory names for display while htmlspecialchars() prevents XSS.


242-247: LGTM: Proper handling of broken symlinks in UI.

Disabling onclick for broken symlinks prevents accidental file creation while preserving context menu functionality for other operations. The is_broken flag correctly controls this behavior.

- Browse.php: Fix loop condition to prevent skipping last entry
  - Changed from: $i < count($fields_array) - 7
  - Changed to: $i + 7 <= count($fields_array)
  - Bug: With 7 fields (1 entry), loop wouldn't run at all (0 < 0)
  - Bug: With 14 fields (2 entries), last entry was skipped (7 < 7)

- file_manager: Add JSON validation before extract()
  - Prevents PHP warning when json_decode() returns null
  - Check is_array($data) before calling extract()
- Remove legacy INI calculation from Browse.page line 316
- Frontend now sends direct job numbers (1,2,3...) to backend
- Backend converts to array indices (0,1,2...) in Control.php
- Consistent 1-based UI numbering for users (Job [0001], [0002]...)
- Eliminates confusion between old INI format and new JSON format
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
emhttp/plugins/dynamix/include/Browse.php (1)

165-206: Excellent refactoring to handle special characters safely.

The NULL-delimited pipeline with two separate finds (working vs. broken symlinks) correctly handles filenames containing quotes, backslashes, newlines, and other special characters—directly addressing issue #2488. The 7-field format is well-structured.

The loop condition at line 182 ($i + 7 <= count($fields_array)) correctly processes only complete 7-field groups and skips incomplete trailing data.

The device name derivation logic for symlinks (lines 191-205) works correctly:

  • Absolute symlink targets: device extracted from target path (e.g., /mnt/disk2/foodisk2)
  • Regular files/relative symlinks: device from source path
Optional: Add field validation for extra robustness

Consider adding validation after array_slice to handle potential edge cases where find output might be malformed:

 for ($i = 0; $i + 7 <= count($fields_array); $i += 7) {
   $fields = array_slice($fields_array, $i, 7);
+  // Skip if any required fields are empty (except target which may be empty for non-symlinks)
+  if (empty($fields[0]) || empty($fields[1]) || empty($fields[5])) continue;
   [$type,$owner,$perm,$size,$time,$name,$target] = $fields;

This would skip entries where type, owner, or name is unexpectedly empty, preventing potential rendering issues.

emhttp/plugins/dynamix/include/Control.php (1)

112-121: LGTM! JSON format correctly replaces INI parsing.

The job listing now reads JSON lines instead of INI blocks. The basic validation at lines 113-115 handles empty lines and invalid JSON gracefully. Missing keys are handled by the ?? operator with appropriate defaults.

Optional: Add explicit JSON structure validation

Consider validating that required keys exist after decoding to make debugging easier if the JSON format is ever corrupted:

   foreach ($rows as $row) {
     if (empty($row)) continue;
     $data = json_decode($row, true);
-    if (!$data) continue;
+    if (!$data || !isset($data['source'])) continue;
     $task = $data['task'] ?? '';

This would skip malformed entries more explicitly, though the current approach handles them gracefully via ?? defaults.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3895e97 and 1acc6cd.

📒 Files selected for processing (4)
  • emhttp/plugins/dynamix/Browse.page
  • emhttp/plugins/dynamix/include/Browse.php
  • emhttp/plugins/dynamix/include/Control.php
  • emhttp/plugins/dynamix/nchan/file_manager
🚧 Files skipped from review as they are similar to previous changes (1)
  • emhttp/plugins/dynamix/nchan/file_manager
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-03-27T22:04:00.594Z
Learnt from: zackspear
Repo: unraid/webgui PR: 2099
File: emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php:58-74
Timestamp: 2025-03-27T22:04:00.594Z
Learning: The file `emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php` is synced from a different repository, and modifications should not be suggested in this repository's context. Changes should be proposed in the source repository instead.

Applied to files:

  • emhttp/plugins/dynamix/include/Control.php
🔇 Additional comments (7)
emhttp/plugins/dynamix/Browse.page (1)

315-315: LGTM! Job numbering simplified.

The change from offset-based calculation to direct ID extraction correctly aligns with the backend's new JSON-based job handling. The frontend now sends 1-based job numbers directly, and Control.php converts them to 0-based array indices (line 162: $line_number = $row - 1).

emhttp/plugins/dynamix/include/Browse.php (3)

85-86: LGTM! Broken symlink icon added.

The new icon class for broken symlinks provides clear visual feedback to users.


154-161: Well done addressing the bounds checking issue.

The added if (!isset($tmp[$i+1])) break; guard at line 158 correctly handles cases where getfattr output is incomplete, preventing undefined index access. The octal escape decoding ensures filenames with newlines are correctly matched.


229-256: Perfect handling of newlines and broken symlinks.

The use of nl2br() at lines 233 and 247 correctly preserves newline characters in filenames for display without breaking the WebGUI—a key fix for issue #2488.

The broken symlink handling is well-implemented:

  • Line 242: Detects broken symlinks (type == 'broken')
  • Line 243: Uses 'broken-symlink' extension for icon
  • Line 247: Disables editor for broken symlinks (conditional onclick)
  • Shows red chain icon and symlink target device in location column
emhttp/plugins/dynamix/include/Control.php (3)

139-153: Start operation correctly queues first JSON job.

The logic properly reads the first JSON line (line 142), writes it to the active file, removes it from the queue (line 144), and handles the empty queue case (lines 149-150).


160-175: Undo operation correctly converts job numbering.

Line 162's conversion $line_number = $row - 1 correctly maps from 1-based job numbers (sent by frontend) to 0-based array indices. The unset() at line 164 safely removes entries, and implode() at line 168 correctly reassembles the remaining lines.


183-200: File operation correctly builds and stores JSON data.

The data array construction (lines 183-192) cleanly maps POST parameters to JSON fields. The conditional logic at lines 193-200 properly handles both queuing (append JSON to jobs file) and immediate execution (write JSON to active file).

mgutt added a commit to mgutt/webgui that referenced this pull request Dec 30, 2025
This PR addresses multiple issues and feature requests from unraid#2500:

1. Fix PR unraid#2402 Markdown Parser regression (literal colon bug)
   - Changed dialog templates from <div> to <span> for inline processing
   - Cleaned up unused dfm_info CSS rules

2. Show total size of running transfer
   - Enhanced rsync parser to calculate total transfer size
   - Displays 'Transferring X of Y' format

3. Show last N used destination paths in FileTree
   - New PopularDestinations.php with frequency-based scoring
   - FUSE conflict prevention for /mnt/user vs /mnt/diskX

4. Manually typing destination path updates FileTree
   - Keyboard navigation with arrow keys and Enter/Escape
   - Automatic FileTree open/close on input interaction

5. Add 'Open Terminal here' button
   - OpenTerminal.php accepts path parameter
   - File Manager can open terminal at selected folder

Note: This PR depends on unraid#2491 and unraid#2490 being merged first.
…all icons

- Use find -L for both user share and disk paths
- Build device ID map from all /mnt/* directories
- Add 'symlink' case to icon_class() function
- Set ext='symlink' for user share symlinks, use icon_class() consistently
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
emhttp/plugins/dynamix/include/Browse.php (1)

186-188: Consider memory usage for large directories (optional).

Reading the entire find output into memory with stream_get_contents() could consume significant memory for directories with thousands of files. However, this is typical for file manager UIs and not a regression from the previous implementation.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1acc6cd and e93762e.

📒 Files selected for processing (1)
  • emhttp/plugins/dynamix/include/Browse.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-28T15:54:58.673Z
Learnt from: mgutt
Repo: unraid/webgui PR: 2496
File: emhttp/plugins/dynamix/Browse.page:901-906
Timestamp: 2025-12-28T15:54:58.673Z
Learning: In the unraid/webgui codebase, CSRF validation is centralized in the global auto_prepend_file (local_prepend.php) which runs before every PHP request. Do not add per-script CSRF checks in individual files like Browse.page or Control.php. If a script relies on global validation, ensure it does not duplicate CSRF logic; otherwise extend the central preface to cover the needed checks.

Applied to files:

  • emhttp/plugins/dynamix/include/Browse.php
🔇 Additional comments (4)
emhttp/plugins/dynamix/include/Browse.php (4)

85-88: LGTM!

The new icon mappings for broken symlinks and regular symlinks are clear and appropriate.


154-161: Device mapping approach looks good.

The device ID to name mapping is well-implemented and includes a sensible fallback (line 209: $device_map[$device_id] ?? $lock) when the map is incomplete.


165-172: Octal escape decoding is correctly implemented.

The bounds checking and octal decoding logic properly handles getfattr output variations and matches the actual newline characters from the find command.


216-252: Rendering logic correctly handles special characters.

The use of nl2br(htmlspecialchars(basename($name))) on lines 220 and 242 properly preserves newlines while preventing XSS. Disabling the editor for broken symlinks (line 242) is a good UX decision.

Note: Due to the symlink detection issue flagged above, lines 234-235 (elseif ($is_symlink && $user)) are effectively unreachable since $is_symlink is only true for broken symlinks, which are already handled by the earlier if ($is_broken) condition.

@mgutt mgutt marked this pull request as draft January 1, 2026 19:05
- Always display target file icon, size, and permissions for symlinks in both user share and disk views
- Add fa-external-link icon after filename to visually indicate symlinks
- Include Unraid-style tooltip on external-link icon showing symlink target path
- Support broken symlinks with tooltip showing their invalid target path
- Move file edit onclick from table cell to filename span to prevent tooltip click interference
- Remove unused 'symlink' case from icon_class function
- Fix LOCATION column to properly display current disk in disk view
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
emhttp/plugins/dynamix/include/Browse.php (1)

160-160: Add bounds check for explode result.

The direct array access explode('"',$tmp[$i+1])[1] will cause an undefined offset error if the getfattr attribute line is malformed (missing quotes or only one quote).

🔎 Proposed fix
     if (!isset($tmp[$i+1])) break;
     $filename = preg_replace_callback('/\\\\([0-7]{3})/', function($m) { return chr(octdec($m[1])); }, $tmp[$i]);
-    $set[basename($filename)] = explode('"',$tmp[$i+1])[1];
+    $parts = explode('"', $tmp[$i+1]);
+    if (count($parts) < 2) continue; // Skip if attribute value is malformed
+    $set[basename($filename)] = $parts[1];
   }
🧹 Nitpick comments (1)
emhttp/plugins/dynamix/include/Browse.php (1)

231-250: Solid file rendering with proper broken-symlink handling.

The conditional editing (disabled for broken symlinks), consistent nl2br() usage for newlines, and proper XSS prevention with htmlspecialchars() are all correct.

💡 Optional: Extract duplicate symlink tooltip construction

The symlink tooltip construction on lines 221 and 240 is identical. Consider extracting it into a helper function to reduce duplication:

function symlink_tooltip($is_symlink, $target) {
  return $is_symlink ? '<a class="info" href="#" onclick="return false;"><i class="fa fa-external-link" style="margin-left:4px;"></i><span>'.htmlspecialchars($target).'</span></a>' : '';
}

Then use: $symlink_tooltip = symlink_tooltip($is_symlink, $symlinks[basename($name)]['target'] ?? '');

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e93762e and d3e8d92.

📒 Files selected for processing (1)
  • emhttp/plugins/dynamix/include/Browse.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-28T15:54:58.673Z
Learnt from: mgutt
Repo: unraid/webgui PR: 2496
File: emhttp/plugins/dynamix/Browse.page:901-906
Timestamp: 2025-12-28T15:54:58.673Z
Learning: In the unraid/webgui codebase, CSRF validation is centralized in the global auto_prepend_file (local_prepend.php) which runs before every PHP request. Do not add per-script CSRF checks in individual files like Browse.page or Control.php. If a script relies on global validation, ensure it does not duplicate CSRF logic; otherwise extend the central preface to cover the needed checks.

Applied to files:

  • emhttp/plugins/dynamix/include/Browse.php
🧬 Code graph analysis (1)
emhttp/plugins/dynamix/include/Browse.php (4)
emhttp/plugins/dynamix/include/SmartInfo.php (1)
  • size (30-32)
emhttp/plugins/dynamix/include/FileTree.php (1)
  • path (30-32)
emhttp/plugins/dynamix/include/Helpers.php (1)
  • my_scale (19-39)
emhttp/plugins/dynamix/include/Control.php (1)
  • my_age (25-35)
🔇 Additional comments (3)
emhttp/plugins/dynamix/include/Browse.php (3)

85-86: LGTM! Clear visual indicator for broken symlinks.

The red chain icon appropriately signals broken symlinks to users.


165-175: Excellent symlink detection approach.

Using find without -L correctly identifies all symlinks (both working and broken), addressing the limitation mentioned in past reviews where find -L would only detect broken symlinks as type 'l'. The tab-delimited format and bounds checking are appropriate.


177-212: Well-designed NULL-delimited processing.

The 8-field NULL-delimited approach correctly handles filenames with newlines and special characters. The loop bounds are correct (addressing past review concerns), and the symlink detection using the pre-built map properly identifies both working and broken symlinks.

Remove htmlspecialchars() from rawurlencode() calls in href attributes.
rawurlencode() already produces HTML-safe output, so htmlspecialchars()
causes double-encoding which breaks navigation for paths with &, <, >, ", or '.

Example:
- Before: a&b → htmlspecialchars → a&amp;b → rawurlencode → a%26amp%3Bb ❌
- After:  a&b → rawurlencode → a%26b ✓

Fixes: parent_link() and folder navigation hrefs
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
emhttp/plugins/dynamix/include/Browse.php (1)

136-136: Critical: Line 136 must be updated to match URL encoding changes.

The URL encoding fixes on lines 58 and 222 removed htmlspecialchars() wrapping, but line 136 still applies htmlspecialchars_decode(). This creates an inconsistency that will break navigation for paths containing &, <, >, ", or '.

Current flow:

  1. Line 222 generates href: rawurlencode($name) → URL contains a%26b for filename a&b
  2. PHP auto-decodes $_GET['dir'] → contains a&b
  3. Line 136 applies htmlspecialchars_decode() → still a&b (nothing to decode)
  4. Navigation works by accident, but logic is wrong

Broken scenario:
If a filename contains &amp; (literal text), line 136 will incorrectly decode it to &, changing the filename.

🔎 Proposed fix
-$dir = validdir(htmlspecialchars_decode(rawurldecode($_GET['dir'])));
+$dir = validdir($_GET['dir'] ?? '');

Rationale: PHP automatically URL-decodes $_GET parameters, and the hrefs no longer contain HTML entities, so both htmlspecialchars_decode() and rawurldecode() should be removed.

♻️ Duplicate comments (1)
emhttp/plugins/dynamix/include/Browse.php (1)

156-161: Add bounds checking for explode on line 160.

While the bounds check on line 158 prevents accessing missing lines, line 160's explode('"',$tmp[$i+1])[1] could still fail if the attribute line is malformed (e.g., missing quotes). This would cause an undefined array index error.

🔎 Proposed fix
   for ($i = 0; $i < count($tmp); $i+=3) {
     // Check bounds: if getfattr fails for a file, we might not have all 3 lines
     if (!isset($tmp[$i+1])) break;
     $filename = preg_replace_callback('/\\\\([0-7]{3})/', function($m) { return chr(octdec($m[1])); }, $tmp[$i]);
-    $set[basename($filename)] = explode('"',$tmp[$i+1])[1];
+    $parts = explode('"', $tmp[$i+1]);
+    if (count($parts) >= 2) {
+      $set[basename($filename)] = $parts[1];
+    }
   }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3e8d92 and 73ea9c2.

📒 Files selected for processing (1)
  • emhttp/plugins/dynamix/include/Browse.php
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-30T20:27:59.455Z
Learnt from: mgutt
Repo: unraid/webgui PR: 2501
File: emhttp/plugins/dynamix/Browse.page:1282-1339
Timestamp: 2025-12-30T20:27:59.455Z
Learning: In emhttp/plugins/dynamix/Browse.page, the xlink() function's clipboard functionality is intentionally designed to silently fail on HTTP connections without showing warnings. The path text is pre-selected to allow manual copying, which is preferred over showing repetitive error messages that would annoy HTTP users.

Applied to files:

  • emhttp/plugins/dynamix/include/Browse.php
📚 Learning: 2025-02-01T15:12:16.914Z
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.

Applied to files:

  • emhttp/plugins/dynamix/include/Browse.php
📚 Learning: 2025-12-28T15:54:58.673Z
Learnt from: mgutt
Repo: unraid/webgui PR: 2496
File: emhttp/plugins/dynamix/Browse.page:901-906
Timestamp: 2025-12-28T15:54:58.673Z
Learning: In the unraid/webgui codebase, CSRF validation is centralized in the global auto_prepend_file (local_prepend.php) which runs before every PHP request. Do not add per-script CSRF checks in individual files like Browse.page or Control.php. If a script relies on global validation, ensure it does not duplicate CSRF logic; otherwise extend the central preface to cover the needed checks.

Applied to files:

  • emhttp/plugins/dynamix/include/Browse.php
🧬 Code graph analysis (1)
emhttp/plugins/dynamix/include/Browse.php (4)
emhttp/plugins/dynamix/include/FileTree.php (1)
  • path (30-32)
emhttp/plugins/dynamix/include/SmartInfo.php (1)
  • size (30-32)
emhttp/plugins/dynamix/include/Helpers.php (2)
  • my_time (47-51)
  • my_scale (19-39)
emhttp/plugins/dynamix/include/Control.php (1)
  • my_age (25-35)
🔇 Additional comments (7)
emhttp/plugins/dynamix/include/Browse.php (7)

58-58: LGTM! URL encoding fix applied correctly.

The change to use rawurlencode($parent) without htmlspecialchars() is correct. URL-encoded output is already HTML-safe for href attributes.


85-86: LGTM! Broken symlink icon case added.

The new 'broken-symlink' case with a red chain-broken icon provides clear visual feedback for broken symlinks.


165-175: LGTM! Symlink detection correctly implemented.

The approach of using find without -L to detect all symlinks (working and broken) and building a lookup map is correct and addresses the previous issue where find -L couldn't identify working symlinks.

The bounds checking on line 172 ensures malformed output doesn't corrupt the map.


177-194: LGTM! Null-delimited 8-field parsing correctly handles special characters.

The implementation properly addresses special character handling in filenames:

  • NULL delimiters support newlines, quotes, and other special characters
  • Loop condition $i + 8 <= count($fields_array) correctly ensures complete 8-field records
  • Command injection protection via escapeshellarg($dir)
  • Field format (type, linktype, owner, perms, size, timestamp, name, device_id) provides all needed data

Note: Using stream_get_contents() to read all output at once may use significant memory for very large directories, but this is acceptable for a file manager UI.


195-213: LGTM! Entry processing and device resolution logic is sound.

The implementation correctly:

  • Identifies broken symlinks via $link_type == 'N'
  • Detects all symlinks (working and broken) via the pre-built map
  • Resolves device names appropriately for user shares (via xattr/share config) vs disk paths (via $lock)
  • Handles multi-device scenarios with warning tags

217-229: LGTM! Directory rendering properly handles special characters and symlinks.

The implementation correctly:

  • Uses nl2br() to preserve newlines in filenames without breaking HTML
  • Applies rawurlencode($name) for hrefs (correct encoding order per past review)
  • Uses htmlspecialchars() for display text (XSS protection)
  • Shows symlink targets in tooltips with proper escaping
  • Uses escapeQuote() for data attributes

231-249: LGTM! File rendering correctly handles broken symlinks and special characters.

The implementation properly:

  • Sets ext = 'broken-symlink' to trigger the red chain icon for broken symlinks
  • Disables editing for broken symlinks while allowing it for working files/symlinks
  • Applies nl2br() and htmlspecialchars() to preserve newlines and prevent XSS
  • Shows symlink targets in tooltips with safe escaping

Design note: Working symlinks are editable, which will edit the target file (since find -L follows the link). This appears intentional, as the symlink icon with tooltip informs users they're working with a symlink.

1. Remove htmlspecialchars_decode() from line 136:
   - href now uses rawurlencode() without htmlspecialchars()
   - PHP auto-decodes $_GET['dir'], so only rawurldecode() is needed
   - Prevents incorrect decoding of literal HTML entities in filenames

2. Add bounds checking for explode() on getfattr output:
   - Prevents undefined array index error if attribute line is malformed
   - Check count(parts) >= 2 before accessing parts[1]
@mgutt mgutt marked this pull request as ready for review January 1, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File manager cannot handle double quotes in directory names safely

1 participant