<?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 - 2017 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flUser Class
 *
 * Class to provide user authentication and data access.
 *
 * @package fusionLib
 */
class flUser {

	/**#@+
	 * Authentication results.
	 */
	const AUTH_UNKNOWN = 0;
	const AUTH_OK = 1;
	const AUTH_BAD_USER_PASS = 2;
	const AUTH_FAILED = 3;
	const AUTH_IP_BLOCKED = 4;
	const AUTH_PENDING_ACTIVATION = 5;
	const AUTH_DISABLED = 6;
	const AUTH_BLOCKED = 7;
	/**#@-*/

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

	/**
	 * The request object.
	 *
	 * @var flRequest
	 */
	protected $request;

	/**
	 * The response object.
	 *
	 * @var flResponse
	 */
	protected $response;

	/**
	 * The name of the cookie.
	 *
	 * @var string
	 */
	static protected $cookieName = '__userid';

	/**
	 * The domain the cookie is available on.
	 *
	 * @var string
	 */
	static protected $cookieDomain = '';

	/**
	 * The path for the cookie.
	 *
	 * @var string
	 */
	static protected $cookiePath = '/';

	/**
	 * Flags if to use secure cookies.
	 *
	 * @var bool
	 */
	static protected $cookieSecure = false;

	/**
	 * Authentication status code.
	 *
	 * @var enum
	 */
	protected $authState = self::AUTH_UNKNOWN;

	/**
	 * The user id.
	 *
	 * @var int
	 */
	protected $uid = null;

	/**
	 * The username.
	 *
	 * @var string
	 */
	protected $username = null;

	/**
	 * The display name of the user.
	 *
	 * @var string
	 */
	protected $displayName = null;

	/**
	 * The users bio.
	 *
	 * @var string
	 */
	protected $bio = null;

	/**
	 * The users email address.
	 *
	 * @var string
	 */
	protected $email = null;

	/**
	 * The users timezone
	 *
	 * @var string
	 */
	protected $timezone = 'UTC';

	/**
	 * Hashing secret.
	 *
	 * @var string
	 */
	protected $secret = '';

	/**
	 * The authentication service used to auth the user.
	 *
	 * @var string
	 */
	protected $service = 'internal';

	/**
	 * Flags if the user is being remembered between browser sessions.
	 *
	 * @var bool
	 */
	protected $rememberMe = false;

	/**
	 * The user permissions.
	 *
	 * @var array
	 */
	private $perms = null;

	/**
	 * The roles assigned to the user.
	 *
	 * @var array
	 */
	private $roles = null;

	/**
	 * The user authentication adapter.
	 *
	 * @var flUserAdapter
	 */
	protected $adapter = null;

	/**
	 * The users private auth key.
	 *
	 * @var string
	 */
	protected $authKey = '';

	/**
	 * The time to remember a user for, default 1 year.
	 *
	 * @var int
	 */
	static protected $rememberMeTime = 31536000;

	/**
	 * Set the time to remember a user for.
	 *
	 * @param int $time The time in seconds to remember a user for.
	 */
	static function setRememberMeTime($time) {
		self::$rememberMeTime = $time;
	}

	/**
	 * Get the time to remember a user for.
	 *
	 * @return int The time in seconds to remember a user for.
	 */
	static function getRememberMeTime() {
		return self::$rememberMeTime;
	}

	/**
	 * Set the user authentication cookie information.
	 *
	 * @param string $cookieName The name of the cookie holding user data.
	 * @param string $cookieDomain The optional domain the cookie is available on, e.g. for all subdomains '.example.com'
	 * @param string $cookiePath The optional path for the cookie.
	 * @param bool $cookieSecure true if using secure cookies.
	 */
	static function setCookieInfo($cookieName, $cookieDomain, $cookiePath, $cookieSecure = false) {
		self::$cookieName = $cookieName;
		self::$cookieDomain = $cookieDomain;
		self::$cookiePath = $cookiePath;
		self::$cookieSecure = $cookieSecure;
	}

	/**
	 * Get the cookie name.
	 *
	 * @return string The cookie name.
	 */
	static function getCookieName() {
		return self::$cookieName;
	}

	/**
	 * Get the cookie domain.
	 *
	 * @return string The cookie domain.
	 */
	static function getCookieDomain() {
		return self::$cookieDomain;
	}

