<?php
// $Id$

/**
 * @file
 * Provides common BOINC module functionality.
 *
 * In general, any custom feature or function required independently by
 * multiple BOINC modules should be in this module.
 */


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Includes that provide supporting functions
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

//require_once('includes/boinccore.forms.inc');
//require_once('includes/boinccore.helpers.inc');
require_once('includes/boinccore.rules.inc');
require_once('boinccore.admin.inc');


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Hooks into core modules
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Implementation of hook_menu(); determine the actions that correspond
 * with defined URL paths
 */
function boinccore_menu() {
  $items['node_control'] = array(
    'page callback' => 'boinccore_node_control',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK
  );
  $items['comment_control'] = array(
    'page callback' => 'boinccore_comment_control',
    'access arguments' => array('administer comments'),
    'type' => MENU_CALLBACK
  );
  $items['goto'] = array(
    'page callback' => 'boinccore_url_pagination_handler',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK
  );
  $items['admin/boinc/community'] = array(
    'title' => 'Community',
    'description' => 'Global settings for community interaction.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('boinccore_admin_community'),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'boinccore.admin.inc'
  );
  $items['admin/boinc/standard-content-generator'] = array(
    'title' => 'Standard page generator',
    'description' => 'Regenerate standard site content (e.g. Help and About Us
      pages).',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('boinccore_admin_default_content'),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'boinccore.admin.inc'
  );

  // BOINC RPC wrappers
  $items['lookup_account.php'] = array(
    'title' => 'Look up account RPC',
    'description' => 'RPC for getting the authenticator for a user account.',
    'page callback' => 'boinccore_lookup_account',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['am_get_info.php'] = array(
    'title' => 'Account manager get info RPC',
    'description' => 'RPC for getting assorted details of a user account.',
    'page callback' => 'boinccore_am_get_info',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['am_set_info.php'] = array(
    'title' => 'Account manager set info RPC',
    'description' => 'RPC for updating assorted details of a user account.',
    'page callback' => 'boinccore_am_set_info',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['am_set_host_info.php'] = array(
    'title' => 'Account manager set host info RPC',
    'description' => 'RPC for updating the venue for a given host.',
    'page callback' => 'boinccore_am_set_host_info',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['show_user.php'] = array(
    'title' => 'Show user RPC',
    'description' => 'RPC for getting assorted details of a user account.',
    'page callback' => 'boinccore_show_user',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['pending.php'] = array(
    'title' => 'Pending credit RPC',
    'description' => 'RPC for getting pending credit for a given account.',
    'page callback' => 'boinccore_pending_credit',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['create_team.php'] = array(
    'title' => 'Create team RPC',
    'description' => 'RPC for creating a new team.',
    'page callback' => 'boinccore_create_team',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['team_lookup.php'] = array(
    'title' => 'Team lookup RPC',
    'description' => 'RPC for getting info on a team or matching teams.',
    'page callback' => 'boinccore_team_lookup',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['team_email_list.php'] = array(
    'title' => 'Get team member list RPC',
    'description' => 'RPC for getting a list of members of a given team.',
    'page callback' => 'boinccore_team_email_list',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['edit_forum_preferences_action.php'] = array(
    'title' => 'Forum preferences RPC',
    'description' => 'RPC for setting forum preferences for a given user.',
    'page callback' => 'boinccore_edit_forum_preferences',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['forum_get_data.php'] = array(
    'title' => 'Forum get data RPC',
    'description' => 'RPC for getting recent forum activity for a given user.',
    'page callback' => 'boinccore_forum_get_data',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );
  $items['apps.php'] = array(
    'title' => 'Apps RPC',
    'description' => 'RPC for getting the applications in the system.',
    'page callback' => 'boinccore_apps',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );

  return $items;
}

/**
 * Implementation of hook_menu_alter()
 */
function boinccore_menu_alter(&$items) {
  // If using Solr search and not core Drupal search, disable core search!
  if (module_exists('global_search_solr') AND !module_exists('global_search')) {
    // Add permission to user search.
    $check = array(
      'search/node/%menu_tail',
      'search/user/%menu_tail',
      'user/autocomplete'
    );
    foreach ($check as $path) {
      if (isset($items[$path])) {
        $items[$path]['access callback'] = FALSE;
      }
    }
  }
  // If using content profile module (for user profiles) setup special
  // delete page.
  if (module_exists('content_profile')) {
    $items['node/%node/delete'] = array(
      'title' => 'Delete',
      'page callback' => 'drupal_get_form',
      'page arguments' => array('boinccore_delete_confirm', 1),
      'access callback' => 'node_access',
      'access arguments' => array('delete', 1),
      'module' => 'boinccore',
      'file' => 'includes/boinccore.forms.inc',
      'weight' => 1,
      'type' => MENU_CALLBACK,
    );
  }

  // 'Remove' menu paths for ignore_user and privatemsg
  // (pm_block_user) module which we have 'overridden' with
  // boincwork's own functions.
  if (module_exists('ignore_user')) {
    $check = array(
      'ignore_user/add',
      'ignore_user/remove',
    );
    foreach ($check as $path) {
      if (isset($items[$path])) {
        $items[$path]['access callback'] = FALSE;
      }
    }

    // Redirect user to privacy prefs page.
    $path1 = 'ignore_user/list';
    if (isset($items[$path1])) {
      $items[$path1]['page callback'] = 'drupal_goto';
      $items[$path1]['page arguments'] = array('account/prefs/privacy');
    }
  }

  if (module_exists('pm_block_user')) {
    $check = array(
      'messages/block/%user',
    );
    foreach ($check as $path) {
      if (isset($items[$path])) {
        $items[$path]['access callback'] = FALSE;
      }
    }
  }
}

/**
 * Implementation of hook_enable()
 */
function boinccore_enable() {
  // Automatically try to establish standard site content when this module is
  // enabled (e.g. Help and About Us pages)
  boinccore_admin_default_content_generate();
}

/**
 * Implementation of hook_boot()
 * Caching for anonymous users in Drupal 6 is not language specific, so using
 * the browser language without path prefix can result in mixed languages being
 * present in the cache for pages with the default (no language given) path.
 * As a workaround, redirect anonymous users to a language prefix if they are
 * on the default site path and the browser reports a non-default language.
 * See: https://www.drupal.org/node/339958#comment-3012662
 */
function boinccore_boot() {
  global $user, $language;
  $cache = variable_get('cache', CACHE_DISABLED);
  $language_negotiation = variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE);

  // If we have an anonymous user, page cache is enabled, the URL has no prefix
  // and browser language negotiation is set, we perform a redirect to the
  // prefixed URL.
  if (empty($user->uid) && $cache != CACHE_DISABLED && $language_negotiation == LANGUAGE_NEGOTIATION_PATH) {
    $args = isset($_GET['q']) ? explode('/', $_GET['q']) : array();
    $prefix = array_shift($args);

    // Initialize language as we need the proper language negotiation to be
    // performed.
    drupal_bootstrap(DRUPAL_BOOTSTRAP_LANGUAGE);

    // But Drupal 6 language negotiation does not always work that well...
    // so call this backported Drupal 7 language negotation function, which
    // does both case insensitive and longest prefix matching
    if (function_exists('locale_language_from_browser')) {
      $language = locale_language_from_browser();
    }

    if (!empty($language->prefix) && $prefix != $language->prefix) {
      // We need full path support to perform the redirect.
      drupal_bootstrap(DRUPAL_BOOTSTRAP_PATH);

      require_once './includes/common.inc';
      $url = url($_GET['q'], array('query' => drupal_query_string_encode($_GET, array('q'))));
      $code = variable_get('locale_redirect_http_code', 302);

      // Since we are in a hook_boot() implementation cache mode is normal, so
      // we can safely call hook_exit().
      bootstrap_invoke_all('exit');
      header("Location: $url", TRUE, $code);
      exit;
    }
  }
}


if (!function_exists('locale_language_from_browser')) {
/**
 * Backport of locale_language_from_browser from Drupal 7
 *
 * The language_from_browser() function in Drupal 6 does not match the browser
 * language preference case-insensitively or by longest prefix
 *
 * Obsolete in Drupal 7...
 */
  function locale_language_from_browser() {
    if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
      return FALSE;
    }

    $languages = language_list('enabled');
    $languages = $languages[1];

    // The Accept-Language header contains information about the language
    // preferences configured in the user's browser / operating system.
    // RFC 2616 (section 14.4) defines the Accept-Language header as follows:
    //   Accept-Language = "Accept-Language" ":"
    //                  1#( language-range [ ";" "q" "=" qvalue ] )
    //   language-range  = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
    // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
    $browser_langcodes = array();
    if (preg_match_all('@(?<=[, ]|^)([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) {
      foreach ($matches as $match) {
        // We can safely use strtolower() here, tags are ASCII.
        // RFC2616 mandates that the decimal part is no more than three digits,
        // so we multiply the qvalue by 1000 to avoid floating point comparisons.
        $langcode = strtolower($match[1]);
        $qvalue = isset($match[2]) ? (float) $match[2] : 1;
        $browser_langcodes[$langcode] = (int) ($qvalue * 1000);
      }
    }

    // We should take pristine values from the HTTP headers, but Internet Explorer
    // from version 7 sends only specific language tags (eg. fr-CA) without the
    // corresponding generic tag (fr) unless explicitly configured. In that case,
    // we assume that the lowest value of the specific tags is the value of the
    // generic language to be as close to the HTTP 1.1 spec as possible.
    // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 and
    // http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx
    asort($browser_langcodes);
    foreach ($browser_langcodes as $langcode => $qvalue) {
      $generic_tag = strtok($langcode, '-');
      if (!isset($browser_langcodes[$generic_tag])) {
        $browser_langcodes[$generic_tag] = $qvalue;
      }
    }

    // Find the enabled language with the greatest qvalue, following the rules
    // of RFC 2616 (section 14.4). If several languages have the same qvalue,
    // prefer the one with the greatest weight.
    $best_match_language = FALSE;
    $max_qvalue = 0;
    foreach ($languages as $langcode => $language) {
      // Language tags are case insensitive (RFC2616, sec 3.10).
      $langcode = strtolower($langcode);

      // If nothing matches below, the default qvalue is the one of the wildcard
      // language, if set, or is 0 (which will never match).
      $qvalue = isset($browser_langcodes['*']) ? $browser_langcodes['*'] : 0;

      // Find the longest possible prefix of the browser-supplied language
      // ('the language-range') that matches this site language ('the language tag').
      $prefix = $langcode;
      do {
        if (isset($browser_langcodes[$prefix])) {
          $qvalue = $browser_langcodes[$prefix];
          break;
        }
      }
       while ($prefix = substr($prefix, 0, strrpos($prefix, '-')));

      // Find the best match.
      if ($qvalue > $max_qvalue) {
        $best_match_language = $language;
        $max_qvalue = $qvalue;
      }
    }

    return $best_match_language;
  }
}

/**
 * Implementation of hook_cron()
 */
function boinccore_cron() {
  // Notify moderators if there is site content that requires moderator action
  $items_in_queue = boincuser_moderation_queue_count('cron');
  $last_notification = variable_get('boinccore_last_content_moderation_reminder', 0);
  if ($items_in_queue AND $last_notification < time() - 24*60*60) {
    global $base_url;
    global $base_path;
    $site_name = variable_get('site_name', 'Drupal-BOINC');
    $site_url = $base_url . $base_path;
    $settings = array(
      'from' => '',
      'subject' => "Content at {$site_name} is awaiting moderation",
      'message' => ''
      . "Please visit the moderation queue page to review content that "
      . "requires moderator action: \n"
      . "\n"
      . "{$site_url}moderate",
    );
    boinccore_rules_action_mail_to_moderators($settings);
    variable_set('boinccore_last_content_moderation_reminder', time());
  }

  // Delete expired tokens in BOINC DB
  _boinccore_delete_expired_tokens();
}

/**
 * Implementation of hook_rules_action_info().
 */
function boinccore_rules_action_info() {
  return array(
    'boinccore_rules_action_mail_to_moderators' => array(
      'label' => t('Notify moderators via email'),
      'module' => 'BOINC core',
      'eval input' => array('subject', 'message', 'from'),
    ),
    'boinccore_rules_action_mark_content_for_moderation' => array(
      'label' => t('Mark content for moderation'),
      'arguments' => array(
        'node' => array('type' => 'node', 'label' => t('Content')),
      ),
      'module' => 'BOINC core',
    ),
    'boinccore_rules_action_mail_to_admins' => array(
      'label' => t('Notify admins via email'),
      'module' => 'BOINC core',
      'eval input' => array('subject', 'message', 'from'),
    ),
  );
}

/**
 * Implementation of hook_form_alter()
 */
function boinccore_form_alter(&$form, $form_state, $form_id) {
  global $user;
  $account = user_load($user->uid);
  switch ($form_id) {
  // Comment form
  case 'comment_form':
    $user_image = boincuser_get_user_profile_image($user->uid);
    $user_info = '<div class="user">';
    if ($user_image) {
      $user_info .= '  <div class="picture">';
      if (is_array($user_image) AND $user_image['image']['filepath']) {
        $user_info .= theme('imagefield_image', $user_image['image'], $user_image['alt'], $user_image['alt'], array(), false);
      }
      elseif (is_string($user_image)) {
        $user_info .= '<img src="' . $user_image . '"/>';
      }
      $user_info .= '  </div>';
    }
    $user_info .= '  <div class="name">' . theme('username', $account) . '</div>';
    if ($account->uid) {
      //$user_info .= '  <div class="join-date">Joined: ' . date('j M y', $account->created) . '</div>';
      //$user_info .= '  <div class="post-count">Posts: ' . $account->post_count . '</div>';
      //$user_info .= '  <div class="credit">Credit: ' . $account->boincuser_total_credit . '</div>';
      //$user_info .= '  <div class="rac">RAC: ' . $account->boincuser_expavg_credit . '</div>';
    }
    $user_info .= '</div>';
    array_unshift($form, array(
      'user_info' => array(
        '#value' => $user_info,
      ),
      'comment_form_prefix' => array(
        '#value' => '<div class="form-body">',
      ),
    ));
    $form['comment_form_suffix'] = array(
      '#value' => '</div>',
    );

    module_load_include('inc', 'bbcode', 'bbcode-help');
    $form['bbcode_help'] = array(
      '#title' => bts('BBcode help', array(), NULL, 'boinc:help-with-user-input-with-BBCode'),
      '#type' => 'fieldset',
      '#attributes' => array(
        'class' => 'bbcode_help',
      ),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      'content' => array(
        '#value' => _bbcode_filter_tip(),
      ),
    );

    // If a specific comment is being referenced, go back to it on cancel
    if ($comment_ref = $form['pid']['#value']) {
      $cancel_url = "goto/comment/{$comment_ref}";
    }
    else {
      $cancel_url = "node/{$form['nid']['#value']}";
    }

    // Wrap action buttons for styling consistency
    $form['buttons']['form control tabs prefix'] = array(
      '#value' => '<ul class="form-control tab-list">',
      '#weight' => 1001,
    );
    $form['buttons']['submit'] = $form['submit'];
    unset($form['submit']);
    $form['buttons']['submit']['#prefix'] = '<li class="first tab">';
    $form['buttons']['submit']['#value'] = bts('Post comment', array(), NULL, 'boinc:post-forum-comment');
    $form['buttons']['submit']['#suffix'] = '</li>';
    $form['buttons']['submit']['#weight'] = 1002;
    // We want all these buttons together under the "buttons" array, but the
    // default submit and preview buttons cannot both be unset or odd display
    // bugs result. Somehow the comment form ends up above the preview div in
    // this case. So... don't unset the preview button, just hide it
    $form['buttons']['preview'] = $form['preview'];
    //unset($form['preview']);
    $form['preview']['#attributes']['style'] = 'display: none;';
    $form['buttons']['preview']['#prefix'] = '<li class="tab">';
    $form['buttons']['preview']['#suffix'] = '</li>';
    $form['buttons']['preview']['#weight'] = 1003;
    $form['buttons']['cancel'] = array(
      '#value' => '<li class="tab">' . l(bts('Cancel', array(), NULL, 'boinc:form-cancel'), $cancel_url) . '</li>',
      '#weight' => 1004,
    );
    $form['buttons']['form control tabs suffix'] = array(
      '#value' => '</ul>',
      '#weight' => 1010,
    );

    // Remove redundant name field
    unset($form['_author']);

    // If user is role='community member' only, then user may not post
    // any comments in non-forum node-types, team_forums are also not
    // allowed.. This allows users who have site-wide comment
    // permission to comment on forums in special
    // circumstances. Additionally, any comments require a captcha.
    //
    $node = node_load($form['nid']['#value']);
    $community_role = array_search('community member', user_roles(true));
    $unrestricted_role = array_search('verified contributor', user_roles(true));
    if ( (isset($account->roles[$community_role])) and (!isset($account->roles[$unrestricted_role])) ) {
      if ($node->type == 'forum') {
        if (module_exists('captcha')) {
          $form['comment_captcha'] = array(
            '#type' => 'captcha',
            '#weight' => 1000,
          );
        }
      }
      else {
        $form = NULL;
        $form['from'] = array(
          '#type'  => 'item',
          '#value' => 'You do not have permission to post comments in this forum.',
        );
      }
    }

    break;

  // Forum node form
  case 'forum_node_form':
  case 'team_forum_node_form':
    $forum_id = key($form['#node']->taxonomy);
    $form['buttons']['separator_bottom'] = array(
      '#value' => '<div class="separator buttons"></div>',
      '#weight' => 999,
    );

    // Wrap action buttons for styling consistency
    $form['buttons']['form control tabs prefix'] = array(
      '#value' => '<ul class="form-control tab-list">',
      '#weight' => 1001,
    );
    $form['buttons']['submit']['#prefix'] = '<li class="first tab">';
    $form['buttons']['submit']['#value'] = bts('Post topic', array(), NULL, 'boinc:post-forum-comment');
    $form['buttons']['submit']['#suffix'] = '</li>';
    $form['buttons']['submit']['#weight'] = 1002;
    $form['buttons']['preview']['#prefix'] = '<li class="tab">';
    $form['buttons']['preview']['#suffix'] = '</li>';
    $form['buttons']['preview']['#weight'] = 1003;
    $form['buttons']['preview_changes']['#prefix'] = '<li class="tab">';
    $form['buttons']['preview_changes']['#suffix'] = '</li>';
    $form['buttons']['preview_changes']['#weight'] = 1004;
    $form['buttons']['cancel'] = array(
      '#value' => '<li class="tab">' . l(bts('Cancel', array(), NULL, 'boinc:form-cancel'), "community/forum/{$forum_id}") . '</li>',
      '#weight' => 1005,
    );
    $form['buttons']['form control tabs suffix'] = array(
      '#value' => '</ul>',
      '#weight' => 1010,
    );

    // Add captcha for role='community member'
    $community_role = array_search('community member', user_roles(true));
    $unrestricted_role = array_search('verified contributor', user_roles(true));
    if ( (isset($account->roles[$community_role])) and (!isset($account->roles[$unrestricted_role])) ) {
      if (module_exists('captcha')) {
        $form['comment_captcha'] = array(
          '#type' => 'captcha',
          '#weight' => 1000,
        );
      }
    }

    break;
  case 'boinccore_delete_confirm':
    // Wrap action buttons for styling consistency
    $form['actions']['#prefix'] = '<ul class="form-control tab-list">';
    $form['actions']['#suffix'] = '</ul>';

    $form['actions']['submit']['#prefix'] = '<li class="first tab">';
    $form['actions']['submit']['#suffix'] = '</li>';
    $form['actions']['cancel']['#prefix'] = '<li class="tab">';
    $form['actions']['cancel']['#suffix'] = '</li>';
    break;
  default:
  }
}

/**
 * Implementation of hook_link_alter()
 */
function boinccore_link_alter(&$links, $node, $comment = NULL) {
  //echo '<pre>' . print_r($links, true) . '</pre>';
  foreach ($links as $module => $link) {
    // Remove the ignore user link
    if (strstr($module, 'ignore_user')) {
      unset($links[$module]);
    }
  }
  // Node first, then comment: to be consistent with boinccore_link() function
  if (!($comment)) {
    // modify the comment_add link
    if (isset($links['comment_add'])) {
      $links['comment_add']['title'] = bts('reply', array(), NULL, 'boinc:forum-reply-to-comment');
      $links['comment_add']['attributes'] = array(
          'title' => bts('Reply to this comment', array(), NULL, 'boinc:forum-reply-to-comment')
      );
    }
    // modify quote link
    if (isset($links['quote'])) {
      $links['quote']['attributes'] = array(
          'title' => bts('Reply to this topic with a quote', array(), NULL, 'boinc:forum-reply-to-comment')
      );
    }
  }
  else {
    // Standard EDIT, DELETE, and REPLY links are created by Drupal, but we want to alter them
    if (isset($links['comment_delete'])) {
      $links['comment_delete']['attributes'] = array(
          'title' => bts('Delete this comment', array(), NULL, 'boinc:forum-delete-comment')
      );
    }
    if (isset($links['comment_edit'])) {
      $links['comment_edit']['attributes'] = array(
          'title' => bts('Edit this comment', array(), NULL, 'boinc:forum-edit-comment')
      );
    }
    if (isset($links['comment_reply'])) {
      $links['comment_reply']['attributes'] = array(
          'title' => bts('Reply to this comment', array(), NULL, 'boinc:forum-reply-to-comment')
      );
    }
  }// if !$comment
}

/**
 * Implementation of hook_locale().
 */
function boinccore_locale($op = 'groups', $group = NULL) {
  switch ($op) {
    case 'groups':
        return array('boinc' => 'BOINC');
    case 'info':
      //$info['boinc']['refresh callback'] = 'boinccore_locale_refresh';
      $info['boinc']['format'] = FALSE;
      return $info;
  }
}


/**
 * Implementation of hook_link()
 */
function boinccore_link($type, $object, $teaser = FALSE) {
  // Add custom links with this hook

  if ($type=='node') {
    if ( ($object->type=='forum') OR ($object->type=='team_forum') ) {
      // Add topic moderator controls
      if (user_access('edit any forum topic')) {
        $node_control = "node_control/{$object->nid}";
        if ($object->sticky) {
          $links['make_unsticky'] = array(
              'title' => bts('Make unsticky', array(), NULL, 'boinc:forum-unsticky-comment'),
              'href' => "{$node_control}/unsticky",
              'attributes' => array(
                  'title' => bts('Remove sticky status from this topic', array(), NULL, 'boinc:forum-unsticky-comment')
              )
          );
        }
        else {
          $links['make_sticky'] = array(
              'title' => bts('Make sticky', array(), NULL, 'boinc:forum-sticky-comment'),
              'href' => "{$node_control}/sticky",
              'attributes' => array(
                  'title' => bts('Make this topic sticky', array(), NULL, 'boinc:forum-sticky-comment')
              )
          );
        }
        if ($object->comment == COMMENT_NODE_READ_WRITE) {
          $links['lock'] = array(
              'title' => bts('Lock', array(), NULL, 'boinc:forum-lock-comment'),
              'href' => "{$node_control}/lock",
              'attributes' => array(
                  'title' => bts('Lock this thread for comments', array(), NULL, 'boinc:forum-lock-comment')
              )
          );
        }
        else {
          $links['unlock'] = array(
              'title' => bts('Unlock', array(), NULL, 'boinc:forum-unlock-comment'),
              'href' => "{$node_control}/unlock",
              'attributes' => array(
                  'title' => bts('Unlock this thread for comments', array(), NULL, 'boinc:forum-unlock-comment')
              )
          );
        }
        if ($object->status) {
          $links['hide'] = array(
              'title' => bts('Hide', array(), NULL, 'boinc:forum-hide-comment'),
              'href' => "{$node_control}/hide",
              'attributes' => array(
                  'title' => bts('Hide this topic', array(), NULL, 'boinc:forum-hide-comment')
              )
          );
        }
        else {
          $links['unhide'] = array(
              'title' => bts('Unhide', array(), NULL, 'boinc:forum-unhide-comment'),
              'href' => "{$node_control}/unhide",
              'attributes' => array(
                  'title' => bts('Unhide this topic', array(), NULL, 'boinc:forum-unhide-comment')
              )
          );
        }
      }// if user_access('edit any forum topic')
    }
  }
  else if ($type=='comment') {
    $node = node_load($object->nid);
    $nid = $object->nid;
    $cid = $object->cid;

    // QUOTE
    if ($node->comment == COMMENT_NODE_READ_WRITE) {
        $links['quote'] = array(
            'title' => bts('Quote', array(), NULL, 'boinc:forum-reply-to-comment-with-quote'),
            'href' => "comment/reply/{$nid}/{$cid}",
            'attributes' => array(
                'title' => bts('Reply to this comment with a quote', array(), NULL, 'boinc:forum-reply-to-comment')
            ),
            'fragment' => 'comment-form',
            'query' => 'quote=1',
        );
    }
    // HIDE and COVERT comment
    // The following are moderator only links
    if (user_access('administer comments')) {
      $comment_control = "comment_control/{$cid}";
      // Add link to convert comment into a new topic
      $reply_count = db_result(db_query(' SELECT COUNT(*) FROM comments WHERE pid = %d', $cid ));
      if ($reply_count == 0) {
        $links['convert'] = array(
            'title' => bts('Convert', array(), NULL, 'boinc:forum-convert-comment-to-topic'),
            'href' => "{$comment_control}/convert",
            'attributes' => array(
                'title' => bts('Convert this comment to a new topic', array(), NULL, 'boinc:forum-convert-comment-to-topic')
            )
        );
      }
      // Add hide link
      if ($object->status == 0) {
        $links['hide'] = array(
            'title' => bts('Hide', array(), NULL, 'boinc:forum-hide-comment'),
            'href' => "{$comment_control}/hide",
            'attributes' => array(
                'title' => bts('Hide this comment', array(), NULL, 'boinc:forum-hide-comment')
            )
        );
      }
      else {
        $links['unhide'] = array(
            'title' => bts('Unhide', array(), NULL, 'boinc:forum-unhide-comment'),
            'href' => "{$comment_control}/unhide",
            'attributes' => array(
                'title' => bts('Unhide this comment', array(), NULL, 'boinc:forum-unhide-comment')
            )
        );
      }
    }// if user_access('administer comments')
  }//if $type

  return $links;
}

/**
 * Implementation of hook_mail_alter()
 */
function boinccore_mail_alter(&$message) {
  /* This code alters the headers for all emails sent by the Drupal Web site to use
     'Quote-Printable' as the encoding, both altering the header and modifies the body to the
     new encoding.
  */
  $message['headers']['Content-Transfer-Encoding'] = 'Quoted-Printable';
  if (is_array($message['body'])) {
    $message['body'] = array_map( "quoted_printable_encode", $message['body'] );
  } else {
    $message['body'] = quoted_printable_encode($message['body']);
  }
}

/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Page callbacks from hook_menu()
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Controller for handling simple node status configuration.
 * Allow published, sticky, and comment status to be set on nodes by direct
 * link rather than through the node form.
 */
function boinccore_node_control($nid = NULL, $action = NULL) {
  if ((!$nid OR !$node = node_load($nid)) OR
      ($node->type == 'forum' AND !user_access('edit any forum topic')) OR
      ($node->type != 'forum' AND !user_access('administer nodes'))) {
    // What are you even doing here...
    return FALSE;
  }
  switch ($action) {
  case 'unhide':
    node_publish_action($node);
    break;
  case 'hide':
    node_unpublish_action($node);
    break;
  case 'sticky':
    node_make_sticky_action($node);
    break;
  case 'unsticky':
    node_make_unsticky_action($node);
    break;
  case 'lock':
    $node->comment = 1;
    watchdog('action', 'Locked comments on @type %title.', array(
      '@type' => node_get_types('name', $node), '%title' => $node->title));
    break;
  case 'unlock':
    $node->comment = 2;
    watchdog('action', 'Unlocked comments on @type %title.', array(
      '@type' => node_get_types('name', $node), '%title' => $node->title));
    break;
  default:
  }
  node_save($node);
  // If hiding a forum topic (node) and not an administrator, go to
  // the forum page. Otherwise the user will reach an access-denied
  // page.
  if (($action=="hide") and (!user_access('administer forums'))) {
    drupal_goto("community/forum/{$node->tid}");
  }
  drupal_goto("node/{$nid}");
}

/**
 * Controller for handling simple comment status configuration.
 * Allow published status to be set on comments by direct link rather than
 * through the comment edit form.
 */
function boinccore_comment_control($cid = NULL, $action = NULL) {
  if (!$cid OR !$comment = _comment_load($cid)) {
    // What are you even doing here...
    return FALSE;
  }
  $return_path = "node/{$comment->nid}";
  $return_anchor = "comment-{$cid}";
  switch ($action) {
  case 'unhide':
    comment_publish_action($comment);
    rules_invoke_event('boinccore_comment_unhidden', $comment);
    if (module_exists("boinc_solr_comments")) {
      boinc_solr_comments_publish($comment);
    }
    break;
  case 'hide':
    comment_unpublish_action($comment);
    rules_invoke_event('comment_unpublish', $comment);
    if (module_exists("boinc_solr_comments")) {
      boinc_solr_comments_unpublish($comment);
    }
    break;
  case 'convert':
    $node = new stdClass();
    $node->name = $comment->subject;
    $node->title = $node->name;
    $node->body = $comment->comment;
    $node->type = 'forum';
    $node->created = $comment->timestamp;
    $node->changed = $node->created;
    $node->promote = 0; // Display on front page?
    $node->sticky = 0;  // Display top of page?
    $node->status = 1;   // Published?
    $node->comment = 2;
    //$node->language = 'en';
    $node->uid = $comment->uid;

    // Preselect the parent's taxonomy ID
    $parent_node = node_load($comment->nid);
    $node->tid = $parent_node->tid;

    if (filter_access($comment->format)) {
      $node->format = $comment->format;
    }

    // Save the new node
    node_save($node);
    taxonomy_node_save($node, array($node->tid));

    // Delete the comment
    module_load_include('inc', 'comment', 'comment.admin');
    _comment_delete_thread($comment);

    rules_invoke_event('boinccore_comment_convert', $comment);

    $return_path = "node/{$node->nid}";
    $return_anchor = NULL;
    break;
  default:
  }
  drupal_goto($return_path, NULL, $return_anchor);
}

/**
 * Page callback for the look up account RPC (lookup_account.php).
 * Get the authenticator for a given account
 */
function boinccore_lookup_account() {
  include_boinc('user/lookup_account.php');
}

/**
 * Page callback for the account manager get info RPC (am_get_info.php).
 * Get assorted details for a given account
 */
function boinccore_am_get_info() {
  // Remove q from the GET request or BOINC will panic
  unset($_GET['q']);
  // Capture the XML output of the RPC so we can override things
  ob_start();
  include_boinc('user/am_get_info.php');
  $xml = ob_get_clean();
  $xml = load_configuration($xml);
  // See if the account has an approved profile in Drupal
  $uid = !empty($xml['am_get_info_reply']['id']) ? boincuser_lookup_uid($xml['am_get_info_reply']['id']) : 0;
  if ($uid) {
    $content_profile = content_profile_load('profile', $uid);
    $profile_is_approved = ($content_profile->status AND !$content_profile->moderate);
    $country = check_plain($content_profile->field_country[0]['value']);
    $website = '';
    if ($profile_is_approved) {
      $website = check_plain($content_profile->field_url[0]['value']);
    }
    // Override BOINC values with Drupal values as needed
    $xml['am_get_info_reply']['has_profile']['@value'] = ($profile_is_approved) ? 1 : 0;
    $xml['am_get_info_reply']['country']['@value'] = $country;
    $xml['am_get_info_reply']['url']['@value'] = $website;
  }
  print save_configuration($xml);
}

/**
 * Page callback for the account manager set info RPC (am_set_info.php).
 * Update assorted details for a given account
 */
function boinccore_am_set_info() {
  // Remove q from the GET request or BOINC will panic
  unset($_GET['q']);
  // Validate the XML prefs (in case BOINC does not)
  $global_prefs = !empty($_POST['global_prefs']) ? $_POST['global_prefs'] : (!empty($_GET['global_prefs']) ? $_GET['global_prefs'] : NULL);
  $project_prefs = !empty($_POST['project_prefs']) ? $_POST['project_prefs'] : (!empty($_GET['project_prefs']) ? $_GET['project_prefs'] : NULL);
  libxml_use_internal_errors(true);
  $xml = new DomDocument();
  if ($global_prefs) {
    $xml->loadXML($global_prefs, LIBXML_NOBLANKS);
    if ($errors = libxml_get_errors($xml)) {
      $lines = explode("\r", $global_prefs);
      watchdog('am_set_info', "XML validation error in global_prefs: {$errors[0]->message} at line {$errors[0]->line}" .
        ': <br/>' . htmlentities($lines[$errors[0]->line - 1]), array(), WATCHDOG_ERROR);
    }
  }
  elseif ($project_prefs) {
    $xml->loadXML($project_prefs, LIBXML_NOBLANKS);
    if ($errors = libxml_get_errors($xml)) {
      $lines = explode("\r", $project_prefs);
      watchdog('am_set_info', "XML validation error in project_prefs: {$errors[0]->message} at line {$errors[0]->line}" .
        ': <br/>' . htmlentities($lines[$errors[0]->line - 1]), array(), WATCHDOG_ERROR);
    }
  }
  if (!$errors) {
    // Capture the XML output of the RPC so we can override things
    ob_start();
    include_boinc('user/am_set_info.php');
    $xml = ob_get_clean();
    $xml = load_configuration($xml);
    if (isset($xml['am_set_info_reply']['success'])) {
      // Find the account in Drupal
      $boinc_auth = !empty($_POST['account_key']) ? $_POST['account_key'] : $_GET['account_key'];
      db_set_active('boinc_rw');
      $boinc_id = db_result(db_query("
        SELECT id
        FROM {user}
        WHERE authenticator = '%s'",
        $boinc_auth
      ));
      db_set_active('default');
      $uid = boincuser_lookup_uid($boinc_id);

      // Apply any relevant updates to the Drupal account, also
      $country = !empty($_POST['country']) ? $_POST['country'] : (!empty($_GET['country']) ? $_GET['country'] : NULL);
      $url = !empty($_POST['url']) ? $_POST['url'] : (!empty($_GET['url']) ? $_GET['url'] : NULL);
      $email_addr = !empty($_POST['email_addr']) ? $_POST['email_addr'] : (!empty($_GET['email_addr']) ? $_GET['email_addr'] : NULL);

      if ($email_addr) {
        $account = user_load($uid);
        user_save($account, array('mail' => $email_addr));
      }
      if ($country OR $url) {
        $content_profile = content_profile_load('profile', $uid);
        if ($country) $content_profile->field_country[0]['value'] = $country;
        if ($url) $content_profile->field_url[0]['value'] = $url;
        node_save($content_profile);
      }
    }
  }
  else {
    $xml = array(
      'error' => array(
        'error_num' => -112,
        'error_msg' => "{$errors[0]->message} at line {$errors[0]->line}" .
        ': <br/>' . htmlentities($lines[$errors[0]->line - 1])
      ),
    );
  }
  print save_configuration($xml);
}

/**
 * Page callback for the set host info RPC (am_set_host_info.php).
 * Update the venue for the given host
 */
function boinccore_am_set_host_info() {
  // Remove q from the GET request or BOINC will panic
  unset($_GET['q']);
  // Capture the XML output of the RPC so we can override things
  ob_start();
  include_boinc('user/am_set_host_info.php');
  $xml = ob_get_clean();
  $xml = load_configuration($xml);
  if (isset($xml['am_set_host_info_reply']['success'])) {
    // Override anything that needs overriding
  }
  print save_configuration($xml);
}

/**
 * Page callback for the show user RPC (show_user.php).
 * Get assorted details for a given account
 */
function boinccore_show_user() {
  // Remove q from the GET request or BOINC will panic
  unset($_GET['q']);
  // Capture the XML output of the RPC so we can override things
  ob_start();
  include_boinc('user/show_user.php');
  $xml = ob_get_clean();
  $xml = load_configuration($xml);
  // See if the account has an approved profile in Drupal
  $uid = !empty($xml['user']['id']) ? boincuser_lookup_uid($xml['user']['id']) : 0;
  if ($uid) {
    $content_profile = content_profile_load('profile', $uid);
    $profile_is_approved = ($content_profile->status AND !$content_profile->moderate);
    $country = check_plain($content_profile->field_country[0]['value']);
    $website = '';
    if ($profile_is_approved) {
      $website = check_plain($content_profile->field_url[0]['value']);
    }
    // Override BOINC values with Drupal values as needed
    $xml['user']['has_profile']['@value'] = ($profile_is_approved) ? 1 : 0;
    $xml['user']['country']['@value'] = $country;
    $xml['user']['url']['@value'] = $website;
  }
  print save_configuration($xml);
}

/**
 * Page callback for the pending credit RPC (pending.php).
 * Get pending credit information for a given account
 */
function boinccore_pending_credit() {
  // Remove q from the GET request or BOINC will panic
  unset($_GET['q']);
  include_boinc('user/pending.php');
}

/**
 * Page callback for the create team RPC (create_team.php).
 * Create a team with the provided details
 */
function boinccore_create_team() {

  if (module_exists('boincteam')) {
    module_load_include('module', 'boincteam', 'boincteam');

    $input_format = !empty($values['format']) ? $values['format'] : 4;
    if (!empty($_GET['description'])) $_GET['description'] = check_markup($_GET['description'], $input_format);

    // Try to add the team to BOINC and capture the XML output of the RPC
    ob_start();
    include_boinc('user/create_team.php');
    $xml = ob_get_clean();
    $xml = load_configuration($xml);

    if (isset($xml['create_team_reply']['success'])) {
      $boincteam_id = isset($xml['create_team_reply']['team_id']['@value']) ? $xml['create_team_reply']['team_id']['@value'] : 0;

      if ($boincteam_id) {

        $boincteam = boincteam_load($boincteam_id);
        $team_owner_uid = boincuser_lookup_uid($boincteam->userid);

        // Create the team node in Drupal

        $values = array(
          'name' => isset($_GET['name']) ? $_GET['name'] : '',
          'description' => isset($_GET['description']) ? $_GET['description'] : '',
          'type' => isset($_GET['type']) ? $_GET['type'] : '',
        );
        $teaser = node_teaser($values['description']);
        $created_time = time();

        $node = array(
          'type' => 'team',
          'title' => $values['name'],
          'body' => $values['description'],
          'teaser' => $teaser,
          'uid' => $team_owner_uid,
          'path' => null,
          'status' => 1,  // published or not - always publish
          'promote' => 0,
          'created' => $created_time,
          'comment' => 0,  // comments disabled
          'moderate' => 0,
          'sticky' => 0,
          'format' => $input_format
        );

        // Use pathauto function, if available, to clean up the path
        if (module_exists('pathauto')) {
          module_load_include('inc', 'pathauto', 'pathauto');
          $node['path'] = pathauto_cleanstring($values['name']);

          $node = (object) $node; // node_save requires an object form
          $team_categories = taxonomy_get_term_by_name($values['type']);
          $node->taxonomy[] = $team_categories[0];

          // Save the team node
          node_save($node);
          // Save the team IDs to a BOINC <--> Drupal reference table.
          db_query('INSERT INTO {boincteam} (team_id, nid) VALUES (%d, %d)', $boincteam_id, $node->nid);
        }
        else {
          $xml['create_team_reply']['warning'] = t(
            'There was a problem integrating this team into Drupal. Please
            contact @project administrators!', array('@project' => PROJECT)
          );
          watchdog(
            'BOINC team',
            'BOINC teams require the Pathauto module. Team cannot be created in
              Drupal via create_team.php RPC.',
            array(),
            WATCHDOG_ERROR
          );
          // Delete from the BOINC DB if we can't sync with Drupal
          $boincteam::delete();
        }
      }
      else {
        $xml['create_team_reply']['warning'] = t(
          'There was a problem integrating this team into Drupal. Please
          contact @project administrators!', array('@project' => PROJECT)
        );
        watchdog(
          'BOINC team',
          'Team cannot be created in Drupal via create_team.php RPC because no
            BOINC team ID was returned in the XML.',
          array(),
          WATCHDOG_ERROR
        );
      }
    }
  }
  else {
    $xml = array(
      'error' => array(
        'error_num' => -1,
        'error_msg' => t(
          'Teams are not enabled for this project'
        ),
      ),
    );
    watchdog(
      'BOINC team',
      'Team cannot be created in Drupal via create_team.php RPC because the
        BOINC team module is not enabled.',
      array(),
      WATCHDOG_ERROR
    );
  }
  print save_configuration($xml);
}

/**
 * Page callback for the team lookup RPC (team_lookup.php).
 * Get info on a team by ID or a list of teams that match a given name
 */
function boinccore_team_lookup() {
  // Remove q from the GET request or BOINC will panic
  unset($_GET['q']);
  include_boinc('user/team_lookup.php');
}

/**
 * Page callback for the team email list RPC (team_email_list.php).
 * Get members of a given team
 */
function boinccore_team_email_list() {
  // See if the account has an approved profile in Drupal
  $boincteam_id = !empty($_POST['teamid']) ? $_POST['teamid'] : $_GET['teamid'];
  $credit_only = !empty($_POST['creditonly']) ? $_POST['creditonly'] : $_GET['creditonly'];
  $show_xml = !empty($_POST['xml']) ? $_POST['xml'] : $_GET['xml'];
  if (!$show_xml) {
      // creditonly does not affect non xml output in BOINC
      $credit_only = FALSE;
  }
  $xml = array();

  if ($boincteam_id && is_numeric($boincteam_id)) {
    if ($credit_only) {
      db_set_active('boinc_rw');
      $result = db_query("
        SELECT
          u.id, u.name, u.cross_project_id, u.email_addr, u.total_credit, u.expavg_credit, u.expavg_time
        FROM {user} u
        WHERE u.teamid = %d and u.total_credit > 0",
        $boincteam_id
      );
      db_set_active('default');

      $xml = array('users' => array());
      while ($member = db_fetch_object($result)) {
        $team_member = array(
          'id' => $member->id,
          'cpid' => md5($member->cross_project_id.$member->email_addr),
          'name' => htmlspecialchars($member->name),
          'total_credit' => round($member->total_credit),
          'expavg_credit' => round($member->expavg_credit),
          'expavg_time' => round($member->expavg_time),
        );
        $xml['users']['user'][] = $team_member;
      }
    }
    else {
      // Query BOINC database user table
      db_set_active('boinc_rw');
      $result = db_query("
        SELECT
          u.id, u.name, u.cross_project_id, u.create_time, u.email_addr, u.total_credit, u.expavg_credit, u.expavg_time
        FROM {user} u
        WHERE u.teamid = %d ORDER BY u.email_addr ASC",
        $boincteam_id
      );
      db_set_active('default');

      // Extract information from BOINC database for team members.
      $team_members = array();
      while ($member = db_fetch_object($result)) {
        $team_members[$member->id] = array(
          'id' => $member->id,
          'cpid' => md5($member->cross_project_id.$member->email_addr),
          'create_time' => $member->create_time,
          'name' => htmlspecialchars($member->name),
          'country' => NULL,
          'total_credit' => round($member->total_credit),
          'expavg_credit' => round($member->expavg_credit),
          'expavg_time' => round($member->expavg_time),
          'url' => NULL,
          'has_profile' => 0,
        );
      }

      // Query Drupal database, multiple tables
      $sql1 = "
         SELECT
           bu.boinc_id, du.uid, du.name, n.nid, n.type, n.field_country_value, n.field_url_value
         FROM {boincuser} AS bu
         INNER JOIN {users} AS du ON bu.uid=du.uid
         LEFT JOIN (
           SELECT node.nid, node.uid, node.type, p.field_country_value, p.field_url_value
           FROM {node}
           INNER JOIN {content_type_profile} AS p ON node.nid=p.nid
           WHERE node.type='profile' ) n ON du.uid=n.uid
         WHERE bu.boinc_id IN (%s)";
      $member_id_list = implode(',', array_keys($team_members));
      if (!$member_id_list) {
        $member_id_list = '-1';
      }
      $result = db_query($sql1, $member_id_list);
      while ($member = db_fetch_object($result)) {
        $team_members[$member->boinc_id]['country'] = $member->field_country_value;
        $team_members[$member->boinc_id]['url'] = $member->field_url_value;
        $team_members[$member->boinc_id]['has_profile'] = isset($member->nid) ? 1:  0;
        $xml['users']['user'][] = $team_members[$member->boinc_id];
      }
    } // end if credit_only
  }
  else {
    $xml = array(
      'error' => array(
        'error_num' => -136,
        'error_msg' => 'Not found',
      ),
    );
  } // end if boincteam_id
  print xml_to_text(array_to_xml($xml), TRUE, TRUE);
}

/**
 * Page callback for the forum prefs RPC (edit_forum_preferences_action.php).
 * Edit the forum preferences for a given user
 */
function boinccore_edit_forum_preferences() {
  // Do not pass through to BOINC in this case as BOINC forums are no longer
  // relevant -- use Drupal data only

  $account_key = !empty($_POST['account_key']) ? $_POST['account_key'] : (!empty($_GET['account_key']) ? $_GET['account_key'] : NULL);

  require_boinc('boinc_db');
  $boincuser = BoincUser::lookup_auth($account_key);
  if ($boincuser) {
    $uid = boincuser_lookup_uid($boincuser->id);
    if ($uid AND $account = user_load($uid)) {
      // Currently supported preferences
      $avatar_url = isset($_POST['avatar_url']) ? $_POST['avatar_url'] : (isset($_GET['avatar_url']) ? $_GET['avatar_url'] : NULL);
      $hide_signatures = isset($_POST['forum_hide_signatures']) ? $_POST['forum_hide_signatures'] : (isset($_GET['forum_hide_signatures']) ? $_GET['forum_hide_signatures'] : NULL);
      $signature = isset($_POST['signature']) ? $_POST['signature'] : (isset($_GET['signature']) ? $_GET['signature'] : NULL);
      $post_order = isset($_POST['thread_sort']) ? $_POST['thread_sort'] : (isset($_GET['thread_sort']) ? $_GET['thread_sort'] : NULL);
      // Update preferences as needed
      if ($avatar_url) {
        $avatar_image = file_get_contents($avatar_url);
        if ($avatar_image) {
          $image_dir = 'profiles';
          $image_path = "{$image_dir}/{$boincuser->id}_avatar.jpg";
          $file = file_save_data($avatar_image, $image_path, FILE_EXISTS_REPLACE);
          if ($file) {
            $profile = new stdClass();
            $profile->type = 'profile';
            $profile->language = NULL;
            if ($profile_nid = content_profile_profile_exists($profile, $account->uid)) {
              $profile_node = node_load($profile_nid);
              if ($profile_node) {
                $avatar_image = get_cck_image_object(file_directory_path() . '/' . $image_path, 'field_image', 'profile', TRUE);
                if ($avatar_image) {
                  $profile_node->field_image[0] = $avatar_image;
                  node_save($profile_node);
                }
                else {
                  $error = 'Error saving avatar to profile';
                }
              }
              else {
                $error = 'Unable to load user profile';
              }
            }
            else {
              $error = 'User has no profile, so avatar could not be added';
            }
          }
          else {
            $error = 'Unable to save remote image';
          }
        }
        else {
          $error = 'Could not find avatar image at URL';
        }
      }
      if ($hide_signatures !== NULL) {
        user_save($account, array('hide_signatures' => ($hide_signatures ? 1 : 0)));
      }
      if ($signature !== NULL) {
        $signature = check_markup($signature);
        user_save($account, array('signature' => $signature));
      }
      if ($post_order) {
        require_boinc('forum');
        $post_order_map = array(
          CREATE_TIME_OLD => 2,
          CREATE_TIME_NEW => 1,
        );
        $drupal_post_order = isset($post_order_map[$post_order]) ? $post_order_map[$post_order] : 2;
        user_save($account, array('sort' => $drupal_post_order));
      }
      if (!$error) {
        // Success
        $xml = array(
          'edit_forum_preferences' => array(
            'success' => NULL,
          ),
        );
      }
      else {
        // Failure
        $xml = array(
          'error' => array(
            'error_num' => -1,
            'error_msg' => $error,
          ),
        );
      }
    }
    else {
      // Drupal user not found
      $xml = array(
        'error' => array(
          'error_num' => -1,
          'error_msg' => 'Unknown error',
        ),
      );
    }
  }
  else {
    // BOINC user not found
    $xml = array(
      'error' => array(
        'error_num' => -136,
        'error_msg' => 'Not found',
      ),
    );
  }
  print xml_to_text(array_to_xml($xml), TRUE, TRUE);
}

/**
 * Page callback for the user forum activity RPC (forum_get_data.php).
 * Get the last comments OR threads made by a given user
 */
function boinccore_forum_get_data() {
  // Do not pass through to BOINC in this case as BOINC forums are no longer
  // relevant -- use Drupal data only

  $xml = array();
  $boinc_id = !empty($_POST['userid']) ? $_POST['userid'] : (!empty($_GET['userid']) ? $_GET['userid'] : NULL);
  $uid = boincuser_lookup_uid($boinc_id);

  if ($uid) {
    $method = !empty($_POST['method']) ? $_POST['method'] : (!empty($_GET['method']) ? $_GET['method'] : NULL);
    $count = !empty($_POST['count']) ? $_POST['count'] : (!empty($_GET['count']) ? $_GET['count'] : NULL);
    if (!$count) $count = 10;

    switch ($method) {
    case 'user_posts':
      $content_length = !empty($_POST['content_length']) ? $_POST['content_length'] : (!empty($_GET['content_length']) ? $_GET['content_length'] : NULL);
      $posts = db_query("
        SELECT
          c.cid,
          n.nid,
          c.uid,
          n.title,
          c.comment,
          c.timestamp
        FROM comments c
        INNER JOIN node n ON c.nid = n.nid
        WHERE (n.status = 1) AND (c.uid = '%d')
        ORDER BY timestamp DESC
        LIMIT %d",
        $uid, (int) $count
      );
      $xml = array(
        'rpc_response' => array(
          'count' => 0,
          'posts' => array(),
        ),
      );
      $post_count = 0;
      while ($post = db_fetch_object($posts)) {
        $xml['rpc_response']['posts']['post'][] = array(
          'id' => $post->cid,
          'threadid' => $post->nid,
          'threadtitle' => $post->title,
          'timestamp' => $post->timestamp,
          'content' => ($content_length ? substr($post->comment, 0, $content_length) : $post->comment),
        );
        $post_count++;
      }
      $xml['rpc_response']['count'] = $post_count;
      break;

    case 'user_threads':
      $threads = db_query("
        SELECT
          n.nid,
          n.uid,
          n.title,
          nc.totalcount AS views,
          n.changed,
          n.title,
          tn.tid,
          (
            SELECT COUNT(*)
            FROM {comments} c
            WHERE c.nid = n.nid
          ) AS replies
        FROM {node} n
        LEFT JOIN {node_counter} nc ON nc.nid = n.nid
        LEFT JOIN {term_node} tn ON tn.nid = n.nid
        WHERE (n.status = 1) AND (n.uid = '%d')
        ORDER BY changed DESC
        LIMIT %d",
        $uid, (int) $count
      );
      $xml = array(
        'rpc_response' => array(
          'count' => 0,
          'threads' => array(),
        ),
      );
      $thread_count = 0;
      while ($thread = db_fetch_object($threads)) {
        $xml['rpc_response']['threads']['thread'][] = array(
          'id' => $thread->nid,
          'forumid' => $thread->tid,
          'replies' => $thread->replies,
          'views' => $thread->views,
          'timestamp' => $thread->changed,
          'title' => $thread->title,
        );
        $thread_count++;
      }
      $xml['rpc_response']['count'] = $thread_count;
      break;
    default:
      $xml = array(
        'error' => array(
          'error_num' => -1,
          'error_msg' => 'Unknown error',
        ),
      );
    }
  }
  else {
    $xml = array(
      'error' => array(
        'error_num' => -136,
        'error_msg' => 'Not found',
      ),
    );
  }
  print xml_to_text(array_to_xml($xml), TRUE, TRUE);
}

/**
 * Page callback for the applications RPC (apps.php).
 * Get information on applications in the system
 */
function boinccore_apps() {
  // Remove q from the GET request or BOINC will panic
  unset($_GET['q']);
  include_boinc('user/apps.php');
}

/**
 * Controller for handling direct linking to paginated content.
 * Because pagination settings are user configurable, it is impossible to know
 * in advance on which page a given comment will appear. Link instead to this
 * controller function, which can compute the page based on the user's settings
 */
function boinccore_url_pagination_handler($type, $object_id = NULL) {
  global $user;
  $path = NULL;
  $params = array();
  switch ($type) {
  case 'boinc-forum':
    // Redirect requests to a BOINC forum to the Drupal forum
    $forum_id = db_result(db_query('
      SELECT tid
      FROM {boincimport_temp_forum}
      WHERE forum_id = %d',
      $object_id
    ));
    if ($forum_id) {
      drupal_goto("community/forum/{$forum_id}");
    }
    break;
  case 'boinc-forum-index':
    // Redirect requests to the top level BOINC forum to Drupal forums
    drupal_goto("community/forum");
    break;
  case 'boinc-forum-post':
    // Redirect requests to a BOINC forum post to the Drupal comment
    $cid = db_result(db_query('
      SELECT cid
      FROM {boincimport_temp_post}
      WHERE post_id = %d',
      $object_id
    ));
    if ($cid) {
      drupal_goto("goto/comment/{$cid}");
    }
    break;
  case 'boinc-forum-topic':
    // Redirect requests to a BOINC forum topic to the Drupal node
    $nid = db_result(db_query('
      SELECT nid
      FROM {boincimport_temp_topic}
      WHERE topic_id = %d',
      $object_id
    ));
    if ($nid) {
      drupal_goto("node/{$nid}");
    }
    break;
  case 'boinc-host':
    // Redirect requests to BOINC host details to the host page in Drupal
    drupal_goto("host/{$object_id}");
    break;
  case 'boinc-hosts-user':
    // Redirect requests to a BOINC user host list to the host list in Drupal
    $uid = boincuser_lookup_uid($object_id);
    if ($uid) {
      drupal_goto("account/{$uid}/computers");
    }
    break;
  case 'boinc-result':
    // Redirect requests to BOINC task details to the task page in Drupal
    drupal_goto("task/{$object_id}");
  case 'boinc-results-host':
    // Redirect requests to BOINC tasks by host to the host task page in Drupal
    drupal_goto("host/{$object_id}/tasks");
    break;
  case 'boinc-results-user':
    // Redirect requests to a BOINC user's tasks to the Drupal account tasks
    drupal_goto("account/tasks");
    break;
  case 'boinc-user':
    // Redirect requests to a BOINC user ID to the Drupal profile
    $uid = boincuser_lookup_uid($object_id);
    if ($uid) {
      drupal_goto("account/{$uid}");
    }
    break;
  case 'boinc-workunit':
    // Redirect requests to BOINC workunits to the workunit page in Drupal
    drupal_goto("workunit/{$object_id}");
    break;
  case 'comment':
    $object = _comment_load($object_id);
    if ($object) {
      $path = "node/{$object->nid}";
      // Get the sort order
      $gt_lt = '<';
      $comment_order = (isset($user->sort)) ? $user->sort : variable_get('comment_default_order_forum', 2);
      if ($comment_order == COMMENT_ORDER_NEWEST_FIRST) {
        $gt_lt = '>';
      }
      // don't filter hidden comments for admins/mods
      $include_hidden = '';
      if (in_array('administrator', array_values($user->roles)) ||
          in_array('moderator', array_values($user->roles))) {
        $include_hidden = 'OR status = 1';
      }
      // See how many comments there are before (or after) this comment...
      $comment_offset = db_result(db_query('
        SELECT COUNT(*) FROM {comments}
        WHERE nid = %d AND (status = 0 %s) AND timestamp %s %d',
        $object->nid, $include_hidden, $gt_lt, $object->timestamp));
      if ($comment_offset) {
        // Get the number of comments per page
        if ($user->comments_per_page) {
          $comments_per_page = $user->comments_per_page;
        }
        else {
          $node = node_load($object->nid);
          $comments_per_page = variable_get(
            "comment_default_per_page_{$node->type}", 50
          );
        }
        // Determine which page to display in order to show the given comment
        $page = floor($comment_offset / $comments_per_page);
        if ($page) {
          $params = array('page' => $page);
        }
      }
    }
    if ($path) {
      drupal_goto($path, $params, "comment-{$object->cid}");
    }
    break;
  case 'friend-requests':
    // Check for a login, then redirect to the user's friend requests page
    if ($user->uid) {
      drupal_goto("account/{$user->uid}/friends/pending");
    }
    else {
      drupal_goto('user/login', array(
        'destination' => 'goto/friend-requests'
      ));
    }
    break;
  default:
  }
  drupal_not_found();
}

/*
 * Fetch an array of all countries
 */
function boinccore_get_country_list() {
  return array(
    'None' => 'None',
    'International' => 'International',
    'Afghanistan' => 'Afghanistan',
    'Albania' => 'Albania',
    'Algeria' => 'Algeria',
    'American Samoa' => 'American Samoa',
    'Andorra' => 'Andorra',
    'Angola' => 'Angola',
    'Anguilla' => 'Anguilla',
    'Antarctica' => 'Antarctica',
    'Antigua and Barbuda' => 'Antigua and Barbuda',
    'Argentina' => 'Argentina',
    'Armenia' => 'Armenia',
    'Aruba' => 'Aruba',
    'Australia' => 'Australia',
    'Austria' => 'Austria',
    'Azerbaijan' => 'Azerbaijan',
    'Bahamas, The' => 'Bahamas, The',
    'Bahrain' => 'Bahrain',
    'Bangladesh' => 'Bangladesh',
    'Barbados' => 'Barbados',
    'Belarus' => 'Belarus',
    'Belgium' => 'Belgium',
    'Belize' => 'Belize',
    'Benin' => 'Benin',
    'Bermuda' => 'Bermuda',
    'Bhutan' => 'Bhutan',
    'Bolivia' => 'Bolivia',
    'Bosnia and Herzegovina' => 'Bosnia and Herzegovina',
    'Botswana' => 'Botswana',
    'Brazil' => 'Brazil',
    'British Virgin Islands' => 'British Virgin Islands',
    'Brunei' => 'Brunei',
    'Bulgaria' => 'Bulgaria',
    'Burkina Faso' => 'Burkina Faso',
    'Burma' => 'Burma',
    'Burundi' => 'Burundi',
    'Cambodia' => 'Cambodia',
    'Cameroon' => 'Cameroon',
    'Canada' => 'Canada',
    'Cape Verde' => 'Cape Verde',
    'Cayman Islands' => 'Cayman Islands',
    'Central African Republic' => 'Central African Republic',
    'Chad' => 'Chad',
    'Channel Islands' => 'Channel Islands',
    'Chile' => 'Chile',
    'China' => 'China',
    'Colombia' => 'Colombia',
    'Comoros' => 'Comoros',
    'Congo, Democratic Republic of the' => 'Congo, Democratic Republic of the',
    'Congo, Republic of the' => 'Congo, Republic of the',
    'Cook Islands' => 'Cook Islands',
    'Costa Rica' => 'Costa Rica',
    'Cote d\'Ivoire' => 'Cote d\'Ivoire',
    'Croatia' => 'Croatia',
    'Cuba' => 'Cuba',
    'Cyprus' => 'Cyprus',
    'Czech Republic' => 'Czech Republic',
    'Denmark' => 'Denmark',
    'Djibouti' => 'Djibouti',
    'Dominica' => 'Dominica',
    'Dominican Republic' => 'Dominican Republic',
    'East Timor' => 'East Timor',
    'Ecuador' => 'Ecuador',
    'Egypt' => 'Egypt',
    'El Salvador' => 'El Salvador',
    'Equatorial Guinea' => 'Equatorial Guinea',
    'Eritrea' => 'Eritrea',
    'Estonia' => 'Estonia',
    'Ethiopia' => 'Ethiopia',
    'Falkland Islands' => 'Falkland Islands',
    'Faroe Islands' => 'Faroe Islands',
    'Fiji' => 'Fiji',
    'Finland' => 'Finland',
    'France' => 'France',
    'French Guiana' => 'French Guiana',
    'French Polynesia' => 'French Polynesia',
    'Gabon' => 'Gabon',
    'Gambia, The' => 'Gambia, The',
    'Gaza Strip' => 'Gaza Strip',
    'Georgia' => 'Georgia',
    'Germany' => 'Germany',
    'Ghana' => 'Ghana',
    'Gibraltar' => 'Gibraltar',
    'Greece' => 'Greece',
    'Greenland' => 'Greenland',
    'Grenada' => 'Grenada',
    'Guadeloupe' => 'Guadeloupe',
    'Guam' => 'Guam',
    'Guatemala' => 'Guatemala',
    'Guinea' => 'Guinea',
    'Guinea-Bissau' => 'Guinea-Bissau',
    'Guyana' => 'Guyana',
    'Haiti' => 'Haiti',
    'Holy See (Vatican City)' => 'Holy See (Vatican City)',
    'Honduras' => 'Honduras',
    'Hong Kong' => 'Hong Kong',
    'Hungary' => 'Hungary',
    'Iceland' => 'Iceland',
    'India' => 'India',
    'Indonesia' => 'Indonesia',
    'Iran' => 'Iran',
    'Iraq' => 'Iraq',
    'Ireland' => 'Ireland',
    'Isle of Man' => 'Isle of Man',
    'Israel' => 'Israel',
    'Italy' => 'Italy',
    'Jamaica' => 'Jamaica',
    'Japan' => 'Japan',
    'Jordan' => 'Jordan',
    'Kazakhstan' => 'Kazakhstan',
    'Korea, North' => 'Korea, North',
    'Korea, South' => 'Korea, South',
    'Kenya' => 'Kenya',
    'Kiribati' => 'Kiribati',
    'Kuwait' => 'Kuwait',
    'Kyrgyzstan' => 'Kyrgyzstan',
    'Laos' => 'Laos',
    'Latvia' => 'Latvia',
    'Lebanon' => 'Lebanon',
    'Lesotho' => 'Lesotho',
    'Liberia' => 'Liberia',
    'Libya' => 'Libya',
    'Liechtenstein' => 'Liechtenstein',
    'Lithuania' => 'Lithuania',
    'Luxembourg' => 'Luxembourg',
    'Macau' => 'Macau',
    'Macedonia' => 'Macedonia',
    'Madagascar' => 'Madagascar',
    'Malawi' => 'Malawi',
    'Malaysia' => 'Malaysia',
    'Maldives' => 'Maldives',
    'Mali' => 'Mali',
    'Malta' => 'Malta',
    'Marshall Islands' => 'Marshall Islands',
    'Martinique' => 'Martinique',
    'Mauritania' => 'Mauritania',
    'Mauritius' => 'Mauritius',
    'Mexico' => 'Mexico',
    'Micronesia' => 'Micronesia',
    'Moldova' => 'Moldova',
    'Monaco' => 'Monaco',
    'Mongolia' => 'Mongolia',
    'Montenegro' => 'Montenegro',
    'Montserrat' => 'Montserrat',
    'Morocco' => 'Morocco',
    'Mozambique' => 'Mozambique',
    'Namibia' => 'Namibia',
    'Nauru' => 'Nauru',
    'Nepal' => 'Nepal',
    'Netherlands' => 'Netherlands',
    'Netherlands Antilles' => 'Netherlands Antilles',
    'New Caledonia' => 'New Caledonia',
    'New Zealand' => 'New Zealand',
    'Nicaragua' => 'Nicaragua',
    'Niger' => 'Niger',
    'Nigeria' => 'Nigeria',
    'Niue' => 'Niue',
    'Northern Mariana Islands' => 'Northern Mariana Islands',
    'Norway' => 'Norway',
    'Oman' => 'Oman',
    'Pakistan' => 'Pakistan',
    'Palau' => 'Palau',
    'Panama' => 'Panama',
    'Papua New Guinea' => 'Papua New Guinea',
    'Paraguay' => 'Paraguay',
    'Peru' => 'Peru',
    'Philippines' => 'Philippines',
    'Pitcairn Islands' => 'Pitcairn Islands',
    'Poland' => 'Poland',
    'Portugal' => 'Portugal',
    'Puerto Rico' => 'Puerto Rico',
    'Qatar' => 'Qatar',
    'Reunion' => 'Reunion',
    'Romania' => 'Romania',
    'Russia' => 'Russia',
    'Rwanda' => 'Rwanda',
    'Saint Kitts and Nevis' => 'Saint Kitts and Nevis',
    'Saint Lucia' => 'Saint Lucia',
    'Saint Pierre and Miquelon' => 'Saint Pierre and Miquelon',
    'Saint Vincent and the Grenadines' => 'Saint Vincent and the Grenadines',
    'Samoa' => 'Samoa',
    'San Marino' => 'San Marino',
    'Sao Tome and Principe' => 'Sao Tome and Principe',
    'Saudi Arabia' => 'Saudi Arabia',
    'Senegal' => 'Senegal',
    'Serbia' => 'Serbia',
    'Seychelles' => 'Seychelles',
    'Sierra Leone' => 'Sierra Leone',
    'Singapore' => 'Singapore',
    'Slovakia' => 'Slovakia',
    'Slovenia' => 'Slovenia',
    'Solomon Islands' => 'Solomon Islands',
    'Somalia' => 'Somalia',
    'South Africa' => 'South Africa',
    'Spain' => 'Spain',
    'Sri Lanka' => 'Sri Lanka',
    'Sudan' => 'Sudan',
    'Suriname' => 'Suriname',
    'Swaziland' => 'Swaziland',
    'Sweden' => 'Sweden',
    'Switzerland' => 'Switzerland',
    'Syria' => 'Syria',
    'Taiwan' => 'Taiwan',
    'Tajikistan' => 'Tajikistan',
    'Tanzania' => 'Tanzania',
    'Thailand' => 'Thailand',
    'Togo' => 'Togo',
    'Tokelau' => 'Tokelau',
    'Tonga' => 'Tonga',
    'Trinidad and Tobago' => 'Trinidad and Tobago',
    'Tunisia' => 'Tunisia',
    'Turkey' => 'Turkey',
    'Turkmenistan' => 'Turkmenistan',
    'Turks and Caicos Islands' => 'Turks and Caicos Islands',
    'Tuvalu' => 'Tuvalu',
    'Uganda' => 'Uganda',
    'Ukraine' => 'Ukraine',
    'United Arab Emirates' => 'United Arab Emirates',
    'United Kingdom' => 'United Kingdom',
    'United States' => 'United States',
    'Uruguay' => 'Uruguay',
    'Uzbekistan' => 'Uzbekistan',
    'Vanuatu' => 'Vanuatu',
    'Venezuela' => 'Venezuela',
    'Vietnam' => 'Vietnam',
    'Virgin Islands' => 'Virgin Islands',
    'Wallis and Futuna' => 'Wallis and Futuna',
    'West Bank' => 'West Bank',
    'Western Sahara' => 'Western Sahara',
    'Yemen' => 'Yemen',
    'Zambia' => 'Zambia',
    'Zimbabwe' => 'Zimbabwe',
  );
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Functions related to rules module
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Action: Mark node for moderation
 */
function boinccore_rules_action_mark_content_for_moderation($node) {
  $node->moderate = 1;
  return array('node' => $node);
}

/**
 * Action: Send mail to moderators.
 */
function boinccore_rules_action_mail_to_moderators($settings) {
  module_load_include('inc', 'rules', 'modules/system.rules');
  $mailing_list = variable_get('boinc_moderator_mailing_list', '');
  $force_individual_notifications = variable_get('boinc_moderator_force_individual_notifications', 0);

  if (!$mailing_list OR $force_individual_notifications) {
    // Send individual emails to all moderators
    $roles = user_roles();
    $moderator_rid = array_search('moderator', $roles);
    if ($moderator_rid !== FALSE) {
      $settings['recipients'] = array($moderator_rid);
      rules_action_mail_to_users_of_role($settings);
    }
    else {
      watchdog(
        'boinccore',
        'Unable to identify members of the moderator role to send moderator notifications!',
        array(),
        WATCHDOG_ERROR
      );
    }
  }
  if ($mailing_list) {
    $settings['to'] = $mailing_list;
    rules_action_mail($settings);
  }
}
/**
 * Action "Send mail to moderators" configuration form
 */
function boinccore_rules_action_mail_to_moderators_form($settings = array(), &$form) {
  // Select only non-anonymous user roles because anonymous users won't have emails.
  rules_action_mail_to_user_form($settings, $form);
}


/**
 * Action: Send mail to admins.
 */
function boinccore_rules_action_mail_to_admins($settings) {
  module_load_include('inc', 'rules', 'modules/system.rules');
  $mailing_list = variable_get('boinc_admin_mailing_list', '');
  if (empty($mailing_list)) {
    $mailing_list = variable_get('site_mail', '');
    if (empty($mailing_list)) {
      watchdog('boinccore',
        'Attempting to email administrators, but there is no admin email list or site email to send to.', array(),
        WATCHDOG_ERROR
      );
      return FALSE;
    }
  }
  $settings['to'] = $mailing_list;
  rules_action_mail($settings);
}

/**
 * Action "Send mail to admins" configuration form
 */
function boinccore_rules_action_mail_to_admins_form($settings = array(), &$form) {
  rules_action_mail_to_user_form($settings, $form);
}

/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * BOINC Translation functions
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/*
 * BOINC wrapper for string translation
 */
function bts($string, $args = array(), $langcode = NULL, $context = 'boinc: ', $update = TRUE) {
  if (function_exists('i18nstrings_ts')) {
    $string = i18nstrings_ts($context, $string, $langcode, $update);
  }
  if (empty($args)) {
    return $string;
  }
  else {
    // Transform arguments before inserting them.
    foreach ($args as $key => $value) {
      switch ($key [0]) {
        case '@':
          // Escaped only.
          $args [$key] = check_plain($value);
          break;

        case '%':
        default:
          // Escaped and placeholder.
          $args [$key] = theme('placeholder', $value);
          break;

        case '!':
          // Pass-through.
      }
    }
    return strtr($string, $args);
  }
}


/*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
 * Pane content for panels
 *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  */

/**
 * Overview of moderation queues
 */
function boinccore_moderation_overview_panel() {
  $output = '<h2 class="pane-title">' . bts('Moderation', array(), NULL, 'boinc:moderate-profiles') . '</h2>';
  $output .= '<ul class="tab-list action-list">';
  if (module_exists('user_profiles')) {
    $profile_count = db_result(db_query("
      SELECT COUNT(DISTINCT(n.nid))
      FROM {node} n
      WHERE n.type = 'profile' AND n.moderate = 1"
    ));
    $output .= '<li class="tab primary">';
    $output .= l(bts('Unvetted profiles', array(), NULL, 'boinc:moderate-profiles'), 'moderate/profiles');
    $output .= '<div class="item-count-wrapper">';
    $output .= '<span class="item-count">' . $profile_count . '</span>';
    $output .= '</div>';
    $output .= '</li>';
  }
  $output .= '</ul>';
  return $output;
}


/**
 * Utility function to delete expired tokens in the BOINC database,
 * token table.
 *
 */
function  _boinccore_delete_expired_tokens() {
  require_boinc('boinc_db');

  $num_deleted = BoincToken::delete_expired();
  if ($num_deleted>0) {
    watchdog('boinccore', "Deleted ${num_deleted} tokens from table token", WATCHDOG_NOTICE);
  }
}
