Protect file downloads with Ultimate Member WordPress plugin

Ultimate Member is one of the premier WordPress plugins for membership management, but it is lacking one feature that many users might find handy: the ability to restrict file downloads to specific roles.

The developers of Ultimate Member have indicated that protecting content isn’t high on their list of features to implement, so for the time being you’ll have to improvise. The good news is that it’s not too difficult of a task – as long as your requirements are relatively simple.

For my particular use case, I need to allow only members who are assigned to a particular community role (or administrators) to download PDF files from a specific folder.

My approach is pretty straightforward:

  1. Create a php script that uses the Ultimate Member API to check that the current user is in a role that matches the download location hard coded in the script, and then return the contents of the file with the appropriate http headers
  2. Prevent normal http downloads from the protected folder with a .htaccess file

Limitations of my approach:

  1. Files need to be uploaded to a specific directory using FTP as you cannot choose the directory from the WordPress media manager
  2. You’ll need multiple php scripts depending on the number of roles -> download folder location pairs you have.
  3. There is no UI for controlling these permissions – it’s all in the scripts

So, it’s a super simple approach with some limitations – but it’s good enough for me, for now.

Okay, show me how

Let’s start with the download folder on the server. In my case, I’ve created a new directory under the wp-content/uploads directory called paid-content:

/public_html/wp-content/uploads/paid-content

In that folder, I’ve placed a .htaccess file that prevents downloads:

order deny,allow
deny from all

Then, I’ve created a new php script in my root directory:

/public_html/paid-download.php

require( dirname( __FILE__ ) . '/wp-blog-header.php' );
global $ultimatemember;
 
// The path to the protected folder relative to the wordpress uploads directory
// e.g. /wp-content/uploads/<$protectedDir>
$protectedDir = 'paid-content';
 
// The required member role (slug of the role)
$requiredRole = 'paidsubscriber';
 
// Map file extensions to mime types (TODO: better implementation?)
$extMap = array(
    'pdf' => 'application/pdf',
    'zip' => 'application/zip'
);
 
// ----- you shouldn't need to modify anything below here -------
 
// Get the user id of the currently logged in user. If no user is logged in, return a 404
$userId = um_profile_id();
if (!$userId) {
    exitWith404();
}
 
// Check the status of the user
um_fetch_user($userId);
$isAdmin = um_user('administrator') === true; // is the user an admin?
$isApproved = um_user('status') === 'approved'; // is the user approved?
$userRole = um_user('role_name'); // the slug of the community role assigned to the user
 
// Check if the user is allowed to access the requested folder
$allow = (($isAdmin === true || $userRole === $requiredRole) && $isApproved === true);
if ($allow !== true) {
    exitWith404();
}
 
// Ensure that the file name provided really and truly exists in the directory we expect
$uploadBase = preg_replace('/[\\/\\\]/', DIRECTORY_SEPARATOR, wp_upload_dir()['basedir']);
$docName = $_GET['f'];
$docPath = realpath(join('/', array($uploadBase, $protectedDir, $docName)));
if (strpos($docPath, $uploadBase) !== 0 || !is_file($docPath)) {
    exitWith404();
}
 
if ($fd = fopen($docPath, "r")) {
    // Try to determine the mime type based on the file extension
    $pathInfo = pathinfo($docPath);
    $ext = strtolower($pathInfo["extension"]);
    if (isset($extMap[$ext])) {
        $contentType = $extMap[$ext];
    }
    else {
        $contentType = 'application/octet-stream'; // TODO: Just a hack fallback
    }
 
    // Set http status to 200 (OK), otherwise it will default to 404
    status_header(200);
    header("Content-type: $contentType");
    header("Content-Disposition: attachment; filename=\"".$pathInfo["basename"]."\"");
    header("Content-Transfer-Encoding: chunked");
    header("Content-length: " . filesize($docPath));
    header("Cache-control: private");
 
    // Output the contents of the file in 2048 byte chunks
    ob_clean();
    flush();
    while (!feof($fd)) {
        echo fread($fd, 2048);
    }
    ob_end_flush();
    fclose ($fd);
    exit;
}
else {
    wp_die('Oops, there was a problem downloading your file. Please try again.');
}
 
