<?php
###############################################################################
# Auth.php
#
# @author Anil Kumar <akumar@codepunch.com>
# @link   https://codepunch.com
#
############################################################################### 

namespace 	CodePunch\Config;

use 		CodePunch\Base\Util as UTIL;
use			CodePunch\Base\Text as TEXT;
use			CodePunch\DB\Audit as AUDIT;
use 		Exception;

###############################################################################

class Auth extends Config {
	
	private $primary_authentication = null;
	private $secondary_authentication = null;
	
	###########################################################################
	
	public function __construct($dbaction=\CodePunch\DB\Database::READONLY_TABLES, $defaultcfg="") 
	{
		self::initSession();
		$this->preInit();
		parent::__construct($defaultcfg, $dbaction);
		$this->postInit();
	}
	
	###########################################################################
	
	public function getPrimaryAuthentication() {
		return $this->primary_authentication;
	}
	
	###########################################################################
	
	public function getSecondaryAuthentication() {
		return $this->secondary_authentication;
	}
	
	###########################################################################
	
	private function postInit()
	{
		if(!$this->hasAnAdminUser())
			throw new Exception(TEXT::get("missing_setup_user"));
		if(UTIL::is_request_key_set('link'))
			$this->processLink(UTIL::get_sanitized_request_string('link'), $_REQUEST);
		$setup = new \CodePunch\Config\Settings($this);
		$authmode = $setup->getOption("2fa_provider", "");
		if($authmode != "") {
			$class = "\\CodePunch\\Config\\Security\\" . $authmode;
			if(class_exists($class))
				$this->secondary_authentication = $class;
		}
	}
	
	###########################################################################
	
	private function preInit()
	{
		UTIL::parse_request_data_in_cli();
		if(UTIL::is_request_key_set('lang')) {
			try {
				$lang = UTIL::get_sanitized_request_string("lang", "");
				\CodePunch\Base\Text::setLanguage($lang);
			}
			catch(Exception $e) {
				$logger = new \CodePunch\Base\CPLogger();
				$logger->error($e->getMessage());
			}
		}
	}
	
	###########################################################################
	
	public function processLink($link, $params) 
	{
		$link = "custom_url_$link";
		$links = \CodePunch\Config\Settings::getDefaultBranding();
		$url = "";
		if(in_array($link, array_keys($links)))
			$url = $links[$link];
		if(self::isLoggedIn()) {
			$setup = new \CodePunch\Config\Settings($this);
			$setupurl = $setup->getOption($link, $url);
			if($setupurl != "")
				$url = $setupurl;
		}
		if($url != "" && filter_var($url, FILTER_VALIDATE_URL)) {
			if(isset($params['key'])) {
				$pos = strpos($url, "?");
				if($pos !== false)
					$url = substr($url, 0, $pos);
				$path = preg_replace('/\x00|<[^>]*>?/', '', $params['key']);
				$path = str_replace(["'", '"'], ['&#39;', '&#34;'], $path);
				$url .= str_replace("-", "/", $path) . ".php";
			}
			if(strchr($url, "?") === false)
				$url .= "?sed6";
			header('Location: ' . $url);
			exit;
		}
	}
	
	###########################################################################
	
	public static function initSession()
	{
		if(!isset($_SESSION) && !UTIL::is_cli())
			session_start();
	}
	
	###########################################################################
	# Doesn't check if session has timed out. 
	# Use validateSession(false, false) for a proper "logged in" check.
	
	public static function isLoggedIn()
	{
		if(isset($_SESSION[self::LOGGEDIN])) {
			if($_SESSION[self::LOGGEDIN] === true) 
					return true;
		}
		return false;
	}
	
	###########################################################################
	
