You are here

multiupload_filefield_widget.module in Multiupload Filefield Widget 7

File

multiupload_filefield_widget.module
View source
<?php

/**
 * @file
 * Module creating a multiple file upload widget
 */
module_load_include('inc', 'multiupload_filefield_widget', 'multiupload_filefield_widget.field');

/**
 * Implements hook_element_info().
 */
function multiupload_filefield_widget_element_info() {
  $file_path = drupal_get_path('module', 'file');
  $mfw_path = drupal_get_path('module', 'multiupload_filefield_widget');
  $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,
    '#extended' => FALSE,
    '#size' => 22,
    '#attached' => array(
      'css' => array(
        $file_path . '/file.css',
      ),
      'js' => array(
        $mfw_path . '/mfw.js',
        $file_path . '/file.js',
      ),
    ),
  );
  return $types;
}

/**
 * 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'] .= '[]';
  if (!empty($element['upload']['#attached']['js'][0]['data']['file'])) {
    $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.
 *
 * Mostly copied from file.module.
 */
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_parent = drupal_array_get_nested_value($form_state['complete form'], array_slice($element['#parents'], 0, -1));
    $element['#file_upload_delta_original'] = isset($element_parent['#file_upload_delta']) ? $element_parent['#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;
      }
    }
  }
  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.
 *
 * Mostly copied from file.module.
 *
 * @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 = 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['files']['name'][$upload_name][$file_number])) {
    $name = $_FILES['files']['name'][$upload_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.
 *
 * Mostly copied from drupal core file /include/file.inc.
 *
 * 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.
 *
 * Rewrite of /include/file.inc
 *
 * @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['files']['name'][$source].
 * @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['files']['name'][$source][$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['files']['error'][$source][$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['files']['name'][$source][$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['files']['name'][$source][$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['files']['tmp_name'][$source][$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['files']['name'][$source][$file_number],
      )), 'error');
      return FALSE;
  }

  // Begin building file object.
  $file = new stdClass();
  $file->uid = $user->uid;
  $file->status = 0;
  $file->filename = trim(drupal_basename($_FILES['files']['name'][$source][$file_number]), '.');
  $file->uri = $_FILES['files']['tmp_name'][$source][$file_number];
  $file->filemime = file_get_mimetype($file->filename);
  $file->filesize = $_FILES['files']['size'][$source][$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['files']['tmp_name'][$source][$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;
}

/**
 * Implements hook_insert_widgets().
 */
function multiupload_filefield_widget_insert_widgets() {
  return array(
    'file_mfw' => array(
      'element_type' => 'mfw_managed_file',
      'wrapper' => '.file-widget',
      'fields' => array(
        'description' => 'input[name$="[description]"]',
      ),
    ),
  );
}

Functions

Namesort descending Description
mfw_file_save_upload Saves a file upload to a new location.
mfw_managed_file_process Process function to expand the mfw_managed_file element type.
mfw_managed_file_save_upload Given a mfw_managed_file element, save any files that have been uploaded into it.
mfw_managed_file_value The #value_callback for a mfw_managed_file type element.
multiupload_filefield_widget_element_info Implements hook_element_info().
multiupload_filefield_widget_insert_widgets Implements hook_insert_widgets().