July 1, 2010 | Web Design & Development, WordPress | 35 Comments
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:
-
- RewriteEngine On
- RewriteBase /
-
- RewriteCond %{REQUEST_FILENAME} ^.*(mp3|m4a)$
- RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in.*$ [NC]
- 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:
- <?php
-
- get_header(); ?>
- <div id="container">
- <div id="content" role="main">
- <div id="post-0" class="post error403 not-allowed">
- <h1 class="entry-title"><?php _e( "Action Not Allowed", "twentyten" ); ?></h1>
- <div class="entry-content">
- <p><?php _e("Apologies, but you are not allowed to download files while not logged in.", "twentyten"; ); ?></p>
- </div><!-- .entry-content -->
- </div><!-- #post-0 -->
- </div><!-- #content -->
- </div><!-- #container -->
- <?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:
- RewriteCond %{REQUEST_FILENAME} ^.*(mp3|m4a)$
- RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in.*$ [NC]
- 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:
- <?php
-
- function exclude_error_pages($excludes) {
- array_push($excludes, 8);
- return $excludes;
- }
-
- if (!is_admin()) {
- add_filter('wp_list_pages_excludes', 'exclude_error_pages');
- }
- ?>
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.
Why not just add to the template files:
[code][/code]
Do stuff…
[code][/code]
Do something else…
I’m not sure what part you’re referring to there.
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!!!
yeah, I think if you replace the
RewriteCond %{REQUEST_FILENAME} ^.*(mp3|m4a)$withRewriteCond %{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.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!
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.
hi,i just want to hide original link file from direct download, how to modify that script?
thanks…
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.
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
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 ???
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.
[...] 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 [...]
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!
I’m pretty sure that you can do this like so:
RewriteRule ^(.*) /wordpress/wp-login.php?redirect_to=$1 [R,L]The
redirect_toshould tell wordpress where to send the user after successful login. The only part that I’m not sure about there is whetherredirect_toshould be a full url or not. If that doesn’t work you could try changing$1to%{HTTP_HOST}$1.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!
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.
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.
Great solution. Simple to implement. Thank you!
I guess I can download the file by adding “wordpress_logged_in” into my cookies.
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)
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?
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.
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?
No, sorry, I don’t work with IIS at all so I can’t even speculate.
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!
Thanks. This helped me out!
[...] http://top-frog.com/2010/07/01/a-simple-way-to-limit-file-downloads-to-only-logged-in-users-in-wordp… [...]
You are NOT top-frog but TOP TOP TOP TOP frog. Exactly what I was looking for. Thank you very much.
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
Not really with this setup. You can, but it would require a different approach to the protection method.
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?
Yes, you should be able to replace
(mp3|m4a)in the .htaccess file with simply(pdf).Easy to implement and works perfectly. Thanks!
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.