<?php

// Check if user is an admin
function is_admin($user_id=0)
{

    // Use logged in user's ID
    if ($user_id == 0 && @isset($_SESSION["user_id"])) {
        $user_id = $_SESSION["user_id"];
    }

    $level = get_user_attr('level', $user_id, 0);
    if ($level == 1) {
        return true;
    }

    return false;
}


function add_user($username, $password, $name, $email, $enabled, $level = USER_LEVEL_NONPRIV) {

    global $db;

    if (get_user_id($username))
        return false;

    $db->query('INSERT INTO users (username, password, name, email, enabled, level) VALUES (:username, :password, :name, :email, :enabled, :level)');
    $db->bind(':username', $username, PDO::PARAM_STR);
    $db->bind(':password', hash('sha256', $password), PDO::PARAM_STR);
    $db->bind(':name', $name, PDO::PARAM_STR);
    $db->bind(':email', $email, PDO::PARAM_STR);
    $db->bind(':enabled', $enabled, PDO::PARAM_INT);
    $db->bind(':level', $level, PDO::PARAM_INT);

    $user_id = 0;
    if ($db->exec())
        $user_id = $db->last_insert_id();

    if ($user_id > 0) {

        set_user_attr('backend_ticket', random_string(32), $user_id);
        set_user_attr('password_set_time', date("Y-m-d H:i:s", time()), $user_id);

        $cbargs = array(
            'user_id' => $user_id,
            'username' => $username,
            'email' => $email,
            'level' => $level,
            );

        do_callbacks(CALLBACK_USER_CREATED, $cbargs);

        return $user_id;
    }

    return false;
}


function delete_user($user_id) {

    global $db;

    // can't delete a non-numeric user id, or nagiosadmin (anything below that is invalid as well)
    if (!is_numeric($user_id) || $user_id <= 1)
        return false;

    // can't delete your own user, either
    if (!empty($_SESSION['user_id']) && $_SESSION['user_id'] == $user_id)
        return false;

    // shouldn't delete invalid users!
    if (!is_valid_user_id($user_id))
        return false;

    // check if we are cloud
    if (is_v2_license_type('cloud')) {
        $username = get_username($user_id);
        if ($username == "nagiosadmin") {
            return false;
        }
    }

    // get rid of all the user meta
    delete_user_meta('', $user_id, $all_meta = true);

    $db->query('DELETE FROM users WHERE user_id = :user_id');
    $db->bind(':user_id', $user_id, PDO::PARAM_INT);

    return $db->exec();
}


/**
 * @param string $username
 * @param string $password
 * @param string[ref] &$error - string to set messages with [messages to show users]
 * @param array[ref] &$debug - array to set messages with [detailed messages for backend use, defined in constants under CHECKCRED_FAILED]
 *
 * @return bool based on successful authentication
 */
// TODO: fix this. add logging
function check_login_credentials($username, $password, &$error, &$debug) {

    $default_error = _('Invalid username or password.');

    // clear messages
    $error = '';
    $debug = array();

    // get the user id (potentially) and check for validity
    $user_id = get_user_id($username);
    if ($user_id === false) {
        $debug[] = CHECKCRED_FAILED_GET_USER_ID;
        $error = $default_error;
        return false;
    }

    // check if our account is locked
    if (get_user_attr('locked', $user_id) == 1) {
        $debug[] = CHECKCRED_FAILED_LOCKED;
        $error = _('This account is locked. Please contact your administrator.');
        return false;
    }

    // check if the account is enabled
    if (get_user_attr('enabled', $user_id) == 0) {
        $debug[] = CHECKCRED_FAILED_DISABLED;
        $error = _('This account is disabled. Please contact your administrator.');
        return false;
    }

    // try other authentication methods first
    $auth_ok = false;
    $cbargs = array(
        'credentials' => array(
            'username' => $username,
            'password' => $password,
        ),
        'login_ok' => 0,
        'debug' => array(),
        'error' => array(),
    );

    do_callbacks(CALLBACK_PROCESS_AUTH_INFO, $cbargs);

    $debug = array_merge($debug, $cbargs['debug']);
    $error = $cbargs['error'];

    // check the callback args to see if any component has changed them
    if ($cbargs['login_ok'] == 1) {
        $auth_ok = true;
        return true;
    } else {
        $debug[] = CHECKCRED_FAILED_EXTAUTH;
    }

    // check if this user is set to allow local auth
    $allow_local = get_user_meta('allow_local', 0, $user_id);
    $auth_type = get_user_meta('auth_type', 'local', $user_id);

    // always allow nagiosadmin to login via local
    if ($auth_type != 'local' && $allow_local != 1 && $username != 'nagiosadmin') {
        $debug[] = CHECKCRED_FAILED_SKIPLOCAL;
        $error = $default_error;
        return false;
    }

    // check to make sure the password matches
    $pwhash = get_user_attr('password', $user_id);
    if ($pwhash == hash('sha256', $password)) {
        log_info(LOG_TYPE_SECURITY, _('User authenticated against Nagios Fusion local credentials'), 0, $user_id);
        return true;
    } else if ($pwhash == md5($password)) {
        // Removing this later but validate against md5 passwords too so Bryan's Fusion doesn't break :) may also be
        // useful if we write some sort of script to import old users into Fusion 4
        log_info(LOG_TYPE_SECURITY, _('User authenticated against deprecated Nagios Fusion local credentials'), 0, $user_id);
        return true;
    } else {
        $debug[] = CHECKCRED_FAILED_INVALIDPASS;
        $error = $default_error;
    }

    return false;
}

