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.

Share this Story

In the market for PHP (especially WordPress) hosting? You owe it to yourself to check out SiteGround. The reliability and performance is unmatched for the price and their support really is top notch.
There aren't a lot of web hosting providers that I would recommend, but SiteGround is at the top of the list.