	public function validateSession($pagetoshow="", $swipein = TRUE) 
	{
		if(UTIL::is_request_key_set('logout'))
			$this->processLogout();
	
		$status = $this->validate($pagetoshow);
		if($status != Auth::UNKNOWN && $pagetoshow === false) 
			return Auth::UNKNOWN;
		if(isset($_SESSION[self::LOGGEDIN])) {
			if($_SESSION[self::LOGGEDIN] === true) {
				if(!$this->isSessionTimedout()) 
					$status = Auth::VALID;
				else {
					$status = Auth::TIMEDOUT;
					if(!isset($_SESSION[self::TIMEOUTAUDIT])) {
						AUDIT::add($this->getDatabase(), AUDIT::SESSION_TIMEOUT);
						$_SESSION[self::TIMEOUTAUDIT] = true;
					}
				}
			}
			else
				$status = Auth::INVALID;
		}
		if($status == Auth::VALID && $swipein) 
			self::swipeIn();

		if($pagetoshow !== false) {
			$layout = new \CodePunch\UI\Layout($this);
			$show = $pagetoshow;
			if($status != Auth::VALID) {
				if($pagetoshow != "login" && !isset($_SESSION[self::DESTINATION]))
					$_SESSION[self::DESTINATION] = $pagetoshow;
				$reason = UTIL::get_sanitized_request_string("r", "");
				if($reason != "")
					$reason = TEXT::get($reason);
				$reason = ($status == Auth::TIMEDOUT ? TEXT::get("auth_error_session_timeout") : $reason);
				$login = new \CodePunch\UI\Modules\Login();
				$show = $login->get($this);
				if($reason != "" && is_array($show) && count($show) == 1 && isset($show['template']))
					$show['body'] = $reason;
			}
			else {
				if($show == "login")
					$show =  isset($_SESSION[self::DESTINATION]) ? $_SESSION[self::DESTINATION] : "";
			}
			if(!is_array($show)) {
				$show = $this->validateDestinationPage($show);
				if(stristr($show, ".php") !== false) {
					header("Location: $show");
					exit;
				}
			}
			$layout->show($show);
			exit;
		}
		return $status;
	}
	
	###########################################################################
	
	public function loginCheckCompleted(&$responce)
	{
		parent::loginCheckCompleted($responce);
		if($responce['status'] == 'ok') {
			$username = $responce['user'];
			$secondary = $this->getSecondaryAuthentication();
			if($secondary != null && class_exists($secondary) && !$this->isSetupUser($username)) {
				$setup = new \CodePunch\Config\Settings($this);
				$twofa_cookie_set_days = $setup->getString("twofa_cookie_set_days", 0);
				if($twofa_cookie_set_days > 0 && $twofa_cookie_set_days <= 45) {
					$cookiename = trim(str_replace(array("@", ".", "-"), "_", "deval_$username"));
					$cookiename = str_replace(" ", "", $cookiename);
					if(isset($_COOKIE[$cookiename])) {
						if($_COOKIE[$cookiename] == "1") 
							return;
					}
				}
				$responce['secondary'] = 1;
			}
		}
	}
	
	###########################################################################
	
	public function swipeIn() {
		$_SESSION[self::TIMEOUT] = time();
	}
	
	###########################################################################
	
	public function getLastSwipein() 
	{
		if(isset($_SESSION[self::TIMEOUT]))
		{
			$ts = intval($_SESSION[self::TIMEOUT]);
			if($ts > 0)
				return date("Y-m-d H:i:s", $ts);
		}
		return "";
	}
	
	###########################################################################
	
	public function processLogout() 
	{
		if(isset($_SESSION[self::LOGGEDIN])) {
			$db = $this->getDatabase();
			if($db) 
				AUDIT::add($db, AUDIT::LOGGED_OUT);
			unset($_SESSION[self::LOGGEDIN]);
			unset($_SESSION[self::REMOTEIP]);
			unset($_SESSION[self::TIMEOUT]);
			unset($_SESSION[self::USERNAME]);
			unset($_SESSION[self::USERINFO]);
			unset($_SESSION[self::DESTINATION]);
			unset($_SESSION[self::VALIDITY]);
			unset($_SESSION[self::TIMEOUTAUDIT]);
			unset($_SESSION[self::SAMLUSER]);
			unset($_SESSION[self::SAMLIDP]);
		}
		if(UTIL::is_request_key_set(self::DESTINATION))
			$_SESSION[self::DESTINATION] = UTIL::get_sanitized_request_string(self::DESTINATION);
	}
	
