If anyone is interested, the full code of my snippet is below.
<?php
// filemanager_retreiver snippet
// Andrew Pettican November 2010
//
// This script retreives the requested file from assets/filemanager/ directory.
//
// GET Parameters
// - fd (File Descriptor)
// The path to the requested file. NB: this must be a file which exists in the assets/filemanager/ directory, but
// it can be supplied in pretty much any format:
// Example 1. contacts/joe_bloggs.jpg
// Example 2. assets/filemanager/contacts/joe_bloggs.jpg
// Example 3. /home/server/public_html/informationcentre/assets/filemanager/contacts/joe_bloggs.jpg
// Example 4. http://localhost/assets/filemanager/contacts/joe_bloggs.jpg
//
// - dl (Download Flag, optional, default: false)
// Identifies if the file should be supplied to the browser as a download or inline, to the browser page.
//////////////////////////////////////////////////////////////////////
mb_internal_encoding("UTF-8");
$output = "";
// Fetch parameters
// Leading '/' '\' '<' '>' '$' and '.' chars will be removed from the requested path
$file_path = ltrim($modx->getOption('fd',$_REQUEST,false), "/\\.<>$");
// Flag indicating if this file should load as a download.
$file_download = ($modx->getOption('dl',$_REQUEST,false) == "1");
// Settings
$filemanager_path = "assets/filemanager/";
$base_path = $modx->getOption('base_path').$filemanager_path;
$base_url = $modx->getOption('site_url').$filemanager_path;
$debug_mode = FALSE;
// Accepted file types and their mime's
// See: http://www.w3schools.com/media/media_mimeref.asp
// See: http://en.wikipedia.org/wiki/Internet_media_type#List_of_common_media_types
$accepted_file_types = array();
// Images
$accepted_file_types['jpg'] = "image/jpeg";
$accepted_file_types['jpeg'] = "image/jpeg";
$accepted_file_types['gif'] = "image/gif";
$accepted_file_types['png'] = "image/png";
$accepted_file_types['bmp'] = "bmp";
$accepted_file_types['tif'] = "image/tiff";
$accepted_file_types['tiff'] = "image/tiff";
// Data
$accepted_file_types['zip'] = "application/zip";
$accepted_file_types['eps'] = "application/postscript";
// Audio/Video
$accepted_file_types['wmv'] = "video/x-ms-wmv";
$accepted_file_types['mov'] = "video/quicktime";
$accepted_file_types['flv'] = "video/x-flv";
$accepted_file_types['mp4'] = "video/mp4";
$accepted_file_types['webm'] = "video/webm";
$accepted_file_types['ogv'] = "video/ogg";
$accepted_file_types['ogg'] = "video/ogg";
$accepted_file_types['mp3'] = "audio/mpeg";
// Debug
if($debug_mode) {
$output .= "<!-- Path/url requested : '$file_path' -->\n";
}
// Validation - stage 1
// Cut $filemanager_path, $base_path or $base_url from the start of the file path if it exists
$filemanager_path_len = mb_strlen($filemanager_path);
if(mb_strpos($file_path, $filemanager_path) === 0)
{
$file_path = mb_substr($file_path, mb_strlen($filemanager_path));
}
if(mb_strpos("/".$file_path, $base_path) === 0)
{
$file_path = mb_substr("/".$file_path, mb_strlen($base_path));
}
if(mb_strpos($file_path, $base_url) === 0)
{
$file_path = mb_substr($file_path, mb_strlen($base_url));
}
if($debug_mode) {
$output .= "<!-- (V1) Local path requested : '$file_path' -->\n";
}
// Validation - stage 2
// Determine the real path to the file, if the real path is different to the actual path, it could be v dangerous:
// e.g. assets/filemanager/../../../../restricted/dir/above/web/root.jpg
$file_path = $base_path.$file_path;
$real_path = realpath($file_path);
$validation_passed = ($real_path == $file_path);
if($debug_mode) {
$output .= "<!-- (V2) Real Path Check : '$real_path' == '".$file_path."' => ".($validation_passed ? "Y" : "N")." -->\n";
}
// Validation - stage 3
// Sanity check, ensure the $real_path begins with $base_path
$validation_passed = ($validation_passed && ($base_path == mb_substr($real_path, 0, mb_strlen($base_path))));
if($debug_mode) {
$output .= "<!-- (V3) Sanity Path Check : '$base_path' == '".mb_substr($real_path, 0, mb_strlen($base_path))."' => ".($validation_passed ? "Y" : "N")." -->\n";
}
// Fetch some file info from the path
$file_info = pathinfo($real_path);
$file_name = $file_info['basename'];
$file_ext = mb_strtolower($file_info['extension']);
$file_mime = "application/octet-stream";
// Validation - stage 4
// Ensure the file exists and has an accepted file extension
$validation_passed = ($validation_passed && $real_path != "" && is_file($real_path) && array_key_exists($file_ext, $accepted_file_types));
if($validation_passed)
{
$file_mime = $accepted_file_types[$file_ext];
}
// Debug
if($debug_mode)
{
$output .= "
<!--
file_path : '$file_path'
real_path : '$real_path'
file_name : '$file_name'
file_ext : '$file_ext'
file_mime : '$file_mime'
validation_passed : '".($validation_passed ? "Y" : "N")."'
-->
";
}
// Continue?
//$validation_passed = FALSE; // Debug
if($validation_passed)
{
// Streaming the file may take a while
set_time_limit(0);
// Stream!
header('Content-Description: File Transfer');
header('Content-Type: '.$file_mime);
header('Content-Disposition: '.($file_download ? 'attachment' : 'inline').'; filename='.$file_name);
header('Content-Transfer-Encoding: binary');
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($real_path));
ob_clean();
ob_end_flush();
readfile($real_path);
exit();
}
else
{
$output .= "Cannot read file";
}
return $output;
?>
I then created a resource in the document tree, using a blank template. This simply contains a single call to the snippet:
[[!filemanager_retreiver]]
You can then do the following:
http://localhost/filemanager-retreiver.html?fd=contacts/joe_bloggs.jpg
This will serve the file inline to the browser. This allows you to do something like <img src="filemanager-retreiver.html?fd=contacts/joe_bloggs.jpg" alt="" />
http://localhost/filemanager-retreiver.html?fd=contacts/joe_bloggs.jpg&dl=1
This will serve the file to the browser as a download rather than inline.
I’ve also tried to make it flexible, so that the following requests all return the same file
1.
http://localhost/filemanager-retreiver.html?fd=contacts/joe_bloggs.jpg
2.
http://localhost/filemanager-retreiver.html?fd=assets/filemanager/contacts/joe_bloggs.jpg
3.
http://localhost/filemanager-retreiver.html?fd=/home/server/public_html/informationcentre/assets/filemanager/contacts/joe_bloggs.jpg
4.
http://localhost/filemanager-retreiver.html?fd=http://localhost/assets/filemanager/contacts/joe_bloggs.jpg
Currently I use a hardcoded array of file types, only file extensions in this array will be returned (see $accepted_file_types).
I’ve also setup a .htaccess rule so that all requests to assets/filemanager/* get rerouted through
http://localhost/filemanager-retreiver.html?fd=assets/filemanager/* ... Pretty neat I think
# Pass Filemanager Requests through filemanager-retreiver.html
RewriteCond %{REQUEST_URI} assets/filemanager/*
RewriteRule ^(.*)$ filemanager-retreiver.html?fd=$1 [L,QSA]
Like I said earlier, I’m sure there are more efficient/cleaner ways of achieving what I want, but this is working pretty well for me. I’m open to suggestions for improvement though.
To ensure files cannot be read by unauthorised users, I believe it should be sufficient to set access permissions on the filemanager-retreiver.html document. I suppose the snippet could be modified to perform additional checks too.