<?php 
/**
* @version		$Id: auth.class.php 2578 2024-05-05 19:36:57Z IOS $
* @package		Elxis
* @subpackage	User authentication
* @copyright	Copyright (c) 2006-2025 Elxis CMS (https://www.elxis.org). All rights reserved.
* @license		Elxis Public License ( https://www.elxis.org/elxis-public-license.html )
* @author		Elxis Team ( https://www.elxis.org )
* @description 	Elxis CMS is free software. Read the license for copyright notices and details
*/

defined('_ELXIS_') or die ('Direct access to this location is not allowed');


elxisLoader::loadFile('components/com_user/auth/auth.interface.php');


class elxisAuth {

	private $auths = array();
	private $errormsg = ''; //error message
	private $auth_current = '';
	private $auth = null; //loaded authentication class
	private $respose = null;


	/*********************/
	/* MAGIC CONSTRUCTOR */
	/*********************/
	public function __construct() {
		$this->initResponse();
		$this->loadAuthMethods();
	}


	/*****************************************/
	/* LOAD AVAILABLE AUTHENTICATION METHODS */
	/*****************************************/
	private function loadAuthMethods() {
		$elxis = eFactory::getElxis();
		$eLang = eFactory::getLang();
		$db = eFactory::getDB();

		if (defined('ELXIS_ADMIN')) {
			$this->auths['elxis'] = array();
			$this->auths['elxis']['title'] = $eLang->exist('WEBSITE') ? $eLang->get('WEBSITE') : 'Web site';
			$this->auths['elxis']['params'] = '';
			return;
		}

		$sql = "SELECT * FROM ".$db->quoteId('#__authentication')." WHERE ".$db->quoteId('published')."=1 ORDER BY ".$db->quoteId('ordering')." ASC";
		$stmt = $db->prepare($sql);
		$stmt->execute();
		$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
		if (!$rows) {
			$this->errormsg = $eLang->exist('NOAUTHMETHODS') ? $eLang->get('NOAUTHMETHODS') : 'There are no available Authentication methods!';
			return false;
		}

		foreach ($rows as $row) {
			$auth = $row['auth'];
			if (!file_exists(ELXIS_PATH.'/components/com_user/auth/'.$auth.'/'.$auth.'.auth.php')) { continue; }
			if ($elxis->getConfig('TWOFA') != '') {
				if ($auth != 'elxis') { continue; }
			}

			if ($auth == 'elxis') {
				$title = $eLang->exist('WEBSITE') ? $eLang->get('WEBSITE') : $row['title'];
			} else {
				$uppereng = strtoupper($auth);
				$title = $row['title'];
				if ($eLang->exist($uppereng)) { $title = $eLang->get($uppereng); }				
			}
			$this->auths[$auth] = array(
				'title' => $title, 
				'params' => $row['params']
			);
		}

		if (!$this->auths) {
			$this->errormsg = $eLang->exist('NOAUTHMETHODS') ? $eLang->get('NOAUTHMETHODS') : 'There are no available Authentication methods!';
			return false;
		}
	}


	/*************************************/
	/* SET CURRENT AUTHENTICATION METHOD */
	/*************************************/
	public function setAuth($auth) {
		$eLang = eFactory::getLang();

		if ((trim($auth) != '') && isset($this->auths[$auth])) {
			if ($auth == $this->auth_current) { return true; } //already loaded
			$this->loadAuthLang($auth);
			elxisLoader::loadFile('includes/libraries/elxis/parameters.class.php');
			$params = new elxisParameters($this->auths[$auth]['params'], '', 'auth');

			$class = $auth.'Authentication';
			elxisLoader::loadFile('components/com_user/auth/'.$auth.'/'.$auth.'.auth.php');
			if (!class_exists($class, false)) {
				$this->errormsg = ucfirst($auth).' is not a valid Elxis Authentication method!';
				return false;
			}

			$this->auth_current = $auth;
			$this->auth = new $class($params);
			return true;
		}

		if (($auth != '') && $eLang->exist('AUTHMETHNOTEN')) {
			$this->errormsg = sprintf($eLang->get('AUTHMETHNOTEN'), $auth);
		} else {
			$this->errormsg = 'Invalid Authentication method!';
		}

		return false;
	}


