Commit dd907896 authored by catch's avatar catch

Issue #3035312 by kim.pepper, andypost, martin107, yogeshmpawar,...

Issue #3035312 by kim.pepper, andypost, martin107, yogeshmpawar, naveenvalecha, pguillard, Berdir, dww, claudiu.cristea, jibran, Mile23: Move file_scan_directory() to file_system service
parent 5d68f9fc
......@@ -992,81 +992,23 @@ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EX
* @return
* An associative array (keyed on the chosen key) of objects with 'uri',
* 'filename', and 'name' properties corresponding to the matched files.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0.
* Use \Drupal\Core\File\FileSystemInterface::scanDirectory() instead.
*
* @see https://www.drupal.org/node/3038437
*/
function file_scan_directory($dir, $mask, $options = [], $depth = 0) {
// Merge in defaults.
$options += [
'callback' => 0,
'recurse' => TRUE,
'key' => 'uri',
'min_depth' => 0,
];
// Normalize $dir only once.
if ($depth == 0) {
/** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
$stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
$dir = $stream_wrapper_manager->normalizeUri($dir);
$dir_has_slash = (substr($dir, -1) === '/');
}
// Allow directories specified in settings.php to be ignored. You can use this
// to not check for files in common special-purpose directories. For example,
// node_modules and bower_components. Ignoring irrelevant directories is a
// performance boost.
if (!isset($options['nomask'])) {
$ignore_directories = Settings::get('file_scan_ignore_directories', []);
array_walk($ignore_directories, function (&$value) {
$value = preg_quote($value, '/');
});
$default_nomask = '/^' . implode('|', $ignore_directories) . '$/';
}
$options['key'] = in_array($options['key'], ['uri', 'filename', 'name']) ? $options['key'] : 'uri';
@trigger_error('file_scan_directory() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\File\FileSystemInterface::scanDirectory() instead. See https://www.drupal.org/node/3038437', E_USER_DEPRECATED);
$files = [];
// Avoid warnings when opendir does not have the permissions to open a
// directory.
if (is_dir($dir)) {
if ($handle = @opendir($dir)) {
while (FALSE !== ($filename = readdir($handle))) {
// Skip this file if it matches the nomask or starts with a dot.
if ($filename[0] != '.'
&& !(isset($options['nomask']) && preg_match($options['nomask'], $filename))
&& !(!empty($default_nomask) && preg_match($default_nomask, $filename))
) {
if ($depth == 0 && $dir_has_slash) {
$uri = "$dir$filename";
}
else {
$uri = "$dir/$filename";
}
if ($options['recurse'] && is_dir($uri)) {
// Give priority to files in this folder by merging them in after
// any subdirectory files.
$files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files);
}
elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
// Always use this match over anything already set in $files with
// the same $options['key'].
$file = new stdClass();
$file->uri = $uri;
$file->filename = $filename;
$file->name = pathinfo($filename, PATHINFO_FILENAME);
$key = $options['key'];
$files[$file->$key] = $file;
if ($options['callback']) {
$options['callback']($uri);
}
}
}
}
closedir($handle);
}
else {
\Drupal::logger('file')->error('@dir can not be opened', ['@dir' => $dir]);
try {
if (is_dir($dir)) {
$files = \Drupal::service('file_system')->scanDirectory($dir, $mask, $options);
}
}
catch (FileException $e) {
// Ignore and return empty array for BC.
}
return $files;
}
......
......@@ -444,7 +444,9 @@ function install_begin_request($class_loader, &$install_state) {
else {
$directory = $site_path . '/files/translations';
}
$container->set('string_translator.file_translation', new FileTranslation($directory));
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = $container->get('file_system');
$container->set('string_translator.file_translation', new FileTranslation($directory, $file_system));
$container->get('string_translation')
->addTranslator($container->get('string_translator.file_translation'));
......@@ -1324,9 +1326,9 @@ function _install_select_profile(&$install_state) {
*
* @return
* An associative array of file URIs keyed by language code. URIs as
* returned by file_scan_directory().
* returned by FileSystemInterface::scanDirectory().
*
* @see file_scan_directory()
* @see \Drupal\Core\File\FileSystemInterface::scanDirectory()
*/
function install_find_translations() {
$translations = [];
......
......@@ -5,15 +5,15 @@
* API functions for installing modules and themes.
*/
use Drupal\Core\Extension\Dependency;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\File\FileSystemInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\OpCodeCache;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Extension\Dependency;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Requirement severity -- Informational message only.
......@@ -166,9 +166,11 @@ function drupal_get_database_types() {
// The internal database driver name is any valid PHP identifier.
$mask = '/^' . DRUPAL_PHP_FUNCTION_PATTERN . '$/';
$files = file_scan_directory(DRUPAL_ROOT . '/core/lib/Drupal/Core/Database/Driver', $mask, ['recurse' => FALSE]);
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
$files = $file_system->scanDirectory(DRUPAL_ROOT . '/core/lib/Drupal/Core/Database/Driver', $mask, ['recurse' => FALSE]);
if (is_dir(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database')) {
$files += file_scan_directory(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database/', $mask, ['recurse' => FALSE]);
$files += $file_system->scanDirectory(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database/', $mask, ['recurse' => FALSE]);
}
foreach ($files as $file) {
if (file_exists($file->uri . '/Install/Tasks.php')) {
......
......@@ -216,7 +216,10 @@ function drupal_find_theme_templates($cache, $extension, $path) {
// Escape the periods in the extension.
$regex = '/' . str_replace('.', '\.', $extension) . '$/';
// Get a listing of all template files in the path to search.
$files = file_scan_directory($path, $regex, ['key' => 'filename']);
$files = [];
if (is_dir($path)) {
$files = \Drupal::service('file_system')->scanDirectory($path, $regex, ['key' => 'filename']);
}
// Find templates that implement registered theme hooks and include that in
// what is returned so that the registry knows that the theme has this
......
......@@ -196,7 +196,9 @@ public function deleteAll() {
$this->fileSystem->delete($uri);
}
};
file_scan_directory('public://css', '/.*/', ['callback' => $delete_stale]);
if (is_dir('public://css')) {
$this->fileSystem->scanDirectory('public://css', '/.*/', ['callback' => $delete_stale]);
}
}
}
......@@ -198,7 +198,9 @@ public function deleteAll() {
$this->fileSystem->delete($uri);
}
};
file_scan_directory('public://js', '/.*/', ['callback' => $delete_stale]);
if (is_dir('public://js')) {
$this->fileSystem->scanDirectory('public://js', '/.*/', ['callback' => $delete_stale]);
}
}
}
<?php
namespace Drupal\Core\File\Exception;
/**
* Exception thrown when a target is not a regular directory (e.g. a file).
*/
class NotRegularDirectoryException extends FileException {}
......@@ -8,6 +8,7 @@
use Drupal\Core\File\Exception\FileExistsException;
use Drupal\Core\File\Exception\FileNotExistsException;
use Drupal\Core\File\Exception\FileWriteException;
use Drupal\Core\File\Exception\NotRegularDirectoryException;
use Drupal\Core\File\Exception\NotRegularFileException;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
......@@ -625,4 +626,97 @@ public function createFilename($basename, $directory) {
return $destination;
}
/**
* {@inheritdoc}
*/
public function scanDirectory($dir, $mask, array $options = []) {
// Merge in defaults.
$options += [
'callback' => 0,
'recurse' => TRUE,
'key' => 'uri',
'min_depth' => 0,
];
$dir = $this->streamWrapperManager->normalizeUri($dir);
if (!is_dir($dir)) {
throw new NotRegularDirectoryException("$dir is not a directory.");
}
// Allow directories specified in settings.php to be ignored. You can use
// this to not check for files in common special-purpose directories. For
// example, node_modules and bower_components. Ignoring irrelevant
// directories is a performance boost.
if (!isset($options['nomask'])) {
$ignore_directories = $this->settings->get('file_scan_ignore_directories', []);
array_walk($ignore_directories, function (&$value) {
$value = preg_quote($value, '/');
});
$options['nomask'] = '/^' . implode('|', $ignore_directories) . '$/';
}
$options['key'] = in_array($options['key'], ['uri', 'filename', 'name']) ? $options['key'] : 'uri';
return $this->doScanDirectory($dir, $mask, $options);
}
/**
* Internal function to handle directory scanning with recursion.
*
* @param string $dir
* The base directory or URI to scan, without trailing slash.
* @param string $mask
* The preg_match() regular expression for files to be included.
* @param array $options
* The options as per ::scanDirectory().
* @param int $depth
* The current depth of recursion.
*
* @return array
* An associative array as per ::scanDirectory().
*
* @throws \Drupal\Core\File\Exception\NotRegularDirectoryException
* If the directory does not exist.
*
* @see \Drupal\Core\File\FileSystemInterface::scanDirectory()
*/
protected function doScanDirectory($dir, $mask, array $options = [], $depth = 0) {
$files = [];
// Avoid warnings when opendir does not have the permissions to open a
// directory.
if ($handle = @opendir($dir)) {
while (FALSE !== ($filename = readdir($handle))) {
// Skip this file if it matches the nomask or starts with a dot.
if ($filename[0] != '.' && !(preg_match($options['nomask'], $filename))) {
if (substr($dir, -1) == '/') {
$uri = "$dir$filename";
}
else {
$uri = "$dir/$filename";
}
if ($options['recurse'] && is_dir($uri)) {
// Give priority to files in this folder by merging them in after
// any subdirectory files.
$files = array_merge($this->doScanDirectory($uri, $mask, $options, $depth + 1), $files);
}
elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
// Always use this match over anything already set in $files with
// the same $options['key'].
$file = new \stdClass();
$file->uri = $uri;
$file->filename = $filename;
$file->name = pathinfo($filename, PATHINFO_FILENAME);
$key = $options['key'];
$files[$file->$key] = $file;
if ($options['callback']) {
$options['callback']($uri);
}
}
}
}
closedir($handle);
}
else {
$this->logger->error('@dir can not be opened', ['@dir' => $dir]);
}
return $files;
}
}
......@@ -472,4 +472,43 @@ public function createFilename($basename, $directory);
*/
public function getDestinationFilename($destination, $replace);
/**
* Finds all files that match a given mask in a given directory.
*
* Directories and files beginning with a dot are excluded; this prevents
* hidden files and directories (such as SVN working directories) from being
* scanned. Use the umask option to skip configuration directories to
* eliminate the possibility of accidentally exposing configuration
* information. Also, you can use the base directory, recurse, and min_depth
* options to improve performance by limiting how much of the filesystem has
* to be traversed.
*
* @param string $dir
* The base directory or URI to scan, without trailing slash.
* @param string $mask
* The preg_match() regular expression for files to be included.
* @param array $options
* An associative array of additional options, with the following elements:
* - 'nomask': The preg_match() regular expression for files to be excluded.
* Defaults to the 'file_scan_ignore_directories' setting.
* - '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 associative array of files.
* Possible values are 'uri', for the file's URI; 'filename', for the
* basename of the file; and 'name' for the name of the file without the
* extension. Defaults to 'uri'.
* - 'min_depth': Minimum depth of directories to return files from.
* Defaults to 0.
*
* @return array
* An associative array (keyed on the chosen key) of objects with 'uri',
* 'filename', and 'name' properties corresponding to the matched files.
*
* @throws \Drupal\Core\File\Exception\NotRegularDirectoryException
* If the directory does not exist.
*/
public function scanDirectory($dir, $mask, array $options = []);
}
......@@ -4,6 +4,7 @@
use Drupal\Component\Gettext\PoStreamReader;
use Drupal\Component\Gettext\PoMemoryWriter;
use Drupal\Core\File\FileSystemInterface;
/**
* File based string translation.
......@@ -22,15 +23,29 @@ class FileTranslation extends StaticTranslation {
*/
protected $directory;
/**
* The file system.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* Constructs a StaticTranslation object.
*
* @param string $directory
* The directory to retrieve file translations from.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
*/
public function __construct($directory) {
public function __construct($directory, FileSystemInterface $file_system = NULL) {
parent::__construct();
$this->directory = $directory;
if (!isset($file_system)) {
@trigger_error('Calling FileTranslation::__construct() without the $file_system argument is deprecated in drupal:8.8.0. The $file_system argument will be required in drupal:9.0.0. See https://www.drupal.org/node/3038437', E_USER_DEPRECATED);
$file_system = \Drupal::service('file_system');
}
$this->fileSystem = $file_system;
}
/**
......@@ -65,12 +80,15 @@ protected function getLanguage($langcode) {
*
* @return array
* An associative array of file information objects keyed by file URIs as
* returned by file_scan_directory().
* returned by FileSystemInterface::scanDirectory().
*
* @see file_scan_directory()
* @see \Drupal\Core\File\FileSystemInterface::scanDirectory()
*/
public function findTranslationFiles($langcode = NULL) {
$files = file_scan_directory($this->directory, $this->getTranslationFilesPattern($langcode), ['recurse' => FALSE]);
$files = [];
if (is_dir($this->directory)) {
$files = $this->fileSystem->scanDirectory($this->directory, $this->getTranslationFilesPattern($langcode), ['recurse' => FALSE]);
}
return $files;
}
......
......@@ -106,12 +106,17 @@ public static function getUpdaterFromDirectory($directory) {
* Path to the info file.
*/
public static function findInfoFile($directory) {
$info_files = file_scan_directory($directory, '/.*\.info.yml$/');
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
$info_files = [];
if (is_dir($directory)) {
$info_files = $file_system->scanDirectory($directory, '/.*\.info.yml$/');
}
if (!$info_files) {
return FALSE;
}
foreach ($info_files as $info_file) {
if (mb_substr($info_file->filename, 0, -9) == \Drupal::service('file_system')->basename($directory)) {
if (mb_substr($info_file->filename, 0, -9) == $file_system->basename($directory)) {
// Info file Has the same name as the directory, return it.
return $info_file->uri;
}
......
......@@ -314,7 +314,7 @@ function file_test_validator(File $file, $errors) {
}
/**
* Helper function for testing file_scan_directory().
* Helper function for testing FileSystemInterface::scanDirectory().
*
* Each time the function is called the file is stored in a static variable.
* When the function is called with no $filepath parameter, the results are
......
......@@ -44,7 +44,11 @@ public function createSampleImage(ImageStyleInterface $style) {
* Count the number of images currently create for a style.
*/
public function getImageCount(ImageStyleInterface $style) {
return count(file_scan_directory('public://styles/' . $style->id(), '/.*/'));
$count = 0;
if (is_dir('public://styles/' . $style->id())) {
$count = count(\Drupal::service('file_system')->scanDirectory('public://styles/' . $style->id(), '/.*/'));
}
return $count;
}
/**
......
......@@ -40,7 +40,11 @@ public function testValid() {
$this->drupalPostForm(NULL, [], t('Save'));
// Get invalid image test files from simpletest.
$files = file_scan_directory(drupal_get_path('module', 'simpletest') . '/files', '/invalid-img-.*/');
$dir = drupal_get_path('module', 'simpletest') . '/files';
$files = [];
if (is_dir($dir)) {
$files = $file_system->scanDirectory($dir, '/invalid-img-.*/');
}
$invalid_image_files = [];
foreach ($files as $file) {
$invalid_image_files[$file->filename] = $file;
......
......@@ -42,7 +42,11 @@ public function createSampleImage($style, $wrapper) {
* Count the number of images currently created for a style in a wrapper.
*/
public function getImageCount($style, $wrapper) {
return count(file_scan_directory($wrapper . '://styles/' . $style->id(), '/.*/'));
$count = 0;
if (is_dir($wrapper . '://styles/' . $style->id())) {
$count = count(\Drupal::service('file_system')->scanDirectory($wrapper . '://styles/' . $style->id(), '/.*/'));
}
return $count;
}
/**
......
......@@ -97,7 +97,10 @@ function locale_translate_get_interface_translation_files(array $projects = [],
// {project}-{version}.{langcode}.po.
// Only files of known projects and languages will be returned.
$directory = \Drupal::config('locale.settings')->get('translation.path');
$result = file_scan_directory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', ['recurse' => FALSE]);
$result = [];
if (is_dir($directory)) {
$result = \Drupal::service('file_system')->scanDirectory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', ['recurse' => FALSE]);
}
foreach ($result as $file) {
// Update the file object with project name and version from the file name.
......
......@@ -32,10 +32,12 @@ function locale_uninstall() {
if (is_dir($locale_js_directory)) {
$locale_javascripts = \Drupal::state()->get('locale.translation.javascript') ?: [];
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
foreach ($locale_javascripts as $langcode => $file_suffix) {
if (!empty($file_suffix)) {
try {
\Drupal::service('file_system')->delete($locale_js_directory . '/' . $langcode . '_' . $file_suffix . '.js');
$file_system->delete($locale_js_directory . '/' . $langcode . '_' . $file_suffix . '.js');
}
catch (FileException $e) {
// Ignore and continue.
......@@ -43,8 +45,10 @@ function locale_uninstall() {
}
}
// Delete the JavaScript translations directory if empty.
if (!file_scan_directory($locale_js_directory, '/.*/')) {
\Drupal::service('file_system')->rmdir($locale_js_directory);
if (is_dir($locale_js_directory)) {
if (!$file_system->scanDirectory($locale_js_directory, '/.*/')) {
$file_system->rmdir($locale_js_directory);
}
}
}
......
......@@ -175,11 +175,13 @@ function locale_translation_source_check_file($source) {
$directory = $source_file->directory;
$filename = '/' . preg_quote($source_file->filename) . '$/';
if ($files = file_scan_directory($directory, $filename, ['key' => 'name', 'recurse' => FALSE])) {
$file = current($files);
$source_file->uri = $file->uri;
$source_file->timestamp = filemtime($file->uri);
return $source_file;
if (is_dir($directory)) {
if ($files = \Drupal::service('file_system')->scanDirectory($directory, $filename, ['key' => 'name', 'recurse' => FALSE])) {
$file = current($files);
$source_file->uri = $file->uri;
$source_file->timestamp = filemtime($file->uri);
return $source_file;
}
}
}
return FALSE;
......
......@@ -24,7 +24,7 @@ function media_install() {
$file_system = \Drupal::service('file_system');
$file_system->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
$files = file_scan_directory($source, '/.*\.(svg|png|jpg|jpeg|gif)$/');
$files = $file_system->scanDirectory($source, '/.*\.(svg|png|jpg|jpeg|gif)$/');
foreach ($files as $file) {
// When reinstalling the media module we don't want to copy the icons when
// they already exist. The icons could be replaced (by a contrib module or
......
<?php
namespace Drupal\Tests\system\Unit\Installer;
namespace Drupal\Tests\system\Kernel\Installer;
use Drupal\Core\StringTranslation\Translator\FileTranslation;
use Drupal\Tests\UnitTestCase;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests for installer language support.
*
* @group Installer
*/
class InstallTranslationFilePatternTest extends UnitTestCase {
class InstallTranslationFilePatternTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* @var \Drupal\Core\StringTranslation\Translator\FileTranslation
......@@ -27,7 +32,7 @@ class InstallTranslationFilePatternTest extends UnitTestCase {
*/
protected function setup() {
parent::setUp();
$this->fileTranslation = new FileTranslation('filename');
$this->fileTranslation = new FileTranslation('filename', $this->container->get('file_system'));
$method = new \ReflectionMethod('\Drupal\Core\StringTranslation\Translator\FileTranslation', 'getTranslationFilesPattern');
$method->setAccessible(TRUE);
$this->filePatternMethod = $method;
......
......@@ -690,7 +690,9 @@ function update_verify_update_archive($project, $archive_file, $directory) {
// functionality).
$compatible_project = FALSE;
$incompatible = [];
$files = file_scan_directory("$directory/$project", '/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', ['key' => 'name', 'min_depth' => 0]);
/** @var \Drupal\Core\File\FileSystemInterface $file_syste