You are here

uif.admin.inc in User Import Framework 6

Same filename and directory in other branches
  1. 7 uif.admin.inc

Simple, extensible user import from a CSV file.

File

uif.admin.inc
View source
<?php

/**
 * @file
 * Simple, extensible user import from a CSV file.
 */

/**
 * User import multi-part form.
 */
function uif_import_form(&$form_state) {

  // Cause return to beginning if we just completed an import
  if ($form_state['storage']['step'] >= 3) {
    unset($form_state['storage']);
  }
  $step = empty($form_state['storage']['step']) ? 1 : $form_state['storage']['step'];
  $form_state['storage']['step'] = $step;
  switch ($step) {
    case 1:
      $form['instructions'] = array(
        '#prefix' => '<div id="uif_form_help">',
        '#suffix' => '</div>',
        '#value' => theme('uif_form_help'),
      );
      $file_size_msg = t('Your PHP settings limit the maximum file size per upload to %size. Depending on your server environment, these settings may be changed in the system-wide php.ini file, a php.ini file in your Drupal root directory, in your Drupal site\'s settings.php file, or in the .htaccess file in your Drupal root directory.', array(
        '%size' => format_size(file_upload_max_size()),
      ));
      $form['user_upload'] = array(
        '#type' => 'file',
        '#title' => t('Import file'),
        '#size' => 40,
        '#description' => t('Select the CSV file to be imported.') . '<br />' . $file_size_msg,
      );
      $preview_count = drupal_map_assoc(array(
        0,
        1,
        10,
        100,
        1000,
        10000,
        9999999,
      ));
      $preview_count[0] = t('None - just do it');
      $preview_count[9999999] = t('Preview all');
      $form['preview_count'] = array(
        '#type' => 'select',
        '#title' => t('Users to preview'),
        '#default_value' => 10,
        '#options' => $preview_count,
        '#description' => t('Number of users to preview before importing. Note: If you run out of memory set this lower or increase your memory.'),
      );
      $form['notify'] = array(
        '#type' => 'checkbox',
        '#title' => t('Notify new users of account'),
        '#description' => t('If checked, each newly created user will receive the <em>Welcome, new user created by administrator</em> email using the template on the <a href="@url1">user settings page</a>. This is the same email sent for <a href="@url2">admin-created accounts</a>.', array(
          '@url1' => url('admin/user/settings'),
          '@url2' => url('admin/user/user/create'),
        )),
      );
      $form['next'] = array(
        '#type' => 'submit',
        '#value' => t('Next'),
      );

      // Set form parameters so we can accept file uploads.
      $form['#attributes'] = array(
        'enctype' => 'multipart/form-data',
      );
      break;
    case 2:
      $form['instructions'] = array(
        '#prefix' => '<div id="uif_form_instructions">',
        '#suffix' => '</div>',
        '#value' => t('Preview these records and when ready to import click Import users.'),
      );
      $form['user_preview'] = array(
        '#prefix' => '<div id="uif_user_preview">',
        '#suffix' => '</div>',
        '#value' => $form_state['storage']['user_preview'],
      );
      $form['back'] = array(
        '#type' => 'submit',
        '#value' => t('Back'),
        '#submit' => array(
          'uif_import_form_back',
        ),
      );
      $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Import users'),
      );
      break;
  }
  return $form;
}

/**
 * Validate the import data.
 */
function uif_import_form_validate($form, &$form_state) {
  $step = empty($form_state['storage']['step']) ? 1 : $form_state['storage']['step'];
  switch ($step) {
    case 1:

      // Validate the upload file
      $validators = array(
        'file_validate_extensions' => array(
          'csv',
        ),
        'file_validate_size' => array(
          file_upload_max_size(),
        ),
      );
      if ($user_file = file_save_upload('user_upload', $validators)) {
        $errors = uif_validate_user_file($user_file->filepath, $data, $form_state);
        if (!empty($errors)) {
          form_set_error('user_upload', '<ul><li>' . implode('</li><li>', $errors) . '</li></ul>');
          return;
        }
      }
      else {
        form_set_error('user_upload', t('Cannot save the import file to temporary storage.  Please try again.'));
        return;
      }

      // Save the validated data to avoid reparsing
      $form_state['storage']['data'] = $data;
      break;
  }
}

/**
 * Form submission handler.
 */
