? .DS_Store ? .cache ? .git ? .project ? .settings ? empty ? hook_file_6.patch ? logs ? includes/tests/file.test ? sites/all/modules ? sites/default/files ? sites/default/settings.php ? sites/default/test Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.791 diff -u -p -r1.791 common.inc --- includes/common.inc 8 Sep 2008 21:24:30 -0000 1.791 +++ includes/common.inc 9 Sep 2008 02:15:31 -0000 @@ -1815,7 +1815,7 @@ function drupal_build_css_cache($types, $data = implode('', $matches[0]) . $data; // Create the CSS file. - file_save_data($data, $csspath . '/' . $filename, FILE_EXISTS_REPLACE); + file_save_data_plain($data, $csspath . '/' . $filename, FILE_EXISTS_REPLACE); } return $csspath . '/' . $filename; } @@ -1916,7 +1916,7 @@ function _drupal_load_stylesheet($matche * Delete all cached CSS files. */ function drupal_clear_css_cache() { - file_scan_directory(file_create_path('css'), '.*', array('.', '..', 'CVS'), 'file_delete', TRUE); + file_scan_directory(file_create_path('css'), '.*', array('.', '..', 'CVS'), 'file_delete_plain', TRUE); } /** @@ -2279,7 +2279,7 @@ function drupal_build_js_cache($files, $ } // Create the JS file. - file_save_data($contents, $jspath . '/' . $filename, FILE_EXISTS_REPLACE); + file_save_data_plain($contents, $jspath . '/' . $filename, FILE_EXISTS_REPLACE); } return $jspath . '/' . $filename; @@ -2289,7 +2289,7 @@ function drupal_build_js_cache($files, $ * Delete all cached JS files. */ function drupal_clear_js_cache() { - file_scan_directory(file_create_path('js'), '.*', array('.', '..', 'CVS'), 'file_delete', TRUE); + file_scan_directory(file_create_path('js'), '.*', array('.', '..', 'CVS'), 'file_delete_plain', TRUE); variable_set('javascript_parsed', array()); } Index: includes/file.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/file.inc,v retrieving revision 1.130 diff -u -p -r1.130 file.inc --- includes/file.inc 6 Sep 2008 08:36:19 -0000 1.130 +++ includes/file.inc 9 Sep 2008 02:15:32 -0000 @@ -10,6 +10,20 @@ * @defgroup file File interface * @{ * Common file handling functions. + * + * Fields on the file object: + * - fid - Primary Key: Unique files ID. + * - uid - The {users}.uid of the user who is associated with the file. + * - filename - Name of the file with no path components. This may differ + * from the basename of the filepath if the file is renamed to avoid + * overwriting an existing file. + * - filepath - Path of the file relative to Drupal root. + * - filemime - The file's MIME type. + * - filesize - The size of the file in bytes. + * - status - A flag indicatingwhether file is temporary (1) or permanent (0). + * Temporary files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed + * during a cron run. + * - timestamp' UNIX timestamp for the date the file was added to the database. */ /** @@ -29,17 +43,18 @@ define('FILE_DOWNLOADS_PUBLIC', 1); define('FILE_DOWNLOADS_PRIVATE', 2); /** - * Flag used by file_create_directory() -- create directory if not present. + * Flag used by file_check_directory() -- create directory if not present. */ define('FILE_CREATE_DIRECTORY', 1); /** - * Flag used by file_create_directory() -- file permissions may be changed. + * Flag used by file_check_directory() -- file permissions may be changed. */ define('FILE_MODIFY_PERMISSIONS', 2); /** - * Flag for dealing with existing files: Append number until filename is unique. + * Flag for dealing with existing files: Append number until filename is + * unique. */ define('FILE_EXISTS_RENAME', 0); @@ -77,7 +92,8 @@ define('FILE_STATUS_PERMANENT', 1); * @return A string containing a URL that can be used to download the file. */ function file_create_url($path) { - // Strip file_directory_path from $path. We only include relative paths in urls. + // Strip file_directory_path from $path. We only include relative paths in + // URLs. if (strpos($path, file_directory_path() . '/') === 0) { $path = trim(substr($path, strlen(file_directory_path())), '\\/'); } @@ -93,28 +109,30 @@ function file_create_url($path) { * Make sure the destination is a complete path and resides in the file system * directory, if it is not prepend the file system directory. * - * @param $dest A string containing the path to verify. If this value is + * @param $destination A string containing the path to verify. If this value is * omitted, Drupal's 'files' directory will be used. * @return A string containing the path to file, with file system directory * appended if necessary, or FALSE if the path is invalid (i.e. outside the * configured 'files' or temp directories). */ -function file_create_path($dest = 0) { +function file_create_path($destination = NULL) { $file_path = file_directory_path(); - if (!$dest) { + if (!$destination) { return $file_path; } - // file_check_location() checks whether the destination is inside the Drupal files directory. - if (file_check_location($dest, $file_path)) { - return $dest; - } - // check if the destination is instead inside the Drupal temporary files directory. - else if (file_check_location($dest, file_directory_temp())) { - return $dest; + // file_check_location() checks whether the destination is inside the Drupal + // files directory. + if (file_check_location($destination, $file_path)) { + return $destination; + } + // Check if the destination is instead inside the Drupal temporary files + // directory. + else if (file_check_location($destination, file_directory_temp())) { + return $destination; } // Not found, try again with prefixed directory path. - else if (file_check_location($file_path . '/' . $dest, $file_path)) { - return $file_path . '/' . $dest; + else if (file_check_location($file_path . '/' . $destination, $file_path)) { + return $file_path . '/' . $destination; } // File not found. return FALSE; @@ -133,7 +151,7 @@ function file_create_path($dest = 0) { * work, a form error will be set preventing them from saving the settings. * @return FALSE when directory not found, or TRUE when directory exists. */ -function file_check_directory(&$directory, $mode = 0, $form_item = NULL) { +function file_check_directory(&$directory, $mode = FALSE, $form_item = NULL) { $directory = rtrim($directory, '/\\'); // Check if directory exists. @@ -152,9 +170,13 @@ function file_check_directory(&$director // Check to see if the directory is writable. if (!is_writable($directory)) { - if (($mode & FILE_MODIFY_PERMISSIONS) && !@chmod($directory, 0775)) { - form_set_error($form_item, t('The directory %directory is not writable', array('%directory' => $directory))); - watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => $directory), WATCHDOG_ERROR); + // If not able to modify permissions, or if able to, but chmod + // fails, return false. + if (!$mode || (($mode & FILE_MODIFY_PERMISSIONS) && !@chmod($directory, 0775))) { + if ($form_item) { + form_set_error($form_item, t('The directory %directory is not writable', array('%directory' => $directory))); + watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => $directory), WATCHDOG_ERROR); + } return FALSE; } } @@ -176,7 +198,7 @@ function file_check_directory(&$director } /** - * Checks path to see if it is a directory, or a dir/file. + * Checks path to see if it is a directory, or a directory/file. * * @param $path A string containing a file path. This will be set to the * directory's path. @@ -200,9 +222,11 @@ function file_check_path(&$path) { } /** - * Check if a file is really located inside $directory. Should be used to make - * sure a file specified is really located within the directory to prevent - * exploits. + * Check if a file is really located inside $directory. + * + * This Should be used to make sure a file specified is really located within + * the directory to prevent exploits. Note that the file or path being checked + * does not actually need to exist yet. * * @code * // Returns FALSE: @@ -211,7 +235,8 @@ function file_check_path(&$path) { * * @param $source A string set to the file to check. * @param $directory A string where the file should be located. - * @return FALSE for invalid path or the real path of the source. + * @return FALSE if the path does not exist in the directory; + * otherwise, the real path of the source. */ function file_check_location($source, $directory = '') { $check = realpath($source); @@ -219,7 +244,7 @@ function file_check_location($source, $d $source = $check; } else { - // This file does not yet exist + // This file does not yet exist. $source = realpath(dirname($source)) . '/' . basename($source); } $directory = realpath($directory); @@ -230,88 +255,124 @@ function file_check_location($source, $d } /** - * Copies a file to a new location. This is a powerful function that in many ways + * Copy a file to a new location and adds a file record to the database. + * + * This function should be used when manipulating files that have records + * stored in the database. This is a powerful function that in many ways * performs like an advanced version of copy(). - * - Checks if $source and $dest are valid and readable/writable. - * - Performs a file copy if $source is not equal to $dest. - * - If file already exists in $dest either the call will error out, replace the - * file or rename the file based on the $replace parameter. - * - * @param $source A string specifying the file location of the original file. - * This parameter will contain the resulting destination filename in case of - * success. - * @param $dest A string containing the directory $source should be copied to. - * If this value is omitted, Drupal's 'files' directory will be used. - * @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 + * - 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. + * - Adds the new file to the files database. If the source file is a + * temporary file, the resulting file will also be a temporary file. + * @see file_save_upload about temporary files. + * + * @param $source + * A file object. + * @param $destination + * A string containing the directory $source should be copied to. If this + * value is omitted, Drupal's 'files' directory will be used. + * @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 for success, FALSE for failure. + * @return + * File object if the copy is successful, or FALSE in the event of an error. + * + * @see file_copy_plain() */ -function file_copy(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { - $dest = file_create_path($dest); - - $directory = $dest; - $basename = file_check_path($directory); - - // Make sure we at least have a valid directory. - if ($basename === FALSE) { - $source = is_object($source) ? $source->filepath : $source; - drupal_set_message(t('The selected file %file could not be uploaded, because the destination %directory is not properly configured.', array('%file' => $source, '%directory' => $dest)), 'error'); - watchdog('file system', 'The selected file %file could not be uploaded, because the destination %directory could not be found, or because its permissions do not allow the file to be written.', array('%file' => $source, '%directory' => $dest), WATCHDOG_ERROR); - return FALSE; - } - - // Process a file upload object. - if (is_object($source)) { - $file = $source; - $source = $file->filepath; - if (!$basename) { - $basename = $file->filename; +function file_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + if ($filepath = file_copy_plain($source->filepath, $destination, $replace)) { + $file = clone $source; + $file->fid = NULL; + $file->filename = basename($filepath); + $file->filepath = $filepath; + if ($file = file_save($file)) { + // Inform modules that the file has been copied. + module_invoke_all('file_copy', $file, $source); + return $file; } } + return FALSE; +} +/** + * Copy a file to a new location without calling any hooks or making any + * changes to the database. + * + * This is a powerful function that in many ways performs like an advanced + * version of copy(). + * - 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 file location of the original file. + * @param $destination + * A string containing the directory $source should be copied to. If this + * value is omitted, Drupal's 'files' directory will be used. + * @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, or FALSE in the event of an error. + * + * @see file_copy() + */ +function file_copy_plain($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { $source = realpath($source); if (!file_exists($source)) { - drupal_set_message(t('The selected file %file could not be copied, because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $source)), 'error'); + drupal_set_message(t('The specified file %file could not be copied, because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $source)), 'error'); return FALSE; } - // If the destination file is not specified then use the filename of the source file. - $basename = $basename ? $basename : basename($source); - $dest = $directory . '/' . $basename; + $destination = file_create_path($destination); + $directory = $destination; + $basename = file_check_path($directory); - // Make sure source and destination filenames are not the same, makes no sense - // to copy it if they are. In fact copying the file will most likely result in - // a 0 byte file. Which is bad. Real bad. - if ($source != realpath($dest)) { - if (!$dest = file_destination($dest, $replace)) { - drupal_set_message(t('The selected file %file could not be copied, because a file by that name already exists in the destination.', array('%file' => $source)), 'error'); - return FALSE; - } + // Make sure we at least have a valid directory. + if ($basename === FALSE) { + drupal_set_message(t('The specified file %file could not be copied, because the destination %directory is not properly configured.', array('%file' => $source, '%directory' => $destination)), 'error'); + return FALSE; + } - if (!@copy($source, $dest)) { - drupal_set_message(t('The selected file %file could not be copied.', array('%file' => $source)), 'error'); - return FALSE; - } + // If the destination file is not specified then use the filename of the + // source file. + $basename = $basename ? $basename : basename($source); + $destination = file_destination($directory . '/' . $basename, $replace); - // Give everyone read access so that FTP'd users or - // non-webserver users can see/read these files, - // and give group write permissions so group members - // can alter files uploaded by the webserver. - @chmod($dest, 0664); + if ($destination === FALSE) { + drupal_set_message(t('The specified file %file could not be copied because a file by that name already exists in the destination.', array('%file' => $source)), 'error'); + return FALSE; } - - if (isset($file) && is_object($file)) { - $file->filename = $basename; - $file->filepath = $dest; - $source = $file; + // Make sure source and destination filenames are not the same, makes no + // sense to copy it if they are. In fact copying the file will most likely + // result in a 0 byte file. Which is bad. Real bad. + if ($source == realpath($destination)) { + drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error'); + return FALSE; } - else { - $source = $dest; + if (!@copy($source, $destination)) { + drupal_set_message(t('The specified file %file could not be copied.', array('%file' => $source)), 'error'); + return FALSE; } - return TRUE; // Everything went ok. + // Give everyone read access so that FTP'd users or + // non-webserver users can see/read these files, + // and give group write permissions so group members + // can alter files uploaded by the webserver. + @chmod($destination, 0664); + + return $destination; } /** @@ -320,9 +381,9 @@ function file_copy(&$source, $dest = 0, * * @param $destination A string specifying the desired path. * @param $replace Replace behavior when the destination file already exists. - * - FILE_EXISTS_REPLACE - Replace the existing file + * - FILE_EXISTS_REPLACE - Replace the existing file. * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is - * unique + * unique. * - FILE_EXISTS_ERROR - Do nothing and return FALSE. * @return The destination file path or FALSE if the file already exists and * FILE_EXISTS_ERROR was specified. @@ -330,6 +391,10 @@ function file_copy(&$source, $dest = 0, function file_destination($destination, $replace) { if (file_exists($destination)) { switch ($replace) { + case FILE_EXISTS_REPLACE: + // Do nothing here, we want to overwrite the existing file. + break; + case FILE_EXISTS_RENAME: $basename = basename($destination); $directory = dirname($destination); @@ -337,7 +402,7 @@ function file_destination($destination, break; case FILE_EXISTS_ERROR: - drupal_set_message(t('The selected file %file could not be copied, because a file by that name already exists in the destination.', array('%file' => $destination)), 'error'); + drupal_set_message(t('The specified file %file could not be copied, because a file by that name already exists in the destination.', array('%file' => $destination)), 'error'); return FALSE; } } @@ -345,38 +410,76 @@ function file_destination($destination, } /** - * Moves a file to a new location. - * - Checks if $source and $dest are valid and readable/writable. - * - Performs a file move if $source is not equal to $dest. - * - If file already exists in $dest either the call will error out, replace the - * file or rename the file based on the $replace parameter. - * - * @param $source A string specifying the file location of the original file. - * This parameter will contain the resulting destination filename in case of - * success. - * @param $dest A string containing the directory $source should be copied to. - * If this value is omitted, Drupal's 'files' directory will be used. - * @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 + * Move 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, + * replace the file or rename the file based on the $replace parameter. + * - Adds the new file to the files database. + * + * @param $source + * A file object. + * @param $destination + * A string containing the directory $source should be copied to. If this + * value is omitted, Drupal's 'files' directory will be used. + * @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 for success, FALSE for failure. + * @return + * Resulting file object for success, or FALSE in the event of an error. + * + * @see file_move_plain() */ -function file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { - $path_original = is_object($source) ? $source->filepath : $source; - - if (file_copy($source, $dest, $replace)) { - $path_current = is_object($source) ? $source->filepath : $source; - - if ($path_original == $path_current || file_delete($path_original)) { - return TRUE; +function file_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + if ($filepath = file_move_plain($source->filepath, $destination, $replace)) { + $file = clone $source; + $file->filename = basename($filepath); + $file->filepath = $filepath; + if ($file = file_save($file)) { + // Inform modules that the file has been moved. + module_invoke_all('file_move', $file, $source); + return $file; } - drupal_set_message(t('The removal of the original file %file has failed.', array('%file' => $path_original)), 'error'); + drupal_set_message(t('The removal of the original file %file has failed.', array('%file' => $source->filepath)), 'error'); } return FALSE; } /** + * Move a file to a new location without calling any hooks or making any + * changes to the database. + * + * @param $source + * A string specifying the file location of the original file. + * @param $destination + * A string containing the directory $source should be copied to. If this + * value is omitted, Drupal's 'files' directory will be used. + * @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 filepath of the moved file, or FALSE in the event of an error. + * + * @see file_move() + */ +function file_move_plain($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + $filepath = file_copy_plain($source, $destination, $replace); + if ($filepath == FALSE || file_delete_plain($source) == FALSE) { + return FALSE; + } + return $filepath; +} + +/** * Munge the filename as needed for security purposes. For instance the file * name "exploit.php.pps" would become "exploit.php_.pps". * @@ -438,9 +541,9 @@ function file_unmunge_filename($filename * @return */ function file_create_filename($basename, $directory) { - $dest = $directory . '/' . $basename; + $destination = $directory . '/' . $basename; - if (file_exists($dest)) { + if (file_exists($destination)) { // Destination file already exists, generate an alternative. if ($pos = strrpos($basename, '.')) { $name = substr($basename, 0, $pos); @@ -452,45 +555,111 @@ function file_create_filename($basename, $counter = 0; do { - $dest = $directory . '/' . $name . '_' . $counter++ . $ext; - } while (file_exists($dest)); + $destination = $directory . '/' . $name . '_' . $counter++ . $ext; + } while (file_exists($destination)); } - return $dest; + return $destination; +} + +/** + * Delete a file and its database record. + * + * If the $force parameter is not TRUE hook_file_references() will be called + * to determine if the file is being used by any modules. If the file is being + * used is the delete will be cancled. + * + * @param $path + * A file object. + * @param $force + * Boolean indicating that the file should be deleted even if + * hook_file_references() reports that the file is in use. + * @return mixed + * TRUE for success, Array for reference count block, or FALSE in the event + * of an error. + * + * @see hook_file_references() + * @see file_delete_plain() + */ +function file_delete($file, $force = FALSE) { + // If any module returns a value from the reference hook, the file will not + // be deleted from Drupal, but file_delete will return a populated array that + // tests as TRUE. + if (!$force && ($references = module_invoke_all('file_references', $file))) { + return $references; + } + + // Let other modules clean up any references to the deleted file. + module_invoke_all('file_delete', $file); + + // Make sure the file is deleted before removing its row from the + // database, so UIs can still find the file in the database. + if (file_delete_plain($file->filepath)) { + db_query('DELETE FROM {files} WHERE fid = %d', array($file->fid)); + return TRUE; + } + return FALSE; } /** - * Delete a file. + * Delete a file without calling any hooks or making any changes to the + * database. + * + * 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. + * @return + * TRUE for success or path does not exist, or FALSE in the event of an + * error. * - * @param $path A string containing a file path. - * @return TRUE for success, FALSE for failure. + * @see file_delete() */ -function file_delete($path) { +function file_delete_plain($path) { + if (is_dir($path)) { + watchdog('file', t('%path is a directory and cannot be removed with file_delete_plain.', array('%path' => $path)), WATCHDOG_ERROR); + return FALSE; + } if (is_file($path)) { return unlink($path); } + // Return TRUE for non-existant file, but log that nothing was actually + // deleted, as the intended result of file_delete_plain is in fact the + // current state. + if (!file_exists($path)) { + watchdog('file', t('The file %path was not deleted, because it does not exist.', array('%path' => $path)), WATCHDOG_NOTICE); + return TRUE; + } + // Catch all for everything else: sockets, symbolic links, etc. + return FALSE; } /** * Determine total disk space used by a single user or the whole filesystem. * * @param $uid - * An optional user id. A NULL value returns the total space used - * by all files. + * Optional. A user id, specifying NULL returns the total space used by all + * non-temporary files. + * @param $status + * Optional. File Status to return. Combine with a bitwise OR(|) to return + * multiple statuses. The default status is FILE_STATUS_PERMANENT. + * @return + * An integer containing the number of bytes used. */ -function file_space_used($uid = NULL) { +function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) { if (isset($uid)) { - return (int) db_result(db_query('SELECT SUM(filesize) FROM {files} WHERE uid = %d', $uid)); + return (int)db_result(db_query('SELECT SUM(filesize) FROM {files} WHERE uid = %d AND status & %d', array($uid, $status))); } - return (int) db_result(db_query('SELECT SUM(filesize) FROM {files}')); + return (int)db_result(db_query('SELECT SUM(filesize) FROM {files} WHERE status & %d', array($status))); } /** * Saves a file upload to a new location. The source file is validated as a * proper upload and handled as such. * - * The file will be added to the files table as a temporary file. Temporary files - * are periodically cleaned. To make the file permanent file call + * The file will be added to the files table as a temporary file. Temporary + * files are periodically cleaned. To make the file permanent file call * file_set_status() to change its status. * * @param $source @@ -502,7 +671,7 @@ function file_space_used($uid = NULL) { * functions should return an array of error messages, an empty array * indicates that the file passed validation. The functions will be called in * the order specified. - * @param $dest + * @param $destination * A string containing the directory $source should be copied to. If this is * not provided or is not writable, the temporary directory will be used. * @param $replace @@ -510,25 +679,27 @@ function file_space_used($uid = NULL) { * destination directory should overwritten. A false value will generate a * new, unique filename in the destination directory. * @return - * An object containing the file information, or FALSE in the event of an error. + * An object containing the file information, or FALSE in the event of an + * error. */ -function file_save_upload($source, $validators = array(), $dest = FALSE, $replace = FILE_EXISTS_RENAME) { +function file_save_upload($source, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) { global $user; static $upload_cache; - // Add in our check of the the file name length. - $validators['file_validate_name_length'] = array(); - // Return cached objects without processing since the file will have // already been processed and the paths in _FILES will be invalid. if (isset($upload_cache[$source])) { return $upload_cache[$source]; } + // Add in our check of the the file name length. + $validators['file_validate_name_length'] = array(); + + // If a file was uploaded, process it. if (isset($_FILES['files']) && $_FILES['files']['name'][$source] && is_uploaded_file($_FILES['files']['tmp_name'][$source])) { - // Check for file upload errors and return FALSE if a - // lower level system error occurred. + // Check for file upload errors and return FALSE if a lower level system + // error occurred. switch ($_FILES['files']['error'][$source]) { // @see http://php.net/manual/en/features.file-upload.errors.php case UPLOAD_ERR_OK: @@ -560,9 +731,12 @@ function file_save_upload($source, $vali // Begin building file object. $file = new stdClass(); + $file->uid = $user->uid; + $file->status = FILE_STATUS_TEMPORARY; $file->filename = file_munge_filename(trim(basename($_FILES['files']['name'][$source]), '.'), $extensions); $file->filepath = $_FILES['files']['tmp_name'][$source]; $file->filemime = file_get_mimetype($file->filename); + $file->filesize = $_FILES['files']['size'][$source]; // Rename potentially executable files, to help prevent exploits. if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file->filename) && (substr($file->filename, -4) != '.txt')) { @@ -573,26 +747,21 @@ function file_save_upload($source, $vali // If the destination is not provided, or is not writable, then use the // temporary directory. - if (empty($dest) || file_check_path($dest) === FALSE) { - $dest = file_directory_temp(); + if (empty($destination) || file_check_path($destination) === FALSE) { + $destination = file_directory_temp(); } $file->source = $source; - $file->destination = file_destination(file_create_path($dest . '/' . $file->filename), $replace); - $file->filesize = $_FILES['files']['size'][$source]; + $file->destination = file_destination(file_create_path($destination . '/' . $file->filename), $replace); - // Call the validation functions. - $errors = array(); - foreach ($validators as $function => $args) { - array_unshift($args, $file); - $errors = array_merge($errors, call_user_func_array($function, $args)); - } + // Call the validation functions specified by this function's caller. + $errors = file_validate($file, $validators); - // Check for validation errors. + // Check for errors. if (!empty($errors)) { - $message = t('The selected file %name could not be uploaded.', array('%name' => $file->filename)); + $message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename)); if (count($errors) > 1) { - $message .= ''; + $message .= theme('item_list', $errors); } else { $message .= ' ' . array_pop($errors); @@ -601,8 +770,9 @@ function file_save_upload($source, $vali return FALSE; } - // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary directory. - // This overcomes open_basedir restrictions for future file operations. + // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary + // directory. This overcomes open_basedir restrictions for future file + // operations. $file->filepath = $file->destination; if (!move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->filepath)) { form_set_error($source, t('File upload error. Could not move uploaded file.')); @@ -610,15 +780,11 @@ function file_save_upload($source, $vali return FALSE; } - // If we made it this far it's safe to record this file in the database. - $file->uid = $user->uid; - $file->status = FILE_STATUS_TEMPORARY; - $file->timestamp = $_SERVER['REQUEST_TIME']; - drupal_write_record('files', $file); - - // Add file to the cache. - $upload_cache[$source] = $file; - return $file; + if ($file = file_save($file)) { + // Add file to the cache. + $upload_cache[$source] = $file; + return $file; + } } return FALSE; } @@ -634,6 +800,9 @@ function file_save_upload($source, $vali function file_validate_name_length($file) { $errors = array(); + if (empty($file->filename)) { + $errors[] = t('Its name is empty. Please give a name to the file.'); + } if (strlen($file->filename) > 255) { $errors[] = t('Its name exceeds the 255 characters limit. Please rename the file and try again.'); } @@ -641,27 +810,24 @@ function file_validate_name_length($file } /** - * Check that the filename ends with an allowed extension. This check is not - * enforced for the user #1. + * Check that the filename ends with an allowed extension. * * @param $file * A Drupal file object. * @param $extensions * A string with a space separated * @return - * An array. If the file extension is not allowed, it will contain an error message. + * An array. If the file extension is not allowed, it will contain an error + * message. */ function file_validate_extensions($file, $extensions) { global $user; $errors = array(); - // Bypass validation for uid = 1. - if ($user->uid != 1) { - $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i'; - if (!preg_match($regex, $file->filename)) { - $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions)); - } + $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i'; + if (!preg_match($regex, $file->filename)) { + $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions)); } return $errors; } @@ -676,10 +842,11 @@ function file_validate_extensions($file, * An integer specifying the maximum file size in bytes. Zero indicates that * no limit should be enforced. * @param $$user_limit - * An integer specifying the maximum number of bytes the user is allowed. Zero - * indicates that no limit should be enforced. + * An integer specifying the maximum number of bytes the user is allowed. + * Zero indicates that no limit should be enforced. * @return - * An array. If the file size exceeds limits, it will contain an error message. + * An array. If the file size exceeds limits, it will contain an error + * message. */ function file_validate_size($file, $file_limit = 0, $user_limit = 0) { global $user; @@ -724,15 +891,16 @@ function file_validate_is_image(&$file) * maximum and minimum dimensions. Non-image files will be ignored. * * @param $file - * A Drupal file object. This function may resize the file affecting its size. + * A Drupal file object. This function may resize the file affecting its + * size. * @param $maximum_dimensions * An optional string in the form WIDTHxHEIGHT e.g. '640x480' or '85x85'. If * an image toolkit is installed the image will be resized down to these * dimensions. A value of 0 indicates no restriction on size, so resizing * will be attempted. * @param $minimum_dimensions - * An optional string in the form WIDTHxHEIGHT. This will check that the image - * meets a minimum size. A value of 0 indicates no restriction. + * An optional string in the form WIDTHxHEIGHT. This will check that the + * image meets a minimum size. A value of 0 indicates no restriction. * @return * An array. If the file is an image and did not meet the requirements, it * will contain an error message. @@ -774,47 +942,100 @@ function file_validate_image_resolution( } /** - * Save a string to the specified destination. + * Save a string to the specified destination and create a database file entry. * - * @param $data A string containing the contents of the file. - * @param $dest A string containing the destination location. - * @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 + * @param $data + * A string containing the contents of the file. + * @param $destination + * A string containing the destination location. If no value is provided + * then a randomly name will be generated and the file saved in Drupal's + * files directory. + * @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 + * An file object or FALSE on error * - * @return A string containing the resulting filename or FALSE on error + * @see file_save_data_plain() */ -function file_save_data($data, $dest, $replace = FILE_EXISTS_RENAME) { - $temp = file_directory_temp(); - // On Windows, tempnam() requires an absolute path, so we use realpath(). - $file = tempnam(realpath($temp), 'file'); - if (!$fp = fopen($file, 'wb')) { - drupal_set_message(t('The file could not be created.'), 'error'); - return FALSE; +function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + global $user; + + if ($filepath = file_save_data_plain($data, $destination, $replace)) { + // Create a file object. + $file = new stdClass(); + $file->filepath = $filepath; + $file->filename = basename($file->filepath); + $file->filemime = file_get_mimetype($file->filepath); + $file->uid = $user->uid; + $file->status = FILE_STATUS_PERMANENT; + return file_save($file); } - fwrite($fp, $data); - fclose($fp); + return FALSE; +} - if (!file_move($file, $dest, $replace)) { +/** + * Save a string to the specified destination without calling any hooks or + * making any changes to the database. + * + * This function is identical to file_save_data() except the file will not be + * saved to the files 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. If no value is provided + * then a randomly name will be generated and the file saved in Drupal's + * files directory. + * @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. + * + * @see file_save_data() + */ +function file_save_data_plain($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + global $user; + + // Write the data to a temporary file. + $temp_name = tempnam(file_directory_temp(), 'file'); + if (file_put_contents($temp_name, $data) === FALSE) { + drupal_set_message(t('The file could not be created.'), 'error'); return FALSE; } - return $file; + // Move the file to its final destination. + return file_move_plain($temp_name, $destination, $replace); } /** * Set the status of a file. * - * @param file A Drupal file object - * @param status A status value to set the file to. - * @return FALSE on failure, TRUE on success and $file->status will contain the - * status. + * @param $file + * A Drupal file object. + * @param $status + * A status value to set the file to. + * - FILE_STATUS_TEMPORARY - A temporary file that Drupal's garbage + * collection will remove. + * - FILE_STATUS_PERMANENT - A permanent file that Drupal's garbage + * collection will not remove. + * @return + * File object if the change is successful, or FALSE in the event of an + * error. */ -function file_set_status(&$file, $status) { - if (db_query('UPDATE {files} SET status = %d WHERE fid = %d', $status, $file->fid)) { +function file_set_status($file, $status = FILE_STATUS_PERMANENT) { + if (db_query('UPDATE {files} SET status = %d WHERE fid = %d', array($status, $file->fid))) { $file->status = $status; - return TRUE; + // Notify other modules that the file's status has changed. + module_invoke_all('file_status', $file); + return $file; } return FALSE; } @@ -823,8 +1044,10 @@ function file_set_status(&$file, $status * Transfer file using http to client. Pipes a file through Drupal to the * client. * - * @param $source File to transfer. - * @param $headers An array of http headers to send along with file. + * @param $source + * String specifying the file path to transfer. + * @param $headers + * An array of http headers to send along with file. */ function file_transfer($source, $headers) { ob_end_clean(); @@ -853,6 +1076,8 @@ function file_transfer($source, $headers } /** + * Menu handler private file transfers. + * * Call modules that implement hook_file_download() to find out if a file is * accessible and what headers it should be transferred with. If a module * returns -1 drupal_access_denied() will be returned. If one or more modules @@ -870,6 +1095,7 @@ function file_download() { } if (file_exists(file_create_path($filepath))) { + // Let other modules provide headers and controls access to the file. $headers = module_invoke_all('file_download', $filepath); if (in_array(-1, $headers)) { return drupal_access_denied(); @@ -907,7 +1133,8 @@ function file_download() { * @param $min_depth * Minimum depth of directories to return files from. * @param $depth - * Current depth of recursion. This parameter is only used internally and should not be passed. + * Current depth of recursion. This parameter is only used internally and + * should not be passed. * * @return * An associative array (keyed on the provided key) of objects with @@ -926,7 +1153,8 @@ function file_scan_directory($dir, $mask $files = array_merge(file_scan_directory("$dir/$file", $mask, $nomask, $callback, $recurse, $key, $min_depth, $depth + 1), $files); } elseif ($depth >= $min_depth && ereg($mask, $file)) { - // Always use this match over anything already set in $files with the same $$key. + // Always use this match over anything already set in $files with the + // same $$key. $filename = "$dir/$file"; $basename = basename($file); $name = substr($basename, 0, strrpos($basename, '.')); @@ -965,13 +1193,11 @@ function file_directory_temp() { // Operating system specific dirs. if (substr(PHP_OS, 0, 3) == 'WIN') { - $directories[] = 'c:\\windows\\temp'; - $directories[] = 'c:\\winnt\\temp'; - $path_delimiter = '\\'; + $directories[] = 'c:/windows/temp'; + $directories[] = 'c:/winnt/temp'; } else { $directories[] = '/tmp'; - $path_delimiter = '/'; } foreach ($directories as $directory) { @@ -980,8 +1206,8 @@ function file_directory_temp() { } } - // if a directory has been found, use it, otherwise default to 'files/tmp' or 'files\\tmp'; - $temporary_directory = $temporary_directory ? $temporary_directory : file_directory_path() . $path_delimiter . 'tmp'; + // if a directory has been found, use it, otherwise default to 'files/tmp' + $temporary_directory = $temporary_directory ? $temporary_directory : file_directory_path() . '/tmp'; variable_set('file_directory_temp', $temporary_directory); } @@ -1001,7 +1227,8 @@ function file_directory_path() { * Determine the maximum file upload size by querying the PHP settings. * * @return - * A file size limit in bytes based on the PHP upload_max_filesize and post_max_size + * A file size limit in bytes based on the PHP upload_max_filesize and + * post_max_size */ function file_upload_max_size() { static $max_size = -1; @@ -1387,5 +1614,111 @@ function file_get_mimetype($filename, $m } /** + * Load a file object from the database. + * + * @param $param + * Either the id of a file or an array of conditions to match against in the + * database query + * @param $reset + * Whether to reset the internal file_load cache. + * @return + * A file object. + */ +function file_load($param, $reset = NULL) { + static $files = array(); + + if ($reset) { + $files = array(); + } + + $arguments = array(); + if (is_numeric($param)) { + if (isset($files[(string) $param])) { + return is_object($files[$param]) ? clone $files[$param] : $files[$param]; + } + $cond = 'f.fid = %d'; + $arguments[] = $param; + } + elseif (is_array($param)) { + // Turn the conditions into a query. + $cond = array(); + foreach ($param as $key => $value) { + $cond[] = 'f.' . db_escape_table($key) . " = '%s'"; + $arguments[] = $value; + } + $cond = implode(' AND ', $cond); + } + else { + return FALSE; + } + $file = db_fetch_object(db_query('SELECT f.* FROM {files} f WHERE ' . $cond, $arguments)); + + if ($file && $file->fid) { + // Allow modules to add or change the file object. + module_invoke_all('file_load', $file); + + // Cache the fully loaded value. + $files[(string) $file->fid] = clone $file; + } + + return $file; +} + +/** + * Check that a file meets the criteria specified by the validators. + * + * @param $file + * A Drupal file object. + * @param $validators + * An optional, associative array of callback functions used to validate the + * file. The keys are function names and the values arrays of callback + * parameters which will be passed in after the user and file objects. The + * functions should return an array of error messages, an empty array + * indicates that the file passed validation. The functions will be called in + * the order specified. + * @return + * An array contaning validation error messages. + */ +function file_validate(&$file, $validators = array()) { + // Call the validation functions specified by this function's caller. + $errors = array(); + foreach ($validators as $function => $args) { + array_unshift($args, $file); + $errors = array_merge($errors, call_user_func_array($function, $args)); + } + + // Let other modules perform validation on the new file. + return array_merge($errors, module_invoke_all('file_validate', $file)); +} +/** + * Save a file object to the database. + * + * If the $file->fid is not set a new record will be added. Re-saving an + * existing file will not change its status. + * + * @param $file + * A file object returned by file_load(). + * @return + * The updated file object. + */ +function file_save($file) { + $file->timestamp = $_SERVER['REQUEST_TIME']; + $file->filesize = filesize($file->filepath); + + if (empty($file->fid)) { + drupal_write_record('files', $file); + // Inform modules about the newly added file. + module_invoke_all('file_insert', $file); + } + else { + drupal_write_record('files', $file, 'fid'); + // Inform modules that the file has been updated. + module_invoke_all('file_update', $file); + } + + return $file; +} + +/** * @} End of "defgroup file". */ Index: includes/locale.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/locale.inc,v retrieving revision 1.180 diff -u -p -r1.180 locale.inc --- includes/locale.inc 21 Aug 2008 19:36:36 -0000 1.180 +++ includes/locale.inc 9 Sep 2008 02:15:32 -0000 @@ -2091,11 +2091,11 @@ function _locale_rebuild_js($langcode = // Construct the array for JavaScript translations. // We sort on plural so that we have all plural forms before singular forms. - $result = db_query("SELECT s.lid, s.source, t.plid, t.plural, t.translation - FROM {locales_source} s - LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language - WHERE s.location LIKE '%.js%' - AND s.textgroup = 'default' + $result = db_query("SELECT s.lid, s.source, t.plid, t.plural, t.translation + FROM {locales_source} s + LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language + WHERE s.location LIKE '%.js%' + AND s.textgroup = 'default' ORDER BY t.plural DESC", array(':language' => $language->language)); $translations = $plurals = array(); @@ -2158,7 +2158,7 @@ function _locale_rebuild_js($langcode = // Save the file. $dest = $dir . '/' . $language->language . '_' . $data_hash . '.js'; - if (file_save_data($data, $dest)) { + if (file_save_data_plain($data, $dest)) { $language->javascript = $data_hash; $status = ($status == 'deleted') ? 'updated' : 'created'; } Index: modules/aggregator/aggregator.test =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.test,v retrieving revision 1.7 diff -u -p -r1.7 aggregator.test --- modules/aggregator/aggregator.test 3 Sep 2008 19:25:08 -0000 1.7 +++ modules/aggregator/aggregator.test 9 Sep 2008 02:15:32 -0000 @@ -151,7 +151,7 @@ class AggregatorTestCase extends DrupalW EOF; $path = file_directory_path() . '/valid-opml.xml'; - file_save_data($opml, $path); + file_save_data_plain($opml, $path); return $path; } @@ -169,7 +169,7 @@ EOF; EOF; $path = file_directory_path() . '/invalid-opml.xml'; - file_save_data($opml, $path); + file_save_data_plain($opml, $path); return $path; } @@ -192,7 +192,7 @@ EOF; EOF; $path = file_directory_path() . '/empty-opml.xml'; - file_save_data($opml, $path); + file_save_data_plain($opml, $path); return $path; } @@ -226,7 +226,7 @@ EOF; EOT; $path = file_directory_path() . '/rss091.xml'; - file_save_data($feed, $path); + file_save_data_plain($feed, $path); return $path; } } Index: modules/blogapi/blogapi.module =================================================================== RCS file: /cvs/drupal/drupal/modules/blogapi/blogapi.module,v retrieving revision 1.123 diff -u -p -r1.123 blogapi.module --- modules/blogapi/blogapi.module 6 Sep 2008 08:36:19 -0000 1.123 +++ modules/blogapi/blogapi.module 9 Sep 2008 02:15:32 -0000 @@ -385,12 +385,12 @@ function blogapi_metaweblog_new_media_ob return blogapi_error(t('No file sent.')); } - if (!$file = file_save_data($data, $name)) { + if (!$filepath = file_save_data_plain($data, $name)) { return blogapi_error(t('Error storing file.')); } // Return the successful result. - return array('url' => file_create_url($file), 'struct'); + return array('url' => file_create_url($filepath), 'struct'); } /** * Blogging API callback. Returns a list of the taxonomy terms that can be Index: modules/color/color.module =================================================================== RCS file: /cvs/drupal/drupal/modules/color/color.module,v retrieving revision 1.43 diff -u -p -r1.43 color.module --- modules/color/color.module 31 Aug 2008 09:15:12 -0000 1.43 +++ modules/color/color.module 9 Sep 2008 02:15:32 -0000 @@ -308,7 +308,7 @@ function color_scheme_form_submit($form, foreach ($info['copy'] as $file) { $base = basename($file); $source = $paths['source'] . $file; - file_copy($source, $paths['target'] . $base); + file_copy_plain($source, $paths['target'] . $base); $paths['map'][$file] = $base; $paths['files'][] = $paths['target'] . $base; } @@ -435,7 +435,7 @@ function _color_rewrite_stylesheet($them * Save the rewritten stylesheet to disk. */ function _color_save_stylesheet($file, $style, &$paths) { - file_save_data($style, $file, FILE_EXISTS_REPLACE); + file_save_data_plain($style, $file, FILE_EXISTS_REPLACE); $paths['files'][] = $file; // Set standard file permissions for webserver-generated files. Index: modules/simpletest/simpletest.install =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.install,v retrieving revision 1.7 diff -u -p -r1.7 simpletest.install --- modules/simpletest/simpletest.install 16 Aug 2008 20:57:14 -0000 1.7 +++ modules/simpletest/simpletest.install 9 Sep 2008 02:15:32 -0000 @@ -33,7 +33,7 @@ function simpletest_install() { $original = drupal_get_path('module', 'simpletest') . '/files'; $files = file_scan_directory($original, '(html|image|javascript|php|sql)-.*'); foreach ($files as $file) { - file_copy($file->filename, $path . '/' . $file->basename); + file_copy_plain($file->filename, $path . '/' . $file->basename); } $generated = TRUE; } Index: modules/simpletest/simpletest.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.module,v retrieving revision 1.11 diff -u -p -r1.11 simpletest.module --- modules/simpletest/simpletest.module 21 Aug 2008 19:36:38 -0000 1.11 +++ modules/simpletest/simpletest.module 9 Sep 2008 02:15:32 -0000 @@ -558,7 +558,7 @@ function simpletest_clean_temporary_dire simpletest_clean_temporary_directory($file_path); } else { - file_delete($file_path); + file_delete_plain($file_path); } } } Index: modules/simpletest/tests/file.test =================================================================== RCS file: modules/simpletest/tests/file.test diff -N modules/simpletest/tests/file.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/file.test 9 Sep 2008 02:15:32 -0000 @@ -0,0 +1,772 @@ + t('File validation'), + 'description' => t('Tests the functions used to validate uploaded files.'), + 'group' => t('File'), + ); + } + + /** + * Implementation of setUp(). + */ + function setUp() { + parent::setUp('file_test'); + + $this->image = new stdClass(); + $this->image->filepath = 'misc/druplicon.png'; + $this->iamge->filename = basename($this->image->filepath); + + $this->non_image = new stdClass(); + $this->non_image->filepath = 'misc/jquery.js'; + $this->non_image->filename = basename($this->non_image->filepath); + } + + function testFileValidate() { + // Empty validators + $this->assertEqual(file_validate($this->image, array()), array(), t('Validating an empty array works succesfully.')); + + // Use the file_test.module's test validator to ensure that passing tests + // return correctly. + $passing = array('file_test_validator' => array(array())); + $this->assertEqual(file_validate($this->image, $passing), array(), t('Validating passes.')); + + $failing = array('file_test_validator' => array(array('Failed', 'Badly'))); + $this->assertEqual(file_validate($this->image, $failing), array('Failed', 'Badly'), t('Validating returns errors.')); + } + + /** + * Test the file_validate_extensions() function. + */ + function testFileValidateExtensions() { + $file = new stdClass(); + $file->filename = 'asdf.txt'; + $errors = file_validate_extensions($file, 'asdf txt pork'); + $this->assertEqual(count($errors), 0, t("Valid extension accepted."), 'File'); + + $file->filename = 'asdf.txt'; + $errors = file_validate_extensions($file, 'exe png'); + $this->assertEqual(count($errors), 1, t("Invalid extension blocked."), 'File'); + } + + /** + * This ensures a specific file is actually an image. + */ + function testFileValidateIsImage() { + $this->assertTrue(file_exists($this->image->filepath), t('The image being tested exists.'), 'File'); + $errors = file_validate_is_image($this->image); + $this->assertEqual(count($errors), 0, t("No error reported for our image file."), 'File'); + + $this->assertTrue(file_exists($this->non_image->filepath), t('The non-image being tested exists.'), 'File'); + $errors = file_validate_is_image($this->non_image); + $this->assertEqual(count($errors), 1, t("An error reported for our non-image file."), 'File'); + } + + /** + * This ensures the resolution of a specific file is within bounds. + * The image will be resized if it's too large. + */ + function testFileValidateImageResolution() { + // Non-images + $errors = file_validate_image_resolution($this->non_image); + $this->assertEqual(count($errors), 0, t("Shouldn't get any errors for a non-image file."), 'File'); + $errors = file_validate_image_resolution($this->non_image, '50x50', '100x100'); + $this->assertEqual(count($errors), 0, t("Don't check the resolution on non files."), 'File'); + + // Minimum size + $errors = file_validate_image_resolution($this->image); + $this->assertEqual(count($errors), 0, t("No errors for an image when there is no minimum or maximum resolution."), 'File'); + $errors = file_validate_image_resolution($this->image, 0, '200x1'); + $this->assertEqual(count($errors), 1, t("Got an error for an image that wasn't wide enough"), 'File'); + $errors = file_validate_image_resolution($this->image, 0, '1x200'); + $this->assertEqual(count($errors), 1, t("Got an error for an image that wasn't tall enough"), 'File'); + $errors = file_validate_image_resolution($this->image, 0, '200x200'); + $this->assertEqual(count($errors), 1, t("Small images report an error."), 'File'); + + // Maximum size + if (image_get_toolkit()) { + // Copy the image so that the original doesn't get resized. + $temp_dir = file_directory_temp(); + copy(realpath('misc/druplicon.png'), realpath($temp_dir) .'/druplicon.png'); + $this->image->filepath = $temp_dir .'/druplicon.png'; + + $errors = file_validate_image_resolution($this->image, '10x5'); + $this->assertEqual(count($errors), 0, t("No errors should be reported when an oversized image can be scaled down."), 'File'); + + $info = image_get_info($this->image->filepath); + $this->assertTrue($info['width'] <= 10, t("Image scaled to correct width."), 'File'); + $this->assertTrue($info['height'] <= 5, t("Image scaled to correct height."), 'File'); + + unlink(realpath($temp_dir .'/druplicon.png')); + } + else { + // TODO: should check that the error is returned if no toolkit is available. + $errors = file_validate_image_resolution($this->image, '5x10'); + $this->assertEqual(count($errors), 1, t("Oversize images that can't be scaled get an error."), 'File'); + } + + // Clear out any resizing messages. +# drupal_get_messages(); + } + + /** + * This will ensure the filename length is valid. + */ + function testFileValidateNameLength() { + // Create a new file object. + $file = new stdClass(); + + // Add a filename with an allowed length and test it. + $file->filename = str_repeat('x', 255); + $this->assertEqual(strlen($file->filename), 255); + $errors = file_validate_name_length($file); + $this->assertEqual(count($errors), 0, t('No errors reported for 255 length filename.'), 'File'); + + // Add a filename with a length too long and test it. + $file->filename = str_repeat('x', 256); + $errors = file_validate_name_length($file); + $this->assertEqual(count($errors), 1, t('An error reported for 256 length filename.'), 'File'); + + // Add a filename with an empty string and test it. + $file->filename = ''; + $errors = file_validate_name_length($file); + $this->assertEqual(count($errors), 1, t('An error reported for 0 length filename.'), 'File'); + } + + + /** + * Test file_validate_size(). + */ + function testFileValidateSize() { + global $user; + $original_user = $user; + session_save_session(FALSE); + + // Run these test as uid = 1 + $user = user_load(array('uid' => 1)); + + $file = new stdClass(); + $file->filesize = 999999; + $errors = file_validate_size($file, 1, 1); + $this->assertEqual(count($errors), 0, t("No size limits enforced on uid=1."), 'File'); + + + // Run these test as a regular user + $user = $this->drupalCreateUser(); + + $file = new stdClass(); + $file->filesize = 1000; + $errors = file_validate_size($file, 0, 0); + $this->assertEqual(count($errors), 0, t("No limits means no errors."), 'File'); + $errors = file_validate_size($file, 1, 0); + $this->assertEqual(count($errors), 1, t("Error for the file being over the limit."), 'File'); + $errors = file_validate_size($file, 0, 1); + $this->assertEqual(count($errors), 1, t("Error for the user being over their limit."), 'File'); + $errors = file_validate_size($file, 1, 1); + $this->assertEqual(count($errors), 2, t("Errors for both the file and their limit."), 'File'); + + + $user = $original_user; + session_save_session(TRUE); + } +} + + +/** + * This will run tests against file validation. + * + */ +class FileLoadSaveTest extends DrupalWebTestCase { + /** + * Implementation of getInfo(). + */ + function getInfo() { + return array( + 'name' => t('File loading and saving'), + 'description' => t('Tests the file loading and saving functions.'), + 'group' => t('File'), + ); + } + + /** + * Implementation of setUp(). + */ + function setUp() { + parent::setUp('file_test'); + } + + /** + * This will test saving file data to the database. + */ + function testFileLoadSave() { + // Create a new file object. + $file = array( + 'uid' => 1, + 'filename' => 'druplicon.png', + 'filepath' => 'misc/druplicon.png', + 'filemime' => 'image/png', + 'timestamp' => 1, + 'status' => FILE_STATUS_PERMANENT, + ); + $this->file = (object) $file; + + // Try to load bogus stuff + file_test_reset_calls(); + $this->assertFalse(file_load(-1), t("Try to load an invalid fid")); + $this->assertEqual(count(file_test_get_calls('load')), 0, t('hook_file_load was not called.')); + file_test_reset_calls(); + $this->assertFalse(file_load(array('filepath' => $this->file->filepath)), t("Try to load a file that doesn't exist in the database.")); + $this->assertEqual(count(file_test_get_calls('load')), 0, t('hook_file_load was not called.')); + + // Save it, inserting a new record + file_test_reset_calls(); + $saved_file = file_save($this->file); + $this->assertEqual(count(file_test_get_calls('insert')), 1, t('hook_file_insert was called.')); + $this->assertNotNull($saved_file, t("Saving the file should give us back a file object."), 'File'); + $this->assertTrue($saved_file->fid > 0, t("A new file ID is set when saving a new file to the database."), 'File'); + $this->assertEqual(db_result(db_query('SELECT COUNT(*) FROM {files} f WHERE f.fid = %d', array($saved_file->fid))), 1, t("Record exists in the database.")); + $this->assertEqual($saved_file->filesize, filesize($this->file->filepath), t("File size was set correctly."), 'File'); + $this->assertTrue($saved_file->timestamp > 1, t("File size was set correctly."), 'File'); + + // Load by path + file_test_reset_calls(); + $by_path_file = file_load(array('filepath' => $this->file->filepath)); + $this->assertEqual(count(file_test_get_calls('load')), 1, t('hook_file_load was called.')); + $this->assertTrue($by_path_file->file_test['loaded'], t('file_test_file_load() was able to modify the file during load.')); + $this->assertEqual($by_path_file->fid, $this->file->fid, t("Loading by filepath got the correct fid."), 'File'); + + // Load by fid. + file_test_reset_calls(); + $by_fid_file = file_load($this->file->fid); + $this->assertEqual(count(file_test_get_calls('load')), 0, t('hook_file_load was not called because the loaded file was cached.')); + $this->assertTrue($by_fid_file->file_test['loaded'], t('file_test_file_load() was able to modify the file during load.')); + $this->assertEqual($by_fid_file->filepath, $this->file->filepath, t("Loading by fid got the correct filepath."), 'File'); + + // Load again by fid to make sure the caching doesn't screw anythign up + file_test_reset_calls(); + $by_fid_file = file_load($this->file->fid); + $this->assertEqual(count(file_test_get_calls('load')), 0, t('hook_file_load was not called since it loaded from cache.')); + $this->assertEqual($by_fid_file->filepath, $this->file->filepath, t("Loading by fid got the correct filepath."), 'File'); + + // Resave the file, updating the existing record. + file_test_reset_calls(); + $resaved_file = file_save($saved_file); + $this->assertEqual(count(file_test_get_calls('update')), 1, t('hook_file_update was called.')); + $this->assertEqual($resaved_file->fid, $saved_file->fid, t("The file ID of an existing file is not changed when updating the database."), 'File'); + $this->assertTrue($resaved_file->timestamp >= $saved_file->timestamp, t("Timestamp didn't go backwards."), 'File'); + $count = db_result(db_query('SELECT COUNT(*) FROM {files} f WHERE f.fid = %d', array($saved_file->fid))); + $this->assertEqual($count, 1, t("Record still exists in the database."), 'File'); + + // TODO: test the reset parameter + } + + function testFileSaveDataPlain() { + $contents = $this->randomName(8); + + // No filename + $filepath = file_save_data_plain($contents); + $this->assertTrue($filepath, t("Unnamed file saved correctly")); + $this->assertEqual(file_directory_path(), dirname($filepath), t("File was placed in Drupal's files directory")); + $this->assertEqual($contents, file_get_contents(realpath($filepath)), t("Contents of the file are correct.")); + + // Provide a filename + $filepath = file_save_data_plain($contents, 'asdf.txt', FILE_EXISTS_REPLACE); + $this->assertTrue($filepath, t("Unnamed file saved correctly")); + $this->assertEqual(file_directory_path(), dirname($filepath), t("File was placed in Drupal's files directory.")); + $this->assertEqual('asdf.txt', basename($filepath), t("File was named correctly.")); + $this->assertEqual($contents, file_get_contents(realpath($filepath)), t("Contents of the file are correct.")); + } + + function testFileSaveData() { + $contents = $this->randomName(8); + + // No filename + $file = file_save_data($contents); + $this->assertTrue($file, t("Unnamed file saved correctly")); + $this->assertEqual(file_directory_path(), dirname($file->filepath), t("File was placed in Drupal's files directory")); + $this->assertEqual($contents, file_get_contents(realpath($file->filepath)), t("Contents of the file are correct.")); + $this->assertEqual($file->filemime, 'application/octet-stream', t("A MIME type was set.")); + $this->assertEqual($file->status, FILE_STATUS_PERMANENT, t("The file's status was set to permanent.")); + + // Try loading the file. + $loaded_file = file_load($file->fid); + $this->assertTrue($loaded_file, t("File loaded from database.")); + + // Provide a filename + $file = file_save_data($contents, 'asdf.txt', FILE_EXISTS_REPLACE); + $this->assertTrue($file, t("Unnamed file saved correctly")); + $this->assertEqual(file_directory_path(), dirname($file->filepath), t("File was placed in Drupal's files directory.")); + $this->assertEqual('asdf.txt', basename($file->filepath), t("File was named correctly.")); + $this->assertEqual($contents, file_get_contents(realpath($file->filepath)), t("Contents of the file are correct.")); + + // Check the overwrite error. + $file = file_save_data($contents, 'asdf.txt', FILE_EXISTS_ERROR); + $this->assertFalse($file, t("Overwriting a file fails when FILE_EXISTS_ERROR is specified.")); + + // Clear out the error messages. +# drupal_get_messages(); + } + + function testFileSaveUpload() { + $max_fid_before = db_result(db_query("SELECT MAX(fid) AS fid FROM {files}")); + + $upload_user = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($upload_user); + + $image = current($this->drupalGetTestFiles('image')); + $this->assertTrue(is_file($image->filename), t("the file we're going to upload exists.")); + $edit = array('files[file_test_upload]' => realpath($image->filename)); + $this->drupalPost('file-test/upload', $edit, t('Submit')); + $this->assertResponse(200, t("Received a 200 response for posted test file.")); + $this->assertText(t('hook_file_validate() was called.'), t('hook_file_validate() was called.')); + $this->assertText(t('hook_file_insert() was called.'), t('hook_file_insert() was called.')); + + $max_fid_after = db_result(db_query("SELECT MAX(fid) AS fid FROM {files}")); + + $this->assertTrue($max_fid_after > $max_fid_before, t("A new file was created.")); + } +} + +/** + * Directory related tests. + */ +class FileDirectoryTest extends DrupalWebTestCase { + /** + * Implementation of getInfo(). + */ + function getInfo() { + return array( + 'name' => t('File paths and directories'), + 'description' => t('Tests operations dealing with directories.'), + 'group' => t('File'), + ); + } + + /** + * Implementation of setUp(). + */ + function setUp() { + parent::setUp('file_test'); + + // A directory to operate on. + $this->directory = file_directory_path() . '/' . $this->randomName(); + // Save initial temp directory as this gets modified. + $this->initial_temp_directory = variable_get('file_directory_temp', NULL); + } + + /** + * Implementation of tearDown(). + */ + function tearDown() { + @rmdir($this->directory); + variable_set('file_directory_temp', $this->initial_temp_directory); + + // clear out form error messages generated +# drupal_get_messages(); + + parent::tearDown(); + } + + /** + * Check directory creation and validation + */ + function testFileCheckDirectory() { + // non-existent directory + $form_element = $this->randomName(); + $this->assertFalse(file_check_directory($this->directory, 0, $form_element), t("Error reported for non-existing directory."), 'File'); + + // check that an error was set for the form element above + $errors = form_get_errors(); + $this->assertEqual($errors[$form_element], t('The directory %directory does not exist.', array('%directory' => $this->directory)), t("Properly generated an error for the passed form element."), 'File'); + + // make a directory + $this->assertTrue(file_check_directory($this->directory, FILE_CREATE_DIRECTORY), t("No error reported when creating a new directory"), 'File'); + + // make sure directory actually exists + $this->assertTrue(is_dir($this->directory), t("Directory actually exists"), 'File'); + + // make directory read only + @chmod($this->directory, 0444); + $form_element = $this->randomName(); + $this->assertFalse(file_check_directory($this->directory, 0, $form_element), t("Error reported for a non-writeable directory"), 'File'); + + // check if form error was set + $errors = form_get_errors(); + $this->assertEqual($errors[$form_element], t('The directory %directory is not writable', array('%directory' => $this->directory)), t("Properly generated an error for the passed form element."), 'File'); + + // test directory permission modification + $this->assertTrue(file_check_directory($this->directory, FILE_MODIFY_PERMISSIONS), t("No error reported when making directory writeable."), 'File'); + + // verify directory actually is writeable + $this->assertTrue(is_writeable($this->directory), t("Directory is writeable"), 'File'); + + // remove .htaccess file to then test the writing of .htaccess file + @unlink(file_directory_path() .'/.htaccess'); + file_check_directory(file_directory_path()); + $this->assertTrue(is_file(file_directory_path() . '/.htaccess'), t('Successfully created the .htaccess file in the files directory.'), 'File'); + + // verify contents of .htaccess file + $file = file_get_contents(file_directory_path() .'/.htaccess'); + $this->assertEqual($file, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks", t('The .htaccess file contains the proper content.'), 'File'); + } + + /** + * Check file_directory_path() and file_directory_temp(). + */ + function testFileDirectoryPath() { + // directory path + $path = variable_get('file_directory_path', conf_path() . '/files'); + $this->assertEqual($path, file_directory_path(), t("Properly returns the stored file directory path."), 'File'); + } + + /** + * Check file_directory_path() and file_directory_temp(). + */ + function testFileDirectoryTemp() { + // temp directory handling + variable_set('file_directory_temp', NULL); + $temp = file_directory_temp(); + $this->assertTrue(!is_null($temp), t("Properly set and retrieved temp directory %directory", array('%directory' => $temp)), 'File'); + } + + /** + * This tests that a file is actually in the specified directory, to prevent exploits. + */ + function testFileCheckLocation() { + $source = 'misc/xyz.txt'; + $directory = 'misc'; + $result = file_check_location($source, $directory); + $this->assertTrue($result, t("Non-existent file validates when checked for location in existing directory."), 'File'); + + $source = 'fake/xyz.txt'; + $directory = 'fake'; + $result = file_check_location($source, $directory); + $this->assertTrue($result, t("Non-existent file validates when checked for location in non-existing directory."), 'File'); + + $source = 'misc/fake/xyz.txt'; + $directory = 'misc'; + $result = file_check_location($source, $directory); + $this->assertTrue($result, t("Non-existent file validates when checked for location in directory, but name contains a non-existent subfolder."), 'File'); + + $source = 'misc/../install.php'; + $directory = 'misc'; + $result = file_check_location($source, $directory); + $this->assertFalse($result, t("Existing file fails validation when it exists outside the directory path, using a /../ exploit."), 'File'); + + $source = 'misc/druplicon.png'; + $directory = 'misc'; + $result = file_check_location($source, $directory); + $this->assertTrue($result, t("Existing file passes validation when checked for location in directory path, and filepath contains a subfolder of the checked path."), 'File'); + + $result = file_check_location($source, $directory); + $this->assertTrue($result, t("Existing file passes validation, returning the source when checked for location in directory."), 'File'); + } + + + /** + * This will take a directory and path, and find a valid filepath that is not taken by another file. + * First we test against an imaginary file that does not exist in a directory. + * Then we test against a file that already exists within that directory. + * @TODO: Finally we copy a file into a directory several times, to ensure a properly iterating filename suffix. + */ + function testFileCreateNewFilepath() { + $basename = 'xyz.txt'; + $directory = 'misc'; + $original = $directory .'/'. $basename; + $path = file_create_filename($basename, $directory); + $this->assertEqual($path, $original, t("New filepath %new equals %original.", array('%new' => $path, '%original' => $original)), 'File'); + + $basename = 'druplicon.png'; + $original = $directory .'/'. $basename; + $expected = $directory .'/druplicon_0.png'; + $path = file_create_filename($basename, $directory); + $this->assertEqual($path, $expected, t("Creating a new filepath from %original equals %new.", array('%new' => $path, '%original' => $original)), 'File'); + } + + /** + * This will test the filepath for a destination based on passed flags and + * whether or not the file exists. + * If a file exists, file_destination($destination, $replace) will either return the existing filepath, + * if $replace is FILE_EXISTS_REPLACE, a new filepath if FILE_EXISTS_RENAME, or an error (returning FALSE) + * if FILE_EXISTS_ERROR. + * If the file doesn't currently exist, then it will simply return the filepath. + */ + function testFileDestination() { + // First test for non-existent file. + $destination = 'misc/xyz.txt'; + $path = file_destination($destination, FILE_EXISTS_REPLACE); + $this->assertEqual($path, $destination, t("Non-existing filepath destination is correct with FILE_EXISTS_REPLACE."), 'File'); + $path = file_destination($destination, FILE_EXISTS_RENAME); + $this->assertEqual($path, $destination, t("Non-existing filepath destination is correct with FILE_EXISTS_RENAME."), 'File'); + $path = file_destination($destination, FILE_EXISTS_ERROR); + $this->assertEqual($path, $destination, t("Non-existing filepath destination is correct with FILE_EXISTS_ERROR."), 'File'); + + $destination = 'misc/druplicon.png'; + $path = file_destination($destination, FILE_EXISTS_REPLACE); + $this->assertEqual($path, $destination, t("Existing filepath destination remains the same with FILE_EXISTS_REPLACE."), 'File'); + $path = file_destination($destination, FILE_EXISTS_RENAME); + $this->assertNotEqual($path, $destination, t("A new filepath destination is created when filepath destination already exists with FILE_EXISTS_RENAME."), 'File'); + $path = file_destination($destination, FILE_EXISTS_ERROR); + $this->assertEqual($path, FALSE, t("An error is returned when filepath destination already exists with FILE_EXISTS_ERROR."), 'File'); + } +} + + +/** + * Deletion related tests + */ +class FileCopyDeleteMoveTest extends DrupalWebTestCase { + /** + * Implementation of getInfo(). + */ + function getInfo() { + return array( + 'name' => t('File management'), + 'description' => t('Tests the file copy, delete and move functions.'), + 'group' => t('File'), + ); + } + + /** + * Implementation of setUp(). + */ + function setUp() { + // Install file_test module + parent::setUp('file_test'); + + // A directory to operate on. + $this->dirname = file_directory_path() . '/' . $this->randomName(); + mkdir($this->dirname); + + // Create a file for testing + $f = new stdClass(); + $f->filepath = file_directory_path() . '/' . $this->randomName(); + $f->filename = basename($f->filepath); + $f->filemime = 'text/plain'; + $f->uid = 1; + touch($f->filepath); + $this->file = file_save($f); + } + + /** + * Implementation of tearDown(). + */ + function tearDown() { + @rmdir($this->dirname); + @unlink($this->file->filepath); + if (!empty($this->file->fid)) { + db_query('DELETE FROM {files} WHERE fid = %d', array($this->file->fid)); + } + + parent::tearDown(); + } + + + function testFileDeletePlain() { + // Delete a regular file + $this->assertTrue(is_file($this->file->filepath), t("File exists.")); + $this->assertTrue(file_delete_plain($this->file->filepath), t("Deleted worked.")); + $this->assertFalse(file_exists($this->file->filepath), t("Test file has actually been deleted.")); + } + + function testFileDeletePlain_Missing() { + // Try to delete a non-existing file + $this->assertTrue(file_delete_plain(file_directory_path() . '/' . $this->randomName()), t("Returns true when deleting a non-existant file.")); + } + + function testFileDeletePlain_Directory() { + // Try to delete a directory + $this->assertTrue(is_dir($this->dirname), t("Directory exists.")); + $this->assertFalse(file_delete_plain($this->dirname), t("Could not delete the delete directory.")); + $this->assertTrue(file_exists($this->dirname), t("Directory has not been deleted.")); + } + + + function testFileDelete() { + file_test_reset_calls(); + + // Check that deletion removes the file and database record. + $this->assertTrue(is_file($this->file->filepath), t("File exists.")); + $this->assertTrue(file_delete($this->file), t("Delete worked.")); + $this->assertEqual(count(file_test_get_calls('references')), 1, t('hook_file_references was called.')); + $this->assertEqual(count(file_test_get_calls('delete')), 1, t('hook_file_delete was called.')); + $this->assertFalse(file_exists($this->file->filepath), t("Test file has actually been deleted.")); + $this->assertFalse(file_load(array('filepath' => $this->file->filepath)), t("File was removed from the database")); + + // TODO: implement hook_file_references() in file_test.module and report a + // file in use and test the $force parameter. + } + + function testFileMovePlain() { + // Moving to a new name. + $this->assertTrue(file_exists($this->file->filepath), t("File exists before moving.")); + $desired_filepath = file_directory_path() . '/' . $this->randomName(); + $new_filepath = file_move_plain($this->file->filepath, $desired_filepath, FILE_EXISTS_ERROR); + $this->assertTrue($new_filepath, t("Move was successful.")); + $this->assertEqual($new_filepath, $desired_filepath, t("Returned expected filepath.")); + $this->assertTrue(file_exists($new_filepath), t("File exists at the new location.")); + $this->assertFalse(file_exists($this->file->filepath), t("No file remains at the old location.")); + + // Moving with rename. + $desired_filepath = file_directory_path() . '/' . $this->randomName(); + $this->assertTrue(file_exists($new_filepath), t("File exists before moving.")); + $this->assertTrue(touch($desired_filepath), t('Created a file so a rename will have to happen.')); + $newer_filepath = file_move_plain($new_filepath, $desired_filepath, FILE_EXISTS_RENAME); + $this->assertTrue($newer_filepath, t("Move was successful.")); + $this->assertNotEqual($newer_filepath, $desired_filepath, t("Returned expected filepath.")); + $this->assertTrue(file_exists($newer_filepath), t("File exists at the new location.")); + $this->assertFalse(file_exists($new_filepath), t("No file remains at the old location.")); + + // TODO: test moving to a directory (rather than full directory/file path) + } + + function testFileMovePlain_Missing() { + // Move non-existant file + $new_filepath = file_move_plain($this->randomName(), $this->randomName()); + $this->assertFalse($new_filepath, t("Moving a missing file fails")); + + drupal_get_messages(); + } + + function testFileMovePlain_OverwriteSelf() { + // Move the file onto itself without renaming shouldn't make changes. + $this->assertTrue(file_exists($this->file->filepath), t("File exists before moving.")); + $new_filepath = file_move_plain($this->file->filepath, $this->file->filepath, FILE_EXISTS_REPLACE); + $this->assertFalse($new_filepath, t("Moving onto itself without renaming fails.")); + $this->assertTrue(file_exists($this->file->filepath), t("File exists after moving onto itself.")); + + // Move the file onto itself with renaming will result in a new filename. + $this->assertTrue(file_exists($this->file->filepath), t("File exists before moving.")); + $new_filepath = file_move_plain($this->file->filepath, $this->file->filepath, FILE_EXISTS_RENAME); + $this->assertTrue($new_filepath, t("Moving onto itself with renaming works.")); + $this->assertFalse(file_exists($this->file->filepath), t("Original file has been removed.")); + $this->assertTrue(file_exists($new_filepath), t("File exists after moving onto itself.")); + + drupal_get_messages(); + } + + function testFileMove() { + file_test_reset_calls(); + $desired_filepath = file_directory_path() . '/' . $this->randomName(); + + $file = file_move(clone $this->file, $desired_filepath, FILE_EXISTS_ERROR); + $this->assertTrue($file, t("File moved sucessfully.")); + $this->assertEqual(count(file_test_get_calls('move')), 1, t('hook_file_move was called.')); + $this->assertEqual(count(file_test_get_calls('update')), 1, t('hook_file_update was called.')); + $this->assertEqual($file->fid, $this->file->fid, t("File id $file->fid is unchanged after move.")); + + $loaded_file = file_load($file->fid, TRUE); + $this->assertTrue($loaded_file, t("File can be loaded from the database.")); + $this->assertEqual($file->filename, $loaded_file->filename, t("File name was updated correctly in the database.")); + $this->assertEqual($file->filepath, $loaded_file->filepath, t("File path was updated correctly in the database.")); + + // Clean up the file so that the directory can be removed + @unlink($loaded_file->filepath); + } + + + function testFileCopyPlain() { + // Copying to a new name. + $desired_filepath = file_directory_path() . '/' . $this->randomName(); + $new_filepath = file_copy_plain($this->file->filepath, $desired_filepath, FILE_EXISTS_ERROR); + $this->assertTrue($new_filepath, t("Copy was successful.")); + $this->assertEqual($new_filepath, $desired_filepath, t("Returned expected filepath.")); + $this->assertTrue(file_exists($this->file->filepath), t("Original file remains.")); + $this->assertTrue(file_exists($new_filepath), t("New file exists.")); + + // Copying with rename. + $desired_filepath = file_directory_path() . '/' . $this->randomName(); + $this->assertTrue(touch($desired_filepath), t('Created a file so a rename will have to happen.')); + $newer_filepath = file_copy_plain($new_filepath, $desired_filepath, FILE_EXISTS_RENAME); + $this->assertTrue($newer_filepath, t("Copy was successful.")); + $this->assertNotEqual($newer_filepath, $desired_filepath, t("Returned expected filepath.")); + $this->assertTrue(file_exists($this->file->filepath), t("Original file remains.")); + $this->assertTrue(file_exists($new_filepath), t("New file exists.")); + + // TODO: test copying to a directory (rather than full directory/file path) + } + + function testFileCopyPlain_NonExistant() { + // Copy non-existant file + $desired_filepath = $this->randomName(); + $this->assertFalse(file_exists($desired_filepath), t("Randomly named file doesn't exists.")); + $new_filepath = file_copy_plain($desired_filepath, $this->randomName()); + $this->assertFalse($new_filepath, t("Copying a missing file fails")); + + drupal_get_messages(); + } + + function testFileCopyPlain_OverwriteSelf() { + // Copy the file onto itself with renaming works. + $this->assertTrue(file_exists($this->file->filepath), t("File exists before copying.")); + $new_filepath = file_copy_plain($this->file->filepath, $this->file->filepath, FILE_EXISTS_RENAME); + $this->assertTrue($new_filepath, t("Copying onto itself with renaming works.")); + $this->assertNotEqual($new_filepath, $this->file->filepath, t("Copied file has a new name.")); + $this->assertTrue(file_exists($this->file->filepath), t("Original file exists after copying onto itself.")); + $this->assertTrue(file_exists($new_filepath), t("Copied file exists after copying onto itself.")); + + // Copy the file onto itself without renaming fails. + $this->assertTrue(file_exists($this->file->filepath), t("File exists before copying.")); + $new_filepath = file_copy_plain($this->file->filepath, $this->file->filepath, FILE_EXISTS_ERROR); + $this->assertFalse($new_filepath, t("Copying onto itself without renaming fails.")); + $this->assertTrue(file_exists($this->file->filepath), t("File exists after copying onto itself.")); + + // Copy the file into same directory without renaming fails. + $this->assertTrue(file_exists($this->file->filepath), t("File exists before copying.")); + $new_filepath = file_copy_plain($this->file->filepath, dirname($this->file->filepath), FILE_EXISTS_ERROR); + $this->assertFalse($new_filepath, t("Copying onto itself fails.")); + $this->assertTrue(file_exists($this->file->filepath), t("File exists after copying onto itself.")); + + // Copy the file into same directory with renaming works. + $this->assertTrue(file_exists($this->file->filepath), t("File exists before copying.")); + $new_filepath = file_copy_plain($this->file->filepath, dirname($this->file->filepath), FILE_EXISTS_RENAME); + $this->assertTrue($new_filepath, t("Copying into same directory works.")); + $this->assertNotEqual($new_filepath, $this->file->filepath, t("Copied file has a new name.")); + $this->assertTrue(file_exists($this->file->filepath), t("Original file exists after copying onto itself.")); + $this->assertTrue(file_exists($new_filepath), t("Copied file exists after copying onto itself.")); + + drupal_get_messages(); + } + + + function testFileCopy() { + file_test_reset_calls(); + $desired_filepath = file_directory_path() . '/' . $this->randomName(); + + $file = file_copy(clone $this->file, $desired_filepath, FILE_EXISTS_ERROR); + $this->assertTrue($file, t("File copied sucessfully.")); + $this->assertEqual(count(file_test_get_calls('copy')), 1, t('hook_file_copy was called.')); + $this->assertEqual(count(file_test_get_calls('insert')), 1, t('hook_file_insert was called.')); + $this->assertNotEqual($file->fid, $this->file->fid, t("A new file id was created.")); + $this->assertNotEqual($file->filepath, $this->file->filepath, t("A new filepath was created.")); + $this->assertEqual($file->filepath, $desired_filepath, t('The copied file object has the desired filepath')); + $this->assertTrue(file_exists($this->file->filepath), t('The original file still exists.')); + $this->assertTrue(file_exists($file->filepath), t('The copied file exists')); + + // Check that the changes were actually saved to the database. + $loaded_file = file_load($file->fid, TRUE); + $this->assertTrue($loaded_file, t("File can be loaded from the database.")); + $this->assertEqual($file->filename, $loaded_file->filename, t("File name was updated correctly in the database.")); + $this->assertEqual($file->filepath, $loaded_file->filepath, t("File path was updated correctly in the database.")); + + // Clean up the file so that the directory can be removed + @unlink($loaded_file->filepath); + } + +} + Index: modules/simpletest/tests/file_test.info =================================================================== RCS file: modules/simpletest/tests/file_test.info diff -N modules/simpletest/tests/file_test.info --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/file_test.info 9 Sep 2008 02:15:32 -0000 @@ -0,0 +1,8 @@ +; $Id$ +name = "File test" +description = "Support module for file handling tests." +package = Testing +version = VERSION +core = 7.x +files[] = file_test.module +hidden = TRUE Index: modules/simpletest/tests/file_test.module =================================================================== RCS file: modules/simpletest/tests/file_test.module diff -N modules/simpletest/tests/file_test.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/file_test.module 9 Sep 2008 02:15:32 -0000 @@ -0,0 +1,194 @@ + t('Upload test'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('_file_test_form'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Reset/initialize the history of calls to the file_* hooks. + */ +function file_test_reset_calls() { + global $file_test_results; + + $file_test_results = array( + 'load' => array(), + 'validate' => array(), + 'download' => array(), + 'references' => array(), + 'status' => array(), + 'insert' => array(), + 'update' => array(), + 'copy' => array(), + 'move' => array(), + 'delete' => array(), + ); +} + +/** + * Log the invocation of a file hook. + * + * @param $op One of the hook_file_* operations. + * @param $args Arguments passed to the hook. + */ +function _file_test_add_call($op, $args) { + global $file_test_results; + $file_test_results[$op][] = $args; +} + +/** + * Get the values passed to a the hook calls for a given operation. + * + * @param $op One of the hook_file_* operations. + * @returns Array of the parameters passed to each call. + */ +function file_test_get_calls($op) { + global $file_test_results; + return $file_test_results[$op]; +} + + +/** + * Helper validator that returns the $errors parameter. + */ +function file_test_validator($file, $errors) { + return $errors; +} + + +/** + * Implementation of hook_file_load(). + */ +function file_test_file_load(&$file) { + // Assign a value on the object so that we can test that the $file is passed + // by reference. + $file->file_test['loaded'] = TRUE; + $args = func_get_args(); + _file_test_add_call('load', $args); +} + +/** + * Implementation of hook_file_validate(). + */ +function file_test_file_validate(&$file) { + $args = func_get_args(); + _file_test_add_call('validate', $args); +} + +/** + * Implementation of hook_file_status(). + */ +function file_test_file_status(&$file) { + $args = func_get_args(); + _file_test_add_call('status', $args); +} + +/** + * Implementation of hook_file_download(). + */ +function file_test_file_download(&$file) { + $args = func_get_args(); + _file_test_add_call('download', $args); +} + +/** + * Implementation of hook_file_references(). + */ +function file_test_file_references(&$file) { + $args = func_get_args(); + _file_test_add_call('references', $args); +} + +/** + * Implementation of hook_file_insert(). + */ +function file_test_file_insert(&$file) { + $args = func_get_args(); + _file_test_add_call('insert', $args); +} + +/** + * Implementation of hook_file_update(). + */ +function file_test_file_update(&$file) { + $args = func_get_args(); + _file_test_add_call('update', $args); +} + +/** + * Implemenation of hook_file_copy(). + */ +function file_test_file_copy(&$file, &$source) { + $args = func_get_args(); + _file_test_add_call('copy', $args); +} + +/** + * Implemenation of hook_file_move(). + */ +function file_test_file_move(&$file, &$source) { + $args = func_get_args(); + _file_test_add_call('move', $args); +} + +/** + * Implementation of hook_file_delete(). + */ +function file_test_file_delete(&$file) { + $args = func_get_args(); + _file_test_add_call('delete', $args); +} + + +/** + * Form to test file uploads. + */ +function _file_test_form(&$form_state) { + $form['#validate'][] = '_file_test_validate_upload'; + $form['file_test_upload'] = array( + '#type' => 'file', + '#title' => t('Upload image file'), + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + return $form; +} + +/** + * Process the upload. + */ +function _file_test_validate_upload(&$form, &$form_state) { + // Validate the uploaded picture. + $validators = array( + 'file_validate_is_image' => array(), + ); +file_put_contents('/Users/amorton/Sites/dh/sites/default/files/drupal.log', "before upload\n", FILE_APPEND); + if ($file = file_save_upload('file_test_upload', $validators)) { + $form_state['values']['file_test_upload'] = $file; + if (count(file_test_get_calls('validate'))) { + drupal_set_message(t('hook_file_validate() was called.')); + } + if (count(file_test_get_calls('insert'))) { + drupal_set_message(t('hook_file_insert() was called.')); + } + } +} Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.87 diff -u -p -r1.87 system.admin.inc --- modules/system/system.admin.inc 6 Sep 2008 08:36:21 -0000 1.87 +++ modules/system/system.admin.inc 9 Sep 2008 02:15:32 -0000 @@ -338,9 +338,9 @@ function system_theme_settings(&$form_st // The image was saved using file_save_upload() and was added to the // files table as a temporary file. We'll make a copy and let the garbage // collector delete the original upload. - if (file_copy($file, $filename, FILE_EXISTS_REPLACE)) { + if ($filepath = file_copy_plain($file->filepath, $filename, FILE_EXISTS_REPLACE)) { $_POST['default_logo'] = 0; - $_POST['logo_path'] = $file->filepath; + $_POST['logo_path'] = $filepath; $_POST['toggle_logo'] = 1; } } @@ -353,9 +353,9 @@ function system_theme_settings(&$form_st // The image was saved using file_save_upload() and was added to the // files table as a temporary file. We'll make a copy and let the garbage // collector delete the original upload. - if (file_copy($file, $filename)) { + if ($filepath = file_copy_plain($file->filepath, $filename, FILE_EXISTS_REPLACE)) { $_POST['default_favicon'] = 0; - $_POST['favicon_path'] = $file->filepath; + $_POST['favicon_path'] = $filepath; $_POST['toggle_favicon'] = 1; } } Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.617 diff -u -p -r1.617 system.module --- modules/system/system.module 6 Sep 2008 08:36:21 -0000 1.617 +++ modules/system/system.module 9 Sep 2008 02:15:32 -0000 @@ -1401,7 +1401,7 @@ function system_cron() { if (file_exists($file->filepath)) { // If files that exist cannot be deleted, continue so the database remains // consistent. - if (!file_delete($file->filepath)) { + if (!file_delete($file)) { watchdog('file system', 'Could not delete temporary file "%path" during garbage collection', array('%path' => $file->filepath), WATCHDOG_ERROR); continue; } Index: modules/upload/upload.module =================================================================== RCS file: /cvs/drupal/drupal/modules/upload/upload.module,v retrieving revision 1.205 diff -u -p -r1.205 upload.module --- modules/upload/upload.module 24 Jul 2008 16:25:19 -0000 1.205 +++ modules/upload/upload.module 9 Sep 2008 02:15:32 -0000 @@ -263,6 +263,38 @@ function upload_form_alter(&$form, $form } /** + * Implementation of hook_file_load(). + */ +function upload_file_load(&$file, $source = NULL) { + // Add the upload specific data into the file object. + $values = db_fetch_array(db_query('SELECT * FROM {upload} u WHERE u.fid = %d', $file->fid)); + foreach ((array)$values as $key => $value) { + $file->{$key} = $value; + } +} + +/** + * Implementation of hook_file_references(). + */ +function upload_file_references(&$file, $source = NULL) { + // If upload.module is still using a file, do not let other modules delete it. + $count = db_result(db_query('SELECT count(*) FROM {upload} WHERE fid = %d', $file->fid)); + if ($count) { + // return the name of the module and how many references it has to the file. + return array('upload' => $count); + } +} + +/** + * Implementation of hook_file_delete(). + */ +function upload_file_delete(&$file, $source = NULL) { + // Delete all information associated with the file. + db_query('DELETE FROM {upload} WHERE fid = %d', $file->fid); +} + + +/** * Implementation of hook_nodeapi(). */ function upload_nodeapi(&$node, $op, $teaser) { @@ -406,13 +438,15 @@ function upload_save(&$node) { // If the file isn't used by any other revisions delete it. $count = db_result(db_query('SELECT COUNT(fid) FROM {upload} WHERE fid = %d', $fid)); if ($count < 1) { - file_delete($file->filepath); + file_delete($file); db_query('DELETE FROM {files} WHERE fid = %d', $fid); } // Remove it from the session in the case of new uploads, // that you want to disassociate before node submission. unset($_SESSION['upload_files'][$fid]); + // Try to clean up a file that is no longer in use. + file_delete($file); // Move on, so the removed file won't be added to new revisions. continue; } @@ -420,13 +454,12 @@ function upload_save(&$node) { // Create a new revision, or associate a new file needed. if (!empty($node->old_vid) || isset($_SESSION['upload_files'][$fid])) { db_query("INSERT INTO {upload} (fid, nid, vid, list, description, weight) VALUES (%d, %d, %d, %d, '%s', %d)", $file->fid, $node->nid, $node->vid, $file->list, $file->description, $file->weight); - file_set_status($file, FILE_STATUS_PERMANENT); } // Update existing revision. else { db_query("UPDATE {upload} SET list = %d, description = '%s', weight = %d WHERE fid = %d AND vid = %d", $file->list, $file->description, $file->weight, $file->fid, $node->vid); - file_set_status($file, FILE_STATUS_PERMANENT); } + file_set_status($file, FILE_STATUS_PERMANENT); } // Empty the session storage after save. We use this variable to track files // that haven't been related to the node yet. @@ -434,38 +467,23 @@ function upload_save(&$node) { } function upload_delete($node) { - $files = array(); - $result = db_query('SELECT DISTINCT f.* FROM {upload} u INNER JOIN {files} f ON u.fid = f.fid WHERE u.nid = %d', $node->nid); - while ($file = db_fetch_object($result)) { - $files[$file->fid] = $file; + db_query('DELETE FROM {upload} WHERE nid = %d', $node->nid); + if (!is_array($node->files)) { + return; } - - foreach ($files as $fid => $file) { - // Delete all files associated with the node - db_query('DELETE FROM {files} WHERE fid = %d', $fid); - file_delete($file->filepath); + foreach($node->files as $file) { + file_delete($file); } - - // Delete all file revision information associated with the node - db_query('DELETE FROM {upload} WHERE nid = %d', $node->nid); } function upload_delete_revision($node) { - if (is_array($node->files)) { - foreach ($node->files as $file) { - // Check if the file will be used after this revision is deleted - $count = db_result(db_query('SELECT COUNT(fid) FROM {upload} WHERE fid = %d', $file->fid)); - - // if the file won't be used, delete it - if ($count < 2) { - db_query('DELETE FROM {files} WHERE fid = %d', $file->fid); - file_delete($file->filepath); - } - } - } - - // delete the revision db_query('DELETE FROM {upload} WHERE vid = %d', $node->vid); + if (!is_array($node->files)) { + return; + } + foreach ($node->files as $file) { + file_delete($file); + } } function _upload_form($node) { @@ -479,11 +497,11 @@ function _upload_form($node) { if (!empty($node->files) && is_array($node->files)) { $form['files']['#theme'] = 'upload_form_current'; $form['files']['#tree'] = TRUE; - foreach ($node->files as $key => $file) { + foreach ($node->files as $file) { $file = (object)$file; - $description = file_create_url($file->filepath); - $description = "" . check_plain($description) . ""; - $form['files'][$key]['description'] = array('#type' => 'textfield', '#default_value' => !empty($file->description) ? $file->description : $file->filename, '#maxlength' => 256, '#description' => $description ); + $key = $file->fid; + + $form['files'][$key]['description'] = array('#type' => 'textfield', '#default_value' => !empty($file->description) ? $file->description : $file->filename, '#maxlength' => 256, '#description' => '' . file_create_url($file->filepath) . ''); $form['files'][$key]['size'] = array('#markup' => format_size($file->filesize)); $form['files'][$key]['remove'] = array('#type' => 'checkbox', '#default_value' => !empty($file->remove)); $form['files'][$key]['list'] = array('#type' => 'checkbox', '#default_value' => $file->list); @@ -498,12 +516,26 @@ function _upload_form($node) { if (user_access('upload files')) { $limits = _upload_file_limits($user); + + $limit_description = t('The maximum size of file uploads is %filesize. ', array('%filesize' => format_size($limits['file_size']))); + if (!empty($limits['resolution'])) { + if (image_get_toolkit()) { + $limit_description .= t('Images larger than %resolution will be resized. ', array('%resolution' => $limits['resolution'])); + } + else { + $limit_description .= t('Images may not be larger than %resolution. ', array('%resolution' => $limits['resolution'])); + } + } + if ($user->uid != 1) { + $limit_description .= t('Only files with the following extensions may be uploaded: %extensions. ', array('%extensions' => $limits['extensions'])); + } + $form['new']['#weight'] = 10; $form['new']['upload'] = array( '#type' => 'file', '#title' => t('Attach new file'), '#size' => 40, - '#description' => ($limits['resolution'] ? t('Images are larger than %resolution will be resized. ', array('%resolution' => $limits['resolution'])) : '') . t('The maximum upload size is %filesize. Only files with the following extensions may be uploaded: %extensions. ', array('%extensions' => $limits['extensions'], '%filesize' => format_size($limits['file_size']))), + '#description' => $limit_description, ); $form['new']['attach'] = array( '#type' => 'submit', @@ -565,9 +597,9 @@ function upload_load($node) { $files = array(); if ($node->vid) { - $result = db_query('SELECT * FROM {files} f INNER JOIN {upload} r ON f.fid = r.fid WHERE r.vid = %d ORDER BY r.weight, f.fid', $node->vid); + $result = db_query('SELECT u.fid FROM {upload} u WHERE u.vid = %d ORDER BY u.fid', $node->vid); while ($file = db_fetch_object($result)) { - $files[$file->fid] = $file; + $files[$file->fid] = file_load($file->fid); } } Index: modules/upload/upload.test =================================================================== RCS file: /cvs/drupal/drupal/modules/upload/upload.test,v retrieving revision 1.3 diff -u -p -r1.3 upload.test --- modules/upload/upload.test 6 Jun 2008 10:36:44 -0000 1.3 +++ modules/upload/upload.test 9 Sep 2008 02:15:32 -0000 @@ -106,7 +106,7 @@ class UploadTestCase extends DrupalWebTe // Attempt to upload .txt file when .test is only extension allowed. $this->uploadFile($node, $files[0], FALSE); - $this->assertRaw(t('The selected file %name could not be uploaded. Only files with the following extensions are allowed: %files-allowed.', array('%name' => basename($files[0]), '%files-allowed' => $settings['upload_extensions'])), 'File '. $files[0] . ' was not allowed to be uploaded'); + $this->assertRaw(t('The specified file %name could not be uploaded. Only files with the following extensions are allowed: %files-allowed.', array('%name' => basename($files[0]), '%files-allowed' => $settings['upload_extensions'])), 'File '. $files[0] . ' was not allowed to be uploaded'); // Attempt to upload .test file when .test is only extension allowed. $this->uploadFile($node, $files[1]); @@ -143,7 +143,7 @@ class UploadTestCase extends DrupalWebTe $filename = basename($file); $filesize = format_size($info['size']); $maxsize = format_size(parse_size(($settings['upload_uploadsize'] * 1024) . 'KB')); // Won't parse decimals. - $this->assertRaw(t('The selected file %name could not be uploaded. The file is %filesize exceeding the maximum file size of %maxsize.', array('%name' => $filename, '%filesize' => $filesize, '%maxsize' => $maxsize)), t('File upload was blocked since it was larger than maxsize.')); + $this->assertRaw(t('The specified file %name could not be uploaded. The file is %filesize exceeding the maximum file size of %maxsize.', array('%name' => $filename, '%filesize' => $filesize, '%maxsize' => $maxsize)), t('File upload was blocked since it was larger than maxsize.')); } function setUploadSettings($settings, $rid = NULL) { Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.919 diff -u -p -r1.919 user.module --- modules/user/user.module 8 Sep 2008 15:44:57 -0000 1.919 +++ modules/user/user.module 9 Sep 2008 02:15:32 -0000 @@ -407,16 +407,16 @@ function user_validate_picture(&$form, & if ($file = file_save_upload('picture_upload', $validators)) { // Remove the old picture. if (isset($form_state['values']['_account']->picture) && file_exists($form_state['values']['_account']->picture)) { - file_delete($form_state['values']['_account']->picture); + file_delete_plain($form_state['values']['_account']->picture); } // The image was saved using file_save_upload() and was added to the // files table as a temporary file. We'll make a copy and let the garbage // collector delete the original upload. $info = image_get_info($file->filepath); - $destination = variable_get('user_picture_path', 'pictures') . '/picture-' . $form['#uid'] . '.' . $info['extension']; - if (file_copy($file, $destination, FILE_EXISTS_REPLACE)) { - $form_state['values']['picture'] = $file->filepath; + $destination = file_create_path(variable_get('user_picture_path', 'pictures') . '/picture-' . $form['#uid'] . '.' . $info['extension']); + if ($filepath = file_copy_plain($file->filepath, $destination, FILE_EXISTS_REPLACE)) { + $form_state['values']['picture'] = $filepath; } else { form_set_error('picture_upload', t("Failed to upload the picture image; the %directory directory doesn't exist or is not writable.", array('%directory' => variable_get('user_picture_path', 'pictures')))); @@ -1538,7 +1538,7 @@ function _user_edit_submit($uid, &$edit) // Delete picture if requested, and if no replacement picture was given. if (!empty($edit['picture_delete'])) { if ($user->picture && file_exists($user->picture)) { - file_delete($user->picture); + file_delete_plain($user->picture); } $edit['picture'] = ''; }