	###########################################################################
	/**
	 * Validates and resolves a destination page name after login or session redirect.
	 *
	 * Accepts either a module name or a tools/ path and confirms it is a legitimate
	 * destination before allowing a redirect to it.
	 *
	 * Validation is performed in two stages:
	 *
	 * 1. MODULE CHECK
	 *    Scans lib/php/CodePunch/UI/Modules/ for .php files and treats each filename
	 *    (without extension) as a valid page name. The 'admin' module is excluded for
	 *    non-admin users.
	 *
	 * 2. TOOLS & USER-TOOLS PATH CHECK
	 *    Accepts paths in the format "tools/xxx" and confirms that tools/xxx.php
	 *    physically exists in the installation's tools/ folder.
	 *    Also accepts "tools/" and "tools/index" if tools/index.php exists.
	 *    same for user-tools
	 *
	 * If the destination cannot be validated, falls back to the configured default
	 * home page, or "data" if that is also invalid.
	 *
	 * @param  string $gotopage  The requested destination (e.g. "dashboard", "tools/portfolio")
	 * @return string            A validated destination page string safe to redirect to
	 */
	public function validateDestinationPage($gotopage)
	{
		$folder = UTIL::get_install_folder_path();

		// --- Existing module check ---
		$files = UTIL::find_all_matched_files($folder . "lib/php/CodePunch/UI/Modules/", "*.php");
		$validpages = array();
		foreach ($files as $r)
			$validpages[] = trim(strtolower(str_replace(".php", "", $r)));
		$validpages = array_filter($validpages, 'strlen');
		if (!$this->isAdmin())
			$validpages = array_diff($validpages, array('admin'));

		if (in_array($gotopage, $validpages))
			return $gotopage;

		// --- tools/ path check ---
		// Support "tools/" and "tools/index" if tools/index.php exists
		if ($gotopage === 'tools/' || $gotopage === 'tools/index') {
			if (file_exists($folder . 'tools/index.php'))
				return $gotopage;
		}

		// Support "tools/xxx" if tools/xxx.php exists in root tools folder
		if (preg_match('#^tools/([a-zA-Z0-9_-]+)$#', $gotopage, $matches)) {
			$toolFile = $folder . 'tools/' . $matches[1] . '.php';
			if (file_exists($toolFile))
				return $gotopage;
		}
		
		// --- user-tools/ path check ---
		// Support "user-tools/" and "user-tools/index" if user-tools/index.php exists
		if ($gotopage === 'user-tools/' || $gotopage === 'user-tools/index') {
			if (file_exists($folder . 'user-tools/index.php'))
				return $gotopage;
		}
		// Support "user-tools/xxx" if user-tools/xxx.php exists in root user-tools folder
		if (preg_match('#^user-tools/([a-zA-Z0-9_-]+)$#', $gotopage, $matches)) {
			$toolFile = $folder . 'user-tools/' . $matches[1] . '.php';
			if (file_exists($toolFile))
				return $gotopage;
		}

		// --- Fallback to default ---
		$setup = new \CodePunch\Config\Settings($this);
		$gotopage = $setup->getOption('default_home_page', self::DEFAULT_HOME_PAGE);

		if (!in_array($gotopage, $validpages) && count($validpages))
			$gotopage = "data";

		return $gotopage;
	}
	
	###########################################################################
	
	public function getDestinationPage()
	{
		$gotopage = isset($_SESSION[self::DESTINATION]) ? $_SESSION[self::DESTINATION] : "";
		if($gotopage == "") {
			if($this->isSetupAdmin())
				$gotopage = "admin";
			else if($this->isAdmin())
				$gotopage = "dashboard";
		}
		$gotopage = $this->validateDestinationPage($gotopage);
		if($gotopage == "domains")
			$gotopage = UTIL::get_root_url();
		else
			$gotopage = UTIL::get_root_url() . $gotopage . ".php";
		return $gotopage;
	}
	
	###########################################################################
	
	public function processSecondaryLogin($username, $twofa_cookie_set_days)
	{
		$_SESSION[self::LOGGEDIN] = true;
		$validate_device = UTIL::get_request_data_boolean("validate_device", false);
		if($twofa_cookie_set_days) {
			$cookiename = trim(str_replace(array("@", ".", "-"), "_", "deval_$username"));
			$cookiename = str_replace(" ", "", $cookiename);
			if(!isset($_COOKIE[$cookiename]) && $validate_device)
				setcookie($cookiename, "1", time() + (86400 * $twofa_cookie_set_days), "/");
		}
		AUDIT::add($this->getDatabase(), AUDIT::LOGGED_IN, null, $_SESSION[self::USERNAME]);
		$gotopage = $this->getDestinationPage();
		header("Location: $gotopage");
		exit;
	}
	
	###########################################################################
	
	public function processPrimaryLogin($responce) 
	{
		unset($_SESSION[self::TIMEOUTAUDIT]);
		if($responce['secondary'] != 1)
			$_SESSION[self::LOGGEDIN] = true;
		else
			$_SESSION[self::LOGGEDIN] = false;
		$_SESSION[self::USERNAME]  = $responce['user'];
		$_SESSION[self::USERINFO]  = $responce;
		$_SESSION[self::REMOTEIP]  = $_SERVER["REMOTE_ADDR"];
		$_SESSION[self::TIMEOUT]   = time();
		if($responce['secondary'] == 1)
			$responce['url'] = UTIL::get_root_url() . "login.php?2fa";
		else {
			$responce['url'] = $this->getDestinationPage();
			AUDIT::add($this->getDatabase(), AUDIT::LOGGED_IN, null, $_SESSION[self::USERNAME]);
		}
		
		return $responce;
	}
	
