Jump to content

Website with protected download area


sins7ven
 Share

Recommended Posts

Hi there,

I am kinda new to PW but I gave it a try for a current project and I am pretty amazed so far. It really works great and allthough I have pretty much no knowledge about PHP I can wrap my mind around these functions and also get lots of my queries working. My site is almost complete but I also struggeling with a problem which is the following.

I am building a website for a music label. They want to provide a dedicated press and download area where press people can download latest press releases, music, artworks and stuff like this.

The site structure should be like this:

www.domain.com

 - Page 1

 - Page 2

 - and so on

and the press area which should be www.domain.com/press 

The link to the press area is not visible on the main website. The page is hidden and should have a simple protection with htaccess. No user or login / member management required. Just a simple htaccess password protection. How can I do that? What I would normally do is just create a folder "press" on my FTP directory where I put the htaccess and thats it but how can I accomplish this in PW?

I also provide downloads on that press page. These files for download have been uploaded via the backend and now sit in the assets/file folders. How can I protect them from being downloaded? I want to achieve that once people got access to www.domain.com/press that they are also able to download these files. 

Maybe its a simple task but I am not sure how to do this properly.

Thanks for your help - I really appreciate all that kind support here in the forum!

Cheers,

Martin

Link to comment
Share on other sites

I think you could build a simple template for the "press" page which makes use of HTTP Auth. It's similar to what is described here: http://www.php.net/manual/en/features.http-auth.php

