FileSystemRepository
in package
Filesystem repository of the uploads folder (`%appWwwDir%uploads/`).
All paths are relative to the root. The class strictly prevents escaping the root (path traversal,
symlinks): the admin never receives a real server path and sees nothing outside uploads/.
Produces the value objects FtpFile / FtpFolder. All real filesystem work (scandir, mkdir, rename, unlink, read/write) happens here only.
Table of Contents
Constants
- DANGEROUS_NAME_SEGMENTS : array<string|int, mixed> = ['php', 'php3', 'php4', 'php5', 'php7', 'php8',...
- Deny-list of executable and server configuration extensions.
Properties
Methods
- __construct() : mixed
- createFolder() : Result
- Creates the folder `$name` in the relative folder `$relParent` (the name is sanitized).
- delete() : bool
- Deletes a file or (recursively) a folder inside the root.
- listFolder() : FtpFile>}
- Lists a folder: subfolders and files (value objects with relative paths).
- read() : array{name: string, content: string, contentType: string}|null
- Reads a file for download (proxy): returns the name, base64 content and MIME type.
- rename() : bool
- Renames a file or folder to `$newName` (in the same folder).
- sanitizeAll() : Result
- Walks the whole uploads tree and renames files and folders with a non-conforming name to a sanitized form (diacritics and spaces become a safe shape); a collision gets a `-N` suffix.
- writeFile() : Result
- Writes an uploaded file into a relative folder under the sanitized `$name` (diacritics and spaces removed, extension kept).
- deleteDir() : bool
- Deletes a folder recursively (called only on a verified path inside the root).
- isSafeFileName() : bool
- Rejects names whose file the server could execute or read as configuration.
- logUnsafeFileName() : void
- Logs a rejected attempt with a dangerous name (Tracy channel `security`).
- resolve() : string|null
- Safely resolves a relative path to an absolute one inside the root.
- root() : string
- Absolute uploads root (realpath, no trailing slash); empty means unavailable.
- safeName() : string|null
- Whether the name is a safe file/folder name (basename only, no slashes, `..` or control characters).
- sanitize() : string|null
- Sanitizes a name for the filesystem and URLs.
- sanitizeDir() : array{renamed: int, skipped: int}
- Sanitizes a folder recursively, bottom-up: the content first, then the folder itself.
- uniqueTarget() : string
- Returns a free (non-colliding) target name in a folder; on a collision appends `-1`, `-2`, .
- withinRoot() : bool
- Whether the absolute path is inside the root (or is the root itself).
Constants
DANGEROUS_NAME_SEGMENTS
Deny-list of executable and server configuration extensions.
private
array<string|int, mixed>
DANGEROUS_NAME_SEGMENTS
= ['php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', 'pht', 'phtml', 'phar', 'cgi', 'pl', 'py', 'sh', 'bash', 'htaccess', 'htpasswd', 'ini', 'exe', 'dll', 'so']
A .php file in uploads/ could be served directly by Apache. An allowlist would break unknown
legitimate upload types, so the deny-list covers only scripts and sensitive configuration files
and does not restrict regular uploads.
Properties
$config read-only
private
Config
$config
$svgSanitizer read-only
private
SvgSanitizer
$svgSanitizer
Methods
__construct()
public
__construct(Config $config, SvgSanitizer $svgSanitizer) : mixed
Parameters
- $config : Config
- $svgSanitizer : SvgSanitizer
createFolder()
Creates the folder `$name` in the relative folder `$relParent` (the name is sanitized).
public
createFolder(string $relParent, string $name) : Result
IMPORTANT: returns a Result, which is always truthy; callers must ask ->isSuccess(). The
failure reason is carried by ResultCode; it does not reach the wire {success} of the Api
presenter, which stays unchanged.
Parameters
- $relParent : string
- $name : string
Return values
Resultdelete()
Deletes a file or (recursively) a folder inside the root.
public
delete(string $rel) : bool
Parameters
- $rel : string
Return values
boollistFolder()
Lists a folder: subfolders and files (value objects with relative paths).
public
listFolder(string $rel) : FtpFile>}
Parameters
- $rel : string
Return values
FtpFile>}read()
Reads a file for download (proxy): returns the name, base64 content and MIME type.
public
read(string $rel) : array{name: string, content: string, contentType: string}|null
Returns null when the path is not a valid file inside the root. The real path never leaves the class.
Parameters
- $rel : string
Return values
array{name: string, content: string, contentType: string}|nullrename()
Renames a file or folder to `$newName` (in the same folder).
public
rename(string $rel, string $newName) : bool
A file keeps its original extension: the admin renames only the base of the name.
Parameters
- $rel : string
- $newName : string
Return values
boolsanitizeAll()
Walks the whole uploads tree and renames files and folders with a non-conforming name to a sanitized form (diacritics and spaces become a safe shape); a collision gets a `-N` suffix.
public
sanitizeAll() : Result
Returns a Result: success carries data = ['renamed' => int, 'skipped' => int]. An unknown
uploads root (missing configuration) is the failure ResultCode::INVALID_CONFIGURATION, not
a silent zero.
Return values
ResultwriteFile()
Writes an uploaded file into a relative folder under the sanitized `$name` (diacritics and spaces removed, extension kept).
public
writeFile(string $relFolder, string $name, string $bytes) : Result
IMPORTANT: returns a Result, see createFolder().
Parameters
- $relFolder : string
- $name : string
- $bytes : string
Return values
ResultdeleteDir()
Deletes a folder recursively (called only on a verified path inside the root).
private
deleteDir(string $abs) : bool
Parameters
- $abs : string
Return values
boolisSafeFileName()
Rejects names whose file the server could execute or read as configuration.
private
isSafeFileName(string $name) : bool
Checks every dot segment of the name, not only the last one: Apache AddHandler/AddType matches
shell.php.jpg too (the PHP handler runs although the name looks like an image). Hidden files
(.htaccess, .user.ini) are rejected by the leading dot regardless of the deny-list.
Parameters
- $name : string
Return values
boollogUnsafeFileName()
Logs a rejected attempt with a dangerous name (Tracy channel `security`).
private
logUnsafeFileName(string $action, string $name) : void
A logger failure (unconfigured directory, full disk) must not break the rejection itself: the
caller always gets false, just without a record.
Parameters
- $action : string
- $name : string
resolve()
Safely resolves a relative path to an absolute one inside the root.
private
resolve(string $rel, bool $mustExist) : string|null
$mustExist requires an existing target (realpath); otherwise the parent and the last segment are
validated. Returns null on any attempt to escape the root or on invalid input.
Parameters
- $rel : string
- $mustExist : bool
Return values
string|nullroot()
Absolute uploads root (realpath, no trailing slash); empty means unavailable.
private
root() : string
Return values
stringsafeName()
Whether the name is a safe file/folder name (basename only, no slashes, `..` or control characters).
private
safeName(string $name) : string|null
Parameters
- $name : string
Return values
string|nullsanitize()
Sanitizes a name for the filesystem and URLs.
private
sanitize(string $name, bool $isFile) : string|null
Transliterates diacritics, turns spaces into - and keeps only [a-zA-Z0-9-] (plus the dot that
separates the extension for files). A file keeps its extension (webalized separately, lowercase).
Returns null when no usable name remains after cleaning.
Parameters
- $name : string
- $isFile : bool
Return values
string|nullsanitizeDir()
Sanitizes a folder recursively, bottom-up: the content first, then the folder itself.
private
sanitizeDir(string $dirAbs) : array{renamed: int, skipped: int}
Parameters
- $dirAbs : string
Return values
array{renamed: int, skipped: int}uniqueTarget()
Returns a free (non-colliding) target name in a folder; on a collision appends `-1`, `-2`, .
private
uniqueTarget(string $dirAbs, string $name) : string
..
Parameters
- $dirAbs : string
- $name : string
Return values
stringwithinRoot()
Whether the absolute path is inside the root (or is the root itself).
private
withinRoot(string $abs, string $root) : bool
Parameters
- $abs : string
- $root : string