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); }