<?php


/**
 * This file is part of LEPTON Core, released under the GNU GPL
 * Please see LICENSE and COPYING files in your package for details, specially for terms and warranties.
 *
 * NOTICE:LEPTON CMS Package has several different licenses.
 * Please see the individual license in the header of each single file or info.php of modules and templates.
 *
 * @author          LEPTON Project
 * @copyright       2010-2026 LEPTON Project
 * @link            https://lepton-cms.org
 * @license         https://gnu.org/licenses/gpl.html
 * @license_terms   please see LICENSE and COPYING files in your package
 *
 */



class LEPTON_login extends LEPTON_admin
{
	private string $USERS_TABLE = TABLE_PREFIX."users";
	private string $GROUPS_TABLE = TABLE_PREFIX."groups";

	private int $user_id = -1;
	private string $username = '';
	private string $password = '';
	private string $url = '';
	public string $message = '';

	private string $username_fieldname = "";
	private string $password_fieldname = "";

	private int $max_attempts = MAX_ATTEMPTS;

	private string $warning_url    = "";
	private string $login_url      = "";
	public string $redirect_url    = "";	// must be public

	private string $template_dir = THEME_PATH."/templates";
	private string $template_file = "";

	public bool $frontend = false;
	private string $forgotten_url = "";

	//	Private var that holds the length of the given username
	private int $username_len = 0;

	//	Private var that holds the length of the given password
	private int $password_len = 0;

    /**
     *  @type   object  The reference to the *Singleton* instance of this class.
     *  @notice Keep in mind that a child-object has to have his own one!
     */
    public static $instance;

    /**
     *  L* 5.3
     *  @see: https://www.php.net/manual/de/function.hash.php
     *          especially the User Contributed Notes!
     */
    const FINGERPRINT_HASH_ALGO = "haval128,4";

   /**
     *  Return the instance of this class.
     *
     *  @param  array  $config_array    Assoc array with config vars.
     *  @return object                  The generated instance of theis class
     */
    #[\Override]
    public static function getInstance(array $config_array = [] ): object
    {
        if (null === static::$instance)
        {
            static::$instance = new static($config_array);
        }
        return static::$instance;
    }

