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

/**
 * flUserAdapterRemote Class
 *
 * User authentication adaptor for authenticating against remote fusionLib via http(s).
 *
 * @package fusionLib
 */
class flUserAdapterRemote extends flUserAdapter {

	/**
	 * The endpoint for the requests.
	 *
	 * @var string
	 */
	protected $endPoint;

	/**
	 * API Key to use when accessing the endpoint.
	 *
	 * @var string
	 */
	protected $apiKey;

	/**
	 * The time to cache validation for.
	 *
	 * @var int
	 */
	protected $cacheTime;

	/**
	 * Construct the adapter.
	 *
	 * @param flDb $db The system database object.
	 * @param flConfig $cfg The system configuration object.
	 * @param bool $notifyEvents Flags if to send user event notifications.
	 * @throws flUserException
	 */
	function __construct($db, $cfg = null, $notifyEvents = true) {
		parent::__construct($db, $cfg, $notifyEvents);

		if(!($this->endPoint = rtrim($cfg->get('userAdapterEndpoint'), '/')))
			throw new flUserException('The configuration option userAdapterEndpoint is missing.');

		if(!($this->apiKey = $cfg->get('userAdapterAPIKey')))
			throw new flUserException('The configuration option userAdapterAPIKey is missing.');

		$this->cacheTime = $cfg->get('userAdapterCacheTime', 60);
	}

	/**
	 * Authenticate the user with the given name and password.
	 *
	 * @param string $ip The client IP address.
	 * @param string $username The name of the user.
	 * @param string $password The MD5 of the users password.
	 * @param string $usernameEmail Select if $username holds a username 'username', an email address 'email', or null if unknown.
	 * @param bool $updateAccessTime true to update the last access time; else false.
	 * @return array Array of user data and auth state.
	 */
	function authenticate($ip, $username, $password, $usernameEmail, $updateAccessTime = true) {

		$client = new flHttpClient($this->endPoint . '/1/user/authenticate/');
		$client->setAuth($this->apiKey, '')
			->setPostData(array(
			'ip' => $ip,
			'username' => $username,
			'password' => $password,
			'by' => $usernameEmail ? $usernameEmail : ''
		));

		if($client->sendRequest(flHttpClient::POST)) {
			$r = json_decode($client->getBody());
			if($client->getResponseCode() == 400 || $r === null)
				$r = array('authState' => flUser::AUTH_FAILED);
			else {
				$r = get_object_vars($r);

				if($r['authState'] == flUser::AUTH_OK) {
					if(!isset($r['displayname'])) $r['displayname'] = '';
					if(!isset($r['authkey'])) $r['authkey'] = '';

					// If user exists track changes
					if($existing = parent::getInfo($r['uid'])) {
						if($r['username'] != $existing['username'])
							parent::change($r['uid'], 'username', $r['username']);
						if($r['displayname'] != $existing['displayname'])
							parent::change($r['uid'], 'displayname', $r['displayname']);
						if($r['email'] != $existing['email'])
							parent::change($r['uid'], 'email', $r['email']);
						if($r['tz'] != $existing['tz'])
							parent::change($r['uid'], 'tz', $r['tz']);
						if($r['authkey'] != $existing['authkey'])
							parent::change($r['uid'], 'authkey', $r['authkey']);
						if($r['authcode'] != $existing['authcode'])
							parent::change($r['uid'], 'authcode', $r['authcode']);
						if($r['bio'] != $existing['bio'])
							parent::change($r['uid'], 'bio', $r['bio']);
					}
					// Else create the user local
					else {
						if(!parent::create($r['username'], $r['displayname'], $r['email'], $r['token'], 'active', $r['tz'], $r['uid'], $r['bio']))
							return array('authState' => flUser::AUTH_FAILED);
					}

					// Copy the users roles from the master
					parent::setRoles($r['uid'], is_array($r['roles']) ? $r['roles'] : get_object_vars($r['roles']));
					unset($r['roles']);

					if($r['service'] != 'internal') {
						$this->db->replace('fl_user_ext', array(
							'uid' => $r['uid'],
							'service' => $r['service'],
							'authcode' => md5($r['uid'])
						));
					}

					// Update last access time & cache time
					$now = new flDate();
					$d = array(
						'passwd' => $r['token'],
						'lastslaveupd' => $now->toString(),
						'state' => 'active'
					);
					if($updateAccessTime)
						$d['lastaccess'] = $now->toString();
					$this->db->update('fl_users', $d, 'uid=?', $r['uid']);
				}
			}
		}
		else
			$r = array('authState' => flUser::AUTH_FAILED);

		if($r['authState'] != flUser::AUTH_OK)
			$this->db->update('fl_users', array('state' => ($r['authState'] == flUser::AUTH_BLOCKED ? 'blocked' : 'disabled')), 'username=?', $username);

		return $r;
	}

