From 4fd54aabc574f9f7afb2f10960e033af1cb3275b Mon Sep 17 00:00:00 2001 From: Dries Buytaert <dries@buytaert.net> Date: Wed, 30 May 2007 08:08:59 +0000 Subject: [PATCH] - Patch #115267 by drewish, dopry et al: simplified file uploads code, improved file API, centralized file validation, implemented quotas and fixed file previews. --- CHANGELOG.txt | 3 + includes/bootstrap.inc | 2 +- includes/file.inc | 566 +++++++++++++++++++-------- includes/locale.inc | 2 +- modules/aggregator/aggregator.module | 4 +- modules/locale/locale.module | 4 +- modules/system/system.install | 33 ++ modules/system/system.module | 56 ++- modules/system/system.schema | 33 +- modules/upload/upload.install | 18 + modules/upload/upload.module | 466 +++++++--------------- modules/upload/upload.schema | 19 + modules/user/user.module | 38 +- themes/bluemarine/style-rtl.css | 18 +- themes/bluemarine/style.css | 2 +- 15 files changed, 697 insertions(+), 567 deletions(-) create mode 100644 modules/upload/upload.install create mode 100644 modules/upload/upload.schema diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1baa84e7f40c..4e0ba2530be0 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -48,6 +48,9 @@ Drupal 6.0, xxxx-xx-xx (development version) * Tags are now automatically closed at the end of the teaser. - Performance: * Made it easier to conditionally load include files. +- File handling improvements: + * Entries in the files table are now keyed to a user, and not a node. + * Added re-usable validation functions to check for uploaded file sizes, extensions, and image resolution. Drupal 5.0, 2007-01-15 ---------------------- diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index d0f9e5109356..738e2b5740d8 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1053,7 +1053,7 @@ function language_list($field = 'language', $reset = FALSE) { /** * Default language used on the site - * + * * @param $property * Optional property of the language object to return */ diff --git a/includes/file.inc b/includes/file.inc index c033c712502a..cebc97194dba 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -20,6 +20,18 @@ define('FILE_EXISTS_REPLACE', 1); define('FILE_EXISTS_ERROR', 2); +/** + * A files status can be one of two values: temorary or permanent. The status + * for each file Drupal manages is stored in the {files} tables. If the status + * is temporary Drupal's file garbage collection will delete the file and + * remove it from the files table after a set period of time. + * + * If you wish to add custom statuses for use by contrib modules please expand as + * binary flags and consider the first 8 bits reserved. (0,1,2,4,8,16,32,64,128) + */ +define('FILE_STATUS_TEMPORARY', 0); +define('FILE_STATUS_PERMANENT', 1); + /** * Create the download path to a file. * @@ -152,115 +164,6 @@ function file_check_path(&$path) { return FALSE; } - -/** - * Check if $source is a valid file upload. If so, move the file to Drupal's tmp dir - * and return it as an object. - * - * The use of SESSION['file_uploads'] should probably be externalized to upload.module - * - * @todo Rename file_check_upload to file_prepare upload. - * @todo Refactor or merge file_save_upload. - * @todo Extenalize SESSION['file_uploads'] to modules. - * - * @param $source An upload source (the name of the upload form item), or a file - * @return FALSE for an invalid file or upload. A file object for valid uploads/files. - * - */ - -function file_check_upload($source = 'upload') { - // Cache for uploaded files. Since the data in _FILES is modified - // by this function, we cache the result. - static $upload_cache; - - // Test source to see if it is an object. - if (is_object($source)) { - - // Validate the file path if an object was passed in instead of - // an upload key. - if (is_file($source->filepath)) { - return $source; - } - else { - return FALSE; - } - } - - // 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]; - } - - // 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. - switch ($_FILES["files"]["error"][$source]) { - - // @see http://php.net/manual/en/features.file-upload.errors.php - case UPLOAD_ERR_OK: - break; - - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - drupal_set_message(t('The file %file could not be saved, because it exceeds the maximum allowed size for uploads.', array('%file' => $source)), 'error'); - return 0; - - case UPLOAD_ERR_PARTIAL: - case UPLOAD_ERR_NO_FILE: - drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $source)), 'error'); - return 0; - - // Unknown error - default: - drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $source)), 'error'); - return 0; - } - - // Begin building file object. - $file = new stdClass(); - $file->filename = trim(basename($_FILES["files"]["name"][$source]), '.'); - - // Create temporary name/path for newly uploaded files. - $file->filepath = tempnam(file_directory_temp(), 'tmp_'); - - $file->filemime = $_FILES["files"]["type"][$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')) { - $file->filemime = 'text/plain'; - $file->filepath .= '.txt'; - $file->filename .= '.txt'; - } - - // Move uploaded files from php's upload_tmp_dir to Drupal's file temp. - // This overcomes open_basedir restrictions for future file operations. - if (!move_uploaded_file($_FILES["files"]["tmp_name"][$source], $file->filepath)) { - drupal_set_message(t('File upload error. Could not move uploaded file.')); - watchdog('file', 'Upload Error. Could not move uploaded file (%file) to destination (%destination).', array('%file' => $_FILES["files"]["tmp_name"][$source], '%destination' => $file->filepath)); - return FALSE; - } - - $file->filesize = $_FILES["files"]["size"][$source]; - $file->source = $source; - - // Add processed file to the cache. - $upload_cache[$source] = $file; - return $file; - } - - else { - // In case of previews return previous file object. - if (isset($_SESSION['file_uploads']) && file_exists($_SESSION['file_uploads'][$source]->filepath)) { - return $_SESSION['file_uploads'][$source]; - } - } - // If nothing was done, return FALSE. - return FALSE; -} - /** * 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 @@ -347,29 +250,9 @@ function file_copy(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { // 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 (file_exists($dest)) { - switch ($replace) { - case FILE_EXISTS_RENAME: - // Destination file already exists and we can't replace is so we try and - // and find a new filename. - if ($pos = strrpos($basename, '.')) { - $name = substr($basename, 0, $pos); - $ext = substr($basename, $pos); - } - else { - $name = $basename; - } - - $counter = 0; - do { - $dest = $directory .'/'. $name .'_'. $counter++ . $ext; - } while (file_exists($dest)); - 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' => $source)), 'error'); - return 0; - } + 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; } if (!@copy($source, $dest)) { @@ -378,7 +261,9 @@ function file_copy(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { } // Give everyone read access so that FTP'd users or - // non-webserver users can see/read these files. + // non-webserver users can see/read these files, + // and give group write permissions so group memebers + // can alter files uploaded by the webserver. @chmod($dest, 0664); } @@ -394,6 +279,36 @@ function file_copy(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { return 1; // Everything went ok. } +/** + * Determines the destination path for a file depending on how replacement of + * existing files should be handled. + * + * @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_RENAME - Append _{incrementing number} until the filename is + * 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. + */ +function file_destination($destination, $replace) { + if (file_exists($destination)) { + switch ($replace) { + case FILE_EXISTS_RENAME: + $basename = basename($destination); + $directory = dirname($destination); + $destination = file_create_filename($basename, $directory); + 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' => $source)), 'error'); + return FALSE; + } + } + return $destination; +} + /** * Moves a file to a new location. * - Checks if $source and $dest are valid and readable/writable. @@ -413,7 +328,6 @@ function file_copy(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { * @return True for success, FALSE for failure. */ function file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { - $path_original = is_object($source) ? $source->filepath : $source; if (file_copy($source, $dest, $replace)) { @@ -427,6 +341,59 @@ function file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { return 0; } +/** + * Munge the filename as needed for security purposes. For instance the file + * name "exploit.php.pps" would become "exploit.php_.pps". + * + * @param $filename The name of a file to modify. + * @param $extensions A space separated list of extensions that should not + * be altered. + * @param $alerts Whether alerts (watchdog, drupal_set_message()) should be + * displayed. + * @return $filename The potentially modified $filename. + */ +function file_munge_filename($filename, $extensions, $alerts = TRUE) { + $original = $filename; + + // Allow potentially insecure uploads for very savvy users and admin + if (!variable_get('allow_insecure_uploads', 0)) { + $whitelist = array_unique(explode(' ', trim($extensions))); + + // Split the filename up by periods. The first part becomes the basename + // the last part the final extension. + $filename_parts = explode('.', $filename); + $new_filename = array_shift($filename_parts); // Remove file basename. + $final_extension = array_pop($filename_parts); // Remove final extension. + + // Loop through the middle parts of the name and add an underscore to the + // end of each section that could be a file extension but isn't in the list + // of allowed extensions. + foreach ($filename_parts as $filename_part) { + $new_filename .= '.'. $filename_part; + if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) { + $new_filename .= '_'; + } + } + $filename = $new_filename .'.'. $final_extension; + + if ($alerts && $original != $filename) { + drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $filename))); + } + } + + return $filename; +} + +/** + * Undo the effect of upload_munge_filename(). + * + * @param $filename string filename + * @return string + */ +function file_unmunge_filename($filename) { + return str_replace('_.', '.', $filename); +} + /** * Create a full file path from a directory and filename. If a file with the * specified name already exists, an alternative will be used. @@ -461,7 +428,7 @@ function file_create_filename($basename, $directory) { * Delete a file. * * @param $path A string containing a file path. - * @return True for success, FALSE for failure. + * @return TRUE for success, FALSE for failure. */ function file_delete($path) { if (is_file($path)) { @@ -469,46 +436,304 @@ function file_delete($path) { } } +/** + * Determine the total amount of disk space used by a single user's files, or + * the filesystem as a whole. + * + * @param $uid An optional, user id. A NULL value returns the total space used + * by all files. + */ +function file_space_used($uid = NULL) { + if (is_null($uid)) { + return db_result(db_query('SELECT SUM(filesize) FROM {files} WHERE uid = %d', $uid)); + } + return db_result(db_query('SELECT SUM(filesize) FROM {files}')); +} + /** * Saves a file upload to a new location. The source file is validated as a * proper upload and handled as such. * - * @param $source A string specifying the name of the upload field to save. - * This parameter will contain the resulting destination filename in case of - * success. - * @param $dest A string containing the directory $source should be copied to, - * will use the temporary directory in case no other value is set. - * @param $replace A boolean, set to TRUE if the destination should be replaced - * when in use, but when FALSE append a _X to the filename. - * @return An object containing file info or 0 in case of error. + * The file will be added to the files table as a temporary file. Temorary files + * are periodically cleaned. To make the file permanent file call + * file_set_status() to change it's status. + * + * @param $source + * A string specifying the name of the upload field to save. + * @param $dest + * A string containing the directory $source should be copied to. If this is + * not provided, the temporary directory will be used. + * @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. + * @param $replace + * A boolean indicating whether an existing file of the same name in the + * 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 0 in the event of an error. */ -function file_save_upload($source, $dest = FALSE, $replace = FILE_EXISTS_RENAME) { - // Make sure $source exists && is valid. - if ($file = file_check_upload($source)) { +function file_save_upload($source, $validators = array(), $dest = 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]; + } - // This should be refactored, file_check_upload has already - // moved the file to the temporary folder. + // 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. + switch ($_FILES['files']['error'][$source]) { + // @see http://php.net/manual/en/features.file-upload.errors.php + case UPLOAD_ERR_OK: + break; + + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + drupal_set_message(t('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $source, '%maxsize' => format_size(file_upload_max_size()))), 'error'); + return 0; + + case UPLOAD_ERR_PARTIAL: + case UPLOAD_ERR_NO_FILE: + drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $source)), 'error'); + return 0; + + // Unknown error + default: + drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $source)), 'error'); + return 0; + } + + // Build the list of non-munged extensions. + // @todo: this should not be here. we need to figure out the right place. + $extensions = ''; + foreach ($user->roles as $rid => $name) { + $extensions .= ' '. variable_get("upload_extensions_$rid", + variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp')); + } + + // Begin building file object. + $file = new stdClass(); + $file->filename = file_munge_filename(trim(basename($_FILES['files']['name'][$source]), '.'), $extensions); + $file->filepath = $_FILES['files']['tmp_name'][$source]; + $file->filemime = $_FILES['files']['type'][$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')) { + $file->filemime = 'text/plain'; + $file->filepath .= '.txt'; + $file->filename .= '.txt'; + } + + // Create temporary name/path for newly uploaded files. if (!$dest) { - $dest = file_directory_temp(); - $temporary = 1; - if (is_file($file->filepath)) { - // If this file was uploaded by this user before replace the temporary copy. - $replace = FILE_EXISTS_REPLACE; - } + $dest = file_destination(file_create_path($file->filename), FILE_EXISTS_RENAME); + } + $file->source = $source; + $file->destination = $dest; + $file->filesize = $_FILES['files']['size'][$source]; + + // 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)); } - unset($_SESSION['file_uploads'][is_object($source) ? $source->source : $source]); - if (file_move($file, $dest, $replace)) { - if ($temporary) { - $_SESSION['file_uploads'][is_object($source) ? $source->source : $source] = $file; + // Check for validation errors. + if (!empty($errors)) { + $message = t('The selected file %name could not be uploaded. ', array('%name' => $file->filename)); + if (count($errors) > 1) { + $message .= '<ul><li>'. implode('</li><li>', $errors) .'</li></ul>'; + } + else { + $message .= array_pop($errors); } - return $file; + form_set_error($source, $message); + return 0; } - return 0; + + // 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.')); + watchdog('file', t('Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination', $file->filepath))); + return 0; + } + + // If we made it this far it's safe to record this file in the database. + $file->fid = db_next_id('fid'); + db_query("INSERT INTO {files} (fid, uid, filename, filepath, filemime, filesize, status, timestamp) VALUES (%d, %d, '%s', '%s', '%s', %d, %d, %d)", $file->fid, $user->uid, $file->filename, $file->filepath, $file->filemime, $file->filesize, FILE_STATUS_TEMPORARY, time()); + + // Add file to the cache. + $upload_cache[$source] = $file; + return $file; } return 0; } +/** + * Check for files with names longer than we can store in the database. + * + * @param $file + * A Drupal file object. + * @return + * An array. If the file name is too long, it will contain an error message. + */ +function file_validate_name_length($file) { + $errors = array(); + + if (strlen($file->filename) > 255) { + $errors[] = t('Its name exceeds the 255 characters limit. Please rename the file and try again.'); + } + return $errors; +} + +/** + * Check that the filename ends with an allowed extension. This check is not + * enforced for the user #1. + * + * @param $file + * A Drupal file object. + * @param $extensions + * A string with a space separated + * @return + * An array. If the file name is too long, 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 = '/\.('. ereg_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; +} + +/** + * Check that the file's size is below certain limits. This check is not + * enforced for the user #1. + * + * @param $file + * A Drupal file object. + * @param $file_limit + * 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. + * @return + * An array. If the file name is too long, it will contain an error message. + */ +function file_validate_size($file, $file_limit = 0, $user_limit = 0) { + global $user; + + $errors = array(); + + // Bypass validation for uid = 1. + if ($user->uid != 1) { + if ($file_limit && $file->filesize > $file_limit) { + $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file->filesize), '%maxsize' => format_size($file_limit))); + } + + $total_size = file_space_used($user->uid) + $file->filesize; + if ($user_limit && $total_size > $user_limit) { + $errors[] = t('The file is %filesize which would exceed your disk quota of %quota.', array('%filesize' => format_size($file->filesize), '%quota' => format_size($user_limit))); + } + } + return $errors; +} + +/** + * Check that the file is recognized by image_get_info() as an image. + * + * @param $file + * A Drupal file object. + * @return + * An array. If the file is not an image, it will contain an error message. + */ +function file_validate_is_image(&$file) { + $errors = array(); + + $info = image_get_info($file->filepath); + if (!$info || empty($info['extension'])) { + $errors[] = t('Only JPEG, PNG and GIF images are allowed.'); + } + + return $errors; +} + +/** + * If the file is an image verify that its dimensions are within the specified + * 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. + * @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. + * @return + * An array. If the file is an image and did not meet the requirements, it + * will contain an error message. + */ +function file_validate_image_resolution(&$file, $maximum_dimensions = 0, $minimum_dimensions = 0) { + $errors = array(); + + // Check first that the file is an image. + if ($info = image_get_info($file->filepath)) { + if ($maximum_dimensions) { + // Check that it is smaller than the given dimensions. + list($width, $height) = explode('x', $maximum_dimensions); + if ($info['width'] > $width || $info['height'] > $height) { + // Try to resize the image to fit the dimensions. + if (image_get_toolkit() && image_scale($file->filepath, $file->filepath, $width, $height)) { + drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions))); + + // Clear the cached filesize and refresh the image information. + clearstatcache(); + $info = image_get_info($file->filepath); + $file->filesize = $info['file_size']; + } + else { + $errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions)); + } + } + } + + if ($minimum_dimensions) { + // Check that it is larger than the given dimensions. + list($width, $height) = explode('x', $minimum_dimensions); + if ($info['width'] < $width || $info['height'] < $maxheight) { + $errors[] = t('The image is too small; the minimum dimensions are %dimensions pixels.', array('%dimensions' => $minimum_dimensions)); + } + } + } + + return $errors; +} + /** * Save a string to the specified destination. * @@ -538,6 +763,22 @@ function file_save_data($data, $dest, $replace = FILE_EXISTS_RENAME) { return $file; } +/** + * 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. + */ +function file_set_status(&$file, $status) { + if (db_query('UPDATE {files} SET status = %d WHERE fid = %d', $status, $file->fid)) { + $file->status = $status; + return TRUE; + } + return FALSE; +} + /** * Transfer file using http to client. Pipes a file through Drupal to the * client. @@ -578,7 +819,6 @@ function file_transfer($source, $headers) { * returned headers the download will start with the returned headers. If no * modules respond drupal_not_found() will be returned. */ - function file_download() { // Merge remainder of arguments from GET['q'], into relative file path. $args = func_get_args(); @@ -592,10 +832,10 @@ function file_download() { if (file_exists(file_create_path($filepath))) { $headers = module_invoke_all('file_download', $filepath); if (in_array(-1, $headers)) { - return drupal_access_denied(); + return drupal_access_denied(); } if (count($headers)) { - file_transfer($filepath, $headers); + file_transfer($filepath, $headers); } } return drupal_not_found(); diff --git a/includes/locale.inc b/includes/locale.inc index f2ff4e088b05..c067e1068867 100644 --- a/includes/locale.inc +++ b/includes/locale.inc @@ -612,7 +612,7 @@ function locale_translate_import_form() { */ function locale_translate_import_form_submit($form, &$form_state, $form_values) { // Ensure we have the file uploaded - if ($file = file_check_upload('file')) { + if ($file = file_save_upload('file')) { // Add language, if not yet supported $languages = language_list('language', TRUE); diff --git a/modules/aggregator/aggregator.module b/modules/aggregator/aggregator.module index 4467b893a442..659771a6e77b 100644 --- a/modules/aggregator/aggregator.module +++ b/modules/aggregator/aggregator.module @@ -1080,7 +1080,7 @@ function aggregator_page_category() { drupal_add_feed(url('aggregator/rss/'. arg(2)), variable_get('site_name', 'Drupal') .' '. t('aggregator - @title', array('@title' => $category->title))); - return _aggregator_page_list('SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_category_item} c LEFT JOIN {aggregator_item} i ON c.iid = i.iid LEFT JOIN {aggregator_feed} f ON i.fid = f.fid WHERE cid = '. $category->cid .' ORDER BY timestamp DESC, iid DESC', arg(3)); + return _aggregator_page_list('SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_category_item} c LEFT JOIN {aggregator_item} i ON c.iid = i.iid LEFT JOIN {aggregator_feed} f ON i.fid = f.fid WHERE cid = '. $category->cid .' ORDER BY timestamp DESC, i.iid DESC', arg(3)); } function aggregator_page_list($sql, $header, $categorize) { @@ -1223,7 +1223,7 @@ function aggregator_page_rss() { $category = db_fetch_object(db_query('SELECT cid, title FROM {aggregator_category} WHERE cid = %d', arg(2))); $url = '/categories/'. $category->cid; $title = ' '. t('in category') .' '. $category->title; - $sql = 'SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_category_item} c LEFT JOIN {aggregator_item} i ON c.iid = i.iid LEFT JOIN {aggregator_feed} f ON i.fid = f.fid WHERE cid = %d ORDER BY timestamp DESC, iid DESC'; + $sql = 'SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_category_item} c LEFT JOIN {aggregator_item} i ON c.iid = i.iid LEFT JOIN {aggregator_feed} f ON i.fid = f.fid WHERE cid = %d ORDER BY timestamp DESC, i.iid DESC'; $result = db_query_range($sql, $category->cid, 0, variable_get('feed_default_items', 10)); } // or, get the default aggregator items diff --git a/modules/locale/locale.module b/modules/locale/locale.module index 7d0933e8eaf4..80351506b5b6 100644 --- a/modules/locale/locale.module +++ b/modules/locale/locale.module @@ -366,7 +366,7 @@ function locale_refresh_cache() { * Returns plural form index for a specific number. * * The index is computed from the formula of this language. - * + * * @param $count * Number to return plural for. * @param $langcode @@ -378,7 +378,7 @@ function locale_get_plural($count, $langcode = NULL) { static $locale_formula, $plurals = array(); $langcode = $langcode ? $langcode : $language->language; - + if (!isset($plurals[$langcode][$count])) { if (!isset($locale_formula)) { $language_list = language_list(); diff --git a/modules/system/system.install b/modules/system/system.install index d3bfd6ed7988..96d64bfabf15 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -3306,6 +3306,39 @@ function system_update_6021() { return $ret; } +/** + * Update files tables to associate files to a uid by default instead of a nid. + * Rename file_revisions to upload since it should only be used by the upload + * module used by upload to link files to nodes. + */ +function system_update_6022() { + $ret = array(); + + // Rename the nid field to vid, add status and timestamp fields, and indexes. + db_drop_index($ret, 'files', 'nid'); + db_change_field($ret, 'files', 'nid', 'uid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0)); + db_add_field($ret, 'files', 'status', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); + db_add_field($ret, 'files', 'timestamp', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0)); + db_add_index($ret, 'files', 'uid', array('uid')); + db_add_index($ret, 'files', 'status', array('status')); + db_add_index($ret, 'files', 'timestamp', array('timestamp')); + + // Rename the file_revisions table to upload then add nid column. Since we're + // changing the table name we need to drop and re-add the vid index so both + // pgsql ends up with the corect index name. + db_drop_index($ret, 'file_revisions', 'vid'); + db_rename_table($ret, 'file_revisions', 'upload'); + db_add_field($ret, 'upload', 'nid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0)); + db_add_index($ret, 'upload', 'nid', array('nid')); + db_add_index($ret, 'upload', 'vid', array('vid')); + + // The nid column was renamed to uid. Use the old nid to find the node's uid. + $ret[] = update_sql('UPDATE {files} f JOIN {node} n ON f.uid = n.nid SET f.uid = n.uid'); + // Use the existing vid to find the nid. + $ret[] = update_sql('UPDATE {upload} u JOIN {node_revisions} r ON u.vid = r.vid SET u.nid = r.nid'); + + return $ret; +} /** * @} End of "defgroup updates-5.x-to-6.x" * The next series of updates should start at 7000. diff --git a/modules/system/system.module b/modules/system/system.module index 53b496d30c7b..3a3e094c3bc6 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -13,6 +13,9 @@ define('DRUPAL_MINIMUM_PGSQL', '7.4'); // If using PostgreSQL define('DRUPAL_MINIMUM_APACHE', '1.3'); // If using Apache +// Maximum age of temporary files in seconds. +define('DRUPAL_MAXIMUM_TEMP_FILE_AGE', 1440); + /** * Implementation of hook_help(). */ @@ -83,7 +86,7 @@ function system_theme() { * Implementation of hook_perm(). */ function system_perm() { - return array('administer site configuration', 'access administration pages', 'select different theme'); + return array('administer site configuration', 'access administration pages', 'select different theme', 'administer files'); } /** @@ -2171,28 +2174,29 @@ function system_theme_settings($key = '') { $form['var'] = array('#type' => 'hidden', '#value' => $var); // Check for a new uploaded logo, and use that instead. - if ($file = file_check_upload('logo_upload')) { - if ($info = image_get_info($file->filepath)) { - $parts = pathinfo($file->filename); - $filename = ($key) ? str_replace('/', '_', $key) .'_logo.'. $parts['extension'] : 'logo.'. $parts['extension']; - - if ($file = file_save_upload('logo_upload', $filename, 1)) { - $_POST['default_logo'] = 0; - $_POST['logo_path'] = $file->filepath; - $_POST['toggle_logo'] = 1; - } - } - else { - form_set_error('file_upload', t('Only JPEG, PNG and GIF images are allowed to be used as logos.')); + if ($file = file_save_upload('logo_upload', array('file_validate_is_image' => array()))) { + $parts = pathinfo($file->filename); + $filename = ($key) ? str_replace('/', '_', $key) .'_logo.'. $parts['extension'] : 'logo.'. $parts['extension']; + + // The image was saved using file_save_upload() and was added to the + // files table as a temorary file. We'll make a copy and let the garbage + // collector delete the original upload. + if (file_copy($file, $filename, FILE_EXISTS_REPLACE)) { + $_POST['default_logo'] = 0; + $_POST['logo_path'] = $file->filepath; + $_POST['toggle_logo'] = 1; } } // Check for a new uploaded favicon, and use that instead. - if ($file = file_check_upload('favicon_upload')) { + if ($file = file_save_upload('favicon_upload')) { $parts = pathinfo($file->filename); $filename = ($key) ? str_replace('/', '_', $key) .'_favicon.'. $parts['extension'] : 'favicon.'. $parts['extension']; - if ($file = file_save_upload('favicon_upload', $filename, 1)) { + // The image was saved using file_save_upload() and was added to the + // files table as a temorary file. We'll make a copy and let the garbage + // collector delete the original upload. + if (file_copy($file, $filename)) { $_POST['default_favicon'] = 0; $_POST['favicon_path'] = $file->filepath; $_POST['toggle_favicon'] = 1; @@ -2636,13 +2640,27 @@ function theme_system_admin_by_module($menu_items) { /** * Implementation of hook_cron(). * - * Remove older rows from flood table + * Remove older rows from flood and batch table. Remove old temporary files. */ function system_cron() { - // Cleanup the flood + // Cleanup the flood. db_query('DELETE FROM {flood} WHERE timestamp < %d', time() - 3600); - // Cleanup the batch table + // Cleanup the batch table. db_query('DELETE FROM {batch} WHERE timestamp < %d', time() - 864000); + + // Remove temporary files that are older than DRUPAL_MAXIMUM_TEMP_FILE_AGE. + $result = db_query('SELECT * FROM {files} WHERE status = %s and timestamp < %d', FILE_STATUS_TEMPORARY, time() - DRUPAL_MAXIMUM_TEMP_FILE_AGE); + while ($file = db_fetch_object($result)) { + if (file_exists($file->filepath)) { + // If files that exist cannot be deleted, continue so the database remains + // consistant. + if (!file_delete($file->filepath)) { + watchdog('file system', t('Could not delete temporary file "%path" during garbage collection', array('%path' => $file->filepath)), 'error'); + continue; + } + } + db_query('DELETE FROM {files} WHERE fid = %d', $file->fid); + } } /** diff --git a/modules/system/system.schema b/modules/system/system.schema index 608d752d8003..5cf4b21c7086 100644 --- a/modules/system/system.schema +++ b/modules/system/system.schema @@ -32,26 +32,21 @@ function system_schema() { $schema['files'] = array( 'fields' => array( - 'fid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), - 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), - 'filename' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), - 'filepath' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), - 'filemime' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), - 'filesize' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0) - ), - 'indexes' => array('nid' => array('nid')), - 'primary key' => array('fid'), - ); - - $schema['file_revisions'] = array( - 'fields' => array( - 'fid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), - 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), - 'description' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), - 'list' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, 'size' => 'tiny') + 'fid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), + 'uid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'filename' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'filepath' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'filemime' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'filesize' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'status' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + 'timestamp' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), ), - 'primary key' => array('fid', 'vid'), - 'indexes' => array('vid' => array('vid')), + 'indexes' => array( + 'uid' => array('uid'), + 'status' => array('status'), + 'timestamp' => array('timestamp'), + ), + 'primary key' => array('fid'), ); $schema['flood'] = array( diff --git a/modules/upload/upload.install b/modules/upload/upload.install new file mode 100644 index 000000000000..1c07a190d9a6 --- /dev/null +++ b/modules/upload/upload.install @@ -0,0 +1,18 @@ +<?php +// $Id$ + +/** + * Implementation of hook_install(). + */ +function upload_install() { + // Create tables. + drupal_install_schema('upload'); +} + +/** + * Implementation of hook_uninstall(). + */ +function upload_uninstall() { + // Remove tables. + drupal_uninstall_schema('upload'); +} diff --git a/modules/upload/upload.module b/modules/upload/upload.module index 4e489d323859..c0b7685d1afc 100644 --- a/modules/upload/upload.module +++ b/modules/upload/upload.module @@ -94,28 +94,9 @@ function upload_menu() { } function upload_menu_alter(&$items) { - $items['system/files']['page callback'] = 'upload_download'; $items['system/files']['access arguments'] = array('view uploaded files'); } -function upload_init() { - if (arg(0) == 'system' && arg(1) == 'files' && isset($_SESSION['file_previews'])) { - $item = menu_get_item('system/files'); - foreach ($_SESSION['file_previews'] as $fid => $file) { - $filename = file_create_filename($file->filename, file_create_path()); - if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE) { - // strip file_directory_path() from filename. @see file_create_url - if (strpos($filename, file_directory_path()) !== FALSE) { - $filename = trim(substr($filename, strlen(file_directory_path())), '\\/'); - } - $filename = 'system/files/'. $filename; - } - $_SESSION['file_previews'][$fid]->_filename = $filename; - menu_set_item($filename, $item); - } - } -} - /** * Form API callback to validate the upload settings form. */ @@ -139,11 +120,11 @@ function upload_admin_settings_validate($form, &$form_state, $form_values) { form_set_error('upload_usersize_default', t('The %role file size limit must be a number and greater than zero.', array('%role' => t('default')))); } if ($default_uploadsize > file_upload_max_size()) { - form_set_error('upload_uploadsize_default', $exceed_max_msg . $more_info); - $more_info = ''; + form_set_error('upload_uploadsize_default', $exceed_max_msg . $more_info); + $more_info = ''; } if ($default_uploadsize > $default_usersize) { - form_set_error('upload_uploadsize_default', t('The %role maximum file size per upload is greater than the total file size allowed per user', array('%role' => t('default')))); + form_set_error('upload_uploadsize_default', t('The %role maximum file size per upload is greater than the total file size allowed per user', array('%role' => t('default')))); } foreach ($form_values['roles'] as $rid => $role) { @@ -157,11 +138,11 @@ function upload_admin_settings_validate($form, &$form_state, $form_values) { form_set_error('upload_usersize_'. $rid, t('The %role file size limit must be a number and greater than zero.', array('%role' => $role))); } if ($uploadsize > file_upload_max_size()) { - form_set_error('upload_uploadsize_'. $rid, $exceed_max_msg . $more_info); - $more_info = ''; + form_set_error('upload_uploadsize_'. $rid, $exceed_max_msg . $more_info); + $more_info = ''; } if ($uploadsize > $usersize) { - form_set_error('upload_uploadsize_'. $rid, t('The %role maximum file size per upload is greater than the total file size allowed per user', array('%role' => $role))); + form_set_error('upload_uploadsize_'. $rid, t('The %role maximum file size per upload is greater than the total file size allowed per user', array('%role' => $role))); } } } @@ -185,7 +166,7 @@ function upload_admin_settings() { '#default_value' => variable_get('upload_max_resolution', 0), '#size' => 15, '#maxlength' => 10, - '#description' => t('The maximum allowed image size (e.g. 640x480). Set to 0 for no restriction.'), + '#description' => t('The maximum allowed image size (e.g. 640x480). Set to 0 for no restriction. If an <a href="!image-toolkit-link">image toolkit</a> is installed, files exceeding this value will be scalled down to fit.', array('!image-toolkit-link' => url('admin/settings/image-toolkit'))), '#field_suffix' => '<kbd>'. t('WIDTHxHEIGHT') .'</kbd>' ); $form['settings_general']['upload_list_default'] = array( @@ -209,8 +190,8 @@ function upload_admin_settings() { '#default_value' => $upload_uploadsize_default, '#size' => 5, '#maxlength' => 5, - '#description' => t('The default maximum file size a user can upload.'), - '#field_suffix' => t('MB') + '#description' => t('The default maximum file size a user can upload. If an image is uploaded and a maximum resolution is set, the size will be checked after the file has been resized.'), + '#field_suffix' => t('MB'), ); $form['settings_general']['upload_usersize_default'] = array( '#type' => 'textfield', @@ -219,7 +200,7 @@ function upload_admin_settings() { '#size' => 5, '#maxlength' => 5, '#description' => t('The default maximum size of all files a user can have on the site.'), - '#field_suffix' => t('MB') + '#field_suffix' => t('MB'), ); $form['settings_general']['upload_max_size'] = array('#value' => '<p>'. t('Your PHP settings limit the maximum file size per upload to %size.', array('%size' => format_size(file_upload_max_size()))) .'</p>'); @@ -247,7 +228,8 @@ function upload_admin_settings() { '#default_value' => variable_get('upload_uploadsize_'. $rid, $upload_uploadsize_default), '#size' => 5, '#maxlength' => 5, - '#description' => t('The maximum size of a file a user can upload (in megabytes).'), + '#description' => t('The maximum size of a file a user can upload. If an image is uploaded and a maximum resolution is set, the size will be checked after the file has been resized.'), + '#field_suffix' => t('MB'), ); $form['settings_role_'. $rid]['upload_usersize_'. $rid] = array( '#type' => 'textfield', @@ -255,96 +237,117 @@ function upload_admin_settings() { '#default_value' => variable_get('upload_usersize_'. $rid, $upload_usersize_default), '#size' => 5, '#maxlength' => 5, - '#description' => t('The maximum size of all files a user can have on the site (in megabytes).'), + '#description' => t('The maximum size of all files a user can have on the site.'), + '#field_suffix' => t('MB'), ); } return system_settings_form($form); } -function upload_download() { - foreach ($_SESSION['file_previews'] as $file) { - if ($file->_filename == $_GET['q']) { - file_transfer($file->filepath, array('Content-Type: '. mime_header_encode($file->filemime), 'Content-Length: '. $file->filesize)); - } - } +/** + * Determine the limitations on files that a given user may upload. The user + * may be in multiple roles so we select the most permissive limitations from + * all of their roles. + * + * @param $user + * A Drupal user object. + * @return + * An associative array with the following keys: + * 'extensions' + * A white space separated string containing all the file extensions this + * user may upload. + * 'file_size' + * The maximum size of a file upload in bytes. + * 'user_size' + * The total number of bytes for all for a user's files. + * 'resolution' + * A string specifying the maximum resolution of images. + */ +function _upload_file_limits($user) { + $file_limit = variable_get('upload_uploadsize_default', 1); + $user_limit = variable_get('upload_usersize_default', 1); + $all_extensions = explode(' ', variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp')); + foreach ($user->roles as $rid => $name) { + $extensions = variable_get("upload_extensions_$rid", variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp')); + $all_extensions = array_merge($all_extensions, explode(' ', $extensions)); + + // A zero value indicates no limit, take the least restrictive limit. + $file_size = variable_get("upload_uploadsize_$rid", variable_get('upload_uploadsize_default', 1)) * 1024 * 1024; + $file_limit = ($file_limit && $file_size) ? max($file_limit, $file_size) : 0; + + $user_size = variable_get("upload_usersize_$rid", variable_get('upload_usersize_default', 1)) * 1024 * 1024; + $user_limit = ($user_limit && $user_size) ? max($user_limit, $user_size) : 0; + } + $all_extensions = implode(' ', array_unique($all_extensions)); + return array( + 'extensions' => $all_extensions, + 'file_size' => $file_limit, + 'user_size' => $user_limit, + 'resolution' => variable_get('upload_max_resolution', 0), + ); } +/** + * Implementation of hook_file_download(). + */ function upload_file_download($file) { + if (!user_access('view uploaded files')) { + return -1; + } $file = file_create_path($file); - $result = db_query("SELECT f.* FROM {files} f WHERE filepath = '%s'", $file); + $result = db_query("SELECT f.* FROM {files} f INNER JOIN {upload} u ON f.fid = u.uid WHERE filepath = '%s'", $file); if ($file = db_fetch_object($result)) { - if (user_access('view uploaded files')) { - $node = node_load($file->nid); - if (node_access('view', $node)) { - $type = mime_header_encode($file->filemime); - return array( - 'Content-Type: '. $type, - 'Content-Length: '. $file->filesize, - ); - } - else { - return -1; - } - } - else { - return -1; - } + return array( + 'Content-Type: '. $file->filemime, + 'Content-Length: '. $file->filesize, + ); } } /** - * Save new uploads and attach them to the node object. - * append file_previews to the node object as well. + * Save new uploads and store them in the session to be associated to the node + * on upload_save. + * + * @param $node + * A node object to associate with uploaded files. */ function _upload_prepare(&$node) { + global $user; - // Clean up old file previews if a post didn't get the user to this page. - // i.e. the user left the edit page, because they didn't want to upload anything. - if (count($_POST) == 0) { - if (!empty($_SESSION['file_previews']) && is_array($_SESSION['file_previews'])) { - foreach ($_SESSION['file_previews'] as $fid => $file) { - file_delete($file->filepath); - } - unset($_SESSION['file_previews']); - } + // Initialize _SESSION['upload_files'] if no post occured. + // This clears the variable from old forms and makes sure it + // is an array to prevent notices and errors in other parts + // of upload.module. + if (!$_POST) { + $_SESSION['upload_files'] = array(); } - // $_SESSION['file_current_upload'] tracks the fid of the file submitted this page request. + // $_SESSION['upload_current_file'] tracks the fid of the file submitted this page request. // form_builder sets the value of file->list to 0 for checkboxes added to a form after // it has been submitted. Since unchecked checkboxes have no return value and do not // get a key in _POST form_builder has no way of knowing the difference between a check // box that wasn't present on the last form build, and a checkbox that is unchecked. + unset($_SESSION['upload_current_file']); - unset($_SESSION['file_current_upload']); - - global $user; - - // Save new file uploads to tmp dir. - if (($file = file_check_upload()) && user_access('upload files')) { - - // Scale image uploads. - $file = _upload_image($file); - - $key = 'upload_'. (!isset($_SESSION['file_previews']) ? 0 : count($_SESSION['file_previews'])); - $file->fid = $key; - $file->source = $key; - $file->list = variable_get('upload_list_default', 1); - $_SESSION['file_previews'][$key] = $file; + $limits = _upload_file_limits($user); + $validators = array( + 'file_validate_extensions' => array($limits['extensions']), + 'file_validate_image_resolution' => array($limits['resolution']), + 'file_validate_size' => array($limits['file_size'], $limits['user_size']), + ); - // Store the uploaded fid for this page request in case of submit without - // preview or attach. See earlier notes. - $_SESSION['file_current_upload'] = $key; + // Save new file uploads. + if (($user->uid != 1 || user_access('upload files')) && ($file = file_save_upload('upload', $validators))) { + $file->list = variable_get('upload_list_default',1); + $file->description = $file->filename; + $_SESSION['upload_current_file'] = $file->fid; + $_SESSION['upload_files'][$file->fid] = $file; } - // Attach file previews to node object. - if (!empty($_SESSION['file_previews']) && is_array($_SESSION['file_previews'])) { - foreach ($_SESSION['file_previews'] as $fid => $file) { - if ($user->uid != 1) { - // Here something.php.pps becomes something.php_.pps - $file->filename = upload_munge_filename($file->filename, NULL, 0); - $file->description = $file->filename; - } + // attach session files to node. + if (count($_SESSION['upload_files'])) { + foreach($_SESSION['upload_files'] as $fid => $file) { $node->files[$fid] = $file; } } @@ -407,80 +410,6 @@ function upload_form_alter(&$form, $form_state, $form_id) { } } -function _upload_validate(&$node) { - // Accumulator for disk space quotas. - $filesize = 0; - - // Check if node->files exists, and if it contains something. - if (isset($node->files) && is_array($node->files)) { - // Update existing files with form data. - foreach ($node->files as $fid => $file) { - // Convert file to object for compatibility - $file = (object)$file; - - // Validate new uploads. - if (strpos($fid, 'upload') !== FALSE && empty($file->remove)) { - global $user; - - // Bypass validation for uid = 1. - if ($user->uid != 1) { - // Update filesize accumulator. - $filesize += $file->filesize; - - // Validate file against all users roles. - // Only denies an upload when all roles prevent it. - - $total_usersize = upload_space_used($user->uid) + $filesize; - $error = array(); - foreach ($user->roles as $rid => $name) { - $extensions = variable_get("upload_extensions_$rid", variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp')); - $uploadsize = variable_get("upload_uploadsize_$rid", variable_get('upload_uploadsize_default', 1)) * 1024 * 1024; - $usersize = variable_get("upload_usersize_$rid", variable_get('upload_usersize_default', 1)) * 1024 * 1024; - - $regex = '/\.('. ereg_replace(' +', '|', preg_quote($extensions)) .')$/i'; - - if (!preg_match($regex, $file->filename)) { - $error['extension']++; - } - - if ($uploadsize && $file->filesize > $uploadsize) { - $error['uploadsize']++; - } - - if ($usersize && $total_usersize + $file->filesize > $usersize) { - $error['usersize']++; - } - } - - $user_roles = count($user->roles); - $valid = TRUE; - if ($error['extension'] == $user_roles) { - form_set_error('upload', t('The selected file %name can not be attached to this post, because it is only possible to attach files with the following extensions: %files-allowed.', array('%name' => $file->filename, '%files-allowed' => $extensions))); - $valid = FALSE; - } - elseif ($error['uploadsize'] == $user_roles) { - form_set_error('upload', t('The selected file %name can not be attached to this post, because it exceeded the maximum filesize of %maxsize.', array('%name' => $file->filename, '%maxsize' => format_size($uploadsize)))); - $valid = FALSE; - } - elseif ($error['usersize'] == $user_roles) { - form_set_error('upload', t('The selected file %name can not be attached to this post, because the disk quota of %quota has been reached.', array('%name' => $file->filename, '%quota' => format_size($usersize)))); - $valid = FALSE; - } - elseif (strlen($file->filename) > 255) { - form_set_error('upload', t('The selected file %name can not be attached to this post, because the filename is too long.', array('%name' => $file->filename))); - $valid = FALSE; - } - - if (!$valid) { - unset($node->files[$fid], $_SESSION['file_previews'][$fid]); - file_delete($file->filepath); - } - } - } - } - } -} - /** * Implementation of hook_nodeapi(). */ @@ -499,10 +428,6 @@ function upload_nodeapi(&$node, $op, $teaser) { _upload_prepare($node); break; - case 'validate': - _upload_validate($node); - break; - case 'view': if (isset($node->files) && user_access('view uploaded files')) { // Add the attachments list to node body with a heavy @@ -517,31 +442,7 @@ function upload_nodeapi(&$node, $op, $teaser) { } } break; - case 'alter': - if (isset($node->files) && user_access('view uploaded files')) { - // Manipulate so that inline references work in preview - if (!variable_get('clean_url', 0)) { - $previews = array(); - foreach ($node->files as $file) { - if (strpos($file->fid, 'upload') !== FALSE) { - $previews[] = $file; - } - } - // URLs to files being previewed are actually Drupal paths. When Clean - // URLs are disabled, the two do not match. We perform an automatic - // replacement from temporary to permanent URLs. That way, the author - // can use the final URL in the body before having actually saved (to - // place inline images for example). - foreach ($previews as $file) { - $old = file_create_filename($file->filename, file_create_path()); - $new = url($old); - $node->body = str_replace($old, $new, $node->body); - $node->teaser = str_replace($old, $new, $node->teaser); - } - } - } - break; case 'insert': case 'update': if (user_access('upload files')) { @@ -595,7 +496,7 @@ function theme_upload_attachments($files) { $rows = array(); foreach ($files as $file) { $file = (object)$file; - if ($file->list && !$file->remove) { + if ($file->list && empty($file->remove)) { // Generate valid URL for both existing attachments and preview of new attachments (these have 'upload' in fid) $href = file_create_url((strpos($file->fid, 'upload') === FALSE ? $file->filepath : file_create_filename($file->filename, file_create_path()))); $text = $file->description ? $file->description : $file->filename; @@ -616,7 +517,7 @@ function theme_upload_attachments($files) { * The amount of disk space used by the user in bytes. */ function upload_space_used($uid) { - return db_result(db_query('SELECT SUM(filesize) FROM {files} f INNER JOIN {node} n ON f.nid = n.nid WHERE n.uid = %d', $uid)); + return file_space_used($uid); } /** @@ -626,67 +527,7 @@ function upload_space_used($uid) { * The amount of disk space used by uploaded files in bytes. */ function upload_total_space_used() { - return db_result(db_query('SELECT SUM(filesize) FROM {files}')); -} - -/** - * Munge the filename as needed for security purposes. - * - * @param $filename - * The name of a file to modify. - * @param $extensions - * A space separated list of valid extensions. If this is blank, we'll use - * the admin-defined defaults for the user role from upload_extensions_$rid. - * @param $alerts - * Whether alerts (watchdog, drupal_set_message()) should be displayed. - * @return $filename - * The potentially modified $filename. - */ -function upload_munge_filename($filename, $extensions = NULL, $alerts = 1) { - global $user; - - $original = $filename; - - // Allow potentially insecure uploads for very savvy users and admin - if (!variable_get('allow_insecure_uploads', 0)) { - - if (!isset($extensions)) { - $extensions = ''; - foreach ($user->roles as $rid => $name) { - $extensions .= ' '. variable_get("upload_extensions_$rid", variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp')); - } - - } - - $whitelist = array_unique(explode(' ', trim($extensions))); - - $filename_parts = explode('.', $filename); - - $new_filename = array_shift($filename_parts); // Remove file basename. - $final_extension = array_pop($filename_parts); // Remove final extension. - - foreach ($filename_parts as $filename_part) { - $new_filename .= ".$filename_part"; - if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) { - $new_filename .= '_'; - } - } - $filename = "$new_filename.$final_extension"; - } - - if ($alerts && $original != $filename) { - $message = t('Your filename has been renamed to conform to site policy.'); - drupal_set_message($message); - } - - return $filename; -} - -/** - * Undo the effect of upload_munge_filename(). - */ -function upload_unmunge_filename($filename) { - return str_replace('_.', '.', $filename); + return db_result(db_query('SELECT SUM(f.filesize) FROM {files} f INNER JOIN {upload} u ON f.fid = u.fid')); } function upload_save(&$node) { @@ -701,69 +542,52 @@ function upload_save(&$node) { // Remove file. Process removals first since no further processing // will be required. if ($file->remove) { - // Remove file previews... - if (strpos($file->fid, 'upload') !== FALSE) { - file_delete($file->filepath); - } - - // Remove managed files. - else { - db_query('DELETE FROM {file_revisions} WHERE fid = %d AND vid = %d', $fid, $node->vid); - // Only delete a file if it isn't used by any revision - $count = db_result(db_query('SELECT COUNT(fid) FROM {file_revisions} WHERE fid = %d', $fid)); - if ($count < 1) { - db_query('DELETE FROM {files} WHERE fid = %d', $fid); - file_delete($file->filepath); - } - } - } - - // New file upload - elseif (strpos($file->fid, 'upload') !== FALSE) { - if ($file = file_save_upload($file, $file->filename)) { - $file->fid = db_next_id('{files}_fid'); - db_query("INSERT INTO {files} (fid, nid, filename, filepath, filemime, filesize) VALUES (%d, %d, '%s', '%s', '%s', %d)", $file->fid, $node->nid, $file->filename, $file->filepath, $file->filemime, $file->filesize); - db_query("INSERT INTO {file_revisions} (fid, vid, list, description) VALUES (%d, %d, %d, '%s')", $file->fid, $node->vid, $file->list, $file->description); - // Tell other modules where the file was stored. - $node->files[$fid] = $file; - } - unset($_SESSION['file_previews'][$fid]); + db_query('DELETE FROM {upload} WHERE fid = %d AND vid = %d', $fid, $node->vid); + // Remove it from the session in the case of new uploads, + // that you want to disassociate before node submission. + unset($_SESSION['upload_files'][$fid]); + // Move on, so the removed file won't be added to new revisions. + continue; } - // Create a new revision, as needed - elseif ($node->old_vid && is_numeric($fid)) { - db_query("INSERT INTO {file_revisions} (fid, vid, list, description) VALUES (%d, %d, %d, '%s')", $file->fid, $node->vid, $file->list, $file->description); + // Create a new revision, or associate a new file needed. + if (!empty($node->old_vid) || array_key_exists($fid, $_SESSION['upload_files'])) { + db_query("INSERT INTO {upload} (fid, nid, vid, list, description) VALUES (%d, %d, %d, %d, '%s')", $file->fid, $node->nid, $node->vid, $file->list, $file->description); + file_set_status($file, FILE_STATUS_PERMANENT); } - - // Update existing revision + // Update existing revision. else { - db_query("UPDATE {file_revisions} SET list = %d, description = '%s' WHERE fid = %d AND vid = %d", $file->list, $file->description, $file->fid, $node->vid); + db_query("UPDATE {upload} SET list = %d, description = '%s' WHERE fid = %d AND vid = %d", $file->list, $file->description, $file->fid, $node->vid); + 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. + unset($_SESSION['upload_files']); } function upload_delete($node) { $files = array(); - $result = db_query('SELECT * FROM {files} WHERE nid = %d', $node->nid); + $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; } foreach ($files as $fid => $file) { - // Delete all file revision information associated with the node - db_query('DELETE FROM {file_revisions} WHERE fid = %d', $fid); + // Delete all files associated with the node + db_query('DELETE FROM {files} WHERE fid = %d', $fid); file_delete($file->filepath); } - // Delete all files associated with the node - db_query('DELETE FROM {files} WHERE nid = %d', $node->nid); + // 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 {file_revisions} WHERE fid = %d', $file->fid)); + $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) { @@ -774,10 +598,11 @@ function upload_delete_revision($node) { } // delete the revision - db_query('DELETE FROM {file_revisions} WHERE vid = %d', $node->vid); + db_query('DELETE FROM {upload} WHERE vid = %d', $node->vid); } function _upload_form($node) { + global $user; $form['#theme'] = 'upload_form_new'; @@ -785,7 +610,8 @@ function _upload_form($node) { $form['files']['#theme'] = 'upload_form_current'; $form['files']['#tree'] = TRUE; foreach ($node->files as $key => $file) { - // Generate valid URL for both existing attachments and preview of new attachments (these have 'upload' in fid) + // Generate valid URL for both existing attachments and preview of new + // attachments (these have 'upload' in fid). $description = file_create_url((strpos($file->fid, 'upload') === FALSE ? $file->filepath : file_create_filename($file->filename, file_create_path()))); $description = "<small>". check_plain($description) ."</small>"; $form['files'][$key]['description'] = array('#type' => 'textfield', '#default_value' => !empty($file->description) ? $file->description : $file->filename, '#maxlength' => 256, '#description' => $description ); @@ -793,9 +619,10 @@ function _upload_form($node) { $form['files'][$key]['size'] = array('#value' => 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); - // if the file was uploaded this page request, set value. this fixes the problem - // formapi has recognizing new checkboxes. see comments in _upload_prepare. - if (isset($_SESSION['file_current_upload']) && $_SESSION['file_current_upload'] == $file->fid) { + // If the file was uploaded this page request, set value. this fixes the + // problem formapi has recognizing new checkboxes. see comments in + // _upload_prepare. + if (isset($_SESSION['upload_current_file']) && $_SESSION['upload_current_file'] == $file->fid) { $form['files'][$key]['list']['#value'] = variable_get('upload_list_default', 1); } $form['files'][$key]['filename'] = array('#type' => 'value', '#value' => $file->filename); @@ -807,12 +634,19 @@ function _upload_form($node) { } if (user_access('upload files')) { + $limits = _upload_file_limits($user); + // This div is hidden when the user uploads through JS. $form['new'] = array( '#prefix' => '<div id="attach-hide">', '#suffix' => '</div>', ); - $form['new']['upload'] = array('#type' => 'file', '#title' => t('Attach new file'), '#size' => 40); + $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']))), + ); $form['new']['attach'] = array( '#type' => 'submit', '#value' => t('Attach'), @@ -824,7 +658,7 @@ function _upload_form($node) { $form['attach-url'] = array('#type' => 'hidden', '#value' => url('upload/js', array('absolute' => TRUE)), '#attributes' => array('class' => 'upload')); } - // Needed for JS + // Needed for JS. $form['current']['vid'] = array('#type' => 'hidden', '#value' => isset($node->vid) ? $node->vid : 0); return $form; } @@ -861,7 +695,7 @@ function upload_load($node) { $files = array(); if ($node->vid) { - $result = db_query('SELECT * FROM {files} f INNER JOIN {file_revisions} r ON f.fid = r.fid WHERE r.vid = %d ORDER BY f.fid', $node->vid); + $result = db_query('SELECT * FROM {files} f INNER JOIN {upload} r ON f.fid = r.fid WHERE r.vid = %d ORDER BY f.fid', $node->vid); while ($file = db_fetch_object($result)) { $files[$file->fid] = $file; } @@ -870,27 +704,6 @@ function upload_load($node) { return $files; } -/** - * Check an upload, if it is an image, make sure it fits within the - * maximum dimensions allowed. - */ -function _upload_image($file) { - $info = image_get_info($file->filepath); - - if ($info) { - list($width, $height) = explode('x', variable_get('upload_max_resolution', '0x0')); - if ($width && $height) { - $result = image_scale($file->filepath, $file->filepath, $width, $height); - if ($result) { - $file->filesize = filesize($file->filepath); - drupal_set_message(t('The image was resized to fit within the maximum allowed resolution of %resolution pixels.', array('%resolution' => variable_get('upload_max_resolution', 0)))); - } - } - } - - return $file; -} - /** * Menu-callback for JavaScript-based uploads. */ @@ -903,7 +716,6 @@ function upload_js() { // Handle new uploads, and merge tmp files into node-files. _upload_prepare($node); - _upload_validate($node); $form = _upload_form($node); $form += array( @@ -915,6 +727,8 @@ function upload_js() { drupal_alter('form', $form, array(), 'upload_js'); $form_state = array('submitted' => FALSE); $form = form_builder('upload_js', $form, $form_state); + // @todo: Put status messages inside wrapper, instead of above so they do not + // persist across ajax reloads. $output = theme('status_messages') . drupal_render($form); // We send the updated file attachments form. print drupal_to_js(array('status' => TRUE, 'data' => $output)); diff --git a/modules/upload/upload.schema b/modules/upload/upload.schema new file mode 100644 index 000000000000..09843b58fd8f --- /dev/null +++ b/modules/upload/upload.schema @@ -0,0 +1,19 @@ +<?php +// $Id$ + +function upload_schema() { + $schema['upload'] = array( + 'fields' => array( + 'fid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'description' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'list' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, 'size' => 'tiny') + ), + 'primary key' => array('fid', 'vid'), + 'indexes' => array('vid' => array('vid'), 'nid' => array('nid')), + ); + + return $schema; +} + diff --git a/modules/user/user.module b/modules/user/user.module index c3df981c4467..e858d2de1f14 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -320,32 +320,22 @@ function user_validate_mail($mail) { function user_validate_picture(&$form, &$form_state, $form_values) { // If required, validate the uploaded picture. - if (isset($form['picture']) && ($file = file_check_upload('picture_upload'))) { - // Check that uploaded file is an image, with a maximum file size - // and maximum height/width. + $validators = array( + 'file_validate_is_image' => array(), + 'file_validate_image_resolution' => array(variable_get('user_picture_dimensions', '85x85')), + 'file_validate_size' => array(variable_get('user_picture_file_size', '30') * 1024), + ); + if ($file = file_save_upload('picture_upload', $validators)) { + // The image was saved using file_save_upload() and was added to the + // files table as a temorary file. We'll make a copy and let the garbage + // collector delete the original upload. $info = image_get_info($file->filepath); - list($maxwidth, $maxheight) = explode('x', variable_get('user_picture_dimensions', '85x85')); - - if (!$info || !$info['extension']) { - form_set_error('picture_upload', t('The uploaded file was not an image.')); - } - else if (image_get_toolkit()) { - image_scale($file->filepath, $file->filepath, $maxwidth, $maxheight); - } - else if (filesize($file->filepath) > (variable_get('user_picture_file_size', '30') * 1000)) { - form_set_error('picture_upload', t('The uploaded image is too large; the maximum file size is %size kB.', array('%size' => variable_get('user_picture_file_size', '30')))); + $destination = variable_get('user_picture_path', 'pictures') .'/picture-'. $form['#uid'] .'.'. $info['extension']; + if (file_copy($file, $destination, FILE_EXISTS_REPLACE)) { + $form_values['picture'] = $file->filepath; } - else if ($info['width'] > $maxwidth || $info['height'] > $maxheight) { - form_set_error('picture_upload', t('The uploaded image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => variable_get('user_picture_dimensions', '85x85')))); - } - - if (!form_get_errors()) { - if ($file = file_save_upload('picture_upload', variable_get('user_picture_path', 'pictures') .'/picture-'. $form['#uid'] .'.'. $info['extension'], 1)) { - $form_values['picture'] = $file->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')))); - } + 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')))); } } } diff --git a/themes/bluemarine/style-rtl.css b/themes/bluemarine/style-rtl.css index f672decbbd7b..eb313114907b 100644 --- a/themes/bluemarine/style-rtl.css +++ b/themes/bluemarine/style-rtl.css @@ -3,35 +3,35 @@ body { direction: rtl; } #logo img { - float: right; + float: right; } #menu { - padding: 0.5em 0.5em 0 0.5em; - text-align: left; + padding: 0.5em 0.5em 0 0.5em; + text-align: left; } #navlist { - padding: 0 0 1.2em 0.8em; + padding: 0 0 1.2em 0.8em; } #subnavlist { - padding: 0.5em 0 0.4em 1.2em; + padding: 0.5em 0 0.4em 1.2em; } ul.links li { - border-right: 1px solid #9cf; + border-right: 1px solid #9cf; border-left: inherit; } .block, .box { padding: 0 1.5em 0 0; } .node .taxonomy { - padding-right: 1.5em; + padding-right: 1.5em; } .node .picture { float: left; } .comment .new { text-align: left; - float: left; + float: left; } .comment .picture { - float: left; + float: left; } diff --git a/themes/bluemarine/style.css b/themes/bluemarine/style.css index 2d5332d19bab..c88def14310b 100644 --- a/themes/bluemarine/style.css +++ b/themes/bluemarine/style.css @@ -138,7 +138,7 @@ ul.links li.first { font-weight: bold; } .site-name { - margin: 0.6em 0 0 ; + margin: 0.6em 0 0 ; padding: 0; font-size: 2em; } -- GitLab