Commit f841d1a7 authored by webchick's avatar webchick
Browse files

#142995 by dopry, drewish, quicksketch, jpetso, and flobruit: Adding...

#142995 by dopry, drewish, quicksketch, jpetso, and flobruit: Adding hook_file_X(). This is an enabler of lots and lots of goodies. See CHANGELOG.txt for more. Awesome work, guys. :)
parent 72e09d7b
......@@ -52,6 +52,16 @@ Drupal 7.0, xxxx-xx-xx (development version)
and memory improvements.
- Theme system:
* Converted the 'bluemarine' theme to a tableless layout.
- File handling:
* Files are now first class Drupal objects with file_load(), file_save(),
and file_validate() functions and corresponding hooks.
* The file_move(), file_copy() and file_delete() functions now operate on
file objects and invoke file hooks so that modules are notified and can
respond to changes.
* For the occasions when only basic file manipulation are needed--such as
uploading a site logo--that don't require the overhead of databases and
hooks, the current unmanaged copy, move and delete operations have been
preserved but renamed to file_unmanaged_*().
Drupal 6.0, 2008-02-13
----------------------
......
......@@ -648,7 +648,7 @@ function _drupal_get_last_caller($backtrace) {
// The first trace is the call itself.
// It gives us the line and the file of the last call.
$call = $backtrace[0];
// The second call give us the function where the call originated.
if (isset($backtrace[1])) {
if (isset($backtrace[1]['class'])) {
......@@ -1851,7 +1851,7 @@ function drupal_build_css_cache($types, $filename) {
$data = implode('', $matches[0]) . $data;
// Create the CSS file.
file_save_data($data, $csspath . '/' . $filename, FILE_EXISTS_REPLACE);
file_unmanaged_save_data($data, $csspath . '/' . $filename, FILE_EXISTS_REPLACE);
}
return $csspath . '/' . $filename;
}
......@@ -1952,7 +1952,7 @@ function _drupal_load_stylesheet($matches) {
* 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_unmanaged_delete', TRUE);
}
/**
......@@ -2315,7 +2315,7 @@ function drupal_build_js_cache($files, $filename) {
}
// Create the JS file.
file_save_data($contents, $jspath . '/' . $filename, FILE_EXISTS_REPLACE);
file_unmanaged_save_data($contents, $jspath . '/' . $filename, FILE_EXISTS_REPLACE);
}
return $jspath . '/' . $filename;
......@@ -2325,7 +2325,7 @@ function drupal_build_js_cache($files, $filename) {
* 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_unmanaged_delete', TRUE);
variable_set('javascript_parsed', array());
}
......
......@@ -254,7 +254,140 @@ function file_check_location($source, $directory = '') {
}
/**
* Copy a file to a new location.
* 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.
*
* @see hook_file_load()
*/
function file_load($param, $reset = NULL) {
static $files = array();
if ($reset) {
$files = array();
}
if (is_numeric($param)) {
if (isset($files[(string) $param])) {
return is_object($files[$param]) ? clone $files[$param] : $files[$param];
}
$result = db_query('SELECT f.* FROM {files} f WHERE f.fid = :fid', array(':fid' => $param));
}
elseif (is_array($param)) {
// Turn the conditions into a query.
$cond = array();
$arguments = array();
foreach ($param as $key => $value) {
$cond[] = 'f.' . db_escape_table($key) . " = '%s'";
$arguments[] = $value;
}
$result = db_query('SELECT f.* FROM {files} f WHERE ' . implode(' AND ', $cond), $arguments);
}
else {
return FALSE;
}
$file = $result->fetch(PDO::FETCH_OBJ);
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;
}
/**
* 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.
* @see hook_file_insert()
* @see hook_file_update()
*/
function file_save($file) {
$file = (object)$file;
$file->timestamp = 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;
}
/**
* 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 $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
* File object if the copy is successful, or FALSE in the event of an error.
* @see file_unmanaged_copy()
* @see hook_file_copy()
*/
function file_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
$source = (object)$source;
if ($filepath = file_unmanaged_copy($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().
......@@ -277,8 +410,9 @@ function file_check_location($source, $directory = '') {
* - 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($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
$source = realpath($source);
if (!file_exists($source)) {
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');
......@@ -360,7 +494,52 @@ function file_destination($destination, $replace) {
}
/**
* Move a file to a new location.
* 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
* Resulting file object for success, or FALSE in the event of an error.
* @see file_unmanaged_move()
* @see hook_file_move()
*/
function file_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
$source = (object)$source;
if ($filepath = file_unmanaged_move($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' => $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.
......@@ -375,10 +554,11 @@ function file_destination($destination, $replace) {
* - 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($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
$filepath = file_copy($source, $destination, $replace);
if ($filepath == FALSE || file_delete($source) == FALSE) {
function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
$filepath = file_unmanaged_copy($source, $destination, $replace);
if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) {
return FALSE;
}
return $filepath;
......@@ -470,17 +650,64 @@ function file_create_filename($basename, $directory) {
}
/**
* Delete a file.
* 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 canceled.
*
* @param $file
* 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, FALSE in the event of an error, or an array if the file
* is being used by another module. The array keys are the module's name and
* the values are the number of references.
* @see file_unmanaged_delete()
* @see hook_file_references()
* @see hook_file_delete()
*/
function file_delete($file, $force = FALSE) {
$file = (object)$file;
// 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_unmanaged_delete($file->filepath)) {
db_delete('files')->condition('fid', $file->fid)->execute();
return TRUE;
}
return FALSE;
}
/**
* 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.
* @see file_delete()
*/
function file_delete($path) {
function file_unmanaged_delete($path) {
if (is_dir($path)) {
watchdog('file', t('%path is a directory and cannot be removed using file_delete().', array('%path' => $path)), WATCHDOG_ERROR);
watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), WATCHDOG_ERROR);
return FALSE;
}
if (is_file($path)) {
......@@ -489,7 +716,7 @@ function file_delete($path) {
// Return TRUE for non-existant file, but log that nothing was actually
// deleted, as the current state is the indended result.
if (!file_exists($path)) {
watchdog('file', t('The file %path was not deleted, because it does not exist.', array('%path' => $path)), WATCHDOG_NOTICE);
watchdog('file', '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.
......@@ -510,9 +737,9 @@ function file_delete($path) {
*/
function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
if (!is_null($uid)) {
return (int)db_result(db_query('SELECT SUM(filesize) FROM {files} WHERE uid = %d AND status & %d', array($uid, $status)));
return db_query('SELECT SUM(filesize) FROM {files} WHERE uid = :uid AND status & :status', array(':uid' => $uid, ':status' => $status))->fetchField();
}
return (int)db_result(db_query('SELECT SUM(filesize) FROM {files} WHERE status & %d', array($status)));
return db_query('SELECT SUM(filesize) FROM {files} WHERE status & :status', array(':status' => $status))->fetchField();
}
/**
......@@ -642,14 +869,11 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
}
// If we made it this far it's safe to record this file in the database.
$file->status = FILE_STATUS_TEMPORARY;
$file->timestamp = 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;
}
......@@ -658,6 +882,9 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
/**
* Check that a file meets the criteria specified by the validators.
*
* After executing the validator callbacks specified hook_file_validate() will
* also be called to allow other modules to report errors about the file.
*
* @param $file
* A Drupal file object.
* @param $validators
......@@ -669,6 +896,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
* the order specified.
* @return
* An array contaning validation error messages.
* @see hook_file_validate()
*/
function file_validate(&$file, $validators = array()) {
// Call the validation functions specified by this function's caller.
......@@ -678,7 +906,8 @@ function file_validate(&$file, $validators = array()) {
$errors = array_merge($errors, call_user_func_array($function, $args));
}
return $errors;
// Let other modules perform validation on the new file.
return array_merge($errors, module_invoke_all('file_validate', $file));
}
/**
......@@ -833,7 +1062,7 @@ function file_validate_image_resolution(&$file, $maximum_dimensions = 0, $minimu
}
/**
* 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.
......@@ -848,9 +1077,49 @@ function file_validate_image_resolution(&$file, $maximum_dimensions = 0, $minimu
* unique.
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
* @return
* A string with the path of the resulting file, or FALSE on error.
* A file object, or FALSE on error.
* @see file_unmanaged_save_data()
*/
function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
global $user;
if ($filepath = file_unmanaged_save_data($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);
}
return FALSE;
}
/**
* 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_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
// Write the data to a temporary file.
$temp_name = tempnam(file_directory_temp(), 'file');
if (file_put_contents($temp_name, $data) === FALSE) {
......@@ -859,7 +1128,7 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
}
// Move the file to its final destination.
return file_move($temp_name, $destination, $replace);
return file_unmanaged_move($temp_name, $destination, $replace);
}
/**
......@@ -876,11 +1145,21 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
* @return
* File object if the change is successful, or FALSE in the event of an
* error.
* @see hook_file_status()
*/
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 = (object)$file;
$num_updated = db_update('files')
->fields(array('status' => $status))
->condition('fid', $file->fid)
->execute();
if ($num_updated) {
$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;
}
......@@ -928,6 +1207,8 @@ function file_transfer($source, $headers) {
* returns -1 drupal_access_denied() will be returned. If one or more modules
* returned headers the download will start with the returned headers. If no
* modules respond drupal_not_found() will be returned.
*
* @see hook_file_download()
*/
function file_download() {
// Merge remainder of arguments from GET['q'], into relative file path.
......
......@@ -2165,7 +2165,7 @@ function _locale_rebuild_js($langcode = NULL) {
// Save the file.
$dest = $dir . '/' . $language->language . '_' . $data_hash . '.js';
if (file_save_data($data, $dest)) {
if (file_unmanaged_save_data($data, $dest)) {
$language->javascript = $data_hash;
$status = ($status == 'deleted') ? 'updated' : 'created';
}
......
......@@ -151,7 +151,7 @@ class AggregatorTestCase extends DrupalWebTestCase {
EOF;
$path = file_directory_path() . '/valid-opml.xml';
return file_save_data($opml, $path);
return file_unmanaged_save_data($opml, $path);
}
/**
......@@ -168,7 +168,7 @@ EOF;
EOF;
$path = file_directory_path() . '/invalid-opml.xml';
return file_save_data($opml, $path);
return file_unmanaged_save_data($opml, $path);
}
/**
......@@ -190,7 +190,7 @@ EOF;
EOF;
$path = file_directory_path() . '/empty-opml.xml';
return file_save_data($opml, $path);
return file_unmanaged_save_data($opml, $path);
}
function getRSS091Sample() {
......@@ -223,7 +223,7 @@ EOF;
EOT;
$path = file_directory_path() . '/rss091.xml';
return file_save_data($feed, $path);
return file_unmanaged_save_data($feed, $path);
}
}
......
......@@ -385,7 +385,7 @@ function blogapi_metaweblog_new_media_object($blogid, $username, $password, $fil
return blogapi_error(t('No file sent.'));
}
if (!$filepath = file_save_data($data, $name)) {
if (!$filepath = file_unmanaged_save_data($data, $name)) {
return blogapi_error(t('Error storing file.'));
}
......
......@@ -308,7 +308,7 @@ function color_scheme_form_submit($form, &$form_state) {
foreach ($info['copy'] as $file) {
$base = basename($file);
$source = $paths['source'] . $file;
$filepath = file_copy($source, $paths['target'] . $base);
$filepath = file_unmanaged_copy($source, $paths['target'] . $base);
$paths['map'][$file] = $base;
$paths['files'][] = $filepath;
}
......@@ -435,7 +435,7 @@ function _color_rewrite_stylesheet($theme, &$info, &$paths, $palette, $style) {
* Save the rewritten stylesheet to disk.
*/
function _color_save_stylesheet($file, $style, &$paths) {
$filepath = file_save_data($style, $file, FILE_EXISTS_REPLACE);
$filepath = file_unmanaged_save_data($style, $file, FILE_EXISTS_REPLACE);
$paths['files'][] = $filepath;
// Set standard file permissions for webserver-generated files.
......
......@@ -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_unmanaged_copy($file->filename, $path . '/' . $file->basename);
}
$generated = TRUE;
}
......
......@@ -571,7 +571,7 @@ function simpletest_clean_temporary_directory($path) {
simpletest_clean_temporary_directory($file_path);
}
else {
file_delete($file_path);
file_unmanaged_delete($file_path);
}
}
}
......
This diff is collapsed.
......@@ -50,3 +50,117 @@ function _file_test_form_submit(&$form, &$form_state) {
drupal_set_message(t('Epic upload FAIL!'), 'error');
}
}
/**
* Reset/initialize the history of calls to the file_* hooks.
*/
function file_test_reset() {
// Keep track of calls to these hooks
$GLOBALS['file_test_results'] = array(
'load' => array(),
'validate' => array(),
'download' => array(),
'references' => array(),
'status' => array(),
'insert' => array(),
'update' => array(),
'copy' => array(),
'move' => array(),