<?php
// $Id: user_stats.module,v 1.2.2.16.2.21 2009/03/14 00:26:56 liammcdermott Exp $

/**
 * @file
 * User Stats provides commonly requested user statistics for themers.
 * These are:
 *  - days registered;
 *  - join date;
 *  - days since last login;
 *  - days since last post;
 *  - post count;
 *  - login count;
 *  - user online/offline;
 *  - IP address;
 *
 * Note for hackers: function parameters should go in the order
 * $op/$type, $uid, $data (where applicable).
 */

/**
 * Implementation of hook_perm().
 */
function user_stats_perm() {
  return array('administer user stats', 'View statistics', 'View IP addresses');
}

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

  // Admin settings
  $items['admin/settings/user_stats'] = array(
    'title' => 'User Stats settings',
    'description' => 'Configuration of user stats module options.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('user_stats_admin_settings'),
    'access arguments' => array('administer user stats'),
    'file' => 'user_stats.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/settings/user_stats/reset_post_count'] = array(
    'title' => 'reset user post stats',
    'page callback' => 'user_stats_reset_post_count',
    'access arguments' => array('administer user stats'),
    'file' => 'user_stats.admin.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/settings/user_stats/reset_login_count'] = array(
    'title' => 'reset user login stats',
    'page callback' => 'user_stats_reset_login_count',
    'access arguments' => array('administer user stats'),
    'file' => 'user_stats.admin.inc',
    'type' => MENU_CALLBACK,
  );

  return $items;
}

/**
 * Returns user stats.
 *
 * @param $type
 *   The statistic to return. Possible values are:
 *   - "ip_address"
 *   - "join_date"
 *   - "login_count"
 *   - "login_days"
 *   - "post_count"
 *   - "post_days"
 *   - "reg_days"
 *   - "online"
 *   - "profile"
 * @param $uid
 *   The user id who's stats should be retrieved.
 *
 * @return
 *   The statistic requested. Every statistic except join_date, online and IP address is a numeric.
 *   Join date is a string, whilst online is a boolean and IP Address a string.
 *   Note: if $type = "post_days" and the user hasn't posted any content (of the
 *   counted types) then 'n/a' is returned.
 */