function uif_import_form_submit($form, &$form_state) {
  $step = empty($form_state['storage']['step']) ? 1 : $form_state['storage']['step'];
  if (1 == $step) {
    $form_state['storage']['notify'] = isset($form_state['values']['notify']) ? $form_state['values']['notify'] : FALSE;
    $preview_count = $form_state['values']['preview_count'];
    if ($preview_count) {
      $form_state['storage']['preview_count'] = $preview_count;
      $form_state['storage']['user_preview'] = theme('uif_preview_users', $form_state['storage']['data'], $preview_count);
    }
    else {
      $step = 2;
    }
  }
  if (2 == $step) {
    uif_batch_import_users($form_state);
  }
  $form_state['storage']['step'] = $step + 1;
}

/**
 * Read the user import file and validate on the way.
 *
 *  @param $filepath
 *    filepath to the user import file
 *  @param $data
 *    returns with array of users
 *  @return
 *    FALSE if no errors found
 *    array of error strings if error found
 */
function uif_validate_user_file($filepath, &$data, $form_state) {
  $data = array();
  $data['user'] = array();
  $line = 0;

  // Without this fgetcsv() fails for Mac-created files
  ini_set('auto_detect_line_endings', TRUE);
  if ($fp = fopen($filepath, 'r')) {

    // Read the header and allow alterations
    $header_row = fgetcsv($fp);
    drupal_alter('uif_header', $header_row);
    $header_row = uif_normalize_header(array_map('trim', $header_row));
    $line++;
    $errors = module_invoke_all('uif_validate_header', $header_row, $form_state);
    uif_add_line_number($errors, $line);
    if (!empty($errors)) {
      return $errors;
    }
    $data['header'] = $header_row;

    // Read the data
    $errors = array();
    while (!feof($fp) && count($errors) < 20) {

      // Read a row and allow alterations
      $row = fgetcsv($fp);
      drupal_alter('uif_row', $row, $header_row);
      $line++;
      if (uif_row_has_data($row)) {
        $user_row = uif_clean_and_key_row($header_row, $row, $line);
        $uid = db_result(db_query_range("SELECT uid FROM {users} WHERE mail LIKE '%s'", $user_row['email'], 0, 1));
        $more_errors = module_invoke_all('uif_validate_user', $user_row, $uid, $header_row, $form_state);
        uif_add_line_number($more_errors, $line);
        $errors = array_merge($errors, $more_errors);
        $data['user'][] = $user_row;
      }
    }

    // Any errors?
    if (!empty($errors)) {
      return $errors;
    }
  }
  else {
    return t('Cannot open that import file.');
  }

  // Final validation opportunity after header and all users validated individually.
  $errors = module_invoke_all('uif_validate_all_users', $data['user'], $form_state);
  if (!empty($errors)) {
    return $errors;
  }
}

/**
 * Trim all elements of $row, and pad $row out to the number of columns in the 
 * $header.  Then replace keys in $row with $header values.
 */
function uif_clean_and_key_row($header, $row, $line) {
  $row = array_map('trim', $row);
  $raw_row = $row;
  $row = array_map('uif_clean_value', $row);
  for ($i = 0; $i < count($row); $i++) {
    if ($raw_row[$i] !== $row[$i]) {
      $vars = array(
        '!line' => $line,
        '%column' => $header[$i],
      );
      drupal_set_message(t('Warning on row !line: Non UTF-8 characters were removed from %column column.', $vars), 'warning');
    }
  }
  if (count($row) < count($header)) {
    $row = array_merge($row, array_fill(count($row), count($header) - count($row), ''));
    drupal_set_message(t('Warning on row !line: Empty values added for missing data.', array(
      '!line' => $line,
    )), 'warning');
  }
  elseif (count($row) > count($header)) {
    array_splice($row, count($header));
    drupal_set_message(t('Warning on row !line: Data values beyond header were truncated.', array(
      '!line' => $line,
    )), 'warning');
  }
  $row = array_combine($header, $row);
  return $row;
}

/**
 * Check that input is UTF-8.
 */
function uif_clean_value($value) {
  if (!drupal_validate_utf8($value)) {

    // Remove all chars except LF, CR, and basic ascii
    return preg_replace('/[^\\x0A\\x0D\\x20-\\x7E]/', '', $value);
  }
  return $value;
}

/**
 * Is there data in the row?
 */
function uif_row_has_data($row) {
  if (isset($row) && is_array($row)) {
    foreach ($row as $value) {
      $value = trim($value);
      if (!empty($value)) {
        return TRUE;
      }
    }
  }
  return FALSE;
}

/**
 * Normalize the header columns.
 */
function uif_normalize_header($header) {
  $normal_header = array();
  foreach ($header as $column) {
    $normal_header[] = strtolower($column);
  }
  return $normal_header;
}

/**
 * Implementation of hook_uif_validate_header().
 */
function uif_uif_validate_header($header) {
  foreach ($header as $column) {
    if ('email' === $column) {
      $email_found = TRUE;
    }
  }
  if (!$email_found) {
    return t('I can find no email column in the import file.');
  }
}

