Digital Sovereignty for everyone

Architecture

Directory layout

The private tree mirrors the public uploads layout. The main site’s private files sit at the root of the private directory (private-uploads/2026/10/file.pdf); every other site’s sit under sites/ID/ (private-uploads/sites/60/2026/10/file.pdf), exactly as its public uploads sit under uploads/sites/ID/. Proxy URLs carry the same path below /protected-files/.

Releases before 1.5.0 put every site’s private files at the root, alongside the main site’s. On a subsite, a file still there is a legacy flat file (LH_Private_Uploads_Paths::is_legacy_flat_path()). It stays readable through the proxy only while that site’s own postmeta holds its exact path, can still be moved back to public, and once moved into its site’s directory, a request for its old flat URL is redirected (301) to the new one.

Classes

  • Paths: all path and URL mapping. private_site_dir() is the current site’s own directory; public_to_private() and private_to_public() swap only a leading directory; path_belongs_to_current_site() checks a path is in the site’s own directory (and, on the main site, not in sites/). Also the /protected-files/ rewrite rule and the wp_get_attachment_url filter that returns proxy URLs.
  • Mover: moves a file and its image sizes between public and private on status changes (the parent post’s transition, a new attachment, an attachment edit or re-parenting), then updates _wp_attached_file to the absolute path and the guid. A move never overwrites an existing file: it fails with target_exists.
  • Proxy: resolves a /protected-files/ request, refuses anything outside the private tree (404) or another site’s directory (403), looks the attachment up by exact absolute path in this site’s postmeta, runs the password or capability check and streams the file.
  • Access: the default access rules for the built-in private status, behind the access filters.
  • Metabox and Admin: the File Access control on attachments, and a network admin migration scan that moves files to match their status.

Filters

  • lh_private_uploads_protected_statuses: post statuses whose files are kept private. Default private.
  • lh_private_uploads_user_can_access: whether the current user may open a protected file. Starts false.
  • lh_private_uploads_attachment_is_protected: per-attachment override of that decision.
  • lh_private_uploads_password_form: the password form HTML.
  • lh_private_uploads_metabox_statuses and lh_private_uploads_unregister_attachment_statuses: the statuses offered in the File Access control.

Constants

  • LH_PRIVATE_UPLOADS_DIR: the private directory. Default private-uploads one level above the WordPress root.
  • LH_PRIVATE_UPLOADS_XSENDFILE: true to stream files with X-Sendfile (mod_xsendfile) instead of PHP.

If you like this plugin please consider making a donation to further development

Install LHero.org

lhero.org

Open it straight from your home screen, full screen and quicker to load.

  1. Tap the Share button . If you cannot see it, tap ••• first.
  2. Scroll down and tap Add to Home Screen.
  3. Tap Add.
  1. Tap the browser menu ⋮.
  2. Tap Install app or Add to Home screen.
  1. In the menu bar, choose File, then Add to Dock.

Use your browser's menu to install this site or add it to your home screen.