diff --git a/core/includes/file.inc b/core/includes/file.inc index a963e71a59..ea71ec727f 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -474,19 +474,91 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST * Internal function that prepares the destination for a file_unmanaged_copy or * file_unmanaged_move operation. * + * - Checks if $source and $destination are valid and readable/writable. + * - Checks that $source is not equal to $destination; if they are an error + * is reported. + * - If file already exists in $destination either the call will error out, + * replace the file or rename the file based on the $replace parameter. + * + * @param $source + * A string specifying the filepath or URI of the source file. + * @param $destination + * A URI containing the destination that $source should be moved/copied to. + * The URI may be a bare filepath (without a scheme) and in that case the + * default scheme (file://) will be used. If this value is omitted, Drupal's + * default files scheme will be used, usually "public://". + * @param $replace + * Replace behavior when the destination file already exists: + * - FILE_EXISTS_REPLACE - Replace the existing file. + * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR - Do nothing and return FALSE. + * * @deprecated in Drupal 8.7.0, will be removed before Drupal 9.0.0. - * Use \Drupal\Core\File\UnmanagedFileHandler::prepareDestination() instead. + * Use \Drupal\Core\File\FileSystemInterface::getDestinationFilename() instead. * * @see https://www.drupal.org/node/3006851 */ function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_EXISTS_RENAME) { - @trigger_error('file_unmanaged_prepare() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\File\UnmanagedFileHandler::prepareDestination(). See https://www.drupal.org/node/3006851.', E_USER_DEPRECATED); - try { - \Drupal::service('file_system')->prepareDestination($source, $destination, $replace); + @trigger_error('file_unmanaged_prepare() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\File\FileSystemInterface::getDestinationFilename() instead. See https://www.drupal.org/node/3006851.', E_USER_DEPRECATED); + $logger = \Drupal::logger('file'); + /** @var \Drupal\Core\File\FileSystemInterface $file_system */ + $file_system = \Drupal::service('file_system'); + + // Assert that the source file actually exists. + if (!file_exists($source)) { + // @todo Replace \Drupal::messenger()->addError() calls with exceptions + // instead. + \Drupal::messenger()->addError(t('The specified file %file could not be moved/copied because no file by that name exists. Please check that you supplied the correct filename.', ['%file' => $original_source])); + if (($realpath = $file_system->realpath($original_source)) !== FALSE) { + $logger->notice('File %file (%realpath) could not be moved/copied because it does not exist.', ['%file' => $original_source, '%realpath' => $realpath]); + } + else { + $logger->notice('File %file could not be moved/copied because it does not exist.', ['%file' => $original_source]); + } + return FALSE; } - catch (FileException $e) { + + // Build a destination URI if necessary. + if (!isset($destination)) { + $destination = file_build_uri(drupal_basename($source)); + } + + // Prepare the destination directory. + if (file_prepare_directory($destination)) { + // The destination is already a directory, so append the source basename. + $destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source)); + } + else { + // Perhaps $destination is a dir/file? + $dirname = drupal_dirname($destination); + if (!file_prepare_directory($dirname)) { + // The destination is not valid. + $logger->notice('File %file could not be moved/copied because the destination directory %destination is not configured correctly.', ['%file' => $original_source, '%destination' => $dirname]); + \Drupal::messenger()->addError(t('The specified file %file could not be moved/copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', ['%file' => $original_source])); + return FALSE; + } + } + + // Determine whether we can perform this operation based on overwrite rules. + $destination = file_destination($destination, $replace); + if ($destination === FALSE) { + \Drupal::messenger()->addError(t('The file %file could not be moved/copied because a file by that name already exists in the destination directory.', ['%file' => $original_source])); + $logger->notice('File %file could not be moved/copied because a file by that name already exists in the destination directory (%destination)', ['%file' => $original_source, '%destination' => $destination]); return FALSE; } + + // Assert that the source and destination filenames are not the same. + $real_source = $file_system->realpath($source); + $real_destination = $file_system->realpath($destination); + if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) { + \Drupal::messenger()->addError(t('The specified file %file was not moved/copied because it would overwrite itself.', ['%file' => $source])); + $logger->notice('File %file could not be moved/copied because it would overwrite itself.', ['%file' => $source]); + return FALSE; + } + // Make sure the .htaccess files are present. + file_ensure_htaccess(); + return TRUE; } /** diff --git a/core/lib/Drupal/Core/File/FileSystem.php b/core/lib/Drupal/Core/File/FileSystem.php index 36b65f6d5d..a9b7457508 100644 --- a/core/lib/Drupal/Core/File/FileSystem.php +++ b/core/lib/Drupal/Core/File/FileSystem.php @@ -311,7 +311,7 @@ public function validScheme($scheme) { * {@inheritdoc} */ public function copy($source, $destination = NULL, $replace = self::EXISTS_RENAME) { - $this->doPrepareDestination($source, $destination, $replace); + $this->prepareDestination($source, $destination, $replace); // Perform the copy operation. if (!@copy($source, $destination)) { @@ -324,7 +324,7 @@ public function copy($source, $destination = NULL, $replace = self::EXISTS_RENAM '%source' => $source, '%destination' => $destination, ]); - throw new FileWriteException(sprintf("The specified file '%s' could not be copied to '%s'.", $source, $destination)); + throw new FileWriteException("The specified file '$source' could not be copied to '$destination'."); } } @@ -341,14 +341,14 @@ public function delete($path) { if (is_file($path)) { if (!$this->unlink($path)) { $this->logger->error("Failed to unlink file '%path'.", ['%path' => $path]); - throw new FileException(sprintf("Failed to unlink file '%s'.", $path)); + throw new FileException("Failed to unlink file '$path'."); } return TRUE; } if (is_dir($path)) { $this->logger->error("Cannot delete '%path' because it is a directory. Use deleteRecursive() instead.", ['%path' => $path]); - throw new NotRegularFileException(sprintf("Cannot delete '%s' because it is a directory. Use deleteRecursive() instead.", $path)); + throw new NotRegularFileException("Cannot delete '$path' because it is a directory. Use deleteRecursive() instead."); } // Do not throw exception for non-existent file, but return FALSE to @@ -361,7 +361,7 @@ public function delete($path) { // We cannot handle anything other than files and directories. // Throw an exception for everything else (sockets, symbolic links, etc). $this->logger->error("The file '%path' is not of a recognized type so it was not deleted.", ['%path' => $path]); - throw new NotRegularFileException(sprintf("The file '%s' is not of a recognized type so it was not deleted.", $path)); + throw new NotRegularFileException("The file '$path' is not of a recognized type so it was not deleted."); } /** @@ -393,7 +393,7 @@ public function deleteRecursive($path, callable $callback = NULL) { * {@inheritdoc} */ public function move($source, $destination = NULL, $replace = self::EXISTS_RENAME) { - $this->doPrepareDestination($source, $destination, $replace); + $this->prepareDestination($source, $destination, $replace); // Ensure compatibility with Windows. // @see \Drupal\Core\File\FileSystemInterface::unlink(). @@ -417,14 +417,14 @@ public function move($source, $destination = NULL, $replace = self::EXISTS_RENAM '%source' => $source, '%destination' => $destination, ]); - throw new FileWriteException(sprintf("The specified file '%s' could not be moved to '%s'.", $source, $destination)); + throw new FileWriteException("The specified file '$source' could not be moved to '$destination'."); } if (!@unlink($real_source)) { $this->logger->error("The source file '%source' could not be unlinked after copying to '%destination'.", [ '%source' => $source, '%destination' => $destination, ]); - throw new FileException(sprintf("The source file '%s' could not be unlinked after copying to '%s'.", $source, $destination)); + throw new FileException("The source file '$source' could not be unlinked after copying to '$destination'."); } } @@ -457,44 +457,10 @@ public function move($source, $destination = NULL, $replace = self::EXISTS_RENAM * is unique. * - FILE_EXISTS_ERROR - Do nothing and return FALSE. * - * @deprecated This public method is deprecated in 8.7.0 and will be converted - * into a protected method before 9.0.0. - * - * @see \Drupal\Core\File\UnmanagedFileHandler::copy() - * @see \Drupal\Core\File\UnmanagedFileHandler::move() - */ - public function prepareDestination($source, &$destination = NULL, $replace = self::EXISTS_RENAME) { - @trigger_error('FileSystem::prepareDestination() is deprecated in Drupal 8.7.0 and will be converted to a protected method before Drupal 9.0.0. See https://www.drupal.org/node/3006851.', E_USER_DEPRECATED); - $this->doPrepareDestination($source, $destination, $replace); - } - - /** - * Prepares the destination for a file copy or move operation. - * - * - Checks if $source and $destination are valid and readable/writable. - * - Checks that $source is not equal to $destination; if they are an error - * is reported. - * - If file already exists in $destination either the call will error out, - * replace the file or rename the file based on the $replace parameter. - * - * @param string $source - * A string specifying the filepath or URI of the source file. - * @param string|null $destination - * A URI containing the destination that $source should be moved/copied to. - * The URI may be a bare filepath (without a scheme) and in that case the - * default scheme (file://) will be used. If this value is omitted, Drupal's - * default files scheme will be used, usually "public://". - * @param int $replace - * Replace behavior when the destination file already exists: - * - FILE_EXISTS_REPLACE - Replace the existing file. - * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename - * is unique. - * - FILE_EXISTS_ERROR - Do nothing and return FALSE. - * - * @see \Drupal\Core\File\UnmanagedFileHandler::copy() - * @see \Drupal\Core\File\UnmanagedFileHandler::move() + * @see \Drupal\Core\File\FileSystemInterface::copy() + * @see \Drupal\Core\File\FileSystemInterface::move() */ - protected function doPrepareDestination($source, &$destination = NULL, $replace = self::EXISTS_RENAME) { + protected function prepareDestination($source, &$destination = NULL, $replace = self::EXISTS_RENAME) { $original_source = $source; // Assert that the source file actually exists. @@ -504,13 +470,13 @@ protected function doPrepareDestination($source, &$destination = NULL, $replace '%original_source' => $original_source, '%realpath' => $realpath, ]); - throw new FileNotExistsException(sprintf("File '%s' ('%s') could not be copied because it does not exist.", $original_source, $realpath)); + throw new FileNotExistsException("File '$original_source' ('$realpath') could not be copied because it does not exist."); } else { $this->logger->error("File '%original_source' could not be copied because it does not exist.", [ '%original_source' => $original_source, ]); - throw new FileNotExistsException(sprintf("File '%s' could not be copied because it does not exist.", $original_source)); + throw new FileNotExistsException("File '$original_source' could not be copied because it does not exist."); } } @@ -531,7 +497,7 @@ protected function doPrepareDestination($source, &$destination = NULL, $replace $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.", [ '%original_source' => $original_source, ]); - throw new DirectoryNotReadyException(sprintf("The specified file '%s' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.", $original_source)); + 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."); } } @@ -542,7 +508,7 @@ protected function doPrepareDestination($source, &$destination = NULL, $replace '%original_source' => $original_source, '%destination' => $destination, ]); - throw new FileExistsException(sprintf("File '%s' could not be copied because a file by that name already exists in the destination directory ('%s').", $original_source, $destination)); + throw new FileExistsException("File '$original_source' could not be copied because a file by that name already exists in the destination directory ('$destination')."); } // Assert that the source and destination filenames are not the same. @@ -552,7 +518,7 @@ protected function doPrepareDestination($source, &$destination = NULL, $replace $this->logger->error("File '%source' could not be copied because it would overwrite itself.", [ '%source' => $source, ]); - throw new FileException(sprintf("File '%s' could not be copied because it would overwrite itself.", $source)); + throw new FileException("File '$source' could not be copied because it would overwrite itself."); } // Make sure the .htaccess files are present. // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304. @@ -567,7 +533,7 @@ public function saveData($data, $destination = NULL, $replace = self::EXISTS_REN $temp_name = $this->tempnam('temporary://', 'file'); if (file_put_contents($temp_name, $data) === FALSE) { $this->logger->error("Temporary file '%temp_name' could not be created.", ['%temp_name' => $temp_name]); - throw new FileWriteException(sprintf("Temporary file '%s' could not be created.", $temp_name)); + throw new FileWriteException("Temporary file '$temp_name' could not be created."); } // Move the file to its final destination. diff --git a/core/tests/Drupal/KernelTests/Core/File/FileSystemDeprecationTest.php b/core/tests/Drupal/KernelTests/Core/File/FileSystemDeprecationTest.php index 1cd157a9e3..1e434c4246 100644 --- a/core/tests/Drupal/KernelTests/Core/File/FileSystemDeprecationTest.php +++ b/core/tests/Drupal/KernelTests/Core/File/FileSystemDeprecationTest.php @@ -55,7 +55,7 @@ public function testDeprecatedUnmanagedFileMove() { } /** - * @expectedDeprecation file_unmanaged_prepare() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\File\UnmanagedFileHandler::prepareDestination(). See https://www.drupal.org/node/3006851. + * @expectedDeprecation file_unmanaged_prepare() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\File\FileSystemInterface::getDestinationFilename() instead. See https://www.drupal.org/node/3006851. */ public function testDeprecatedUnmanagedPrepare() { $this->assertNotNull(file_unmanaged_prepare(NULL));