	/**
	 * Validate the user id, data payload and signature.
	 *
	 * @param int $uid The user id to validate.
	 * @param string $data The data to be verified.
	 * @param string $signature The signature of the data.
	 * @param bool $updateAccessTime true to update the last access time; else false.
	 * @return array The array of user data and auth state.
	 */
	function validate($uid, $data, $signature, $updateAccessTime = true) {
		$now = new flDate();
		$d = array();
		$r = null;

		// Try auth against the local database
		if($user = $this->db->fetchRow('SELECT username,displayname,email,passwd,tz,lastslaveupd,bio,service FROM fl_users LEFT JOIN fl_users_ext USING(uid) WHERE uid=?', $uid)) {

			// Find age of last update from the master and if less than cache time validate off slave
			$lastCheck = new flDate($user['lastslaveupd']);
			if(($now->getTimestamp() - $lastCheck->getTimestamp()) < $this->cacheTime && $signature == hash_hmac('md5', $data, $user['passwd'])) {
				$r = array(
					'uid' => $uid,
					'username' => $user['username'],
					'displayname' => $user['displayname'],
					'email' => $user['email'],
					'tz' => $user['tz'],
					'token' => $user['passwd'],
					'authState' => flUser::AUTH_OK,
					'bio' => $user['bio'],
					'service' => $user['service']
				);
			}
		}

		if(!$r) {
			// Validate against master server
			$client = new flHttpClient($this->endPoint . '/1/user/validate/');
			$client->setAuth($this->apiKey, '')
				->setPostData(array(
					'uid' => $uid,
					'data' => $data,
					'hash' => $signature
				));

			if($client->sendRequest(flHttpClient::POST)) {
				$r = json_decode($client->getBody());
				if($client->getResponseCode() == 400 || $r === null)
					$r = array('authState' => flUser::AUTH_FAILED);
				else {
					$r = get_object_vars($r);
					if($r['authState'] == flUser::AUTH_OK) {
						if(!isset($r['displayname'])) $r['displayname'] = '';

						// Track changes
						if($existing = parent::getInfo($r['uid'])) {
							if($r['username'] != $existing['username'])
								parent::change($r['uid'], 'username', $r['username']);
							if($r['displayname'] != $existing['displayname'])
								parent::change($r['uid'], 'displayname', $r['displayname']);
							if($r['email'] != $existing['email'])
								parent::change($r['uid'], 'email', $r['email']);
							if($r['tz'] != $existing['tz'])
								parent::change($r['uid'], 'tz', $r['tz']);
							if($r['bio'] != $existing['bio'])
								parent::change($r['uid'], 'bio', $r['bio']);

							$d['state'] = 'active';
						}
						// Else create the user local
						else {
							if(!parent::create($r['username'], $r['displayname'], $r['email'], $r['token'], 'active', $r['tz'], $r['uid'], $r['bio']))
								return array('authState' => flUser::AUTH_FAILED);
						}

						$d['passwd'] = $r['token'];
						$d['lastslaveupd'] = $now->toString();

						if($r['service'] != 'internal') {
							$this->db->replace('fl_user_ext', array(
								'uid' => $r['uid'],
								'service' => $r['service'],
								'authcode' => md5($r['uid'])
							));
						}

						// Copy the users roles from the master
						parent::setRoles($r['uid'], is_array($r['roles']) ? $r['roles'] : get_object_vars($r['roles']));
						unset($r['roles']);
					}
				}
			}
			else
				$r = array('authState' => flUser::AUTH_FAILED);
		}

		// Update last access time & cache time
		if($r['authState'] == flUser::AUTH_OK) {
			if($updateAccessTime)
				$d['lastaccess'] = $now->toString();
			if(count($d))
				$this->db->update('fl_users', $d, 'uid=?', $uid);
		}
		else
			$this->db->update('fl_users', array('state' => ($r['authState'] == flUser::AUTH_BLOCKED ? 'blocked' : 'disabled')), 'uid=?', $uid);

		return $r;
	}

	/**
	 * Regenerate the authentication token.
	 *
	 * @param int $uid The ID of the user to regenerate.
	 * @return array Array of user data and state.
	 */
	function regenerate($uid) {
		$client = new flHttpClient($this->endPoint . '/1/user/regenerate/');
		$client->setAuth($this->apiKey, '')
			->setPostData(array(
				'uid' => $uid
			));

		if($client->sendRequest(flHttpClient::POST)) {
			$r = json_decode($client->getBody());
			if($client->getResponseCode() == 400 || $r === null)
				$r = array('authState' => flUser::AUTH_FAILED);
			else {
				$r = get_object_vars($r);
				if($r['authState'] == flUser::AUTH_OK)
					parent::change($r['uid'], 'passwd', $r['token']);
			}
		}
		else
			$r = array('authState' => flUser::AUTH_FAILED);

		return $r;
	}