/**
 * Implementation of hook_uif_validate_user().
 */
function uif_uif_validate_user($user_data, $uid, $header = NULL) {
  if (!valid_email_address($user_data['email'])) {
    return t('Missing or invalid email address !mail.', array(
      '!mail' => $user_data['email'],
    ));
  }
}

/**
 * Prepend the line number on the error.
 */
function uif_add_line_number(&$errors, $line) {
  foreach ($errors as &$error) {
    $error = t('Error on row !line: ', array(
      '!line' => $line,
    )) . $error;
  }
}

/**
 * Return user to starting point on template multi-form.
 */
function uif_import_form_back($form, &$form_state) {
  $form_state['storage']['step'] = 1;
}

/**
 * Theme preview of all users.
 */
function theme_uif_preview_users($data, $limit) {
  $current = 0;
  foreach ($data['user'] as $user_data) {
    $current++;
    if ($current > $limit) {
      break;
    }
    $output .= theme('uif_preview_one_user', $user_data);
  }
  if (!$output) {
    $output = t('There are no users to import.');
  }
  return $output;
}

/**
 * Theme preview of a single user.
 */
function theme_uif_preview_one_user($user_data) {
  $rows = array();
  foreach ($user_data as $field => $value) {
    $rows[] = array(
      $field,
      $value,
    );
  }
  $user_exists = db_result(db_query("SELECT count(*) FROM {users} WHERE mail='%s'", $user_data['email']));
  $annotation = $user_exists ? t('update') : t('create');
  $heading = $user_data['email'] . ' (' . $annotation . ')';
  return '<h3>' . $heading . '</h3>' . theme('table', NULL, $rows);
}

/**
 * Batch import all users.
 */
function uif_batch_import_users($form_state) {
  $batch = array(
    'title' => t('Importing users'),
    'operations' => array(
      array(
        'uif_batch_import_users_process',
        array(
          $form_state,
        ),
      ),
    ),
    'progress_message' => '',
    // uses count(operations) which is irrelevant in this case
    'finished' => 'uif_batch_import_users_finished',
    'file' => drupal_get_path('module', 'uif') . '/uif.admin.inc',
  );
  batch_set($batch);
}

/**
 * User import batch processing.
 */