function user_stats_get_stats($type, $uid) {
  // Dirt simple error checking.
  if (!is_numeric($uid)) {
    trigger_error('UID is not a number.', E_USER_WARNING);
  }

  // IP address is really a bit of feature creep.
  // At some point in the future, this could be split off into its own module.
  if ($type == 'ip_address') {
    if (!user_access('View IP addresses')) {
      return FALSE;
    }
    // Check cache.
    if (user_stats_cache_get($type, $uid) === FALSE) {
      $query = db_query("SELECT ip_address
        FROM {user_stats_ips} WHERE uid = %d
        ORDER BY first_seen_timestamp LIMIT 1", $uid);
      user_stats_cache_set($type, $uid, db_result($query));
    }
    return user_stats_cache_get($type, $uid);
  }

  // Everything else is under the 'View statistics' permission.
  if (!user_access('View statistics')) {
    return FALSE;
  }

  // Check cache first.
  if (user_stats_cache_get($type, $uid) !== FALSE) {
    return user_stats_cache_get($type, $uid);
  }

  switch ($type) {
    case 'join_date':
      $query = db_query("SELECT created FROM {users} WHERE uid = %d", $uid);
      $data = db_result($query);
      break;
    case 'login_count':
      if (!variable_get('user_stats_count_logins', TRUE)) {
        $data = 'n/a';
      }
      else if (user_stats_isset($type, $uid)) {
        $query = db_query("SELECT value FROM {user_stats_values}
          WHERE name = 'login_count' AND uid = %d", $uid);
        $data = db_result($query);
      }
      else {
        return 0;
      }
      break;
    case 'login_days':
      $user_access = db_result(db_query("SELECT access FROM {users} WHERE uid = %d", $uid));
      $data = floor((time() - $user_access) / 86400);
      break;
    case 'post_count':
      if (!variable_get('user_stats_count_posts', TRUE) && !variable_get('user_stats_count_comments', TRUE)) {
        $data = 'n/a';
      }
      else if (!user_stats_isset('post_count', $uid)) {
        user_stats_post_count_update('reset', $uid);
      }
      $query = db_query("SELECT value FROM {user_stats_values}
        WHERE name = 'post_count' AND uid = %d", $uid);
      $data = db_result($query);
      break;
    case 'post_days':
      $last_post = _user_stats_last_post($uid);
      if ($last_post !== FALSE) {
        $data = floor((time() - $last_post) / 86400);
      }
      else {
        $data = 'n/a';
      }
      break;
    case 'reg_days':
      $user_access = db_result(db_query("SELECT access FROM {users} WHERE uid = %d", $uid));
      $data = floor((time() - $user_access) / 86400);
      break;
    case 'online':
      $user_access = db_result(db_query("SELECT access FROM {users} WHERE uid = %d", $uid));
      $data = (round((time() - $user_access) / 60) < 15 ? TRUE : FALSE);
      break;
    default:  
      // Raise an error if the statistic doesn't exist.
      $err_message = 'Statistic "'. check_plain($type) .'" does not exist.';
      trigger_error($err_message, E_USER_WARNING);
      return;
  }
  user_stats_cache_set($type, $uid, $data);
  return user_stats_cache_get($type, $uid);
}

/**
 * Return data from the non-persistent User Stats cache. Single values
 * are returned according to type of statistic and unique user id.
 *
 * @param $type
 *   The type of statistic to retrieve, this corresponds to the statistic
 *   types used by user_stats_get_stats().
 * @param $uid
 *   Unique ID of the user who's statistic is being retrieved.
 *
 * @return
 *   A single value, representing the statistic $type where the unique user id
 *   is $uid. Or FALSE if there is no value in the cache for this combination
 *   of $type and $uid.
 *
 * @see user_stats_get_stats().
 * @see user_stats_cache_set().
 */
function user_stats_cache_get($type, $uid) {
  // Dirt simple error checking.
  if (!is_numeric($uid)) {
    trigger_error('UID is not a number.', E_USER_WARNING);
  }

  $user_stats_cache = user_stats_cache_set();
  if (isset($user_stats_cache[$uid][$type])) {
    return $user_stats_cache[$uid][$type];
  }
  else {
    return FALSE;
  }
}

/**
 * Store a value in the non-persistent User Stats cache.
 *
 * If the function is called with no arguments, the entire cache is returned
 * without being cleared.
 *
 * The User Stats cache is a static array, which is why we call it
 * non-persistent. The array structure is:
 * $user_stats_cache[$uid][$type] = $value.
 *
 * @param $type
 *   The type of statistic being stored, this corresponds to the statistic
 *   types used by user_stats_get_stats(), and one extra used to reset the
 *   cache: 'reset'.
 * @param $uid
 *   Unique ID of the user who's statistic is being stored. If the type
 *   is set to 'reset', this user id will have the cache values associated with
 *   it reset. Alternatively, if $type is set to 'reset' and this is -1, the
 *   entire cache will be reset.
 *
 * @return
 *   Array of the entire cache, or NULL if the cache has been reset.
 *
 * @see user_stats_get_stats().
 * @see user_stats_cache_get().
 */
function user_stats_cache_set($type = NULL, $uid = 0, $data = NULL) {
  // Dirt simple error checking.
  if (!is_numeric($uid)) {
    trigger_error('UID is not a number.', E_USER_WARNING);
  }

  static $user_stats_cache = array();
  // Flush entire cache.
  if ($uid == -1 && $type == 'reset') {
    unset($user_stats_cache);
    return;
  }
  else if ($uid > -1 && $type == 'reset') {
    unset($user_stats_cache[$uid]);
    return;
  }
  // Set cache data.
  if ($type && $data) {
    $user_stats_cache[$uid][$type] = $data;
  }
  return $user_stats_cache;
}

/**
 * Drupal hook implementations.
 */

/**
 * Implementation of hook_nodeapi().
 *
 * Increments post count on insert or publish.
 * Decrements post count on delete or unpublish.
 * Checks and updates users without post counts on view.
 *
 * @param &$node
 *   The node the action is being performed on.
 * @param $op
 *   The operation being performed. We are interested in insert, delete, update and view.
 */
function user_stats_nodeapi(&$node, $op) {
  // We stop before any work is done if the $op isn't one of the ones we need
  if (!in_array($op, array('insert', 'delete', 'update'))) {
    return;
  }

  $post_count_content_types = variable_get('user_stats_included_content_types', array());
  if ((empty($post_count_content_types) ||
    in_array($node->type, $post_count_content_types)) &&
    variable_get('user_stats_count_posts', TRUE)) {

    switch ($op) {
      case 'insert':
        if ($node->status) {
          user_stats_post_count_update('increment', $node->uid);
        }
        break;
      case 'delete':
        // Node must be published as unpublished nodes would have already been
        // removed from user's post count.
        if ($node->status) {
          user_stats_post_count_update('decrement', $node->uid);
        }
        break;
      case 'update':
        // Can't think of any other way of doing this than resetting the user.
        user_stats_post_count_update('reset', $node->uid);
        break;
    }
  }

  // Do IP Address update.
  switch ($op) {
    case 'insert':
    case 'update':
      global $user;
      // User IP addresses are only interesting if they are posting the content.
      if ($node->uid == $user->uid) {
        user_stats_ip_address_update($user->uid, ip_address());
      }
  }
}

/**
 * Implementation of hook_comment().
 *
 * Increments post count on insert or publish.
 * Decrements post count on delete or unpublish.
 * Updates users with no post count on view.
 *
 * @param $a1
 *   Either the form values being submitted, the module form to be displayed,
 *   or the comment object.
 * @param $op
 *   What kind of action is being performed.
 */
function user_stats_comment(&$a1, $op) {
  // We stop before any work is done if the $op isn't one of the ones we need.
  if (!in_array($op, array('insert', 'delete', 'update'))) {
    return;
  }

  // check to see if comments should be counted at all.
  if (variable_get('user_stats_count_comments', TRUE)) {
    $comment = (object)$a1;
    $post_count_content_types = variable_get('user_stats_included_content_types', array());
    $node = node_load(array('nid' => $comment->nid));

    if (empty($post_count_content_types) ||
      in_array($node->type, $post_count_content_types)) {

      switch ($op) {
        case 'insert':
          if ($comment->status == COMMENT_PUBLISHED) {
            user_stats_post_count_update('increment', $comment->uid);
          }
          break;
        case 'delete':
          if ($comment->status == COMMENT_PUBLISHED) {
            user_stats_post_count_update('decrement', $comment->uid);
          }
          break;
        case 'update':
          user_stats_post_count_update('reset', $comment->uid);
          break;
      }
    }
  }

  // Do IP address update.
  global $user;
  if ($op == 'insert' && $comment->uid == $user->uid) {
    // User IP addresses are only interesting if they are posting the content.
    user_stats_ip_address_update($user->uid, ip_address());
  }
}

/**
 * Implementation of hook_cron().
 *
 * We slowly work through all users without a post count
 * updating them.
 */
function user_stats_cron() {
  if (variable_get('user_stats_rebuild_stats', TRUE) &&
    (variable_get('user_stats_count_posts', TRUE) ||
    variable_get('user_stats_count_comments', TRUE))) {

    $sql  = "SELECT uid FROM {users} WHERE uid NOT IN
      (SELECT uid FROM {user_stats_values} WHERE name = 'post_count')";
    // Update 25 users per cron run.
    $result = db_query_range($sql, 0, variable_get('user_stats_user_per_cron', '25'));
    $users_updated = FALSE;
    while ($update_user = db_fetch_object($result)) {
      user_stats_post_count_update('reset', $update_user->uid);
      $users_updated = TRUE;
    }
    // If all users have been updated we'll avoid running this expensive
    // query again by setting the following flag!
    if (!$users_updated) {
      variable_set('user_stats_rebuild_stats', FALSE);
    }
  }
  // Fire rules day_older event.
  // This may seem grossly inefficient, but testing showed that, even firing
  // the event for ~100 users, takes less than a second to run when there are
  // no rules using this event. With a rule (that adds a role if the user has
  // been a member for over 1,000 days) cron took an extra ~40 seconds to run.
  // Basically, this has no potential to harm a site's performance, unless a
  // rule is configured.
  // Having said this: if there's a better way, please raise a bug report!
  if (module_exists('rules')) {
    $sql  = "SELECT uid FROM {users} u ";
    // ((last cron - created) - (time() - created)) > one day
    $sql .= "WHERE (FLOOR((%d-created)/(60*60*24))-FLOOR((%d-created)/(60*60*24)))>0
      AND uid>0";
    $result = db_query($sql, time(), variable_get('cron_last', time()));
    $reset_user_count = 0;
    while ($update_user = db_fetch_object($result)) {
      rules_invoke_event('user_stats_day_older', $update_user->uid);
    }
  }
  if (variable_get('user_stats_track_ips', TRUE)) {
    // Delete items from the IP log that are past expiry.
    db_query("DELETE FROM {user_stats_ips} WHERE first_seen_timestamp < %d",
      time() - variable_get('user_stats_flush_ips_timer', 31536000));
  }
}

/**
 * Implementation of hook_user().
 */
function user_stats_user($op, &$edit, &$account) {
  // Update login count.
  if ($op == 'login' && variable_get('user_stats_count_logins', TRUE)) {
    user_stats_login_count_update('increment', $account->uid);
  }
  // Update IP Address.
  if ($op == 'login' || $op == 'logout') {
    user_stats_ip_address_update($account->uid, ip_address());
  }
}

/**
 * Helper function to get the last post created by the user.
 *
 * @param $account
 *   User object.
 *
 * @return
 *   Unix timestamp: date of the last post (node or comment).
 */
function _user_stats_last_post($uid) {
  $sql  = "SELECT MAX(created) FROM {node} WHERE status=%d AND uid=%d";
  $post_count_content_types = variable_get('user_stats_included_content_types', array());
  if (!empty($post_count_content_types)) {
    $content_types = "'". implode("','", $post_count_content_types) ."'";
    $where = ' AND type IN ('. $content_types .')';
    $sql .= $where;
  }
  $max_node = db_result(db_query($sql, 1, $uid));
  $sql  = "SELECT MAX(timestamp) FROM {comments} c
    INNER JOIN {node} n ON c.nid=n.nid
    WHERE c.status=%d AND c.uid=%d";
  if (!empty($post_count_content_types)) {
    $where = ' AND n.type IN ('. $content_types .')';
    $sql .= $where;
  }
  $max_comments = db_result(db_query($sql, COMMENT_PUBLISHED, $uid));

  if (is_null($max_node) && is_null($max_comments)) {
    return FALSE;
  }
  else if ($max_node > $max_comments) {
    return $max_node;
  }
  else if ($max_node <= $max_comments) {
    return $max_comments;
  }
}

/**
 * Implementation of hook_views_api().
 *
 * Other Views hooks in user_stats.views.inc.
 */
function user_stats_views_api() {
  return array(
    'api' => '2.0',
    'path' => drupal_get_path('module', 'user_stats') .'/views',
  );
}

/**
 * Actions/Rules hooks and implementing functions.
 *
 * (we don't do Triggers as the API doesn't seem complete -- having to use
 * _trigger_get_hook_aids() for example). Patches welcome for this, as long
 * as they do not use private member functions!
 *
 * Most Rules hooks are in user_stats.rules.inc.
 */

/**
 * Implementation of hook_action_info().
 */
function user_stats_action_info() {
  return array(
    'user_stats_post_count_reset_action' => array(
      'description' => t('Reset user post count'),
      'type' => 'user',
      'configurable' => FALSE,
      'hooks' => array(
        'nodeapi' => array('delete', 'insert', 'update', 'view'),
        'comment' => array('view', 'insert', 'update', 'delete'),
        'user' => array('login', 'logout'),
      ),
    ),
    'user_stats_login_count_reset_action' => array(
      'description' => t('Reset user login count'),
      'type' => 'user',
      'configurable' => FALSE,
      'hooks' => array(
        'nodeapi' => array('delete', 'insert', 'update', 'view'),
        'comment' => array('view', 'insert', 'update', 'delete'),
        'user' => array('login', 'logout'),
      ),
    ),
  );
}

/**
 * Implementation of a Drupal action.
 * Resets a user's post count.
 */
function user_stats_post_count_reset_action(&$object, $context = array()) {
  if (isset($object->uid)) {
    $uid = $object->uid;
  }
  elseif (isset($context['uid'])) {
    $uid = $context['uid'];
  }
  else {
    global $user;
    $uid = $user->uid;
  }
  user_stats_post_count_update('reset', $uid);
}

/**
 * Implementation of a Drupal action.
 * Resets a user's login count.
 */
function user_stats_login_count_reset_action(&$object, $context = array()) {
  if (isset($object->uid)) {
    $uid = $object->uid;
  }
  elseif (isset($context['uid'])) {
    $uid = $context['uid'];
  }
  else {
    global $user;
    $uid = $user->uid;
  }
  user_stats_login_count_update('reset', $uid);
}

/**
 * Implementation of hook_user_stats().
 * For invoking rules module.
 */
function user_stats_user_stats($type, $op, $uid, $value) {
  if (module_exists('rules')) {
    rules_invoke_event('user_stats_'. $type .'_'. $op, $uid, $value);
  }
}

/**
 * Token hook implementations
 */

/**
 * Implementation of hook_token_values().
 */
function user_stats_token_values($type, $object = NULL) {
  switch ($type) {
    case 'user':
    case 'all' :
      if (isset($object)) {
        // Think this is sometimes an array (please raise this as an issue if wrong).
        $object = (object)$object;
        $uid = $object->uid;
      }
      else {
        global $user;
        $uid = $user->uid;
      }

      // Check_plain added as per Greggles suggestion: http://drupal.org/node/166305#comment-665874
      $values['reg-days']               = check_plain(user_stats_get_stats('reg_days', $uid));
      $values['login-days']             = check_plain(user_stats_get_stats('login_days', $uid));
      $values['post-days']              = check_plain(user_stats_get_stats('post_days', $uid));
      $values['post-count']             = check_plain(user_stats_get_stats('post_count', $uid));
      $values['ip-address']             = check_plain(user_stats_get_stats('ip_address', $uid));
      $values['login-count']            = check_plain(user_stats_get_stats('login_count', $uid));
      return $values;
  }
}

/**
 * Implementation of hook_token_list().
 */
function user_stats_token_list($type = 'all') {
  if ($type == 'user' || $type == 'all') {
    $tokens['user']['reg-days']               = t('Number of days since the user registered');
    $tokens['user']['login-days']             = t('Number of days since the user logged in');
    $tokens['user']['post-days']              = t('Number of days since the user posted');
    $tokens['user']['post-count']             = t("User's post count");
    $tokens['user']['ip-address']             = t("User's IP address");
    $tokens['user']['login-count']            = t("User's login count");
    return $tokens;
  }
}

/**
 * Checks whether a statistic is set for a given user.
 *
 * @param $uid
 *   User ID of the user who's statistics should be checked.
 * @param $statistic
 *   What statistic to check.
 */
function user_stats_isset($statistic, $uid) {
  // Dirt simple error checking.
  if (!is_numeric($uid)) {
    trigger_error('UID is not a number.', E_USER_WARNING);
  }

  $result = db_result(db_query("SELECT COUNT(*) FROM {user_stats_values}
    WHERE uid = %d AND name = '%s'", $uid, $statistic));

  if ($result > 0) {
    return TRUE;
  }

  return FALSE;
}

/**
 * Manage the login count of a given user.
 *
 * @param $uid
 *   Unique id of the user who's record should be updated.
 * @param $op
 *   Whether the user login count should be incremented, decremented, or reset.
 *   Possible values are:
 *   'increment'
 *   'decrement'
 *   'reset'
 */
function user_stats_login_count_update($op, $uid) {
  // Dirt simple error checking.
  if (!is_numeric($uid)) {
    trigger_error('UID is not a number.', E_USER_WARNING);
  }

  switch ($op) {
    case 'increment':
      if (user_stats_isset('login_count', $uid)) {
        // Update existing value.
        db_query("UPDATE {user_stats_values} SET value = value + 1
          WHERE name = 'login_count' AND uid = %d", $uid);
      }
      else {
        // If there isn't a value insert it.
        db_query("INSERT INTO {user_stats_values} (name, uid, value)
          VALUES ('login_count', %d, 1)", $uid);
      }
      break;
    case 'decrement':
      if (user_stats_isset('login_count', $uid)) {
        // Update existing value.
        db_query("UPDATE {user_stats_values} SET value = value - 1
          WHERE name = 'login_count' AND uid = %d", $uid);
      }
      else {
        // If there isn't a value insert it.
        db_query("INSERT INTO {user_stats_values} (name, uid, value)
          VALUES ('login_count', %d, 0)", $uid);
      }
      break;
    case 'reset':
      db_query("DELETE FROM {user_stats_values}
        WHERE name = 'login_count' AND uid = %d", $uid);
      break;
  }
  // Flush token cache.
  if (module_exists('token')) {
    token_get_values('user', NULL, TRUE);
  }
  // Flush internal cache.
  user_stats_cache_set('reset', $uid);
  // Allow modules to react to a statistic change.
  module_invoke_all('user_stats', 'login_count', $op, $uid, user_stats_get_stats('login_count', $uid));
}

/**
 * Manage the post count of a given user.
 *
 * @param $uid
 *   Unique id of the user who's record should be updated.
 * @param $op
 *   Whether the user post count should be incremented, decremented, or reset.
 *   The default is to increment. Possible values are:
 *   'increment'
 *   'decrement'
 *   'reset'
 */
function user_stats_post_count_update($op, $uid) {
  // Dirt simple error checking.
  if (!is_numeric($uid)) {
    trigger_error('UID is not a number.', E_USER_WARNING);
  }

  switch ($op) {
    case 'increment':
      if (user_stats_isset('post_count', $uid)) {
        db_query("UPDATE {user_stats_values} SET value = value + 1
          WHERE name = 'post_count' AND uid = %d", $uid);
        // Flush internal cache.
        user_stats_cache_set('reset', $uid);
      }
      else {
        user_stats_post_count_update('reset', $uid);
      }
      break;
    case 'decrement':
      if (user_stats_isset('post_count', $uid)) {
        db_query("UPDATE {user_stats_values} SET value = value - 1
          WHERE name = 'post_count' AND uid = %d", $uid);
        // Flush internal cache.
        user_stats_cache_set('reset', $uid);
      }
      else {
        user_stats_post_count_update('reset', $uid);
      }
      break;
    case 'reset':
      $total_count = 0;
      if (variable_get('user_stats_count_posts', TRUE)) {
        $sql  = "SELECT COUNT(*) FROM {node} WHERE uid = %d AND status = 1";
        $post_count_content_types = variable_get('user_stats_included_content_types', array());
        if (!empty($post_count_content_types)) {
          $content_types = "'". implode("','", $post_count_content_types) ."'";
          $where = ' AND type IN ('. $content_types .')';
          $sql .= $where;
        }
        $node_count = db_result(db_query($sql, $uid));
        $total_count += $node_count;
      }
      if (variable_get('user_stats_count_comments', TRUE)) {
        $sql = "SELECT COUNT(*) FROM {comments} c
          INNER JOIN {node} n ON c.nid = n.nid
          WHERE c.uid = %d AND c.status = 0 AND n.status = 1";
        if (!empty($post_count_content_types)) {
          $where = ' AND n.type IN ('. $content_types .')';
          $sql .= $where;
        }
        $comments_count = db_result(db_query($sql, $uid));
        $total_count += $comments_count;
      }
      db_query("DELETE FROM {user_stats_values}
        WHERE name = 'post_count' AND uid = %d", $uid);
      db_query("INSERT INTO {user_stats_values} (name, uid, value)
        VALUES ('post_count', %d, %d)", $uid, $total_count);
      // Prime the cache, this will be used by module_invoke_all() below.
      user_stats_cache_set('post_count', $uid, $total_count);
      break;
  }
  // Flush token cache
  if (module_exists('token')) {
    token_get_values('user', NULL, TRUE);
  }

  // Allow modules to react to a statistic change.
  module_invoke_all('user_stats', 'post_count', $op, $uid, user_stats_get_stats('post_count', $uid));
}

/**
 * Update the IP address of a given user.
 *
 * The IP address is not updated if it is the same as the last recorded IP,
 * however, if the user has IP address A, then switches to IP address B
 * and back to A again, A will be recorded twice. This is to keep an accurate
 * log of IP addresses used by users.
 *
 * @param $uid
 *   User ID of user who's IP is being updated.
 * @param $ip_address
 *   IP address to assign to user.
 */
function user_stats_ip_address_update($uid, $ip_address) {
  // Dirt simple error checking.
  if (!is_numeric($uid)) {
    trigger_error('UID is not a number.', E_USER_WARNING);
  }

  // Don't bother recording IPs of anonymous users, and don't record any
  // addresses if the config form tells us not to.
  if ($uid == 0 || !variable_get('user_stats_track_ips', TRUE)) {
    return;
  }

  $query = db_query_range("SELECT ip_address
    FROM {user_stats_ips} WHERE uid = %d
    ORDER BY first_seen_timestamp DESC", $uid, 0, 1);
  if ($ip_address != db_result($query)) {
    // Reset internal cache.
    user_stats_cache_set('reset', $uid);
    db_query("INSERT INTO {user_stats_ips} (uid, ip_address, first_seen_timestamp)
      VALUES (%d, '%s', %d)", $uid, $ip_address, time());
    // Allow modules to react to an IP address change.
    module_invoke_all('user_stats', 'ip_address', 'insert', $uid, $ip_address);
  }
}

/**
 * Resets statistics. Full stop.
 *
 * @param $statistic
 *   The name of the statistic to be reset.
 *   Corresponds with {user_stats_values}.name.
 */
function user_stats_reset_counts($statistic) {
  db_query("DELETE FROM {user_stats_values} WHERE name = '%s'", $statistic);
}