	/**
	 * Change a field.
	 *
	 * @param int $uid The ID of the user to update.
	 * @param string $what What to change (passwd,username,email,tz, state, displayname,authkey,bio)
	 * @param string $value The new value to set, for passwd is the MD5 of the password.
	 * @return bool true if the change has been made; else false/null.
	 */
	function change($uid, $what, $value) {
		$client = new flHttpClient($this->endPoint . '/1/user/change/');
		$client->setAuth($this->apiKey, '')
			->setPostData(array(
			'uid' => $uid,
			'field' => $what,
			'value' => $value
		));

		if($client->sendRequest(flHttpClient::POST)) {
			$r = json_decode($client->getBody());
			return $client->getResponseCode() == 200 && $r !== null && $r->result ? parent::change($uid, $what, $value) : false;
		}
		else
			$r = false;

		return $r;
	}

	/**
	 * Check for the presence of a username or email address.
	 *
	 * @param string $what What to check for (username, email)
	 * @param string $value The value to check for.
	 * @return bool true if a user exists that matches the check; else false.
	 */
	function check($what, $value) {
		$client = new flHttpClient($this->endPoint . '/1/user/check/');
		$client->setAuth($this->apiKey, '')
			->setQueryData(array(
			'field' => $what,
			'value' => $value
		));

		if($client->sendRequest(flHttpClient::GET)) {
			$r = json_decode($client->getBody());
			return $client->getResponseCode() == 200 && $r !== null && $r->result ? true : false;
		}
		else
			$r = false;

		return $r;
	}