function uif_batch_import_users_process($form_state, &$context) {

  // Initialize
  if (empty($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($form_state['storage']['data']['user']);
    $context['results']['created'] = 0;
    $context['results']['updated'] = 0;
  }

  // Process max 20 users at a time
  $processed = 0;
  $notify = $form_state['storage']['notify'];
  while ($context['sandbox']['progress'] < $context['sandbox']['max'] && $processed < 20) {
    $index = $context['sandbox']['progress'];
    uif_import_user($form_state['storage']['data']['user'][$index], $notify, $context['results'], $form_state);
    $context['sandbox']['progress']++;
    $processed++;
  }

  // Finished yet?
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * User import batch completion.
 */
function uif_batch_import_users_finished($success, $results, $operations) {
  if ($success) {
    $done = t('User import complete. ');
    $created = $results['created'] ? format_plural($results['created'], 'One user was created.', '@count users were created.') . ' ' : '';
    $updated = $results['updated'] ? format_plural($results['updated'], 'One user was updated.', '@count users were updated.') . ' ' : '';
    $more = t('View the <a href="@url">user list</a>.', array(
      '@url' => url('admin/user/user'),
    ));
    drupal_set_message($done . $created . $updated . $more);
  }
  else {
    drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
  }
}

/**
 * Import one user.
 */
function uif_import_user($user_data, $notify, &$results, $form_state) {
  if ($uid = db_result(db_query("SELECT uid FROM {users} WHERE mail LIKE '%s'", $user_data['email']))) {
    $account = uif_update_user($user_data, $uid, $form_state);
    $results['updated']++;
  }
  else {
    $account = uif_create_user($user_data, $notify, $form_state);
    $results['created']++;
  }
}

/**
 * Create a new user.
 */
function uif_create_user($user_data, $notify, $form_state) {
  $account = array();
  $account['mail'] = $user_data['email'];
  $account['init'] = $user_data['email'];
  $account['status'] = 1;

  // Use the provided username if any, or derive it from the email
  $username = empty($user_data['username']) ? preg_replace('/@.*$/', '', $user_data['email']) : $user_data['username'];
  $account['name'] = uif_unique_username($username);

  // Use the provided password if any, otherwise a random one
  $pass = $user_data['password'] ? $user_data['password'] : user_password();
  $account['pass'] = $pass;

  // If access is not set, no users will be able to view the
  // new user's profile until such time that the newly
  // created user logs in for the first time.
  $account['access'] = time();
  $account = array_merge($account, module_invoke_all('uif_pre_create', $account, $user_data, $form_state));
  $account = user_save('', $account);
  module_invoke_all('uif_post_create', $account, $user_data, $form_state);
  if ($notify) {
    $account->password = $pass;

    // For mail token; _user_mail_notify() expects this
    _user_mail_notify('register_admin_created', $account);
  }
  return $account;
}

/**
 * Update an existing user.
 */
function uif_update_user($user_data, $uid, $form_state) {
  $account = user_load($uid);

  // todo: Support update of user mail, name, and password
  // Supporting user mail change requires optional inclusion of uid column, which
  // would override use of email column as uid lookup method.
  $additions = module_invoke_all('uif_pre_update', $account, $user_data, $form_state);
  $account = user_save($account, $additions);
  module_invoke_all('uif_post_update', $account, $user_data, $form_state);
  return $account;
}

/**
 * Given a starting point for a Drupal username (e.g. the name portion of an email address) return
 * a legal, unique Drupal username.
 *
 * @param $name
 *   A name from which to base the final user name.  May contain illegal characters; these will be stripped.
 *
 * @param $uid
 *   (optional) Uid to ignore when searching for unique user (e.g. if we update the username after the 
 *   {users} row is inserted) 
 *
 * @return
 *   A unique user name based on $name.
 *
 */
function uif_unique_username($name, $uid = 0) {

  // Strip illegal characters
  $name = preg_replace('/[^\\x{80}-\\x{F7} a-zA-Z0-9@_.\'-]/', '', $name);

  // Strip leading and trailing whitespace
  $name = trim($name);

  // Convert any other series of spaces to a single space
  $name = preg_replace('/ +/', ' ', $name);

  // If there's nothing left use a default
  $name = '' === $name ? t('user') : $name;

  // Truncate to reasonable size
  $name = drupal_strlen($name) > USERNAME_MAX_LENGTH - 10 ? drupal_substr($name, 0, USERNAME_MAX_LENGTH - 11) : $name;

  // Iterate until we find a unique name
  $i = 0;
  do {
    $newname = empty($i) ? $name : $name . '_' . $i;
    $found = db_result(db_query_range("SELECT uid from {users} WHERE uid <> %d AND name = '%s'", $uid, $newname, 0, 1));
    $i++;
  } while ($found);
  return $newname;
}

/**
 * Theme function for import form help.
 */
function theme_uif_form_help() {
  $basic_help = '<p>' . t('Choose an import file. You\'ll have a chance to preview the data before doing the import. The import file must have a header row with a name in each column for the value you are importing. The header names are not case sensitive. Importable fields include:') . '</p>';
  $basic_help .= '<ul><li>' . t('email (required) - the user\'s email') . '</li>';
  $basic_help .= '<li>' . t('username (optional) - a name for the user. If not provided, a name is created based on the email.') . '</li>';
  $basic_help .= '<li>' . t('password (optional) - a password for the user. If not provided, a password is generated.') . '</li></ul>';
  $helps = module_invoke_all('uif_help');
  array_unshift($helps, $basic_help);
  foreach ($helps as $help) {
    $output .= '<div class="uif_help_section">' . $help . '</div>';
  }
  $fieldset = array(
    '#type' => 'fieldset',
    '#title' => t('User import help'),
    '#value' => $output,
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  return theme('fieldset', $fieldset);
}

Functions

Namesort descending Description
theme_uif_form_help Theme function for import form help.
theme_uif_preview_one_user Theme preview of a single user.
theme_uif_preview_users Theme preview of all users.
uif_add_line_number Prepend the line number on the error.
uif_batch_import_users Batch import all users.
uif_batch_import_users_finished User import batch completion.
uif_batch_import_users_process User import batch processing.
uif_clean_and_key_row Trim all elements of $row, and pad $row out to the number of columns in the $header. Then replace keys in $row with $header values.
uif_clean_value Check that input is UTF-8.
uif_create_user Create a new user.
uif_import_form User import multi-part form.
uif_import_form_back Return user to starting point on template multi-form.
uif_import_form_submit Form submission handler.
uif_import_form_validate Validate the import data.
uif_import_user Import one user.
uif_normalize_header Normalize the header columns.
uif_row_has_data Is there data in the row?
uif_uif_validate_header Implementation of hook_uif_validate_header().
uif_uif_validate_user Implementation of hook_uif_validate_user().
uif_unique_username Given a starting point for a Drupal username (e.g. the name portion of an email address) return a legal, unique Drupal username.
uif_update_user Update an existing user.
uif_validate_user_file Read the user import file and validate on the way.