diff --git a/includes/file.inc b/includes/file.inc index de9d17d..c4b5e1e 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -854,7 +854,7 @@ function file_valid_uri($uri) { } /** - * Copies a file to a new location without invoking the file API. + * Copies 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 copy(). @@ -864,10 +864,9 @@ function file_valid_uri($uri) { * - If the $source and $destination are equal, the behavior depends on the * $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME * will rename the file until the $destination is unique. - * - Provides a fallback using realpaths if the move fails using stream - * wrappers. This can occur because PHP's copy() function does not properly - * support streams if safe_mode or open_basedir are enabled. See - * https://bugs.php.net/bug.php?id=60456 + * - Works around a PHP bug where copy() 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. @@ -888,6 +887,54 @@ function file_valid_uri($uri) { * @see file_copy() */ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + if (!file_unmanaged_prepare($source, $destination, $replace)) { + return FALSE; + } + // Attempt to resolve the URIs. This is necessary in certain configurations + // (see above). + $real_source = ($real_source = drupal_realpath($source)) ? $real_source : $source; + $real_destination = ($real_destination = drupal_realpath($destination)) ? $real_destination : $destination; + // Perform the copy operation. + if (!@copy($real_source, $real_destination)) { + watchdog('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination)); + return FALSE; + } + // Set the permissions on the new file. + drupal_chmod($destination); + return $destination; +} + +/** + * 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. + * + * @return + * TRUE, or FALSE in the event of an error. + * + * @see file_unmanaged_copy() + * @see file_unmanaged_move() + */ +function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_EXISTS_RENAME) { $original_source = $source; $original_destination = $destination; @@ -909,7 +956,6 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST $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. @@ -944,20 +990,7 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST } // Make sure the .htaccess files are present. file_ensure_htaccess(); - // Perform the copy operation. - if (!@copy($source, $destination)) { - // 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)) { - watchdog('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination), WATCHDOG_ERROR); - return FALSE; - } - } - - // Set the permissions on the new file. - drupal_chmod($destination); - - return $destination; + return TRUE; } /** @@ -1008,8 +1041,6 @@ function file_destination($destination, $replace) { /** * Moves a file to a new location and update the file's database entry. * - * Moving a file is performed by copying the file to the new location and then - * deleting the original. * - Checks if $source and $destination are valid and readable/writable. * - Performs a file move if $source is not equal to $destination. * - If file already exists in $destination either the call will error out, @@ -1088,12 +1119,24 @@ function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS /** * 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 original file. + * A string specifying the filepath or URI of the source file. * @param $destination - * A string containing the destination that $source should be moved to. - * This must be a stream wrapper URI. If this value is omitted, Drupal's - * default files scheme will be used, usually "public://". + * 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. @@ -1102,16 +1145,37 @@ function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS * - FILE_EXISTS_ERROR - Do nothing and return FALSE. * * @return - * The URI of the moved file, or FALSE in the event of an error. + * The path to the new file, or FALSE in the event of an error. * * @see file_move() */ function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { - $filepath = file_unmanaged_copy($source, $destination, $replace); - if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) { + if (!file_unmanaged_prepare($source, $destination, $replace)) { return FALSE; } - return $filepath; + // 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); + } + // Attempt to resolve the URIs. This is necessary in certain configurations + // (see above) and can also permit fast moves across local schemes. + $real_source = ($real_source = drupal_realpath($source)) ? $real_source : $source; + $real_destination = ($real_destination = drupal_realpath($destination)) ? $real_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)) { + watchdog('file', 'The specified file %file could not be moved to %destination.', array('%file' => $source, '%destination' => $destination)); + return FALSE; + } + } + // Set the permissions on the new file. + drupal_chmod($destination); + return $destination; } /**