    /**
     * @param array $config_array
     */
    public function __construct(array $config_array = [])
	{
        $database = LEPTON_database::getInstance();

        $MESSAGE = LEPTON_core::getGlobal("MESSAGE");

        if (NULL === self::$instance)
	    {
	        self::$instance = $this;
	    }

		// Get configuration values
		// [2.3]
        if (isset($config_array['USERNAME_FIELDNAME']))
		{
		    $this->username_fieldname = $config_array['USERNAME_FIELDNAME'];
		}
		// [2.4]
        if (isset($config_array['PASSWORD_FIELDNAME']))
		{
		    $this->password_fieldname = $config_array['PASSWORD_FIELDNAME'];
		}
		// [2.6]
        if (isset($config_array['WARNING_URL']))
		{
		    $this->warning_url = $config_array['WARNING_URL'];
		}
		// [2.7]
        if (isset($config_array['LOGIN_URL']))
		{
		    $this->login_url = $config_array['LOGIN_URL'];
		}
		// [2.8]
        if (isset($config_array['TEMPLATE_DIR']))
		{
		    $this->template_dir = $config_array['TEMPLATE_DIR'];
		}
		// [2.9]
        if (isset($config_array['TEMPLATE_FILE']))
		{
		    $this->template_file = $config_array['TEMPLATE_FILE'];
		}
		// [2.11]
        if (isset($config_array['frontend']))
        {
            $this->frontend = $config_array['frontend'];
        }
        // [2.12]
        if (isset($config_array['FORGOTTEN_URL']))
		{
		    $this->forgotten_url = $config_array['FORGOTTEN_URL'];
        }
        // [2.13]
		if (isset($config_array['REDIRECT_URL']))
		{
			$this->redirect_url = $config_array['REDIRECT_URL'];
		}
		else
		{
			$this->redirect_url = LEPTON_core::getValue('redirect','string_clean','request') ?? "";
		}

		// Get the supplied username and password
		if (!is_null(LEPTON_core::getValue('username_fieldname','username','post')))
		{
			$username_fieldname = LEPTON_core::getValue('username_fieldname','username','post');
			$password_fieldname = LEPTON_core::getValue('password_fieldname','username','post');
		}
		else
		{
			$username_fieldname = 'username';
			$password_fieldname = 'password';
		}

		$this->username = LEPTON_core::getValue($username_fieldname,'clean_string','post') ?? "";

		$this->password = LEPTON_core::getValue($password_fieldname,'password','post') ?? "";

		// Get the length of the supplied username and password
		$this->username_len = strlen($this->username);
		$this->password_len = strlen($this->password);

		// If the url is blank, set it to the default url		
        if ($this->redirect_url != '')
		{
			$this->url = $this->redirect_url;
		}
		else
		{
			$this->url = LEPTON_core::getValue('url') ?? '';			
		}

        if (strlen($this->url) < 2)
		{
			$token = (!LEPTOKEN_LIFETIME) ? '' : '?leptoken=' . $this->createLepToken();
			$this->url = ($config_array['DEFAULT_URL'] ?? "") . $token;
		}

		if ($this->is_authenticated() === true)
		{
			// User already logged-in, so redirect to default url
			header('Location: '.$this->url);
			exit();
		}
		elseif ($this->username == '')
		{
			$this->message = $MESSAGE['LOGIN_USERNAME_BLANK'];
			$this->increase_attempts();
		}
		elseif ($this->password == '')
		{
			$this->message = $MESSAGE['LOGIN_PASSWORD_BLANK'];
			$this->increase_attempts();
		}
		elseif ($this->username_len < AUTH_MIN_LOGIN_LENGTH)
		{
			$this->message = $MESSAGE['LOGIN_USERNAME_TOO_SHORT'];
			$this->increase_attempts();
		}
		elseif ($this->password_len < AUTH_MIN_PASS_LENGTH)
		{
			$this->message = $MESSAGE['LOGIN_PASSWORD_TOO_SHORT'];
			$this->increase_attempts();
		}
		else
		{
			if ($this->authenticate())
			{
				// Authentication successful
				$token = (!LEPTOKEN_LIFETIME) ? '' : '?leptoken=' . $this->createLepToken();

				/**
				 *	reset the temp Counter
				 */
				$browser_fingerprint = hash( self::FINGERPRINT_HASH_ALGO, $_SERVER['HTTP_USER_AGENT'] );
				$ip_fingerprint = hash(self::FINGERPRINT_HASH_ALGO, $_SERVER['REMOTE_ADDR'] );

				$fields = [
                    'temp_active' => 1,
                    'temp_count' => 0,
                    'temp_time' => TIME()
                ];

				$database->build_and_execute(
					'update',
					TABLE_PREFIX."temp",
					$fields,
					"temp_ip = '".$ip_fingerprint."' AND temp_browser='".$browser_fingerprint."'"
				);
				// 	End: reset


				// check for tfa use local PIN
                if (TFA === 'local' && LEPTOKEN_LIFETIME > 0)    // second step in process set vars
				{
					$oTFA = LEPTON_tfa::getInstance();
					$oTFA->initialize(intval($_SESSION['USER_ID']));

					if (isset($_REQUEST['redirect'])) // frontend account
					{
						if ($oTFA->key_new === true)
						{
							if (isset($_POST['submit']) )
							{
								$oTFA->set_fe_pin('create');
								exit();
							}
							else
							{
								header('Location: '.LEPTON_URL.'/account/logout.php');
								exit();
							}
						}
						else
						{
                            if (isset($_POST['submit']))
							{
								$oTFA->display_fe_pin('display');
								exit();
							}
							else
							{
								header('Location: '.LEPTON_URL.'/account/logout.php');
								exit();
							}
						}
					}
					else    // backend directory
					{
						if ($oTFA->key_new === true)
						{
							if (isset($_POST['submit']))
							{
								$oTFA->set_be_pin('create');
								exit();
							}
							else
							{
								header('Location: '.ADMIN_URL.'/logout/index.php');
								exit();
							}
						}
						else
						{
							if (!isset($_POST['lkey']))
							{
								$oTFA->display_be_pin('display');
								exit();
							}
							else
							{
								header('Location: '.ADMIN_URL.'/logout/index.php');
								exit();
							}
						}
					}
				}


				// check for tfa to send TAN via mail
				if (TFA === 'mail' && LEPTOKEN_LIFETIME > 0)
				{
					$oTFA = LEPTON_tfa::getInstance();
					$oTFA->initialize(intval($_SESSION['USER_ID']));
					if(isset($_REQUEST['redirect'])) // frontend account
					{
						// send PIN via mail to user
						$oTFA->display_fe_pin('display');
						exit();
					}
					else    // backend directory
					{
						if (!isset($_POST['lkey']) )
						{
							// send PIN via mail to user
							$oTFA->display_be_pin('display');
							exit();
						}
						else
						{
							header('Location: '.ADMIN_URL.'/logout/index.php');
							exit();
						}

					}
				}
				// end check for tfa

				header("Location: ".$this->url . $token);
				exit(0);
			}
			else
			{
				$this->message = $MESSAGE['LOGIN_AUTHENTICATION_FAILED'];
				$this->increase_attempts();
			}
		}
	}