	/**
	 * Get the cookie path.
	 *
	 * @return string The cookie path.
	 */
	static function getCookiePath() {
		return self::$cookiePath;
	}

	/**
	 * Get the secure cookie status.
	 *
	 * @return bool true if using secure cookies; else false.
	 */
	static function getCookieSecure() {
		return self::$cookieSecure;
	}

	/**
	 * Construct the object.
	 *
	 * @param flDb $db The database access object to use.
	 * @param flRequest $request The request object to use.
	 * @param flResponse $response The response object to use.
	 * @param flUserAdapter $adapter The authentication adapter.
	 * @param int $uid If given and $request is null loads the user.
	 */
	function __construct($db, $request, $response, $adapter = null, $uid = null) {
		$this->db = $db;
		$this->request = $request;
		$this->response = $response;

		// If no adapter then set the default
		$this->adapter = $adapter === null ? new flUserAdapter($db) : $adapter;

		if($request !== null) {
			// Test if already logged in
			if(($cookie = $request->getCookie(self::$cookieName)) !== null && preg_match('/^\d+-[01]-\d+-[a-z0-9]{32}$/', $cookie)) {
				$parts = explode('-', $cookie);
				if($parts[2] >= time()) {
					// Validate the user and payload
					$r = $this->adapter->validate($parts[0], $parts[0] . $parts[1] . $parts[2], $parts[3]);
					if($r['authState'] == flUser::AUTH_OK)
						$this->setFromResults($r, $parts[1]);
					else
						$this->logout();
				}
				else
					$this->logout();
			}
		}
		else if($uid !== null) {
			if($r = $this->adapter->getInfo($uid)) {
				$r['uid'] = $uid;
				$this->setFromResults($r, false);
			}
		}
	}

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

	/**
	 * Regenerate the authentication cookie, forces logout on all machines.
	 *
	 * @return string The new value for the auth cookie.
	 * @throws flUserException
	 */
	function regenerateAuth() {
		if($this->response !== null && $this->uid !== null) {
			$r = $this->adapter->regenerate($this->uid);
			if(($this->authState = $r['authState']) == flUser::AUTH_OK)
				$this->setFromResults($r, $this->rememberMe);
		}
		else
			throw new flUserException('Unable to regenerate authentication token as user not logged in.');
	}

	/**
	 * Log the current user out.
	 */
	function logout() {
		// Delete any auth cookie
		if($this->response !== null)
			$this->response->setCookie(self::$cookieName, 0, -3600, self::$cookieDomain, self::$cookiePath, self::$cookieSecure, true);

		$this->authState = self::AUTH_UNKNOWN;
		$this->uid = $this->username = $this->email = null;
		$this->timezone = 'UTC';
	}

	/**
	 * Authenticate the user with the given name and password.
	 *
	 * @param string $username The name of the user or external service name e.g. facebook.
	 * @param string $password The users password or external service id e.g. Facebook user id.
	 * @param bool $rememberMe true if the user should be remembered between visits; else false.
	 * @param string $usernameEmail Select if $username holds a username 'username', an email address 'email', 'external' if using external services such as Facebook, or null if unknown.
	 * @return bool true if the user was authenticated else false.
	 */
	function authenticate($username, $password, $rememberMe, $usernameEmail = 'username') {
		if($this->response === null || $this->uid !== null)
			return true;

		$r = $this->adapter->authenticate($this->request->getClientIp(), $username, $usernameEmail == 'external' ? $password : md5($password), $usernameEmail);
		if(($this->authState = $r['authState']) == flUser::AUTH_OK) {
			$this->setFromResults($r, $rememberMe);
			return true;
		}
		else {
			$this->logout();
			$this->authState = $r['authState'];
			return false;
		}
	}

	/**
	 * Test if the user is authenticated.
	 *
	 * return bool true if the user is authenticated else false.
	 */
	function isAuthenticated() {
		return $this->uid !== null;
	}

	/**
	 * Get the authentication error/state.
	 *
	 * @return int One of AUTH_
	 */
	function getState() {
		return $this->authState;
	}

	/**
	 * Get the id of the currently authenticated user.
	 *
	 * @return int The users id or null if no user authenticated.
	 */
	function getId() {
		return $this->uid;
	}

	/**
	 * Get the username.
	 *
	 * @return string The username.
	 */
	function getUsername() {
		return $this->username;
	}

