You are here

webform_import.module in Webform Import 6

Same filename and directory in other branches
  1. 7 webform_import.module

Allows delimited data files to be imported as results into webforms.

This is usefull for importing results from other systems in to Webform.

@author John C Jemmett <jjemmett@northwind-inc.com> @author Greg Bosen <gbosen@northwind-inc.com>

File

webform_import.module
View source
<?php

/**
 * @file
 * Allows delimited data files to be imported as results into webforms.
 *
 * This is usefull for importing results from other systems in to Webform.
 *
 * @author John C Jemmett <jjemmett@northwind-inc.com>
 * @author Greg Bosen <gbosen@northwind-inc.com>
 *
 */

/**
 * Implements hook_help().
 */
function webform_import_help($section = 'admin/help#webform', $arg = NULL) {
  $output = '';
  switch ($section) {
    case 'node/%/webform/import':
      $output = '<p>' . t('Webform import enables the upload of delimited files to fill in data.') . '</p>';
      $output .= '<p>' . t('To upload data to this form:');
      $items = array(
        array(
          'data' => 'Create a webform',
        ),
        array(
          'data' => 'Create a delimited file with the data you want uploaded',
          'children' => array(
            'data' => 'Make sure that you have a single header row consisting of either the Component names or keys',
          ),
        ),
        array(
          'data' => 'Go to node/??? > Results > Upload',
        ),
        array(
          'data' => 'Choose the type of delimiter, which type of header you are using and upload the file',
        ),
      );
      $output .= theme("item_list", $items, $title, "ol");
      $output .= '</p>';
      break;
  }
  return $output;
}

/**
 * Implements hook_menu().
 */
