Secure File Download from a path outside of website root

How would I use the Server Action „if file exists“ with a path outside of wwwroot?

Paths outside site root are not supported in server connect.
That’s not something usually used and we have not integrated such a functionality in Server Connect.

I understand, but is there a workaround with php, like @George mentioned?

Still feeling like utter sh*t my friend unfortunately… Thanks for asking though!

How about storing the name of the file and the full path/url to the file in the database and using something like a scheduled event to keep everything updated and fresh? Timestamp the new files and so anything less than or equal to a specific time period is removed from the table and only files greater than the set time criteria remain?

Maybe a scheduled event is a bit much… Possibly just a view based on less than or greater than will be OK. As long as the full path/url is in the DB it should all be fine?

I‘m not sure if I understand what you mean.

It would be no problem for me to adjust the view with the full path.

I think my main problem will be the adjustment of the php script @George suggested.

Do you have an idea how to adjust this with Server Connect repeat output?

Inside your www directory, create a "image.php" file, with a similar content to:

<?php
  header('Content-Type: image/png');
  readfile("../img/" . $_GET['img']);
?>

And call your images with

<img src="image.php?img=myimage.png" />

Hi. The solution given by @George seems similar to your current setup.
Correct me if I am wrong, in your current setup, clicking on download button redirects to another PHP file (which is in your web root) where the full path of the file outside root is created and file begins downloading.

If the above observation is correct, the stackoverflow solution works in the same way. You create a file in web root, downloadimg.php, with code something like this:

<?php
header('Content-Type: image/png');
readfile("../../usr/home/myuser/private/" . $_GET['dbpath'] . "/" . $_GET['img']);
?>

Then, in your Wappler project, download link can be created like so:

<a href="downloadimg.php?dbpath=something&img=filename">Click to Download</a>

Clicking on download link will redirect to another PHP file (which is in your web root) where the full path of the file outside root is created and file begins downloading.

1 Like

Hi @nshkrsh,

thanks for your help. You are correct with my current setup.

In my current solution I use a form based download with a button. This way the path doesn’t get exposed in the URL. In the hidden value there is the Id of the document.

In a separate query on POST it will get the correct path and filename.

I’m not sure if I can do this with Wappler.

Any concerns about security?

I don’t think you need to change your correct setup in any way.
The existing method of sending POST data can be replicated in Wappler.

And, if you want to use the solution given in stackoverflow, you can change the simple anchor tag with a POST form and fetch these values in second PHP file via $_POST.

I assume you already have some security in place in the page where download button is. As for the second download page, you will have to add the same security measures before the actual file download code executes, and you should be good to go.
I usually am paranoid about security and never satisfied with any solutions. You must await reply of someone else more confident about this.

Hi @George, @Teodor and @patrick,

any chance that this will be integrated in future versions of Wappler?

Hi,

I have to come back to my problem from January, as I’m approaching the end of my conversion of our website from webassist to Wappler.

I can’t figure out how to create the download.php file with Wappler.

This is how my form looks like at the moment:

<form role="form" dmx-bind:id="form1_{{$index}}" method="post" dmx-bind:name="form1_{{$index}}" action="../../../download.php"><input name="hiddenField" type="hidden" id="hiddenField" dmx-bind:value="DokumentId"><button type="submit" class="pull-right btn btn-sm btn-default-transparent btn-animated" name="download" value="Dokument laden">Dokument laden<i class="fa fa-download"></i></button></form>

How should the download.php file look like? I created a action in Sever Connect which filters the query with the hidden POST value to get the correct path and filename.

Thanks for your help in advance.

This is how the download.php file looks at the moment:

<!doctype html>
<html><head>
  <base href="/">
  <meta charset="UTF-8">
  <title>Download</title>
  <script src="dmxAppConnect/dmxAppConnect.js"></script>
  </head>
  <body is="dmx-app" id="download">
    <dmx-serverconnect id="DokumenteDownload" url="dmxConnect/api/Kundenportal/Dokumente/naviDokumenteDownload.php"></dmx-serverconnect>
    <?php 
      header('Content-Type: application/octet-stream');
      readfile("../../usr/home/myuser/private/" . $_POST['dbpath'] . "/" . $_POST['']);
    ?>
  </body></html>

