<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2008 - 2016 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flUserMgr Class
 *
 * Class to manage users and their profile fields.
 *
 * @package fusionLib
 */
class flUserMgr {

	/**
	 * The database object to use.
	 *
	 * @var flDb
	 */
	protected $db;

	/**
	 * The user manager adapter.
	 *
	 * @var flUserAdapter
	 */
	protected $adapter;

	/**
	 * Construct the object.
	 *
	 * @param flDb $db The database object.
	 * @param flUserAdapter The user manager adapter.
	 */
	function __construct($db, $adapter = null) {
		$this->adapter = $adapter === null ? new flUserAdapter($db) : $adapter;
		$this->db = $db;
	}

	/**
	 * Get the user authentication adapter in use.
	 *
	 * @return flUserAdapter The adapter.
	 */
	function getAdapter() {
		return $this->adapter;
	}

	/**
	 * Test if the given username is already used.
	 *
	 * @param string $username The username to test.
	 * @return int 0 if the username is not used; else non-zero.
	 */
	function isUsernameUsed($username) {
		return $this->adapter->check('username', $username);
	}

	/**
	 * Test if the given email address is already used.
	 *
	 * @param string $email The email address to test.
	 * @return int 0 if the email is not used; else non-zero.
	 */
	function isEmailUsed($email) {
		return $this->adapter->check('email', $email);
	}

	/**
	 * Create a user within the database.
	 *
	 * @param string $username The username or null if creating an unnamed user.
	 * @param string $email The users email address.
	 * @param string $password The users password or array of external service ['service', 'authcode'].
	 * @param string $state The state to create the user in: 'active','disabled','pending'
	 * @param string $tz The timezone string for the user.
	 * @param string $displayName The display name for the user.
	 * @param string $bio The users bio.
	 * @return int The user id or 0 if the user could not be created.
	 */
	function create($username, $email, $password, $state, $tz, $displayName = '', $bio = '') {
		return $this->adapter->create($username, $displayName, $email, is_array($password) ? $password : md5($password), $state, $tz, null, $bio);
	}

	/**
	 * Delete the user.
	 *
	 * @param int $uid The id of the user to delete
	 */
	function delete($uid) {
		$this->adapter->delete($uid);
	}

	/**
	 * Set a new password for the user.
	 *
	 * @param int $uid The id of the user to set the password for.
	 * @param string $password The new password for the user.
	 * @return bool true if the password was set for the user; else false.
	 */
	function setPassword($uid, $password) {
		return $this->adapter->change($uid, 'passwd', md5($password));
	}

	/**
	 * Set a new email address for the user.
	 *
	 * @param int $uid The id of the user to set the email for.
	 * @param string $email The email address to set for the user.
	 * @return bool true if the email address was updated; else false.
	 */
	function setEmail($uid, $email) {
		return $this->adapter->change($uid, 'email', $email);
	}

	/**
	 * Set a timezone for the user.
	 *
	 * @param int $uid The id of the user to set the email for.
	 * @param string $tz The new timezone.
	 * @return bool true if the email address was updated; else false.
	 */
	function setTimezone($uid, $tz) {
		return $this->adapter->change($uid, 'tz', $tz);
	}

	/**
	 * Change the username of an existing user.
	 *
	 * @param int $uid The id of the user to update.
	 * @param string $username The new username of the user.
	 * @return bool true if the username was updated; else false.
	 */
	function changeUsername($uid, $username) {
		return $this->adapter->change($uid, 'username', $username);
	}

	/**
	 * Change the display name of an existing user.
	 *
	 * @param int $uid The id of the user to update.
	 * @param string $name The new display name for the user.
	 * @return bool true if the username was updated; else false.
	 */
	function changeDisplayName($uid, $name) {
		return $this->adapter->change($uid, 'displayname', $name);
	}

	/**
	 * Change the bio of an existing user.
	 *
	 * @param int $uid The id of the user to update.
	 * @param string $bio The new bio for the user.
	 * @return bool true if the username was updated; else false.
	 */
	function changeBio($uid, $bio) {
		return $this->adapter->change($uid, 'bio', $bio);
	}

