Fork me on GitHub

article

A simple way to limit file downloads to only logged in users in WordPress

July 1, 2010 | Web Design & Development, WordPress

So, you’ve used WordPress to build your client’s site and to provide downloads for the site’s users. You’re hiding the links to download content based on the user’s logged in status. Great. But what happens when the logged in user copies the download URL and sends it to his friend? Well, unless you’re filtering the download links and checking them with WordPress first his friend gets to download the file.

I’m not a big fan of checking every file download with WordPress as it can take a lot of overhead if you’re running a busy site. So here is a pretty straight forward way to limit downloads from a WordPress site with a minimal amount of code. In this example I’ll illustrate how to prevent non-logged in users from downloading audio files in mp3 and m4a format.

Basic Blocking

First, lets use some ModRewrite rules to get Apache to show users a 403 forbidden page when trying to access the files. This isn’t pretty, it simply gets Apache to dump the user in to a default 403 page and the user is told that their access is forbidden. The .htaccess changes:

  1.  # These next two lines will already exist in your .htaccess file
  2.  RewriteEngine On
  3.  RewriteBase /
  4.  # Add these lines right after the preceding two
  5.  RewriteCond %{REQUEST_FILENAME} ^.*(mp3|m4a)$
  6.  RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in.*$ [NC]
  7.  RewriteRule . - [R=403,L]

On the first line of this code you’ll see (mp3|m4a). This is the part that looks at the ending of the file name and determines which files it will act upon. replace the items inside the parentheses with the file types that you want to protect, each one separated by the pipe character. So, for example, if you wanted to protect PDF and RTF files you’d change it to: RewriteCond %{REQUEST_FILENAME} ^.*(pdf|doc)$

That is really all we need but its not very nice to dump the user like that and not inform them of why they were forbidden. They may assume that the site is broken. So lets do this the right way and get the users redirected to a page that will inform them of why they were denied access to the content.

Redirecting the Access Denied

For this we’ll need a page template. You can create anything you’d like, but the basics of it are:

  1.  <?php
  2.  /**
  3.   * Template Name: 403
  4.   *
  5.   * The template for displaying 403 pages (Forbidden/Not Allowed).
  6.   */
  7.   get_header(); ?>
  8.   <div id="container">
  9.   <div id="content" role="main">
  10.   <div id="post-0" class="post error403 not-allowed">
  11.   <h1 class="entry-title"><?php _e( "Action Not Allowed", "twentyten" ); ?></h1>
  12.   <div class="entry-content">
  13.   <p><?php _e("Apologies, but you are not allowed to download files while not logged in.", "twentyten"; ); ?></p>
  14.   </div><!-- .entry-content -->
  15.   </div><!-- #post-0 -->
  16.   </div><!-- #content -->
  17.   </div><!-- #container -->
  18.  <?php get_footer(); ?>

Your template will obviously look a bit different. I did this one as an extension to the new Twenty Ten theme in WordPress 3.0 and based it off of the provided 404 template.

Save the file to your theme’s directory. It doesn’t matter what you name it. WordPress will actually pick up on the Template Name: 403 portion as the template ID.