The files can be protected by a simple download proxy script, which takes care of permission and delivers the file (the files originally stays assets/files/..., but since no one knows the real path, it's safe by obscurity.) It's not what I would call NASA safety, but I think it's safe enough for your purpose.

To be sure only files are served which are published, set this option in /site/config.php: set "$config->pagefileSecure = false;" to true.

Link to comment
Share on other sites

just another question - how complicated would it be to build this with the user/permission settings from PW? Maybe its possible to create a user (maybe called "press") and then only this user is allowed to view a particular page where only this user is allowed? Is that possible somehow? 

Link to comment
Share on other sites

Welcome to the forums, sins7ven.

This is much more comfortable and and it's simple!

(I will reply more here in a few minutes, just have to do a phone call.)

------

EDIT:

There are different possibilities, you can go with creating a role and a template and only allow access to pages with that template if user has a this special role. Or you could do it on template basis, what I would prefer in this situation. Because you need to build / provide a frontend login form for the press users. (I don't would send them to the backend)

How to do it via template:

  1. create a new template file press.php and put it into the templates folder of your site profile
  2. in admin create a new template "press" by selecting the templatefile
  3. create a new user "press" and assign him only the guest-user
  4. create a new page "press" in PW, set it to "hidden", (you should upload all downloadable stuff to this page only!)
  5. now you have to edit your templatefile press.php as following:

At first you need to check if the page is requested by a logged in user. If yes, you may check which user / which role he has, if you have different ones. But I assume you only have stuff and press and nothing more actually.

// check if user is logged in
if(!$user->isLoggedIn()) {
    // present him a login form with fields for username, password and a hidden field called "presslogin"
    // send the form via post
    ...
    return;  // end with execution here
}

if($input->post->presslogin) {
    // a user try to login
    $user = $sanitizer->username( $input->post->username );
    $pass = strval(strip_tags($input->post->password));
    $u = $session->login($user, $pass);  // try to login with the posted credentials and save result temporary in variable $u
    if($u) $user = $u;  // if it was successfull, override the current user with the temporary one
    // last check if user is logged in now
    if(!user->isLoggedin()) {
        // present him the login form or what ever you like
        ...
        // and stop exeuting this page
        return;
    }
}

// user is already logged in, following the stuff for presenting the download listings
....

If you need to check for specific users, it could be like:

if(!$user->hasRole('superuser|editor') {

// or

if(!$user->name == 'press') {

How to build a simple login form and how to use the $session->login() etc could be found here in the forums, in the API and useful is the cheatsheet.

Edited by horst
  • Like 7
Link to comment
Share on other sites

Thank you very much Horst - this is a great starting point for me. I already have the basic login procedure running. Regarding the user I also gave the new user "press" a dedicated role. You wrote that its only neccessary to asign him to the "guest-user" role but shouldn't he be asigned to the press role as well? Anyway this is a good start for me and I slowly getting there so thanks a lot for your help! :)

Link to comment
Share on other sites

Yes, if you have created the a role for press, you must assign him to it!

I have explained an approach without a special role "press", just with a user. But it is pretty fine to use a role. :)

Link to comment
Share on other sites

Now it works! I also used parts of the code from this post https://processwire.com/talk/topic/107-custom-login/ So I am bit more flexible now and I can include the login form wherever I want to because I also have some child pages below my press page and I only needed to include that login code snippet in my child pages and thats it. Amazing!

Now I need to figure out how to set up these download redirects because it doesn't matter if a user is logged in or not when he know the exact path to the files that are situated in the assets/files folders.

  • Like 1
Link to comment
Share on other sites

A short update on my project. I tried to build a download passthrough page based on this forum post: https://processwire.com/talk/topic/3634-down-uploads-on-a-per-user-policy/ but my script is not working. 

This is how my script looks like (download.php):

<?php
include("./index.php");

if($user->isLoggedin()){
   
    $file = $_GET['file'];
    $filepath = $config->path->files.$page;
    $filename = $filepath . '/' . $file;

    $options = array(
        'exit' => true, 
        'forceDownload' => false, 
        'downloadFilename' => '',
    );

    wireSendFile($filename, $options);
    
    } else {
        wire("session")->redirect("/");
    }
?>

My download links ony my press page are looking like this. In this particular example its looping through all the logos that I have in my download section.

<?php foreach($page->logos as $logo) { ?>
    <p><a href="download.php?file=<?php echo $logo->logo_file; ?>">Download file</a></p>	
<?php }	?>		

When I click on one of these download links I always get redirected to my standard 404 page which means that no page was found. But what exactly is the problem? Is it the download.php file which cannot be found? I placed it as a normal php file into my templates directory.

Seems I am getting slowly into all of this but at this particular point I am stuck right now :)

Link to comment
Share on other sites

sorry, I answer very short because I'm on the run ;-)

 

// you need to use bootstrapped code

$user-> versus wire('user')->

$pages-> versus wire('pages')->

etc.

And also: $filepath = $config->path->files . wire('pages')->get(  ?? which page ?? );
 
You need to send the ID of the page you refer to:

<?php
    $url = $config->urls->root . 'download.php';
    $url .= '?pageid=' . $page->id . '&file=' . $logo->logo_file->name;
?>
<a href="<?php echo $url ?>">Download file</a>

You can read more about including / bootstrapping pw here: http://processwire.com/api/include/

Edited by horst
  • Like 1
Link to comment
Share on other sites

thank you both for your input but I think the problem is that every download that I provide on my press page, has been uploaded via the backend to the page so as it is the nature of PW every single download file sits in its own folder. And I need to tell the download script what exactly that ID of each folder is to append it to the download path. That is why I thought I receive these individual IDs by using $page->id. But obviously that only gives me the ID of the download page itself and not of each individual download.

Link to comment
Share on other sites

What do you mean with setup? I am running this site on a local machine with Xampp. Its not on a live server. Or do you mean the code? This is what I have so far. 

Example Downloadlink from my press page:

<?php foreach($page->logos as $logo) { ?>
	<p><a href="<?php echo $config->urls->root ?>download.php?pageid=<?php echo $logo->id ?>&file=<?php echo $logo->logo_file; ?>">Download File</a></p>	
<?php }	?>	

As you can see I managed to receive the individual page IDs from each download folder ($logo->id) and it is now included to the link.

The download.php file:

<?php
include("./index.php");

if(wire("user")->isLoggedin()){
   
    $filename = wire('input')->get->file;
    $pid = wire('input')->get->pageid;
    $filepath = $config->path->files . $pid;
    $full_file = $filepath . '/' . $filename;

    $options = array(
        'exit' => true, 
        'forceDownload' => false, 
        'downloadFilename' => '',
    );

    wireSendFile($full_file, $options);
    
    } else {
        wire("session")->redirect("/");
    }
?>

So both "file" and "pageid" from the link are handed over to the script that I am able to create the full path to the file. At least this is what I thought would work ;)

I upload the files to via the PW backend to the download page. I set up various repeater fields for various types of downloads ( for example, one repeater for logos, one for images, one for documents etc.) These Repeaters are looping through the uploaded files via foreach so I basically end up with a download page and various download links. Everything has been set up with an own template and everything worked fine except now when I try to get this download passthrough running :)

The download page also is going to have some child pages (in my example for a music label, each artist gets his own download page as a child so its going to look like this in the end:

www.domain.com/press/artist/

Sorry if that sounds a bit confusing :)

Link to comment
Share on other sites

Yes it sounds confusing :)

