diff --git a/core/includes/file.inc b/core/includes/file.inc index adedc0a..985ea0e 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -11,6 +11,7 @@ use Drupal\Component\PhpStorage\FileStorage; use Drupal\Component\Utility\Bytes; use Drupal\Core\File\Exception\FileException; +use Drupal\Core\File\Exception\FileWriteException; use Drupal\Core\File\FileHandlerInterface; use Drupal\Core\File\FileSystem; use Drupal\Core\Site\Settings; @@ -562,35 +563,19 @@ function file_destination($destination, $replace) { * @return * The path to the new file, or FALSE in the event of an error. * + * @deprecated in Drupal 8.3.x, will be removed before Drupal 9.0.0. + * Use \Drupal::service('file_handler.unmanaged')->move(). + * * @see file_move() */ function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { - if (!file_unmanaged_prepare($source, $destination, $replace)) { - return FALSE; - } - // Ensure compatibility with Windows. - // @see drupal_unlink() - if ((substr(PHP_OS, 0, 3) == 'WIN') && (!file_stream_wrapper_valid_scheme(file_uri_scheme($source)))) { - chmod($source, 0600); + try { + return \Drupal::service('file_handler.unmanaged')->move($source, $destination, $replace); } - // Attempt to resolve the URIs. This is necessary in certain configurations - // (see above) and can also permit fast moves across local schemes. - $real_source = drupal_realpath($source) ?: $source; - $real_destination = drupal_realpath($destination) ?: $destination; - // Perform the move operation. - if (!@rename($real_source, $real_destination)) { - // Fall back to slow copy and unlink procedure. This is necessary for - // renames across schemes that are not local, or where rename() has not been - // implemented. It's not necessary to use drupal_unlink() as the Windows - // issue has already been resolved above. - if (!@copy($real_source, $real_destination) || !@unlink($real_source)) { - \Drupal::logger('file')->error('The specified file %file could not be moved to %destination.', array('%file' => $source, '%destination' => $destination)); - return FALSE; - } + catch (FileException $e) { + \Drupal::logger('file')->error($e->getMessage()); + return FALSE; } - // Set the permissions on the new file. - drupal_chmod($destination); - return $destination; } /** @@ -775,28 +760,27 @@ function file_delete_multiple(array $fids) { * TRUE for success or path does not exist, or FALSE in the event of an * error. * + * @deprecated in Drupal 8.3.x, will be removed before Drupal 9.0.0. + * Use \Drupal::service('file_handler.unmanaged')->delete(). + * * @see file_delete() * @see file_unmanaged_delete_recursive() */ function file_unmanaged_delete($path) { - if (is_file($path)) { - return drupal_unlink($path); + try { + $success = \Drupal::service('file_handler.unmanaged')->delete($path); + if (!$success) { + // Operation returned FALSE because target path does not exist. Log this + // condition, but return TRUE anyway because this is not an error and the + // current state is the intended result. + \Drupal::logger('file')->notice('The file %path was not deleted because it does not exist.', array('%path' => $path)); + } + return TRUE; } - $logger = \Drupal::logger('file'); - if (is_dir($path)) { - $logger->error('%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path)); + catch (FileException $e) { + \Drupal::logger('file')->error($e->getMessage()); return FALSE; } - // Return TRUE for non-existent file, but log that nothing was actually - // deleted, as the current state is the intended result. - if (!file_exists($path)) { - $logger->notice('The file %path was not deleted because it does not exist.', array('%path' => $path)); - return TRUE; - } - // We cannot handle anything other than files and directories. Log an error - // for everything else (sockets, symbolic links, etc). - $logger->error('The file %path is not of a recognized type so it was not deleted.', array('%path' => $path)); - return FALSE; } /** @@ -822,30 +806,30 @@ function file_unmanaged_delete($path) { * TRUE for success or if path does not exist, FALSE in the event of an * error. * + * @deprecated in Drupal 8.3.x, will be removed before Drupal 9.0.0. + * Use \Drupal::service('file_handler.unmanaged')->delete(). + * * @see file_unmanaged_delete() */ function file_unmanaged_delete_recursive($path, $callback = NULL) { - if (isset($callback)) { - call_user_func($callback, $path); - } - if (is_dir($path)) { - $dir = dir($path); - while (($entry = $dir->read()) !== FALSE) { - if ($entry == '.' || $entry == '..') { - continue; - } - $entry_path = $path . '/' . $entry; - file_unmanaged_delete_recursive($entry_path, $callback); - } - $dir->close(); + $callback = is_callable($callback) ? $callback : NULL; - return drupal_rmdir($path); + try { + $success = \Drupal::service('file_handler.unmanaged')->deleteRecursive($path, $callback); + if (!$success) { + // Operation returned FALSE because target path does not exist. Log this + // condition, but return TRUE anyway because this is not an error and the + // current state is the intended result. + \Drupal::logger('file')->notice('The directory %path was not deleted because it does not exist.', array('%path' => $path)); + } + return TRUE; + } + catch (FileException $e) { + \Drupal::logger('file')->error($e->getMessage()); + return FALSE; } - return file_unmanaged_delete($path); } - - /** * Moves an uploaded file to a new location. * @@ -880,18 +864,24 @@ function drupal_move_uploaded_file($filename, $uri) { * @return * A string with the path of the resulting file, or FALSE on error. * + * @deprecated in Drupal 8.3.x, will be removed before Drupal 9.0.0. + * Use \Drupal::service('file_handler.unmanaged')->saveData(). + * * @see file_save_data() */ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) { - // Write the data to a temporary file. - $temp_name = drupal_tempnam('temporary://', 'file'); - if (file_put_contents($temp_name, $data) === FALSE) { + try { + return \Drupal::service('file_handler.unmanaged')->saveData($source, $destination, $replace); + } + catch (FileWriteException $e) { drupal_set_message(t('The file could not be created.'), 'error'); + \Drupal::logger('file')->error($e->getMessage()); + return FALSE; + } + catch (FileException $e) { + \Drupal::logger('file')->error($e->getMessage()); return FALSE; } - - // Move the file to its final destination. - return file_unmanaged_move($temp_name, $destination, $replace); } /** diff --git a/core/lib/Drupal/Core/File/Exception/FileException.php b/core/lib/Drupal/Core/File/Exception/FileException.php index ef8db92..4bb6a0c 100644 --- a/core/lib/Drupal/Core/File/Exception/FileException.php +++ b/core/lib/Drupal/Core/File/Exception/FileException.php @@ -5,5 +5,5 @@ /** * Base class for exceptions related to file handling operations. */ -class FileException extends \Exception { +class FileException extends \RuntimeException { } diff --git a/core/lib/Drupal/Core/File/Exception/FileWriteException.php b/core/lib/Drupal/Core/File/Exception/FileWriteException.php new file mode 100644 index 0000000..d0af7c9 --- /dev/null +++ b/core/lib/Drupal/Core/File/Exception/FileWriteException.php @@ -0,0 +1,9 @@ +prepare($source, $destination, $replace); // Perform the copy operation. @@ -40,7 +42,97 @@ public function copy($source, $destination, $replace = self::FILE_EXISTS_RENAME) // If the copy failed and realpaths exist, retry the operation using them // instead. if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) { - throw new FileException(sprintf("The specified file '%s' could not be copied to '%s'.", $source, $destination)); + throw new FileWriteException(sprintf("The specified file '%s' could not be copied to '%s'.", $source, $destination)); + } + } + + // Set the permissions on the new file. + $this->fileSystem->chmod($destination); + + return $destination; + } + + /** + * {@inheritdoc} + */ + public function delete($path) { + if (is_file($path)) { + if (!$this->fileSystem->unlink($path)) { + throw new FileException(sprintf("Failed to unlink file '%s'.", $path)); + } + return TRUE; + } + + if (is_dir($path)) { + throw new NotRegularFileException(sprintf("Cannot delete '%s' because it is a directory. Use deleteRecursive() instead.", $path)); + } + + // Do not throw exception for non-existent file, but return FALSE to signal + // that no action was taken. + if (!file_exists($path)) { + return FALSE; + } + + // We cannot handle anything other than files and directories. + // Throw an exception for everything else (sockets, symbolic links, etc). + throw new NotRegularFileException(sprintf("The file '%s' is not of a recognized type so it was not deleted.", $path)); + } + + /** + * {@inheritdoc} + */ + public function deleteRecursive($path, callable $callback = NULL) { + if ($callback) { + call_user_func($callback, $path); + } + + if (is_dir($path)) { + $dir = dir($path); + while (($entry = $dir->read()) !== FALSE) { + if ($entry == '.' || $entry == '..') { + continue; + } + $entry_path = $path . '/' . $entry; + $this->deleteRecursive($entry_path, $callback); + } + $dir->close(); + + if ($this->fileSystem->rmdir($path)) { + throw new FileException(sprintf("Failed to remove directory '%s'.", $path)); + } + return TRUE; + } + + return $this->delete($path); + } + + /** + * {@inheritdoc} + */ + public function move($source, $destination = NULL, $replace = self::FILE_EXISTS_RENAME) { + $this->prepare($source, $destination, $replace); + + // Ensure compatibility with Windows. + // @see \Drupal\Core\File\FileSystemInterface::unlink(). + $scheme = $this->fileSystem->uriScheme($source); + if (!$this->fileSystem->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) { + chmod($source, 0600); + } + // Attempt to resolve the URIs. This is necessary in certain configurations + // (see above) and can also permit fast moves across local schemes. + $real_source = $this->fileSystem->realpath($source) ?: $source; + $real_destination = $this->fileSystem->realpath($destination) ?: $destination; + // Perform the move operation. + if (!@rename($real_source, $real_destination)) { + // Fall back to slow copy and unlink procedure. This is necessary for + // renames across schemes that are not local, or where rename() has not been + // implemented. It's not necessary to use drupal_unlink() as the Windows + // issue has already been resolved above. + if (!@copy($real_source, $real_destination)) { + throw new FileWriteException(sprintf("The specified file '%s' could not be moved to '%s'.", $source, $destination)); + } + if (!@unlink($real_source)) { + throw new FileException(sprintf("The source file '%s' could not be unlinked after copying to '%s'.", $source, $destination)); } } @@ -141,4 +233,18 @@ public function prepare($source, &$destination = NULL, $replace = self::FILE_EXI file_ensure_htaccess(); } + /** + * {@inheritdoc} + */ + public function saveData($data, $destination = NULL, $replace = self::FILE_EXISTS_RENAME) { + // Write the data to a temporary file. + $temp_name = $this->fileSystem->tempnam('temporary://', 'file'); + if (file_put_contents($temp_name, $data) === FALSE) { + throw new FileWriteException(sprintf("Tempoarary file '%s' could not be created.", $temp_name)); + } + + // Move the file to its final destination. + return $this->move($temp_name, $destination, $replace); + } + } diff --git a/core/lib/Drupal/Core/File/UnmanagedFileHandlerInterface.php b/core/lib/Drupal/Core/File/UnmanagedFileHandlerInterface.php index 248d6ca..a02e999 100644 --- a/core/lib/Drupal/Core/File/UnmanagedFileHandlerInterface.php +++ b/core/lib/Drupal/Core/File/UnmanagedFileHandlerInterface.php @@ -42,11 +42,120 @@ * @return * The path to the new file. * - * @throws \Drupal\Core\File\Exception\DirectoryNotReadyException * @throws \Drupal\Core\File\Exception\FileException - * @throws \Drupal\Core\File\Exception\FileExistsException - * @throws \Drupal\Core\File\Exception\FileNotExistsException + * Implementation may throw FileException or its subtype on failure. */ - public function copy($source, $destination, $replace = self::FILE_EXISTS_RENAME); + public function copy($source, $destination = NULL, $replace = self::FILE_EXISTS_RENAME); + + /** + * Deletes a file without database changes or hook invocations. + * + * This function should be used when the file to be deleted does not have an + * entry recorded in the files table. + * + * @param $path + * A string containing a file path or (streamwrapper) URI. + * + * @return + * TRUE for success; FALSE if path does not exist. + * + * @throws \Drupal\Core\File\Exception\FileException + * Implementation may throw FileException or its subtype on failure. + */ + public function delete($path); + + /** + * Deletes all files and directories in the specified filepath recursively. + * + * If the specified path is a directory then the function is called + * recursively to process the contents. Once the contents have been removed + * the directory is also removed. + * + * If the specified path is a file then it will be processed with delete() + * method. + * + * Note that this only deletes visible files with write permission. + * + * @param string $path + * A string containing either an URI or a file or directory path. + * @param callable|null $callback + * Callback function to run on each file prior to deleting it and on each + * directory prior to traversing it. For example, can be used to modify + * permissions. + * + * @return + * TRUE for success; FALSE if path does not exist. + * + * @throws \Drupal\Core\File\Exception\FileException + * Implementation may throw FileException or its subtype on failure. + */ + public function deleteRecursive($path, callable $callback = NULL); + + /** + * Moves a file to a new location without database changes or hook invocation. + * + * This is a powerful function that in many ways performs like an advanced + * version of rename(). + * - 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. + * - Works around a PHP bug where rename() does not properly support streams if + * safe_mode or open_basedir are enabled. + * @see https://bugs.php.net/bug.php?id=60456 + * + * @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 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. + * + * @return + * The path to the new file. + * + * @throws \Drupal\Core\File\Exception\FileException + * Implementation may throw FileException or its subtype on failure. + */ + public function move($source, $destination = NULL, $replace = self::FILE_EXISTS_RENAME); + + /** + * Saves a file to the specified destination without invoking file API. + * + * This function is identical to file_save_data() except the file will not be + * saved to the {file_managed} table and none of the file_* hooks will be + * called. + * + * @param $data + * A string containing the contents of the file. + * @param $destination + * A string containing the destination location. This must be a stream wrapper + * URI. If no value is provided, a randomized name will be generated and the + * file will be saved using Drupal's default files scheme, 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. + * + * @return + * A string with the path of the resulting file, or FALSE on error. + * + * @throws \Drupal\Core\File\Exception\FileException + * Implementation may throw FileException or its subtype on failure. + * + * @see file_save_data() + */ + public function saveData($data, $destination = NULL, $replace = self::FILE_EXISTS_RENAME); }