/**
 * get_user_id() - return a user_id of a specific username
 * @param $username - the user to search for
 * @return mixed false if not found , the user_id (int) otherwise
 */
function get_user_id($username) {

    global $db;

    if (empty($username))
        return false;

    $db->query('SELECT user_id FROM users WHERE username = :username LIMIT 1');
    $db->bind(':username', $username, PDO::PARAM_STR);
    $db->exec();

    $rows = $db->fetch_rows();
    if (count($rows) == 1) {

        $value = $rows[0]['user_id'];
        return intval($value);
    }

    return false;
}


/**
 * get_username() - return a username of a specific user_id
 * @param $userid - the userid to search for
 * @return mixed (bool)false if not found [or invalid data], the (string)username otherwise
 */
function get_username($user_id) {

    global $db;

    if (!is_numeric($user_id))
        return false;

    $db->query('SELECT username FROM users WHERE user_id = :user_id LIMIT 1');
    $db->bind(':user_id', $user_id);
    $db->exec();

    $rows = $db->fetch_rows();
    if (count($rows) == 1) {

        $value = $rows[0]['username'];
        return $value;
    }

    return false;
}


/**
 * get_user_meta_session_vars() - set session variables for currently logged in user
 * @param bool $overwrite - overwrite current session vars if true
 */
function get_user_meta_session_vars($overwrite = false) {

    global $db;

    if (!isset($_SESSION['user_id']))
        return null;

    $obj_type = META_TYPE_USER;
    $db->query("SELECT * FROM meta WHERE obj_type = $obj_type AND obj_id = :user_id AND autoload = 1");
    $db->bind(':user_id', intval($_SESSION['user_id']));
    $db->exec();

    $rows = $db->fetch_rows();
    if (count($rows) > 0) {

        foreach ($rows as $row) {
            foreach ($row as $key => $val) {

                if ($key == 'user_id')
                    continue;

                if ($overwrite || !isset($_SESSION[$key]))
                    $_SESSION[$key] = $val;
            }
        }
    }
}


/**
 * get_user_meta() - pull a value from usermeta (alias to meta)
 * @param string $keyname - the key of the usermeta to pull the value for
 * @param mixed $default - if the query happens and doesn't get a result the function will return this value
 * @param int $user_id - if 0, set to current user (the user to pull the value for)
 * @return mixed false if problem with user_id is detected, the value if everything is successful, and $default if nothing found
 */
function get_user_meta($keyname, $default = null, $user_id = 0) {

    global $db;

    if ($user_id == 0 && isset($_SESSION['user_id']))
        $user_id = $_SESSION['user_id'];

    if (!is_numeric($user_id) || $user_id == 0)
        return false;

    $obj_type = META_TYPE_USER;
    $db->query("SELECT meta_value FROM meta WHERE obj_type = $obj_type AND obj_id = :user_id AND meta_key = :keyname LIMIT 1");
    $db->bind(':user_id', $user_id);
    $db->bind(':keyname', $keyname);
    $db->exec();

    $rows = $db->fetch_rows();
    if (count($rows) == 1) {

        $value = $rows[0]['meta_value'];

        return $value;
    }

    return $default;
}