	/**
	 * Get the users display name.
	 *
	 * @return string The display name.
	 */
	function getDisplayName() {
		return $this->displayName;
	}

	/**
	 * Get the users bio.
	 *
	 * @return string The bio.
	 */
	function getBio() {
		return $this->bio;
	}

	/**
	 * Get the email address.
	 *
	 * @return string The email address.
	 */
	function getEmail() {
		return $this->email;
	}

	/**
	 * Test if remember me is enabled.
	 *
	 * @return bool true if remember me is on; else false.
	 */
	function isRememberMe() {
		return $this->rememberMe;
	}

	/**
	 * Get the users timezone.
	 *
	 * @return string The timezone.
	 */
	function getTimezone() {
		return $this->timezone;
	}

	/**
	 * Test if the username has been set.
	 *
	 * @return bool true if the username has been set; else false.
	 */
	function usernameSet() {
		return $this->username != md5($this->uid . '__fusionLib.User__');
	}

	/**
	 * Get the users private key for two factor authentication.
	 *
	 * @return string The two factor authenticaiton secret.
	 */
	function getAuthKey() {
		return $this->authKey;
	}

	/**
	 * Get the roles assigned to a user.
	 *
	 * @return array Array of role names assigned to the user.
	 */
	function getRoles() {
		if($this->uid === null)
			return array();
		else if($this->roles === null)
			$this->roles = $this->adapter->getRoles($this->uid);
		return $this->roles;
	}

	/**
	 * Get a list of all the permissions granted to the user.
	 *
	 * @return array Array holding all the permissions granted to the user, empty if not logged in.
	 */
	function getPermissions() {
		if($this->uid === null)
			return array();

		if($this->perms === null) {
			$this->perms = array('__flLoggedInUser__' => '__flLoggedInUser__');
			$result = $this->db->query('SELECT DISTINCT permission FROM fl_usersroles LEFT JOIN fl_roleperms USING(roleid) WHERE uid=?', $this->uid);
			while($row = $result->fetch())
				$this->perms[$row['permission']] = $row['permission'];
		}

		return $this->perms;
	}

	/**
	 * Test if the user has been granted one or more permission.
	 *
	 * @param string|array $perm The permission(s) to test for.
	 * @return bool true if the permission(s) have been granted; else false.
	 */
	function hasPermission($perm) {
		if($this->uid === null)
			return false;

		// If parameters given rather than array then get args
		if(!is_array($perm))
			$perm = func_get_args();

		$granted = $this->getPermissions();
		foreach($perm as $p) {
			if(!empty($p) && !isset($granted[$p]))
				return false;
		}

		return true;
	}

	/**
	 * Get the user secret.
	 *
	 * @return string The secret.
	 */
	function getSecret() {
		return $this->secret;
	}

	/**
	 * Get the authentication service used to auth the user.
	 *
	 * @return string The service name.
	 */
	function getAuthService() {
		return $this->service;
	}

	/**
	 * Set the user information and authentication cookie from adapter returned results.
	 *
	 * @param array $r Array of user data.
	 * @param bool $rememberMe true if user selected remember me; else false.
	 */
	protected function setFromResults($r, $rememberMe) {
		$this->email = $r['email'];
		$this->timezone = $r['tz'];
		$this->uid = $r['uid'];
		$this->username = $r['username'];
		$this->displayName = isset($r['displayname']) && !empty($r['displayname']) ? $r['displayname'] : $r['username'];
		$this->authKey = isset($r['authkey']) ? $r['authkey'] : '';
		$this->bio = isset($r['bio']) ? $r['bio'] : '';
		$this->rememberMe = $rememberMe;
		$this->secret = $r['token'];
		$this->service = isset($r['service']) ? $r['service'] : 'internal';

		// Set the cookie
		if($this->response !== null) {
			$expire = time() + ($this->rememberMe ? self::$rememberMeTime : 172800);
			$ui = $this->uid . ($this->rememberMe ? '1' : '0') . $expire;
			$this->response->setCookie(self::$cookieName, $this->uid . '-' . ($this->rememberMe ? '1' : '0') . '-' . $expire . '-' . hash_hmac('md5', $ui, $r['token']), $this->rememberMe ? self::$rememberMeTime : 0, self::$cookieDomain, self::$cookiePath, self::$cookieSecure, true);
		}
	}
}
