name: rmfa-file-protection description: Guides work on the Restrict Media File Access file protection pipeline including protecting/unprotecting files, hash-based URL rewriting, file serving with range request support, access control, attachment tracking, and URL replacement in post content. Use when debugging access issues, modifying the protection flow, working with URL rewriting, or changing how files are served.
Skill: RMFA File Protection
When to Use
Use this skill when:
- Debugging or modifying file protection/unprotection flows.
- Working with hash-based URL rewriting or the rewrite rules.
- Modifying how protected files are served (headers, range requests, access checks).
- Changing access control logic (who can view protected files).
- Working with attachment tracking (which posts use which attachments).
- Debugging URL replacement in post content after protect/unprotect.
- Troubleshooting "file not found" or access-denied issues.
File Protection Architecture
Core Concept
When a file is "protected", it is:
- Physically moved from
wp-content/uploads/YYYY/MM/towp-content/uploads/.protected/YYYY/MM/. - Assigned a SHA-256 hash stored in post meta
_protected_file_hash. - Served via a WordPress rewrite rule at
protected-files/{hash}instead of a direct file URL. - Access-controlled via the
restrict_media_file_access_protect_filefilter (defaults to! is_user_logged_in()).
Protection Flow (Restrict)
User clicks "Is restricted file" checkbox
→ AttachmentsAdmin::save_attachment_field()
→ AttachmentsFileManager::set_file_as_protected( $attachment_id )
1. ensure_file_hash() — Generate SHA-256 hash if not exists
2. store_original_paths() — Save original file + sizes paths to meta
3. move_files_to_protected_directory() — Move main file + all sizes + scaled originals
4. update_metadata_for_protected_file() — Update _wp_attachment_metadata
5. replace_file_urls() — Update all posts containing this file
→ update_post_meta( '_restricted_file', '1' )
Unprotection Flow (Unrestrict)
User unchecks "Is restricted file" checkbox
→ AttachmentsAdmin::save_attachment_field()
→ AttachmentsFileManager::set_file_as_unprotected( $attachment_id )
1. move_files_back_to_original_location() — Restore from .protected/ to original paths
2. update_metadata_for_unprotected_file() — Restore _wp_attachment_metadata
3. cleanup_original_paths() — Remove _original_file_path, _original_sizes_paths meta
4. replace_file_urls() — Update all posts back to normal URLs
→ delete_post_meta( '_restricted_file' )
Post Meta Keys
| Meta Key | Stored On | Description |
|---|---|---|
_restricted_file |
attachment | '1' if file is restricted. |
_protected_file_hash |
attachment | SHA-256 hash used in protected URLs. |
_original_file_path |
attachment | Original absolute path before protection. |
_original_sizes_paths |
attachment | Array of original size variation paths. |
_restricted_file_urls_map |
attachment | Map of old URL → new URL for content replacement. |
_rmfa_used_in_posts |
attachment | Array of post IDs that contain this attachment. |
_rmfa_attachments |
post | Array of attachment IDs used in this post. |
_rmfa_api_restricted |
attachment | Latest REST API restriction activity log. |
Hash-Based URL System
URL Format
Protected files are served at:
https://example.com/protected-files/{hash}
https://example.com/protected-files/{hash}-{width}x{height}
Hash Generation
$salt = AUTH_SALT (or fallback to rmfa_hash_salt option)
$file_hash = hash( 'sha256', $salt . basename( $file_path ) . wp_generate_password( 8, false ) )
Stored in _protected_file_hash post meta. The hash is generated once when a file is first protected and persists across protect/unprotect cycles.
URL Resolution
- WordPress rewrite rule:
^protected-files/([^/]+)→index.php?protected_file=$matches[1]. AttachmentsProtector::handle_protected_file()picks up theprotected_filequery var ontemplate_redirect.- Extracts hash and optional size suffix from the URL.
- Looks up attachment ID via
rmfa_find_attachment_id_by_hash(). - Resolves the physical file path including size variations.
Attachment URL Modification
Attachments::modify_attachment_url() intercepts wp_get_attachment_url and returns hash-based URLs for protected files. Similarly modifies:
wp_get_attachment_metadata— Hash-based file paths in metadata.wp_calculate_image_srcset— Hash-based srcset URLs.attachment_url_to_postid— Reverse lookup from protected URLs to post IDs.wp_get_attachment_image_attributes— Addsrmfa-protected="true"attribute.
File Serving Pipeline
Access Check
$is_protected = apply_filters(
'restrict_media_file_access_protect_file',
! is_user_logged_in(),
$protected_file
);
Default behavior: Logged-in users can access protected files; logged-out users cannot.
Protected Response (Access Denied)
Returns a 1x1 transparent GIF with no-cache headers:
Content-Type: image/gifCache-Control: no-store, no-cache, must-revalidate, max-age=0- Customizable via
restrict_media_file_access_protected_headersandrestrict_media_file_access_protected_imagefilters.
Unprotected Response (Access Granted)
- Determines MIME type via
mime_content_type(). - Supports HTTP Range requests (partial content / byte serving) for media streaming.
- Sets
Accept-Ranges: bytesheader. - For range requests: returns
206 Partial ContentwithContent-Rangeheader. - Streams file in 8KB chunks.
- Fires
restrict_media_file_access_before_serveaction before output. - Disables page caching (
DONOTCACHEPAGE,batcache_cancel()).
Attachment Tracking
AttachmentsTracking maintains a bidirectional index between posts and attachments:
On Post Save (wp_insert_post)
- Extracts all media URLs from post content (href, src, poster, srcset).
- Resolves each URL to an attachment ID.
- Computes diff vs previously tracked attachments.
- Updates
_rmfa_used_in_postson each attachment. - Updates
_rmfa_attachmentson the post.
On Post Delete (before_delete_post)
Removes the post from all tracked attachments.
URL Fixing (wp_insert_post_data)
Before saving, checks if post content contains wrong URLs (e.g., public URL for a restricted file or protected URL for an unrestricted file) and auto-corrects them using the _restricted_file_urls_map.
Key Filters and Actions
Filters
| Filter | Description |
|---|---|
restrict_media_file_access_protect_file |
Controls access to protected files. Receives (bool $protect, string $file). |
restrict_media_file_access_protected_headers |
Modify headers sent for access-denied responses. |
restrict_media_file_access_protected_image |
Modify the base64 image data sent for access-denied responses. |
rmfa_restricted_file_help_text |
Change the help text on the restriction checkbox. |
rmfa_restricted_file_helps_text |
Change the description text below the checkbox. |
rmfa_restricted_file_is_disabled |
Disable the restriction toggle for specific attachments. |
rmfa_rest_api_restricted_files_access |
Control whether restricted files appear in REST API queries. |
rmfa_api_restricted_meta_box_html |
Customize the restriction activity meta box HTML. |
Actions
| Action | Description |
|---|---|
restrict_media_file_access_before_serve |
Fires before serving a protected file. Receives (int $attachment_id, string $file_path). |
rmfa_attachment_restrictions_updated_on_save |
Fires after restriction status changes. Receives (bool $result, int $file_id). |
Jetpack Compatibility
JetpackCompatibility skips Jetpack Photon processing for any URL containing .protected/ or protected-files/ paths, preventing Photon from attempting to serve protected images.
Procedure: Debugging File Access Issues
- Check that rewrite rules are flushed — visit Settings → Permalinks or check
_rmfa_flush_rewrite_rulestransient. - Verify the
protected_filequery var is registered —RewriteRules::add_query_vars(). - Check
_protected_file_hashexists on the attachment —rmfa_get_media_protected_file_hash(). - Verify the physical file exists at the expected
.protected/path —get_attached_file(). - Check the
restrict_media_file_access_protect_filefilter return value. - For 404s on protected URLs: ensure pretty permalinks are enabled.
Procedure: Adding a New Access Control Rule
- Hook into
restrict_media_file_access_protect_filefilter. - The filter receives
(bool $protect, string $protected_file_hash). - Return
trueto block access,falseto allow. - Example: allow access based on user role, membership, or token.
add_filter( 'restrict_media_file_access_protect_file', function ( $protect, $protected_file ) {
if ( current_user_can( 'subscriber' ) ) {
return false; // Allow subscribers
}
return $protect;
}, 10, 2 );
Verification
composer lint:phppasses.- Protected files are physically in
wp-content/uploads/.protected/. - Protected URLs resolve correctly at
protected-files/{hash}. - Logged-in users can access protected files.
- Logged-out users receive a 1x1 transparent GIF.
- URL replacement works correctly in post content after protect/unprotect.
- Attachment tracking meta is accurate.