	/***********************************************/
	/* LOAD CURRENT AUTHENTICATION METHOD LANGUAGE */
	/***********************************************/
	private function loadAuthLang($auth) {
		$eLang = eFactory::getLang();

		$clang = $eLang->currentLang();
		if (file_exists(ELXIS_PATH.'/language/'.$clang.'/'.$clang.'.auth_'.$auth.'.php')) {
			$langfile = ELXIS_PATH.'/language/'.$clang.'/'.$clang.'.auth_'.$auth.'.php';
		} else if (file_exists(ELXIS_PATH.'/language/en/en.auth_'.$auth.'.php')) {
			$langfile = ELXIS_PATH.'/language/en/en.auth_'.$auth.'.php';
		} else if (file_exists(ELXIS_PATH.'/components/com_user/auth/'.$auth.'/language/'.$clang.'.auth_'.$auth.'.php')) {
			$langfile = ELXIS_PATH.'/components/com_user/auth/'.$auth.'/language/'.$clang.'.auth_'.$auth.'.php';
		} else if (file_exists(ELXIS_PATH.'/components/com_user/auth/'.$auth.'/language/en.auth_'.$auth.'.php')) {
			$langfile = ELXIS_PATH.'/components/com_user/auth/'.$auth.'/language/en.auth_'.$auth.'.php';
		} else {
			$langfile = '';
		}

		if ($langfile != '') { $eLang->loadFile($langfile); }
	}


	/***********************/
	/* INITIALIZE RESPONSE */
	/***********************/
	private function initResponse() {
		$this->response = new stdClass;
		$this->response->loginsuccess = false;
		$this->response->method = '';
		$this->response->errormsg = '';
		$this->response->uid = 0;
		$this->response->firstname = null;
		$this->response->lastname = null;
		$this->response->uname = null;
		$this->response->email = null;
		$this->response->gid = 7;
		$this->response->avatar = null;
	}


	/*********************/
	/* AUTHENTICATE USER */
	/*********************/
	public function authenticate($options=array()) {
		$this->initResponse();
		if ($this->errormsg != '') {
			$this->response->errormsg = $this->errormsg;
			return false;
		}

		if (defined('ELXIS_ADMIN')) { $this->auth_current = 'elxis'; }
		
		if ($this->auth_current == '') {
			$this->response->errormsg = 'No authentication method has been set!';
			return false;
		}

		$this->response->method = $this->auth_current;

		$preauth = $this->preAuthCheck($options);
		if ($preauth['success'] == false) {
			$this->response->loginsuccess = false;
			$this->response->errormsg = $preauth['errormsg'];
			return $this->response->loginsuccess;
		}

		$ok = $this->auth->authenticate($this->response, $options);
		if ($ok) {
			$this->response->loginsuccess = true;
			$this->response->errormsg = '';
			if ($this->auth_current != 'elxis') { $this->response->gid = 6; $this->response->uid = 0; }
			$this->onAuthSuccess($options, $preauth['pwlastchange']);
		} else {
			$this->response->loginsuccess = false;
			if ($this->response->errormsg == '') { $this->response->errormsg = 'Authentication failed!'; }
			$this->onAuthFail($options);
		}

		return $this->response->loginsuccess;
	}


