Index: install.php =================================================================== RCS file: /cvs/drupal/drupal/install.php,v retrieving revision 1.154 diff -u -9 -p -r1.154 install.php --- install.php 10 Feb 2009 05:43:06 -0000 1.154 +++ install.php 14 Feb 2009 22:15:33 -0000 @@ -398,19 +398,19 @@ function install_settings_form_submit($f // Continue to install profile step install_goto("install.php?profile=$profile" . ($install_locale ? "&locale=$install_locale" : '')); } /** * Find all .profile files. */ function install_find_profiles() { - return file_scan_directory('./profiles', '/\.profile$/', '/(\.\.?|CVS)$/', 0, TRUE, 'name', 0); + return file_scan_directory('./profiles', '/\.profile$/', array('key' => 'name')); } /** * Allow admin to select which profile to install. * * @return * The selected profile. */ function install_select_profile() { @@ -484,19 +484,19 @@ function install_select_profile_form(&$f '#value' => st('Save and continue'), ); return $form; } /** * Find all .po files for the current profile. */ function install_find_locales($profilename) { - $locales = file_scan_directory('./profiles/' . $profilename . '/translations', '/\.po$/', '/(\.\.?|CVS)$/', 0, FALSE); + $locales = file_scan_directory('./profiles/' . $profilename . '/translations', '/\.po$/', array('recurse' => FALSE)); array_unshift($locales, (object) array('name' => 'en')); return $locales; } /** * Allow admin to select which locale to use for the current profile. * * @return * The selected language. Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.867 diff -u -9 -p -r1.867 common.inc --- includes/common.inc 13 Feb 2009 04:43:00 -0000 1.867 +++ includes/common.inc 14 Feb 2009 22:15:33 -0000 @@ -2309,19 +2309,19 @@ function _drupal_load_stylesheet($matche $file = drupal_load_stylesheet($filename); // Alter all url() paths, but not external. return preg_replace('/url\(([\'"]?)(?![a-z]+:)([^\'")]+)[\'"]?\)?;/i', 'url(\1' . dirname($filename) . '/', $file); } /** * Delete all cached CSS files. */ function drupal_clear_css_cache() { - file_scan_directory(file_create_path('css'), '/.*/', '/(\.\.?|CVS)$/', 'file_unmanaged_delete', TRUE); + file_scan_directory(file_create_path('css'), '/.*/', array('callback' => 'file_unmanaged_delete')); } /** * Add a JavaScript file, setting or inline code to the page. * * The behavior of this function depends on the parameters it is called with. * Generally, it handles the addition of JavaScript to the page, either as * reference to an existing file or as inline code. The following actions can be * performed using this function: @@ -2771,19 +2771,19 @@ function drupal_build_js_cache($files, $ } return $jspath . '/' . $filename; } /** * Delete all cached JS files. */ function drupal_clear_js_cache() { - file_scan_directory(file_create_path('js'), '/.*/', '/(\.\.?|CVS)$/', 'file_unmanaged_delete', TRUE); + file_scan_directory(file_create_path('js'), '/.*/', array('callback' => 'file_unmanaged_delete')); variable_set('javascript_parsed', array()); } /** * Converts a PHP variable into its Javascript equivalent. * * We use HTML-safe strings, i.e. with <, > and & escaped. */ function drupal_to_js($var) { @@ -3140,19 +3140,19 @@ function drupal_system_listing($mask, $d $searchdir[] = "profiles/$profile/$directory"; } if (file_exists("$config/$directory")) { $searchdir[] = "$config/$directory"; } // Get current list of items foreach ($searchdir as $dir) { - $files = array_merge($files, file_scan_directory($dir, $mask, '/(\.\.?|CVS)$/', 0, TRUE, $key, $min_depth)); + $files = array_merge($files, file_scan_directory($dir, $mask, array('key' => $key, 'min_depth' => $min_depth))); } return $files; } /** * Hands off structured Drupal arrays to type-specific *_alter implementations. * * This dispatch function hands off structured Drupal arrays to type-specific Index: includes/file.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/file.inc,v retrieving revision 1.157 diff -u -9 -p -r1.157 file.inc --- includes/file.inc 13 Feb 2009 00:39:01 -0000 1.157 +++ includes/file.inc 14 Feb 2009 22:15:33 -0000 @@ -1376,63 +1376,77 @@ function file_download() { * * Directories and files beginning with a period are excluded; this * prevents hidden files and directories (such as SVN working directories) * from being scanned. * * @param $dir * The base directory for the scan, without trailing slash. * @param $mask * The preg_match() regular expression of the files to find. - * @param $nomask - * The preg_match() regular expression of the files to ignore. - * @param $callback - * The callback function to call for each match. - * @param $recurse - * When TRUE, the directory scan will recurse the entire tree - * starting at the provided directory. - * @param $key - * The key to be used for the returned array of files. Possible - * values are "filename", for the path starting with $dir, - * "basename", for the basename of the file, and "name" for the name - * of the file without an extension. - * @param $min_depth - * Minimum depth of directories to return files from. + * @param $options + * An associative array of additional options, with the following keys: + * - 'nomask' + * The preg_match() regular expression of the files to ignore. Defaults to + * '/(\.\.?|CVS)$/'. + * - 'callback' + * The callback function to call for each match. There is no default + * callback. + * - 'recurse' + * When TRUE, the directory scan will recurse the entire tree starting at + * the provided directory. Defaults to TRUE. + * - 'key' + * The key to be used for the returned array of files. Possible values are + * "filename", for the path starting with $dir, "basename", for the + * basename of the file, and "name" for the name of the file without an + * extension. Defaults to 'filename'. + * - 'min_depth' + * Minimum depth of directories to return files from. Defaults to 0. * @param $depth * Current depth of recursion. This parameter is only used internally and * should not be passed. * @return * An associative array (keyed on the provided key) of objects with - * "path", "basename", and "name" members corresponding to the + * "filename", "basename", and "name" members corresponding to the * matching files. */ -function file_scan_directory($dir, $mask, $nomask = '/(\.\.?|CVS)$/', $callback = 0, $recurse = TRUE, $key = 'filename', $min_depth = 0, $depth = 0) { - $key = (in_array($key, array('filename', 'basename', 'name')) ? $key : 'filename'); +function file_scan_directory($dir, $mask, $options = array(), $depth = 0) { + // Merge in defaults. + $options += array( + 'nomask' => '/(\.\.?|CVS)$/', + 'callback' => 0, + 'recurse' => TRUE, + 'key' => 'filename', + 'min_depth' => 0, + ); + + $options['key'] = (in_array($options['key'], array('filename', 'basename', 'name')) ? $options['key'] : 'filename'); $files = array(); if (is_dir($dir) && $handle = opendir($dir)) { while (FALSE !== ($file = readdir($handle))) { - if (!preg_match($nomask, $file) && $file[0] != '.') { - if (is_dir("$dir/$file") && $recurse) { + if (!preg_match($options['nomask'], $file) && $file[0] != '.') { + if (is_dir("$dir/$file") && $options['recurse']) { // Give priority to files in this folder by merging them in after any subdirectory files. - $files = array_merge(file_scan_directory("$dir/$file", $mask, $nomask, $callback, $recurse, $key, $min_depth, $depth + 1), $files); + $files = array_merge(file_scan_directory("$dir/$file", $mask, $options, $depth + 1), $files); } - elseif ($depth >= $min_depth && preg_match($mask, $file)) { + elseif ($depth >= $options['min_depth'] && preg_match($mask, $file)) { // Always use this match over anything already set in $files with the - // same $$key. + // same $$options['key']. $filename = "$dir/$file"; $basename = basename($file); $name = substr($basename, 0, strrpos($basename, '.')); - $files[$$key] = new stdClass(); - $files[$$key]->filename = $filename; - $files[$$key]->basename = $basename; - $files[$$key]->name = $name; - if ($callback) { - $callback($filename); + $files[${$options['key']}] = (object) array( + 'filename' => $filename, + 'basename' => $basename, + 'name' => $name, + ); + if ($options['callback']) { + $options['callback']($filename); } } } } closedir($handle); } return $files; Index: includes/install.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/install.inc,v retrieving revision 1.83 diff -u -9 -p -r1.83 install.inc --- includes/install.inc 3 Feb 2009 17:30:10 -0000 1.83 +++ includes/install.inc 14 Feb 2009 22:15:33 -0000 @@ -209,19 +209,19 @@ function drupal_detect_baseurl($file = ' function drupal_detect_database_types() { $databases = array(); // We define a driver as a directory in /includes/database that in turn // contains a database.inc file. That allows us to drop in additional drivers // without modifying the installer. // Because we have no registry yet, we need to also include the install.inc // file for the driver explicitly. - foreach (file_scan_directory(DRUPAL_ROOT . '/includes/database', '/^[a-z]*$/i', '/(\.\.?|CVS)$/', 0, FALSE) as $file) { + foreach (file_scan_directory(DRUPAL_ROOT . '/includes/database', '/^[a-z]*$/i', array('recurse' => FALSE)) as $file) { include_once "{$file->filename}/install.inc"; include_once "{$file->filename}/database.inc"; $drivers[$file->basename] = $file->filename; } foreach ($drivers as $driver => $file) { $class = 'DatabaseInstaller_' . $driver; $installer = new $class(); if ($installer->installable()) { Index: includes/locale.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/locale.inc,v retrieving revision 1.203 diff -u -9 -p -r1.203 locale.inc --- includes/locale.inc 13 Feb 2009 00:45:18 -0000 1.203 +++ includes/locale.inc 14 Feb 2009 22:15:33 -0000 @@ -2611,19 +2611,19 @@ function locale_batch_by_language($langc if (count($skip)) { $query->condition('name', $skip, 'NOT IN'); } $result = $query->execute(); foreach ($result as $component) { // Collect all files for all components, names as $langcode.po or // with names ending with $langcode.po. This allows for filenames // like node-module.de.po to let translators use small files and // be able to import in smaller chunks. - $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)' . $langcode . '\.po$/', '/(\.\.?|CVS)$/', 0, FALSE)); + $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)' . $langcode . '\.po$/', array('recurse' => FALSE))); $components[] = $component->name; } return _locale_batch_build($files, $finished, $components); } /** * Prepare a batch to run when installing modules or enabling themes. * This batch will import translations for the newly added components @@ -2643,19 +2643,19 @@ function locale_batch_by_component($comp $language_list = join('|', array_keys($languages[1])); // Collect all files to import for all $components. $result = db_query("SELECT name, filename FROM {system} WHERE status = 1"); while ($component = db_fetch_object($result)) { if (in_array($component->name, $components)) { // Collect all files for this component in all enabled languages, named // as $langcode.po or with names ending with $langcode.po. This allows // for filenames like node-module.de.po to let translators use small // files and be able to import in smaller chunks. - $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)(' . $language_list . ')\.po$/', '/(\.\.?|CVS)$/', 0, FALSE)); + $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)(' . $language_list . ')\.po$/', array('recurse' => FALSE))); } } return _locale_batch_build($files, $finished); } return FALSE; } /** * Build a locale batch from an array of files. Index: modules/simpletest/tests/file.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/file.test,v retrieving revision 1.23 diff -u -9 -p -r1.23 file.test --- modules/simpletest/tests/file.test 13 Feb 2009 00:39:01 -0000 1.23 +++ modules/simpletest/tests/file.test 14 Feb 2009 22:15:33 -0000 @@ -9,18 +9,44 @@ /** * Helper validator that returns the $errors parameter. */ function file_test_validator($file, $errors) { return $errors; } /** + * Helper function for testing file_scan_directory(). + * + * Each time the function is called the file is stored in a static variable. + * When the function is called with $reset parameter TRUE the cache is cleared + * and the results returned. + * + * @param $file + * File object + * @param $reset + * Boolean indicating that the stored files should be removed and returned. + * @return + * An array of all previous $file parameters since $reset was last called. + */ +function file_test_file_scan_callback($file, $reset = FALSE) { + static $files = array(); + + if ($reset) { + $ret = $files; + $files = array(); + return $ret; + } + + $files[] = $file; +} + +/** * Base class for file tests that adds some additional file specific * assertions and helper functions. */ class FileTestCase extends DrupalWebTestCase { /** * Check that two files have the same values for all fields other than the * timestamp. * * @param $before @@ -780,32 +806,131 @@ class FileDirectoryTest extends FileTest class FileScanDirectoryTest extends FileTestCase { function getInfo() { return array( 'name' => t('File scan directory'), 'description' => t('Tests the file_scan_directory() function.'), 'group' => t('File'), ); } + function setUp() { + parent::setUp(); + $this->path = $this->originalFileDirectory . '/simpletest'; + } + /** - * Check that the no-mask parameter is honored. + * Check the format of the returned values. */ - function testNoMask() { - $path = $this->originalFileDirectory . '/simpletest'; + function testReturn() { + // Grab a listing of all the JavaSscript files and check that they're + // passed to the callback. + $all_files = file_scan_directory($this->path, '/^javascript-/'); + ksort($all_files); + $this->assertEqual(2, count($all_files), t('Found two, expected javascript files.')); + + // Check the first file. + $file = reset($all_files); + $this->assertEqual(key($all_files), $file->filename, t('Correct array key was used for the first returned file.')); + $this->assertEqual($file->filename, $this->path . '/javascript-1.txt', t('First file name was set correctly.')); + $this->assertEqual($file->basename, 'javascript-1.txt', t('First basename was set correctly')); + $this->assertEqual($file->name, 'javascript-1', t('First name was set correctly.')); + + // Check the second file. + $file = next($all_files); + $this->assertEqual(key($all_files), $file->filename, t('Correct array key was used for the second returned file.')); + $this->assertEqual($file->filename, $this->path . '/javascript-2.script', t('Second file name was set correctly.')); + $this->assertEqual($file->basename, 'javascript-2.script', t('Second basename was set correctly')); + $this->assertEqual($file->name, 'javascript-2', t('Second name was set correctly.')); + } - // Grab a listing of all the JS files. - $all_files = file_scan_directory($path, '/javascript*/'); + /** + * Check that the callback function is called correctly. + */ + function testOptionCallback() { + // When nothing is matched nothing should be passed to the callback. + $all_files = file_scan_directory($this->path, '/^NONEXISTINGFILENAME/', array('callback' => 'file_test_file_scan_callback')); + $this->assertEqual(0, count($all_files), t('No files were found.')); + $results = file_test_file_scan_callback(NULL, TRUE); + $this->assertEqual(0, count($results), t('No files were passed to the callback.')); + + // Grab a listing of all the JavaSscript files and check that they're + // passed to the callback. + $all_files = file_scan_directory($this->path, '/^javascript-/', array('callback' => 'file_test_file_scan_callback')); + $this->assertEqual(2, count($all_files), t('Found two, expected javascript files.')); + $results = file_test_file_scan_callback(NULL, TRUE); + $this->assertEqual(2, count($results), t('Files were passed to the callback.')); + } + + /** + * Check that the no-mask parameter is honored. + */ + function testOptionNoMask() { + // Grab a listing of all the JavaSscript files. + $all_files = file_scan_directory($this->path, '/^javascript-/'); $this->assertEqual(2, count($all_files), t('Found two, expected javascript files.')); // Now use the nomast parameter to filter out the .script file. - $filtered_files = file_scan_directory($path, '/javascript*/', '/.script$/'); + $filtered_files = file_scan_directory($this->path, '/^javascript-/', array('nomask' => '/.script$/')); $this->assertEqual(1, count($filtered_files), t('Filtered correctly.')); } + + /** + * Check that key parameter sets the return value's key. + */ + function testOptionKey() { + // "filename", for the path starting with $dir. + $expected = array($this->path . '/javascript-1.txt', $this->path . '/javascript-2.script'); + $actual = array_keys(file_scan_directory($this->path, '/^javascript-/', array('key' => 'filename'))); + sort($actual); + $this->assertEqual($expected, $actual, t('Returned the correct values for the filename key.')); + + // "basename", for the basename of the file. + $expected = array('javascript-1.txt', 'javascript-2.script'); + $actual = array_keys(file_scan_directory($this->path, '/^javascript-/', array('key' => 'basename'))); + sort($actual); + $this->assertEqual($expected, $actual, t('Returned the correct values for the basename key.')); + + // "name" for the name of the file without an extension. + $expected = array('javascript-1', 'javascript-2'); + $actual = array_keys(file_scan_directory($this->path, '/^javascript-/', array('key' => 'name'))); + sort($actual); + $this->assertEqual($expected, $actual, t('Returned the correct values for the name key.')); + + // Invalid option that should default back to "filename". + $expected = array($this->path . '/javascript-1.txt', $this->path . '/javascript-2.script'); + $actual = array_keys(file_scan_directory($this->path, '/^javascript-/', array('key' => 'INVALID'))); + sort($actual); + $this->assertEqual($expected, $actual, t('An invalid key defaulted back to the default.')); + } + + /** + * Check that the recurse option decends into subdirectories. + */ + function testOptionRecurse() { + $files = file_scan_directory($this->originalFileDirectory, '/^javascript-/', array('recurse' => FALSE)); + $this->assertTrue(empty($files), t("Without recursion couldn't find javascript files.")); + + $files = file_scan_directory($this->originalFileDirectory, '/^javascript-/', array('recurse' => TRUE)); + $this->assertEqual(2, count($files), t('With recursion we found the expected javascript files.')); + } + + + /** + * Check that the min_depth options lets us ignore files in the starting + * directory. + */ + function testOptionMinDepth() { + $files = file_scan_directory($this->path, '/^javascript-/', array('min_depth' => 0)); + $this->assertEqual(2, count($files), t('No minimum-depth gets files in current directory.')); + + $files = file_scan_directory($this->path, '/^javascript-/', array('min_depth' => 1)); + $this->assertTrue(empty($files), t("Minimum-depth of 1 successfully excludes files from current directory.")); + } } /** * Deletion related tests. */ class FileUnmanagedDeleteTest extends FileTestCase { function getInfo() { return array(