/**
* Output the WordPress 404 page
*
*/
function exitWith404() {
    status_header(404);
    nocache_headers();
    include(get_query_template('404'));
    exit;
}

How do I use it?

Well that’s the easy part 🙂 So say you have uploaded a document to this location:

/wp-content/uploads/paid-content/VeryValuableInfo.pdf

To create a protected link so that only members in the Paid Subscriber role (slug = paidsubscriber) can download it, simply create the link like this:

<a href="/paid-download.php?f=VeryValuableInfo.pdf">Download some valuable info!</a>

That’s all there is to it! Anyone who is not logged in (and who is not in the Paid Subscriber role) that tries to access this link will see the standard 404 WordPress error page for your site.

  • WordPress

    Protect file downloads with Ultimate Member WordPress plugin

    Ultimate Member is one of the premier WordPress plugins for membership management, but it is lacking one feature that many users ...
Load More Related Articles
  • Review

    Soundcore Life P2 True Wireless Review

    After a long search for aptX (crucial if you want to watch videos with minimal latency) true wireless earbuds that charge via USB-C (finally!), I’ve found a pair I’m happy with in the Soundcore Life P2. Besides sounding quite good, they have physical button controls that I prefer over touch (which I tend to activate by accident frequently). If you’re looking for true wireless earbuds for music or video without spending a fortune, and you value USB-C charging, these should probably be near the top of your list. As long as the provided tips give a good and tight seal in your ear, they should work for a wide range of activities too.
  • Android

    HTC Has Given Up

    SafetyNet is broken on the HTC U11. No more Google Pay, no installing Disney+ from the Play Store, etc. And HTC seems to have given up on fixing it. I'd suggest not buying an HTC device ever again if you expect it to continue working.
  • Android How-to

    Force Plex to Download/Sync Videos Without Transcoding

    With a minor modification to the Plex server configuration, you can stop Plex from transcoding videos when you download/sync them to your devices at what should be 'original' quality.
  • Android How-to

    Run Telus Pik TV on NVIDIA Shield TV (and other Android TV devices)

    The Pik TV app is now officially supported on NVIDIA Shield. If you use a different Android TV device, you can download the apk and sideload it. Previous versions of the app no longer work, so everyone will need to update to the latest version 2 release.
  • Android Review

    Daqi M1 Bluetooth Game Controller Review

    With an understated appearance, comfortable form factor and excellent Bluetooth connectivity, the Daqi M1 is a Bluetooth controller you should definitely ...
  • Commentary

    Uber’s fatal crash and the incredible spin machine

    So an Uber self-driving vehicle struck and killed a pedestrian in Arizona. It was bound to happen sooner or later, of ...
Load More By Some Guy
Load More In WordPress

One Comment


  1. Josef

    May 21, 2021 at 12:48 pm

    Thanks for your script, it made me find the way of doing this. By the way, you don’t really need to access the Ultimate Member api. You can do it with only the WordPress API and it will work even with other plugins like the Ultimate Member. I’m using this on my script:

    $auth_user = wp_get_current_user();
    $auth_user_login = ‘Anonymous’;
    $auth_user_role = ‘Anonymous’;
    if ($auth_user->ID != 0) {
    $auth_user_login = $auth_user->user_login;
    $auth_user_role = $auth_user->roles[0];
    }

    The only part I’m not sure of is this one:
    $isApproved = um_user(‘status’) === ‘approved’; // is the user approved?

    I don’t know really if an user with a non approved status will be able to login. If so, then I guess you will still need that line.

    By the way, I ended using this script:
    https://gist.github.com/hakre/1552239

    I think that using RewriteRules are even better than disabling the entire site. With those RewriteRules you can even do things like:
    RewriteRule ^wp-content/uploads/(ultimatemember/.*)$ dl-file.php?file=$1 [QSA,L]

    which will protect only the files inside the ultimatemember folder and the rest of the website will remain public.

    Best regards
    Josef

Looking for a new web hosting provider? I personally use a recommend FullHost.

Their support is top notch and reliability and performance has been virtually perfect. Highly recommended.