public
Last active

Wordpress login to download uploaded files

  • Download Gist
dl-file.php
PHP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
<?php
/*
* dl-file.php
*
* Protect uploaded files with login.
*
* @link http://wordpress.stackexchange.com/questions/37144/protect-wordpress-uploads-if-user-is-not-logged-in
*
* @author hakre <http://hakre.wordpress.com/>
* @license GPL-3.0+
* @registry SPDX
*/
 
require_once('wp-load.php');
 
is_user_logged_in() || auth_redirect();
 
list($basedir) = array_values(array_intersect_key(wp_upload_dir(), array('basedir' => 1)))+array(NULL);
 
$file = rtrim($basedir,'/').'/'.str_replace('..', '', isset($_GET[ 'file' ])?$_GET[ 'file' ]:'');
if (!$basedir || !is_file($file)) {
status_header(404);
die('404 &#8212; File not found.');
}
 
$mime = wp_check_filetype($file);
if( false === $mime[ 'type' ] && function_exists( 'mime_content_type' ) )
$mime[ 'type' ] = mime_content_type( $file );
 
if( $mime[ 'type' ] )
$mimetype = $mime[ 'type' ];
else
$mimetype = 'image/' . substr( $file, strrpos( $file, '.' ) + 1 );
 
header( 'Content-Type: ' . $mimetype ); // always send this
if ( false === strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS' ) )
header( 'Content-Length: ' . filesize( $file ) );
 
$last_modified = gmdate( 'D, d M Y H:i:s', filemtime( $file ) );
$etag = '"' . md5( $last_modified ) . '"';
header( "Last-Modified: $last_modified GMT" );
header( 'ETag: ' . $etag );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + 100000000 ) . ' GMT' );
 
// Support for Conditional GET
$client_etag = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? stripslashes( $_SERVER['HTTP_IF_NONE_MATCH'] ) : false;
 
if( ! isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )
$_SERVER['HTTP_IF_MODIFIED_SINCE'] = false;
 
$client_last_modified = trim( $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
// If string is empty, return 0. If not, attempt to parse into a timestamp
$client_modified_timestamp = $client_last_modified ? strtotime( $client_last_modified ) : 0;
 
// Make a timestamp for our most recent modification...
$modified_timestamp = strtotime($last_modified);
 
if ( ( $client_last_modified && $client_etag )
? ( ( $client_modified_timestamp >= $modified_timestamp) && ( $client_etag == $etag ) )
: ( ( $client_modified_timestamp >= $modified_timestamp) || ( $client_etag == $etag ) )
) {
status_header( 304 );
exit;
}
 
// If we made it this far, just serve the file
readfile( $file );

This is exactly what I'd like to do with the contents of a WP upload folder on my site.
I stumbled on your solution at the wordpress.stackexchange.com but am unsure how to use the script.

You mention these two lines of code:

RewriteCond %{REQUEST_FILENAME} -s
RewriteRule ^wp-content/uploads/(.*)$ dl-file.php?file=$1 [QSA,L]

Are these lines added to a .htaccess file or somewhere else?
I apologize for this [most likely] basic question but am a php beginner.

Thanks!

Yes those Apache Directives belong into the .htaccess file of Wordpress, see as well Apache Module mod_rewrite­Docs and .htaccess Glossary Entry­Codex. You can leave comments and questions as well on How to Protect Uploads, if User is not Logged In? (Wordpress Answers).

Hi @enes9, this requires additional debugging. I assume this is related to headers being send in the request or the response. Also ensure that the flash plugin of your browser sends the needed cookies. You should be able to gather more information by tracking the network connections with Firebug in Firefox or a similar tool in the browser of your choice.

If you're looking for individual support, please leave an email address.

Hi hakre,

Sorry for noob question, but how would this be modified to work on WP Multisite for all the subsites? So the user would have be a subscriber of the site (rather than the network preferably) to access the files.

I noticed that my multisite .htaccess file has these rewrite rules:

RewriteRule ^files/(.+) wp-includes/ms-files.php?file=$1 [L]

RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule . index.php [L]

Thanks,
lifepix

Hi @lifepix, the solution has not been written for a multisite installation.

Hi! Thanks for this. I'm a total noob and stumbled upon this. How would I change the .php file and .htaccess for the folder named, "work-files" which at the same level as "wp-content" (ie it's in the root folder). Also, can it work recursively through all the subfolders? Thanks!