Next we need to create a page in the WordPress admin for our notification page. Create a new page, name it whatever you want. For my purposes I titled the page “Not Allowed” so my slug ended up as “not-allowed”. You can edit these to be any values you want if the page title created a slug that you don’t like. You’ll need that slug here in the next step. Next select the 403 page template from page templates select input in the Attributes meta box (typically on the right side of the page, underneath the Publish button. Publish the page.

Now let’s alter that .htacess directive to redirect to this page instead of showing that unfriendly Apache notice. The modified directives are:

  1.  RewriteCond %{REQUEST_FILENAME} ^.*(mp3|m4a)$
  2.  RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in.*$ [NC]
  3.  RewriteRule . /not-allowed [R,L]

The relevant change happened on the third line where the dash was replaced by the relative url to my 403 page. In my case that is /not-allowed. Yours will differ depending upon how named your page. Don’t forget the leading slash when adding your slug so that it’s a valid relative path. (This can be an absolute path as well, ie: one that contains the full http://domain.com/blah-blah but there’s no need to do that here unless you’ve got valid reason to do so, like if you want to redirect the user to a different domain).

Now, whenever someone who is not logged in tries to download a file type that you’ve specified they’ll be redirected to your 403 page. What you tell them there is up to you.

But…

…now, if you’re theme lists out pages anywhere, you’ve got this 403 page sticking its nose in where it doesn’t belong. Not very pretty, now, is it? This is pretty easily remedied. Head on in to the WP Admin and to the page edit screen for the 403 page. Take note of its page ID in the url. It will be the number after the word “post=” in the url. For example, if your URL looks like http://wp30.local/wp-admin/post.php?post=8&action=edit the post id is 8.

We now need to make an addition to your theme’s functions.php file. This file is located in your theme directory. Open this file and add in the following code:

  1.  <?php
  2.  // Do error page excludes
  3.  function exclude_error_pages($excludes) {
  4.   array_push($excludes, 8);
  5.   return $excludes;
  6.  }
  7.  // we don't want any funny stuff in the admin, only add to front end
  8.  if (!is_admin()) {
  9.   add_filter('wp_list_pages_excludes', 'exclude_error_pages');
  10.  }
  11.  ?>

On the line that says array_push($excludes, 8); replace the 8 with the id of the page you just created. This will keep the function wp_list_pages() from outputting the 403 page in any of its lists.

Note: if you’ve got other places in your theme that are pulling page lists through different means you’ll want to modify or filter those results as well. Depending upon the methods used to make the lists you can probably exclude the page as a parameter of the call for pages instead of using a filter. Your mileage will vary, but it is certainly doable.

Ta da!

And there you have it. Simple and straight forward. No real frills, though. As it sits now the user is redirected to a page that just tells then simply that they need to be a logged in user. That’s not very informative all by itself. There are a hundred ways to modify this to make it more convenient on the user.

Hopefully this gives you some ideas on what can be done to help legitimate users get to your content while keeping the rif-raff from poaching it.

40 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  • Why not just add to the template files:
    [code][/code]
    Do stuff…
    [code][/code]
    Do something else…

    Damien, July 1, 2010 9:10 pm | permalink

  • I’m not sure what part you’re referring to there.

    Shawn, July 1, 2010 9:56 pm | permalink

  • So… would this be possible to redirect a user who is not logged in based on a whole directory? For example if I upload all my attachments I want to remain private to a subdirectory of wp-uploads/premium/ couldn’t I set up a 403 direct to anyone wanting to access any files in that folder if they’re not logged in?

    Thanks this is very interesting and the closest i’ve got to accomplishing my goal!!!

    Craig, July 23, 2010 9:14 am | permalink

  • yeah, I think if you replace the RewriteCond %{REQUEST_FILENAME} ^.*(mp3|m4a)$ with RewriteCond %{REQUEST_URI} .*uploads/premium/.* it should accomplish your goal. I don’t have time to test right now so I can’t be sure. This is off the top of my head.

    Shawn, July 23, 2010 10:16 am | permalink

  • I could give you the biggest kiss ever right now!!! Thank you so much, I’ve been trying to achieve complete privacy in regards to links and downloads for 2 weeks solid now and I’m so glad I stumbled across your site! It might not mean much but I hope you can sleep tonight knowing that you’ve made one person very very happy!

    Thanks!

    Craig, July 23, 2010 10:54 am | permalink

  • Hey! Glad it helped. If you go farther than that in how you process the redirect I’d be very interested to see how you end up implementing it.

    Shawn, July 23, 2010 1:53 pm | permalink

  • hi,i just want to hide original link file from direct download, how to modify that script?

    thanks…

    johnwick7, September 21, 2010 2:18 am | permalink

  • John,
    That depends a lot on how your site is set up. If your download link display is determined by your template then you can use an `is_user_logged_in()` check to determine the display of the link, but if the link is being put in to the content for each post then this solution is probably better for you than any other that’ll require some more advanced trickery by filtering the content.

    Shawn, September 21, 2010 7:07 am | permalink

  • Hello Mr. frog
    I am endless thankful to you for posting this post. it helped me a lot a lot, infact learn many things regarding this issue.
    I really appreciate your work and hope to see more useful posts by you.

    Thank you so much

    Fawaz

    Fawaz, October 9, 2010 4:30 am | permalink

  • Mr. Frog, i found an issue in your code, this htaccess doesn’t work on DOCx and XLSx files. i tried ^.*(xlsx|docx|xls|doc)$ ,however, it works on doc and xls file but unable to deal with X files.
    Any idea ???

    Fawaz, October 10, 2010 4:33 am | permalink

  • Can you elaborate on what it is (or is not) doing? I’ve got a sneaking suspicion that I know what the issue is but need more info from you to diagnose.

    Shawn, October 10, 2010 10:31 am | permalink

  • Hi there,

    Thanks for the nice tip.
    I am wondering how to update .htaccess file so that user will be forece to login page first.
    If they login, they can directly download otherwise, go to the 403 page.

    I am using restrict access plugin so all my site must be logged in before people access the page.

    I tried to change it
    # RewriteRule . /wordpress/wp-login.php [R,L]
    but it goes to login page only and people need to type the url again after they download.

    Please help!

    Jung, May 14, 2011 9:59 pm | permalink

  • I’m pretty sure that you can do this like so:

    RewriteRule ^(.*) /wordpress/wp-login.php?redirect_to=$1 [R,L]

    The redirect_to should tell wordpress where to send the user after successful login. The only part that I’m not sure about there is whether redirect_to should be a full url or not. If that doesn’t work you could try changing $1 to %{HTTP_HOST}$1.

    Shawn, May 15, 2011 10:54 am | permalink

  • Thanks for this tutorial! I’m trying to get it to work for multisite installation (i.e. using the same code to apply to a subsite to protect pdf files) but it doesn’t seem to be working.

    If it’s any help, my current htaccess (due to multsite) looks something like this:

    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.php$ – [L]

    # uploaded files
    RewriteRule ^([_0-9a-zA-Z-]+/)?files/(.+) wp-includes/ms-files.php?file=$2 [L]

    # add a trailing slash to /wp-admin
    RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]

    `RewriteCond %{REQUEST_FILENAME} -f [OR]
    RewriteCond %{REQUEST_FILENAME} -d
    RewriteRule ^ – [L]
    RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
    RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
    RewriteRule . index.php [L]`

    Any guidance would be much appreciated!

    Flick, July 20, 2011 1:58 pm | permalink

  • Sorry, I’m not ignoring this. I’m neck deep in work right now and haven’t had a chance to really look in to your question.

    Shawn, July 24, 2011 11:05 am | permalink

  • Hi Shawn

    No worries at all. Thanks for publishing my comment – I appreciate you’re busy so no worries at all.

    I am trying it out on a single-site installation and appear to be doing it incorrectly, because everytime I press ‘Save’ on Permalinks it overwrites the edits.

    Any suggestions would be appreciated if you have a spare moment.

    Flick, July 25, 2011 1:03 pm | permalink

  • Great solution. Simple to implement. Thank you!

    Caleb, August 13, 2011 2:43 pm | permalink

  • I guess I can download the file by adding “wordpress_logged_in” into my cookies.

    NIcolas, September 26, 2011 4:46 pm | permalink

  • Yeah, there’s a good chance that approach would circumvent this. There’s little way that someone would know to try that, but, yes, its not perfect.

    The real way to do this would be to use a rewrite-map that uses a perl script to ping a login-service on your blog that authenticates users based on the login cookie. This way you get real cookie based login, but you also require access to the httpd.conf file to do it. We did this for a client a while back and it worked wonderfully (and was quite the learning experience)

    Shawn, September 27, 2011 12:10 am | permalink

  • Hi Shawn,

    Thx for such nice post. I would like to know if files protected this way are still accessible by, for instance, flash players. I mean, is it possible to play the protected files with flash player on my site for non-logged-in users?

    N Atta Kusi Adusei, October 6, 2011 6:04 pm | permalink

  • Atta,

    This would most definitely exclude flash players from accessing those files. You can experiment with looking at the headers passed by the flash player and see if there’s anything there that you can use to exclude the .htaccess rule with and that would allow you to do what you want to do.

    Shawn, October 7, 2011 8:17 am | permalink

  • This is great! However, I am on a Windows IIS server. Do you know if there is an equivalent way to do this on IIS?

    Mattheew, November 5, 2011 8:50 am | permalink

  • No, sorry, I don’t work with IIS at all so I can’t even speculate.

    Shawn, November 5, 2011 11:20 am | permalink

  • Your amazing. What experience in the past gave you this answer or did you stumble upon it? Either way your genius is appreciated greatly. Thank you sooooo much! :)

    Mr. Tucker, December 8, 2011 3:16 am | permalink

  • Thanks. This helped me out!

    Kyle, December 29, 2011 1:56 pm | permalink

  • 😉 Thank you so much for the informative post ! It helped me to resolve a security issue quickly and efficiently…

    Sanuja, June 25, 2012 12:04 am | permalink

  • You are NOT top-frog but TOP TOP TOP TOP frog. Exactly what I was looking for. Thank you very much.

    Rahi, August 29, 2012 11:05 am | permalink

  • hello sir. it is very good.
    can we limit not logged in users to download 3 times per day or month?

    please help me

    thanks

    Karim, January 24, 2013 7:19 am | permalink

  • Not really with this setup. You can, but it would require a different approach to the protection method.

    Shawn, January 24, 2013 7:30 pm | permalink

  • Would this approach work for password protected pages with PDF content? i.e. I want users who have a password to the password protected page to be able to access the PDF files but not by putting in the direct URL to the files?

    Elaine, February 2, 2013 10:22 am | permalink

  • Yes, you should be able to replace (mp3|m4a) in the .htaccess file with simply (pdf).

    Shawn, February 3, 2013 11:22 am | permalink

  • Easy to implement and works perfectly. Thanks!

    Clare, April 23, 2013 12:25 pm | permalink

  • An impressive share! I’ve just forwarded this onto a coworker who was doing a little homework on this. And he in fact ordered me lunch due to the fact that I stumbled upon it for him… lol. So allow me to reword this…. Thanks for the meal!! But yeah, thanks for spending time to discuss this topic here on your blog.

    Reginald, May 1, 2013 9:25 pm | permalink

  • Thank you very much, keep up the good work!
    BTW, I only use the ,.htaccess and it work like charm.

    Smart Arab, June 16, 2014 7:43 am | permalink

  • EXTREMELY helpful! Thank you thank you! Was tearing my hair out after a client required a “downloads” page, but our server kept crashing from all the bot activity and “false” downloads.

    Incidentally enough blocking the filetype didn’t work for me, but as per the comments, you suggested blocking access to the folder where all the sound files are stored, and THAT fixed it.

    Thanks once again for taking the time to post this. 4 years on from your post, and it’s still helping people!

    Jeff, August 31, 2014 6:39 am | permalink

