Developer information for extending and diagnosing Medifence from your theme or plugin.
Public API (the medifence_allow_access filter)
The final access decision can be overridden with the medifence_allow_access filter. It is applied after the default checks (logged in → holds the configured capability → carries a valid share token), so every decision can be changed.
add_filter( 'medifence_allow_access', function ( $allowed, $file_path ) {
// Example: restrict access to a specific role.
if ( ! current_user_can( 'edit_posts' ) ) {
return false;
}
return $allowed;
}, 10, 2 );
$allowed is the result of the default checks, and $file_path is the realpath()-resolved absolute path (the filter is only called for requests that passed path validation). The return value becomes the final decision. For files outside the protection scope (excluded paths or extensions) the filter is not called at all, because they are served before the authorization step.
How share links are signed
A share link is the file’s normal URL plus two query arguments:
mf_expires— the expiry as a Unix timestampmf_token—hash_hmac( 'sha256', <path relative to uploads> . '|' . <mf_expires>, <signing key> )
Because the signature covers both the file path and the expiry, a token cannot be reused for another file, nor can the deadline be extended by editing the URL. Comparison uses hash_equals() to avoid timing attacks. The signing key is stored in the database and replaced by “Revoke all share links”, which invalidates every issued link at once.
WP-CLI commands
wp medifence status [--fresh] [--format=json] # report the protection status (exit code 1 when unprotected)
wp medifence verify # run the same check and repair as the daily task
wp medifence rewrite / remove # write / remove the .htaccess rules
wp medifence inspect <file> [--format=json] # the verdict for one file, and the reason
wp medifence audit [--limit=N] [--format=…] # walk the library and list what anyone can open
wp medifence stray [--limit=N] [--format=…] # walk the uploads directory for unmanaged files
wp medifence share <id> [--days=N] [--uses=N] [--label=…] [--password=…]
wp medifence log [--clear] / history [--clear] # the access log / the settings history
wp medifence override <id> <mode> # set a per-file exception
wp medifence pause [--minutes=N] / resume # pause / resume the protection
wp medifence export / import <file> [--dry-run] # carry the settings between sites
status exits non-zero when the protection is not working, which makes it usable from deployment scripts and CI. --fresh ignores the five-minute cache on the live test, and --format=json prints the whole report as one JSON object (the exit codes are unchanged).
Diagnostic header (X-Medifence)
Whenever Medifence decides a response it adds an X-Medifence header. The values are denied, placeholder, login (sent to the login screen), page (sent to a page of your own), password (a share link asking for its password), and paused. A response without the header never reached Medifence at all, which separates an excluded path or extension, missing rules, a disabled mod_rewrite, and a cache in front of the server.
Shortcode ([medifence_files])
[medifence_files path="members/gold"] lists the files of that uploads folder — but only the ones the visitor viewing the page could actually download. The decision follows the delivery path exactly (per-file exception → exclusions → published sizes → scope → capability) and goes through the medifence_allow_access filter too. The output is one unstyled list with class names, for the theme to style. Because the content depends on who is looking, the page must not be page-cached; the plugin defines DONOTCACHEPAGE.
REST field and constants
The media REST endpoint wp/v2/media carries a read-only medifence field with protected, reason and description. It is returned only to callers with manage_options; everyone else receives null, because which files are reachable is itself information. Setting MEDIFENCE_LOCK_SETTINGS in wp-config.php refuses every operation that moves the protection scope, in the admin and over WP-CLI alike, while diagnosis, audits and share links stay available. The medifence_exposure_limit filter changes how many attachments the daily count examines.
.htaccess rules
The rules are written into the uploads .htaccess as a Medifence marker block; content from other plugins is preserved. In order: deny serving the .htaccess file itself → serve excluded extensions and excluded paths directly → rewrite requests for existing files to the site’s index.php?mf_file= (in the second mode, only the listed paths are rewritten).
WordPress bootstraps normally and the plugin intercepts the request on the init hook (priority 0) to authenticate and stream the file. No WordPress core file is ever loaded directly. Deactivating or uninstalling removes the block and files are served normally again. The full generated rules are shown at the bottom of the settings screen.
Delivery specification
- ETag / 304 — an ETag is derived from the path, modification time, and size; conditional requests receive an empty 304. Logged-in users’ browsers cache files locally, so the PHP delivery overhead is not paid repeatedly.
- Range support (206 / 416) — partial requests required for video and audio seeking are supported.
Cache-Control: private— storage in shared caches (CDNs, proxies) is forbidden. This is a fixed part of the design: it prevents files served to authenticated users from leaking to anonymous visitors.- Content-Disposition — only images, video, audio, PDF, and text are displayed inline; everything else is treated as a download (combined with
X-Content-Type-Options: nosniff). - Streaming — files are streamed in 8KB chunks, so large files never exhaust memory.
External services
The plugin never contacts any external service. The live test on the settings screen sends exactly one HTTP request — to your own site.