	/***************************************************************************/
	/* CHECK IF THERE IS A TEMPORARY ACCOUNT LOCK DUE TO FAILED LOGIN ATTEMPTS */
	/***************************************************************************/
	private function preAuthCheck($options) {
		$response = array('success' => true, 'errormsg' => '', 'pwlastchange' => 0);

		if (!isset($options['uname'])) { return $response; }
		if ($options['uname'] == '') { return $response; }
		$is_email_login = (strpos($options['uname'], '@') !== false) ? true : false;

		$db = eFactory::getDB();

		$sql = "SELECT ".$db->quoteId('uid').", ".$db->quoteId('pwlastchange').", ".$db->quoteId('blockuntil')." FROM ".$db->quoteId('#__users');
		if ($is_email_login) {
			$sql .= "\n WHERE ".$db->quoteId('email')." = :xcred";
		} else {
			$sql .= "\n WHERE ".$db->quoteId('uname')." = :xcred";
		}
		$stmt = $db->prepareLimit($sql, 0, 1);
		$stmt->bindParam(':xcred', $options['uname'], PDO::PARAM_STR);
		$stmt->execute();
		$row = $stmt->fetch(PDO::FETCH_ASSOC);
		if (!$row) { return $response; }

		$response['pwlastchange'] = (int)$row['pwlastchange'];
		$blockuntil = (int)$row['blockuntil'];
		if ($blockuntil < 1) { return $response; }

		$now = time();
		$dt = $blockuntil - $now;
		if ($dt > 0) {
			$response['success'] = false;
			$eLang = eFactory::getLang();
			if ($dt > 59) {
				$min = ceil($dt / 60);
				$txt = $min.' min';
				$response['errormsg'] = sprintf($eLang->get('ACCTEMPLOCKED'), $txt);
			} else {
				$txt = $dt.' sec';
				$response['errormsg'] = sprintf($eLang->get('ACCTEMPLOCKED'), $txt);
			}
		}
		return $response;
	}


	/**********************************/
	/* ACTION ON FAILED LOGIN ATTEMPT */
	/**********************************/
	private function onAuthFail($options) {
		if (!isset($options['uname'])) { return; }
		if ($options['uname'] == '') { return; }
		$is_email_login = (strpos($options['uname'], '@') !== false) ? true : false;

		$db = eFactory::getDB();

		$sql = "SELECT ".$db->quoteId('uid').", ".$db->quoteId('failedlogin')." FROM ".$db->quoteId('#__users');
		if ($is_email_login) {
			$sql .= "\n WHERE ".$db->quoteId('email')." = :xcred";
		} else {
			$sql .= "\n WHERE ".$db->quoteId('uname')." = :xcred";
		}
		$stmt = $db->prepareLimit($sql, 0, 1);
		$stmt->bindParam(':xcred', $options['uname'], PDO::PARAM_STR);
		$stmt->execute();
		$row = $stmt->fetch(PDO::FETCH_ASSOC);
		if (!$row) { return; }

		$uid = (int)$row['uid'];
		$ts = time();
		$failedlogin = '';
		$blockuntil = 0;

		if (trim($row['failedlogin']) == '') {
			$failedlogin = '1:'.$ts;
		} else {
			$parts = explode(':', $row['failedlogin']);
			$times = (int)$parts[0];
			$lasttime = (int)$parts[1];
			$diff = $ts - $lasttime;

			if ($diff < 300) {//5 minutes
				$times++;
				$failedlogin = $times.':'.$ts;
				if ($times >= 3) {//block for 5 minutes on 3rd+ attempt
					$blockuntil = $ts + 300;
				}
			} else {
				$times = 1;
				$failedlogin = '1:'.$ts;
			}
		}

		$sql = "UPDATE ".$db->quoteId('#__users')." SET ".$db->quoteId('failedlogin')." = :xfl, ".$db->quoteId('blockuntil')." = :xbu"
		."\n WHERE ".$db->quoteId('uid')." = :xid";
		$stmt = $db->prepare($sql);
		$stmt->bindParam(':xfl', $failedlogin, PDO::PARAM_STR);
		$stmt->bindParam(':xbu', $blockuntil, PDO::PARAM_INT);
		$stmt->bindParam(':xid', $uid, PDO::PARAM_INT);
		$stmt->execute();
	}


