Skip to content

Replace elFinder with an own file manager for the rich text editor - #984

Merged
KrzysztofPajak merged 3 commits into
developfrom
feature/file-manager
Oct 24, 2026
Merged

KrzysztofPajak merged 3 commits into
developfrom
feature/file-manager

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Resolves #(none)
Type: feature

Issue

The rich text editor's "Manage pictures" button opened elFinder. Its server side, elFinder.Net.* 1.5.0, is no longer maintained:

  • it is built on SixLabors.ImageSharp 2.x, which has known high severity advisories; on its own it resolves ImageSharp 2.0.0;
  • with the ImageSharp 3.x the rest of the solution brings in, its thumbnails throw MissingMethodException for Image.Load(Stream, out IImageFormat), so the file manager shows no thumbnails (HTTP 500 on /Admin/ElFinder/Thumb/...);
  • it drags in the ASP.NET Core 2.1 package graph, which needed a separate Microsoft.AspNetCore.Http reference only to patch an advisory;
  • its client (~540 KB) depends on jQuery UI and does not follow the panel's style.

The editor only ever used a small part of it: browse assets/images/uploaded, upload, create a folder, rename, delete, and pick a file to insert.

Solution

An own file manager, in the panel's component style.

Server

  • FileManagerService (Grand.Web.AdminShared) over IMediaFileStore, rooted at assets/images/uploaded. Every path is split into names and each name is checked, so nothing can leave that root (.., ., empty segments, drives, reserved Windows names, hidden files, \ / : * ? " < > |). This matters because FileSystemStore only keeps a path inside wwwroot, not inside a subfolder.
  • Only MediaSettings.AllowedFileTypes are listed, uploaded and kept on rename (a picture cannot be renamed to .js or .cshtml); 4 MB per file as elFinder's volume had; an upload never replaces a file (top (1).jpg); a case-only rename works on case-insensitive file systems.
  • FileManagerController (Grand.Web.Admin): List, Upload, CreateFolder, Rename, Delete, JSON only (camelCase, mapped explicitly - the panel's serializer keeps PascalCase for the grids). Same authorization as ElFinderController had: HtmlEditor on the controller and HtmlEditorManagePictures in every action; antiforgery from BaseAdminController. Messages come from new Admin.FileManager.* resources (DefaultLanguage.xml + Upgrade/en_250.xml).

Client

  • GrandAdmin.fileManager (adminapp/src/ui/filemanager.js, in admin.ui.js), opened in GrandAdmin.modal: breadcrumb, up, new folder, upload (button or drag and drop), rename, delete through GrandAdmin.modal.confirm, picture previews, Insert as the one primary button, keyboard navigation. Names are written with textContent only.
  • EditorTemplates/Editor.cshtml opens it instead of elFinder; a picked picture is inserted as an image, a video as an iframe, anything else as a link, as before.

Removed

  • elFinder.Net.AspNetCore, elFinder.Net.Core, elFinder.Net.Drivers.FileSystem and the Microsoft.AspNetCore.Http reference that only patched their graph.
  • ElFinderController, IElFinderViewModelService, ElFinderViewModelService, BaseInfoResponseConverter (+ its test), the vendored client under wwwroot/administration/elfinder and its rules in _custom.scss.
  • MediaSettings.FileManagerEnabledCommands / FileManagerDisabledUICommands (elFinder command lists) from the settings class, the Media settings screen and the Store settings controller. Stored values are ignored when the settings are read.

Breaking changes

  • A plugin or theme that linked /administration/elfinder/... or called $.fn.elfinder / dialogelfinder must ship its own copy.
  • MediaSettings.FileManagerEnabledCommands and FileManagerDisabledUICommands no longer exist.
  • /Admin/ElFinder/Connector and /Admin/ElFinder/Thumb are gone.

Testing

  1. dotnet test src/Tests/Grand.Web.Admin.Tests → all pass (44 new in FileManagerServiceTests: path validation, traversal on list/upload/delete, allowed types on upload and rename, size, no overwrite, case-only rename, root locked). Grand.Web.Store.Tests, Grand.Mapping.Tests, Grand.Business.Storage.Tests pass.
  2. cd src/Web/Grand.SharedUIResources/adminapp && npm ci && npm test && npm run lint → 230 passed (12 new in filemanager.test.js), lint clean. Bundles rebuilt with npm run build.
  3. Fresh install with sample data, Admin → Catalog → Categories → edit a category → "Manage pictures" in the Description toolbar:
    1. the window opens on "Files"; no elFinder script or stylesheet is loaded;
    2. New folder → name it → double-click it → the folder is empty;
    3. Upload a .png and a .js → the picture is listed and selected, the script is refused with "This file type is not allowed…";
    4. Insert → the picture is in the editor; Save → reload → it is still there and served (200);
    5. Rename the picture to x.js → refused; to another .png → renamed;
    6. Delete the picture and then the folder → each asks first, then disappears; Escape closes the window;
    7. Repeat in dark mode.
  4. With the browser's devtools, as the signed-in admin: GET /Admin/FileManager/List?path=../.., POST Delete with path=../../../administration/admin.common.js, POST CreateFolder with name=../escape and POST Rename with an empty path → all answer success: false; a POST without X-CSRF-TOKEN → 400; signed out, /Admin/FileManager/List redirects to the login page.
  5. Configuration → Settings → Media → only "Allowed file types" is left of the file settings.

The editor's "Manage pictures" opens GrandAdmin.fileManager (adminapp, Bootstrap 5 and plain
JavaScript) instead of elFinder. The Admin FileManager controller lists, uploads, creates
folders, renames and deletes under assets/images/uploaded through IMediaFileStore;
FileManagerService keeps every path under that root, accepts only MediaSettings.AllowedFileTypes
(on upload and on rename), refuses files over 4 MB and never replaces an existing file.

elFinder is removed as a whole: the elFinder.Net packages (and the Microsoft.AspNetCore.Http
reference that only patched their dependency graph), the connector, its JSON converter, the
vendored client and its styles, and the two elFinder command settings of the media settings.
Copilot AI balanced review requested due to automatic review settings October 24, 2026 04:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

[TestInitialize]
public void Init()
{
_webRoot = Path.Combine(Path.GetTempPath(), "filemanager-" + Guid.NewGuid().ToString("N"));
public void Init()
{
_webRoot = Path.Combine(Path.GetTempPath(), "filemanager-" + Guid.NewGuid().ToString("N"));
_uploaded = Path.Combine(_webRoot, "assets", "images", "uploaded");
_uploaded = Path.Combine(_webRoot, "assets", "images", "uploaded");
Directory.CreateDirectory(_uploaded);
//what a path that escapes the root would reach: the site's own scripts
Directory.CreateDirectory(Path.Combine(_webRoot, "assets", "custom"));
Directory.CreateDirectory(_uploaded);
//what a path that escapes the root would reach: the site's own scripts
Directory.CreateDirectory(Path.Combine(_webRoot, "assets", "custom"));
File.WriteAllText(Path.Combine(_webRoot, "assets", "custom", "site.js"), "script");
[TestMethod]
public void List_Root_FoldersFirstAndOnlyAllowedTypes()
{
Directory.CreateDirectory(Path.Combine(_uploaded, "zeta"));
public void List_Root_FoldersFirstAndOnlyAllowedTypes()
{
Directory.CreateDirectory(Path.Combine(_uploaded, "zeta"));
File.WriteAllText(Path.Combine(_uploaded, "a.png"), "x");
{
Directory.CreateDirectory(Path.Combine(_uploaded, "zeta"));
File.WriteAllText(Path.Combine(_uploaded, "a.png"), "x");
File.WriteAllText(Path.Combine(_uploaded, "placeholder.txt"), "x");
Directory.CreateDirectory(Path.Combine(_uploaded, "zeta"));
File.WriteAllText(Path.Combine(_uploaded, "a.png"), "x");
File.WriteAllText(Path.Combine(_uploaded, "placeholder.txt"), "x");
File.WriteAllText(Path.Combine(_uploaded, ".hidden.png"), "x");
[TestMethod]
public void List_NestedFolder_RelativePathsCrumbsAndEscapedUrls()
{
Directory.CreateDirectory(Path.Combine(_uploaded, "summer sale"));
Comment thread src/Tests/Grand.Web.Admin.Tests/Services/FileManagerServiceTests.cs Fixed
- FileSystemStore builds a renamed entry's path from the store path; the absolute
  Parent.FullName lost its leading slash and landed under the root twice on Linux
- a new name must be a single name, and the root check requires a separator so a
  sibling folder sharing the root's prefix is outside
- an empty entry in the allowed file types no longer allows files without an extension
- the file manager upload request has its own size ceiling
public void GetDirectoryInfo_SiblingSharingTheRootPrefix_Throws()
{
//Arrange - "<root>-old" starts with the root's path but lies outside it
var root = Path.Combine(Path.GetTempPath(), $"store-{Guid.NewGuid():N}");
Scryber is gone on develop, so elFinder was the last reason for the ImageSharp,
Microsoft.AspNetCore.Http and elFinder.Net pins; the ImageSharp entries in
.trivyignore go with them.
@KrzysztofPajak
KrzysztofPajak merged commit fd89dfa into develop Oct 24, 2026
9 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the feature/file-manager branch October 24, 2026 05:22
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.

2 participants