diff --git a/modules/file/file.field.inc b/modules/file/file.field.inc index 1189704..c7485f9 100644 --- a/modules/file/file.field.inc +++ b/modules/file/file.field.inc @@ -408,6 +408,17 @@ function file_field_widget_info() { 'default value' => FIELD_BEHAVIOR_NONE, ), ), + 'file_mfw' => array( + 'label' => t('Multiupload'), + 'field types' => array('file'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_CUSTOM, + 'default value' => FIELD_BEHAVIOR_NONE, + ), + 'settings' => array( + 'progress_indicator' => 'throbber', + ), + ), ); } @@ -430,6 +441,7 @@ function file_field_widget_settings_form($field, $instance) { '#weight' => 16, '#access' => file_progress_implementation(), ); + $form['#attached']['js'] = array(drupal_get_path('module', 'multiupload_filefield_widget') . '/mfw.js'); return $form; } @@ -444,28 +456,72 @@ function file_field_widget_form(&$form, &$form_state, $field, $instance, $langco 'display' => !empty($field['settings']['display_default']), 'description' => '', ); + if($instance['widget']['type']=="file_generic") { + $widget_type = "generic"; + } elseif($instance['widget']['type']=="file_mfw") { + $widget_type = "multi"; + } - // Load the items for form rebuilds from the field state as they might not be - // in $form_state['values'] because of validation limitations. Also, they are - // only passed in as $items when editing existing entities. - $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state); - if (isset($field_state['items'])) { - $items = $field_state['items']; - } - - // Essentially we use the managed_file type, extended with some enhancements. - $element_info = element_info('managed_file'); - $element += array( - '#type' => 'managed_file', - '#upload_location' => file_field_widget_uri($field, $instance), - '#upload_validators' => file_field_widget_upload_validators($field, $instance), - '#value_callback' => 'file_field_widget_value', - '#process' => array_merge($element_info['#process'], array('file_field_widget_process')), - '#progress_indicator' => $instance['widget']['settings']['progress_indicator'], - // Allows this field to return an array instead of a single value. - '#extended' => TRUE, - ); + if($widget_type == "generic") { + // Load the items for form rebuilds from the field state as they might not be + // in $form_state['values'] because of validation limitations. Also, they are + // only passed in as $items when editing existing entities. + $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state); + if (isset($field_state['items'])) { + $items = $field_state['items']; + } + // Essentially we use the managed_file type, extended with some enhancements. + $element_info = element_info('managed_file'); + $element += array( + '#type' => 'managed_file', + '#upload_location' => file_field_widget_uri($field, $instance), + '#upload_validators' => file_field_widget_upload_validators($field, $instance), + '#value_callback' => 'file_field_widget_value', + '#process' => array_merge($element_info['#process'], array('file_field_widget_process')), + '#progress_indicator' => $instance['widget']['settings']['progress_indicator'], + // Allows this field to return an array instead of a single value. + '#extended' => TRUE, + ); + } elseif($widget_type == "multi") { + // Retrieve any values set in $form_state, as will be the case during AJAX + // rebuilds of this form. + if (isset($form_state['values'])) { + $path = array_merge($element['#field_parents'], array($field['field_name'], $langcode)); + $path_exists = FALSE; + $values = drupal_array_get_nested_value($form_state['values'], $path, $path_exists); + if ($path_exists) { + $items = $values; + drupal_array_set_nested_value($form_state['values'], $path, NULL); + } + } + foreach ($items as $delta => $item) { + $items[$delta] = array_merge($defaults, $items[$delta]); + // Remove any items from being displayed that are not needed. + if ($items[$delta]['fid'] == 0) { + unset($items[$delta]); + } + } + + // Re-index deltas after removing empty items. + $items = array_values($items); + + // Update order according to weight. + $items = _field_sort_items($field, $items); + + // Essentially we use the mfw_managed_file type, extended with some enhancements. + $element_info = element_info('mfw_managed_file'); + $element += array( + '#type' => 'mfw_managed_file', + '#default_value' => isset($items[$delta]) ? $items[$delta] : $defaults, + '#upload_location' => file_field_widget_uri($field, $instance), + '#upload_validators' => file_field_widget_upload_validators($field, $instance), + '#value_callback' => 'mfw_field_widget_value', + '#process' => array_merge($element_info['#process'], array('mfw_field_widget_process')), + // Allows this field to return an array instead of a single value. + '#extended' => TRUE, + ); + } if ($field['cardinality'] == 1) { // Set the default value. $element['#default_value'] = !empty($items) ? $items[0] : $defaults; @@ -477,6 +533,7 @@ function file_field_widget_form(&$form, &$form_state, $field, $instance, $langco } else { // If there are multiple values, add an element for each existing one. + $delta = 0; foreach ($items as $item) { $elements[$delta] = $element; $elements[$delta]['#default_value'] = $item; @@ -496,7 +553,6 @@ function file_field_widget_form(&$form, &$form_state, $field, $instance, $langco $elements['#file_upload_delta'] = $delta; $elements['#theme'] = 'file_widget_multiple'; $elements['#theme_wrappers'] = array('fieldset'); - $elements['#process'] = array('file_field_widget_process_multiple'); $elements['#title'] = $element['#title']; $elements['#description'] = $element['#description']; $elements['#field_name'] = $element['#field_name']; @@ -508,8 +564,12 @@ function file_field_widget_form(&$form, &$form_state, $field, $instance, $langco // a hook_form_alter(). $elements['#file_upload_title'] = t('Add a new file'); $elements['#file_upload_description'] = theme('file_upload_help', array('description' => '', 'upload_validators' => $elements[0]['#upload_validators'])); + if($widget_type == "generic") { + $elements['#process'] = array('file_field_widget_process_multiple'); + } elseif ($widget_type == "multi") { + $elements['#process'] = array('mfw_field_widget_process_multiple'); + } } - return $elements; } @@ -1006,3 +1066,92 @@ function theme_file_formatter_table($variables) { return empty($rows) ? '' : theme('table', array('header' => $header, 'rows' => $rows)); } + +//Multi file processing + +/** + * The #value_callback for the file_mfw field element. + */ +function mfw_field_widget_value($element, $input = FALSE, &$form_state) { + if ($input) { + // Checkboxes lose their value when empty. + // If the display field is present make sure its unchecked value is saved. + $field = field_widget_field($element, $form_state); + if (empty($input['display'])) { + $input['display'] = $field['settings']['display_field'] ? 0 : 1; + } + } + + // We depend on the mfw managed file element to handle uploads. + $return = mfw_managed_file_value($element, $input, $form_state); + + // Ensure that all the required properties are returned even if empty. + $return += array( + 'fid' => 0, + 'display' => 1, + 'description' => '', + ); + + $last_parent = $element['#parents'][count($element['#parents']) - 1]; + $form_state['values'][$element['#field_name']]['und'][$last_parent] = $return; + + return $return; +} + +/** + * An element #process callback for the mfw_file field type. + * + * Expands the mfw_file type to include the description and display fields. + */ +function mfw_field_widget_process($element, &$form_state, &$form) { + return file_field_widget_process($element, $form_state, $form); +} + +/** + * An element #process callback for a group of mfw_file fields. + * + * Adds the weight field to each row so it can be ordered and adds a new AJAX + * wrapper around the entire group so it can be replaced all at once. + */ +function mfw_field_widget_process_multiple($element, &$form_state, $form) { + $upload_name = 'files_' . implode('_', $element['#parents']) . '_' . $element['#file_upload_delta']; + if (array_key_exists($upload_name, $_FILES)) { + $count = count($_FILES[$upload_name]['name']); + //Supposing #file_upload_delta is always the last delta this will work + for ($i = 1; $i < $count; $i++) { + $element[] = $element[$element['#file_upload_delta']]; + } + } + + $element_children = element_children($element, TRUE); + $count = count($element_children); + + foreach ($element_children as $delta => $key) { + if ($key < $element['#file_upload_delta']) { + $description = _file_field_get_description_from_element($element[$key]); + $element[$key]['_weight'] = array( + '#type' => 'weight', + '#title' => $description ? t('Weight for @title', array('@title' => $description)) : t('Weight for new file'), + '#title_display' => 'invisible', + '#delta' => $count, + '#default_value' => $delta, + ); + } + else { + // The title needs to be assigned to the upload field so that validation + // errors include the correct widget label. + $element[$key]['#title'] = $element['#title']; + $element[$key]['_weight'] = array( + '#type' => 'hidden', + '#default_value' => $delta, + ); + } + } + + // Add a new wrapper around all the elements for AJAX replacement. + $element['#prefix'] = '
'; + $element['#suffix'] = '
'; + + return $element; +} + diff --git a/modules/file/file.module b/modules/file/file.module index 3d351fa..bb3d783 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -79,6 +79,25 @@ function file_element_info() { 'js' => array($file_path . '/file.js'), ), ); + $types['mfw_managed_file'] = array( + '#input' => TRUE, + '#process' => array('mfw_managed_file_process'), + '#value_callback' => 'mfw_managed_file_value', + '#element_validate' => array('file_managed_file_validate'), + '#pre_render' => array('file_managed_file_pre_render'), + '#theme' => 'file_managed_file', + '#theme_wrappers' => array('form_element'), + '#progress_indicator' => 'throbber', + '#progress_message' => NULL, + '#upload_validators' => array(), + '#upload_location' => NULL, + '#size' => 22, + '#extended' => FALSE, + '#attached' => array( + 'css' => array($file_path . '/file.css'), + 'js' => array($file_path . '/mfw.js', $file_path . '/file.js'), + ), + ); return $types; } @@ -1011,3 +1030,341 @@ function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISI /** * @} End of "defgroup file-module-api". */ + +// Multiple File Upload helper functions + +/** + * Process function to expand the mfw_managed_file element type. + * + * Expands the file type to include Upload and Remove buttons, as well as + * support for a default value. + */ +function mfw_managed_file_process($element, &$form_state, &$form) { + $element = file_managed_file_process($element, $form_state, $form); + $element['upload']['#attributes'] = array('multiple' => 'multiple'); + $element['upload']['#name'] = 'files_' . implode('_', $element['#parents']) . '[]'; + $element['upload']['#attached']['js'][0]['data']['mfw'] = $element['upload']['#attached']['js'][0]['data']['file']; + unset($element['upload']['#attached']['js'][0]['data']['file']); + return $element; +} + +/** + * The #value_callback for a mfw_managed_file type element. + * + */ +function mfw_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; + } + + // Process any input and save new uploads. + if ($input !== FALSE) { + $return = $input; + $element['#file_upload_delta_original'] = isset($form_state['complete form'][$element['#parents'][0]][$element['#parents'][1]]['#file_upload_delta']) ? $form_state['complete form'][$element['#parents'][0]][$element['#parents'][1]]['#file_upload_delta'] : 0; + + // Uploads take priority over all other values. + if ($file = mfw_managed_file_save_upload($element)) { + $fid = $file->fid; + } + else { + // Check for #filefield_value_callback values. + // Because FAPI does not allow multiple #value_callback values like it + // does for #element_validate and #process, this fills the missing + // functionality to allow File fields to be extended through FAPI. + if (isset($element['#file_value_callbacks'])) { + foreach ($element['#file_value_callbacks'] as $callback) { + $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; + } + } + } + + // 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); + } + else { + $default_fid = isset($element['#default_value']) ? $element['#default_value'] : 0; + $return = array('fid' => 0); + } + + // Confirm that the file exists when used as a default value. + if ($default_fid && $file = file_load($default_fid)) { + $fid = $file->fid; + } + } + + $return['fid'] = $fid; + + return $return; +} + +/** + * Given a mfw_managed_file element, save any files that have been uploaded into it. + * + * + * @param $element + * The FAPI element whose values are being saved. + * @return + * The file object representing the file that was saved, or FALSE if no file + * was saved. + */ +function mfw_managed_file_save_upload($element) { + $last_parent = array_pop($element['#parents']); + $upload_name = 'files_' . implode('_', $element['#parents']) . '_' . $element['#file_upload_delta_original']; + array_push($element['#parents'], $last_parent); + + $file_number = $last_parent - $element['#file_upload_delta_original']; + + if (isset($_FILES[$upload_name]['name'][$file_number])) { + $name = $_FILES[$upload_name]['name'][$file_number]; + if (empty($name)) { + return FALSE; + } + + $destination = isset($element['#upload_location']) ? $element['#upload_location'] : NULL; + if (isset($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) { + watchdog('file', 'The upload directory %directory for the file field !name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', array('%directory' => $destination, '!name' => $element['#field_name'])); + form_set_error($upload_name, t('The file could not be uploaded.')); + return FALSE; + } + + if (!$file = mfw_file_save_upload($upload_name, $file_number, $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; + } + return $file; + } + else { + return FALSE; + } +} + + +/** + * Saves a file upload to a new location. + * + * The file will be added to the {file_managed} table as a temporary file. + * Temporary files are periodically cleaned. To make the file a permanent file, + * assign the status and use file_save() to save the changes. + * + * @param $source + * A string specifying the filepath or URI of the uploaded file to save. + * @param $file_number + * The array key of the file to save in $_FILES[$source]['name']. + * @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. + * If no extension validator is provided it will default to a limited safe + * list of extensions which is as follows: "jpg jpeg gif png txt + * doc xls pdf ppt pps odt ods odp". To allow all extensions you must + * explicitly set the 'file_validate_extensions' validator to an empty array + * (Beware: this is not safe and should only be allowed for trusted users, if + * at all). + * @param $destination + * 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 $replace + * Replace behavior when the destination file already exists: + * - FILE_EXISTS_REPLACE: Replace the existing file. + * - FILE_EXISTS_RENAME: Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR: Do nothing and return FALSE. + * + * @return + * An 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 + * Related topics, or the header at the top of this file, documents the + * components of a file object. 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 mfw_file_save_upload($source, $file_number, $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][$file_number])) { + return $upload_cache[$source][$file_number]; + } + + // Make sure there's an upload to process. + if (empty($_FILES[$source]['name'][$file_number])) { + 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/en/features.file-upload.errors.php. + switch ($_FILES[$source]['error'][$file_number]) { + 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[$source]['name'][$file_number], '%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[$source]['name'][$file_number])), '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[$source]['tmp_name'][$file_number])) { + break; + } + + // Unknown error + default: + drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES[$source]['name'][$source])), 'error'); + return FALSE; + } + + // Begin building file object. + $file = new stdClass(); + $file->uid = $user->uid; + $file->status = 0; + $file->filename = trim(basename($_FILES[$source]['name'][$file_number]), '.'); + $file->uri = $_FILES[$source]['tmp_name'][$file_number]; + $file->filemime = file_get_mimetype($file->filename); + $file->filesize = $_FILES[$source]['size'][$file_number]; + + $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 { + // 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 (!variable_get('allow_insecure_uploads', 0) && 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))); + } + } + + // If the destination is not provided, use the temporary directory. + if (empty($destination)) { + $destination = 'temporary://'; + } + + // Assert that the destination contains a valid stream. + $destination_scheme = file_uri_scheme($destination); + if (!$destination_scheme || !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; + } + + $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; + } + + // Add in our check of the the file name length. + $validators['file_validate_name_length'] = array(); + + // 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); + 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[$source]['tmp_name'][$file_number], $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); + + // If we are replacing an existing file re-use its database record. + if ($replace == FILE_EXISTS_REPLACE) { + $existing_files = file_load_multiple(array(), 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. + if ($file = file_save($file)) { + // Add file to the cache. + $upload_cache[$source][$file_number] = $file; + return $file; + } + return FALSE; +} \ No newline at end of file