function webform_import_menu() {
  $items = array();
  $items['node/%webform_menu/webform-results/upload'] = array(
    'title' => 'Upload',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'webform_import_form',
      1,
    ),
    'access callback' => 'node_access',
    'access arguments' => array(
      'update',
      1,
    ),
    'weight' => 10,
    'type' => MENU_LOCAL_TASK,
  );
  $items['node/%webform_menu/webform-results/upload/%'] = array(
    'title' => 'Get Template',
    'page callback' => 'webform_import_csvtemplate',
    'page arguments' => array(
      1,
      4,
    ),
    'access callback' => 'node_access',
    'access arguments' => array(
      'update',
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/settings/webform-import'] = array(
    'title' => 'Webform Import',
    'description' => 'Configure the settings used for webform import.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'webform_import_admin_settings',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Implements hook_admin_settings().
 *
 * Provide administrative options to configure webform imports functionality.
 */
function webform_import_admin_settings() {
  $form = array();
  $form['webform_import_queue_import'] = array(
    '#type' => 'checkbox',
    '#title' => t('Process imports via Queue'),
    '#description' => t('Enable processing imports via Drupal Queue.'),
    '#default_value' => variable_get('webform_import_queue_import', FALSE),
    '#disabled' => !module_exists('drupal_queue'),
  );
  $form['webform_import_num_per_queue'] = array(
    '#type' => 'select',
    '#title' => t('Lines per Queue entry'),
    '#description' => t('The number of lines to process per entry in the Drupal Queue.'),
    '#options' => array(
      100 => 100,
      250 => 250,
      500 => 500,
      1000 => 1000,
      2500 => 2500,
      10000 => 10000,
    ),
    '#default_value' => variable_get('webform_import_num_per_queue', 100),
    '#disabled' => !module_exists('drupal_queue'),
  );
  $form['webform_import_file_destination'] = array(
    '#type' => 'textfield',
    '#title' => t('Upload destination'),
    '#default_value' => variable_get('webform_import_file_destination', FALSE),
    '#description' => t('The destination for webform import uploads.'),
  );
  return system_settings_form($form);
}

/**
 * Creates a downloadable CSV template file corresponding to a Webform structure.
 *
 * @param $node
 *   The current webform node.
 * @param $type
 *   The type of delimited file template to download.
 */
function webform_import_csvtemplate($node, $type) {
  $types = _webform_import_field_key_options();
  $filename = check_plain($node->title) . '_upload.csv';
  $headers = array();
  $node->webform['components']['-1'] = array(
    'name' => 'User ID',
    'form_key' => 'UID',
  );
  $node->webform['components']['-2'] = array(
    'name' => 'Submission ID',
    'form_key' => 'SID',
  );
  if (array_key_exists($type, $types)) {
    foreach ($node->webform['components'] as $cid => $component) {
      if ($component['type'] != 'file') {
        $headers[] = $component[$type];
      }
    }
    $csv = join(', ', $headers);
    drupal_set_header('Content-type: application/octet-stream; charset=utf-8');
    drupal_set_header('Content-disposition: attachment; filename=' . $filename);
    print $csv;
  }
  else {
    drupal_set_message(t('Invalid header type.'), 'warning');
    $path = explode('/', $_GET['q']);
    array_pop($path);
    $path = join('/', $path);
    drupal_goto(drupal_get_path_alias($path));
  }
}

/**
 * Client form generation function. Allows the user to upload a delimited file.
 *
 * @param $form_state
 *   The current form values of a submission, used in multipage webforms.
 * @param $node
 *   The current webform node.
 *
 * @see webform_import_form_submit()
 * @ingroup forms
 */
function webform_import_form(&$form_state, $node) {
  global $user;

  // Add a css class for all client forms.
  $form['#attributes'] = array(
    'class' => 'webform-import-form',
  );

  // Set the encoding type (necessary for file uploads).
  $form['#attributes']['enctype'] = 'multipart/form-data';
  $form['#redirect'] = 'node/' . $node->nid . '/webform-results/table';
  $form['#submit'][] = 'webform_import_form_submit';
  $form['details']['nid'] = array(
    '#type' => 'value',
    '#value' => $node->nid,
  );

  // @TODO Move these inline style elements into a css file.
  $component_table_header = array(
    array(
      'data' => "<div style='position:relative'><span style='position:absolute;left:0'>" . t('Field Names') . "</span>&nbsp;<span style='position:absolute;right:0'>[ " . l('download template', 'node/' . $node->nid . '/webform-results/upload/name') . ' ]</span></div>',
    ),
    array(
      'data' => "<div style='position:relative'><span style='position:absolute;left:0'>" . t('Field Form Keys') . "</span>&nbsp;<span style='position:absolute;right:0'>[ " . l('download template', 'node/' . $node->nid . '/webform-results/upload/form_key') . ' ]</span></div>',
    ),
  );
  $component_table_rows = array();
  $component_table_rows[] = array(
    'Submission ID',
    'SID',
  );
  $component_table_rows[] = array(
    'User ID',
    'UID',
  );
  foreach ($node->webform['components'] as $cid => $component) {
    if ($component['type'] == 'file') {
      continue;
    }
    $style = '';
    if ($component['mandatory']) {
      $style = 'font-weight: bold';
    }
    $component_table_rows[] = array(
      array(
        'data' => $component['name'],
        'style' => $style,
      ),
      array(
        'data' => $component['form_key'],
        'style' => $style,
      ),
    );
  }
  $form['header'] = array(
    '#value' => '<h2>' . t('Webform Import for "@title"', array(
      '@title' => $node->title,
    )) . '</h2>',
  );
  $form['instructions'] = array(
    '#type' => 'fieldset',
    '#title' => t('Instructions'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $instructions_items = array(
    array(
      'data' => 'Component specific help:',
      'children' => array(
        '<strong>Date</strong>: must be in a format parsable by the php function <code>strtotime()</code> Any time data will be discarded.',
        '<strong>Grid</strong>: option keys must be separated by commas and in the order of the questions in the webform. (e.g., <em>"red,male,car"</em> for the questions <em>"Favorite color, Gender, Type of automobile you drive"</em> respectively.)',
        '<strong>File</strong>: * currently unable to handle this component.',
        '<strong>Select</strong>: for multiselect answer keys must be separated by commas. (e.g., <em>"1,2,3"</em>)',
        '<strong>Time</strong>: must be in a format parsable by the php function <code>strtotime()</code> Any date data will be discarded.',
      ),
    ),
  );
  $form['instructions']['instructions'] = array(
    '#value' => '<p>' . t('Webform import enables the upload of delimited files to fill in data.') . '</p>' . '<p>' . t('The file must use the following values for column headers, anything else will be ignored.') . theme('table', $component_table_header, $component_table_rows) . '<p><small>' . t('* bold names are mandatory and must contain a value') . '</small><br/>' . '' . t('All rows with a submission id (SID) will be updated. Those without a submission id will be inserted.') . '<br/>' . '' . t('All rows with a user id (UID) will be associated with that user. Those without a user id will use the default setting.') . '</p><br/>' . theme("item_list", $instructions_items, NULL, "ol"),
  );
  $form['upload'] = array(
    '#type' => 'file',
    '#title' => t('Delimited data file'),
    '#description' => t('Choose the data file containing the data you want uploaded. All rows with a submission id (SID) will be updated. Those without a submission id will be inserted.'),
  );
  $form['delimiter'] = array(
    '#type' => 'select',
    '#title' => t('File Delimiter'),
    '#default_value' => '\\t',
    '#options' => _webform_import_delimiter_options(),
    '#description' => t('Delimiter for file being uploaded.'),
  );
  $users_list = array();
  $result = db_query('SELECT uid, name FROM {users}');
  while ($row = db_fetch_object($result)) {
    if ($row->uid == 0) {
      $row->name = "Anonymous";
    }
    $users_list[$row->uid] = $row->name;
  }
  $form['default_user'] = array(
    '#type' => 'select',
    '#title' => t('Default User'),
    '#default_value' => $user->uid,
    '#options' => $users_list,
    '#description' => t('Select the default user to associate the submissions with (unless specified in the file).'),
  );
  $form['field_keys'] = array(
    '#type' => 'select',
    '#title' => t('Column header contains'),
    '#default_value' => 'name',
    '#options' => _webform_import_field_key_options(),
    '#description' => t('What to use for the column key. (You probably want to choose form key if you have multiple fields with the same name.)'),
  );
  $form['buttons'] = array(
    '#tree' => FALSE,
    '#weight' => 1000,
  );

  // Add the submit button.
  $form['buttons']['submit'] = array(
    '#type' => 'submit',
    '#value' => empty($node->webform['submit_text']) ? t('Submit') : $node->webform['submit_text'],
    '#weight' => 10,
    '#attributes' => variable_get('webform_import_queue_import', FALSE) ? array(
      'onclick' => "this.disabled=true;this.value='Queueing...';this.form.submit();",
    ) : NULL,
  );
  return $form;
}

/**
 * Form submission function to parse the delimited file and add to the database.
 *
 * @param $form
 *   The current form.
 * @param $form_state
 *   The current form values of a submission.
 *
 * @see webform_import_form()
 */
function webform_import_form_submit($form, &$form_state) {

  // Define your limits for the submission here.
  $limits = array(
    'extensions' => 'csv tsv txt',
  );
  $validators = array(
    'file_validate_extensions' => array(
      $limits['extensions'],
    ),
  );
  $webform = node_load(intval($form_state['values']['nid']));
  $delimiter = $form_state['values']['delimiter'];
  $delimiter = $delimiter == '\\t' ? '\\t' : $delimiter;
  $field_key = $form_state['values']['field_keys'];
  $default_user = $form_state['values']['default_user'];
  $keys = array();
  $keys[$field_key === 'name' ? 'Submission ID' : 'SID'] = array(
    'form_key' => 'SID',
  );
  $keys[$field_key === 'name' ? 'User ID' : 'UID'] = array(
    'form_key' => 'UID',
  );
  foreach ($webform->webform['components'] as $cid => $component) {
    $component['cid'] = $cid;
    $keys[trim($component[$field_key])] = $component;
  }
  if ($file = file_save_upload('upload', $validators, variable_get('webform_import_file_destination', FALSE))) {

    // This makes php auto-detect mac line endings.
    ini_set('auto_detect_line_endings', TRUE);
    if (($handle = fopen($file->destination, 'r')) !== FALSE) {

      // Get the column headings
      $data = fgetcsv($handle, 0, $delimiter);
      foreach ($data as $k => &$v) {
        $v = _webform_import_csvfieldtrim($v);
        if (!isset($keys[$v])) {
          drupal_set_message(t('Can not find column @k in components list, skipping.', array(
            '@k' => $v,
          )), 'warning');
        }
        elseif (isset($keys[$v]['cid'])) {
          $cids[$k] = $keys[$v]['cid'];
        }
      }
      $fields = array_flip($data);
      foreach ($keys as $k => $component) {
        if (!empty($component['mandatory']) && $component['type'] != 'fieldset' && !isset($fields[$k])) {
          form_set_error('upload', t('Column @k is required but could not be found in this file. Alter the file or the webform and try again.', array(
            '@k' => $k,
          )));
          return;
        }
      }
      $fields = $data;
      $item = (object) array(
        'filename' => $file->destination,
        'start' => 1,
        'limit' => FALSE,
        'fields' => $fields,
        'keys' => $keys,
        'cids' => $cids,
        'delimiter' => $delimiter,
        'webform_nid' => $webform->nid,
        'field_key' => $field_key,
        'default_user' => $default_user,
        'submission_time' => time(),
        'remote_addr' => ip_address(),
      );
      if (variable_get('webform_import_queue_import', FALSE)) {
        if (($handle = fopen($file->destination, 'r')) !== FALSE) {
          $linecount = 0;
          while (fgetcsv($handle, 0, $delimiter) !== FALSE) {
            $linecount++;
          }
          fclose($handle);
        }
        $item->limit = variable_get('webform_import_num_per_queue', 100);
        while ($item->start <= $linecount) {
          webform_import_queue_create_item($item);
          $item->start += variable_get('webform_import_num_per_queue', 100);
        }
        drupal_set_message(t('Upload file contained @num_lines lines that will be run @num at a time.', array(
          '@num_lines' => $linecount,
          '@num' => variable_get('webform_import_num_per_queue', 100),
        )));
      }
      else {
        webform_import_process($item);
      }
    }
    else {
      form_set_error('upload', t('File could not be opened for reading.'));
    }
  }
}
function webform_import_queue_create_item($item) {

  // Create the queue item.
  $queue = DrupalQueue::get('webform_import_queue_import');
  return $queue
    ->createItem($item);
}

/**
 * Parse the delimited file and add to the database.
 *
 * @param array $item
 *   All variables we need for the import process.
 */
function webform_import_process($item) {
  module_load_include('inc', 'webform', 'includes/webform.submissions');
  $webform = node_load(intval($item->webform_nid));
  $field_key = $item->field_key;
  $default_user = $item->default_user;
  $delimiter = $item->delimiter;
  $filename = $item->filename;
  $fields = $item->fields;
  $keys = $item->keys;
  $cids = $item->cids;
  $sids = array();
  if (!empty($filename)) {

    // This makes php auto-detect mac line endings.
    ini_set('auto_detect_line_endings', TRUE);
    if ($file = new SplFileObject($filename, 'r')) {

      // This conditional is required because there is a bug in SplFileObject
      // where seek(n) followed by fgetcsv will return the wrong line.  In
      // addition, this bug only occurs when n > 0 so n == 0 must be handled
      // separately.  See https://bugs.php.net/bug.php?id=46569.
      if ($item->start == 1) {
        $file
          ->seek(0);
        $file
          ->fgets();
      }
      elseif ($item->start > 1) {
        $file
          ->seek($item->start - 1);
      }
      $current_line = $item->start;
      while (($item->limit == FALSE || $current_line < $item->start + $item->limit) && ($data = $file
        ->fgetcsv($delimiter))) {
        $current_line++;

        // If handle gets in a bad state this will keep an infinite loop from
        // occuring and clean up after ourselves.
        if ($data === FALSE) {
          webform_import_log('Data was FALSE.', array(), 'error');
          return;
        }

        // Ignore empty rows.
        if ($data[0] === NULL) {
          continue;
        }
        $sid = NULL;
        $uid = NULL;
        if (count($data) == count($fields)) {
          $sub_array = array();
          foreach ($data as $k => &$v) {
            $component = $keys[$fields[$k]];
            $v = _webform_import_csvfieldtrim($v);
            if ($v == '') {

              // Checking mandatory field for a value.
              if ($component['mandatory']) {
                webform_import_log('Required field has no value at row,col: @r,@c.  Skipping this row!', array(
                  '@r' => $current_line,
                  '@c' => $k,
                ), 'warning');
                continue 2;
              }
              else {
                continue 1;

                // Skip field if empty.
              }
            }

            // Checking SID input security.
            if ($component['form_key'] === 'SID') {
              if (!is_numeric($v)) {
                webform_import_log('Invalid Submission ID at row,col: @r,@c.  Skipping this row!', array(
                  '@r' => $current_line,
                  '@c' => $k,
                ), 'warning');
                continue 2;
              }
              else {
                $sid = intval($v);

                // Valid input security (integer values only).
              }
            }
            elseif ($component['form_key'] === 'UID') {
              if (!is_numeric($v)) {
                webform_import_log('Invalid User ID at row,col: @r,@c.  Skipping this row!', array(
                  '@r' => $current_line,
                  '@c' => $k,
                ), 'warning');
                continue 2;
              }
              else {
                $uid = intval($v);

                // Valid input security (integer values only).
              }
            }
            elseif (($cid = isset($cids[$k]) ? $cids[$k] : FALSE) !== FALSE) {
              $type = $component['type'];

              // Date and time components.
              if ($type === 'date' || $type === 'time') {
                if ($time = strtotime($v)) {
                  $v = $type === 'date' ? date('Y-m-d', $time) : date('H:i:s', $time);
                }
                else {
                  webform_import_log('Invalid datetime value at row,col: @r,@c. Skipping this row!', array(
                    '@r' => $current_line,
                    '@c' => $k,
                  ), 'warning');
                  continue 2;
                }
              }
              elseif ($type === 'grid') {

                // Get the value for the grid components.
                webform_component_include('select');
                $options = array_flip(_webform_select_options_from_text($component['extra']['options'], TRUE));
                $v = explode(',', $v);
                foreach ($v as &$value) {
                  if (!empty($options[$value])) {
                    $value = $options[$value];
                  }
                }
              }
              elseif ($type === 'select') {

                // For select options, make sure we use the key and not the values.
                webform_component_include('select');
                $options = array_flip(_webform_select_options($component, TRUE));

                // See if there are multiple values.
                if ($component['extra']['multiple'] == 1) {
                  $v = explode(',', $v);
                  foreach ($v as &$value) {
                    if (!empty($options[$value])) {
                      $value = $options[$value];
                    }
                  }
                }
                else {
                  if (!empty($options[$v])) {
                    $v = $options[$v];
                  }
                }
              }
              $sub_array[$cid] = $v;
            }
          }
          $submission = (object) array(
            'nid' => $webform->nid,
            'uid' => $uid != NULL ? $uid : $default_user,
            'submitted' => $item->submission_time,
            'remote_addr' => $item->remote_addr,
            'is_draft' => FALSE,
            'data' => webform_submission_data($webform, $sub_array),
          );

          // Determine if INSERT or UPDATE based on inclusion of SID.
          if ($sid != NULL) {
            $submission->sid = $sid;
            $sids[] = webform_submission_update($webform, $submission);
          }
          else {
            $sids[] = webform_submission_insert($webform, $submission);
          }
        }
        else {
          webform_import_log('Row @c is malformed and will need to be fixed and resubmitted.', array(
            '@c' => $current_line,
          ), 'warning');
        }
      }
      if (!variable_get('webform_import_queue_import', FALSE)) {
        if (!file_delete($filename)) {
          webform_import_log('File could not be deleted (cleanup process). File: %file at path %path . !details', array(
            '%file' => $file->filename,
            '%path' => $file->destination,
            '!results' => '<br />\\n<pre>' . htmlentities(print_r($form_state['values'], TRUE)) . '</pre>',
          ), 'error');
        }
        webform_import_log('Submission file uploaded to %title.', array(
          '%title' => check_plain($webform->title),
        ));
        webform_import_log('Uploaded @count webform submissions.', array(
          '@count' => count($sids),
        ));
      }
    }
    else {
      webform_import_log('File read error. Could not read file %file at path %path.!details', array(
        '%file' => $file->filename,
        '%path' => $file->filepath,
        '!results' => '<br />\\n<pre>' . htmlentities(print_r($form_state['values'], TRUE)) . '</pre>',
      ), 'error');
    }
  }
}

/**
 * Log an entry regarding webform import and create a drupal message if appropriate.
 */
function webform_import_log($message, $substitutions = array(), $message_type = 'status') {
  switch ($message_type) {
    case 'warning':
      $watchdog_level = WATCHDOG_WARNING;
      break;
    case 'error':
      $watchdog_level = WATCHDOG_ERROR;
      break;
    case 'status':
    default:
      $watchdog_level = WATCHDOG_NOTICE;
      break;
  }
  if (!variable_get('webform_import_queue_import', FALSE)) {
    drupal_set_message(t($message, $substitutions), $message_type);
  }
  else {
    watchdog('webform-import', $message, $substitutions, $watchdog_level);
  }
}

/**
 * Returns a list of value delimiters we can use.
 *
 * @return
 *   An array of key/value pairs for form options list.
 */
function _webform_import_delimiter_options() {
  return array(
    ',' => t('Comma (,)'),
    '\\t' => t('Tab (\\t)'),
    ';' => t('Semicolon (;)'),
    ':' => t('Colon (:)'),
    '|' => t('Pipe (|)'),
    '.' => t('Period (.)'),
    ' ' => t('Space ( )'),
  );
}

/**
 * Returns a list of field header types we can use.
 *
 * @return
 *   An array of key/value pairs for form options list.
 */
function _webform_import_field_key_options() {
  return array(
    'name' => t('Field Names'),
    'form_key' => t('Field Form Keys'),
  );
}

/**
 * Returns a trimmed field value
 *
 * @param $value
 *   Field value to be trimmed.
 * @return
 *   Trimmed field value.
 */
function _webform_import_csvfieldtrim($value) {
  $value = trim($value);

  // Strip off the beginning and ending quotes if necessary.
  $value = preg_replace('/^".*"$/', '', $value);

  // Remove control characters. Some editors add invalid EOL chars.
  // fgetcsv does not handle unicode characters therefore we replace them
  // manually. See http://bugs.php.net/bug.php?id=31632.
  $value = str_replace('\\x00..\\x1F\\xfe\\xff', '', $value);
  return $value;
}

/**
 * Sets up the worker for the queue.
 *
 * @return string
 */
function webform_import_cron_queue_info() {
  $queues = array();
  if (variable_get('webform_import_queue_import', FALSE)) {
    $queues['webform_import_queue_import'] = array(
      'worker callback' => 'webform_import_process',
      'time' => 90,
    );
  }
  return $queues;
}

Functions

Namesort descending Description
webform_import_admin_settings Implements hook_admin_settings().
webform_import_cron_queue_info Sets up the worker for the queue.
webform_import_csvtemplate Creates a downloadable CSV template file corresponding to a Webform structure.
webform_import_form Client form generation function. Allows the user to upload a delimited file.
webform_import_form_submit Form submission function to parse the delimited file and add to the database.
webform_import_help Implements hook_help().
webform_import_log Log an entry regarding webform import and create a drupal message if appropriate.
webform_import_menu Implements hook_menu().
webform_import_process Parse the delimited file and add to the database.
webform_import_queue_create_item
_webform_import_csvfieldtrim Returns a trimmed field value
_webform_import_delimiter_options Returns a list of value delimiters we can use.
_webform_import_field_key_options Returns a list of field header types we can use.