'textfield', '#title' => check_plain($type->title_label), '#default_value' => !empty($node->title) ? $node->title : '', '#required' => TRUE, '#weight' => -6 ); $form['file_path'] = array( '#type' => 'textfield', '#title' => t('The system file path to the directory'), '#description' => t('This can be an absolute path or should be relative to the Drupal root directory.'), '#default_value' => isset($node->file_path) ? check_plain($node->file_path) : '', '#required' => TRUE, '#weight' => -5 ); $form['explore_subdirs'] = array( '#type' => 'checkbox', '#title' => t('Allow subdirectory listings.'), '#default_value' => isset($node->explore_subdirs) ? $node->explore_subdirs : '', '#weight' => -4 ); $form['file_blacklist'] = array( '#type' => 'textfield', '#title' => t('A blacklist of all files that shouldn\'t be accessible by users'), '#description' => t('List files separated by commas. Use .* to signify all files starting with a period.'), '#default_value' => isset($node->file_blacklist) ? $node->file_blacklist : '.*, descript.ion, file.bbs, CVS', '#weight' => -3 ); $form['private_downloads'] = array( '#type' => 'checkbox', '#title' => t("Enable private downloads"), '#description' => t("Some files won't download unless this private downloads are enabled (PHP files for instance)."), '#default_value' => isset($node->private_downloads) ? $node->private_downloads : '', '#weight' => -3 ); return $form; } function filebrowser_node_info() { return array( 'dir_listing' => array( 'name' => t('Directory listing'), 'module' => 'filebrowser', 'description' => t("A listing of files similar to how Apache lists files in a directory."), 'has_body' => FALSE ) ); } function filebrowser_load($node) { $additions = db_fetch_object(db_query('SELECT file_path, explore_subdirs, file_blacklist, private_downloads FROM {filebrowser} WHERE nid = %d', $node->nid)); return $additions; } function filebrowser_insert($node) { db_query("INSERT INTO {filebrowser} (nid, file_path, explore_subdirs, file_blacklist, private_downloads) VALUES (%d, '%s', %d, '%s', %d)", $node->nid, $node->file_path, $node->explore_subdirs, $node->file_blacklist, $node->private_downloads); } function filebrowser_update($node) { db_query("UPDATE {filebrowser} SET file_path = '%s', explore_subdirs = %d, file_blacklist = '%s', private_downloads = %d WHERE nid = %d", $node->file_path, $node->explore_subdirs, $node->file_blacklist, $node->private_downloads, $node->nid); } function filebrowser_delete($node) { // Notice that we're matching all revisions, by using the node's nid. db_query('DELETE FROM {filebrowser} WHERE nid = %d', $node->nid); } /** * Validate the node form submission. Just check if the file path specified is * accessible and readable. * * @param Drupal node object $node */ function filebrowser_validate(&$node) { // Verify the file system location & check that it's a directory. if (!is_dir($node->file_path)) { form_set_error('file_path', t('You must specify a valid directory.')); } // Check that it's readable. if (!is_readable($node->file_path)) { form_set_error('file_path', t('The directory %dir is not readable.', array('%dir' => $node->file_path))); } } function filebrowser_access($op, $node, $account) { if ($op == 'view') { return user_access('view directory listings', $account); } else if ($op == 'create') { return user_access('create directory listings', $account); } else if ($op == 'update') { if (user_access('edit any directory listings', $account) || (user_access('edit own directory listings', $account) && ($account->uid == $node->uid))) { return TRUE; } } else if ($op == 'delete') { if (user_access('delete any directory listings', $account) || (user_access('delete own directory listings', $account) && ($account->uid == $node->uid))) { return TRUE; } } return FALSE; } function filebrowser_perm() { return array( 'create directory listings', 'delete own directory listings', 'delete any directory listings', 'edit own directory listings', 'edit any directory listings', 'view directory listings' ); } /** * Implementation of hook_db_rewrite_sql(). * * Prevents node listings from listing directory nodes if the user doesn't have * permission. */ function filebrowser_db_rewrite_sql($query, $primary_table, $primary_field, $args) { global $user; if ($primary_table == 'n' && $primary_field == 'nid' && !user_access('view directory listings', $user)) { $return = array( 'where' => 'n.type != "dir_listing"' ); return $return; } } function _filebrowser_dir_stats($dir) { $file_count = 0; $total_size = 0; if (is_dir($dir) && $dh = opendir($dir)) { while (($file = readdir($dh)) !== false && is_readable($dir . '/' . $file)) { // Exclude fake directories. if ($file == '.' || $file == '..') { continue; } $full_path = $dir . '/' . $file; $f_size = filesize($full_path); $total_size += $f_size; ++$file_count; } closedir($dh); } return array('file_count' => $file_count, 'total_size' => $total_size); } /** * Implementation of hook_theme. * * @return An array of theming functions. */ function filebrowser_theme() { return array( 'filebrowser_dir_listing' => array( 'arguments' => array('files' => array(), 'folders' => array(), 'curr_dir' => '', 'total_size' => 0) ), 'filebrowser_page_title' => array( 'arguments' => array('node' => NULL) ) ); } /** * Implementation of hook_init. * * Needed to override standard node output. Checks if path refers to a file * in a dir_listing and outputs it if it is. * */ function filebrowser_init() { $path_parts = explode('/', $_GET['q']); // count() checks that $path_parts[1] will succeed. if (count($path_parts) > 2 && $path_parts[0] == 'node' && is_numeric($path_parts[1])) { $node = node_load($path_parts[1]); if ($node->type == 'dir_listing' && $node->private_downloads) { // We just check the rest of the path to verify that it points to a file. array_shift($path_parts); array_shift($path_parts); $file = $node->file_path . '/' . implode('/', $path_parts); if (is_file($file)) { if (is_readable($file)) { // Transfer file in 1024 byte chunks to save memory usage. if ($fd = @fopen($file, 'rb')) { // Set the proper MIME type of the file if possible. // Try two different ways, first using Fileinfo which is the proper way // to do it. If that fails, try the deprecated mime_content_type(). if (function_exists('finfo_open')) { $handle = @finfo_open(FILEINFO_MIME); $mime_type = @finfo_file($handle, $file); if ($mime_type === FALSE && function_exists('mime_content_type')) { $mime_type = @mime_content_type($file); } } if ($mime_type === FALSE) { $mime_type = 'application/x-download'; } // Headers partially from: http://www.php.net/manual/en/function.readfile.php#57972 header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // some day in the past header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); header("Content-type: $mime_type"); header("Content-Disposition: attachment;"); header("Content-Transfer-Encoding: binary"); while (!feof($fd)) { echo fread($fd, 1024); } fclose($fd); exit; } else { drupal_set_message('File could not be opened.', 'error'); } } else { drupal_set_message('Could not access file.', 'error'); } } } } } function filebrowser_view($node, $teaser = FALSE, $page = FALSE) { global $user, $base_url; $node = node_prepare($node, $teaser); // Grab various Drupal paths. $path_alias = drupal_get_path_alias('node/' . $node->nid); // Handle both aliased and non-aliased cases. $subdir = str_replace($path_alias, '', $_GET['q']); $subdir = str_replace('node/' . $node->nid, '', $_GET['q']); $curr_dir = $node->file_path . $subdir; // Keep track of the current location and update breadcrumbs to reflect that. if ($page) { $breadcrumbs = array( l(t('Home'), NULL), l($node->file_path, $path_alias) ); foreach (explode('/', $subdir) as $child_dir) { if (!empty($child_dir)) { $path_alias .= '/' . $child_dir; $breadcrumbs[] = l($child_dir, $path_alias); } } drupal_set_breadcrumb($breadcrumbs); drupal_set_title(theme('filebrowser_page_title', $node)); } // Are we in a subdirectory? $is_subdir = !empty($subdir); // If we shouldn't be in a subdirectory, redirect to root_dir. $has_access = TRUE; if ($is_subdir && !$node->explore_subdirs){ $has_access = FALSE; } // More advanced check to make sure no parent directories match our files. // blacklist if (!empty($subdir) && $has_access) { $dirs = explode('/', $subdir); foreach ($dirs as $dir) { if (!empty($dir)) { if (strpos($node->file_blacklist, $dir) !== FALSE || ($dir[0] == '.' && strpos($node->file_blacklist, '.*') !== FALSE)) { $has_access = FALSE; break; } } } } if (!$has_access) { drupal_set_message(t('You\'re not allowed to view %dir.', array('%dir' => $curr_dir)), 'error'); drupal_goto(drupal_get_path_alias('node/' . $node->nid)); } // Store the descript.ion or file.bbs file for grabbing meta data from . $file_metadata = array(); $files = array(); $folders = array(); $total_size = 0; if (is_dir($curr_dir) && $dh = opendir($curr_dir)) { while (($file = readdir($dh)) !== false && is_readable($curr_dir . '/' . $file)) { $full_path = $curr_dir . '/' . $file; // Check for meta files if we need info. if (empty($file_metadata) && in_array($file, array('descript.ion', 'file.bbs'))) { $file_metadata = filebrowser_get_fileinfo($full_path, $curr_dir); } if (is_file($full_path)) { // Handle files that should not be shown. if (strpos($node->file_blacklist, $file) !== FALSE || ($file[0] == '.' && strpos($node->file_blacklist, '.*') !== FALSE)) { continue; } $files[$file] = array( 'name' => $file, 'path' => $full_path, 'url' => $node->private_downloads == '1'?url($_GET['q'] . '/' . $file):$base_url . '/' . $full_path, 'status' => MARK_READ, 'size' => 0 ); if (($f_stats = stat($full_path)) !== false) { $total_size += $f_stats['size']; // Update file size if we found it. $files[$file]['size'] = $f_stats['size']; // Mark this file new or updated. if ($user->uid) { if ($user->access < $f_stats['ctime']) { $files[$file]['status'] = MARK_NEW; } else if ($user->access < $f_stats['mtime']) { $files[$file]['status'] = MARK_UPDATED; } } } } // Keep a separate list of directories to append to the beginning of the array. else if ($node->explore_subdirs && is_dir($full_path)) { // Always remove '.' and only allow '..' in subdirectories. if ($file == '.' || (!$is_subdir && $file == '..')) { continue; } // Handle files that should not be shown. Make sure that '..' can be // shown even if '.*' is on the blacklist. else if (strpos($node->file_blacklist, $file) !== FALSE || ($file[0] == '.' && $file[1] != '.' && strpos($node->file_blacklist, '.*') !== FALSE)) { continue; } $folders[$file] = array( 'name' => $file, 'path' => $full_path, 'url' => $_GET['q'] . '/' . $file, 'status' => MARK_READ, 'size' => 0 ); // Try to mark a folder as new/updated. if ($user->uid && ($f_stats = stat($full_path)) !== false) { if ($user->access < $f_stats['ctime']) { $folders[$file]['status'] = MARK_NEW; } else if ($user->access < $f_stats['mtime']) { $folders[$file]['status'] = MARK_UPDATED; } } } } closedir($dh); } // Add in metadata from description files. if (!empty($file_metadata)) { // For files foreach ($files as $filename => $row) { if (isset($file_metadata[$filename])) { $files[$filename]['description'] = $file_metadata[$filename][0]; } } // For directories. if ($node->explore_subdirs) { foreach ($folders as $dir_name => $data) { if (isset($file_metadata[$dir_name])) { $folders[$dir_name]['description'] = $file_metadata[$dir_name][0]; } } } } // Display metadata for the current directory at the top of the page. //if (isset($file_metadata['.'])) { // $node->content['filebrowser-help'] = array( // '#value' => '

' . $file_metadata['.'][0] . '

', // '#weight' => 0 // ); //} $node->content['filebrowser'] = array( '#value' => theme('filebrowser_dir_listing', $files, $folders, $curr_dir, $total_size), '#weight' => 1, ); return $node; } /** * 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($info_file_path = NULL, $subfolder = '') { static $metacols = array(); // Return (previously generated) meta column list. if (!isset($info_file_path)) { return $metacols; } // Build meta information list. $metainfo = array(); if (is_readable($info_file_path) && ($file = file($info_file_path))) { foreach ($file as $line) { // Skip empty and commented lines. if (trim($line) == '' || strpos(trim($line), '#') === 0) { continue; } // Use PCRE regular expressions to parse file. $matches = array(); preg_match('/(\S+)\s*:\s*(.*)/', $line, $matches); list(, $name, $description) = $matches; unset($matches); 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*']); } foreach ($metainfo as $name => $description) { $metainfo[$name] = ($callback ? $callback($description, $subfolder, $name) : array($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_fileicon($fullpath = NULL, $iconname = NULL) { // Determine the type of file found. if (isset($fullpath)) { $iconname = (is_dir($fullpath) ? 'folder' : preg_replace("!^.+\\.([^\\.]+)$!", "\\1", $fullpath)); } else if (!isset($iconname)) { $iconname = 'default'; } // Check to make sure the icon we want exists. $icon_path = drupal_get_path('module', 'filebrowser') . "/icons/file-$iconname.png"; if (!file_exists($icon_path)) { $iconname = 'default'; } return theme('image', $icon_path); } /** * Theme a directory listing of files in a system directory. * * @param array $files An array of data of files in this directory. * @param array $folders An array of data of folders in this directory. * @param string $curr_dir The current directory path we're in. * @param integer $total_size The total size in bytes of data in this folder. * @return A string containing real page HTML. */ function theme_filebrowser_dir_listing(&$files, &$folders, $curr_dir, $total_size = 0) { $output = ''; if (!empty($files) || !empty($folders)) { $header = array('', array('data' => t('Name'), 'field' => 'name', 'sort' => 'asc'), array('data' => t('Size'), 'field' => 'size'), array('data' => t('Description'), 'field' => 'description')); // Deal with files. $table_rows_unsorted = array(); foreach ($files as $filename => $data) { $table_rows_unsorted[$filename] = array( array('data' => filebrowser_get_fileicon($data['path']), 'style' => 'width:1%;'), '' . $filename . '' . theme('mark', $data['status']), format_size($data['size']), $data['description'] ); } // Deal with folders. foreach ($folders as $foldername => $data) { $table_rows_unsorted[$foldername] = array( array('data' => filebrowser_get_fileicon($data['path']), 'style' => 'width:1%;'), l($foldername, $data['url']) . theme('mark', $data['status']), ' ', $data['description'] ); } // Handle any and all sorting. $ts = tablesort_init($header); // Sort things by name or size in ascending order. // Grab some keys to sort. $files_order = array_values($files); $folders_order = array_values($folders); // Sort files according to correct column. usort($folders_order, "filebrowser_{$ts['sql']}_sort"); usort($files_order, "filebrowser_{$ts['sql']}_sort"); $final_order = array_merge($folders_order, $files_order); // Combine folders and files array and setup correct order. $rows = array(); foreach ($final_order as $index => $file_info) { $rows[] = $table_rows_unsorted[$file_info['name']]; } if ($ts['sort'] == 'desc') { $rows = array_reverse($rows, TRUE); } // Concatenate them all together. $output .= '

' . t('Displaying %dir.', array('%dir' => $curr_dir)) . '

'; $output .= theme('table', $header, $rows, array('id' => 'filebrowser-file-listing')); $output .= '

' . t('Contains @fc totaling @ds in size.', array('@fc' => format_plural(count($files), '1 file', '@count files'), '@ds' => format_size($total_size))) . '

'; } else { $output .= '

' . t('This directory is empty.') . '

'; } return $output; } function theme_filebrowser_page_title($node) { return !empty($node->title)?$node->title:''; } function filebrowser_size_sort($a, $b) { if ($a['size'] == $b['size']) { return 0; } return ($a['size'] > $b['size']) ? -1 : 1; } function filebrowser_name_sort($a, $b) { return strcmp($a['name'], $b['name']); }