PageRenderTime 55ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/core/lib/Drupal/Core/File/FileSystem.php

http://github.com/drupal/drupal
PHP | 759 lines | 492 code | 61 blank | 206 comment | 89 complexity | b98426a4dd3e30a985fcb8726f9b8bc8 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\Core\File;
  3. use Drupal\Component\FileSystem\FileSystem as FileSystemComponent;
  4. use Drupal\Component\Utility\Unicode;
  5. use Drupal\Core\File\Exception\DirectoryNotReadyException;
  6. use Drupal\Core\File\Exception\FileException;
  7. use Drupal\Core\File\Exception\FileExistsException;
  8. use Drupal\Core\File\Exception\FileNotExistsException;
  9. use Drupal\Core\File\Exception\FileWriteException;
  10. use Drupal\Core\File\Exception\NotRegularDirectoryException;
  11. use Drupal\Core\File\Exception\NotRegularFileException;
  12. use Drupal\Core\Site\Settings;
  13. use Drupal\Core\StreamWrapper\PublicStream;
  14. use Drupal\Core\StreamWrapper\StreamWrapperManager;
  15. use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
  16. use Psr\Log\LoggerInterface;
  17. /**
  18. * Provides helpers to operate on files and stream wrappers.
  19. */
  20. class FileSystem implements FileSystemInterface {
  21. /**
  22. * Default mode for new directories. See self::chmod().
  23. */
  24. const CHMOD_DIRECTORY = 0775;
  25. /**
  26. * Default mode for new files. See self::chmod().
  27. */
  28. const CHMOD_FILE = 0664;
  29. /**
  30. * The site settings.
  31. *
  32. * @var \Drupal\Core\Site\Settings
  33. */
  34. protected $settings;
  35. /**
  36. * The file logger channel.
  37. *
  38. * @var \Psr\Log\LoggerInterface
  39. */
  40. protected $logger;
  41. /**
  42. * The stream wrapper manager.
  43. *
  44. * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
  45. */
  46. protected $streamWrapperManager;
  47. /**
  48. * Constructs a new FileSystem.
  49. *
  50. * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
  51. * The stream wrapper manager.
  52. * @param \Drupal\Core\Site\Settings $settings
  53. * The site settings.
  54. * @param \Psr\Log\LoggerInterface $logger
  55. * The file logger channel.
  56. */
  57. public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager, Settings $settings, LoggerInterface $logger) {
  58. $this->streamWrapperManager = $stream_wrapper_manager;
  59. $this->settings = $settings;
  60. $this->logger = $logger;
  61. }
  62. /**
  63. * {@inheritdoc}
  64. */
  65. public function moveUploadedFile($filename, $uri) {
  66. $result = @move_uploaded_file($filename, $uri);
  67. // PHP's move_uploaded_file() does not properly support streams if
  68. // open_basedir is enabled so if the move failed, try finding a real path
  69. // and retry the move operation.
  70. if (!$result) {
  71. if ($realpath = $this->realpath($uri)) {
  72. $result = move_uploaded_file($filename, $realpath);
  73. }
  74. else {
  75. $result = move_uploaded_file($filename, $uri);
  76. }
  77. }
  78. return $result;
  79. }
  80. /**
  81. * {@inheritdoc}
  82. */
  83. public function chmod($uri, $mode = NULL) {
  84. if (!isset($mode)) {
  85. if (is_dir($uri)) {
  86. $mode = $this->settings->get('file_chmod_directory', static::CHMOD_DIRECTORY);
  87. }
  88. else {
  89. $mode = $this->settings->get('file_chmod_file', static::CHMOD_FILE);
  90. }
  91. }
  92. if (@chmod($uri, $mode)) {
  93. return TRUE;
  94. }
  95. $this->logger->error('The file permissions could not be set on %uri.', ['%uri' => $uri]);
  96. return FALSE;
  97. }
  98. /**
  99. * {@inheritdoc}
  100. */
  101. public function unlink($uri, $context = NULL) {
  102. if (!$this->streamWrapperManager->isValidUri($uri) && (substr(PHP_OS, 0, 3) == 'WIN')) {
  103. chmod($uri, 0600);
  104. }
  105. if ($context) {
  106. return unlink($uri, $context);
  107. }
  108. else {
  109. return unlink($uri);
  110. }
  111. }
  112. /**
  113. * {@inheritdoc}
  114. */
  115. public function realpath($uri) {
  116. // If this URI is a stream, pass it off to the appropriate stream wrapper.
  117. // Otherwise, attempt PHP's realpath. This allows use of this method even
  118. // for unmanaged files outside of the stream wrapper interface.
  119. if ($wrapper = $this->streamWrapperManager->getViaUri($uri)) {
  120. return $wrapper->realpath();
  121. }
  122. return realpath($uri);
  123. }
  124. /**
  125. * {@inheritdoc}
  126. */
  127. public function dirname($uri) {
  128. $scheme = StreamWrapperManager::getScheme($uri);
  129. if ($this->streamWrapperManager->isValidScheme($scheme)) {
  130. return $this->streamWrapperManager->getViaScheme($scheme)->dirname($uri);
  131. }
  132. else {
  133. return dirname($uri);
  134. }
  135. }
  136. /**
  137. * {@inheritdoc}
  138. */
  139. public function basename($uri, $suffix = NULL) {
  140. $separators = '/';
  141. if (DIRECTORY_SEPARATOR != '/') {
  142. // For Windows OS add special separator.
  143. $separators .= DIRECTORY_SEPARATOR;
  144. }
  145. // Remove right-most slashes when $uri points to directory.
  146. $uri = rtrim($uri, $separators);
  147. // Returns the trailing part of the $uri starting after one of the directory
  148. // separators.
  149. $filename = preg_match('@[^' . preg_quote($separators, '@') . ']+$@', $uri, $matches) ? $matches[0] : '';
  150. // Cuts off a suffix from the filename.
  151. if ($suffix) {
  152. $filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename);
  153. }
  154. return $filename;
  155. }
  156. /**
  157. * {@inheritdoc}
  158. */
  159. public function mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
  160. if (!isset($mode)) {
  161. $mode = $this->settings->get('file_chmod_directory', static::CHMOD_DIRECTORY);
  162. }
  163. // If the URI has a scheme, don't override the umask - schemes can handle
  164. // this issue in their own implementation.
  165. if (StreamWrapperManager::getScheme($uri)) {
  166. return $this->mkdirCall($uri, $mode, $recursive, $context);
  167. }
  168. // If recursive, create each missing component of the parent directory
  169. // individually and set the mode explicitly to override the umask.
  170. if ($recursive) {
  171. // Ensure the path is using DIRECTORY_SEPARATOR, and trim off any trailing
  172. // slashes because they can throw off the loop when creating the parent
  173. // directories.
  174. $uri = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $uri), DIRECTORY_SEPARATOR);
  175. // Determine the components of the path.
  176. $components = explode(DIRECTORY_SEPARATOR, $uri);
  177. // If the filepath is absolute the first component will be empty as there
  178. // will be nothing before the first slash.
  179. if ($components[0] == '') {
  180. $recursive_path = DIRECTORY_SEPARATOR;
  181. // Get rid of the empty first component.
  182. array_shift($components);
  183. }
  184. else {
  185. $recursive_path = '';
  186. }
  187. // Don't handle the top-level directory in this loop.
  188. array_pop($components);
  189. // Create each component if necessary.
  190. foreach ($components as $component) {
  191. $recursive_path .= $component;
  192. if (!file_exists($recursive_path)) {
  193. if (!$this->mkdirCall($recursive_path, $mode, FALSE, $context)) {
  194. return FALSE;
  195. }
  196. // Not necessary to use self::chmod() as there is no scheme.
  197. if (!chmod($recursive_path, $mode)) {
  198. return FALSE;
  199. }
  200. }
  201. $recursive_path .= DIRECTORY_SEPARATOR;
  202. }
  203. }
  204. // Do not check if the top-level directory already exists, as this condition
  205. // must cause this function to fail.
  206. if (!$this->mkdirCall($uri, $mode, FALSE, $context)) {
  207. return FALSE;
  208. }
  209. // Not necessary to use self::chmod() as there is no scheme.
  210. return chmod($uri, $mode);
  211. }
  212. /**
  213. * Helper function. Ensures we don't pass a NULL as a context resource to
  214. * mkdir().
  215. *
  216. * @see self::mkdir()
  217. */
  218. protected function mkdirCall($uri, $mode, $recursive, $context) {
  219. if (is_null($context)) {
  220. return mkdir($uri, $mode, $recursive);
  221. }
  222. else {
  223. return mkdir($uri, $mode, $recursive, $context);
  224. }
  225. }
  226. /**
  227. * {@inheritdoc}
  228. */
  229. public function rmdir($uri, $context = NULL) {
  230. if (!$this->streamWrapperManager->isValidUri($uri) && (substr(PHP_OS, 0, 3) == 'WIN')) {
  231. chmod($uri, 0700);
  232. }
  233. if ($context) {
  234. return rmdir($uri, $context);
  235. }
  236. else {
  237. return rmdir($uri);
  238. }
  239. }
  240. /**
  241. * {@inheritdoc}
  242. */
  243. public function tempnam($directory, $prefix) {
  244. $scheme = StreamWrapperManager::getScheme($directory);
  245. if ($this->streamWrapperManager->isValidScheme($scheme)) {
  246. $wrapper = $this->streamWrapperManager->getViaScheme($scheme);
  247. if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) {
  248. return $scheme . '://' . static::basename($filename);
  249. }
  250. else {
  251. return FALSE;
  252. }
  253. }
  254. else {
  255. // Handle as a normal tempnam() call.
  256. return tempnam($directory, $prefix);
  257. }
  258. }
  259. /**
  260. * {@inheritdoc}
  261. */
  262. public function uriScheme($uri) {
  263. @trigger_error('FileSystem::uriScheme() is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Use \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface::getScheme() instead. See https://www.drupal.org/node/3035273', E_USER_DEPRECATED);
  264. return StreamWrapperManager::getScheme($uri);
  265. }
  266. /**
  267. * {@inheritdoc}
  268. */
  269. public function validScheme($scheme) {
  270. @trigger_error('FileSystem::validScheme() is deprecated in drupal:8.8.0 and will be removed before drupal:9.0.0. Use \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface::isValidScheme() instead. See https://www.drupal.org/node/3035273', E_USER_DEPRECATED);
  271. return $this->streamWrapperManager->isValidScheme($scheme);
  272. }
  273. /**
  274. * {@inheritdoc}
  275. */
  276. public function copy($source, $destination, $replace = self::EXISTS_RENAME) {
  277. $this->prepareDestination($source, $destination, $replace);
  278. if (!@copy($source, $destination)) {
  279. // If the copy failed and realpaths exist, retry the operation using them
  280. // instead.
  281. $real_source = $this->realpath($source) ?: $source;
  282. $real_destination = $this->realpath($destination) ?: $destination;
  283. if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
  284. $this->logger->error("The specified file '%source' could not be copied to '%destination'.", [
  285. '%source' => $source,
  286. '%destination' => $destination,
  287. ]);
  288. throw new FileWriteException("The specified file '$source' could not be copied to '$destination'.");
  289. }
  290. }
  291. // Set the permissions on the new file.
  292. $this->chmod($destination);
  293. return $destination;
  294. }
  295. /**
  296. * {@inheritdoc}
  297. */
  298. public function delete($path) {
  299. if (is_file($path)) {
  300. if (!$this->unlink($path)) {
  301. $this->logger->error("Failed to unlink file '%path'.", ['%path' => $path]);
  302. throw new FileException("Failed to unlink file '$path'.");
  303. }
  304. return TRUE;
  305. }
  306. if (is_dir($path)) {
  307. $this->logger->error("Cannot delete '%path' because it is a directory. Use deleteRecursive() instead.", ['%path' => $path]);
  308. throw new NotRegularFileException("Cannot delete '$path' because it is a directory. Use deleteRecursive() instead.");
  309. }
  310. // Return TRUE for non-existent file, but log that nothing was actually
  311. // deleted, as the current state is the intended result.
  312. if (!file_exists($path)) {
  313. $this->logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]);
  314. return TRUE;
  315. }
  316. // We cannot handle anything other than files and directories.
  317. // Throw an exception for everything else (sockets, symbolic links, etc).
  318. $this->logger->error("The file '%path' is not of a recognized type so it was not deleted.", ['%path' => $path]);
  319. throw new NotRegularFileException("The file '$path' is not of a recognized type so it was not deleted.");
  320. }
  321. /**
  322. * {@inheritdoc}
  323. */
  324. public function deleteRecursive($path, callable $callback = NULL) {
  325. if ($callback) {
  326. call_user_func($callback, $path);
  327. }
  328. if (is_dir($path)) {
  329. $dir = dir($path);
  330. while (($entry = $dir->read()) !== FALSE) {
  331. if ($entry == '.' || $entry == '..') {
  332. continue;
  333. }
  334. $entry_path = $path . '/' . $entry;
  335. $this->deleteRecursive($entry_path, $callback);
  336. }
  337. $dir->close();
  338. return $this->rmdir($path);
  339. }
  340. return $this->delete($path);
  341. }
  342. /**
  343. * {@inheritdoc}
  344. */
  345. public function move($source, $destination, $replace = self::EXISTS_RENAME) {
  346. $this->prepareDestination($source, $destination, $replace);
  347. // Ensure compatibility with Windows.
  348. // @see \Drupal\Core\File\FileSystemInterface::unlink().
  349. if (!$this->streamWrapperManager->isValidUri($source) && (substr(PHP_OS, 0, 3) == 'WIN')) {
  350. chmod($source, 0600);
  351. }
  352. // Attempt to resolve the URIs. This is necessary in certain
  353. // configurations (see above) and can also permit fast moves across local
  354. // schemes.
  355. $real_source = $this->realpath($source) ?: $source;
  356. $real_destination = $this->realpath($destination) ?: $destination;
  357. // Perform the move operation.
  358. if (!@rename($real_source, $real_destination)) {
  359. // Fall back to slow copy and unlink procedure. This is necessary for
  360. // renames across schemes that are not local, or where rename() has not
  361. // been implemented. It's not necessary to use FileSystem::unlink() as the
  362. // Windows issue has already been resolved above.
  363. if (!@copy($real_source, $real_destination)) {
  364. $this->logger->error("The specified file '%source' could not be moved to '%destination'.", [
  365. '%source' => $source,
  366. '%destination' => $destination,
  367. ]);
  368. throw new FileWriteException("The specified file '$source' could not be moved to '$destination'.");
  369. }
  370. if (!@unlink($real_source)) {
  371. $this->logger->error("The source file '%source' could not be unlinked after copying to '%destination'.", [
  372. '%source' => $source,
  373. '%destination' => $destination,
  374. ]);
  375. throw new FileException("The source file '$source' could not be unlinked after copying to '$destination'.");
  376. }
  377. }
  378. // Set the permissions on the new file.
  379. $this->chmod($destination);
  380. return $destination;
  381. }
  382. /**
  383. * Prepares the destination for a file copy or move operation.
  384. *
  385. * - Checks if $source and $destination are valid and readable/writable.
  386. * - Checks that $source is not equal to $destination; if they are an error
  387. * is reported.
  388. * - If file already exists in $destination either the call will error out,
  389. * replace the file or rename the file based on the $replace parameter.
  390. *
  391. * @param string $source
  392. * A string specifying the filepath or URI of the source file.
  393. * @param string|null $destination
  394. * A URI containing the destination that $source should be moved/copied to.
  395. * The URI may be a bare filepath (without a scheme) and in that case the
  396. * default scheme (file://) will be used.
  397. * @param int $replace
  398. * Replace behavior when the destination file already exists:
  399. * - FileSystemInterface::EXISTS_REPLACE - Replace the existing file.
  400. * - FileSystemInterface::EXISTS_RENAME - Append _{incrementing number}
  401. * until the filename is unique.
  402. * - FileSystemInterface::EXISTS_ERROR - Do nothing and return FALSE.
  403. *
  404. * @see \Drupal\Core\File\FileSystemInterface::copy()
  405. * @see \Drupal\Core\File\FileSystemInterface::move()
  406. */
  407. protected function prepareDestination($source, &$destination, $replace) {
  408. $original_source = $source;
  409. if (!file_exists($source)) {
  410. if (($realpath = $this->realpath($original_source)) !== FALSE) {
  411. $this->logger->error("File '%original_source' ('%realpath') could not be copied because it does not exist.", [
  412. '%original_source' => $original_source,
  413. '%realpath' => $realpath,
  414. ]);
  415. throw new FileNotExistsException("File '$original_source' ('$realpath') could not be copied because it does not exist.");
  416. }
  417. else {
  418. $this->logger->error("File '%original_source' could not be copied because it does not exist.", [
  419. '%original_source' => $original_source,
  420. ]);
  421. throw new FileNotExistsException("File '$original_source' could not be copied because it does not exist.");
  422. }
  423. }
  424. // Prepare the destination directory.
  425. if ($this->prepareDirectory($destination)) {
  426. // The destination is already a directory, so append the source basename.
  427. $destination = $this->streamWrapperManager->normalizeUri($destination . '/' . $this->basename($source));
  428. }
  429. else {
  430. // Perhaps $destination is a dir/file?
  431. $dirname = $this->dirname($destination);
  432. if (!$this->prepareDirectory($dirname)) {
  433. $this->logger->error("The specified file '%original_source' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.", [
  434. '%original_source' => $original_source,
  435. ]);
  436. throw new DirectoryNotReadyException("The specified file '$original_source' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.");
  437. }
  438. }
  439. // Determine whether we can perform this operation based on overwrite rules.
  440. $destination = $this->getDestinationFilename($destination, $replace);
  441. if ($destination === FALSE) {
  442. $this->logger->error("File '%original_source' could not be copied because a file by that name already exists in the destination directory ('%destination').", [
  443. '%original_source' => $original_source,
  444. '%destination' => $destination,
  445. ]);
  446. throw new FileExistsException("File '$original_source' could not be copied because a file by that name already exists in the destination directory ('$destination').");
  447. }
  448. // Assert that the source and destination filenames are not the same.
  449. $real_source = $this->realpath($source);
  450. $real_destination = $this->realpath($destination);
  451. if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
  452. $this->logger->error("File '%source' could not be copied because it would overwrite itself.", [
  453. '%source' => $source,
  454. ]);
  455. throw new FileException("File '$source' could not be copied because it would overwrite itself.");
  456. }
  457. }
  458. /**
  459. * {@inheritdoc}
  460. */
  461. public function saveData($data, $destination, $replace = self::EXISTS_RENAME) {
  462. // Write the data to a temporary file.
  463. $temp_name = $this->tempnam('temporary://', 'file');
  464. if (file_put_contents($temp_name, $data) === FALSE) {
  465. $this->logger->error("Temporary file '%temp_name' could not be created.", ['%temp_name' => $temp_name]);
  466. throw new FileWriteException("Temporary file '$temp_name' could not be created.");
  467. }
  468. // Move the file to its final destination.
  469. return $this->move($temp_name, $destination, $replace);
  470. }
  471. /**
  472. * {@inheritdoc}
  473. */
  474. public function prepareDirectory(&$directory, $options = self::MODIFY_PERMISSIONS) {
  475. if (!$this->streamWrapperManager->isValidUri($directory)) {
  476. // Only trim if we're not dealing with a stream.
  477. $directory = rtrim($directory, '/\\');
  478. }
  479. if (!is_dir($directory)) {
  480. // Let mkdir() recursively create directories and use the default
  481. // directory permissions.
  482. if ($options & static::CREATE_DIRECTORY) {
  483. return @$this->mkdir($directory, NULL, TRUE);
  484. }
  485. return FALSE;
  486. }
  487. $writable = is_writable($directory);
  488. if (!$writable && ($options & static::MODIFY_PERMISSIONS)) {
  489. return $this->chmod($directory);
  490. }
  491. return $writable;
  492. }
  493. /**
  494. * {@inheritdoc}
  495. */
  496. public function getDestinationFilename($destination, $replace) {
  497. $basename = $this->basename($destination);
  498. if (!Unicode::validateUtf8($basename)) {
  499. throw new FileException(sprintf("Invalid filename '%s'", $basename));
  500. }
  501. if (file_exists($destination)) {
  502. switch ($replace) {
  503. case FileSystemInterface::EXISTS_REPLACE:
  504. // Do nothing here, we want to overwrite the existing file.
  505. break;
  506. case FileSystemInterface::EXISTS_RENAME:
  507. $directory = $this->dirname($destination);
  508. $destination = $this->createFilename($basename, $directory);
  509. break;
  510. case FileSystemInterface::EXISTS_ERROR:
  511. // Error reporting handled by calling function.
  512. return FALSE;
  513. }
  514. }
  515. return $destination;
  516. }
  517. /**
  518. * {@inheritdoc}
  519. */
  520. public function createFilename($basename, $directory) {
  521. $original = $basename;
  522. // Strip control characters (ASCII value < 32). Though these are allowed in
  523. // some filesystems, not many applications handle them well.
  524. $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
  525. if (preg_last_error() !== PREG_NO_ERROR) {
  526. throw new FileException(sprintf("Invalid filename '%s'", $original));
  527. }
  528. if (substr(PHP_OS, 0, 3) == 'WIN') {
  529. // These characters are not allowed in Windows filenames.
  530. $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
  531. }
  532. // A URI or path may already have a trailing slash or look like "public://".
  533. if (substr($directory, -1) == '/') {
  534. $separator = '';
  535. }
  536. else {
  537. $separator = '/';
  538. }
  539. $destination = $directory . $separator . $basename;
  540. if (file_exists($destination)) {
  541. // Destination file already exists, generate an alternative.
  542. $pos = strrpos($basename, '.');
  543. if ($pos !== FALSE) {
  544. $name = substr($basename, 0, $pos);
  545. $ext = substr($basename, $pos);
  546. }
  547. else {
  548. $name = $basename;
  549. $ext = '';
  550. }
  551. $counter = 0;
  552. do {
  553. $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
  554. } while (file_exists($destination));
  555. }
  556. return $destination;
  557. }
  558. /**
  559. * {@inheritdoc}
  560. */
  561. public function getTempDirectory() {
  562. // Use settings.
  563. $temporary_directory = $this->settings->get('file_temp_path');
  564. if (!empty($temporary_directory)) {
  565. return $temporary_directory;
  566. }
  567. // Fallback to config for Backwards compatibility.
  568. // This service is lazy-loaded and not injected, as the file_system service
  569. // is used in the install phase before config_factory service exists. It
  570. // will be removed before Drupal 9.0.0.
  571. if (\Drupal::hasContainer()) {
  572. $temporary_directory = \Drupal::config('system.file')->get('path.temporary');
  573. if (!empty($temporary_directory)) {
  574. @trigger_error("The 'system.file' config 'path.temporary' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Set 'file_temp_path' in settings.php instead. See https://www.drupal.org/node/3039255", E_USER_DEPRECATED);
  575. return $temporary_directory;
  576. }
  577. }
  578. // Fallback to OS default.
  579. $temporary_directory = FileSystemComponent::getOsTemporaryDirectory();
  580. if (empty($temporary_directory)) {
  581. // If no directory has been found default to 'files/tmp'.
  582. $temporary_directory = PublicStream::basePath() . '/tmp';
  583. // Windows accepts paths with either slash (/) or backslash (\), but
  584. // will not accept a path which contains both a slash and a backslash.
  585. // Since the 'file_public_path' variable may have either format, we
  586. // sanitize everything to use slash which is supported on all platforms.
  587. $temporary_directory = str_replace('\\', '/', $temporary_directory);
  588. }
  589. return $temporary_directory;
  590. }
  591. /**
  592. * {@inheritdoc}
  593. */
  594. public function scanDirectory($dir, $mask, array $options = []) {
  595. // Merge in defaults.
  596. $options += [
  597. 'callback' => 0,
  598. 'recurse' => TRUE,
  599. 'key' => 'uri',
  600. 'min_depth' => 0,
  601. ];
  602. $dir = $this->streamWrapperManager->normalizeUri($dir);
  603. if (!is_dir($dir)) {
  604. throw new NotRegularDirectoryException("$dir is not a directory.");
  605. }
  606. // Allow directories specified in settings.php to be ignored. You can use
  607. // this to not check for files in common special-purpose directories. For
  608. // example, node_modules and bower_components. Ignoring irrelevant
  609. // directories is a performance boost.
  610. if (!isset($options['nomask'])) {
  611. $ignore_directories = $this->settings->get('file_scan_ignore_directories', []);
  612. array_walk($ignore_directories, function (&$value) {
  613. $value = preg_quote($value, '/');
  614. });
  615. $options['nomask'] = '/^' . implode('|', $ignore_directories) . '$/';
  616. }
  617. $options['key'] = in_array($options['key'], ['uri', 'filename', 'name']) ? $options['key'] : 'uri';
  618. return $this->doScanDirectory($dir, $mask, $options);
  619. }
  620. /**
  621. * Internal function to handle directory scanning with recursion.
  622. *
  623. * @param string $dir
  624. * The base directory or URI to scan, without trailing slash.
  625. * @param string $mask
  626. * The preg_match() regular expression for files to be included.
  627. * @param array $options
  628. * The options as per ::scanDirectory().
  629. * @param int $depth
  630. * The current depth of recursion.
  631. *
  632. * @return array
  633. * An associative array as per ::scanDirectory().
  634. *
  635. * @throws \Drupal\Core\File\Exception\NotRegularDirectoryException
  636. * If the directory does not exist.
  637. *
  638. * @see \Drupal\Core\File\FileSystemInterface::scanDirectory()
  639. */
  640. protected function doScanDirectory($dir, $mask, array $options = [], $depth = 0) {
  641. $files = [];
  642. // Avoid warnings when opendir does not have the permissions to open a
  643. // directory.
  644. if ($handle = @opendir($dir)) {
  645. while (FALSE !== ($filename = readdir($handle))) {
  646. // Skip this file if it matches the nomask or starts with a dot.
  647. if ($filename[0] != '.' && !(preg_match($options['nomask'], $filename))) {
  648. if (substr($dir, -1) == '/') {
  649. $uri = "$dir$filename";
  650. }
  651. else {
  652. $uri = "$dir/$filename";
  653. }
  654. if ($options['recurse'] && is_dir($uri)) {
  655. // Give priority to files in this folder by merging them in after
  656. // any subdirectory files.
  657. $files = array_merge($this->doScanDirectory($uri, $mask, $options, $depth + 1), $files);
  658. }
  659. elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
  660. // Always use this match over anything already set in $files with
  661. // the same $options['key'].
  662. $file = new \stdClass();
  663. $file->uri = $uri;
  664. $file->filename = $filename;
  665. $file->name = pathinfo($filename, PATHINFO_FILENAME);
  666. $key = $options['key'];
  667. $files[$file->$key] = $file;
  668. if ($options['callback']) {
  669. $options['callback']($uri);
  670. }
  671. }
  672. }
  673. }
  674. closedir($handle);
  675. }
  676. else {
  677. $this->logger->error('@dir can not be opened', ['@dir' => $dir]);
  678. }
  679. return $files;
  680. }
  681. }