<?php
// ; $Id$

// Drupal forums are represented as a hierarchy of terms in a vocabulary. This
// is accomplished through the taxonomy concept (see: http://drupal.org/node/774892).
// The forum vocabulary has categories as terms (e.g. "Discussion", "Help Desk")
// and subsequent sub terms that belong to one of these parent terms. This allows
// an arbitrary number of layers to exist in forum organization. BOINC forums map
// to Drupal as follows:
//
//   Category -> Root level term in the vocabulary
//   Topic / forum -> Term in the vocabulary with parent term
//   Thread -> Content node
//   Post -> Comment

ini_set('memory_limit', '256M');

/**
 * Implementation of hook_menu()
 */
function boincimport_menu() {
  $items = array();

  $items['admin/boinc/import'] = array(
    'title' => 'Environment: Data import',
    'description' => 'Overview of the BOINC data import process.',
    'access callback' => 'user_access',
    'access arguments' => array('import boinc data'),
    'page callback' => 'boincimport_main',
    'file' => 'boincimport.pages.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/boinc/import/post_configuration'] = array(
    'title' => 'Post-migration configuration',
    'description' => 'Tie up a few odds and ends in the system configuration
      after data importation is complete.',
    'access callback' => 'user_access',
    'access arguments' => array('import boinc data'),
    'page callback' => 'boincimport_post_configuration',
    'file' => 'boincimport.pages.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/boinc/import/cleanup'] = array(
    'title' => 'Cleanup',
    'access callback' => 'user_access',
    'access arguments' => array('import boinc data'),
    'page callback' => 'boincimport_cleanup',
    'type' => MENU_CALLBACK,
  );
  $items['admin/boinc/import/complete/%'] = array(
    'title' => 'Complete',
    'access callback' => 'user_access',
    'access arguments' => array('import boinc data'),
    'page callback' => 'boincimport_complete',
    'page arguments' => array(4),
    'type' => MENU_CALLBACK,
  );
  $items['admin/boinc/import/process'] = array(
    'title' => 'Execute migration',
    'access callback' => 'user_access',
    'access arguments' => array('import boinc data'),
    'page callback' => 'boincimport_process',
    'type' => MENU_CALLBACK,
  );
  $items['admin/boinc/import/reset'] = array(
    'title' => 'Reset BOINC database URL',
    'access callback' => 'user_access',
    'access arguments' => array('import boinc data'),
    'page callback' => 'boincimport_reset',
    'type' => MENU_CALLBACK,
  );
  $items['admin/boinc/import/unlock'] = array(
    'title' => 'Unlock BOINC import process',
    'access callback' => 'user_access',
    'access arguments' => array('import boinc data'),
    'page callback' => 'boincimport_unlock',
    'type' => MENU_CALLBACK,
  );
  $items['admin/boinc/import/settings'] = array(
    'title' => 'BOINC data import settings',
    'description' => 'Configure the BOINC data import process in preparation
      for pulling user accounts, teams, and forums into Drupal.',
    'access callback' => 'user_access',
    'access arguments' => array('import boinc data'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('boincimport_admin_settings'),
    'file' => 'boincimport.pages.inc',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implementation of hook_perm()
 */
function boincimport_perm() {
  return array('import boinc data');
}

/**
 * Callback admin/boinc/import/reset
 */
function boincimport_reset() {
  global $db_url;
  $boinc_db_url = (is_array($db_url)) ? (isset($db_url['boinc_rw']) ? $db_url['boinc_rw'] : $db_url['default']) : $db_url;
  variable_set('boincimport_db_url', $boinc_db_url);
  variable_set('boincimport_ready', 0);
  return '<p>'. t('The BOINC database URL has been reset. You may now <a href="@configlink">go back to the configuration page</a>.',
      array('@configlink' => url('admin/boinc/import/settings'))) .'</p>';
}

/**
 * Callback admin/boinc/import/unlock
 */
function boincimport_unlock() {
  variable_del('boincimport_process_locked');
  return '<p>'. t('The BOINC data import process has been unlocked. You may
    now !proceed_with_import.', array(
      '!proceed_with_import' => l(t('proceed with the import'),
        'admin/boinc/import/process')
    )) .'</p>';
}

/**
 * Callback admin/boinc/import/cleanup
 */
function boincimport_cleanup() {
  return boincimport_process_cleanup() .'<p>'. t('Drupal database cleaned.') .'</p>';
}

/**
 * Set database connection for boincimport
 *
 * @return
 *   1 if can connect to BOINC database.
 *
 * BEWARE: if you test using db_connect and the connection
 * fails, the process will die() which is a bit too much since we only
 * want to test. Therefore, the test part of the code is not used, now.
 */
function _boincimport_db_connect($test= 0) {
  global $db_url;
  $db_ready = variable_get('boincimport_db_configured', 1);
  if (!$db_ready) {
    if (is_array($db_url)) {
      $db_url2 = $db_url;
    } else {
      $db_url2['default'] = $db_url;
    }
    $db_url2['boinc_rw'] = variable_get('boincimport_db_url', $db_url);
    $GLOBALS['db_url'] =& $db_url2;
    if ($test) {
      if (!db_connect($db_url2['boinc_rw'])) {
        return 0;
      }
    }
  }
  return 1;

}

/**
 * Check if the module is enabled.
 *
 * @return array
 *    $out['html'] = formatted html.
 *    $out['result'] = boolean.
 */
function _boincimport_check_module($module) {
  $out['html'] = '<ul>';
  $result = module_exists($module);
  $out['result'] = $result;
  if ($result == 1) {
    $out['html'] .= '<li>'. t('Module %module is enabled. OK!', array('%module' => $module)) .'</li>';
  }
  else {
    $out['html'] .= '<li><span class="marker">'. t('Module %module is disabled.', array('%module' => $module)) .'</span></li>';
  }
  $out['html'] .= '</ul>';
  return $out;
}

/**
 * Check if the sql tables are installed.
 *
 * @return array
 *   $out['html'] = formatted html.
 *   $out['result'] = boolean.
 */
function _boincimport_check_tables($tables = array(), $db = 'default' , $prefix = 1) {
  _boincimport_db_connect();

  $out['html'] = '<ul>';
  $out['result']= 1;
  foreach ($tables as $table) {
    if ($prefix) {
      $table = db_prefix_tables('{'. $table .'}');
    }

    db_set_active($db);
    if ($GLOBALS['db_type'] == 'pgsql') {
      // adapt from db_table_exists in database.pgsql.inc
      $result = (bool) db_result(db_query("SELECT COUNT(*) FROM pg_class WHERE relname = '%s'", $table));
    }
    else {
      // adapt from db_table_exists in database.mysql.inc
      $result = (bool) db_fetch_object(db_query("SHOW TABLES LIKE '%s'", $table));
    }
    db_set_active('default');
    if ($result) {
      $out['html'] .= '<li>'. t('Table %table: OK!', array('%table' => $table)) .'</li>';
    }
    else {
      $out['html'] .= '<li><span class="marker">'. t('Table <strong>%table</strong> does not exist!', array('%table' => $table)) .'</span></li>';
      $out['result']= 0;
    }
  }
  $out['html'] .= '</ul>';
  return $out;
}



function boincimport_process() {
  // Start with a quick sanity check on the BOINC environment
  boinc_get_path();

  if (!variable_get('boincimport_ready', 0)) {
    return '<p>'. t('You cannot import the data now. Please <a href="@settings">complete the setup first</a>', array('@settings' => url('admin/boinc/import/settings'))) .'</p>';
  }

  $output = 'BOINC import process form';
  $output .= drupal_get_form('boincimport_process_form');
  return $output;
}

function boincimport_process_form() {
  $form = array();
  _boincimport_db_connect() ;
  // Causes problems with form api redirect
  //ini_set('display_errors', TRUE);

  // Adjust how long you want the script to run...
  if (!ini_get('safe_mode')) {
    // This will always be set on PHP7, but not on PHP5 with safe mode
    set_time_limit(variable_get('boincimport_time_limit', 0));
  }

  // Check for a lock on the import process
  if (variable_get('boincimport_process_locked', 0)) {
    drupal_set_message(t('The import process is locked. If you are sure that
      it is no longer running, you may !unlock_it',
      array('!unlock_it' => l(t('release the lock'),
        'admin/boinc/import/unlock')
      )), 'warning');
    watchdog('boincimport', 'The import process is locked. If you are sure that
      it is no longer running, you may !unlock_it',
      array('!unlock_it' => l(t('release the lock'),
        'admin/boinc/import/unlock')
      ), WATCHDOG_WARNING);
  }
  else {
    $boincimport_functions = array(
      'users' => t('Import users'),
      'teams' => t('Import teams'),
      'friends' => t('Import friendships'),
      'preferences' => t('Import user preferences'),
      'private messages' => t('Import private messages'),
      'categories' => t('Import forum containers'),
      'topics' => t('Import topics'),
      'posts' => t('Import posts'),
      'team forums' => t('Import team forums'),
      'team topics' => t('Import team topics'),
      'team posts' => t('Import team posts'),
      'url' => t('Transform URLs'),
    );

    $form['import'] = array(
      '#type' => 'select',
      '#title' => t('Next import to perform'),
      '#default_value' => $_SESSION['boincimport_stage_selected'],
      '#options' => $boincimport_functions,
    );
    $form[] = array(
      '#type' => 'submit',
      '#value' => t('Import'),
    );
  }
  return $form;
}

function boincimport_process_form_submit($form, $form_state) {

  // Lock the import process
  if (!variable_get('boincimport_process_locked', 0)) {
    variable_set('boincimport_process_locked', 1);
  }
  else {
    watchdog('boincimport', 'The import process is locked, but another process
      is trying to access it...', array(), WATCHDOG_WARNING);
  }

  switch ($form_state['values']['import']) {
    case 'users':
      boincimport_users();
      break;

    case 'teams':
      boincimport_teams();
      if (!variable_get('boincimport_import_team_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'teams';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'friends';
      }
      break;

    case 'friends':
      boincimport_friends();
      if (!variable_get('boincimport_import_friend_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'friends';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'preferences';
      }
      break;

    case 'preferences':
      boincimport_preferences();
      if (!variable_get('boincimport_import_preferences_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'preferences';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'private messages';
      }
      break;

    case 'private messages':
      boincimport_private_msgs();
      if (!variable_get('boincimport_import_private_msg_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'private messages';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'categories';
      }
      break;

    case 'categories':
      boincimport_forum_categories();
      if (!variable_get('boincimport_import_category_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'categories';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'topics';
      }
      break;

    case 'topics':
      boincimport_forum_topics();
      if (!variable_get('boincimport_import_topic_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'topics';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'posts';
      }
      break;

    case 'posts':
      boincimport_forum_posts();
      if (!variable_get('boincimport_import_post_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'posts';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'url';
      }
      break;

    case 'team forums':
      boincimport_team_forums();
      if (!variable_get('boincimport_team_forum_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'team forums';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'team topics';
      }
      break;

    case 'team topics':
      boincimport_team_forum_topics();
      if (!variable_get('boincimport_team_topic_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'team topics';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'team posts';
      }
      break;

    case 'team posts':
      boincimport_team_forum_posts();
      if (!variable_get('boincimport_team_post_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'team posts';
      }
      else {
        $_SESSION['boincimport_stage_selected'] = 'url';
      }
      break;

    case 'url':
      boincimport_replace_urls();
      if (!variable_get('boincimport_replace_url_successful', 0)) {
        $_SESSION['boincimport_stage_selected'] = 'url';
      }
      else {
        drupal_set_message('Congratulations.  Import Finished');
        drupal_set_message('Please visit the '. l('Post migration configuration', 'admin/boinc/import/post_configuration') .' page');
        watchdog('boincimport', 'Import process is complete', array(), WATCHDOG_INFO);
        unset($_SESSION['boincimport_stage_selected']);
      }
      break;
    default:
      $_SESSION['boincimport_stage_selected'] = 'users';
      break;
  }
}

function boincimport_complete($section) {
  switch ($section) {
  case 'users':
    // Set the user import successful flag in the variable table
    variable_set('boincimport_import_user_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'teams';
    break;
  case 'teams':

    break;

  default:
  }

  // Release the lock on the import process
  variable_del('boincimport_process_locked');

  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Users
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Import users (at least those required for further data import)
 */
function boincimport_users() {
  // Check whether the user table has been successfully imported already
  if (variable_get('boincimport_import_user_successful', 0)) {
    drupal_set_message(t('Note: user import has already run successfully'));
    watchdog(
      'boincimport', 'Note: user import has already run successfully',
      array(), WATCHDOG_INFO
    );
  }

  if (!variable_get('boincimport_import_user_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_user_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');
  $import_lurkers = variable_get('boincimport_import_lurkers', 1);

  // Determine which users need to be processed
  db_set_active('boinc_rw');
  if ($import_lurkers) {
    // Import all users, even those who have no community participation; other
    // users will be imported when they first try to log into the drupal site
    $boinc_accounts = db_query('
      SELECT id FROM {user}
      ORDER BY id ASC'
    );
    $user_count = mysqli_num_rows($boinc_accounts);
  }
  else {
    // Need to import any user who is currently ignored in order to keep them
    // ignored... not particularly clean (ignored users are stored in a string)
    $ignored_user_list = array(0);
    $ignoring_users = db_query("
      SELECT ignorelist
      FROM forum_preferences
      WHERE ignorelist <> ''
      ORDER BY userid ASC"
    );
    while ($ignoring_user = db_fetch_object($ignoring_users)) {
      $ignored_user_list = $ignored_user_list + array_fill_keys(explode('|', trim($ignoring_user->ignorelist, '|')), 1);
    }
    $ignored_user_list = array_keys($ignored_user_list);
    // Get IDs for all users who will need to be imported now
    $boinc_accounts = db_query("
      SELECT id FROM
      (
        (SELECT id FROM {user} WHERE teamid > 0 OR id IN(%s)) UNION
        (SELECT DISTINCT user FROM {post}) UNION
        (SELECT DISTINCT userid FROM {subscriptions}) UNION
        (SELECT DISTINCT user_src FROM {friend} WHERE reciprocated = 1) UNION
        (SELECT DISTINCT user_dest FROM {friend} WHERE reciprocated = 1) UNION
        (SELECT DISTINCT userid FROM {forum_preferences} WHERE ignorelist <> '') UNION
        (SELECT DISTINCT userid FROM {private_messages}) UNION
        (SELECT DISTINCT senderid FROM {private_messages})
      ) AS usersToImport",
      implode(',', $ignored_user_list)
    );
    $user_count = mysqli_num_rows($boinc_accounts);
  }
  db_set_active('default');

  if (!$user_count) {
    drupal_set_message(
      t('There were no users found: Aborting script'), 'error'
    );
    watchdog('boincimport',
      'There were no users found: Aborting script', array(), WATCHDOG_INFO
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no users found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %user_count users: Beginning import',
    array('%user_count' => $user_count), WATCHDOG_INFO
  );

  // User import relies on Drupal and BOINC APIs to manage data being read
  // from one database and saved to the other. This approach keeps things
  // clean and simple, but since a sizable user base will wreak havoc on system
  // resources, the job is broken into batches here and each batch is processed
  // by a separate process.

  //$batch_size = variable_get('boincimport_user_batch_size', 50);
  //$batch_count = $user_count - ($user_count % $batch_size) + $batch_size;
  $operations = array();
  $existing_users = array();
  $duplicates = array();

  // Get the list of users already in Drupal to be sure we're not importing
  // any twice
  $result = db_query('
    SELECT uid, boinc_id FROM {boincuser}'
  );
  while ($row = db_fetch_object($result)) {
    $existing_users[$row->boinc_id] = $row->uid;
  }

  // Create batches to process
  while ($boinc_account = db_fetch_object($boinc_accounts)) {
     if (isset($existing_users[$boinc_account->id])) {
      // This user has already been imported
      $duplicates[] = $boinc_account->id;
    }
    else {
      $operations[] = array(
        'boincimport_users_op', array(
          $boinc_account->id
        )
      );
    }
  }

  if ($duplicates) {
    drupal_set_message(t(
      'Skipped @count accounts that were already imported',
      array('@count' => count($duplicates))
    ));
    watchdog('boincimport',
      'Skipped @count accounts that were already imported',
      array('@count' => count($duplicates)), WATCHDOG_INFO
    );
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_users_finished',
    'title' => t('Importing users'),
    'init_message' => t('Beginning user import...'),
    'progress_message' => t('Processed @current out of @total users.'),
    'error_message' => t('User import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing users
 */
function boincimport_users_op($boinc_id, &$context) {

  // Use the $context['sandbox'] to store information needed to track progress
  // between successive calls.
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['current_user'] = 0;
    $context['sandbox']['max'] = 1;
  }

  // Note about batch size: When a batch is processed, the batch update engine
  // determines whether it should continue processing in the same request or
  // provide progress feedback to the user and wait for the next request.

  // Grab the BOINC user object and create a Drupal user from it
  $account = boincuser_register_make_drupal_user($boinc_id);
  $message = '';
  if ($account) {
    // Store some result for post-processing in the finished callback.
    $context['results']['success'][] = $boinc_id;
    $message = "Successfully imported user {$boinc_id}";
  }
  else {
    $context['results']['failure'][] = $boinc_id;
    $message = "Failed to import user {$boinc_id}!";
    watchdog('boincimport',
      'Failed to import user @id!',
      array('@id' => $boinc_id), WATCHDOG_WARNING
    );
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_user'] = $boinc_id;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_users_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_imported = count($results['success']);
    $total_failures = count($results['failure']);
    $message = t(
      'Successfully imported @count users (@fail_count failures)',
      array('@count' => $total_imported, '@fail_count' => $total_failures)
    );
    watchdog('boincimport',
      'Successfully imported @count users (@fail_count failures).',
      array('@count' => $total_imported, '@fail_count' => $total_failures),
      WATCHDOG_INFO
    );
    // Set the user import successful flag in the variable table
    variable_set('boincimport_import_user_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'teams';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Teams
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Create teams and assign members
 */
function boincimport_teams() {

  // Check whether the team table has been successfully imported already
  if (variable_get('boincimport_import_team_successful', 0)) {
    drupal_set_message(t('Note: team import has already run successfully'));
    watchdog(
      'boincimport', 'Note: team import has already run successfully',
      array(), WATCHDOG_INFO
    );
  }

  if (!variable_get('boincimport_import_team_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_team_started', 1);
  }

  // Initialize the map of BOINC team types to taxonomy IDs, if needed
  $team_type_map = variable_get('boincimport_team_types', array());
  if (!$team_type_map) {
    // Import team types from BOINC to a Drupal vocabulary
    require_boinc('team_types');
    global $team_types;

    // Create vocabulary if it isn't set
    $team_vid = db_result(db_query('SELECT vid FROM {vocabulary} WHERE name="%s"', 'Teams'));
    if (!$team_vid) {
      $team_vocab = array(
        'name' => t('Teams'),
        'description' => t('Types of BOINC teams'),
      );
      taxonomy_save_vocabulary($team_vocab);
      $team_vid = db_result(db_query('SELECT vid FROM {vocabulary} WHERE name="%s"', 'Teams'));
    }

    foreach ($team_types as $boinc_type_id => $name) {
      // Check for an existing term in the vocabulary
      $team_type_id = db_result(db_query("SELECT tid FROM {term_data} WHERE vid = '{$team_vid}' AND LOWER(name) = LOWER('%s')", trim($name)));
      if ($team_type_id) {
        $team_type = array(
          'tid' => $team_type_id
        );
      }
      else {
        if (!$name) continue;
        $team_type = array(
          'name' => strip_tags($name),
          'vid' => $team_vid,
          'description' => '',
          'parent' => 0
        );
        taxonomy_save_term($team_type);
      }
      // Note the taxonomy ID for mapping forums to categories
      $team_type_map[$boinc_type_id] = $team_type['tid'];
    }
    variable_set('boincimport_team_types', $team_type_map);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Get the list of teams to import
  db_set_active('boinc_rw');
  $boinc_teams = db_query('
    SELECT id, name, description, userid, create_time
    FROM %steam',
    $pre
  );
  $team_count = mysqli_num_rows($boinc_teams);
  db_set_active('default');

  if (!$team_count) {
    drupal_set_message(
      t('There were no teams found: Aborting script'), 'error'
    );
    watchdog('boincimport',
      'There were no teams found: Aborting script', array(), WATCHDOG_INFO
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no teams found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %team_count teams: Beginning Import',
    array('%team_count' => $team_count), WATCHDOG_INFO
  );

  $operations = array();
  $existing_teams = array();
  $duplicates = array();

  // Get the list of teams already in Drupal to be sure we're not importing
  // any twice
  $result = db_query('
    SELECT nid, team_id FROM {boincteam}'
  );
  while ($row = db_fetch_object($result)) {
    $existing_teams[$row->team_id] = $row->nid;
  }

  // Create batches to process
  while ($boinc_team = db_fetch_object($boinc_teams)) {
     if (isset($existing_teams[$boinc_team->id])) {
      // This team has already been imported
      $duplicates[] = $boinc_team->id;
    }
    else {
      $operations[] = array(
        'boincimport_teams_op', array(
          $boinc_team
        )
      );
    }
  }

  if ($duplicates) {
    drupal_set_message(t(
      'Skipped @count teams that were already imported',
      array('@count' => count($duplicates))
    ));
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_teams_finished',
    'title' => t('Importing teams'),
    'init_message' => t('Beginning team import...'),
    'progress_message' => t('Processed @current out of @total teams.'),
    'error_message' => t('Team import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing teams
 * Create a Drupal node from the given BOINC team object
 */
function boincimport_teams_op($boincteam, &$context) {

  $success = boincteam_import($boincteam);

  $message = '';
  if ($success) {
    // Store some result for post-processing in the finished callback.
    $context['results']['success'][] = $boincteam->id;
    $message = "Successfully imported team {$boincteam->id}";
  }
  else {
    $context['results']['failure'][] = $boincteam->id;
    $message = "Failed to import team {$boincteam->id}!";
    watchdog('boincimport',
      'Failed to import team @id!',
      array('@id' => $boincteam->id), WATCHDOG_WARNING
    );
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_team'] = $boincteam->id;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_teams_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_imported = count($results['success']);
    $message = t(
      'Successfully imported @count teams',
      array('@count' => $total_imported)
    );
    watchdog('boincimport',
      'Successfully imported @count teams.',
      array('@count' => $total_imported), WATCHDOG_INFO
    );
    // Set the team import successful flag in the variable table
    variable_set('boincimport_import_team_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'friends';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Friends
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Create friend relationships between users
 */
function boincimport_friends() {

  // Check whether friendships have been successfully imported already
  if (variable_get('boincimport_import_friend_successful', 0)) {
    drupal_set_message(t('Note: Friends import has already run successfully'));
    watchdog(
      'boincimport', 'Note: Friends import has already run successfully',
      array(), WATCHDOG_INFO
    );
  }

  if (!variable_get('boincimport_import_friend_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_friend_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Get stuff to import
  db_set_active('boinc_rw');
  $friendships = db_query('
    SELECT
      f1.user_src,
      f1.user_dest,
      f1.create_time
    FROM %sfriend f1
    LEFT JOIN %sfriend f2
      ON f2.user_src = f1.user_dest
      AND f2.user_dest = f1.user_src
      AND f2.user_src <> f2.user_dest
    WHERE f1.reciprocated = 1
      AND (f2.user_src IS NULL OR f1.create_time < f2.create_time)
    ORDER BY create_time',
    $pre, $pre
  );
  $friendship_count = mysqli_num_rows($friendships);
  db_set_active('default');

  if (!$friendship_count) {
    drupal_set_message(
      t('There were no friendships found: Aborting script'), 'error'
    );
    watchdog('boincimport',
      'There were no friendships found: Aborting script', array(), WATCHDOG_INFO
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no friendships found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %count friend relationships: Beginning Import',
    array('%count' => $friendship_count), WATCHDOG_INFO
  );

  $operations = array();

  // It doesn't matter if a friend relationship has already been imported, just
  // do it again if so

  // Create batches to process
  while ($friendship = db_fetch_object($friendships)) {
    $operations[] = array(
      'boincimport_friends_op', array(
        $friendship
      )
    );
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_friends_finished',
    'title' => t('Importing friend relationships'),
    'init_message' => t('Beginning friend import...'),
    'progress_message' => t('Processed @current out of @total friendships.'),
    'error_message' => t('Friend import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing friendships
 * Create Drupal flags from the given BOINC friend relationship
 */
function boincimport_friends_op($friendship, &$context) {

  $input_format = variable_get('boincimport_input_format', 0);
  $success = FALSE;
  $message = '';

  // Convert BOINC friends to Drupal friends
  $uid = boincuser_lookup_uid($friendship->user_src);
  $friend_uid = boincuser_lookup_uid($friendship->user_dest);
  if ($uid AND $friend_uid) {
    $success = db_query("
      INSERT IGNORE INTO {flag_friend}
      SET uid = '%d', friend_uid = '%d', created = '%d'",
      $uid, $friend_uid, $friendship->create_time
    );
  }
  else {
    $boinc_id = ($uid) ? $friendship->user_dest : $friendship->user_src;
    $message = "No Drupal account exists for BOINC user {$boinc_id}!";
  }

  if ($success) {
    // Store some result for post-processing in the finished callback.
    $context['results']['success'][] = $uid;
    $message = "Successfully made users {$uid} and {$friend_uid} friends";
  }
  else {
    $context['results']['failure'][] = $uid;
    if (!$message) {
      $message = "Failed to make users {$uid} and {$friend_uid} friends!";
    }
    watchdog('boincimport', $message, array(), WATCHDOG_WARNING);
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_user'] = $uid;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_friends_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_imported = count($results['success']);
    $failures = count($results['failure']);
    $message = t(
      'Successfully imported @count friendships (@failed failures)',
      array(
        '@count' => $total_imported,
        '@failed' => $failures,
      )
    );
    watchdog('boincimport', $message, array(), WATCHDOG_INFO);
    // Set the friend import successful flag in the variable table
    variable_set('boincimport_import_friend_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'preferences';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Community preferences
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Import community preferences for users
 */
function boincimport_preferences() {

  // Check whether preferences have been successfully imported already
  if (variable_get('boincimport_import_preferences_successful', 0)) {
    drupal_set_message(t('Note: preferences import has already run successfully'));
    watchdog(
      'boincimport', 'Note: preferences import has already run successfully',
      array(), WATCHDOG_INFO
    );
  }

  if (!variable_get('boincimport_import_preferences_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_preferences_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Currently the only preferences being imported are BOINC "filtered users"
  // This concept of users blocking other users when they don't get along maps
  // to the Drupal ignore users module

  // Find users who are involved in quarrels
  db_set_active('boinc_rw');
  $quarrelers = db_query("
    SELECT userid, ignorelist
    FROM %sforum_preferences
    WHERE ignorelist <> ''",
    $pre
  );
  $quarreler_count = mysqli_num_rows($quarrelers);
  db_set_active('default');

  if (!$quarreler_count) {
    drupal_set_message(
      t('There were no quarrels found: Moving on...')
    );
    watchdog('boincimport',
      'There were no quarrels found: Moving on...', array(), WATCHDOG_INFO
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no quarrels found: Moving on...');
  }

  watchdog('boincimport',
    'Found %count quarreling users: Beginning Import',
    array('%count' => $quarreler_count), WATCHDOG_INFO
  );

  $operations = array();

  // It doesn't matter if a filtered user preference has already been imported,
  // just do it again if so

  // Create batches to process
  while ($quarreler = db_fetch_object($quarrelers)) {
    $operations[] = array(
      'boincimport_quarrels_op', array(
        $quarreler
      )
    );
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_preferences_finished',
    'title' => t('Importing preferences'),
    'init_message' => t('Beginning preference import...'),
    'progress_message' => t('Processed @current out of @total preferences.'),
    'error_message' => t('Preference import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing ignored user settings
 * Convert BOINC ignored users to Drupal ignored users
 */
function boincimport_quarrels_op($boinc_user, &$context) {

  $success = FALSE;
  $uid = boincuser_lookup_uid($boinc_user->userid);
  $ignored_users = explode('|', trim($boinc_user->ignorelist, '|'));
  foreach ($ignored_users as $ignored_user) {
    $ignored_user_uid = boincuser_lookup_uid($ignored_user);
    if (!$ignored_user_uid) {
      $context['results']['warning'][] = "{$uid}:{$ignored_user_uid}";
      watchdog('boincimport',
        'Error adding to ignore list of user @uid: No Drupal ID found for BOINC user @boinc_id',
        array('@boinc_id' => $ignored_user, '@uid' => $uid),
        WATCHDOG_WARNING
      );
      continue;
    }
    $user_ignored = db_query("
      INSERT IGNORE INTO {ignore_user}
      SET uid = '%d', iuid = '%d'",
      $uid, $ignored_user_uid
    );
    if ($user_ignored) {
      $success = TRUE;
    }
    else {
      $context['results']['warning'][] = "{$uid}:{$ignored_user_uid}";
      watchdog('boincimport',
        'Could not add user @ignored_uid to the ignore list of user @uid',
        array('@ignored_uid' => $ignored_user_uid, '@uid' => $uid),
        WATCHDOG_WARNING
      );
    }
  }

  $message = '';
  if ($success) {
    // Store some result for post-processing in the finished callback.
    $context['results']['success'][] = $uid;
    $message = "Successfully imported ignored users for user {$uid}";
  }
  else {
    $context['results']['failure'][] = $uid;
    $message = "Failed to import any user filter preferences for user {$uid}!";
    watchdog('boincimport',
      'Failed to import any user filter preferences for user @id!',
      array('@id' => $uid), WATCHDOG_WARNING
    );
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_user'] = $uid;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_preferences_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_imported = count($results['success']);
    $total_warnings = count($results['warning']);
    $message = t(
      'Successfully imported preferences for @count users (with @warn warnings)',
      array('@count' => $total_imported, '@warn' => $total_warnings)
    );
    watchdog('boincimport',
      'Successfully imported preferences for @count users (with @warn warnings)',
      array('@count' => $total_imported, '@warn' => $total_warnings),
      WATCHDOG_INFO
    );
    // Set the preference import successful flag in the variable table
    variable_set('boincimport_import_preferences_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'private messages';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Private messages
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Import private messages from BOINC
 */
function boincimport_private_msgs() {

  // Check whether private messages have been successfully imported already
  if (variable_get('boincimport_import_private_msg_successful', 0)) {
    drupal_set_message(t('Private message import has already run successfully -- repeating this process could result in duplicate messages!'), 'warning');
    watchdog(
      'boincimport', 'Private message import has already run successfully',
      array(), WATCHDOG_WARNING
    );
    return;
  }

  if (!variable_get('boincimport_import_private_msg_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_private_msg_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Get stuff to import
  db_set_active('boinc_rw');
  $boinc_private_msgs = db_query('
    SELECT id, subject, content, userid, senderid, date, opened
    FROM %sprivate_messages',
    $pre
  );
  $private_msg_count = mysqli_num_rows($boinc_private_msgs);
  db_set_active('default');

  if (!$private_msg_count) {
    drupal_set_message(
      t('There were no private messages found: Moving on...')
    );
    watchdog('boincimport',
      'There were no private messages found: Moving on...', array(), WATCHDOG_INFO
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no private messages found: Moving on...');
  }

  watchdog('boincimport',
    'Found %count private messages: Beginning Import',
    array('%count' => $private_msg_count), WATCHDOG_INFO
  );

  $operations = array();

  // We don't know if a given private message has been imported already or not;
  // if this is needed, a relation table must be added to the Drupal DB

  // Create batches to process
  while ($boinc_private_msg = db_fetch_object($boinc_private_msgs)) {
    $operations[] = array(
      'boincimport_private_msgs_op', array(
        $boinc_private_msg
      )
    );
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_private_msgs_finished',
    'title' => t('Importing private messages'),
    'init_message' => t('Beginning private message import...'),
    'progress_message' => t('Processed @current out of @total private messages.'),
    'error_message' => t('Private message import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing private messages
 * Create a Drupal message from the given BOINC message object
 */
function boincimport_private_msgs_op($pm, &$context) {

  $input_format = variable_get('boincimport_input_format', 0);

  $uid = boincuser_lookup_uid($pm->userid);
  $sender_uid = boincuser_lookup_uid($pm->senderid);

  $pm->content = _boincimport_strip_bbcode($pm->content);
  $pm->content = _boincimport_text_sanitize($pm->content);

  // First save the message
  $message_added = db_query("
    INSERT INTO {pm_message} (subject, author, body, format, timestamp)
    VALUES ('%s', %d, '%s', %d, %d)",
    $pm->subject, $sender_uid, $pm->content, $input_format, $pm->date
  );
  $mid = db_last_insert_id('pm_message', 'mid');

  // Then attach recipients and set status (note that threads are not a BOINC
  // feature, so just consider every message to be a new thread)
  $recipient_added = db_query("
    INSERT INTO {pm_index} (mid, thread_id, uid, is_new, deleted)
    VALUES (%d, %d, %d, %d, 0)",
    $mid, $mid, $uid, !$pm->opened
  );
  // In Drupal, the sender should be attached as well
  $sender_added = db_query("
    INSERT INTO {pm_index} (mid, thread_id, uid, is_new, deleted)
    VALUES (%d, %d, %d, %d, 0)",
    $mid, $mid, $sender_uid, 0
  );

  $message = '';
  if ($message_added AND $recipient_added AND $sender_added) {
    // Store some result for post-processing in the finished callback.
    $context['results']['success'][] = $pm->id;
    $message = "Successfully imported private message {$pm->id}";
  }
  else {
    $context['results']['failure'][] = $pm->id;
    $message = "Failed to import private message {$pm->id}!";
    watchdog('boincimport',
      'Failed to import private message @id!',
      array('@id' => $pm->id), WATCHDOG_WARNING
    );
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_pm'] = $pm->id;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_private_msgs_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_imported = count($results['success']);
    $message = t(
      'Successfully imported @count private messages',
      array('@count' => $total_imported)
    );
    watchdog('boincimport',
      'Successfully imported @count private messages.',
      array('@count' => $total_imported), WATCHDOG_INFO
    );
    // Set the private message import successful flag in the variable table
    variable_set('boincimport_import_private_msg_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'categories';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Forums and containers
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Create forum containers and forums
 */
function boincimport_forum_categories() {

  // Check whether forums have been successfully imported already
  if (variable_get('boincimport_import_forum_category_successful', 0)) {
    drupal_set_message(t('Note: forum container import has already run successfully'));
    watchdog(
      'boincimport', 'Note: forum container import has already run successfully',
      array(), WATCHDOG_INFO
    );
  }

  if (!variable_get('boincimport_import_forum_category_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_forum_category_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Retrieve the vocabulary vid named "Forums"
  //$forum_vid = variable_get('forum_nav_vocabulary', 0);
  $forum_vid = db_result(db_query('
    SELECT vid FROM {vocabulary}
    WHERE name="%s"',
    'Forums'
  ));
  if (!$forum_vid) {
    $forum_vocab = array(
      'name' => t('Forums'),
      'description' => t('The different forum categories / containers'),
    );
    taxonomy_save_vocabulary($forum_vocab);
    $forum_vid = db_result(db_query('
      SELECT vid FROM {vocabulary}
      WHERE name="%s"',
      'Forums'
    ));
  }

  // Get both categories and forums from BOINC
  db_set_active('boinc_rw');
  $boinc_forum_categories = db_query('
    SELECT id, name
    FROM %scategory
    ORDER BY orderID',
    $pre
  );
  $forum_category_count = mysqli_num_rows($boinc_forum_categories);
  $boinc_forums = db_query('
    SELECT id, category, title, description, orderID
    FROM %sforum
    WHERE parent_type = 0
    ORDER BY category',
    $pre
  );
  $forum_count = mysqli_num_rows($boinc_forums);
  db_set_active('default');

  if (!$forum_category_count) {
    drupal_set_message(
      t('There were no forum containers found: Aborting script'), 'warning'
    );
    watchdog('boincimport',
      'There were no forum containers found: Aborting script', array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no forum containers found: Aborting script.');
  }
  if (!$forum_count) {
    drupal_set_message(
      t('There were no forums found: Aborting script'), 'warning'
    );
    watchdog('boincimport',
      'There were no forums found: Aborting script', array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no forums found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %forum_count forums in %category_count containers: Beginning Import',
    array(
      '%forum_count' => $forum_count,
      '%category_count' => $forum_category_count,
    ), WATCHDOG_INFO
  );

  $operations = array();
  $existing_categories = array();
  $existing_forums = array();
  $duplicate_categories = array();
  $duplicate_forums = array();
  $category_map = array();

  // Get the list of categories already in Drupal so as not to import any twice
  $category_tree = taxonomy_get_tree($forum_vid, 0, -1, 1);
  foreach ($category_tree as $term) {
    $existing_categories[] = $term->name;
  }

  // Get the list of forums already in Drupal
  $result = db_query('
    SELECT forum_id, tid FROM {boincimport_temp_forum}'
  );
  while ($row = db_fetch_object($result)) {
    $existing_forums[$row->forum_id] = $row->tid;
  }

  // Create batches to process

  // Set up the "hidden" category, if necessary
  // This is to support automatic hiding of empty categories
  $hidden_forum_tid = db_result(db_query('
    SELECT tid FROM {term_data}
    WHERE vid = %d
    AND name = "%s"',
    $forum_vid, 'Hidden'
  ));
  if (!$hidden_forum_tid) {
    $operations[] = array(
      'boincimport_forum_categories_op', array(
        NULL, $forum_vid, $pre, TRUE
      )
    );
  }

  // Import categories
  while ($boinc_forum_category = db_fetch_object($boinc_forum_categories)) {
    if (in_array($boinc_forum_category->name, $existing_categories)) {
      // This category has already been imported
      $duplicate_categories[] = $boinc_forum_category->name;
    }
    else {
      $operations[] = array(
        'boincimport_forum_categories_op', array(
          $boinc_forum_category, $forum_vid, $pre, FALSE
        )
      );
    }
  }

  // Import forums
  while ($boinc_forum = db_fetch_object($boinc_forums)) {
     if (isset($existing_forums[$boinc_forum->id])) {
      // This forum has already been imported
      $duplicates[] = $boinc_forum->id;
    }
    else {
      $operations[] = array(
        'boincimport_forums_op', array(
          $boinc_forum, $forum_vid, $pre
        )
      );
    }
  }

  // Report any duplicates that were skipped
  $skipped_message = array();
  $categories_skipped = count($duplicate_categories);
  $forums_skipped = count($duplicate_forums);
  if ($categories_skipped) {
    $skipped_message[] = format_plural(
      $categories_skipped,
      '1 container',
      '@count containers'
    );
  }
  if ($forums_skipped) {
    $skipped_message[] = format_plural(
      $forums_skipped,
      '1 forum',
      '@count forums'
    );
  }
  if ($skipped_message) {
    drupal_set_message(t('Skipped @forums that were already imported',
      array('@forums' => implode(' and ', $skipped_message))
    ));
  }

  // Create and run the batch
  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_forums_finished',
    'title' => t('Importing forums'),
    'init_message' => t('Beginning forum import...'),
    'progress_message' => t('Processed @current out of @total forums.'),
    'error_message' => t('Forum import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing categories
 * Create a Drupal taxonomy term from the given BOINC category object
 */
function boincimport_forum_categories_op($category, $forum_vid, $pre, $create_hidden, &$context) {

  // Set term parameters for categories
  $forum_id = 0;
  $parent_id = 0;
  $description = '';
  $weight = 0;
  $hidden = FALSE;

  $category_map = variable_get('boincimport_forum_category_map', array());

  if (!$category AND $create_hidden) {
    // Create the special "hidden" container
    $category = new stdClass();
    $category->name = 'Hidden';
    $category->id = 0;
    $hidden = TRUE;
  }
  else {
    // If this container is empty, put it into the hidden container
    db_set_active('boinc_rw');
    $forums_contained = db_result(db_query('
      SELECT count(*) FROM %sforum
      WHERE parent_type = 0
      AND category = %d',
      $pre, $category->id));
    db_set_active('default');
    if (!$forums_contained) {
      $parent_id = $category_map[0];
      $hidden = TRUE;
    }
  }

  $forum = array(
    'name' => $category->name,
    'vid' => $forum_vid,
    'description' => $description,
    'parent' => $parent_id,
    'weight' => $weight,
  );
  $forum['description'] = strip_tags($forum['description']);

  taxonomy_save_term($forum);
  $success = isset($forum['tid']);

  // Serialize the forum containers
  $containers = variable_get('forum_containers', array());
  $containers[] = $forum['tid'];
  variable_set('forum_containers', $containers);

  // Note the taxonomy ID for mapping forums to categories
  $category_map[$category->id] = $forum['tid'];
  variable_set('boincimport_forum_category_map', $category_map);

  boincimport_forum_set_permissions($forum, $hidden);

  $message = '';
  if ($success) {
    // Store some result for post-processing in the finished callback.
    if (!$category AND $create_hidden) {
      $message = "Created special hidden container";
    }
    else {
      $context['results']['categories']['success'][] = $category->id;
      $message = "Successfully imported container {$category->id}";
    }
  }
  else {
    $context['results']['categories']['failure'][] = $category->id;
    $message = "Failed to import container {$category->id}!";
    watchdog('boincimport',
      'Failed to import container @id!',
      array('@id' => $category->id), WATCHDOG_WARNING
    );
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_category'] = $category->id;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch operation for importing forums
 * Create a Drupal taxonomy term from the given BOINC forum object
 */
function boincimport_forums_op($boincforum, $forum_vid, $pre = '', &$context) {

  $hidden_forum = FALSE;
  $open_forum = FALSE;
  $category_map = variable_get('boincimport_forum_category_map', array());

  // Set term parameters for forums
  $forum_id = $boincforum->id;
  $parent_id = isset($category_map[$boincforum->category]) ? $category_map[$boincforum->category] : $category_map[0];
  $name = $boincforum->title;
  $description = $boincforum->description;
  $weight = $boincforum->orderID;
  if ($parent_id == $category_map[0]) {
    // If this forum is hidden, flag for appropriate access controls
    $hidden_forum = TRUE;
  }
  if ($name == 'Getting Started') {
    // Must allow users to post in this forum even if they have no credit!
    $open_forum = TRUE;
  }

  // Try to detect a BOINC news forum and flag it so that news can be
  // imported into a Drupal news content type later
  if ($name == 'News') {
    // Save the ID of the News forum for later import
    variable_set('boincimport_news_forum_id', $forum_id);
    $success = TRUE;
  }
  else {
    // Save all other forums as taxonomy terms
    $forum = array(
      'name' => $name,
      'vid' => $forum_vid,
      'description' => $description,
      'parent' => $parent_id,
      'weight' => $weight,
    );
    $forum['description'] = strip_tags($forum['description']);

    taxonomy_save_term($forum);
    $success = isset($forum['tid']);

    // Save the forum ID to a temporary reference table - yes this is hackish.
    db_query('INSERT INTO {boincimport_temp_forum} (forum_id, tid) VALUES (%d, %d)', $boincforum->id, $forum['tid']);

    // Set access controls
    boincimport_forum_set_permissions($forum, $hidden_forum, $open_forum);
  }

  $message = '';
  if ($success) {
    // Store some result for post-processing in the finished callback.
    $context['results']['forums']['success'][] = $forum_id;
    $message = "Successfully imported forum {$forum_id}";
  }
  else {
    $context['results']['forums']['failure'][] = $forum_id;
    $message = "Failed to import forum {$forum_id}!";
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_forum'] = $forum_id;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Helper function to set permissions on forums upon import
 */
function boincimport_forum_set_permissions($forum, $hidden = FALSE, $open = FALSE) {

  // Set access controls
  $forum_perms = array();
  $role_map = array_flip(user_roles());
  $forum_perms[$role_map['anonymous user']] = array(
    'view' => (int) !$hidden,
    'update' => 0,
    'delete' => 0,
    'create' => 0,
  );
  $forum_perms[$role_map['authenticated user']] = array(
    'view' => (int) !$hidden,
    'update' => 0,
    'delete' => 0,
    'create' => 0,
  );
  $forum_perms[$role_map['community member']] = array(
    'view' => (int) !$hidden,
    'update' => 0,
    'delete' => 0,
    'create' => (int) $open,
  );
  $forum_perms[$role_map['verified contributor']] = array(
    'view' => (int) !$hidden,
    'update' => 0,
    'delete' => 0,
    'create' => (int) !$hidden,
  );
  $forum_perms[$role_map['moderator']] = array(
    'view' => (int) !$hidden,
    'update' => (int) !$hidden,
    'delete' => 0,
    'create' => (int) !$hidden,
  );
  $forum_perms[$role_map['administrator']] = array(
    'view' => 1,
    'update' => 1,
    'delete' => 1,
    'create' => 1,
  );
  foreach ($forum_perms as $role => $perm) {
    db_query('
      INSERT INTO {forum_access}
      SET  tid = %d, rid = %d,
        grant_view = %d, grant_update = %d,
        grant_delete = %d, grant_create = %d
      ON DUPLICATE KEY UPDATE
        grant_view = %d, grant_update = %d,
        grant_delete = %d, grant_create = %d',
      $forum['tid'], $role,
      $perm['view'], $perm['update'],
      $perm['delete'], $perm['create'],
      $perm['view'], $perm['update'],
      $perm['delete'], $perm['create']);
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_forums_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $categories_imported = count($results['categories']['success']);
    $forums_imported = count($results['forums']['success']);

    $success_message = array();
    if ($categories_imported) {
      $success_message[] = format_plural(
        $categories_imported,
        '1 container',
        '@count containers'
      );
    }
    if ($forums_imported) {
      $success_message[] = format_plural(
        $forums_imported,
        '1 forum',
        '@count forums'
      );
    }
    $message = t(
      'Successfully imported @forums',
      array('@forums' => implode(' and ', $success_message))
    );
    watchdog('boincimport',
      'Successfully imported @forums',
      array('@forums' => implode(' and ', $success_message)), WATCHDOG_INFO
    );
    // Set the forum import successful flag in the variable table
    variable_set('boincimport_import_forum_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'topics';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Forum topics
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */
/**
 * Import BOINC topics as Drupal forum nodes
 */
function boincimport_forum_topics() {

  // Check whether topics have been successfully imported already
  if (variable_get('boincimport_import_topic_successful', 0)) {
    drupal_set_message(t('Topic import has already run successfully'), 'warning');
    watchdog(
      'boincimport', 'Topic import has already run successfully',
      array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return;
  }

  if (!variable_get('boincimport_import_topic_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_topic_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Get the count of non-team topics to import
  db_set_active('boinc_rw');
  $topic_count = db_result(db_query('
    SELECT COUNT(DISTINCT t.id)
    FROM %sthread t
    JOIN %sforum f ON f.id = t.forum
    JOIN %spost p ON p.thread = t.id
    WHERE f.parent_type = 0',
    $pre, $pre, $pre
  ));
  db_set_active('default');

  if (!$topic_count) {
    drupal_set_message(
      t('There were no topics found: Aborting script'), 'warning'
    );
    watchdog('boincimport',
      'There were no topics found: Aborting script', array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no topics found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %count topics: Beginning Import',
    array('%count' => $topic_count), WATCHDOG_INFO
  );

  $operations = array();
  $batch_size = 100;

  // Create batches to process
  for ($offset = 0; $offset < $topic_count; $offset+=$batch_size) {
    $topics_per_batch = $batch_size;
    if ($offset + $batch_size > $topic_count) {
      $topics_per_batch = $topic_count - $offset;
    }
    $operations[] = array(
      'boincimport_topics_op', array(
        $offset, $topics_per_batch, $pre
      )
    );
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_topics_finished',
    'title' => t('Importing topics'),
    'init_message' => t('Beginning topic import...'),
    'progress_message' => t('Processed @current out of @total batches (@size topics per batch).', array(
      '@size' => $batch_size,
    )),
    'error_message' => t('Topic import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing topics
 * Create a Drupal node from the given BOINC topic object
 */
function boincimport_topics_op($offset, $batch_size, $pre = '', &$context) {
  // Initialize the batch, if needed
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = $batch_size;
  }

  $input_format = variable_get('boincimport_input_format', 0);
  $news_forum_id = variable_get('boincimport_news_forum_id', 0);

  // Get the topic to import
  db_set_active('boinc_rw');
  $topics = db_query('
    SELECT DISTINCT t.id, t.title, t.owner, t.forum, t.locked, t.hidden,
        t.sticky, t.timestamp, t.create_time
    FROM %sthread t
    JOIN %sforum f ON f.id = t.forum
    JOIN %spost p ON p.thread = t.id
    WHERE f.parent_type = 0
    ORDER BY t.id ASC
    LIMIT %d,%d',
    $pre, $pre, $pre, $offset, $batch_size
  );
  db_set_active('default');

  while ($topic = db_fetch_object($topics)) {

    $error_detail = '';

    db_set_active('boinc_rw');

    // Get the content of the post that started the topic
    $post = db_fetch_object(db_query('
      SELECT id, content
      FROM %spost
      WHERE thread = %d
      ORDER BY timestamp ASC
      LIMIT 1',
      $pre, $topic->id
    ));
    db_set_active('default');

    $duplicate = db_result(db_query('
      SELECT COUNT(*) FROM {boincimport_temp_topic}
      WHERE topic_id = %d',
      $topic->id
    ));

    if ($duplicate OR !$post) {
      $success = FALSE;
    }

    else {
      // Get the user and term IDs along with other data to define the topic
      $uid = boincuser_lookup_uid($topic->owner);
      $tid = db_result(db_query('
        SELECT tid FROM {boincimport_temp_forum}
        WHERE forum_id = %d',
        $topic->forum
      ));
      if (!$topic->owner) {
        $uid = 0;
      }

      $node_type = 'forum';
      $promote = 0;
      $comment = ($topic->locked) ? 1 : 2;

      $post->content = _boincimport_strip_bbcode($post->content);
      $post->content = _boincimport_text_sanitize($post->content);
      $teaser = node_teaser($post->content);

      if ($topic->timestamp < $topic->create_time) {
        $topic->timestamp = $topic->create_time;
      }

      // If dealing with a News topic, be sure it is imported as such
      if ($news_forum_id AND $topic->forum == $news_forum_id) {
        $node_type = 'news';
        $promote = 1;
      }

      // Construct the thread as a forum topic node
      $node = array(
        'type' => $node_type,
        'title' => $topic->title,
        'uid' => $uid,
        'status' => ($topic->hidden) ? 0 : 1,  // published or not
        'promote' => $promote,
        'created' => $topic->create_time,
        'changed' => $topic->timestamp,
        'comment' => $comment,
        'moderate' => 0,
        'body' => $post->content,
        'sticky' => $topic->sticky,
        'format' => $input_format,
        'teaser' => $teaser,
      );
      $node['tid'] = $tid;

      // Save the topic node
      $node = (object) $node; // node_save requires an object form
      node_save($node);
      taxonomy_node_save($node, array($tid));
      $success = ($node->nid) ? TRUE : FALSE;
      if ($success) {
        $success = db_query('
          INSERT INTO {boincimport_temp_topic} (topic_id, post_id, nid)
          VALUES (%d, %d, %d)', $topic->id, $post->id, $node->nid
        );
        if ($success) {
          // Hack to keep the topics in correct order
          $success = db_query('UPDATE {node_comment_statistics} SET last_comment_timestamp = %d WHERE nid = %d', $node->created, $node->nid);
          if (!$success) {
            $error_detail = 'topic imported, but failed to set last comment timestamp';
          }
        }
        else {
          $error_detail = 'topic node saved, but failed to link in boincimport_temp_topic table';
        }
      }
      else {
        $error_detail = 'failed to save topic node to database';
      }
    }

    // See if the import worked
    $message = '';
    if ($success) {
      // Store some result for post-processing in the finished callback.
      $context['results']['success'][] = $topic->id;
      $message = "Successfully imported topic {$topic->id}";
    }
    elseif ($duplicate) {
      $context['results']['duplicate'][] = $topic->id;
      $message = "Topic {$topic->id} was already imported";
    }
    elseif (!$post) {
      $context['results']['empty'][] = $topic->id;
      $message = "Skipping topic {$topic->id} as empty";
    }
    else {
      $context['results']['failure'][] = $topic->id;
      $message = "Failed to import topic {$topic->id}!";
      watchdog('boincimport',
        'Failed to import topic @id! (@error)',
        array(
          '@id' => $topic->id,
          '@error' => $error_detail,
        ),
        WATCHDOG_WARNING
      );
    }

    // Update our progress information.
    $context['sandbox']['progress']++;
    $context['sandbox']['current_topic'] = $topic->id;
    $context['message'] = $message;

    // Update the progress for the batch engine
    if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
      $context['finished'] = 1;
    }
    else {
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
    }
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_topics_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_imported = count($results['success']);
    $duplicates = count($results['duplicate']);
    $empty_topics = count($results['empty']);
    $message = t(
      'Successfully imported @count topics (skipped @duplicates already imported, @abandoned empty topics)',
      array(
        '@count' => $total_imported,
        '@duplicates' => $duplicates,
        '@abandoned' => $empty_topics,
      )
    );
    watchdog('boincimport',
      'Successfully imported @count topics (skipped @duplicates already imported, @abandoned empty topics).',
      array(
        '@count' => $total_imported,
        '@duplicates' => $duplicates,
        '@abandoned' => $empty_topics,
      ), WATCHDOG_INFO
    );
    // Set the topic import successful flag in the variable table
    variable_set('boincimport_import_topic_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'posts';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Forum posts
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Import BOINC forum posts as Drupal comments
 */
function boincimport_forum_posts() {

  // Check whether forum posts have been successfully imported already
  if (variable_get('boincimport_import_post_successful', 0)) {
    drupal_set_message(t('Forum post import has already run successfully'), 'warning');
    watchdog(
      'boincimport', 'Forum post import has already run successfully',
      array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return;
  }

  if (!variable_get('boincimport_import_post_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_post_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Get the BOINC threads and get a count of posts to import
  db_set_active('boinc_rw');
  $topic_count = db_result(db_query("
    SELECT COUNT(DISTINCT t.id) FROM %sthread t
    JOIN %sforum f ON f.id = t.forum
    JOIN %spost p ON p.thread = t.id
    WHERE f.parent_type = 0", $pre, $pre, $pre
  ));
  $total_post_count = db_result(db_query("
    SELECT COUNT(p.id) FROM %spost p
    JOIN %sthread t ON t.id = p.thread
    JOIN %sforum f ON f.id = t.forum
    WHERE f.parent_type = 0", $pre, $pre, $pre
  ));
  $post_count = $total_post_count - $topic_count;
  db_set_active('default');

  if ($post_count <= 0) {
    drupal_set_message(
      t('There were no posts found: Aborting script'), 'warning'
    );
    watchdog('boincimport',
      'There were no posts found: Aborting script', array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no posts found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %count posts: Beginning Import',
    array('%count' => $post_count), WATCHDOG_INFO
  );

  $operations = array();
  $batch_size = 100;

  // Create batches to process
  for ($offset = 0; $offset < $topic_count; $offset+=$batch_size) {
    $topics_per_batch = $batch_size;
    if ($offset + $batch_size > $topic_count) {
      $topics_per_batch = $topic_count - $offset;
    }
    $operations[] = array(
      'boincimport_posts_op', array(
        $offset, $topics_per_batch
      )
    );
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_posts_finished',
    'title' => t('Importing posts'),
    'init_message' => t('Beginning post import...'),
    'progress_message' => t(
      'Processed posts in @current out of @total batches (@size topics per batch).',
      array(
        '@size' => $batch_size,
        // @current and @total are managed by the batch API
      )
    ),
    'error_message' => t('Post import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing posts
 * Create a Drupal comment from the given BOINC post object
 */
function boincimport_posts_op($offset, $batch_size, &$context) {
  // Initialize the batch, if needed
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = $batch_size;
  }

  $input_format = variable_get('boincimport_input_format', 0);

 // Get the topics with posts to import
  db_set_active('boinc_rw');
  $boinc_topic_ids = db_query('
    SELECT DISTINCT t.id FROM %sthread t
    JOIN %sforum f ON f.id = t.forum
    JOIN %spost p ON p.thread = t.id
    WHERE f.parent_type = 0
    ORDER BY t.id
    LIMIT %d,%d',
    $pre, $pre, $pre, $offset, $batch_size
  );
  db_set_active('default');

  while ($boinc_topic = db_fetch_object($boinc_topic_ids)) {
    // Get the posts in this topic
    db_set_active('boinc_rw');
    $boinc_posts = db_query('
      SELECT id, user, thread, timestamp, content, parent_post, hidden
      FROM %spost WHERE thread = %d ORDER BY id ASC', $pre, $boinc_topic->id);
    db_set_active('default');

    $first_post = true;
    $topic_has_responses = FALSE;
    $success = FALSE;
    $posts_imported = 0;
    $empty_posts = 0;
    $error_posts = 0;
    $duplicate_posts = 0;

    while ($post = db_fetch_object($boinc_posts)) {

      // Skip the first post as it has already been imported as a topic
      if ($first_post) {
        $first_post = false;
        continue;
      }

      // Making it this far confirms that there are posts to import
      $topic_has_responses = TRUE;

      $is_duplicate = db_result(db_query('
        SELECT COUNT(*) FROM {boincimport_temp_post}
        WHERE post_id = %d',
        $post->id
      ));
      if ($is_duplicate) {
        // This post has already been imported
        $context['results']['posts']['duplicate'][] = $post->id;
        $duplicate_posts++;
        continue;
      }

      // Make sure the post is valid
      if ($post->content) {

        // Get user, node, and parent IDs for the post and sanitize
        $uid = boincuser_lookup_uid($post->user);
        $node = db_fetch_object(db_query('
          SELECT nr.nid, nr.title
          FROM {boincimport_temp_topic} btt
          LEFT JOIN {node_revisions} AS nr ON btt.nid = nr.nid
          WHERE btt.topic_id = %d',
          $post->thread
        ));
        $nid = $node->nid;
        $pid = db_result(db_query('
          SELECT cid
          FROM {boincimport_temp_post}
          WHERE post_id = %d',
          $post->parent_post));
        if (is_null($pid)) $pid = 0;
        if (!$uid) $uid = 0;

        $post->content = _boincimport_strip_bbcode($post->content);
        $post->content = _boincimport_text_sanitize($post->content);

        $topic_reply = db_result(db_query('
          SELECT COUNT(*)
          FROM {comments}
          WHERE nid = %d',
          $nid
        ));
        $post_reply = $pid;

        if ($post_reply OR $topic_reply) {
          // Create a subject for the post from the post content. The body may be in
          // any format, so we:
          //  1) Filter it into HTML
          //  2) Strip out all HTML tags
          //  3) Convert entities back to plain-text.
          // Note: format is checked by check_markup().
          $subject = truncate_utf8(trim(decode_entities(strip_tags(check_markup($post->content, $input_format)))), 29, TRUE);
          // Replace "Quote:" with "RE:"
          $subject = str_replace('Quote:', 'RE: ', $subject);
          // Fringe cases where the comment body is populated only by HTML tags
          // will require a default subject...
          if ($subject === '')
            $subject = "RE: {$node->title}";
        } else {
          // This is the first post in the topic
          $subject = $node->title;
        }

        // Construct the post as a Drupal comment
        $comment = array(
          'pid' => $pid,
          'nid' => $nid,
          'uid' => $uid,
          'subject' => $subject,
          'comment' => $post->content,
          'timestamp' => $post->timestamp,
          'status' => $post->hidden,
          'format' => $input_format
        );

        // Save the comment
        if (boincimport_forum_comment_save($comment)) {
          $success = db_query('
            INSERT INTO {boincimport_temp_post} (post_id, cid)
            VALUES (%d, %d)',
            $post->id, $comment['cid']
          );
          if ($success) {
            $posts_imported++;
            $context['results']['posts']['success'][] = $post->id;
          }
          else {
            $context['results']['posts']['failure'][] = $post->id;
            $error_posts++;
          }
        }
        else {
          $context['results']['posts']['failure'][] = $post->id;
          $error_posts++;
        }
      }
      else {
        $context['results']['posts']['empty'][] = $post->id;
        $empty_posts++;
      }
    }

    $message = '';
    if ($success OR !$topic_has_responses) {
      // Store some result for post-processing in the finished callback.
      $context['results']['success'][] = $boinc_topic->id;
      $message = "Imported {$posts_imported} post(s) for topic {$boinc_topic->id}";
    }
    else {
      $context['results']['failure'][] = $boinc_topic->id;
      $message = "Failed to import any posts for topic {$boinc_topic->id} (excluded {$error_posts} errors, {$duplicate_posts} duplicates, and {$empty_posts} empty)";
      watchdog('boincimport', 'Failed to import any posts for topic @id (excluded @error_posts errors, @duplicate_posts duplicates, and @empty_posts empty)',
        array(
          '@id' => $boinc_topic->id,
          '@error_posts' => $error_posts,
          '@duplicate_posts' => $duplicate_posts,
          '@empty_posts' => $empty_posts,
        ), WATCHDOG_WARNING
      );
    }

    // Update our progress information.
    $context['sandbox']['progress']++;
    $context['sandbox']['current_topic'] = $boinc_topic->id;
    $context['message'] = $message;

    // Update the progress for the batch engine
    if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
      $context['finished'] = 1;
    }
    else {
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
    }
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_posts_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $posts_imported = count($results['posts']['success']);
    $topic_count = count($results['success']);
    $topics_skipped = count($results['failure']);
    $duplicates = count($results['posts']['duplicate']);
    $empty_posts = count($results['posts']['empty']);
    $failed_posts = count($results['posts']['failure']);
    $message = t(
      'Successfully imported @post_count posts in @topic_count topics ' .
      '(@skipped topics either had no replies or all replies were already imported, ' .
      '@duplicates posts were skipped as already imported, ' .
      '@empty_posts had no content, ' .
      'and @error_posts encountered errors during import)',
      array(
        '@post_count' => $posts_imported,
        '@topic_count' => $topic_count,
        '@skipped' => $topics_skipped,
        '@duplicates' => $duplicates,
        '@empty_posts' => $empty_posts,
        '@error_posts' => $failed_posts,
      )
    );
    watchdog('boincimport',
      $message,
      array(), WATCHDOG_INFO
    );
    // Set the post import successful flag in the variable table
    variable_set('boincimport_import_post_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'team forums';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Team forums
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Import team forums
 */
function boincimport_team_forums() {

  // Check whether team forums have been successfully imported already
  if (variable_get('boincimport_import_team_forum_successful', 0)) {
    drupal_set_message(t('Team forum import has already run successfully'), 'warning');
    watchdog(
      'boincimport', 'Team forum import has already run successfully',
      array(), WATCHDOG_WARNING
    );
  }

  if (!variable_get('boincimport_import_team_forum_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_team_forum_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Get team forums from BOINC database
  db_set_active('boinc_rw');
  $boincteam_forums = db_query('
    SELECT id, title, description, category, timestamp, post_min_interval,
      post_min_total_credit, post_min_expavg_credit
    FROM %sforum
    WHERE parent_type = 1
    ORDER BY id ASC',
    $pre
  );
  $team_forum_count = mysqli_num_rows($boincteam_forums);
  db_set_active('default');

  if (!$team_forum_count) {
    drupal_set_message(
      t('There were no team forums found: Aborting script'), 'warning'
    );
    watchdog('boincimport',
      'There were no team forums found: Aborting script', array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no BLAH found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %count team forums: Beginning Import',
    array('%count' => $team_forum_count), WATCHDOG_INFO
  );

  $operations = array();
  $existing_team_forums = array();
  $duplicates = array();

  // Get the list of team forums already in Drupal to be sure we're not
  // importing any twice
  $result = db_query('
    SELECT nid, boinc_id FROM {boincteam_forum}'
  );
  while ($row = db_fetch_object($result)) {
    $existing_team_forums[$row->boinc_id] = $row->nid;
  }

  // Create batches to process
  while ($boincteam_forum = db_fetch_object($boincteam_forums)) {
     if (isset($existing_team_forums[$boincteam_forum->id])) {
      // This team has already been imported
      $duplicates[] = $boincteam_forum->id;
    }
    else {
      $operations[] = array(
        'boincimport_team_forums_op', array(
          $boincteam_forum
        )
      );
    }
  }

  if ($duplicates) {
    drupal_set_message(t(
      'Skipped @count team forums that were already imported',
      array('@count' => count($duplicates))
    ));
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_team_forums_finished',
    'title' => t('Importing team forums'),
    'init_message' => t('Beginning team forum import...'),
    'progress_message' => t('Processed @current out of @total team forums.'),
    'error_message' => t('Team forum import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing team forums
 * Create an entry in the boincteam_forum table for the given BOINC team forum
 * object
 */
function boincimport_team_forums_op($boincteam_forum, &$context) {

  $input_format = variable_get('boincimport_input_format', 0);

  // Set term parameters for forums
  $forum_id = $boincteam_forum->id;
  $team_id = boincteam_lookup_nid($boincteam_forum->category);
  $name = $boincteam_forum->title;
  $description = strip_tags($boincteam_forum->description);

  $success = db_query("
    INSERT INTO {boincteam_forum} SET
      boinc_id = %d,
      nid = %d,
      title = '%s',
      description = '%s',
      created = %d,
      updated = %d,
      public = %d,
      min_time_between_posts = %d,
      min_total_credit_to_post = %d,
      min_avg_credit_to_post = %d",
    $forum_id, $team_id, $name, $description, $boincteam_forum->timestamp,
    time(), 0, $boincteam_forum->post_min_interval,
    $boincteam_forum->post_min_total_credit,
    $boincteam_forum->post_min_expavg_credit
  );

  $message = '';
  if ($success) {
    // Store some result for post-processing in the finished callback.
    $context['results']['success'][] = $forum_id;
    $message = "Successfully imported team forum {$forum_id}";
  }
  else {
    $context['results']['failure'][] = $forum_id;
    $message = "Failed to import team forum {$forum_id}!";
    watchdog('boincimport',
      'Failed to import team forum @id!',
      array('@id' => $forum_id), WATCHDOG_WARNING
    );
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_forum'] = $forum_id;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_team_forums_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_imported = count($results['success']);
    $message = t(
      'Successfully imported @count team forums',
      array('@count' => $total_imported)
    );
    watchdog('boincimport',
      'Successfully imported @count team forums.',
      array('@count' => $total_imported), WATCHDOG_INFO
    );
    // Set the team forum import successful flag in the variable table
    variable_set('boincimport_import_team_forum_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'team topics';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Team forum topics
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Import BOINC team topics to Drupal team_forum type nodes
 */
function boincimport_team_forum_topics() {

  // Check whether team forum topics have been successfully imported already
  if (variable_get('boincimport_import_team_topic_successful', 0)) {
    drupal_set_message(t('Team topic import has already run successfully'), 'warning');
    watchdog(
      'boincimport', 'Team topic import has already run successfully',
      array(), WATCHDOG_WARNING
    );
  }

  if (!variable_get('boincimport_import_team_topic_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_team_topic_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Get all team topics to import from BOINC
  db_set_active('boinc_rw');
  $boincteam_topics = db_query('
    SELECT DISTINCT t.id, t.title, t.owner, t.forum, t.locked, t.hidden,
      t.sticky, t.timestamp, t.create_time
    FROM %sthread t
    JOIN %sforum f ON f.id = t.forum
    JOIN %spost p ON p.thread = t.id
    WHERE f.parent_type = 1
    ORDER BY id',
    $pre, $pre, $pre
  );
  $boincteam_topic_count = mysqli_num_rows($boincteam_topics);
  $total_team_topic_count = db_result(db_query('
    SELECT COUNT(*) FROM %sthread t
    JOIN %sforum f ON f.id = t.forum
    WHERE f.parent_type = 1', $pre, $pre
  ));
  $empty_topic_count = $total_team_topic_count - $boincteam_topic_count;
  db_set_active('default');

  if (!$boincteam_topic_count) {
    drupal_set_message(
      t('There were no team topics found: Aborting script'), 'warning'
    );
    watchdog('boincimport',
      'There were no team topics found: Aborting script', array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no team topics found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %count team topics: Beginning Import',
    array('%count' => $boincteam_topic_count), WATCHDOG_INFO
  );

  $operations = array();
  $existing_team_topics = array();
  $duplicates = array();

  // Get the list of team topics already in Drupal to be sure we're not
  // importing any twice
  $result = db_query('
    SELECT nid, topic_id FROM {boincimport_temp_topic}'
  );
  while ($row = db_fetch_object($result)) {
    $existing_team_topics[$row->topic_id] = $row->nid;
  }

  // Create batches to process
  while ($boincteam_topic = db_fetch_object($boincteam_topics)) {
     if (isset($existing_team_topics[$boincteam_topic->id])) {
      // This team topic has already been imported
      $duplicates[] = $boincteam_topic->id;
    }
    else {
      $operations[] = array(
        'boincimport_team_topics_op', array(
          $boincteam_topic
        )
      );
    }
  }

  if ($duplicates) {
    drupal_set_message(t(
      'Skipped @count team topics that were already imported',
      array('@count' => count($duplicates))
    ));
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_team_topics_finished',
    'title' => t('Importing team topics'),
    'init_message' => t('Beginning team topic import...'),
    'progress_message' => t('Processed @current out of @total team topics.'),
    'error_message' => t('Team topic import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing team topics
 * Create a Drupal node from the given BOINC team topic object
 */
function boincimport_team_topics_op($topic, &$context) {

  $input_format = variable_get('boincimport_input_format', 0);
  $success = FALSE;
  $missing_parent = array();
  $empty_topics = array();

  // Verify that the team forum container has been imported
  $team_forum_id = db_result(db_query("
    SELECT tfid FROM {boincteam_forum}
    WHERE boinc_id = %d",
    $topic->forum
  ));
  if (!$team_forum_id) {
    $missing_parent[] = $topic->id;
  }
  else {
    // Get the content of the post that started the topic
    db_set_active('boinc_rw');
    $query = db_query('
      SELECT id, content
      FROM %spost
      WHERE thread = %d
      ORDER BY timestamp ASC
      LIMIT 1',
      $pre, $topic->id);
    db_set_active('default');

    // Skip this topic if there are no posts
    if (!$post = db_fetch_object($query)) {
      // Empty topics should have already been filtered out of the import, so
      // consider this an error condition
      $empty_topics[] = $topic->id;
    }
    else {
      // Get the user ID along with other data to define the topic
      $uid = boincuser_lookup_uid($topic->owner);
       if (!$topic->owner) {
        $uid = 0;
      }

      $node_type = 'team_forum';
      $promote = 0;
      $comment = ($topic->locked) ? 1 : 2;

      $post->content = _boincimport_strip_bbcode($post->content);
      $post->content = _boincimport_text_sanitize($post->content);
      $teaser = node_teaser($post->content);

      if ($topic->timestamp < $topic->create_time) {
        $topic->timestamp = $topic->create_time;
      }

      // Construct the thread as a team_forum topic node
      $node = array(
        'type' => $node_type,
        'title' => $topic->title,
        'uid' => $uid,
        'status' => ($topic->hidden) ? 0 : 1,  // published or not
        'promote' => $promote,
        'created' => $topic->create_time,
        'changed' => $topic->timestamp,
        'comment' => $comment,
        'moderate' => 0,
        'body' => $post->content,
        'sticky' => $topic->sticky,
        'format' => $input_format,
        'teaser' => $teaser,
        'tfid' => $team_forum_id,
      );

      // Save the team topic node
      $node = (object) $node; // node_save requires an object form
      node_save($node);

      if ($node->nid) {
        db_query('
          INSERT INTO {boincimport_temp_topic} (topic_id, post_id, nid)
          VALUES (%d, %d, %d)',
          $topic->id, $post->id, $node->nid
        );
        // Hack to keep the topics in correct order
        db_query('
          UPDATE {node_comment_statistics}
          SET last_comment_timestamp = %d
          WHERE nid = %d',
          $node->created, $node->nid
        );
        $success = TRUE;
      }
    }
  }

  $message = '';
  if ($success) {
    // Store some result for post-processing in the finished callback.
    $context['results']['success'][] = $topic->id;
    $message = "Successfully imported team topic {$topic->id}";
  }
  else {
    $context['results']['failure'][] = $topic->id;
    $message = "Failed to import team topic {$topic->id}!";
    watchdog('boincimport',
      'Failed to import team topic @id!',
      array('@id' => $topic->id), WATCHDOG_WARNING
    );
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_topic'] = $topic->id;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_team_topics_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_imported = count($results['success']);
    $message = t(
      'Successfully imported @count team topics',
      array('@count' => $total_imported)
    );
    watchdog('boincimport',
      'Successfully imported @count team topics.',
      array('@count' => $total_imported), WATCHDOG_INFO
    );
    // Set the BLAH import successful flag in the variable table
    variable_set('boincimport_import_team_topic_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'team posts';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Team forum posts
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Import BOINC team forum posts as Drupal comments
 */
function boincimport_team_forum_posts() {

  // Check whether team forum posts have been successfully imported already
  if (variable_get('boincimport_import_team_post_successful', 0)) {
    drupal_set_message(t('Team forum post import has already run successfully'), 'warning');
    watchdog(
      'boincimport', 'Team forum post import has already run successfully',
      array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return;
  }

  if (!variable_get('boincimport_import_team_post_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_team_post_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Get the BOINC threads and get a count of team posts to import
  db_set_active('boinc_rw');
  $team_topic_ids = db_query('
    SELECT DISTINCT t.id FROM %sthread t
    JOIN %sforum f ON f.id = t.forum
    JOIN %spost p ON p.thread = t.id
    WHERE f.parent_type = 1
    ORDER BY id', $pre, $pre
  );
  $team_topic_count = db_result(db_query("
    SELECT COUNT(DISTINCT t.id) FROM %sthread t
    JOIN %sforum f ON f.id = t.forum
    JOIN %spost p ON p.thread = t.id
    WHERE f.parent_type = 1", $pre, $pre, $pre
  ));
  $total_team_post_count = db_result(db_query("
    SELECT COUNT(p.id) FROM %spost p
    JOIN %sthread t ON t.id = p.thread
    JOIN %sforum f ON f.id = t.forum
    WHERE f.parent_type = 1", $pre, $pre, $pre
  ));
  $team_post_count = $total_team_post_count - $team_topic_count;
  db_set_active('default');

  if ($team_post_count <= 0) {
    drupal_set_message(
      t('There were no team posts found: Aborting script'), 'warning'
    );
    watchdog('boincimport',
      'There were no team posts found: Aborting script', array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no posts found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %count team posts: Beginning Import',
    array('%count' => $team_post_count), WATCHDOG_INFO
  );

  $operations = array();
  $existing_posts = array();
  $duplicates = array();

  // Get the list of team posts already in Drupal to be sure we're not
  // importing any twice
  $result = db_query('
    SELECT cid, post_id FROM {boincimport_temp_post}'
  );
  while ($row = db_fetch_object($result)) {
    $existing_posts[$row->post_id] = $row->cid;
  }

  // Create batches to process
  while ($boincteam_topic = db_fetch_object($team_topic_ids)) {

    db_set_active('boinc_rw');
    $boincteam_posts = db_query('
      SELECT id, user, thread, timestamp, content, parent_post, hidden
      FROM %spost
      WHERE thread = %d
      ORDER BY timestamp ASC',
      $pre, $boincteam_topic->id
    );
    db_set_active('default');

    $first_post = true;

    while ($boincteam_post = db_fetch_object($boincteam_posts)) {

      // Skip the first post as it has already been imported as a topic
      if ($first_post) {
        $first_post = false;
        continue;
      }

      if (isset($existing_posts[$boincteam_post->id])) {
        // This post has already been imported
        $duplicates[] = $boincteam_post->id;
      }
      else {
        $operations[] = array(
          'boincimport_team_posts_op', array(
            $boincteam_post
          )
        );
      }
    }
  }

  if ($duplicates) {
    drupal_set_message(t(
      'Skipped @count team posts that were already imported',
      array('@count' => count($duplicates))
    ));
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_team_posts_finished',
    'title' => t('Importing team posts'),
    'init_message' => t('Beginning team post import...'),
    'progress_message' => t('Processed @current out of @total team posts.'),
    'error_message' => t('Team post import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing team posts
 * Create a Drupal comment from the given BOINC team post object
 */
function boincimport_team_posts_op($post, &$context) {

  $input_format = variable_get('boincimport_input_format', 0);
  $success = FALSE;

  // Make sure the post is valid
  if ($post->content) {

    // Get user, node, and parent IDs for the post and sanitize
    $uid = boincuser_lookup_uid($post->user);
    $node = db_fetch_object(db_query('
      SELECT nr.nid, nr.title
      FROM {boincimport_temp_topic} btt
      LEFT JOIN {node_revisions} AS nr ON btt.nid = nr.nid
      WHERE btt.topic_id = %d',
      $post->thread
    ));
    $nid = $node->nid;
    $pid = db_result(db_query('
      SELECT cid
      FROM {boincimport_temp_post}
      WHERE post_id = %d',
      $post->parent_post));
    if (is_null($pid)) $pid = 0;
    if (!$uid) $uid = 0;

    $post->content = _boincimport_strip_bbcode($post->content);
    $post->content = _boincimport_text_sanitize($post->content);

    $topic_reply = db_result(db_query('
      SELECT COUNT(*)
      FROM {comments}
      WHERE nid = %d',
      $nid
    ));
    $post_reply = $pid;

    if ($post_reply OR $topic_reply) {
      // Create a subject for the post from the post content. The body may be in
      // any format, so we:
      //  1) Filter it into HTML
      //  2) Strip out all HTML tags
      //  3) Convert entities back to plain-text.
      // Note: format is checked by check_markup().
      $subject = truncate_utf8(trim(decode_entities(strip_tags(check_markup($post->content, $input_format)))), 29, TRUE);
      // Replace "Quote:" with "RE:"
      $subject = str_replace('Quote:', 'RE: ', $subject);
      // Fringe cases where the comment body is populated only by HTML tags
      // will require a default subject...
      if ($subject === '')
        $subject = "RE: {$node->title}";
    } else {
      // This is the first post in the topic
      $subject = $node->title;
    }

    // Construct the post as a Drupal comment
    $comment = array(
      'pid' => $pid,
      'nid' => $nid,
      'uid' => $uid,
      'subject' => $subject,
      'comment' => $post->content,
      'timestamp' => $post->timestamp,
      'status' => $post->hidden,
      'format' => $input_format
    );

    // Save the comment
    if (boincimport_forum_comment_save($comment)) {
      $success = db_query('
        INSERT INTO {boincimport_temp_post} (post_id, cid)
        VALUES (%d, %d)',
        $post->id, $comment['cid']
      );
    }
  }

  $message = '';
  if ($success) {
    // Store some result for post-processing in the finished callback.
    $context['results']['success'][] = $post->id;
    $message = "Successfully imported team post {$post->id}";
  }
  else {
    $context['results']['failure'][] = $post->id;
    $message = "Failed to import team post {$post->id}!";
    watchdog('boincimport',
      'Failed to import team post @id!',
      array('@id' => $post->id), WATCHDOG_WARNING
    );
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_post'] = $post->id;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_team_posts_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_imported = count($results['success']);
    $message = t(
      'Successfully imported @count team posts',
      array('@count' => $total_imported)
    );
    watchdog('boincimport',
      'Successfully imported @count team posts.',
      array('@count' => $total_imported), WATCHDOG_INFO
    );
    // Set the team post import successful flag in the variable table
    variable_set('boincimport_import_team_post_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'url';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Subscriptions
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Import subscriptions for users
 */
function boincimport_subscriptions() {

  // Check whether subscriptions have been successfully imported already
  if (variable_get('boincimport_import_subscription_successful', 0)) {
    drupal_set_message(t('Subscription import has already run successfully'), 'warning');
    watchdog(
      'boincimport', 'Subscription import has already run successfully',
      array(), WATCHDOG_WARNING
    );
  }

  if (!variable_get('boincimport_import_subscription_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_import_subscription_started', 1);
  }

  $pre = variable_get('boincimport_table_prefix', '');

  // Get users with subscriptions to import
  db_set_active('boinc_rw');
  $users_with_subscriptions = db_query('
    SELECT DISTINCT userid
    FROM %ssubscriptions
    ORDER BY userid ASC',
    $pre
  );
  $user_count = mysqli_num_rows($users_with_subscriptions);
  db_set_active('default');

  if (!$user_count) {
    drupal_set_message(
      t('There were no subscriptions found: Aborting script'), 'warning'
    );
    watchdog('boincimport',
      'There were no subscriptions found: Aborting script', array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no subscriptions found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %count users with subscriptions: Beginning import',
    array('%count' => $user_count), WATCHDOG_INFO
  );

  $operations = array();

  // Create batches to process
  while ($subscribed_user = db_fetch_object($users_with_subscriptions)) {
    $operations[] = array(
      'boincimport_subscriptions_op', array(
        $subscribed_user->userid
      )
    );
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_subscriptions_finished',
    'title' => t('Importing subscriptions'),
    'init_message' => t('Beginning subscription import...'),
    'progress_message' => t('Processed @current out of @total subscriptions.'),
    'error_message' => t('Subscription import has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for importing subscriptions
 * Import subscriptions for the given user
 */
function boincimport_subscriptions_op($boincuser_id, &$context) {

  // Get the drupal user and pull subscriptions
  $uid = get_drupal_id($boincuser_id);
  $count = boincuser_pull_subscriptions($uid);

  $message = '';
  if ($count) {
    // Store some result for post-processing in the finished callback.
    $context['results']['success'][] = $boincuser_id;
    $context['results']['subscriptions'][$boincuser_id] = $count;
    $message = "Successfully imported {$count} subscriptions for user {$boincuser_id}";
  }
  else {
    $context['results']['failure'][] = $boincuser_id;
    $message = "Failed to import subscriptions for user {$boincuser_id}!";
    watchdog('boincimport',
      'Failed to import subscriptions for user @id!',
      array('@id' => $boincuser_id), WATCHDOG_WARNING
    );
  }

  // Update our progress information.
  $context['sandbox']['progress']++;
  $context['sandbox']['current_user'] = $boincuser_id;
  $context['message'] = $message;

  // Update the progress for the batch engine
  if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
    $context['finished'] = 1;
  }
  else {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_subscriptions_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $user_count = count($results['success']);
    $subscriptions_imported = array_sum($results['subscriptions']);
    $message = t(
      'Successfully imported @count subscriptions for @distinct users',
      array('@count' => $subscriptions_imported, '@distinct' => $user_count)
    );
    watchdog('boincimport',
      'Successfully imported @count subscriptions for @distinct users.',
      array('@count' => $subscriptions_imported, '@distinct' => $user_count),
      WATCHDOG_INFO
    );
    // Set the subscription import successful flag in the variable table
    variable_set('boincimport_import_subscription_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'url';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * URLs
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */
/**
 * Update relative BOINC URLs to work with Drupal
 */
function boincimport_replace_urls() {

  // Check whether URLs have already been fixed
  if (variable_get('boincimport_replace_url_successful', 0)) {
    drupal_set_message(t('URLs have already been updated'), 'warning');
    watchdog(
      'boincimport', 'URLs have already been updated',
      array(), WATCHDOG_WARNING
    );
  }

  if (!variable_get('boincimport_replace_url_started', 0)) {
    // Could prepare database tables, if new fields are necessary, etc.
    variable_set('boincimport_replace_url_started', 1);
  }

  // Get the count of nodes and comments to process for URL updates
  $node_count = db_result(db_query('
    SELECT COUNT(DISTINCT btt.nid)
    FROM {boincimport_temp_topic} AS btt
    LEFT JOIN {node_revisions} AS nr ON btt.nid = nr.nid'
  ));

  $comment_count = db_result(db_query('
    SELECT COUNT(c.cid)
    FROM {boincimport_temp_post} AS p
    LEFT JOIN {comments} AS c ON p.cid = c.cid'
  ));

  $pm_count = db_result(db_query('
    SELECT COUNT(*)
    FROM {pm_message} pm'
  ));

  if (!$node_count AND !$comment_count AND !$pm_count) {
    drupal_set_message(
      t('There were no nodes, comments, or private messages found: Aborting script'), 'warning'
    );
    watchdog('boincimport',
      'There were no nodes,comments, or private messages found: Aborting script', array(), WATCHDOG_WARNING
    );
    // Release the lock on the import process
    variable_del('boincimport_process_locked');
    return t('There were no nodes, comments, or private messages found: Aborting script.');
  }

  watchdog('boincimport',
    'Found %node_count nodes, %comment_count comments, and %pm_count private messages: Updating URLs...',
    array(
      '%node_count' => $node_count,
      '%comment_count' => $comment_count,
      '%pm_count' => $pm_count,
    ),
    WATCHDOG_INFO
  );

  $operations = array();
  $batch_size = 100;

  // Create node batches to process
  for ($offset = 0; $offset < $node_count; $offset+=$batch_size) {
    $nodes_per_batch = $batch_size;
    if ($offset + $batch_size > $node_count) {
      $nodes_per_batch = $node_count - $offset;
    }
    $operations[] = array(
      'boincimport_replace_urls_node_op', array(
        $offset, $nodes_per_batch
      )
    );
  }
  // Add comment batches
  for ($offset = 0; $offset < $comment_count; $offset+=$batch_size) {
    $comments_per_batch = $batch_size;
    if ($offset + $batch_size > $comment_count) {
      $comments_per_batch = $comment_count - $offset;
    }
    $operations[] = array(
      'boincimport_replace_urls_comment_op', array(
        $offset, $comments_per_batch
      )
    );
  }
  // And don't forget to process private messages
  for ($offset = 0; $offset < $pm_count; $offset+=$batch_size) {
    $messages_per_batch = $batch_size;
    if ($offset + $batch_size > $pm_count) {
      $messages_per_batch = $pm_count - $offset;
    }
    $operations[] = array(
      'boincimport_replace_urls_pm_op', array(
        $offset, $messages_per_batch
      )
    );
  }

  $batch = array(
    'operations' => $operations,
    'finished' => 'boincimport_replace_urls_finished',
    'title' => t('Updating URLs...'),
    'init_message' => t('Beginning URL update...'),
    'progress_message' => t('Processed URLs in @current out of @total batches (@size items per batch).', array(
      '@size' => $batch_size,
    )),
    'error_message' => t('URL update has encountered an error.'),
  );

  batch_set($batch);
}

/**
 * Batch operation for updating URLs in nodes
 * Find URLs for the old system and update them with Drupal paths
 */
function boincimport_replace_urls_node_op($offset, $batch_size, &$context) {
  // Initialize the batch, if needed
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = $batch_size;
  }

  $input_format = variable_get('boincimport_input_format', 0);

  // Since topics have just been imported, there should be only one vid for
  // each nid, so we can update node_revisions by nid
  // Get nodes to process
  $nodes = db_query('
    SELECT btt.nid, nr.body, nr.teaser
    FROM {boincimport_temp_topic} AS btt
    LEFT JOIN {node_revisions} AS nr ON btt.nid = nr.nid
    ORDER BY btt.nid
    LIMIT %d,%d',
    $offset, $batch_size
  );

  while ($node = db_fetch_object($nodes)) {
    $updated = FALSE;

    // Update URLs in node contents
    $original_body = $node->body;
    $original_teaser = $node->teaser;
    $node->body = _boincimport_replace_links($node->body);
    $node->teaser = _boincimport_replace_links($node->teaser);
    if ($node->body != $original_body OR $node->teaser != $original_teaser) {
      $updated = db_query("
        UPDATE {node_revisions}
        SET body= '%s', teaser = '%s'
        WHERE nid = %d",
        $node->body, $node->teaser, $node->nid
      );
    }

    $message = '';
    $context['results']['success'][] = $node->nid;
    if ($updated) {
      // Store some result for post-processing in the finished callback.
      $context['results']['nodes']['updated'][] = $node->nid;
      $message = "Successfully updated node {$node->nid}";
    }
    else {
      $message = "No changes made to node {$node->nid}!";
    }

    // Update our progress information.
    $context['sandbox']['progress']++;
    $context['sandbox']['current_node'] = $node->nid;
    $context['message'] = $message;

    // Update the progress for the batch engine
    if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
      $context['finished'] = 1;
    }
    else {
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
    }
  }
}

/**
 * Batch operation for updating URLs in comments
 * Find URLs for the old system and update them with Drupal paths
 */
function boincimport_replace_urls_comment_op($offset, $batch_size, &$context) {
  // Initialize the batch, if needed
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = $batch_size;
  }

  $input_format = variable_get('boincimport_input_format', 0);

  // Get comments to process
  $comments = db_query('
    SELECT c.cid, c.comment
    FROM {boincimport_temp_post} AS p
    LEFT JOIN {comments} AS c ON p.cid = c.cid
    ORDER BY c.cid
    LIMIT %d,%d',
    $offset, $batch_size
  );

  while ($comment = db_fetch_object($comments)) {
    $updated = FALSE;

    // Update URLs in comment contents
    $original_comment = $comment->comment;
    $comment->comment = _boincimport_replace_links($comment->comment);
    if ($comment->comment != $original_comment) {
      $updated = db_query("
        UPDATE {comments}
        SET comment= '%s'
        WHERE cid = %d",
        $comment->comment, $comment->cid
      );
    }

    $message = '';
    $context['results']['success'][] = $comment->cid;
    if ($updated) {
      // Store some result for post-processing in the finished callback.
      $context['results']['comments']['updated'][] = $comment->cid;
      $message = "Successfully updated comment {$comment->cid}";
    }
    else {
      $message = "No changes made to comment {$comment->cid}!";
    }

    // Update our progress information.
    $context['sandbox']['progress']++;
    $context['sandbox']['current_comment'] = $comment->cid;
    $context['message'] = $message;

    // Update the progress for the batch engine
    if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
      $context['finished'] = 1;
    }
    else {
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
    }
  }
}

/**
 * Batch operation for updating URLs in private messages
 * Find URLs for the old system and update them with Drupal paths
 */
function boincimport_replace_urls_pm_op($offset, $batch_size, &$context) {
  // Initialize the batch, if needed
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = $batch_size;
  }

  $input_format = variable_get('boincimport_input_format', 0);

  // Get private messages to process
  $messages = db_query('
    SELECT pm.mid, pm.body
    FROM {pm_message} pm
    ORDER BY pm.mid
    LIMIT %d,%d',
    $offset, $batch_size
  );

  while ($pm = db_fetch_object($messages)) {
    $updated = FALSE;

    // Update URLs in private message body
    $original_pm_body = $pm->body;
    $pm->body = _boincimport_replace_links($pm->body);
    if ($pm->body != $original_pm_body) {
      $updated = db_query("
        UPDATE {pm_message}
        SET body= '%s'
        WHERE mid = %d",
        $pm->body, $pm->mid
      );
    }

    $message = '';
    $context['results']['success'][] = $pm->mid;
    if ($updated) {
      // Store some result for post-processing in the finished callback.
      $context['results']['pm']['updated'][] = $pm->mid;
      $message = "Successfully updated private message {$pm->mid}";
    }
    else {
      $message = "No changes made to private message {$pm->mid}!";
    }

    // Update our progress information.
    $context['sandbox']['progress']++;
    $context['sandbox']['current_pm'] = $pm->mid;
    $context['message'] = $message;

    // Update the progress for the batch engine
    if ($context['sandbox']['progress'] >= $context['sandbox']['max']) {
      $context['finished'] = 1;
    }
    else {
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
    }
  }
}

/**
 * Batch 'finished' callback
 */
function boincimport_replace_urls_finished($success, $results, $operations) {
  if ($success) {
    // Let's count our successes
    $total_processed = count($results['success']);
    $nodes_updated = count($results['nodes']['updated']);
    $comments_updated = count($results['comments']['updated']);
    $private_messages_updated = count($results['pm']['updated']);
    $message = t(
      'Successfully processed @count nodes, comments, and private messages (@nodes_updated nodes, @comments_updated comments, and @pm_updated private messages were updated)',
      array(
        '@count' => $total_processed,
        '@nodes_updated' => $nodes_updated,
        '@comments_updated' => $comments_updated,
        '@pm_updated' => $private_messages_updated,
      )
    );
    if ($private_messages_updated) {
      watchdog('boincimport',
        'Updated URLs in these private_messages: @mid_list',
        array(
          '@mid_list' => implode(', ', $results['pm']['updated']),
        ),
        WATCHDOG_INFO
      );
    }
    if ($comments_updated) {
      watchdog('boincimport',
        'Updated URLs in these comments: @cid_list',
        array(
          '@cid_list' => implode(', ', $results['comments']['updated']),
        ),
        WATCHDOG_INFO
      );
    }
    if ($nodes_updated) {
      watchdog('boincimport',
        'Updated URLs in these nodes: @nid_list',
        array(
          '@nid_list' => implode(', ', $results['nodes']['updated']),
        ),
        WATCHDOG_INFO
      );
    }
    watchdog('boincimport',
      'Successfully processed @count nodes, comments, and private messages (@nodes_updated nodes, @comments_updated comments, and @pm_updated private messages were updated)',
      array(
        '@count' => $total_processed,
        '@nodes_updated' => $nodes_updated,
        '@comments_updated' => $comments_updated,
        '@pm_updated' => $private_messages_updated,
      ),
      WATCHDOG_INFO
    );
    // Set the replace URLs successful flag in the variable table
    variable_set('boincimport_replace_urls_successful', '1');
    $_SESSION['boincimport_stage_selected'] = 'users';
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = 'An error occurred while processing ' . $error_operation[0] . ' with arguments :' . print_r($error_operation[0], TRUE);
  }
  drupal_set_message($message);

  // Release the lock on the import process
  variable_del('boincimport_process_locked');
  drupal_goto('admin/boinc/import/process');
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Clean up
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Remove temporary variables, clear caches, etc.
 */
function boincimport_process_cleanup() {
  db_set_active('default');

  variable_del('boincimport_base_url_boinc');
  variable_del('boincimport_base_url_drupal');
  variable_del('boincimport_import_user_successful');
  variable_del('boincimport_import_user_started');
  variable_del('boincimport_import_team_successful');
  variable_del('boincimport_import_team_started');
  variable_del('boincimport_import_category_successful');
  variable_del('boincimport_replace_url_successful');
  variable_del('boincimport_import_category_started');
  variable_del('boincimport_import_topic_successful');
  variable_del('boincimport_import_topic_started');
  variable_del('boincimport_import_post_successful');
  variable_del('boincimport_import_post_started');
  variable_del('boincimport_team_forum_successful');
  variable_del('boincimport_team_topic_successful');
  variable_del('boincimport_team_post_successful');
  variable_del('boincimport_team_post_started');
  variable_del('boincimport_ready');
  variable_del('boincimport_db_url');
  variable_del('boincimport_tested');
  variable_del('boincimport_db_configured');
  variable_del('boincimport_table_prefix');
  variable_del('boincimport_team_types');
  variable_del('boincimport_time_limit');
  variable_del('boincimport_import_lurkers');
  variable_del('boincimport_import_polls');
  variable_del('boincimport_import_poll_started');
  variable_del('boincimport_import_poll_successful');
  variable_del('boincimport_import_pm_successful');
  variable_del('boincimport_encode');
  variable_del('boincimport_encoding_phpbb');
  variable_del('boincimport_encoding_drupal');
  variable_del('boincimport_version');

  db_query('DELETE FROM {cache}');
}

/**
 * Helper Functions
 */

function boincimport_forum_comment_save(&$edit) {
  // Here we are building the thread field.  See the comment in comment_render().
  if ($edit['pid'] == 0) {
    // This is a comment with no parent comment (depth 0): we start by retrieving
    // the maximum thread level.
    $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $edit['nid']));
    // Strip the "/" from the end of the thread.
    $max = rtrim($max, '/');
    $thread = int2vancode(vancode2int($max)+1) .'/';
  } else {
    // This is comment with a parent comment: we increase the part of the thread
    // value at the proper depth.
    $parent = db_fetch_object(db_query('SELECT * FROM {comments} WHERE cid = %d', $edit['pid']));
    // Strip the "/" from the end of the parent thread.
    $parent->thread = (string) rtrim((string) $parent->thread, '/');
    // Get the max value in _this_ thread.
    $max = db_result(db_query("SELECT MAX(thread) FROM {comments} WHERE thread LIKE '%s.%%' AND nid = %d", $parent->thread, $edit['nid']));
    if ($max == '') {
      // First child of this parent.
      $thread = $parent->thread .'.'. int2vancode(1) .'/';
    } else {
      // Strip the "/" at the end of the thread.
      $max = rtrim($max, '/');
      // We need to get the value at the correct depth.
      $parts = explode('.', $max);
      $parent_depth = count(explode('.', $parent->thread));
      $last = $parts[$parent_depth];
      // Finally, build the thread field for this new comment.
      $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
    }
  }

  $status = 0; // 1 - not published, 0 - published
  $format = variable_get('boincimport_input_format', 0);
  $score = 0; // 0 default value, comments get higher score depending on the author's roles
  $users = serialize(array(0 => 1));  // default value for everybody!!

  if ($edit['uid'] === $user->uid) { // '===' because we want to modify anonymous users too
    $edit['name'] = $user->name;
  }

  $success = db_query("INSERT INTO {comments} (nid, pid, uid, subject, comment, format, hostname, timestamp, status, thread, name) VALUES (%d, %d, %d, '%s', '%s', %d, '%s', %d, %d, '%s', '%s')", $edit['nid'], $edit['pid'], $edit['uid'], $edit['subject'], $edit['comment'], $edit['format'], ip_address(), $edit['timestamp'], $edit['status'], $thread, $edit['name']);
  if ($success) {
    $edit['cid'] = db_last_insert_id('comments', 'cid');
    _comment_update_node_statistics($edit['nid']);
  }
  return $success;
}

/**
 * Strips text of extra phpbb3 markup and if requested, also strips all bbcode from text.
 */
function _boincimport_strip_bbcode($text) {
  // Strip the text of extra markup - regular expressions taken from phpbb3 includes/function.php, function get_preg_expression().
  $match = array(
    '#<!\-\- e \-\-><a href="mailto:(.*?)">.*?</a><!\-\- e \-\->#',
    '#<!\-\- l \-\-><a (?:class="[\w-]+" )?href="(.*?)(?:(&amp;|\?)sid=[0-9a-f]{32})?">.*?</a><!\-\- l \-\->#',
    '#<!\-\- ([mw]) \-\-><a (?:class="[\w-]+" )?href="(.*?)">.*?</a><!\-\- \1 \-\->#',
    '#<!\-\- s(.*?) \-\-><img src="\{SMILIES_PATH\}\/.*? \/><!\-\- s\1 \-\->#',
    '#<!\-\- .*? \-\->#s',
    '#<.*?>#s',
  );
  $replace = array('$1', '$1', '$2', '$1', '', '');
  $text = preg_replace($match, $replace, $text);

  // If BBcode conversion to has been selected, the following will convert the
  // BBcode to normal html
  if (variable_get('boincimport_bbcode', 0)) {
    $input_format = variable_get('boincimport_input_format', 0);
    $text = bbcode_filter('process', 0 , $input_format, $text);
  }
  return $text;
}

/**
 * Function to properly encode strings.
 */
function _boincimport_text_sanitize($text) {
  $input_format = variable_get('boincimport_input_format', 0);
  $text = html_entity_decode($text, ENT_QUOTES, 'utf-8');
  // Be sure the text is filtered for the default input format
  $text = check_markup($text, $input_format);
  return $text;
}


/**
 * Replace all types of links.
 */
function _boincimport_replace_links($html) {

  $transformer = new BoincImportUrlTransformer();

  // Update links to posts, threads, and forums
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)forum_thread\.php\?id=(\d+)&postid=(\d+)(&\w+=\w*)*?}i', array($transformer, 'transformPostLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)forum_thread\.php\?id=(\d+)(&\w+=\w*)*?(#(\d+)?)}i', array($transformer, 'transformOldPostLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)forum_thread\.php\?id=(\d+)(&\w+=\w*)*?}i', array($transformer, 'transformTopicLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)forum_forum\.php\?id=(\d+)(&\w+=\w*)*?}i', array($transformer, 'transformForumLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)show_user\.php\?userid=(\d+)((&\w+=\w*)+)?}i', array($transformer, 'transformUserLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)workunit\.php\?wuid=(\d+)(&\w+=\w*)*?}i', array($transformer, 'transformWorkUnitLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)result\.php\?resultid=(\d+)(&\w+=\w*)*?}i', array($transformer, 'transformResultLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)results\.php\?userid=(\d+)(&\w+=\w*)*?}i', array($transformer, 'transformUserResultsLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)results\.php\?hostid=(\d+)(&\w+=\w*)*?}i', array($transformer, 'transformHostResultsLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)show_host_detail\.php\?hostid=(\d+)(&\w+=\w*)*?}i', array($transformer, 'transformHostLinks'), $html);
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)hosts_user\.php\?userid=(\d+)(&\w+=\w*)*?}i', array($transformer, 'transformUserHostsLinks'), $html);

  // Update any links to the top level index
  $html = preg_replace_callback('{(?:(http|https)://([^\s]*?)|href="(?:/)?)forum_index.php}i', array($transformer, 'transformForumIndexLinks'), $html);

  return $html;
}

/**
 * Limit redundancy by using this class to do URL transformations
 */
class BoincImportUrlTransformer {

  var $basePath;
  var $boincDomain;
  var $drupalDomain;

  /**
   * Constructor
   */
  function __construct() {
    global $base_url;
    global $base_path;
    $boinc_base_urls = variable_get('boincimport_base_url_boinc', '');
    $drupal_base_url = variable_get('boincimport_base_url_drupal', $base_url);
    $this->basePath = $base_path;
    $this->drupalDomain = parse_url($drupal_base_url, PHP_URL_HOST);
    $this->boincDomains = array();
    $boinc_base_urls = preg_split('/\s+/', $boinc_base_urls);
    foreach ($boinc_base_urls as $url) {
      $domain = parse_url($url, PHP_URL_HOST);
      if ($domain) {
        $this->boincDomains[$domain] = TRUE;
      }
    }
    if (!$this->boincDomains) {
      watchdog('boincimport', 'No valid BOINC base URLs found to transform!',
        array(), WATCHDOG_WARNING);
    }
  }

  // old-style constructor for backwards compatibility
  function BoincImportUrlTransformer() {
    self::__construct();
  }

  /**
   * Get what the new base URL should be (needed for every transformation)
   */
  function getNewBaseUrl($matches) {
    $http = $matches[1];
    $domain = trim($matches[2], '/');
    if ($http) {
      if (isset($this->boincDomains[$domain])) {
        // This is a URL configured to be transformed
        return "{$http}://{$this->drupalDomain}{$this->basePath}";
      }
      else {
        // This URL should not be transformed
        return NULL;
      }
    }
    else {
      // This is a relative URL
      return $this->basePath;
    }
  }

  /**
   * Replace links to specific posts. If the given post is the first in the
   * thread, it is a topic node in Drupal, not a comment.
   */
  function transformPostLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $id = db_result(db_query('
        SELECT p.cid
        FROM {boincimport_temp_post} p
        WHERE p.post_id = %d',
        $matches[4]
      ));
      if ($id) {
        $link = "{$newBaseUrl}goto/comment/{$id}";
      }
      else {
        // This post is not in the post import table, so it's probably a topic
        $link = $this->transformTopicLinks($matches);
      }
    }
    return $link;
  }

  /**
   * Replace links that include anchors to specific posts. If the given post is
   * the first in the thread, it is a topic node in Drupal, not a comment.
   */
  function transformOldPostLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $id = db_result(db_query('
        SELECT p.cid
        FROM {boincimport_temp_post} p
        WHERE p.post_id = %d',
        $matches[6]
      ));
      if ($id) {
        $link = "{$newBaseUrl}goto/comment/{$id}";
      }
      else {
        // This post is not in the post import table, so it's probably a topic
        $link = $this->transformTopicLinks($matches);
      }
    }
    return $link;
  }

  function transformTopicLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $id = db_result(db_query('
        SELECT nid
        FROM {boincimport_temp_topic}
        WHERE topic_id = %d',
        $matches[3]
      ));
      $link = "{$newBaseUrl}node/{$id}";
    }
    return $link;
  }

  function transformForumLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $forum = db_fetch_object(db_query('
        SELECT tid
        FROM {boincimport_temp_forum}
        WHERE forum_id = %d',
        $matches[3]
      ));
      $link = "{$newBaseUrl}community/forum/{$forum->tid}";
    }
    return $link;
  }

  function transformUserLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      // Make sure this isn't an RPC link (no need to transform those)
      if (!$matches[5]) {
        // TODO: This regex doesn't seem to capture the format=xml part of the
        // URL, making it impossible to distinguish if this is an RPC or not...
        //watchdog('DEBUG', 'matches: @m', array('@m' => print_r($matches,true)), WATCHDOG_DEBUG);
        $uid = boincuser_lookup_uid($matches[3]);
        $link = "{$newBaseUrl}account/{$uid}";
      }
    }
    return $link;
  }

  function transformWorkUnitLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $id = $matches[3];
      $link = "{$newBaseUrl}workunit/{$id}";
    }
    return $link;
  }

  function transformResultLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $id = $matches[3];
      $link = "{$newBaseUrl}task/{$id}";
    }
    return $link;
  }

  function transformHostResultsLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $id = $matches[3];
      $link = "{$newBaseUrl}host/{$id}/tasks";
    }
    return $link;
  }

  function transformUserResultsLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $link = "{$newBaseUrl}account/tasks";
    }
    return $link;
  }

  function transformHostLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $id = $matches[3];
      $link = "{$newBaseUrl}host/{$id}";
    }
    return $link;
  }

  function transformUserHostsLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $uid = boincuser_lookup_uid($matches[3]);
      if ($uid) {
        $link = "{$newBaseUrl}account/{$uid}/computers";
      }
    }
    return $link;
  }

  function transformForumIndexLinks($matches) {
    $link = $matches[0];
    $newBaseUrl = $this->getNewBaseUrl($matches);
    if ($newBaseUrl !== NULL) {
      $link = "{$newBaseUrl}community/forum";
    }
    return $link;
  }

}