	/**
	 * Build the query to find a user.
	 *
	 * @param string $sel The item(s) to return.
	 * @param string $term The term to search for.
	 * @param array $where The fields to search in or ALL to search in ALL.
	 * @param bool $exact true if exact matching should be used; else false for loose.
	 * @param int $role ID of role users must be a member of.
	 * @return array [0] The query string, [1] The query parameters
	 */
	protected function _buildFind($sel, $term, $where, $exact, $role) {

		// Build list of fields to search
		$fields = array();
		foreach($where as $field) {
			if($field == 'ALL') {
				$fields[] = 'username';
				$fields[] = 'email';
			}
			else
				$fields[] = $field;
		}

		$fields = array_unique($fields);
		if(!count($fields))
			return array();

		// Build the query
		$sql = 'SELECT ' . $sel . ' FROM fl_users ';
		if($role)
			$sql .= 'LEFT JOIN fl_usersroles USING(uid) ';
		$sql .= 'WHERE (';
		if($exact)
			$sql .= join('=? || ', $fields) . '=?';
		else {
			$term = '%' . str_replace(array('_', '%'), array('\_', '\%'), $term) . '%';
			$sql .= join(' LIKE ? || ', $fields) . ' LIKE ?';
		}
		$sql .= ')';

		$term = array_fill(0, count($fields), $term);
		if($role) {
			$sql .= ' && roleid=?';
			$term[] = $role;
		}

		return array($sql . ' ORDER BY username', $term);
	}

	/**
	 * Find a user or users matching a search.
	 *
	 * @param string $term The term to search for.
	 * @param array $where The fields to search in or ALL to search in ALL.
	 * @param bool $exact true if exact matching should be used; else false for loose.
	 * @param int $start The start position in the results to return.
	 * @param int $limit The number of results to return.
	 * @param int $role ID of role users must be a member of.
	 * @return array The list of uids found.
	 */
	function find($term, $where, $exact, $start = 0, $limit = 1000, $role = null) {
		list($sql, $params) = $this->_buildFind('uid', $term, $where, $exact, $role);

		// Do query
		$uids = array();
		$result = $this->db->query($sql . " LIMIT $start, $limit", $params);
		while($row = $result->fetch())
			$uids[] = $row['uid'];

		return $uids;
	}

	/**
	 * Find the number of users that match a search.
	 *
	 * @param string $term The term to search for.
	 * @param array $where The fields to search in or ALL to search in ALL.
	 * @param bool $exact true if exact matching should be used; else false for loose.
	 * @param int $role ID of role users must be a member of.
	 * @return int The number of users matched.
	 */
	function numFindMatches($term, $where, $exact, $role = null) {
		list($sql, $params) = $this->_buildFind('COUNT(*)', $term, $where, $exact, $role);
		return $this->db->fetchOne($sql, $params);
	}

	/**
	 * Prune accounts that are pending email activation.
	 *
	 * @param int $before The time in seconds relative to now before which accounts are pruned newer accounts are left untouched.
	 */
	function prunePendingActivation($before) {

		// Make sure we're using flUserAdapter
		$adapter = $this->remoteAuth() ? new flUserAdapter($this->db) : $this->adapter;

		$now = new flDate();
		$now->setTimestamp($now->getTimestamp() - $before);
		$result = $this->db->query('SELECT uid FROM fl_users WHERE state=? && added<?', 'pending', $now->toString());
		while($row = $result->fetch())
			$adapter->delete($row['uid']);
	}

	/**
	 * Get the information about a user.
	 *
	 * @param int $uid The user id to get information about.
	 * @return array Array holding the information about the user or false on error.
	 */
	function getInfo($uid) {
		return $this->adapter->getInfo($uid);
	}

	/**
	 * Get the state of a user.
	 *
	 * @param int $uid The id of the user to get the status for.
	 * @return string One of 'active','disabled','pending', 'blocked'
	 */
	function getState($uid) {
		$info = $this->adapter->getInfo($uid);
		return($info['state']);
	}

	/**
	 * Update the state of a user.
	 *
	 * @param int $uid The id of the user to update.
	 * @param string $state One of 'active','disabled','pending'
	 * @return bool true if the status was updated else false.
	 */
	function setState($uid, $state) {
		return $this->adapter->change($uid, 'state', $state);
	}

	/**
	 * Update the authentication key of a user.
	 *
	 * @param int $uid The id of the user to update.
	 * @param string $key The new authentication key to set.
	 * @return bool true if the status was updated else false.
	 */
	function setAuthKey($uid, $key) {
		return $this->adapter->change($uid, 'authkey', $key);
	}

	/**
	 * Block a user.
	 *
	 * @param int $uid The id of the user to block.
	 * @param int $blockedBy The id of the user applying the block.
	 * @param int $for The time in days to block the account for or 0 to block forever.
	 * @param string $reason The reason for the block.
	 * @return bool true if the block was applied; else false.
	 */
	function block($uid, $blockedBy, $for, $reason) {
		return $this->adapter->block($uid, $blockedBy, $for, $reason);
	}

