Humblee

A humble PHP framework & CMS

Media Manager

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.


File Storage

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.


The /media Endpoint

Files 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:

  1. Record lookup — resolve id to a row in humblee_media; 404 if not found
  2. Role check — if required_role != 0, call Core::auth($file->required_role); return 403 if the session lacks the required role
  3. Locate file — physical path is _app_server_path . 'storage/' . $file->filepath
  4. Cache headersCache-Control: private for role-gated files; Cache-Control: public for open ones
  5. Serve — if encrypted == 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.


Access Control

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.


Encryption at Rest

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:

  1. Reads the plaintext file from /storage/{filepath}
  2. Generates a fresh 24-byte random nonce: random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES)
  3. Encrypts: $ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key)
  4. Writes $nonce . $ciphertext back to the same storage path in-place
  5. Sets humblee_media.encrypted = 1

Decryption 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:

  • Not be committed to version control
  • Not be web-accessible (it lives above public/)
  • Be backed up securely — there is no key recovery mechanism; a lost key makes encrypted files permanently unreadable

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.


TinyPNG Integration

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:

  • The uploaded file is an image (MIME type contains image/)
  • The uploader checked the compression checkbox

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.