/**
 * set_user_meta() - set a value in usermeta (alias to meta)
 * @param string $keyname - the key of the usermeta to set
 * @param string $keyvalue - the value to set
 * @param bool $autoload - refers to whether this usermeta should be loaded and stored into session data on login (and when this function is called)
 * @param int $user_id - if 0, set to current user (the user to pull the value for)
 * @return bool false if problem with user_id is detected, or the return value from the sql query execution
 */
function set_user_meta($keyname, $keyvalue, $autoload = true, $user_id = 0) {

    global $db;

    if ($user_id == 0 && isset($_SESSION['user_id']))
        $user_id = $_SESSION['user_id'];

    if (!is_numeric($user_id) || $user_id == 0)
        return false;

    $meta_exists = get_user_meta($keyname, null, $user_id);
    $sql = '';

    $obj_type = META_TYPE_USER;
    if ($meta_exists === null) {
        $sql = "INSERT INTO meta (obj_type, obj_id, meta_key, meta_value, autoload) VALUES ($obj_type, :user_id, :keyname, :keyvalue, :autoload)";
    } else {
        $sql = "UPDATE meta SET meta_value = :keyvalue, autoload = :autoload WHERE obj_type = $obj_type AND obj_id = :user_id AND meta_key = :keyname LIMIT 1";
    }

    $db->query($sql);
    $db->bind(':user_id', $user_id);
    $db->bind(':keyname', $keyname);
    $db->bind(':keyvalue', $keyvalue);
    $db->bind(':autoload', intval($autoload));
    $exec = $db->exec();

    if ($exec && $autoload)
        $_SESSION[$keyname] = $keyvalue;

    return $exec;
}


function delete_user_meta($keyname, $user_id = 0, $all_user_meta = false) {

    global $db;

    if ($user_id == 0 && isset($_SESSION['user_id']))
        $user_id = $_SESSION['user_id'];

    if (!is_numeric($user_id) || $user_id == 0)
        return false
;
    // if we are deleting everything then we don't need to specify a keyname
    $and_keyname = $all_user_meta ? '' : ' AND meta_key = :keyname';

    $obj_type = META_TYPE_USER;
    $db->query("DELETE FROM meta WHERE obj_type = $obj_type AND obj_id = :user_id $and_keyname");
    $db->bind(':user_id', $user_id);
    if (!$all_user_meta)
        $db->bind(':keyname', $keyname);

    return $db->exec();
}


function is_valid_user($identifier) {

    $value = false;

    // check what kind of data we're working with and call the appropriate function
    if (is_numeric($identifier)) {

        $value = get_username($identifier);

    } else if (is_string($identifier)) {

        $value = get_user_id($identifier);
    }

    return !($value === false);
}


function is_valid_user_id($user_id) {

    return is_valid_user($user_id);
}


function get_users() {

    global $db;

    $db->query('SELECT * FROM users');
    $db->exec();

    return $db->fetch_rows();
}


function masquerade_as_user_id($user_id = -1) {

    // only admins can masquerade
    if (is_admin() == false)
        return;

    if (!is_valid_user_id($user_id))
        return;

    $original_user = $_SESSION['username'];

    global $user_attributes;

    $username = get_user_attr('username', $user_id);
    
    deinit_session();
    init_session();

    // set session variables
    $_SESSION['user_id'] = $user_id;
    $_SESSION['username'] = $username;

    // load user session variables (e.g. preferences)
    get_user_meta_session_vars(true);

    // get the new user attributes
    unset($user_attributes);
    get_user_attributes();

    // TODO: logging here
    // log it
    //send_to_audit_log("User '" . $original_user . "' masqueraded as user '" . $username . "'", AUDITLOGTYPE_SECURITY);
}


/**
 * is_valid_user_attr() - determine if a specified attr is valid
 * @return bool
 */
function is_valid_user_attr($attr) {

    return is_valid_column_of_table($attr, 'users');
}


/**
 *
 */