	###########################################################################
	
	public function Authenticate($user, $pass) 
	{
		$setup = new \CodePunch\Config\Settings($this);
		$maxattempts = $setup->getOption('max_login_attempts', self::DEFAULT_MAX_LOGIN_COUNT);
		$loginlockout = $setup->getOption('login_lockout_minutes', self::DEFAULT_LOGIN_LOCKOUT_MINUTES);
		if($this->getDatabase() && $this->getDatabase()->isLoginAllowed($maxattempts, $loginlockout, $timewait)) {
			$responce = $this->getDatabase()->validateLogin($user, $pass);
			if($responce['status'] != 'ok') {
				if($responce['status'] != 'blocked') {
					if($this->checkDefaultLogin($user, $pass) == true) {
						$responce['error'] = "";
						$responce['status'] = 'ok';
						$responce['user'] = $user;
						$responce['userid'] = 0;
						$responce['setup'] = 1;
						$responce['admin'] = 1;
					}
				}
			}
			$this->loginCheckCompleted($responce);
			if($responce['status'] == 'ok') {
				$this->getDatabase()->authenticationLog(true, $user, $responce['userid']);
				$responce = self::processPrimaryLogin($responce);
				$salt = substr(md5(uniqid(rand(), true)), 0, 25);
				$ipaddr = (isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : "0.0.0.0");
				$token = $salt . ';' . session_id() . ';' . time() . ';' . $ipaddr;
				$responce['etoken'] = $this->encrypt($token);
			}
			else {
				$userid = isset($responce['userid']) ? $responce['userid'] : self::INVALID_USERID;
				$failcount = $this->getDatabase()->authenticationLog(false, $user, $userid);
				$responce['error'] .= " ($failcount/$maxattempts)";
				
				AUDIT::add($this->getDatabase(), AUDIT::LOGIN_ATTEMPT_FAIL, null, $user);
			}
		}
		else if(!$this->getDatabase()) {
			$responce['status'] = "notok";
			$responce['error'] = "Unable to connect to database. Please check if license has expired.";
		}
		else {
			$responce['status'] = "notok";
			$responce['error'] = "Too many failed login attempts. Please wait $timewait seconds.";
		}
		return $responce;
	}
	
	###########################################################################
	
	public static function isAdmin()
	{
		if(self::isLoggedIn())
			return UTIL::get_from_array($_SESSION[\CodePunch\Config\Auth::USERINFO]['admin'], false);
		return false;
	}
	
	###########################################################################
	
	public static function isSetupAdmin()
	{
		if(self::isLoggedIn())
			return UTIL::get_from_array($_SESSION[\CodePunch\Config\Auth::USERINFO]['setup'], false);
		return false;
	}
	
	###########################################################################
	
	public function getUserID()
	{
		if(isset($_SESSION[\CodePunch\Config\Auth::USERINFO]['userid']) && $this->validateSession(false, false) == self::VALID) 
			return $_SESSION[\CodePunch\Config\Auth::USERINFO]['userid'];
		return false;
	}
	
	###########################################################################
	
	public function getUserName()
	{
		if($this->validateSession(false, false) == self::VALID)
			return UTIL::get_from_array($_SESSION[self::USERNAME], "");
		return "";
	}
	
	###########################################################################
	
	public function getUserAccess()
	{
		if($this->isAdmin()) 
			return \CodePunch\DB\DomainDB::ALLOW_ADMINLEVEL;
		else {
			$validate = $this->validateSession(false, false);
			if($validate == self::VALID && isset($_SESSION[self::USERNAME])) {
				$user = $_SESSION[self::USERNAME];
				$rights = $this->getDatabase()->findOneOf($this->getDatabase()->getUserTableName(), "name", $user, "rights");
				if($rights !== false)
					return $rights;
			}
			return \CodePunch\DB\DomainDB::ALLOW_NONE;
		}
	}
	
	###########################################################################
	
	public function getSessionTimeout()
	{
		$setup = new \CodePunch\Config\Settings($this);
		$auth_timeout_minutes = $setup->getOption('auth_timeout_minutes', self::DEFAULT_SESSION_TIMEOUT);
		if($auth_timeout_minutes > 0) {
			if(isset($_SESSION[self::TIMEOUT])) {
				$tseconds = $auth_timeout_minutes * 60;
				return $_SESSION[self::TIMEOUT] + $tseconds - time();
			}
		}
		return "";
	}
	
	###########################################################################

	public static function invalidSessionQuit()
	{
		header("HTTP/1.0 400 Bad Request");
		echo "Error: Invalid Session. Please login again.";
		exit;
	}