You have nothing said from repeaters. Why do you use them? I have thought you just use fileInputfields? (Would be much easier!)

So, anyways, lets have some looks, but step by step:

What exactly is $page->logos? A repeaterfield? What fields / fieldtypes has it bundled? (name = type)

As you can see I managed to receive the individual page IDs from each download folder ($logo->id) and it is now included to the link.

I'm not sure with this. What does it output your $logo->id? Differend ids?

just use for debugging something like:

<?php 
    foreach($page->logos as $logo) {
	echo "<p>{$config->urls->root}download.php?pageid={$logo->id}&file={$logo->logo_file}</p>";
    }
?>

Please post a few lines of its output here.

After that we will go on.  :)

Link to comment
Share on other sites

Sorry when I missed to mention the repeaters earlier. I was not aware that these could have an important effect to the logic of the script. But anyway - you are right. $page->logos is a Repeater Field. It consists of an file field (logo_file) and an image field (logo_thumb). The result on the page looks something like this (see attachment)

post-2329-0-27441400-1401052160_thumb.jp

Everything works fine. In my template I use the following code to get this result:

<ul class="blocks-5 dl-list">			
  <?php foreach($page->logos as $logo) { ?>  
  <li>
    <a href="<?php echo $logo->logo_file->url; ?>" title="<?php echo $logo->logo_file->description; ?>">
    <img src="<?php echo $logo->logo_thumb->url; ?>" title="<?php echo $logo->logo_thumb->description; ?>"   alt="<?php echo $logo->logo_thumb->description; ?>">
    <h3><?php echo $logo->logo_thumb->description; ?></h3>
  </a>	    		
  </li>
  <?php }	?>			    
</ul>

When I use your foreach code I get a result something like this:

/localhost/download.php?pageid=1057&file=hi_res_100_percent_pure_logo.zip
/localhost/download.php?pageid=1058&file=hi_res_remote_area_logo.zip

Thanks for your time! I really appreciate this!

Link to comment
Share on other sites

Sorry when I missed to mention the repeaters earlier. I was not aware that these could have an important effect to the logic of the script.

 No problem! But it is hard to follow, if something like this isn't in the info. :)

Everything works fine. In my template I use the following code to get this result:

Great! And your files are protected now, even if the url is known?

Link to comment
Share on other sites

Sorry what I wanted to say was that this page with the repeaters etc was working fine. The output of the fields and the structure. The only problem is still that download script is not working.

I replaced the <a href ..> with the code that builds the download link that it looks like this now:

<?php 
foreach($page->logos as $logo) {
echo "<p><a href='{$config->urls->root}download.php?pageid={$logo->id}&file={$logo->logo_file}'>{$config->urls->root}download.php?pageid={$logo->id}&file={$logo->logo_file}</a></p>";
}
?>

And the link is looking like this:

/return-pw/download.php?pageid=1057&file=hi_res_100_percent_pure_logo.zip
/return-pw/download.php?pageid=1058&file=hi_res_remote_area_logo.zip

But its still not working. When I click on it I get redirected to my 404 page.

Link to comment
Share on other sites

now THAT is confusing ;) Sorry for that - this was on my second installation on a laptop where I have a subdirectory under my localhost folder. So "return-pw" is the project folder where my PW installation is located. 

