Commit 8fd2b477 authored by webchick's avatar webchick

Issue #625958 by slashrsm, quicksketch, eojthebrave, droplet, danlinn, czigor:...

Issue #625958 by slashrsm, quicksketch, eojthebrave, droplet, danlinn, czigor: Support Uploading Multiple Files for HTML5 Browsers via #multiple attribute.
parent 8733925f
......@@ -1020,14 +1020,14 @@ function file_unmanaged_delete_recursive($path, $callback = NULL) {
}
/**
* Saves a file upload to a new location.
* Saves file uploads to a new location.
*
* The file will be added to the {file_managed} table as a temporary file.
* The files will be added to the {file_managed} table as temporary files.
* Temporary files are periodically cleaned. Use file_usage()->add() to register
* the usage of the file which will automatically mark it as permanent.
*
* @param $source
* A string specifying the filepath or URI of the uploaded file to save.
* A string specifying the filepath or URI of the uploaded files to save.
* @param $validators
* An optional, associative array of callback functions used to validate the
* file. See file_validate() for a full discussion of the array format.
......@@ -1041,6 +1041,8 @@ function file_unmanaged_delete_recursive($path, $callback = NULL) {
* A string containing the URI $source should be copied to.
* This must be a stream wrapper URI. If this value is omitted, Drupal's
* temporary files scheme will be used ("temporary://").
* @param $delta
* Delta of the file to save or NULL to save all files. Defaults to NULL.
* @param $replace
* Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE: Replace the existing file.
......@@ -1049,181 +1051,209 @@ function file_unmanaged_delete_recursive($path, $callback = NULL) {
* - FILE_EXISTS_ERROR: Do nothing and return FALSE.
*
* @return
* An object containing the file information if the upload succeeded, FALSE
* in the event of an error, or NULL if no file was uploaded. The
* documentation for the "File interface" group, which you can find under
* Function returns array of files or a single file object if $delta
* != NULL. Each file object contains the file information if the
* upload succeeded or FALSE in the event of an error. Function
* returns NULL if no file was uploaded.
*
* The documentation for the "File interface" group, which you can find under
* Related topics, or the header at the top of this file, documents the
* components of a file entity. In addition to the standard components,
* this function adds:
* - source: Path to the file before it is moved.
* - destination: Path to the file after it is moved (same as 'uri').
*/
function file_save_upload($source, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
function file_save_upload($source, $validators = array(), $destination = FALSE, $delta = NULL, $replace = FILE_EXISTS_RENAME) {
global $user;
static $upload_cache;
// Make sure there's an upload to process.
if (empty($_FILES['files']['name'][$source])) {
return NULL;
}
// Return cached objects without processing since the file will have
// already been processed and the paths in _FILES will be invalid.
// already been processed and the paths in $_FILES will be invalid.
if (isset($upload_cache[$source])) {
if (isset($delta)) {
return $upload_cache[$source][$delta];
}
return $upload_cache[$source];
}
// Make sure there's an upload to process.
if (empty($_FILES['files']['name'][$source])) {
return NULL;
// Prepare uploaded files info. Representation is slightly different
// for multiple uploads and we fix that here.
$uploaded_files = $_FILES;
if (!is_array($uploaded_files['files']['name'][$source])) {
foreach (array('name', 'type', 'tmp_name', 'error', 'size') as $value)
$uploaded_files['files'][$value][$source] = array($uploaded_files['files'][$value][$source]);
}
// Check for file upload errors and return FALSE if a lower level system
// error occurred. For a complete list of errors:
// See http://php.net/manual/features.file-upload.errors.php.
switch ($_FILES['files']['error'][$source]) {
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' => $_FILES['files']['name'][$source], '%maxsize' => format_size(file_upload_max_size()))), 'error');
return FALSE;
$files = array();
foreach ($uploaded_files['files']['name'][$source] as $i => $name) {
// Check for file upload errors and return FALSE for this file if a lower
// level system error occurred. For a complete list of errors:
// See http://php.net/manual/features.file-upload.errors.php.
switch ($uploaded_files['files']['error'][$source][$i]) {
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' => $name, '%maxsize' => format_size(file_upload_max_size()))), 'error');
$files[$i] = FALSE;
continue;
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' => $_FILES['files']['name'][$source])), 'error');
return FALSE;
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' => $name)), 'error');
$files[$i] = FALSE;
continue;
case UPLOAD_ERR_OK:
// Final check that this is a valid upload, if it isn't, use the
// default error handler.
if (is_uploaded_file($_FILES['files']['tmp_name'][$source])) {
break;
}
case UPLOAD_ERR_OK:
// Final check that this is a valid upload, if it isn't, use the
// default error handler.
if (is_uploaded_file($uploaded_files['files']['tmp_name'][$source][$i])) {
break;
}
// Unknown error
default:
drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $name)), 'error');
$files[$i] = FALSE;
continue;
// Unknown error
default:
drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$source])), 'error');
return FALSE;
}
// Begin building file entity.
$values = array(
'uid' => $user->uid,
'status' => 0,
'filename' => trim(drupal_basename($_FILES['files']['name'][$source]), '.'),
'uri' => $_FILES['files']['tmp_name'][$source],
'filesize' => $_FILES['files']['size'][$source],
);
$values['filemime'] = file_get_mimetype($values['filename']);
$file = entity_create('file', $values);
$extensions = '';
if (isset($validators['file_validate_extensions'])) {
if (isset($validators['file_validate_extensions'][0])) {
// Build the list of non-munged extensions if the caller provided them.
$extensions = $validators['file_validate_extensions'][0];
}
// Begin building file entity.
$values = array(
'uid' => $user->uid,
'status' => 0,
'filename' => trim(drupal_basename($name, '.')),
'uri' => $uploaded_files['files']['tmp_name'][$source][$i],
'filesize' => $uploaded_files['files']['size'][$source][$i],
);
$values['filemime'] = file_get_mimetype($values['filename']);
$file = entity_create('file', $values);
$extensions = '';
if (isset($validators['file_validate_extensions'])) {
if (isset($validators['file_validate_extensions'][0])) {
// Build the list of non-munged extensions if the caller provided them.
$extensions = $validators['file_validate_extensions'][0];
}
else {
// If 'file_validate_extensions' is set and the list is empty then the
// caller wants to allow any extension. In this case we have to remove the
// validator or else it will reject all extensions.
unset($validators['file_validate_extensions']);
}
}
else {
// If 'file_validate_extensions' is set and the list is empty then the
// caller wants to allow any extension. In this case we have to remove the
// validator or else it will reject all extensions.
unset($validators['file_validate_extensions']);
// No validator was provided, so add one using the default list.
// Build a default non-munged safe list for file_munge_filename().
$extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
$validators['file_validate_extensions'] = array();
$validators['file_validate_extensions'][0] = $extensions;
}
}
else {
// No validator was provided, so add one using the default list.
// Build a default non-munged safe list for file_munge_filename().
$extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
$validators['file_validate_extensions'] = array();
$validators['file_validate_extensions'][0] = $extensions;
}
if (!empty($extensions)) {
// Munge the filename to protect against possible malicious extension hiding
// within an unknown file type (ie: filename.html.foo).
$file->filename = file_munge_filename($file->filename, $extensions);
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!config('system.file')->get('allow_insecure_uploads') && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
$file->filemime = 'text/plain';
$file->uri .= '.txt';
$file->filename .= '.txt';
// The .txt extension may not be in the allowed list of extensions. We have
// to add it here or else the file upload will fail.
if (!empty($extensions)) {
$validators['file_validate_extensions'][0] .= ' txt';
drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->filename)));
// Munge the filename to protect against possible malicious extension
// hiding within an unknown file type (ie: filename.html.foo).
$file->filename = file_munge_filename($file->filename, $extensions);
}
}
// If the destination is not provided, use the temporary directory.
if (empty($destination)) {
$destination = 'temporary://';
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!config('system.file')->get('allow_insecure_uploads') && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
$file->filemime = 'text/plain';
$file->uri .= '.txt';
$file->filename .= '.txt';
// The .txt extension may not be in the allowed list of extensions. We have
// to add it here or else the file upload will fail.
if (!empty($extensions)) {
$validators['file_validate_extensions'][0] .= ' txt';
drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->filename)));
}
}
// Assert that the destination contains a valid stream.
$destination_scheme = file_uri_scheme($destination);
if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
drupal_set_message(t('The file could not be uploaded because the destination %destination is invalid.', array('%destination' => $destination)), 'error');
return FALSE;
}
// If the destination is not provided, use the temporary directory.
if (empty($destination)) {
$destination = 'temporary://';
}
$file->source = $source;
// A URI may already have a trailing slash or look like "public://".
if (substr($destination, -1) != '/') {
$destination .= '/';
}
$file->destination = file_destination($destination . $file->filename, $replace);
// If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and
// there's an existing file so we need to bail.
if ($file->destination === FALSE) {
drupal_set_message(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $source, '%directory' => $destination)), 'error');
return FALSE;
}
// Assert that the destination contains a valid stream.
$destination_scheme = file_uri_scheme($destination);
if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
drupal_set_message(t('The file could not be uploaded because the destination %destination is invalid.', array('%destination' => $destination)), 'error');
$files[$i] = FALSE;
continue;
}
// Add in our check of the the file name length.
$validators['file_validate_name_length'] = array();
$file->source = $source;
// A file URI may already have a trailing slash or look like "public://".
if (substr($destination, -1) != '/') {
$destination .= '/';
}
$file->destination = file_destination($destination . $file->filename, $replace);
// If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
// there's an existing file so we need to bail.
if ($file->destination === FALSE) {
drupal_set_message(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $source, '%directory' => $destination)), 'error');
$files[$i] = FALSE;
continue;
}
// Call the validation functions specified by this function's caller.
$errors = file_validate($file, $validators);
// Add in our check of the the file name length.
$validators['file_validate_name_length'] = array();
// Check for errors.
if (!empty($errors)) {
$message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename));
if (count($errors) > 1) {
$message .= theme('item_list', array('items' => $errors));
// Call the validation functions specified by this function's caller.
$errors = file_validate($file, $validators);
// Check for errors.
if (!empty($errors)) {
$message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename));
if (count($errors) > 1) {
$message .= theme('item_list', array('items' => $errors));
}
else {
$message .= ' ' . array_pop($errors);
}
form_set_error($source, $message);
$files[$i] = FALSE;
continue;
}
else {
$message .= ' ' . array_pop($errors);
// Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary
// directory. This overcomes open_basedir restrictions for future file
// operations.
$file->uri = $file->destination;
if (!drupal_move_uploaded_file($uploaded_files['files']['tmp_name'][$source][$i], $file->uri)) {
form_set_error($source, t('File upload error. Could not move uploaded file.'));
watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri));
$files[$i] = FALSE;
continue;
}
form_set_error($source, $message);
return FALSE;
}
// Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary
// directory. This overcomes open_basedir restrictions for future file
// operations.
$file->uri = $file->destination;
if (!drupal_move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->uri)) {
form_set_error($source, t('File upload error. Could not move uploaded file.'));
watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri));
return FALSE;
}
// Set the permissions on the new file.
drupal_chmod($file->uri);
// Set the permissions on the new file.
drupal_chmod($file->uri);
// If we are replacing an existing file re-use its database record.
if ($replace == FILE_EXISTS_REPLACE) {
$existing_files = entity_load_multiple_by_properties('file', array('uri' => $file->uri));
if (count($existing_files)) {
$existing = reset($existing_files);
$file->fid = $existing->fid;
// If we are replacing an existing file re-use its database record.
if ($replace == FILE_EXISTS_REPLACE) {
$existing_files = entity_load_multiple_by_properties('file', array('uri' => $file->uri));
if (count($existing_files)) {
$existing = reset($existing_files);
$file->fid = $existing->fid;
}
}
// If we made it this far it's safe to record this file in the database.
$file->save();
$files[$i] = $file;
}
// If we made it this far it's safe to record this file in the database.
$file->save();
// Add file to the cache.
$upload_cache[$source] = $file;
return $file;
// Add files to the cache.
$upload_cache[$source] = $files;
return isset($delta) ? $files[$delta] : $files;
}
/**
......@@ -1390,7 +1420,7 @@ function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
// Avoid warnings when opendir does not have the permissions to open a
// directory.
if (is_dir($dir)) {
if($handle = @opendir($dir)) {
if ($handle = @opendir($dir)) {
while (FALSE !== ($filename = readdir($handle))) {
// Skip this file if it matches the nomask or starts with a dot.
if ($filename[0] != '.' && !preg_match($options['nomask'], $filename)) {
......
......@@ -4608,6 +4608,17 @@ function form_pre_render_file($element) {
return $element;
}
/**
* Processes a file upload element, make use of #multiple if present.
*/
function form_process_file($element) {
if ($element['#multiple']) {
$element['#attributes'] = array('multiple' => 'multiple');
$element['#name'] .= '[]';
}
return $element;
}
/**
* Returns HTML for a form element.
*
......
......@@ -188,7 +188,7 @@ function aggregator_form_opml_validate($form, &$form_state) {
function aggregator_form_opml_submit($form, &$form_state) {
$data = '';
$validators = array('file_validate_extensions' => array('opml xml'));
if ($file = file_save_upload('upload', $validators)) {
if ($file = file_save_upload('upload', $validators, FALSE, 0)) {
$data = file_get_contents($file->uri);
}
else {
......
......@@ -389,7 +389,7 @@ function file_field_widget_value($element, $input = FALSE, $form_state) {
// Ensure that all the required properties are returned even if empty.
$return += array(
'fid' => 0,
'fids' => array(),
'display' => 1,
'description' => '',
);
......@@ -397,6 +397,47 @@ function file_field_widget_value($element, $input = FALSE, $form_state) {
return $return;
}
/**
* Validation callback for upload element on file widget. Checks if user has
* uploaded more files than allowed.
*
* This validator is used only when cardinality not set to 1 or unlimited.
*/
function file_field_widget_multiple_count_validate($element, &$form_state, $form) {
$parents = $element['#parents'];
$values = NestedArray::getValue($form_state['values'], $parents);
array_pop($parents);
$current = count(element_children(NestedArray::getValue($form, $parents))) - 1;
$field = field_info_field($element['#field_name']);
$uploaded = count($values['fids']);
$count = $uploaded + $current;
if ($count > $field['cardinality']) {
$keep = $uploaded - $count + $field['cardinality'];
$removed_files = array_slice($values['fids'], $keep);
$removed_names = array();
foreach ($removed_files as $fid) {
$file = file_load($fid);
$removed_names[] = $file->filename;
}
drupal_set_message(
t(
'Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.',
array(
'%field' => $field['field_name'],
'@max' => $field['cardinality'],
'@count' => $keep,
'%list' => implode(', ', $removed_names),
)
),
'warning'
);
$values['fids'] = array_slice($values['fids'], 0, $keep);
NestedArray::setValue($form_state['values'], $element['#parents'], $values);
}
}
/**
* Render API callback: Processes a file_generic field element.
*
......@@ -406,7 +447,7 @@ function file_field_widget_value($element, $input = FALSE, $form_state) {
*/
function file_field_widget_process($element, &$form_state, $form) {
$item = $element['#value'];
$item['fid'] = $element['fid']['#value'];
$item['fids'] = $element['fids']['#value'];
$field = field_widget_field($element, $form_state);
$instance = field_widget_instance($element, $form_state);
......@@ -415,9 +456,9 @@ function file_field_widget_process($element, &$form_state, $form) {
$element['#theme'] = 'file_widget';
// Add the display field if enabled.
if (!empty($field['settings']['display_field']) && $item['fid']) {
if (!empty($field['settings']['display_field']) && $item['fids']) {
$element['display'] = array(
'#type' => empty($item['fid']) ? 'hidden' : 'checkbox',
'#type' => empty($item['fids']) ? 'hidden' : 'checkbox',
'#title' => t('Include file in display'),
'#value' => isset($item['display']) ? $item['display'] : $field['settings']['display_default'],
'#attributes' => array('class' => array('file-display')),
......@@ -431,7 +472,7 @@ function file_field_widget_process($element, &$form_state, $form) {
}
// Add the description field if enabled.
if (!empty($instance['settings']['description_field']) && $item['fid']) {
if (!empty($instance['settings']['description_field']) && $item['fids']) {
$config = config('file.settings');
$element['description'] = array(
'#type' => $config->get('description.type'),
......@@ -561,18 +602,34 @@ function file_field_widget_submit($form, &$form_state) {
$langcode = $element['#language'];
$parents = $element['#field_parents'];
$submitted_values = NestedArray::getValue($form_state['values'], array_slice($button['#array_parents'], 0, -2));
$submitted_values = NestedArray::getValue($form_state['values'], array_slice($button['#parents'], 0, -2));
foreach ($submitted_values as $delta => $submitted_value) {
if (!$submitted_value['fid']) {
if (empty($submitted_value['fids'])) {
unset($submitted_values[$delta]);
}
}
// If there are more files uploaded via the same widget, we have to separate
// them, as we display each file in it's own widget.
$new_values = array();
foreach ($submitted_values as $delta => $submitted_value) {
if (is_array($submitted_value['fids'])) {
foreach ($submitted_value['fids'] as $fid) {
$new_value = $submitted_value;
$new_value['fids'] = array($fid);
$new_values[] = $new_value;
}
}
else {
$new_value = $submitted_value;
}
}
// Re-index deltas after removing empty items.
$submitted_values = array_values($submitted_values);
$submitted_values = array_values($new_values);
// Update form_state values.
NestedArray::setValue($form_state['values'], array_slice($button['#array_parents'], 0, -2), $submitted_values);
NestedArray::setValue($form_state['values'], array_slice($button['#parents'], 0, -2), $submitted_values);
// Update items.
$field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
......@@ -595,9 +652,10 @@ function theme_file_widget($variables) {
// The "form-managed-file" class is required for proper Ajax functionality.
$output .= '<div class="file-widget form-managed-file clearfix">';
if ($element['fid']['#value'] != 0) {
if (!empty($element['fids']['#value'])) {
// Add the file size after the file name.
$element['filename']['#markup'] .= ' <span class="file-size">(' . format_size($element['#file']->filesize) . ')</span> ';
$file = reset($element['#files']);
$element['file_' . $file->fid]['filename']['#markup'] .= ' <span class="file-size">(' . format_size($file->filesize) . ')</span> ';
}
$output .= drupal_render_children($element);
$output .= '</div>';
......@@ -644,7 +702,7 @@ function theme_file_widget_multiple($variables) {
$rows = array();
foreach ($widgets as $key => &$widget) {
// Save the uploading row for last.
if ($widget['#file'] == FALSE) {
if (empty($widget['#files'])) {
$widget['#title'] = $element['#file_upload_title'];
$widget['#description'] = $element['#file_upload_description'];
continue;
......@@ -726,6 +784,7 @@ function theme_file_widget_multiple($variables) {
function theme_file_upload_help($variables) {
$description = $variables['description'];
$upload_validators = $variables['upload_validators'];
$cardinality = $variables['cardinality'];
$descriptions = array();
......@@ -738,6 +797,14 @@ function theme_file_upload_help($variables) {
if (isset($upload_validators['file_validate_extensions'])) {
$descriptions[] = t('Allowed file types: !extensions.', array('!extensions' => '<strong>' . check_plain($upload_validators['file_validate_extensions'][0]) . '</strong>'));
}
if (isset($cardinality)) {
if ($cardinality == -1) {
$descriptions[] = t('Unlimited number of files can be uploaded to this field.');
}
else {
$descriptions[] = format_plural($cardinality, 'This field can store only one file.', 'This field can store at most @count files.');
}
}
if (isset($upload_validators['file_validate_image_resolution'])) {
$max = $upload_validators['file_validate_image_resolution'][0];
$min = $upload_validators['file_validate_image_resolution'][1];
......
......@@ -246,7 +246,7 @@ function file_update_8000() {
));
}
/**
/*
* Convert the 'id' column in {file_usage} to accept UUIDs.
*/
function file_update_8001() {
......@@ -259,3 +259,32 @@ function file_update_8001() {
);
db_change_field('file_usage', 'id', 'id', $spec);
}
/**
* Convert image field's default image configuration to the new format.
*/
function file_update_8002() {
if (module_exists('field_sql_storage')) {
$fields = field_read_fields(array('type' => 'image'), array('include_deleted' => TRUE, 'include_inactive' => TRUE));