	###########################################################################

	public static function noPermissionQuit()
	{
		header("HTTP/1.0 403 Forbidden");
		echo "Permission Denied.";
		exit;
	}
	
	###########################################################################
	
	public static function authorizationQuit($validate) 
	{
		if($validate == Auth::TIMEDOUT)
			Auth::invalidSessionQuit();
		else if($validate != Auth::VALID)
			Auth::noPermissionQuit();
	}
	
	###########################################################################
	
	public function getRemoteKey($reset=false)
	{
		$clikey = "";
		$validate = $this->validateSession(false, false);
		if($validate == self::VALID || $validate == self::UNKNOWN) {
			$setup = new \CodePunch\Config\Settings($this);
			$clikey = $setup->getEncryptedOption('cli_auth_key_code', "");
			if(($clikey == "" || $reset !== false) && $validate == self::VALID) {
				$clikey = strtolower(UTIL::random_string());
				$setup->setEncryptedOption('cli_auth_key_code', $clikey);
			}
		}
		return strtolower($clikey);
	}
	
	###########################################################################
	# Remote Key is used only to run the data processor over the web. It
	# can't be used for accessing any data. Any real outputs are suppressed 
	# when the remote key is used.
	
	public function isValidRemoteKey($key)
	{
		if($key != "" && $key == $this->getRemoteKey())
			return true;
		return false;
	}
	
	###########################################################################
	
	public function hasAnAdminUserinDB()
	{
		$db = $this->getDatabase();
		if($db) {
			$rows = $db->getFromTable("*", $db->getUserGroupAccessTableName(), "gid=1");
			if($rows !== false && count($rows)) {
				$index = 0;
				foreach($rows as $row) {
					$ui = $db->fetchRow($rows, $index++);
					$uid = $ui['userid'];
					if($uid > 0) {
						if($db->hasRow($db->getUserTableName(), "id", $uid))
							return true;
					}
				}
			}
		}
		return false;
	}
	
	###########################################################################
	
	public function hasAnAdminUser()
	{
		if($this->hasSetupAdminUser())
			return true;
		if($this->hasAnAdminUserinDB())
			return true;
		return false;
	}
	
	###########################################################################
	
	public static function getCurrentUserID()
	{
		if(UTIL::is_cli())
			return self::CLI_ACCESS_USERID;
		else if(self::isLoggedIn())
			return isset($_SESSION[self::USERINFO]['userid']) ? $_SESSION[self::USERINFO]['userid'] : self::SETUPADMIN_USERID;
		return self::INVALID_USERID;
	}
	
	###########################################################################

	public function isPrivilegedUser()
	{
		$db = $this->getDatabase();
		if($db) {
			$userid = $this->getCurrentUserID();
			if($userid == self::SETUPADMIN_USERID || $userid == self::CLI_ACCESS_USERID || $db->isAdminuser($userid))
				return true;
		}
		return false;
	}

	###########################################################################
	
	public static function processor() 
	{
		try {
			$auth = new \CodePunch\Config\Auth();
			$cron = new \CodePunch\Config\Cron($auth);
			$maxrows = UTIL::get_request_data_integer("rows", 50, 4, 150);
			$maxseconds = UTIL::get_request_data_integer("time", 40, 5, 50);
			$qi = 0;
			$sedserver = $auth->getServerID();
			// Set a different start index if not master server.
			if(strcasecmp($sedserver, "master")) {
				$db = $auth->getDatabase();
				$servers = $db->getFromTable("name", $db->getInstalledServersTableName(), "", array(), "id", "asc");
				if($servers !== false && is_array($servers)) {
					foreach($servers as $server) {
						$qi++;
						if(!strcasecmp($sedserver, $server['name']))
							break;
					}
				}
			}
			$start = $qi*$maxrows;
			$forcereports = UTIL::is_request_key_set("reports");
			$responce = $cron->process($maxrows, $maxseconds, $start, $forcereports);
			if($responce === false) {
				$auth->validateSession("cron");
				exit;
			}
			if(UTIL::is_cli())
				UTIL::print($responce);
			else {
				ob_start( );
				UTIL::print($responce);
				$pageinfo['body'] = "<p>" . ob_get_clean() . "</p>";
				$pageinfo['heading'] = "<h1>" . TEXT::get("sed_processor") . "</h1>";
				$layout = new \CodePunch\UI\Layout();
				$layout->show($pageinfo);
			}
		}
		catch(Exception $e) {
			$logger = new \CodePunch\Base\CPLogger();
			$logger->error($e->getMessage());
			UTIL::print($e->getMessage());
		}
	}
}

###############################################################################