	// Authenticate the user (check if they exist in the database)

    /**
     * @return bool
     */
    public function authenticate(): bool
	{
		$database = LEPTON_database::getInstance();

		// Get user information
        $loginName = (preg_match('/[\;\=\&\|\<\> ]/', $this->username) ? '' : $this->username);
		$results_array = [];
		$database->execute_query(
			'SELECT `password` FROM `'.$this->USERS_TABLE.'` WHERE `username` = "'.$loginName.'" AND `active` = 1',
			true,
			$results_array,
			false
		);

        if (!empty($results_array))
		{
			$check = password_verify($this->password,$results_array['password']);

            if ($check != true)
            {
				return false;
		    }

            $authenticated_user = [];
            $database->execute_query(
                'SELECT * FROM `'.$this->USERS_TABLE.'` WHERE `username` = "'.$loginName.'" AND `active` = 1',
                true,
                $authenticated_user,
                false
            );

            $this->user_id = intval($authenticated_user['user_id']);
            $_SESSION['USER_ID'] =$this->user_id;
            $_SESSION['GROUPS_ID'] = $authenticated_user['groups_id'];
            $_SESSION['USERNAME'] = $authenticated_user['username'];
            $_SESSION['DISPLAY_NAME'] = $authenticated_user['display_name'];
            $_SESSION['EMAIL'] = $authenticated_user['email'];
            $_SESSION['HOME_FOLDER'] = $authenticated_user['home_folder'];

            // Set language
            if ($authenticated_user['language'] != '')
            {
                $_SESSION['LANGUAGE'] = $authenticated_user['language'];
            }

            // Set timezone
            if ($authenticated_user['timezone_string'] != '')
            {
                $_SESSION['TIMEZONE_STRING'] = $authenticated_user['timezone_string'];
            }
            $timezone_string = ($_SESSION['TIMEZONE_STRING'] ?? DEFAULT_TIMEZONE_STRING);
            date_default_timezone_set($timezone_string);

            // Set date format
            if ($authenticated_user['date_format'] != '')
            {
                $_SESSION['DATE_FORMAT'] = $authenticated_user['date_format'];
            }
            else
            {
                // Set a session var so apps can tell user is using default date format
                $_SESSION['USE_DEFAULT_DATE_FORMAT'] = true;
            }
            // Set time format
            if ($authenticated_user['time_format'] != '')
            {
                $_SESSION['TIME_FORMAT'] = $authenticated_user['time_format'];
            }
            else
            {
                // Set a session var so apps can tell user is using default time format
                $_SESSION['USE_DEFAULT_TIME_FORMAT'] = true;
            }

            // Get group information
            $_SESSION['SYSTEM_PERMISSIONS'] = [];
            $_SESSION['MODULE_PERMISSIONS'] = [];
            $_SESSION['GROUP_NAME'] = [];

            $first_loop = true;		// there are more loops to go
            foreach (LEPTON_core::getValue('groups_id','string','session',',') as $cur_group_id)
            {
                $results_array_2 = [];
                $database->execute_query(
                    "SELECT * FROM ".$this->GROUPS_TABLE." WHERE group_id = ".$cur_group_id,
                    true,
                    $results_array_2,
                    false
                );

                if (empty($results_array_2))
				{
					continue;
				}

                $_SESSION['GROUP_NAME'][$cur_group_id] = $results_array_2['name'];

                // Set system permissions
                $_SESSION['SYSTEM_PERMISSIONS'] = array_merge($_SESSION['SYSTEM_PERMISSIONS'], explode(',', $results_array_2['system_permissions']));

                // Set module permissions
                if ($first_loop)
				{
                    $_SESSION['MODULE_PERMISSIONS'] = explode(',', $results_array_2['module_permissions']);
                }
                else
                {
                    $_SESSION['MODULE_PERMISSIONS'] = array_merge($_SESSION['MODULE_PERMISSIONS'], explode(',', $results_array_2['module_permissions']));
                }
				
                $first_loop = false;
            }

            if (false === $this->frontend)
            {
                // prevent users with no backend access
                $aTempGroups = explode(",",$authenticated_user['groups_id']);
                $bGotBackendAccess = false;
                foreach($aTempGroups as $tempGroupId)
                {
                    $check_backend = intval( $database->get_one("SELECT `backend_access` FROM `".$this->GROUPS_TABLE."` WHERE `group_id` = ".$tempGroupId ) );
                    if( 1 === $check_backend)
                    {
                        $bGotBackendAccess = true;
                        $_SESSION['BACKEND_ACCESS'] = $check_backend;
                        break;
                    }
                }
                if (false === $bGotBackendAccess)
                {
					$_SESSION = [];				
                    LEPTON_securecms::clearLepTokens();
					header('Location:'.LEPTON_URL);
					exit();
                }
            }


			// Update the users table with current ip and timestamp
			$fields= [
				"login_when" => time(),
				"login_ip"	=> $_SERVER['REMOTE_ADDR']
			];

			$database->build_and_execute(
				"update",
				$this->USERS_TABLE,
				$fields,
				"user_id = ".$authenticated_user['user_id']
			);
			
			return true;
		}
		else
		{		
			// User doesn't exists
			return false;
		}
	}