function get_user_attr($attr, $user_id = 0, $default = null) {

    global $user_attributes;

    if (isset($user_attributes[$attr]) && $user_id == 0) {

        return $user_attributes[$attr];

    } else {

        global $db;

        if (!is_valid_user_attr($attr))
            return null;

        if ($user_id == 0 && isset($_SESSION['user_id']))
            $user_id = $_SESSION['user_id'];

        if (!is_numeric($user_id) || $user_id == 0)
            return null;

        $db->query("SELECT {$attr} AS attr FROM users WHERE user_id = :user_id LIMIT 1");
        $db->bind(':user_id', $user_id, PDO::PARAM_INT);
        $db->exec();

        $rows = $db->fetch_rows();
        if (count($rows) == 1) {

            $value = $rows[0]['attr'];
            if (isset($_SESSION['user_id']) && $user_id == $_SESSION['user_id'])
                $user_attributes[$attr] = $value;

            return $value;
        }
    }

    return $default;
}


/**
 *
 */
function set_user_attr($attr, $value, $user_id = 0) {

    global $user_attributes;
    global $db;

    if (!is_valid_user_attr($attr))
        return null;

    // if user_id is set to 0 or (session id is set and specified user id is session id)
    if ($user_id == 0 || (isset($_SESSION['user_id']) && $user_id == $_SESSION['user_id']))
        $user_attributes[$attr] = $value;

    if ($user_id == 0 && isset($_SESSION['user_id']))
        $user_id = $_SESSION['user_id'];

    if (!is_numeric($user_id) || $user_id == 0)
        return null;

    $db->query("UPDATE users SET {$attr} = :value WHERE user_id = :user_id LIMIT 1");
    $db->bind(':value', $value);
    $db->bind(':user_id', $user_id, PDO::PARAM_INT);
    $db->exec();
}


// TODO: use this EVERYWHERE
/**
 * check_user_id() - helper to determine if a good user id was passed to a function that can be used
 * define good: the user_id isn't checked for validity in the database, but this prevents a non-admin user
 *   from specifying another user's id
 * if there is no session, we assume the user id is good to go
 * if user_id = 0 then try and use the sessions current user id
 * if user_id is nonzero, and we're an admin, its good to go
 * if user_id is nonzero and we aren't an admin, but it is our user_id, then we're good to go
 * @param int $user_id - the user id to check for
 * @return mixed int|bool - bool false on failure, the integer of the user id that is okay to use
 */
function check_user_id($user_id) {

    $user_id = intval($user_id);

    if (!isset($_SESSION['user_id']))
        return $user_id;

    if ($user_id == 0)
        return $_SESSION['user_id'];

    if ($user_id != 0 && is_admin())
        return $user_id;

    if ($user_id == $_SESSION['user_id'])
        return $user_id;

    return false;
}


// Get the user's number format for returning a formatted number
function get_user_number_format($user_id = 0) {

    $number_format = get_option('default_number_format', NF_2);
    $number_format = get_user_meta('number_format', $number_format, $user_id);

    return $number_format;
}


/**
 * get_admin_quantity() - Queries the database to get the administrator count. 
 * @return mixed int|bool - On successful query, returns integer count of administrators. On failure, returns boolean false.
 */
function get_admin_quantity() {
    global $db;
    $db->query("SELECT COUNT(user_id) FROM users WHERE level=1");
    $db->exec();
    $rows = $db->fetch_rows();

    return $rows[0]["COUNT(user_id)"];
}


function get_authlevels()
{
    $levels = array(
        USER_LEVEL_NONPRIV => _("User"),
        USER_LEVEL_ADMIN => _("Admin")
    );
    return $levels;
}


////////////////////////////////////////////////////////////////////////////////
// AUTHENTICATION TOKENS
//////////////////////////////////////////////////////////////////////////////// 


/**
 * Creates a single-use authentication token for login/rapid response
 *
 * @param   int     $user_id        User ID to create token for
 * @param   int     $valid_until    Unix timestamp for when token becomes invalid
 * @param   array   $restrictions   Pages that should be allowed to be viewed, empty by default (so no restriction)
 * @return  string                  Single-use auth token
 */