Hi! Thanks for this script! I have a basic (and silly) question, but which ist the better folder to place the script? I have placed it inside wp-content/uploads and I'm getting a 404 error (accessing a file directly from URL), as the file is seen as a post.

Another question: I want it just to check the uploads when accessing them by URL, but not the images attached to a post. How could I parse it? Thanks in advance!

HI hakre,

I am trying to use your script on a subfolder inside uploads. at the moment it is '/wp-content/uploads/private/'.
The redirect script is working super well, but when I put the credentials it leads to a '404 - file not found.'

I have add 'private' in the code below, now it open the files, but you can access the file without be logged in.
$file = rtrim($basedir,'/').'/private/'.str_replace('..', '', isset($_GET[ 'file' ])?$_GET[ 'file' ]:'');

Any suggestion?

Thanks a lot

Ok guys,
apparently to achieve what I need, we have to change the 'structure' of the wordpress, so I gave up on it for time consuming issue and decided to create a folder called public and do the other way around, now I am facing another problem.
My images sitting on the uploads folder will not show if I am not logged in. How can I solve that?

Kind Regards

@Braus: I see no technical limitation with the private folder you aimed for first, just ensure all paths are correct and you shouldn't have any issues. Albeit I didn't test it, so let me know if you require additional support.

@jorditost: Your 404 issues needs troubleshooting and the feature request needs customization, Wordpress doesn't differ here between uploads, so one need to add this differentiation.

@beezwings: This should be merely a configuration setting, also as the traversal is by the file-system and supported from your OS, I don't see any recursion issues here. With a little modification of the Worpdress upload path configuration, I see no technical showstoppers to just do what you want to do.

1.- Hi, how do I use this?
2.- Where do I put it?
3.- Does it protect access to folders?

I can't get it to work just using:

/file.pdf

or

dl-file.php?file.pdf

Thanks.

Hi,

Can I change it to redirect to login page? With auth_redirect() it redirects to home page of the website. I tried to change it to

if (!is_user_logged_in())
{
wp_redirect( wp_login_url( site_url( '/login/ ' ) )); exit;
}

But that doesnt have any effect. It just redirects to same old home page. I am doing it right?

With WP 3.6.1 I added the following lines after require_once('wp-load.php'); otherwise is_user_logged_in() didn't work correctly in my case

require_once ABSPATH . WPINC . '/formatting.php';
require_once ABSPATH . WPINC . '/capabilities.php';
require_once ABSPATH . WPINC . '/user.php';
require_once ABSPATH . WPINC . '/meta.php';
require_once ABSPATH . WPINC . '/post.php';
require_once ABSPATH . WPINC . '/pluggable.php';
wp_cookie_constants();

I would recommend changing the start of the script to this:

require_once('wp-load.php');
require_once ABSPATH . WPINC . '/formatting.php';
require_once ABSPATH . WPINC . '/capabilities.php';
require_once ABSPATH . WPINC . '/user.php';
require_once ABSPATH . WPINC . '/meta.php';
require_once ABSPATH . WPINC . '/post.php';
require_once ABSPATH . WPINC . '/pluggable.php';
wp_cookie_constants();

is_user_logged_in() ||  auth_redirect();


list($basedir) = array_values(array_intersect_key(wp_upload_dir(), array('basedir' => 1)))+array(NULL);

$file = rtrim($basedir, '/') . '/' . (isset($_GET['file']) ? $_GET['file'] : '');
$file = realpath($file);

if ($file === FALSE || !$basedir || !is_file($file)) {
    status_header(404);
    die('404 &#8212; File not found.');
}

if (strpos($file, $basedir) !== 0) {
    status_header(403);
    die('403 &#8212; Access denied.');
}

This includes the fix for Wordpress 3.6.1 mentioned above (thanks sgissinger!) as well as checks the true filesystem path of the file using realpath() in order to prevents directory traversal attacks. The unmodified script has a simpler version of this in place, but handles it via string replacement - realpath() ensures that any symbolic links and references are resolved first.

After a lot of painful hours I found out that the WP include (require_once('wp-load.php');) is adding a linefeed in the output. As a result binary files are seen as "corrupted", e.g. my png files starts with a linefeed.

I tried to use the PHP OB functions to trap this extra linefeed but it is not working, i.e. the linefeed is still there.

Anyone faced the same challenge in the past? Anyone with a potential solution?

Thanks,

Christian

How about adding an action hook after line 16 (e.g. "do_action('dl_file_before_download');", or somesuch) to allow plugins to register callbacks for authority checks?

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.