How to restrict attachment download to a specific user?

I have a very specific use case where the site built for a lawyer and each of his clients can login to their own ‘specific page/portal’ (custom post type) without the ability to access wp-admin etc. (I created all the login/register/profile-editing pages in the front end). In this page/portal the lawyer will leave messages and files for the client to download, now theoretically speaking, one client can guess (or if has knowledge of another client’s files) other file names and download then thus creating an issue with privacy/security/confidential material etc.

I’m looking for ideas/concepts for a solution, my initial thought was to have the download link point to some download.php sending the attachment id, user id, page/portal id and nonce and the on the other end processing that..

what do you think? am I on the right track or this approach is flawed?

Thanks!

5

What needs to happen is that you need to proxy download requests for the file types you want through WordPress. Let’s assume you’re going to restrict access to “.doc” files.

1. Define a query variable that indicates the requested file

function add_get_file_query_var( $vars ) {
    $vars[] = 'get_file';
    return $vars;
}
add_filter( 'query_vars', 'add_get_file_query_var' );

2. Update .htaccess to forward requests for restricted files to WordPress

This will capture requests to the files you want to restrict and send them back to WordPress using the custom query variable above. Insert the following rule before the RewriteCond lines.

RewriteRule ^wp-content/uploads/(.*\.docx)$ /index.php?get_file=$1

3. Capture the requested file name in custom query variable; and verify access to the file:

function intercept_file_request( $wp ) {
    if( !isset( $wp->query_vars['get_file'] ) )
        return;

    global $wpdb, $current_user;

    // Find attachment entry for this file in the database:
    $query = $wpdb->prepare("SELECT ID FROM {$wpdb->posts} WHERE guid='%s'", $_SERVER['REQUEST_URI'] );
    $attachment_id = $wpdb->get_var( $query );

    // No attachment found. 404 error.  
    if( !$attachment_id ) {
        $wp->query_vars['error'] = '404';
        return;
    }

    // Get post from database 
    $file_post = get_post( $attachment_id );
    $file_path = get_attached_file( $attachment_id );

    if( !$file_post || !$file_path || !file_exists( $file_path ) ) {
        $wp->query_vars['error'] = '404';
        return;
    }

    // Logic for validating current user's access to this file...
    // Option A: check for user capability
    if( !current_user_can( 'required_capability' ) ) {
        $wp->query_vars['error'] = '404';
        return;
    }

    // Option B: check against current user
    if( $current_user->user_login == "authorized_user" ) {
        $wp->query_vars['error'] = '404';
        return;
    }

    // Everything checks out, user can see this file. Simulate headers and go:
    header( 'Content-Type: ' . $file_post->post_mime_type );
    header( 'Content-Dispositon: attachment; filename="'. basename( $file_path ) .'"' );
    header( 'Content-Length: ' . filesize( $file_path ) );

    echo file_get_contents( $file_path );
    die(0);
}
add_action( 'wp', 'intercept_file_request' );

NB This solution works for single-site installs only! This is because WordPress MU already forwards uploaded file requests in sub-sites through wp-includes/ms-files.php. There is a solution for WordPress MU as well, but it’s a bit more involved.

Leave a Comment