A humble PHP framework & CMS
The Media Manager gives administrators a centralized place to upload, organize, and serve files. Unlike a public/uploads/ directory, media is stored outside the web root and served through a PHP controller. This single chokepoint enables per-file role restrictions and optional encryption at rest, with no change to how end users access files.
All uploaded files are written to /storage/ at the server root — a sibling of public/, not a child. The web server cannot directly serve files from this directory.
Each file is stored under a generated filename to prevent collisions and enumeration:
{YmdHis timestamp}{6-char MD5 hash}.{lowercased extension}
// e.g. 20260603153042a1b2c3.jpg
The display name shown to users is stored separately in humblee_media.name. The storage name and display name are decoupled intentionally.
/media EndpointFiles are served by Humblee\Controller\Media via:
/media/{id}/{filename}
The id segment is the database record primary key. The trailing filename is cosmetic — it produces readable URLs and correct browser download prompts but has no effect on which file is returned.
Request lifecycle:
id to a row in humblee_media; 404 if not foundrequired_role != 0, call Core::auth($file->required_role); return 403 if the session lacks the required role_app_server_path . 'storage/' . $file->filepathCache-Control: private for role-gated files; Cache-Control: public for open onesencrypted == 1, decrypt in-memory and stream the plaintext; otherwise stream directly with readfile()Content-Type is set from humblee_media.type, so images, PDFs, and other formats render correctly in the browser without additional configuration.
Each humblee_media record has a required_role field (integer; 0 = public). Assign any role ID to restrict the file to authenticated users who hold that role. Because every request goes through the media controller, the storage path is never exposed and cannot be bypassed by guessing filenames.
Role IDs are managed through the Humblee admin panel. The Media Manager UI displays and allows editing of the role restriction per file.
The media admin panel allows toggling encryption on individual files. Humblee uses libsodium sodium_crypto_secretbox (XSalsa20-Poly1305 authenticated encryption) via PHP 8.3's built-in sodium extension — no external crypto libraries are required.
How encryption works:
When an admin encrypts a file, the controller:
/storage/{filepath}random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES)$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key)$nonce . $ciphertext back to the same storage path in-placehumblee_media.encrypted = 1Decryption reverses the process: the first 24 bytes of the stored file are the nonce; the remainder is the ciphertext passed to sodium_crypto_secretbox_open(). When serving an encrypted file, the media controller decrypts in-memory at request time — the plaintext is never written to disk.
Key storage:
The 32-byte symmetric key is generated during installation and stored in humblee/configuration/crypto/key.php. This file must:
public/)When to use encryption:
Encryption at rest is most valuable for files already restricted by role. Encrypting a publicly accessible file adds disk-level protection but does not change what end users can reach through the /media endpoint. For maximum protection, combine role restriction with encryption.
Humblee optionally compresses and resizes images at upload time using the TinyPNG API (tinify/tinify Composer package).
Configuration in humblee/configuration/env_*.php:
'TINYPNG_Enabled' => false, // set true to activate
'TINYPNG_API_Key' => '', // key from tinypng.com/developers
'TINYPNG_Max_Width' => 1920, // max width in pixels; false disables resizing
When TINYPNG_Enabled is true, the upload dialog presents a "Compress with TinyPNG" checkbox. Compression runs only when both conditions are met:
image/)On a successful API call, Humblee passes the temporary upload to Tinify\fromFile(), resizes if the source width exceeds TINYPNG_Max_Width, then writes the result directly to /storage/. The size and type columns in humblee_media are updated from the TinyPNG response. If the API call fails for any reason, the uncompressed original is stored instead — uploads never fail silently due to a TinyPNG error.
Disabling TinyPNG:
Set TINYPNG_Enabled => false. The compression checkbox disappears from the upload UI and no API calls are made.
Removing the dependency entirely:
Remove tinify/tinify from humblee/composer.json and run composer install. Every Tinify call in the upload path is gated behind the TINYPNG_Enabled config check, so no PHP changes are required.