	// Increase the count for login attempts

    /**
     * @return void
     */
    public function increase_attempts(): void
	{
		$this->test_attempts();

        if (!isset($_SESSION['ATTEMPS']))
		{
			$_SESSION['ATTEMPS'] = 0;
		}
		else
		{
			$_SESSION['ATTEMPS']++;
		}
		$this->display_login();
	}

	// Display the login screen

    /**
     * @return void
     */
    public function display_login(): void
	{
		// Get language vars
		global $MESSAGE;
		global $MENU;
		global $TEXT;

		// If attempts more than allowed, warn the user
        if ($_SESSION['ATTEMPS'] > $this->max_attempts)
		{
			$this->warn();
		}

		// Show the login form
        if ($this->frontend === false)
		{
            $login_values = [
                'ACTION_URL'             => $this->login_url,
                'ATTEMPS'                => LEPTON_core::getValue('ATTEMPS','integer','session'),
                'USERNAME'               => $this->username,
                'USERNAME_FIELDNAME'     => $this->username_fieldname,
                'PASSWORD_FIELDNAME'     => $this->password_fieldname,
                'MESSAGE'                => $this->message,
                'LEPTON_URL'             => LEPTON_URL,
                'THEME_URL'              => THEME_URL,
                'LEPTON_VERSION'         => LEPTON_VERSION,
                'LANGUAGE'               => strtolower(LANGUAGE),
                'FORGOTTEN_URL'  		 => $this->forgotten_url,
                'TEXT_FORGOTTEN_DETAILS' => $TEXT['FORGOTTEN_DETAILS'],
                'TEXT_USERNAME'          => $TEXT['USERNAME'],
                'TEXT_PASSWORD'          => $TEXT['PASSWORD'],
                'TEXT_LOGIN'             => $MENU['LOGIN'],
                'TEXT_HOME'              => $TEXT['HOME'],
                'PAGES_DIRECTORY'        => PAGES_DIRECTORY,
                'SECTION_LOGIN'          => $MENU['LOGIN'],
                'CHARSET'                => DEFAULT_CHARSET
            ];

			$oTWIG = lib_twig_box::getInstance();
			$oTWIG->loader->prependPath( $this->template_dir, "backend" );

			echo $oTWIG->render(
				"@backend/".$this->template_file,
				$login_values
			);
        }
    }