I placed the download.php file into the sites/templates directory. Maybe that is the problem? Does it belong into the document root of the website project ? 

Link to comment
Share on other sites

  • 3 weeks later...

Hi there,

just a small update on this for those who are interested. Finally I've got my script / download proxy up and running. 

This is the code that generates a list with downloadable files (in this case logos) through a repeater field. 

<?php 
foreach($page->logos as $logo) {
echo "<p><a href='{$config->urls->root}download.php?pageid={$logo->id}&file={$logo->logo_file}'>{$config->urls->root}download.php?pageid={$logo->id}&file={$logo->logo_file}</a></p>";
}
?>

So a download links then looks like this:

http://www.domain.com/download.php?pageid=1057&file=hi_res_100_percent_pure_logo.zip

Here is the download.php:

<?php
// bootstrap PW
include("./index.php");

// make sure user is logged in
if(wire("user")->isLoggedin()){
    $fn = wire('input')->get->file;
    $pid = wire('input')->get->pageid;
    $filepage = wire("config")->paths->files;  
    // put together the complete path to the file
    $filename = $filepage.$pid.'/'.$fn;   
        if(!$filename) die("download not found");
        // send file to browser
        wireSendFile($filename, array('exit' => true));
} else {
    wire("session")->redirect("/");
}
?>

Everything works great now. However I still wanted to protect the original path of the files (assets/files/...) allthough nobody would know the exact location. To do this I modified to the .htaccess to suite my needs to block all unwanted access to files with the most common endings for downloads (e.g. zip, rar, pdf, etc.)

So the part in my .htaccess which takes care of that is the following:

<FilesMatch "(^#.*#|\.(htaccess|htpasswd|ini|phps|bak|config|dist|fla|in[ci]|log|psd|pdf|zip|rar|sh|sql|sw[op])|~)$">

    # Apache < 2.3
    <IfModule !mod_authz_core.c>
        Order allow,deny
        Deny from all
        Satisfy All
    </IfModule>

    # Apache  2.3
    <IfModule mod_authz_core.c>
        Require all denied
    </IfModule>

  </FilesMatch>

I found that part during my research for .htaccess modifications and finally I ended up with using that part of the HTML5 Boilerplate. I know that might be a bit strict and I don't know yet if there are any issues with that but so far everything seems to work fine. 

Thanks to all the helpers (especially Horst) for guiding me that way to get my solution to work. :)

  • Like 4
Link to comment
Share on other sites

  • 1 month later...

@sins7ven: I'm a bit scared how you use the download filename concatenation with the user input. Maybe I'm wrong, but before you go online you should test if something like this can work:

http://www.domain.com/download.php?pageid=../logs/&file=errors.txt

or this one?

http://www.domain.com/download.php?pageid=../../&file=config.php

or this?

http://www.domain.com/download.php?pageid=&file=../logs/messages.txt
  • Like 4
Link to comment
Share on other sites

@sins7ven - thanks for sharing the script!

I agree with horst on this. I think this version should be much safer, although it might need some more thought.



$fn = wire('input')->get->file;
$pid = (int) wire('input')->get->pageid;
$p = wire('pages')->get($pid);

$filename = $p->logo_file->get("name=$fn")->filename;


Firstly, it makes sure that pageid is an integer (so that no-one can try entering a path instead) and then gets the page object from that integer. Then it finds the file in the logo_file field that matches the name in $fn. This ensures that the file path in $filename can only be a file from the logo_file field and that it is a valid file in the PW database.

You might also want to consider sanitizing the filename that is passed via get->file using something from the cleanBasename core function:


You could also do some checks to make sure that both pageid and file are not blank and throw an exception if they are, or if the pageid is not provided as an integer, or get->file does not match it's sanitized version - both of which might indicate attempted hacks.

An even better, more secure approach might be to use child pages instead of repeaters (or the new PageTable field) and just pass the id of the page to the download script, so with one image per page, the page id is all that you would need to identify the path to the file and start the download.

  • Like 5
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...