function user_generate_auth_token($user_id=0, $valid_until=0, $restrictions='')
{
    global $db;

    if (empty($user_id)) {
        $user_id = $_SESSION['user_id'];
    }

    // Generate token and set time limit for auth (5 mins by default)
    $auth_token = sha1(uniqid());
    if (empty($valid_until)) {
        $valid_until = time() + (5 * 60);
    }

    // Set restrictions if there are any
    if (!empty($restrictions) || is_array($restrictions)) {
        $restrictions = base64_encode(serialize($restrictions));
    }

    $db->query("INSERT INTO auth_tokens (auth_user_id, auth_token, auth_valid_until, auth_expires_at, auth_restrictions)
                VALUES (:user_id, :auth_token, FROM_UNIXTIME(:valid_until), null, :restrictions)");
    $db->bind(':user_id', $user_id);
    $db->bind(':auth_token', $auth_token);
    $db->bind(':valid_until', $valid_until);
    $db->bind(':restrictions', $restrictions);
    $db->exec();

    return $auth_token;
}


/**
 * Verifies that an auth token is valid and has not yet expired.
 *
 * @param   string  $auth_token     Auth token
 * @return  bool                    True if valid (non-expired) auth token
 */
function user_is_valid_auth_token($auth_token)
{
    global $db;

    // Get token from the DB to validate
    $sql = "SELECT *, UNIX_TIMESTAMP(auth_valid_until) as valid_until_ts FROM auth_tokens WHERE auth_token = :auth_token AND auth_used = '0'";
    $db->query($sql);
    $db->bind(':auth_token', $auth_token);
    $db->exec();
    $rows = $db->fetch_rows();

    // Check to see if the auth token is valid
    if (!empty($rows)) {
        $tk = $rows[0];
        if (time() > $tk['valid_until_ts']) {
            return false;
        }
        return true;
    }

    return false;
}


/**
 * Get the user ID from the auth token specified.
 *
 * @param   string  $auth_token     Auth token
 * @return  int                     User ID or false on failure
 */
function user_get_auth_token_user_id($auth_token)
{
    global $db;

    $sql = "SELECT auth_token_id, auth_user_id FROM auth_tokens WHERE auth_token = :auth_token";
    $db->query($sql);
    $db->bind(':auth_token', $auth_token);
    $db->exec();
    $rows = $db->fetch_rows();

    if (!empty($rows)) {
        return $rows[0]['auth_user_id'];
    }

    return false;
}


/**
 * Sets an auth token's session ID in the database. Also automatically sets
 * the auth_used value to 1.
 *
 * @param   string  $auth_token     Auth token
 * @return  bool                    True if successful
 */
function user_set_auth_token_used($auth_token)
{
    global $db;

    $db->query("UPDATE auth_tokens SET auth_used = '1' WHERE auth_token = :auth_token");
    $db->bind(':auth_token', $auth_token);
    $db->exec();

    return true;
}

/**
 * Returns the current number of admin accounts. Used for
 * making sure the last administrative account is never
 * deleted.
 *
 * @return int     Number of current admin accounts
*/
function get_number_of_admins() {

    global $db;

    $db->query("SELECT COUNT(*) as number_of_admins FROM users WHERE (level = 1 AND enabled = 1)");
    $result = $db->exec();
    $raw_data = $db->fetch_rows();
    $number_of_admins = $raw_data[0]['number_of_admins'];
    $number_of_admins = intval($number_of_admins);

    return $number_of_admins;

}


////////////////////////////////////////////////////////////////////////////////
// SESSION DATABASE
////////////////////////////////////////////////////////////////////////////////


/**
 * Generates a user session in the database given the current session
 * or session passed in.
 *
 * @param   int     $user_id        User ID
 * @param   string  $session_id     Session ID (php generated)
 * @return  int                     Session ID
 */
function user_generate_session($user_id=0, $session_id=null)
{
    global $db;
    $address = $_SERVER['REMOTE_ADDR'];

    if (empty($user_id)) {
        $user_id = $_SESSION['user_id'];
    }

    if ($session_id === null) {
        $session_id = session_id();
    }

    $db->query("INSERT INTO users_sessions (session_phpid, session_created, session_user_id, session_address)
                VALUES (:session_id, FROM_UNIXTIME(:ts), :user_id, :address)");
    $db->bind(':session_id', $session_id, PDO::PARAM_STR);
    $db->bind(':ts', time(), PDO::PARAM_INT);
    $db->bind(':user_id', $user_id, PDO::PARAM_INT);
    $db->bind(':address', $address, PDO::PARAM_STR);
    $db->exec();

    return $db->last_insert_id();
}


/**
 * Updates the current user session in the database. This is what actually
 * tracks where the user is inside of Fusion.
 */
function user_update_session($user_id=0, $session_id=null)
{
    global $db;
    $address = $_SERVER['REMOTE_ADDR'];
    $page = str_replace('//', '/', $_SERVER['SCRIPT_NAME']);

    if ($session_id === null) {
        $session_id = session_id();
    }

    // Pages that don't need to be recorded (because they are called often)
    $skip_pages = array('/nagiosfusion/index.php', '/nagiosfusion/ajaxhelper.php', '/nagiosfusion/ajaxproxy.php');
    if (in_array($page, $skip_pages)) {

        // Update last active time
        $db->query("UPDATE users_sessions SET session_last_active = FROM_UNIXTIME(:ts) WHERE session_phpid = :session_id");
        $db->bind(':ts', time(), PDO::PARAM_INT);
        $db->bind(':session_id', $session_id, PDO::PARAM_STR);
        $db->exec();

        return;
    }

    // Get the current page data that we will store in the DB for later
    $https = 0;
    if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']) { // HTTPS isn't always set on non-HTTPS servers.
        $https = 1;
    }
    $data = array('request_method' => $_SERVER['REQUEST_METHOD'],
                  'request_time' => $_SERVER['REQUEST_TIME'],
                  'request_query_string' => $_SERVER['QUERY_STRING'],
                  'request_https' => $https);
    $data = base64_encode(serialize($data));

    // Update session database
    $db->query("UPDATE users_sessions SET session_address = :address, session_page = :page, session_data = :data, session_last_active = FROM_UNIXTIME(:ts)
                WHERE session_phpid = :session_id");

    $db->bind(':address', $address, PDO::PARAM_STR);
    $db->bind(':page', $page, PDO::PARAM_STR);
    $db->bind(':data', $data, PDO::PARAM_STR);
    $db->bind(':ts', time(), PDO::PARAM_INT);
    $db->bind(':session_id', $session_id, PDO::PARAM_STR);

    $db->exec();
}


/**
 * Remove a session from the sessions database based on php session_id
 *
 * @param   string  $session_id     The session ID to remove
 */
function user_remove_session($session_id)
{
    global $db;

    $db->query("DELETE FROM users_sessions WHERE session_phpid = :session_id");
    $db->bind(':session_id', $session_id, PDO::PARAM_STR);
    $db->exec();
}

////////////////////////////////////////////////////////////////////////////////
// TWO FACTOR AUTHENTICATION
////////////////////////////////////////////////////////////////////////////////


/**
 * Creates and sents a two factor auth token to a user
 *
 * @param   string  $username   The username of the user logging in
 */
function send_two_factor_token($username)
{
    // Get user_id from username
    $user_id = get_user_id($username);
    if (empty($user_id)) { return false; }

    // Grab the details we need
    $email = get_user_attr('email', $user_id);
    $name = get_user_attr('name', $user_id);

    // Generate 5 digit random token
    $token = rand(10000, 99999);

    // Set expiration time (convert minutes to seconds)
    $two_factor_timeout = get_option('two_factor_timeout', 15); 
    $expires = time() + ($two_factor_timeout * 60);

    $data = array('token' => $token,
                  'expires' => $expires);

    // Save token in the DB
    $token_data = base64_encode(serialize($data));
    set_user_meta('two_factor_token', $token_data, false, $user_id);

    // Create the message body
    $message = _("Hello")." $name,\n\n"._("To continue logging in, please enter the token below").":\n\n$token\n\n"._("Note").": "._("If you close your browser window or too much time has elapsed, you may need to start the process over again.")."\n\n"._("Contact your Nagios Fusion admin if you are unsure what this message is for.");

    // Send the token
    $data = array(
        'from'      => 'Nagios Fusion',
        'to' => $email,
        'subject' => _('Email Confirmation'),
        'message' => $message
    );
    send_email($data);
}


/**
 * Checks to see if the token passed matches the token for the user
 *
 * @param   string      $username   The user's username
 * @param   string      $token      The two factor auth token
 * @return  bool|null               True if token matches the user's token or null if expired
 */
function verify_two_factor_auth($username, $token)
{
    // Get user_id from username
    $user_id = get_user_id($username);
    if (empty($user_id) || empty($token)) {    
        return false; 
    }

    // Get token
    $token_data = get_user_meta('two_factor_token', null, $user_id);
    if (empty($token_data)) {
        return false; 
    }
    $token_data = unserialize(base64_decode($token_data));

    // Check if token is expired
    if (time() > $token_data['expires']) {
        delete_user_meta('two_factor_token', $user_id);
        return false;
    }

    // Check for token validity
    if ($token == $token_data['token']) {
        return  true;
    }

    return false;
}


/**
 * Checks a TFV cookie token against the tokens that are stored in
 * the user's meta data. Also removes any old tokens before doing
 * the TFV check.
 *
 * @param   string      $username   The user's username
 * @return  bool    True if token is valid and not expired
 */
function user_tfv_verify($username)
{
    // Get user_id from username
    $user_id = get_user_id($username);
    if (empty($user_id)) { return false; }

    $tfv = 'nagiosfusion_'.$user_id.'_tfv';
    if (!@isset($_COOKIE[$tfv])) { return false; }
    $token = $_COOKIE[$tfv];

    // Check the list of tokens
    $tfv_tokens = user_tfv_get_tokens($user_id);

    if (!empty($tfv_tokens)) {
        if (@isset($tfv_tokens[$token])) {

            // Check expiration
            $expires = $tfv_tokens[$token];
            if (time() < $expires) {
                return true;
            } else {
                user_tfv_delete_token($user_id, $token);
            }

        }
    }

    return false;
}


/**
 * Generates a new TFV token and sets the cookie value for the token
 * for the user that is specified by $username.
 *
 * @param   string      $username   The user's username
 */
function user_tfv_generate_token($username)
{
    // Get user_id from username
    $user_id = get_user_id($username);
    if (empty($user_id)) { return false; }

    // Generate a TFV token and save it for the user
    $tfv_token = random_string(16);

    // Set the expires time
    $two_factor_cookie_timeout = get_option('two_factor_cookie_timeout', 90);
    $expires = time() + ($two_factor_cookie_timeout * (60 * 60 * 24));

    // Add token to user meta
    user_tfv_add_token($user_id, $tfv_token, $expires);

    // Check if the cookie should be secure or not
    $secure = false;
    if (!empty($_SERVER['HTTPS'])) {
        $secure = true;
    }

    setcookie('nagiosfusion_'.$user_id.'_tfv', $tfv_token, $expires, '/', '', $secure, true);
}


/**
 * Add a TFV token to the user's token list
 *
 * @param   int     $user_id    User ID
 * @param   string  $token      TFV token
 * @param   string  $expires    Timestamp of expiration
 */
function user_tfv_add_token($user_id, $token, $expires)
{
    $tfv_tokens = user_tfv_get_tokens($user_id);

    // Add TFV token to the list
    $tfv_tokens[$token] = $expires;

    $encoded = base64_encode(serialize($tfv_tokens));
    set_user_meta('tfv_tokens', $encoded, false, $user_id);
}


/**
 * Delete a TFV token to the user's token list
 *
 * @param   int     $user_id    User ID
 * @param   string  $token      TFV token
 */
function user_tfv_delete_token($user_id, $token)
{
    $tfv_tokens = user_tfv_get_tokens($user_id);

    // Remove token and set the values back into the users's token array
    if (@isset($tfv_tokens[$token])) {
        unset($tfv_tokens[$token]);
        $encoded = base64_encode(serialize($tfv_tokens));
        set_user_meta('tfv_tokens', $encoded, false, $user_id);
    }
}


/**
 * Get an array of TFV tokens from a specified user
 *
 * @param   int     $user_id    User ID
 * @return  array               Array of TFV tokens
 */
function user_tfv_get_tokens($user_id=0)
{
    $tfv_raw = get_user_meta('tfv_tokens', null, $user_id);
    if (!empty($tfv_raw)) {
        $tfv_tokens = unserialize(base64_decode($tfv_raw));
    } else {
        $tfv_tokens = array();
    }
    return $tfv_tokens;
}