    // Warn user that they have had to many login attempts
    public function warn()
    {
        header('Location: '.$this->warning_url);
        exit();
    }

	/**
	 *	Internal counter for the failed attempts.
	 *
	 *	@since	LEPTON-CMS 2.3
	 *	@access	private
	 *
	 */
	private function test_attempts()
	{
		global $database;

		$database->simple_query("DELETE from `".TABLE_PREFIX."temp` WHERE `temp_time` < '".(time()-3600)."'");

		$browser_fingerprint = hash(self::FINGERPRINT_HASH_ALGO, $_SERVER['HTTP_USER_AGENT'] );
		$ip_fingerprint = hash(self::FINGERPRINT_HASH_ALGO, $_SERVER['REMOTE_ADDR'] );

		$info = [];
		$database->execute_query(
            "SELECT * FROM `" . TABLE_PREFIX . "temp` WHERE `temp_ip` = '" . $ip_fingerprint . "'",
			true,
			$info,
			false
		);

        if (empty($info))
		{
			// no entry for this ip
			$fields = [
                'temp_ip' => $ip_fingerprint,
                'temp_browser' => $browser_fingerprint,
                'temp_time' => TIME(),
                'temp_count' => 1,
                'temp_active' => 1
			];

			$database->build_and_execute(
				'insert',
				TABLE_PREFIX."temp",
				$fields
			);

		}
		else
		{
			//	is active?
            if (intval($info['temp_active']) == 0)
			{
                if ($info['temp_time'] + 3600 <= time())
				{
					// zeit abgelaufen ... counter wieder auf 1
					$fields = [
						'temp_active'	=> 1,
						'temp_count'	=> 1,
						'temp_time'	=> TIME()
					];

					$database->build_and_execute(
						'update',
						TABLE_PREFIX."temp",
						$fields,
                        "`temp_id`='" . $info['temp_id'] . "'"
					);

				}
				else
				{
					//	In the time-range!
					$this->warn();
				}

			}
			else
			{
				$actual_count = ++$info['temp_count'];

                if ($actual_count > $this->max_attempts)
				{
					// Too mutch attempts
					$fields = [
						'temp_active'	=> 0,
						'temp_time'	=> TIME()
					];

					$database->build_and_execute(
						'update',
						TABLE_PREFIX."temp",
						$fields,
                        "`temp_id`='" . $info['temp_id'] . "'"
					);

					$this->warn();

				}
				else
				{
					//	 Insert the actual couter with the current time
					$fields = [
                        'temp_count' => $actual_count,
                        'temp_time' => TIME()
					];

					$database->build_and_execute(
						'update',
						TABLE_PREFIX."temp",
						$fields,
                        "`temp_id`='" . $info['temp_id'] . "'"
					);
				}
			}
		}
	}
}