0

variations of this question seem to loiter around on the web quite a bit, however I haven't quite found a solution that seems feasible and comprehensive enough to give me a decent clue on what to do. I will therefore give it a shot and post this here, being as specific as possible.

I am in the process of developing a simple web application that allows a group of restricted users to download files from a website after authenticating. The authentication part incorporating a mySQL backend was simple to achieve and works fine. For delivering the directory structure to the clients, I decided to use jqueryfiletree (https://github.com/daverogers/jQueryFileTree) which is easy to use and works fine. For testing purposes, I simple passed the filename on to the client, which then allowed it to download the file they clicked. This is the code that sets up the directory tree structure and allows the user to download the file:

$('#fileTreeObj').fileTree({ 
    root:'../filestore/',
    script: 'connectors/jqueryFileTree.php', 
    folderEvent: 'click', 
    expandSpeed: 500, 
    collapseSpeed: 500, 
    expandEasing: 'easeOutBounce', 
    collapseEasing: 'easeOutBounce', 
    loadMessage: 'Loading contents...' }, 
    function(file) { 
        openFile(file);
});

Of course this works only as long as the files reside in a location that is either inside apache's webroot or an other location directly accessible to the outside world. The only feasible way to restrict downloading any files from that directory structure is by placing the files outside the webroot and then pointing the filetree to that location- which will result in a http 404 error whenever a client tries to download a file:

Changing the File tree object to

.
.
root:'/var/filestore/',
.
.

results in

"The requested URL /var/filestore/afile.txt was not found on this server."

Which makes sense as the jquery script only delivers the file location, which is then requested by the client.

What I am trying to achieve is the following:

  • Place the downloadable files in a directory outside apache's webroot
  • Add a functionality which allows downloading the files from there
    • without allowing any non- authenticated users to download any files

Using symlinks or aliases are not an option as this compromises the entire authentication concept. Any non- authenticated user could download the entire directory structure provided that he or she is aware of the file names. I have a clue to the extent that I can not achieve this relying solely on jQuery, and that I have to add a php script to deliver the file to the client. Narrowing down what needs to be achieved I'd like to know:

  • How would a php function/ script look like which

    • reads a file of any given type and size from a non-webroot directory on the webserver
    • delivers the file back to a client for download
  • How do I taylor this together with my jQuery filetree? (How would I call the php script from there and how do I pass this on to the client?)

Please note that I havent't yet worked a lot with jQuery and php so I'd like to apologyze for this possibly trivial question. Any help and code snippets are greatly appreciated.

Thanks dear community!


Edit: After some further research I found this excellent post by Chris, which helped me in creating a PHP script to handle retrieving the file from outside the webroot. I'm still struggling with calling the script and returning the file, though. I set things up the following way:

Server side - PHP script (getFile.php)

<?php

$filename = $_POST['fbpath'];
$path = '/var/filestore/' . $filename;

header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($path));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
ob_clean();
flush();
readfile($path);
exit;

?>

Client side - jQuery integration

$('#fileTreeObj').fileTree({ 
        root: '/var/filestore/', 
        script: 'connectors/jqueryFileTree.php', 
        folderEvent: 'click', 
        expandSpeed: 500, 
        collapseSpeed: 500, 
        expandEasing: 'easeOutBounce', 
        collapseEasing: 'easeOutBounce', 
        loadMessage: 'Loading contents...' }, 
        function(file) { 
                $.ajax({
                type:"GET",
                url:"getFile.php",
                data:{fbpath: file},
                success: function(response){
                            openFile(response);
                    }


    });
});

When selecting a file for download by clicking it, I get the following error:

GET https://www.myserver.com/ftree/getFile.php?fbpath=%2Fvar%2Ffilestore%2Ffile1.txt net::ERR_CONTENT_LENGTH_MISMATCH

I'm probably mishandling the PHP output, therefore my questions are:

  • How would the proper AJAX call have to look like? What am I doing wrong?
  • How does the call need to be changed in order to allow the file to be downloaded?

Thanks so much everyone.

  • Mike
Community
  • 1
  • 1
Mike Floyd
  • 337
  • 3
  • 15

1 Answers1

0

OK, after some more research and a lot of trial and error I got this to work, the code now looks like this:

Client Side (jQuery/ AJAX)

function(file) { 

        $.ajax({
        type:"POST",
        url:"getFile.php",
        data:{fbpath: file},
        success: function(data, textStatus, XMLHttpRequest) {
            var popup = window.open();
                popup.document.write(data);
        }

        });
});

Server Side (getFile.php)

<?php

$filename = $_POST['fbpath'];
$path = $filename;

header('Content-Description: File Transfer');
header('Content-Type: application/force-download');
header('Content-Disposition: attachment; filename='.basename($path));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
ob_clean();
flush();
readfile($path);
exit;

?>

In the browser, the User will get a popup window containing the contents of the file. This is still not a very pretty solution: The user won't get a nice download prompt, he is thus confronted with the binary data in a new browser tab. Not exactly what I hoped for, and on top of that, it's ugly!

Some more reading revealed that jQuery/ AJAX need some more handling to provide a user- friendly file download. It seems that John Culviner encountered this issue a while ago ("You can certainly use an XMLHttpRequest object to download a binary (or otherwise) file but there is nothing you can do to the response to somehow get it saved on the user’s computer.") and has a nice and handy solution approach for this.

In terms of getting a file out of a safe location and getting it to the client's browser I consider this question answered however.

I hope this helps anyone else in the future!

Mike

Mike Floyd
  • 337
  • 3
  • 15