	/**************************************/
	/* ACTION ON SUCESSFULL LOGIN ATTEMPT */
	/**************************************/
	private function onAuthSuccess($options, $pwlastchange) {
		if (!isset($options['uname'])) { return; }
		if ($options['uname'] == '') { return; }
		$is_email_login = (strpos($options['uname'], '@') !== false) ? true : false;

		$db = eFactory::getDB();
		$failedlogin = '';
		$blockuntil = 0;

		$sql = "UPDATE ".$db->quoteId('#__users')." SET ".$db->quoteId('failedlogin')." = :xfl, ".$db->quoteId('blockuntil')." = :xbu";
		if ($is_email_login) {
			$sql .= "\n WHERE ".$db->quoteId('email')." = :xcred";
		} else {
			$sql .= "\n WHERE ".$db->quoteId('uname')." = :xcred";
		}
		$stmt = $db->prepare($sql);
		$stmt->bindParam(':xfl', $failedlogin, PDO::PARAM_STR);
		$stmt->bindParam(':xbu', $blockuntil, PDO::PARAM_INT);
		$stmt->bindParam(':xcred', $options['uname'], PDO::PARAM_STR);
		$stmt->execute();

		if ($pwlastchange > 0) {
			$ts = time();
			$policy = eFactory::getElxis()->getConfig('PWPOLICY');
			switch ($policy) {
				case 2: case 6://3 months
					$diff = $ts - $pwlastchange - 7776000;
				break;
				case 3: case 7://6 months
					$diff = $ts - $pwlastchange - 15552000;
				break;
				case 4: case 8://12 months
					$diff = $ts - $pwlastchange - 31104000;
				break;
				case 1: case 2: default:
					$diff = -1;
				break;
			}
			if ($diff > 0) {//password expired, must change it!
				eFactory::getSession()->set('mustchangepw', 1);//handled in elxisDocument class
			}
		}
	}


	/**********************************************/
	/* MAKE ONE TIME PASSWORD (VERIFICATION CODE) */
	/**********************************************/
	public function makeOTP($options=array()) {
		$iresponse = array('success' => false, 'errormsg' => '');

		$this->initResponse();
		if ($this->errormsg != '') {
			$iresponse['errormsg'] = $this->errormsg;
			return $iresponse;
		}

		if ($this->auth_current != 'elxis') {
			$iresponse['errormsg'] = '2 factor authentication login is supported only by Elxis authentication method!';
			return $iresponse;
		}

		$iresponse = $this->auth->makeOTP($options);
		return $iresponse;
	}


	/*********************/
	/* AUTHENTICATE USER */
	/*********************/
	public function loginForm() {
		if ($this->errormsg != '') {
			echo '<div class="elx5_error">'.$this->errormsg."</div>\n";
			return;
		}
		if ($this->auth_current == '') {
			echo '<div class="elx5_error">No authentication method has been loaded!'."</div>\n";
			return;
		}
		$this->auth->loginForm();
	}


	/*********************/
	/* GET ERROR MESSAGE */
	/*********************/
	public function getError() {
		return $this->errormsg;
	}


	/************************************/
	/* GET AUTHENTICATION METHODS ARRAY */
	/************************************/
	public function getAuths() {
		return $this->auths;
	}


	/******************************************/
	/* GET CURRENT AUTHENTICATION METHOD NAME */
	/******************************************/
	public function getAuth() {
		return $this->auth_current;
	}


	/****************************************/
	/* GET AUTHENTICATION METHOD DATA ARRAY */
	/****************************************/
	public function getAuthData($auth_method='') {
		if ($auth_method == '') { $auth_method = $this->auth_current; }
		return ($auth_method != '') ? $this->auths[$auth_method] : array();
	}


	/**********************/
	/* GET LOGIN RESPONSE */
	/**********************/
	public function getResponse() {
		return $this->response;
	}


	/**********************************/
	/* RUN CUSTOM TASK ON AUTH METHOD */
	/**********************************/
	public function runTask($etask='') {
		if ($this->auth_current == '') {
			exitPage::make('error', 'AUTH-0001', 'No authentication method has been set!');
		}
		if ($this->errormsg != '') {
			exitPage::make('error', 'AUTH-0002', $this->errormsg);
		}
		if ($etask == '') {
			exitPage::make('error', 'AUTH-0003', 'No task has been set!');
		}
		$this->auth->runTask($etask);
	}

}

?>