Mentions

  1. WordPress Backup: Plugin instructions | Wendy Cholbi

    […] I created this .htaccess file by combining this limit access to logged-in users code with another bit of code to prevent users from seeing the contents of the backup […]

    December 13, 2010 | 3:04 pm

  2. How to prevent direct file access in your wp-content directory — The WP Guru

    […] http://top-frog.com/2010/07/01/a-simple-way-to-limit-file-downloads-to-only-logged-in-users-in-wordp… […]

    January 11, 2012 | 5:11 am

  3. How do I prevent site scraping? - PHP Solutions - Developers Q & A

    […] music files) unless they’re logged in as a registered user. It’s not too difficult to do in Apache. I assume it wouldn’t be too difficult to do in IIS as […]

    August 6, 2013 | 1:23 am

  4. Limit file downloads to logged in users (WP + Nginx) - WordPress BuddyPress Tweaks

    […] a way to restrict access to mp3 files on my site to logged in users only. The approach listed here http://top-frog.com/2010/07/01/a-simple-way-to-limit-file-downloads-to-only-logged-in-users-in-wordp&#8230; sounds like pretty much like what I […]

    May 17, 2014 | 9:50 pm

  5. How to: How do I prevent site scraping? | SevenNet

    […] music files) unless they’re logged in as a registered user. It’s not too difficult to do in Apache. I assume it wouldn’t be too difficult to do in IIS as […]

    December 17, 2014 | 11:41 pm

Comments are closed