array(
'#type' => 'textfield',
'#title' => t('Root directory'),
'#default_value' => filebrowser_root(),
'#maxlength' => '100',
'#size' => '70',
'#description' => t('Root directory used to present the filebrowser interface. Users will not be able to go up from this folder. Only a directory name under the Drupal root is accepted. Example: "public/files". No trailing slash. For super-user access to all web files, enter "." (at your own risk) '),
),
'filebrowser_icons' => array(
'#type' => 'textfield',
'#title' => t('Icon directory'),
'#default_value' => variable_get('filebrowser_icons', ''),
'#maxlength' => '100',
'#size' => '70',
'#description' => t('Name of directory, where file type icons are stored. Files should be named "file-txt.png", "file-gif.png", etc. The default icon is "file-default.png".')
),
'filebrowser_hide_description_files' => array(
'#type' => 'radios',
'#title' => t('Display of description files'),
'#default_value' => variable_get('filebrowser_hide_description_files', 0),
'#options' => array(t('Show'), t('Hide')),
'#description' => t('Whether to show or hide description files from directory listings.'),
)
);
}
/**
* Implementation of hook_perm().
*/
function filebrowser_perm() {
return array('access filebrowser');
}
/**
* Implementation of hook_menu().
*/
function filebrowser_menu($may_cache) {
$items = array();
if ($may_cache) {
$items[] = array('path' => 'filebrowser', 'title' => t('filebrowser'),
'access' => user_access('access filebrowser'), 'callback' => 'filebrowser_page',
'type' => MENU_SUGGESTED_ITEM);
}
return $items;
}
/**
* Prints a folder layout
*/
function filebrowser_page() {
// Build breadcrumb list for uplevel folders
$parts = func_get_args();
$subfolder = join('/',$parts);
// set context for later filebrowser links
$filebrowser_base = filebrowser_base($subfolder);
filebrowser_proper_path('', $filebrowser_base);
filebrowser_set_breadcrumbs($parts);
// Set page title
drupal_set_title(t('%dirname directory', array("%dirname" => $dirname)));
// Default list of columns to render
$cols = array('icon','name','size','age','info');
if($_REQUEST['edit']['cols']){$cols = $_REQUEST['edit']['cols'];}
filebrowser_set_context($filebrowser_base, $cols);
// LIST FETCHED HERE
// No files to list, return with an empty page
if (!($files = filebrowser_get_list($subfolder, $cols))) {
drupal_set_message(t("Unable to get files for this directory '%subfolder'",array('%subfolder'=>$subfolder)));
return theme("filebrowser_page", '');
}
// Prepare headers for display
$headers = array_map(
create_function(
'$colname',
'$func = "filebrowser_get_cell_".$colname; if(function_exists($func))return $func(""); '
),
$cols
);
// Set sorting criteria eg. array(the 'field' key's associated value, asc/desc)
filebrowser_sort_table(array(tablesort_get_order($headers), tablesort_get_sort($headers)));
// Protect folders from being resorted
$folders = array();
foreach ($files as $rnum => $filei) {
if ($filei['size']['data'] == '') {
$folders[] = $filei;
unset($files[$rnum]);
}
}
usort($files, 'filebrowser_sort_table');
$files = array_merge($folders, $files);
// Drop attributes only used for sorting, so
// these will not end up in the HTML output
foreach($files as $rnum => $filei) {
foreach($filei as $cnum => $cell) {
if (is_array($cell)) {
unset($filei[$cnum]['sv']);
}
}
$files[$rnum] = $filei;
}
// Allow modules to inject content above the table
$pre = join('', module_invoke_all('filebrowser_pre', $subfolder));
// Allow for themeing of the table
return theme('filebrowser_page', $files, $headers, $pre);
}
/**
* Theme a filebrowser page, if files are available or not.
* Here you have some possibility to reformat the data or the table layout.
*/
function theme_filebrowser_page(&$files, $header = NULL, $pre = NULL) {
if ($files) {
// CSS can hook on this ID to style table elements differently
return '
' . $pre . theme("table", $header, $files) . '
';
}
else {
return '';
}
}
/**
* Returns a list of files in a subfolder under the admin
* specified filebrowser root. File system details (size, last
* modification) is added, plus a metafile is parsed to gather
* more information, if available.
* @param $subfolder the folder to scan. Must be under and relative to the
* system files dir unless $unsafe is set
* @param $columns a list of column identifiers to show
* @param $unsafe If set, the listing will NOT prepend the file path to the
* subfolder. File listings from anywhere in the server become possible!
* @return an array of table cells
*/
function filebrowser_get_list($subfolder = '', $cols = array('expander','icon','name','size','age','info'), $unsafe=FALSE) {
$folder = ($unsafe)?$subfolder:filebrowser_safe_folder($subfolder);
if(is_file($folder)){$folder = dirname($folder);}
$inroot = ($folder == filebrowser_root() );
if(!$folder){$folder='.';}
// Signal error in case of bogus directory name
if (!(is_dir($folder) && ($dir = opendir($folder)))) {
drupal_set_message($folder." was not readable");
return FALSE;
}
// Collect folders and files separately and check for a metainfo file
$files = $folders = array();
$infofile = FALSE;
while (($entry = readdir($dir)) !== FALSE) {
if (is_dir("$folder/$entry")) {
// Skip version control system folders
if (!in_array($entry, array(".svn", "CVS"))) {
$folders[] = $entry;
}
}
else {
// Grab information file, decide whether it should be shown or not
if (in_array(strtolower($entry), array("descript.ion", "files.bbs"))) {
$infofile = $entry;
filebrowser_get_cell_info("$folder/$infofile"); // prefetch the info
if (!variable_get('filebrowser_hide_description_files', 0)) {
$files[] = $entry;
}
}
// Display every other file
else {
$files[] = $entry;
}
}
}
closedir($dir);
// Order folders first, then files
sort($folders);
sort($files);
$files = array_merge($folders, $files);
// Build detailed list of files
$details = array();
foreach ($files as $file) {
if ( $file == '.'){continue;}
if ($inroot && $file == '..'){continue;}
// no up from the top, but it's allowed the rest of the time.
// Some real folder or file
$completepath = ($folder && $folder != '.') ? "$folder/$file" : $file;
if ($stat = stat($completepath)) {
$row = array();
// populate the columns
foreach($cols as $colname){
$func = 'filebrowser_get_cell_'.$colname;
if(function_exists($func)){
$row[$colname] = $func($completepath);
}
}
$details[] = $row;
}
}
return $details;
}
function filebrowser_build_table($files,$folder,$cols){
$inroot = ($folder == filebrowser_root());
// Build detailed list of files
$details = array();
foreach ($files as $file) {
if ( $file == '.'){continue;}
if ($inroot && $file == '..'){continue;}
// the up link is now treated almost normally
// Some real folder or file
$completepath = "$folder/$file";
if ($stat = stat($completepath)) {
$row = array();
// populate the columns
foreach($cols as $colname){
$func = 'filebrowser_get_cell_'.$colname;
$row[$colname] = (function_exists($func)) ? $func($completepath) : '';
}
$details[] = $row;
}
}
return $details;
}
/**
* Callback to render just one cell of data - the name link
* A filebrowser_get_cell_hook()
*/
function filebrowser_get_cell_name($completepath){
if(! $completepath){return array('data' => t("Name"), 'field' => 'name'); }
$file = basename($completepath);
if (is_dir($completepath)) {
// for this link I need to prefix the filebrowser relink, BUT chop the filebrowser directory part
$relpath = filebrowser_trim_root($completepath);
$href = filebrowser_proper_path($relpath);
}
else {
$href = $completepath;
}
if ($file == ".." ) { $file = ".. up " ;}
$link = l($file, $href);
return array('data' => $link, 'class' => 'filename', 'sv' => $file);
}
/**
* Return the formatted size of this file.
* A filebrowser_get_cell_hook()
*/
function filebrowser_get_cell_size($completepath){
if(! $completepath){return array('data' => t("Size"), 'field' => 'size'); }
if(is_dir($completepath)){return array();}
$size = filesize($completepath);
return array('data' => $size?format_size($size):'', 'class' => 'size', 'sv' => ($size ? $size : 0));
}
/**
* Return the formatted age of this file.
* A filebrowser_get_cell_hook()
*/
function filebrowser_get_cell_age($completepath){
if(! $completepath){return array('data' => t("Age"), 'field' => 'age'); }
$age = time() - filemtime($completepath);
return array('data' => format_interval($age), 'class' => 'age', 'sv' => $age);
}
/**
* Return the deduced description or extended info for this file.
* A filebrowser_get_cell_hook()
*/
function filebrowser_get_cell_info($completepath){
if(! $completepath){return array('data' => t("Info")); }
static $info;
if(! isset($info)){
// The first time this cell is requested, look hard into the current directory
// cache results
if(!is_file($completepath)){
$subfolder = (is_dir($completepath))?$completepath:dirname($completepath);
$completepath = $subfolder.'/descript.ion';
if(! file_exists($completepath)){
$completepath = $subfolder.'/files.bbs';
}
}
if(is_file($completepath)){
$info = filebrowser_get_fileinfo($completepath, dirname($completepath));
}
else {
$info = array();
}
}
$file = basename($completepath);
// each info returned was an array. - not sure how to deal with that in one cell
$detail = (is_array($info[$file])) ? array_pop($info[$file]) : $info[$file] ;
return array('data' => $detail, 'class' => 'info');
}
/**
* Loads file metainformation from the specified file. Also
* allows the file to specify a *callback* with which the
* descriptions are parsed, so more metainformation can be
* presented on the output.
*/
function filebrowser_get_fileinfo($fullpath = NULL, $subfolder = '') {
static $metacols = array();
// Return (previously generated) meta column list
if (!isset($fullpath)) {
return $metacols;
}
// Build meta information list
$metainfo = array();
if (is_readable($fullpath) && ($file = file($fullpath))) {
foreach ($file as $line) {
// Skip empty and commented lines
if (trim($line) == '' || strpos(trim($line), '#') === 0) {
continue;
}
list($name, $description) = explode(" ", $line, 2);
if (isset($metainfo[$name])) {
$metainfo[$name] .= trim($description) . " ";
}
else {
$metainfo[$name] = trim($description) . " ";
}
}
$callback = FALSE;
if (isset($metainfo['*callback*']) && function_exists(trim($metainfo['*callback*']))) {
$callback = trim($metainfo['*callback*']);
unset($metainfo['*callback*']);
}
if (isset($metainfo['.'])) {
filebrowser_help(NULL, $metainfo['.']);
unset($metainfo['.']);
}
foreach ($metainfo as $name => $description) {
$metainfo[$name] = ($callback ? $callback(trim($description), $subfolder, $name) : array(trim($description)));
}
$metacols = ($callback ? $callback() : array(t('Description')));
}
return $metainfo;
}
/**
* Returns the appropriate HTML code for an icon representing
* a file, based on the extension of the file. A specific icon
* can also be requested with the second parameter.
*/
function filebrowser_get_cell_icon($fullpath = NULL, $iconname = NULL) {
if (isset($fullpath)) {
$iconname = (is_dir($fullpath) ? 'folder' : preg_replace("!^.+\\.([^\\.]+)$!", "\\1", $fullpath));
}
elseif (!isset($iconname)) {
$iconname = 'default';
}
$iconfiles = array(
variable_get('filebrowser_icons', '') . "/file-$iconname.png",
variable_get('filebrowser_icons', '') . "/file-default.png"
);
foreach ($iconfiles as $icon) {
if (file_exists($icon)) {
return theme("image", $icon, $iconname." file", $iconname." file");
}
}
return '';
}
/**
* Convert windows path values to use slashes, prevent slashes used
* repeatedly, and try to catch and eliminate path walkback attemepts.
* Also prevent from accessing version control system folders.
*/
function filebrowser_safe_folder($subfolder) {
$folder = filebrowser_root() . ($subfolder?"/$subfolder":'');
while (TRUE) {
$safer = str_replace(array("\\", "../", "/.svn", "/CVS", ".."), array("/", "", "", "", ""), $folder);
if ($safer !== $folder) {
$folder = $safer;
}
else {
break;
}
}
$folder = preg_replace("!^/*([^/].+[^/])/*$!", "\\1", $folder);
// handle the case when superuser has set files folder to '.'
$folder = preg_replace("!^./!", "", $folder);
return preg_replace("!/+!", "/", $folder);
}
/**
* Prepend the filebrowser location to the path.
* Allows easy path generation even if $path has a leading slash. Setting the
* second parameter allows filebrowser to run from different contexts.
* @param $path
* @param $filebrowser_base
* Define where filebrowser is run from. Requires trailing slash.
*/
function filebrowser_proper_path($path, $filebrowser_base=NULL) {
static $base; if(!$base){$base='filebrowser/';}
if($filebrowser_base){$base=$filebrowser_base;}
return preg_replace("|//+|", "/", $base.'/'.$path);
}
/**
* Does the custom sorting of the table contents based on the
* sort values (sv) added previously.
* Note this sorts by named columns, not field numbers as tablesort usually
* does.
*/
function filebrowser_sort_table($a, $b = NULL) {
static $orderby = 0;
static $sort = '';
// Set sorting criteria
if (!isset($b)) {
$orderby = $a[0]['sql']; // NAME of field to sort with
$sort = $a[1]; // asc/desc
}
// Sort two values with the sorting criteria
elseif (is_array($a) && isset($a[$orderby]) && isset($a[$orderby]['sv'])) {
if ($a[$orderby]['sv'] == $b[$orderby]['sv']) {
return 0;
}
if ($sort == 'asc') {
return $a[$orderby]['sv'] > $b[$orderby]['sv'];
}
else {
return $a[$orderby]['sv'] < $b[$orderby]['sv'];
}
}
}
function filebrowser_set_breadcrumbs($parts){
$breadcrumb = array();
$dirname = t('root');
if ($parts) {
$breadcrumb[] = $dirname = array_pop($parts);
while (count($parts)) {
$breadcrumb[] = l($parts[count($parts) -1], filebrowser_proper_path(join("/", $parts)));
array_pop($parts);
}
$breadcrumb[] = l(t('Filebrowser root'), filebrowser_proper_path(''));
} else {
$breadcrumb[] = t('Filebrowser');
}
$breadcrumb[] = l(t('Home'), NULL);
drupal_set_breadcrumb(array_reverse($breadcrumb));
}
/**
* Returns the base of the allowed area for filebrowsing.
* May be independant of the files directory, and different for different users.
*/
function filebrowser_root(){
return variable_get('filebrowser_root', file_directory_path());
}
/**
* Chop the filebrowser root directory (not the url base) off the start of a
* path - translate from a filepath to a relative path.
*
* May operate by reference.
*/
function filebrowser_trim_root(&$path){
$fbr = filebrowser_root();
if(substr($path,0,strlen($fbr)) == $fbr){
$path = substr($path,strlen($fbr)+1); // trim slash also
}
return $path;
}
/**
* Separate the request from the function.
* Custom filebrowsers can be invoked from any path on the server, and their
* relative subfolders are tacked onto the end of the URL.
*
* [admin/custom/filebrowser/][my/special/files]
*
* Find where to split this request.
* @return the base filebrowser path for the current job. Contains trailing
* slash.
*/
function filebrowser_base($subfolder){
$filebrowser_base = $subfolder ? substr($_GET['q'],0,strpos($_GET['q'],$subfolder)) : $_GET['q'].'/';
return $filebrowser_base;
}
/**
* Tell the system to remember what cols to display when this lookup is called
* again by ajax. An internal memo, so the page which is called asynchronously
* knows what its parent expects to see.
*/
function filebrowser_set_context($context, $cols){
$filebrowser_contexts = variable_get('filebrowser_contexts',array());
$filebrowser_contexts[$context] = $cols;
variable_set('filebrowser_contexts',$filebrowser_contexts);
}