I know, that it is not complete, but I can’t figure out how to insert the path and the filename from my query into the php part.

Hi @George, @Teodor & @patrick,

any idea how to set this up in Wappler?

Hi.
The download.php file should be in the website folder to be accessible from form action.
../../../download.php makes me think, its not. So that’s the first thing you need to fix.

Next, the DokumentId hidden field’s name is hiddenField, so that is what you will need to access the ID.
If that is not the file name, bind file name in the hidden field.
Then, in download.php, you do not have to add app connect or server connects. Just hard code the path, and get the file name from $_POST['hiddenField'] in the readfile method.

Looking at the code, it seems are trying to fetch some document details to make the complete path.
If that is the case, you need to set the form as a Server Connect form and then send the document ID to the server action which would return the extra document details, from DB.
Create another form with action as download.php, method as post, and multiple hidden fields - 1 each for the extra details.
Then in the on-success event of main form server connect, set all the extra details in hidden fields of second form to value from main form server connect response. And, also submit the second form from on-success.

This all seems a bit overkill, but it will keep the path hidden. The file name and other details will still be visible… so if you can fetch it when you get the document ID, you will not need the second form and it will be a bit simpler.

Hi Nishkarsh,

thanks a lot for your time. I think I get the logic now. What I’m missing, is how to get the values for the second form. How do I get the posted values from my first form to insert them in the second form in the same page. Do I have to insert the Server Connect Action which I used in the form, a second time?

The main server connect form which you post documentID with, is the same place you will find the response values.
In the hidden field of second form, go to dynamic attribute > Set Value, then in the dynamic picker, find the main form, and you will see a data option which will contain the response.

A server connection form works exactly like a regular server connect, except that they need a form submit to be invoked.
SCForms also have the same events - executing, processing, start, done, error, success etc.

I succeeded with the correct values for my second form. What doesn’t work is that the second submit does not transfer the POST Values to the download.php file.

This is how my forms look like:

<form role="form" dmx-bind:id="form2_{{$index}}" method="post" dmx-bind:name="form2_{{$index}}" action="../../../dmxConnect/api/Kundenportal/Dokumente/naviDokumenteDownload.php" is="dmx-serverconnect-form" id="NaviAktPersDownload" dmx-on:success="formDownloadPers.submit()"><input name="dokid" type="hidden" id="hiddenFieldPers" dmx-bind:value="DokumentId"><button type="submit" class="pull-right btn btn-sm btn-default-transparent btn-animated" name="download" value="Dokument laden">Dokument laden<i class="fa fa-download"></i></button></form><form id="formDownloadPers" role="form" method="post" action="../../../download.php">
<input id="DateiPfad" name="DateiPfad" type="hidden" dmx-bind:value="NaviAktPersDownload.data.dokumenteDownload[0].DateiPfad"><input id="Dateiname" name="Dateiname" type="hidden" dmx-bind:value="NaviAktPersDownload.data.dokumenteDownload[0].Dateiname">

FYI DateiPfad = Path, Dateiname = Filename

Any idea?

Here is a screenshot of my current download.php

Clever with the 2 forms, but the problem is that App Connect updates the DOM async. The inputs in the second form probably didn’t update before the submit was called.

I will do some testing myself and see if I can find a better solution.

1 Like

Hi @patrick,

thanks a lot for your help.

While Patrick gives a better solution, can you check if the download page is even getting called?
And if it is, what does print_r print?

If that part is working, any only the values are missing… try adding a delay to the success event of first form.
dmx-on:success.debounce:300
This works even on some events which do not expose this option in the UI picker, but I haven’t tested with success event. Here 300 is milliseconds, probably enough time for App Connect to fill in the values. :sweat_smile:

NOTE: This looks like bit of a hack, but delay should work for you in this case as we are calling it only after we have data.
Don’t use it in inconsistent places, where flow of execution might take 2 seconds or 30.

2 Likes