diff --git a/core/includes/file.inc b/core/includes/file.inc index ed4bdce..8f838e2 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -1380,6 +1380,8 @@ function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) { * * @param $source * A string specifying the filepath or URI of the uploaded file to save. + * @param $delta + * The delta of the file being uploaded. Used in conjunction with #multiple. * @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. @@ -1410,52 +1412,52 @@ function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) { * - 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, $delta = 0, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) { global $user; static $upload_cache; // 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 (isset($upload_cache[$source][$delta])) { + return $upload_cache[$source][$delta]; } // Make sure there's an upload to process. - if (empty($_FILES['files']['name'][$source])) { + if (empty($_FILES['files']['name'][$source][$delta])) { return NULL; } // 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]) { + switch ($_FILES['files']['error'][$source][$delta]) { 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'); + 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][$delta], '%maxsize' => format_size(file_upload_max_size()))), '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' => $_FILES['files']['name'][$source])), 'error'); + drupal_set_message(t('The file %file could not be saved because the upload did not complete.', array('%file' => $_FILES['files']['name'][$source][$delta])), 'error'); return FALSE; 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])) { + if (is_uploaded_file($_FILES['files']['tmp_name'][$source][$delta])) { break; } // 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'); + drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$source][$delta])), 'error'); return FALSE; } // Begin building file entity. $values = array( 'uid' => $user->uid, 'status' => 0, - 'filename' => trim(drupal_basename($_FILES['files']['name'][$source]), '.'), + 'filename' => trim(drupal_basename($_FILES['files']['name'][$source][$delta]), '.'), 'uri' => $_FILES['files']['tmp_name'][$source], 'filesize' => $_FILES['files']['size'][$source], ); @@ -1553,7 +1555,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE, // 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)) { + if (!drupal_move_uploaded_file($_FILES['files']['tmp_name'][$source][$delta], $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; @@ -1574,7 +1576,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE, // If we made it this far it's safe to record this file in the database. $file->save(); // Add file to the cache. - $upload_cache[$source] = $file; + $upload_cache[$source][$delta] = $file; return $file; } diff --git a/core/includes/form.inc b/core/includes/form.inc index 362d0d5..0a8ca78 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -4356,6 +4356,17 @@ function theme_file($variables) { } /** + * Processes a file upload element, make use of #multiple if present. + */ +function form_process_file($element) { + if ($element['#multiple'] == TRUE) { + $element['#attributes'] = array('multiple' => 'multiple'); + } + $element['#name'] .= '[]'; + return $element; +} + +/** * Returns HTML for a form element. * * Each form element is wrapped in a DIV container having the following CSS diff --git a/core/modules/file/file.module b/core/modules/file/file.module index d2f6f76..d77479b 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -76,6 +76,7 @@ function file_element_info() { '#upload_validators' => array(), '#upload_location' => NULL, '#size' => 22, + '#multiple' => FALSE, '#extended' => FALSE, '#attached' => array( 'css' => array($file_path . '/file.admin.css'), @@ -370,11 +371,14 @@ function file_file_predelete(File $file) { * This function is assigned as a #process callback in file_element_info(). */ function file_managed_file_process($element, &$form_state, $form) { - $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0; + // This is used some times so let's implode it just once. + $parents_prefix = implode('_', $element['#parents']); + + $fids = isset($element['#value']['fids']) ? $element['#value']['fids'] : array(); // Set some default element properties. $element['#progress_indicator'] = empty($element['#progress_indicator']) ? 'none' : $element['#progress_indicator']; - $element['#file'] = $fid ? file_load($fid) : FALSE; + $element['#files'] = !empty($fids) ? file_load_multiple($fids) : FALSE; $element['#tree'] = TRUE; $ajax_settings = array( @@ -389,7 +393,7 @@ function file_managed_file_process($element, &$form_state, $form) { // Set up the buttons first since we need to check if they were clicked. $element['upload_button'] = array( - '#name' => implode('_', $element['#parents']) . '_upload_button', + '#name' => $parents_prefix . '_upload_button', '#type' => 'submit', '#value' => t('Upload'), '#validate' => array(), @@ -398,26 +402,26 @@ function file_managed_file_process($element, &$form_state, $form) { '#ajax' => $ajax_settings, '#weight' => -5, ); - - // Force the progress indicator for the remove button to be either 'none' or - // 'throbber', even if the upload button is using something else. - $ajax_settings['progress']['type'] = ($element['#progress_indicator'] == 'none') ? 'none' : 'throbber'; - $ajax_settings['progress']['message'] = NULL; - $ajax_settings['effect'] = 'none'; $element['remove_button'] = array( - '#name' => implode('_', $element['#parents']) . '_remove_button', + '#name' => $parents_prefix . '_remove_button', '#type' => 'submit', - '#value' => t('Remove'), + '#value' => $element['#multiple'] ? t('Remove selected') : t('Remove'), '#validate' => array(), '#submit' => array('file_managed_file_submit'), '#limit_validation_errors' => array($element['#parents']), '#ajax' => $ajax_settings, - '#weight' => -5, + '#weight' => 1, ); - $element['fid'] = array( + // Force the progress indicator for the remove button to be either 'none' or + // 'throbber', even if the upload button is using something else. + $ajax_settings['progress']['type'] = ($element['#progress_indicator'] == 'none') ? 'none' : 'throbber'; + $ajax_settings['progress']['message'] = NULL; + $ajax_settings['effect'] = 'none'; + + $element['fids'] = array( '#type' => 'hidden', - '#value' => $fid, + '#value' => $fids, ); // Add progress bar support to the upload if possible. @@ -451,21 +455,32 @@ function file_managed_file_process($element, &$form_state, $form) { // The file upload field itself. $element['upload'] = array( - '#name' => 'files[' . implode('_', $element['#parents']) . ']', + '#name' => 'files[' . $parents_prefix . ']', '#type' => 'file', '#title' => t('Choose a file'), '#title_display' => 'invisible', '#size' => $element['#size'], + '#multiple' => $element['#multiple'], '#theme_wrappers' => array(), '#weight' => -10, ); - if ($fid && $element['#file']) { - $element['filename'] = array( - '#type' => 'markup', - '#markup' => theme('file_link', array('file' => $element['#file'])) . ' ', - '#weight' => -10, - ); + if (!empty($fids) && $element['#files']) { + foreach ($element['#files'] as $delta => $file) { + if ($element['#multiple']) { + $element['file_' . $delta]['selected'] = array( + '#type' => 'checkbox', + '#title' => theme('file_link', array('file' => $file)) . ' ', + ); + } + else { + $element['file_' . $delta]['filename'] = array( + '#type' => 'markup', + '#markup' => theme('file_link', array('file' => $file)) . ' ', + '#weight' => -10, + ); + } + } } // Add the extension list to the page as JavaScript settings. @@ -492,28 +507,30 @@ function file_managed_file_process($element, &$form_state, $form) { * This function is assigned as a #value_callback in file_element_info(). */ function file_managed_file_value(&$element, $input = FALSE, $form_state = NULL) { - $fid = 0; - - // Find the current value of this field from the form state. - $form_state_fid = $form_state['values']; - foreach ($element['#parents'] as $parent) { - $form_state_fid = isset($form_state_fid[$parent]) ? $form_state_fid[$parent] : 0; - } - - if ($element['#extended'] && isset($form_state_fid['fid'])) { - $fid = $form_state_fid['fid']; - } - elseif (is_numeric($form_state_fid)) { - $fid = $form_state_fid; - } + $fids = array(); + + // Find the current value of this field. + $fids = !empty($input['fids']) ? explode(' ', $input['fids']) : array(); + $fids = array_map( + function($item) { + return (int) $item; + }, + $fids + ); + $input['fids'] = $fids; // Process any input and save new uploads. if ($input !== FALSE) { $return = $input; // Uploads take priority over all other values. - if ($file = file_managed_file_save_upload($element)) { - $fid = $file->fid; + if ($files = file_managed_file_save_upload($element)) { + if ($element['#multiple']) { + $fids = array_merge($fids, array_keys($files)); + } + else { + $fids = array_keys($files); + } } else { // Check for #filefield_value_callback values. @@ -525,9 +542,15 @@ function file_managed_file_value(&$element, $input = FALSE, $form_state = NULL) $callback($element, $input, $form_state); } } - // Load file if the FID has changed to confirm it exists. - if (isset($input['fid']) && $file = file_load($input['fid'])) { - $fid = $file->fid; + + // Load files if the FIDs has changed to confirm they exist. + if (!empty($input['fids'])) { + $fids = array(); + foreach ($input['fids'] as $key => $fid) { + if ($file = file_load($fid)) { + $fids[] = $file->fid; + } + } } } } @@ -535,22 +558,26 @@ function file_managed_file_value(&$element, $input = FALSE, $form_state = NULL) // If there is no input, set the default value. else { if ($element['#extended']) { - $default_fid = isset($element['#default_value']['fid']) ? $element['#default_value']['fid'] : 0; - $return = isset($element['#default_value']) ? $element['#default_value'] : array('fid' => 0); + $default_fids = isset($element['#default_value']['fids']) ? $element['#default_value']['fids'] : array(); + $return = isset($element['#default_value']) ? $element['#default_value'] : array('fids' => $default_fids); } else { - $default_fid = isset($element['#default_value']) ? $element['#default_value'] : 0; - $return = array('fid' => 0); + $default_fids = isset($element['#default_value']) ? $element['#default_value'] : array(); + $return = array('fids' => array()); } // Confirm that the file exists when used as a default value. - if ($default_fid && $file = file_load($default_fid)) { - $fid = $file->fid; + if (!empty($default_fids)) { + $fids = array(); + foreach ($default_fids as $key => $fid) { + if ($file = file_load($input['fid'])) { + $fids[] = $file->fid; + } + } } } - $return['fid'] = $fid; - + $return['fids'] = $fids; return $return; } @@ -565,28 +592,47 @@ function file_managed_file_validate(&$element, &$form_state) { // references. This prevents unmanaged files from being deleted if this // item were to be deleted. $clicked_button = end($form_state['triggering_element']['#parents']); - if ($clicked_button != 'remove_button' && !empty($element['fid']['#value'])) { - if ($file = file_load($element['fid']['#value'])) { - if ($file->status == FILE_STATUS_PERMANENT) { - $references = file_usage_list($file); - if (empty($references)) { - form_error($element, t('The file used in the !name field may not be referenced.', array('!name' => $element['#title']))); + if ($clicked_button != 'remove_button' && !empty($element['fids']['#value'])) { + $fids = is_array($element['fids']['#value']) ? $element['fids']['#value'] : explode(' ', $element['fids']['#value']); + foreach ($fids as $fid) { + if ($file = file_load($fid)) { + if ($file->status == FILE_STATUS_PERMANENT) { + $references = file_usage_list($file); + if (empty($references)) { + form_error($element, t('The file used in the !name field may not be referenced.', array('!name' => $element['#title']))); + } } } - } - else { - form_error($element, t('The file referenced by the !name field does not exist.', array('!name' => $element['#title']))); + else { + form_error($element, t('The file referenced by the !name field does not exist.', array('!name' => $element['#title']))); + } } } // Check required property based on the FID. - if ($element['#required'] && empty($element['fid']['#value']) && !in_array($clicked_button, array('upload_button', 'remove_button'))) { + if ($element['#required'] && empty($element['fids']['#value']) && !in_array($clicked_button, array('upload_button', 'remove_button'))) { form_error($element['upload'], t('!name field is required.', array('!name' => $element['#title']))); } - // Consolidate the array value of this field to a single FID. + // Save entire values to storage + $values = drupal_array_get_nested_value($form_state['values'], $element['#array_parents']); + drupal_array_set_nested_value($form_state['storage']['managed_file_values'], $element['#array_parents'], $values); + + // Add 'fid' to values for backward compatibility when not using #multiple. + if (!$element['#multiple']) { + $values = drupal_array_get_nested_value($form_state['values'], $element['#array_parents']); + $values['fid'] = reset($values['fids']); + form_set_value($element, $values, $form_state); + } + + // Consolidate the array value of this field to array of fids. if (!$element['#extended']) { - form_set_value($element, $element['fid']['#value'], $form_state); + if ($element['#multiple']) { + form_set_value($element, $element['fids']['#value'], $form_state); + } + else { + form_set_value($element, $element['fids']['#value'][0], $form_state); + } } } @@ -607,11 +653,29 @@ function file_managed_file_submit($form, &$form_state) { // button was clicked. Action is needed here for the remove button, because we // only remove a file in response to its remove button being clicked. if ($button_key == 'remove_button') { - // If it's a temporary file we can safely remove it immediately, otherwise - // it's up to the implementing module to remove usages of files to have them - // removed. - if ($element['#file'] && $element['#file']->status == 0) { - file_delete($element['#file']->fid); + // Get files that need to be removed from list. + $fids = array(); + $values = drupal_array_get_nested_value($form_state['storage']['managed_file_values'], $parents); + if ($element['#multiple']) { + foreach ($values as $name => $value) { + if (strpos($name, 'file_') === 0 && $value['selected']) { + $fids[] = (int) substr($name, 5); + } + } + } + else { + $fids = $values['fids']; + } + + array_diff($values['fids'], $fids); + + foreach ($fids as $fid) { + // If it's a temporary file we can safely remove it immediately, otherwise + // it's up to the implementing module to remove usages of files to have them + // removed. + if ($element['#files'][$fid] && $element['#files'][$fid]->status == 0) { + file_delete($element['#files'][$fid]->fid); + } } // Update both $form_state['values'] and $form_state['input'] to reflect // that the file has been removed, so that the form is rebuilt correctly. @@ -620,9 +684,9 @@ function file_managed_file_submit($form, &$form_state) { // when the managed_file element is part of a field widget. // $form_state['input'] must be updated so that file_managed_file_value() // has correct information during the rebuild. - $values_element = $element['#extended'] ? $element['fid'] : $element; - form_set_value($values_element, NULL, $form_state); - drupal_array_set_nested_value($form_state['input'], $values_element['#parents'], NULL); + $values_element = $element['#extended'] ? $element['fids'] : $element; + form_set_value($values_element, $values['fids'], $form_state); + drupal_array_set_nested_value($form_state['input'], $values_element['#parents'], implode(' ', $values['fids'])); } // Set the form to rebuild so that $form is correctly updated in response to @@ -657,13 +721,19 @@ function file_managed_file_save_upload($element) { return FALSE; } - if (!$file = file_save_upload($upload_name, $element['#upload_validators'], $destination)) { - watchdog('file', 'The file upload failed. %upload', array('%upload' => $upload_name)); - form_set_error($upload_name, t('The file in the !name field was unable to be uploaded.', array('!name' => $element['#title']))); - return FALSE; + // Loop through any attached files and save them to the database. + $files = array(); + $file_count = count(array_filter($_FILES['files']['name'][$upload_name])); + while ($file_count > 0) { + $file_count--; + if (!$file = file_save_upload($upload_name, $file_count, $element['#upload_validators'], $destination)) { + watchdog('file', 'The file upload failed. %upload', array('%upload' => $upload_name)); + form_set_error($upload_name, t('The file in the !name field was unable to be uploaded.', array('!name' => $element['#title']))); + } + $files[$file->fid] = $file; } - return $file; + return $files; } /** @@ -718,9 +788,11 @@ function theme_file_managed_file($variables) { */ function file_managed_file_pre_render($element) { // If we already have a file, we don't want to show the upload controls. - if (!empty($element['#value']['fid'])) { - $element['upload']['#access'] = FALSE; - $element['upload_button']['#access'] = FALSE; + if (!empty($element['#value']['fids'])) { + if (!$element['#multiple']) { + $element['upload']['#access'] = FALSE; + $element['upload_button']['#access'] = FALSE; + } } // If we don't already have a file, there is nothing to remove. else { diff --git a/core/modules/file/tests/file_module_test.module b/core/modules/file/tests/file_module_test.module index f61d67d..52369ce 100644 --- a/core/modules/file/tests/file_module_test.module +++ b/core/modules/file/tests/file_module_test.module @@ -31,7 +31,7 @@ function file_module_test_menu() { * @see file_module_test_form_submit() * @ingroup forms */ -function file_module_test_form($form, &$form_state, $tree = TRUE, $extended = FALSE, $default_fid = NULL) { +function file_module_test_form($form, &$form_state, $tree = TRUE, $extended = TRUE, $default_fids = NULL) { $form['#tree'] = (bool) $tree; $form['nested']['file'] = array( @@ -41,9 +41,11 @@ function file_module_test_form($form, &$form_state, $tree = TRUE, $extended = FA '#progress_message' => t('Please wait...'), '#extended' => (bool) $extended, '#size' => 13, + '#multiple' => TRUE, ); - if ($default_fid) { - $form['nested']['file']['#default_value'] = $extended ? array('fid' => $default_fid) : $default_fid; + if ($default_fids) { + $default_fids = explode(',', $default_fids); + $form['nested']['file']['#default_value'] = $extended ? array('fid' => $default_fids) : $default_fids; } $form['textfield'] = array( @@ -64,12 +66,28 @@ function file_module_test_form($form, &$form_state, $tree = TRUE, $extended = FA */ function file_module_test_form_submit($form, &$form_state) { if ($form['#tree']) { - $fid = $form['nested']['file']['#extended'] ? $form_state['values']['nested']['file']['fid'] : $form_state['values']['nested']['file']; + if ($form['nested']['file']['#extended']) { + $fids = array(); + foreach ($form_state['values']['nested']['file'] as $fid) { + $fids[] = $fid; + } + } + else { + $fids = $form_state['values']['nested']['file']; + } } else { - $fid = $form['nested']['file']['#extended'] ? $form_state['values']['file']['fid'] : $form_state['values']['file']; + if ($form['nested']['file']['#extended']) { + $fids = array(); + foreach ($form_state['values']['file'] as $file) { + $fids[] = $file['fid']; + } + } + else { + $fids = $form_state['values']['file']; + } } - drupal_set_message(t('The file id is %fid.', array('%fid' => $fid))); + drupal_set_message(t('The file ids are %fid.', array('%fid' => $fid))); } /** diff --git a/core/modules/system/system.module b/core/modules/system/system.module index de7241f..6b3a02c 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -523,6 +523,8 @@ function system_element_info() { ); $types['file'] = array( '#input' => TRUE, + '#multiple' => FALSE, + '#process' => array('form_process_file'), '#size' => 60, '#theme' => 'file', '#theme_wrappers' => array('form_element'),