	/**
	 * Create a user within the system.
	 *
	 * @param string $username The username or null if creating an unnamed user.
	 * @param string $displayName The display name for the user.
	 * @param string $email The users email address.
	 * @param string $password The MD5 of the users password.
	 * @param string $state The state to create the user in: 'active','disabled','pending'
	 * @param string $tz The timezone string for the user.
	 * @param int $forceID null to auto assign an ID or the ID to force, only valid for local accounts.
	 * @param string $bio The users bio.
	 * @return int The user id or 0 if the user could not be created.
	 */
	function create($username, $displayName, $email, $password, $state, $tz, $forceID = null, $bio = '') {
		$client = new flHttpClient($this->endPoint . '/1/user/create/');
		$client->setAuth($this->apiKey, '')
			->setPostData(array(
			'username' => $username,
			'displayname' => $displayName,
			'email' => $email,
			'password' => $password,
			'state' => $state,
			'tz' => $tz,
			'bio' => $bio
		));

		if($client->sendRequest(flHttpClient::POST)) {
			$r = json_decode($client->getBody());
			$r = $client->getResponseCode() != 201 || $r === null ? 0 : parent::create($username, $displayName, $email, $password, $state, $tz, $r->uid, $bio);
		}
		else
			$r = 0;

		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 names to set.
	 * @return bool true if the roles were set; else false.
	 */
	function setRoles($uid, $roles) {
		$client = new flHttpClient($this->endPoint . '/1/user/roles/');
		$client->setAuth($this->apiKey, '')
			->setPostData(array(
			'uid' => $uid,
			'roles' => json_encode(array_values($roles))
		));

		if($client->sendRequest(flHttpClient::POST)) {
			$r = json_decode($client->getBody());
			$r = $client->getResponseCode() != 200 || $r === null ? false : parent::setRoles($uid, is_array($r) ? $r : get_object_vars($r));
		}
		else
			$r = false;

		return $r;
	}

	/**
	 * Get the names of the roles assigned to the user.
	 *
	 * @param int $uid The ID of the user to get the roles for.
	 * @return array Array of role names.
	 */
	function getRoles($uid) {
		// Get the age of the last update from the server and if new enough load from the local database
		$now = new flDate();
		$lastCheck = new flDate($this->db->fetchOne('SELECT lastslaveupd FROM fl_users WHERE uid=?', $uid));
		if(($now->getTimestamp() - $lastCheck->getTimestamp()) < $this->cacheTime)
			$r = parent::getRoles($uid);
		else
			$r = $this->getInfo($uid) ? parent::getRoles($uid) : array();

		return $r;
	}

	/**
	 * Get the names of the roles assigned to the user from the master.
	 *
	 * @param int $uid The ID of the user to get the roles for.
	 * @return array Array of role names.
	 */
	function getRolesFromMaster($uid) {
		$client = new flHttpClient($this->endPoint . '/1/user/roles/');
		$client->setAuth($this->apiKey, '')
			->setQueryData(array('uid' => $uid));

		if($client->sendRequest(flHttpClient::GET)) {
			$r = json_decode($client->getBody());
			if($client->getResponseCode() != 200 || $r === null)
				$r = array();
			else
				parent::setRoles($uid, $r);
		}
		else
			$r = array();

		return $r;
	}

	/**
	 * Get the information on a user.
	 *
	 * @param int $uid The ID of the user to get.
	 * @return array The user information or false/null on error.
	 */
	function getInfo($uid) {

		// Try using the local copy
		if(!($data = $this->db->fetchRow("SELECT username,displayname,email,tz,authkey,added,lastaccess,lastslaveupd,bio,IF(blockliftat IS NULL, state, 'blocked') AS state,service FROM fl_users LEFT JOIN fl_usersblocks USING(uid) LEFT JOIN fl_users_ext USING(uid) WHERE uid=?", $uid)))
			return false;

		// Find age of last update from the master and if less than cache time validate off slave
		$now = new flDate();
		$lastCheck = new flDate($data['lastslaveupd']);
		if(($now->getTimestamp() - $lastCheck->getTimestamp()) < $this->cacheTime) {
			$r = $data;
			unset($r['lastslaveupd']);
		}
		// Too old so fetch from master and update local
		else {
			$client = new flHttpClient($this->endPoint . '/1/user/info/');
			$client->setAuth($this->apiKey, '')
				->setQueryData(array('uid' => $uid));

			if($client->sendRequest(flHttpClient::GET)) {
				$r = json_decode($client->getBody());
				if($client->getResponseCode() != 200 || $r === null)
					$r = false;
				else {
					$r = get_object_vars($r);
					$r['lastaccess'] = $data['lastaccess'];
					if(!isset($r['displayname'])) $r['displayname'] = '';
					if(!isset($r['authkey'])) $r['authkey'] = '';

					// Track changes
					if($existing = parent::getInfo($uid)) {
						if($r['username'] != $existing['username'])
							parent::change($uid, 'username', $r['username']);
						if($r['displayname'] != $existing['displayname'])
							parent::change($uid, 'displayname', $r['displayname']);
						if($r['email'] != $existing['email'])
							parent::change($uid, 'email', $r['email']);
						if($r['tz'] != $existing['tz'])
							parent::change($uid, 'tz', $r['tz']);
						if($r['authkey'] != $existing['authkey'])
							parent::change($uid, 'authkey', $r['authkey']);
						if($r['bio'] != $existing['bio'])
							parent::change($uid, 'bio', $r['bio']);
					}

					// Updates roles from the server
					parent::setRoles($uid, $this->getRolesFromMaster($uid));

					// Update the time last updated from the master
					$this->db->update('fl_users', array('lastslaveupd' => $now->toString()), 'uid=?', $uid);
				}
			}
			else
				$r = false;
		}

		return $r;
	}

	/**
	 * Delete the user.
	 *
	 * @param int $uid The ID of the user to delete.
	 * @return bool true if deleted; else false.
	 */
	function delete($uid) {
		$client = new flHttpClient($this->endPoint . '/1/user/delete/');
		$client->setAuth($this->apiKey, '')
			->setPostData(array('uid' => $uid));

		if($client->sendRequest(flHttpClient::POST)) {
			$r = json_decode($client->getBody());
			$r = $client->getResponseCode() != 200 || $r === null || !$r->result ? false : parent::delete($uid);
		}
		else
			$r = false;

		return $r;
	}

	/**
	 * 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) {
		$client = new flHttpClient($this->endPoint . '/1/user/block/');
		$client->setAuth($this->apiKey, '')
			->setPostData(array(
			'uid' => $uid,
			'blockedBy' => $blockedBy,
			'days' => $for,
			'reason' => $reason
		));

		if($client->sendRequest(flHttpClient::POST)) {
			$r = json_decode($client->getBody());
			$r = $client->getResponseCode() != 200 || $r === null || !$r->result ? false : parent::change($uid, 'state', 'blocked');
		}
		else
			$r = false;

		return $r;
	}

	/**
	 * 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) {
		$client = new flHttpClient($this->endPoint . '/1/user/blockinfo/');
		$client->setAuth($this->apiKey, '')
			->setQueryData(array('uid' => $uid));

		if($client->sendRequest(flHttpClient::GET)) {
			$r = json_decode($client->getBody());
			$r = $client->getResponseCode() != 200 || $r === null ? null : (is_array($r) ? $r : get_object_vars($r));
			if($r !== null && !isset($r['displayname']))
				$r['displayname'] = '';
		}
		else
			$r = null;

		return $r;
	}
}