	/**
	 * Get the information about a block on a user.
	 *
	 * @param int $uid The id of the user to get the block information about.
	 * @return array Array holding the block information or null if the user is not blocked.
	 */
	function getBlockInfo($uid) {
		return $this->adapter->getBlockInfo($uid);
	}

	/**
	 * Get a verification hash which can be sent via email to verify a new user.
	 *
	 * @param int $uid The id of the user to get the verification hash for.
	 * @return string The hash.
	 * @throws flUserException
	 */
	function getVerificationHash($uid) {
		if(!($data = $this->db->fetchRow('SELECT username,passwd,email,state,added,tokensalt FROM fl_users WHERE uid=?', $uid)))
			throw new flUserException('Unable to generate verification hash for unknown user.');

		$d = $data['username'] . $data['passwd'] . $data['email'] . $data['state'] . $data['added'] . $data['tokensalt'];
		return hash_hmac('md5', $d, hash_hmac('md5', $d, $data['passwd'] . $data['added'] . $uid));
	}

	/**
	 * The email address of the user to verify the hash for.
	 *
	 * If the hash is verified then the user state is changed from pending to active.
	 *
	 * @param string $email The email address of the user.
	 * @param string $hash The hash string exactly as created by getVerificationHash
	 * @return bool true if the hash is verified; else false.
	 */
	function verifyHash($email, $hash) {
		if(!($uid = $this->db->fetchOne("SELECT uid FROM fl_users WHERE email=? && state='pending'", $email)))
			return false;

		// Verify the hash
		if($hash !== $this->getVerificationHash($uid))
			return false;

		// Make active
		$this->setState($uid, 'active');

		return true;
	}

	/**
	 * Force the given user ID to be authenticated.
	 *
	 * @param flResponse $response The response object.
	 * @param int $uid The ID of the user to authenticate.
	 */
	function authUser($response, $uid) {
		if($data = $this->db->fetchRow("SELECT passwd, uid, tokensalt FROM fl_users WHERE uid=?", $uid)) {
			$expire = time() + 172800;
			$ui = $uid . '0' . $expire;
			$token = md5($data['passwd'] . $data['uid'] . $data['tokensalt']);
			$response->setCookie(flUser::getCookieName(), $uid . '-0-' . $expire . '-' . hash_hmac('md5', $ui, $token), 0, flUser::getCookieDomain(), flUser::getCookiePath(), flUser::getCookieSecure(), true);
		}
	}

	/**
	 * Force the user to be logged out of all computers.
	 *
 	 * @param int $uid The ID of the user to logout.
	 */
	function forceLogout($uid) {
		$this->adapter->regenerate($uid);
	}

	/**
	 * Get the roles assigned to a user.
	 *
	 * @param int $uid The id of the user to get the roles for.
	 * @param string $name If to get the 'displayname' or the 'rolename'.
	 * @return array Array of role IDs and their display names.
	 */
	function getUserRoles($uid, $name = 'displayname') {
		$r = $this->adapter->getRoles($uid);
		if($name == 'displayname' && count($r)) {
			$ids = array();
			$results = $this->db->query('SELECT roleid,displayname FROM fl_roles WHERE rolename IN(?' . str_repeat(',?', count($r) - 1) . ')', $r);
			while($row = $results->fetch())
				$ids[$row['roleid']] = $row['displayname'];
			$r = $ids;
		}
		return $r;
	}

	/**
	 * Set the roles for a user.
	 *
	 * @param int $uid The id of the user to set the roles for.
	 * @param array $roles Array of role Ids to assign to the user.
	 * @param bool $byName true to set the roles by role name ignoring unknown roles, else false.
	 * @return bool true if the role set; else false.
	 */
	function setUserRoles($uid, $roles, $byName = false) {
		if(!$byName && count($roles)) {
			$names = array();
			$results = $this->db->query('SELECT rolename FROM fl_roles WHERE roleid IN(?' . str_repeat(',?', count($roles) - 1) . ')', $roles);
			while($row = $results->fetch())
				$names[] = $row['rolename'];
			$roles = $names;
		}

		return $this->adapter->setRoles($uid, $roles);
	}

	/**
	 * Test if using remote authentication.
	 *
	 * @return bool true if remote authentication; else false.
	 */
	function remoteAuth() {
		return get_class($this->adapter) !== 'flUserAdapter';
	}
}
