Protecting direct access to PDF and ZIP unless user logged in (without plugin)

The question:

Working on a WordPress support site which has registered only user content, including uploaded PDF and ZIP files.

I’m looking for a way to prevent direct access to those PDFs and ZIP files inside the wp-content/uploads directory without the use of a plugin.

Reading through older questions, this is REALLY close (but comments are closed): as it detects direct access to the file, then does some login to save the file requested and check to see if the user is logged in. If not, they get redirected to the WordPress login. If they are logged in, then it loads the file.

This is the original htaccess:

RewriteCond %{REQUEST_FILENAME} -s
RewriteRule ^wp-content/uploads/(.*)$ dl-file.php?file=$1 [QSA,L]

However, the script blocks access to all files inside the wp-content/uploads directory, including images – so unless the user is logged in, even blog post images (which don’t need protected) are hidden on the front-end.

I’m trying to revise the RewriteRule to check for only PDF or ZIP files, so that the dl-file.php file only gets called if it’s a PDF or ZIP being requested.

I’ve tried the following, but it returns “404-File not found.” when accessing a PDF or ZIP when the user is logged in, so even though the file type check appears to be working, the dl-file.php check is failing.

RewriteCond %{REQUEST_FILENAME} -s
RewriteRule ^wp-content/uploads/([^/]*.(pdf|zip))$ filecheck.php?file=$1 [QSA,L]

How would I revise this so that it only calls the dl-file.php if it’s a pdf or zip file being requested, but still pass the correct info to dl-file.php?

Thank you,

 * dl-file.php
 * Protect uploaded files with login.
 * @link
 * @author hakre <>
 * @license GPL-3.0+
 * @registry SPDX


is_user_logged_in() || auth_redirect();

list($basedir) = array_values(array_intersect_key(wp_upload_dir(), array('basedir' => 1)))+array(NULL);

$file =  rtrim($basedir,'/').'/'.str_replace('..', '', isset($_GET[ 'file' ])?$_GET[ 'file' ]:'');
if (!$basedir || !is_file($file)) {
    die('404 — File not found.');

$mime = wp_check_filetype($file);
if( false === $mime[ 'type' ] && function_exists( 'mime_content_type' ) )
    $mime[ 'type' ] = mime_content_type( $file );

if( $mime[ 'type' ] )
    $mimetype = $mime[ 'type' ];
    $mimetype = 'image/' . substr( $file, strrpos( $file, '.' ) + 1 );

header( 'Content-Type: ' . $mimetype ); // always send this
if ( false === strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS' ) )
    header( 'Content-Length: ' . filesize( $file ) );

$last_modified = gmdate( 'D, d M Y H:i:s', filemtime( $file ) );
$etag = '"' . md5( $last_modified ) . '"';
header( "Last-Modified: $last_modified GMT" );
header( 'ETag: ' . $etag );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + 100000000 ) . ' GMT' );

// Support for Conditional GET
$client_etag = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? stripslashes( $_SERVER['HTTP_IF_NONE_MATCH'] ) : false;

if( ! isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )

$client_last_modified = trim( $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
// If string is empty, return 0. If not, attempt to parse into a timestamp
$client_modified_timestamp = $client_last_modified ? strtotime( $client_last_modified ) : 0;

// Make a timestamp for our most recent modification...
$modified_timestamp = strtotime($last_modified);

if ( ( $client_last_modified && $client_etag )
    ? ( ( $client_modified_timestamp >= $modified_timestamp) && ( $client_etag == $etag ) )
    : ( ( $client_modified_timestamp >= $modified_timestamp) || ( $client_etag == $etag ) )
    ) {
    status_header( 304 );

// If we made it this far, just serve the file
readfile( $file );

The Solutions:

Below are the methods you can try. The first solution is probably the best. Try others if the first one doesn’t work. Senior developers aren’t just copying/pasting – they read the methods carefully & apply them wisely to each case.

Method 1

RewriteCond %{REQUEST_FILENAME} -s
RewriteRule ^wp-content/uploads/([^/]*.(pdf|zip))$ filecheck.php?file=$1 [QSA,L]

This actually looks OK, except if you have additional subdirectories within the /uploads directory? An alternative is to include an additional condition on the original rule that only rewrites the request if the request ends in .pdf or .zip. For example:

RewriteCond %{REQUEST_URI} .(pdf|zip)$ [NC]
RewriteCond %{REQUEST_FILENAME} -s
RewriteRule ^wp-content/uploads/(.*)$ dl-file.php?file=$1 [QSA,L]

It shouldn’t really matter, but make sure this goes before the WordPress front-controller.

All methods was sourced from or, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Comment