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()andprivate_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 insites/). Also the/protected-files/rewrite rule and thewp_get_attachment_urlfilter 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_fileto the absolute path and the guid. A move never overwrites an existing file: it fails withtarget_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. Defaultprivate.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_statusesandlh_private_uploads_unregister_attachment_statuses: the statuses offered in the File Access control.
Constants
LH_PRIVATE_UPLOADS_DIR: the private directory. Defaultprivate-uploadsone level above the WordPress root.LH_PRIVATE_UPLOADS_XSENDFILE: true to stream files with X-Sendfile (mod_xsendfile) instead of PHP.