<?php

declare(strict_types=1);

/**
 * 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
 *
 */

/**
 *  Core class for some basic actions to handle, e.g. install or drop tables, 'register' framework-functions etc.
 *
 */ 
class LEPTON_handle
{
    const HTACCESS_PATH = SECURE_PATH.'/.htaccess';
    const HTPASSWD_PATH = SECURE_PATH.'/.htpasswd';
	const ALT_BACKEND_FILE = '/templates/'.DEFAULT_THEME.'/backend/backend/';	// alternative backend file
	const ALT_FRONTEND_FILE = '/templates/'.TEMPLATE.'/frontend/'; // alternative frontend file
	
    //  [0] class-constants
    const SPECIAL_CHARS_RESTORE = [
        "&amp;lt;"  => "<",
        "&lt;"      => "<",
        "&amp;gt;"  => ">",
        "&gt;"      => ">",
        "&amp;amp;" => "&",
        "&amp;"     => "&",
        "&quot;"    => "\"",
        "&#039;"    => "'"
    ];
	
    /**
     *  Display errors (e.g. from LEPTON_database query results)?
     *
     *	@property bool	For the use of displaying error messages.
     *
     */
    static public bool $display_errors = true;
	
    /**
     *	Method to change the display_errors.
     *  As the property is static we can't set it "outside" direct." 
     *
     *	@param	boolean $bUseDisplay True to use LEPTON_tools::display.
     *
     */
    static public function setDisplay(bool $bUseDisplay = true): void
    {
        self::$display_errors = $bUseDisplay;
    }
    
    /**
     * Called if a method is unknown.  
     * 
     * @param string $name
     * @param array $arguments
     */
    public function __call(string $name, array $arguments): void
    {
        $msg  = "Unknown class-method: '".$name."'\nParams:\n";
        $msg .= LEPTON_tools::display($arguments, "pre");
        echo LEPTON_tools::display($msg, "pre", "ui message red");
    }
    
    /**
     * Called if a static method is unknown.  
     * 
     * @param string $name
     * @param array $arguments
     */
    public static function __callStatic(string $name, array $arguments): void
    {
        $msg  = "Unknown static class-method: '".$name."'\nParams:\n";
        $msg .= LEPTON_tools::display($arguments, "pre");
        echo LEPTON_tools::display($msg, "pre", "ui message red");
    }
    
    /**
     *	install table
     *	@param string $table_name for table_name
     *	@param string $table_fields for table_fields
     *
     *	@code{.php}
     *      $table_name = 'mod_test';
     *      $table_fields='
     *          `id` INT(10) UNSIGNED NOT NULL,
     *          `edit_perm` VARCHAR(50) NOT NULL,
     *          `view_perm` VARCHAR(50) NOT NULL,
     *          PRIMARY KEY ( `id` )
     *      ';
     *      LEPTON_handle::install_table($table_name, $table_fields);
     *
     *	@endcode
     *	@return bool True if successful, otherwise false.
     *
     */	
    static public function install_table(string $table_name='', string $table_fields=''): bool
    {
		if (($table_name == '') || ($table_fields == '')) {
			return false;
		}
		$database = LEPTON_database::getInstance();
        $table = TABLE_PREFIX . $table_name;
        $database->simple_query("CREATE TABLE `" . $table . "`  (" . $table_fields . ") ");
		
		return true;
	}
	
	/**
	 *	encrypt fields in a table as an upgrade routine
	 *	@param string $table_name for table_name
	 *	@param array $aListOfFields for aListOfFields
	 *	@param string $field_condition for field_condition
	 *
	 *	@code{.php}
	 *	LEPTON_handle::encrypt_table($table_name,$aListOfFields,$field_condition);
	 *	@endcode
	 *	@return boolean true if successful
	 *	 
	 *	@example
	 *	$table_name = 'mod_test';
	 *	$aListOfFields = array('name1','name2');
	 *	$field_condition = 'test_id'; 
	 *	LEPTON_handle::encrypt_table($table_name,$aListOfFields,$field_condition);
	 *		 
	 */	
	static public function encrypt_table(string $table_name ='', array $aListOfFields =[], string $field_condition =''): bool
    {
        if ($table_name == '')
        {
			return false;
		}
        if (!is_array($aListOfFields))
		{
		    LEPTON_tools::display("REQUIRED list of names nof table fields must be an array!", "div", "ui red message");
		    return false;
		}

		if ( $field_condition == '' ) {
			return false;
		}
		
		$database = LEPTON_database::getInstance(); 
		self::create_sik_table($table_name);  // keep in mind, that drop_table adds the table_prefix
		
		//get table content
		$table_content = array();
		$database->execute_query(
			"SELECT * FROM ".TABLE_PREFIX.$table_name." ",
			true,
			$table_content,
			true
		);

        foreach ($table_content as $to_encrypt)
        {
            $database->secure_build_and_execute('UPDATE', TABLE_PREFIX . $table_name, $to_encrypt, '' . $field_condition . ' =' . $to_encrypt[$field_condition], $aListOfFields);
		}
		
		return true;
	}	


	/**
	 *	decrypt fields in a table as an upgrade routine
	 *	@param string $table_name for table_name
	 *	@param array $aListOfFields for aListOfFields
	 *	@param string $field_condition for field_condition
	 *
	 *	@code{.php}
	 *	LEPTON_handle::decrypt_table($table_name,$aListOfFields,$field_condition);
	 *	@endcode
	 *	@return boolean true if successful
	 *	 
	 *	@example
	 *	$table_name = 'mod_test';
	 *	$aListOfFields = array('name1','name2');
	 *	$field_condition = 'test_id'; 
	 *	LEPTON_handle::decrypt_table($table_name,$aListOfFields,$field_condition);
	 *		 
	 */
    static public function decrypt_table(string $table_name = '', array $aListOfFields = [], string $field_condition = '')
    {
        if ($table_name == '')
		{
			return false;
		}

        if (!is_array($aListOfFields))
		{
            LEPTON_tools::display("REQUIRED list of names of table-fields must be an array!", "div", "ui red message");
		    return false;
		}

        if ($field_condition == '')
		{
			return false;
		}
		
		$database = LEPTON_database::getInstance();
        self::create_sik_table($table_name);  // keep in mind, that drop_table adds the table_prefix
		
		//get table content
		$table_content = array();
		$database->secure_execute_query(
            "SELECT * FROM " . TABLE_PREFIX . $table_name . " ",
			true,
			$table_content,
			true,
			$aListOfFields
		);

        foreach ($table_content as $to_decrypt)
        {
		    $result = $database->build_and_execute(
                'UPDATE', TABLE_PREFIX.$table_name,
                $to_decrypt,
                $field_condition.' ='.$to_decrypt[$field_condition]
            );
		}
		
		return true;
	}	
	
	/**
	 *	insert data into table
	 *	@param string $table_name for table_name
	 *	@param string $field_values for table_fields
	 *
	 *	@code{.php}
	 *	$table_name = 'mod_test';
	 *	$field_values="
	 *		(1, 'module_order', 'wysiwyg', ''),
	 *		(2, 'max_excerpt', '15', ''),
	 *		(3, 'time_limit', '0', '')
	 *	";
	 *	LEPTON_handle::insert_values($table_name, $field_values);
	 *
	 *	@endcode
	 *	@return bool True if successful, otherwise false.
	 *
	 */	
	static public function insert_values(string $table_name='', string $field_values =''): bool
    {
        if (($table_name == '') || ($field_values == '')) {
			return false;
		}

		$database = LEPTON_database::getInstance();
        $table = TABLE_PREFIX . $table_name;
        $database->simple_query("INSERT INTO `" . $table . "`  VALUES " . $field_values . " ");
		
		return true;
	}	
	
	
	/**
	 *	drop table
	 *	@param string $table_name for table_name
	 *
	 *	@code{.php}
	 *	$table_name = 'mod_test';
	 *	LEPTON_handle::drop_table($table_name);
	 *
	 *	@endcode
	 *	@return bool True if successful.
	 *
	 */	
	static public function drop_table(string $table_name=''): bool
    {
		if ($table_name == '')
        {
			return false;
		}
        $database = LEPTON_database::getInstance();
        $table = TABLE_PREFIX . $table_name;

        $database->simple_query("DROP TABLE IF EXISTS `" . $table . "` ");
		
		return true;
	}
	
	/**
	 *	rename table
	 *	@param string $table_name for table_name
	 *
	 *	@code{.php}
	 *	LEPTON_handle::rename_table($table_name);
	 *	@endcode
	 *	@return boolean true if successful
	 *
	 */	
	static public function rename_table(string $table_name =''): bool
    {
        if ($table_name == '')
		{
			return false;
		}
		$database = LEPTON_database::getInstance();
        $table_source = TABLE_PREFIX . $table_name;
        $table_target = TABLE_PREFIX . 'xsik_' . $table_name;
        self::drop_table('xsik_' . $table_name);
        $database->simple_query("RENAME TABLE `" . $table_source . "` TO `" . $table_target . "` ");
		
		return true;
	}

	/**
	 *	create sik table
	 *	@param string $table_name for table_name
	 *
	 *	@code{.php}
	 *	LEPTON_handle::create_sik_table($table_name);
	 *	@endcode
	 *	@return boolean true if successful
	 */	
	static public function create_sik_table(string $table_name =''): bool
    {
        if ($table_name == '')
		{
			return false;
		}
		$database = LEPTON_database::getInstance();
        $table_source = TABLE_PREFIX . $table_name;
        $table_target = TABLE_PREFIX . 'xsik_' . $table_name;
        self::drop_table('xsik_' . $table_name);  // keep in mind, that drop_table adds the table_prefix
        $database->simple_query("CREATE TABLE `" . $table_target . "` LIKE `" . $table_source . "`");
        $database->simple_query("INSERT INTO `" . $table_target . "` SELECT * FROM `" . $table_source . "`");
		
		return true;
	}

    /**
     *    Delete obsolete files
     *
     *    @param string|array $aFileNames    A linear array with filenames to delete. Since L* IV this method could handle more than one param.
     *
     *    @code{.php}
     *    $file_names = array(
     *      '/modules/lib_r_filemanager/filemanager/js/ZeroClipboard.swf'
     *    );
     *    LEPTON_handle::delete_obsolete_files ($file_names);
     *
     *    @endcode
     *    @return void
     *
     */
    static public function delete_obsolete_files(string|array ...$aFileNames): void
    {
        if (is_string($aFileNames))
        { 
            $aFileNames = [ $aFileNames ];
        }

        foreach ($aFileNames as $del)
        {
            if (is_array($del))
            {
                foreach ($del as $subItem)
                {
                    self::delete_obsolete_files($subItem);
                }
            }
            else
            {
                $temp_path = ((!str_contains($del, LEPTON_PATH)) ? LEPTON_PATH : "") . $del;
                if (file_exists($temp_path)) 
                {
                    $result = unlink($temp_path);
                    if (false === $result)
                    {
                        echo "<p>Cannot delete file " . $temp_path . ". Please check file permissions and ownership or delete file manually.</p>";
                    }
                }
            }
        }
    }

	/**
	 *	Delete obsolete directories
	 *	@param array|string $directory_names linear array with directory_names to delete
	 *
	 *	@code{.php}
	 *	$directory_names = array(
	 *	'/modules/lib_r_filemanager/thumbs'
	 *	);
	 *	LEPTON_handle::delete_obsolete_directories($directory_names);
	 *
	 *	@endcode
	 *	@return void
	 */	
	static public function delete_obsolete_directories(array|string $directory_names=[]): void
    {
        if (is_string($directory_names))
        {
            $directory_names = [$directory_names];
        }
		
		self::register('rm_full_dir');

		foreach ($directory_names as $del)
		{
			$temp_path = LEPTON_PATH . $del;
			if (file_exists($temp_path)) 
			{
				$result = rm_full_dir($temp_path);
				if (false === $result)
				{
                    echo "Cannot delete directory ".$temp_path.". Please check directory permissions and ownership or deleted directories manually.";
				}
			}
		}			
	} 

	/**
	 *	rename recursive directories
	 *	@param array|string $directory_names linear array with directory_names to rename as assoziative array
	 *
	 *	@code{.php}
	 *	$directory_names = array(
	 *		array ('source' =>'old_path1', 'target'=>'new_path1'),
	 *		array ('source' =>'old_path2', 'target'=>'new_path2')
	 *	);
	 *	LEPTON_handle::rename_directories ($directory_names);
	 *
	 *	@endcode
	 *	@return void
	 */	
	static public function rename_directories(array|string $directory_names=[]): void
    {
        if (is_string($directory_names))
        {
            $directory_names = [$directory_names];
        }  
		
		self::register('rename_recursive_dirs');
		foreach ($directory_names as $rename)
		{
			$source_path = LEPTON_PATH . $rename['source'];
			$target_path = LEPTON_PATH . $rename['target'];
			if (file_exists($source_path)) 
			{			

				$result = rename_recursive_dirs($source_path, $target_path);
				if (false === $result)
				{
				    echo "Cannot rename file ".$source_path.". Please check directory permissions and ownership manually.";
				}			
				
			}	
		}
	}

    /**
     *  include files
     *  @param array|string $file_names linear array with filenames to include
     *  @param bool $interrupt
     *  @code{.php}
     *    $file_names = array(
     *    '/framework/initialize.php'
     *    );
     *    LEPTON_handle::include_files ($file_names);
     *
     *  @endcode
     *  @return void
     */
    static public function include_files(array|string $file_names=[], bool $interrupt=true): void
    {
        if (is_string($file_names))
        {
            $file_names = [$file_names];
        }        

        foreach ($file_names as $requestedFile)
        {
            $temp_path = LEPTON_PATH . $requestedFile;
            if (file_exists($temp_path)) 
            {
                require_once $temp_path;
            } elseif($interrupt === true)
            {
                echo LEPTON_tools::display(
                    "<pre class='ui message'>\nCan't include: ".$temp_path."\n</pre>",
                    "pre",
                    "ui message orange"
                );
            }
        }            
    }    

	
	/**
	 *	include files if exist and end process of start file
	 *	@param string $file_name linear array with filenames to include.
	 *  @param string $usage Only "backend" or "frontend" supported.
	 *	@code{.php}
	 *	    
	 *	if(LEPTON_handle::require_alternative('access/index.php') ) return 0; 
	 *  
	 *  if(LEPTON_handle::require_alternative('login/login_form.php','frontend') ) return 0; 
	 *  
	 *	@endcode
	 *	@return boolean True, the given file is found and included, otherwise false.
	 */	
	static public function require_alternative(string $file_name, string $usage = 'backend'): bool
    {
		if (($usage == 'frontend') || ($usage == 'backend'))
		{
			$temp_path = LEPTON_PATH.(($usage === 'backend') ? self::ALT_BACKEND_FILE : self::ALT_FRONTEND_FILE).$file_name;
			
			if (file_exists($temp_path)) 
			{
				require_once $temp_path;
				return true;
			}
		}		
		return false;
	}	

	/**
	 *	install modules
	 *	@param array $module_names linear array with module_names to install
	 *
	 *	@code{.php}
	 *	$module_names = array(
	 *		'code2'
	 *	);
	 *	LEPTON_handle::install_modules($module_names);
	 *
	 *	@endcode
	 *	@return void
	 */	
	static public function install_modules(array $module_names = [] ): void
    {
		global $module_name, $module_license, $module_author, $module_directory, $module_version, $module_function, $module_description, $module_platform, $module_guid, $lepton_platform;
        if (is_string($module_names))
        {
			$module_names = [$module_names];
		}			
		LEPTON_handle::register('load_module');
		$database = LEPTON_database::getInstance();

        foreach ($module_names as $temp_addon)
        {
            $test = $database->get_one("SELECT `addon_id` FROM `" . TABLE_PREFIX . "addons` WHERE `directory` = '" . $temp_addon . "' ");

            if (is_null($test))
			{	
				$module_vars = [
					'module_license', 'module_author'  , 'module_name', 'module_directory',
					'module_version', 'module_function', 'module_description',
					'module_platform', 'module_guid'						
				];

                foreach ($module_vars as $varname)
				{
                    if (isset(${$varname}))
                    {
                        unset(${$varname});
                    }
				}
				
				$temp_path = LEPTON_PATH .'/modules/'.$temp_addon ;
				require $temp_path.'/info.php';
				load_module( $temp_addon, true );					
			}											
		}
	}

	
	/**
	 *	upgrade modules
	 *	@param string|array $module_names linear array with module_names to update
	 *
	 *	@code{.php}
	 *	$module_names = array(
	 *	'code2'
	 *	);
	 *	LEPTON_handle::upgrade_modules($module_names);
	 *
	 *	@endcode
	 *	@return void
	 */	
	static public function upgrade_modules(string|array $module_names = []): void
	{
        if (is_string($module_names))
		{
			$module_names = array($module_names);
		}
		
		LEPTON_handle::register('load_module');		
		foreach ($module_names as $update)	
		{
			$temp_path = LEPTON_PATH . "/modules/" . $update . "/upgrade.php";
			if (file_exists($temp_path)) 
			{
				// call upgrade-script direct
				require $temp_path;
				$database = LEPTON_database::getInstance();
				// update db entries
				load_module( $update, false );
				
				// unset module vars
				foreach(
                    [
						'module_license', 'module_author'  , 'module_name', 'module_directory',
						'module_version', 'module_function', 'module_description',
						'module_platform', 'module_guid'
					] as $varname
				)
				{
                    if (isset(${$varname}))
                    {
                        unset(${$varname});
                    }
				}
			}
		}		
	}
	
	/**
	 *	Install droplets.
	 *
	 *	@param string $module_name for module name
	 *	@param string|array  $zip_names for zip name
	 *
	 *	@code{.php}
	 *      $module_name = 'droplets';
	 *      $zip_names = array(
	 *          'droplet_LoginBox'
	 *      );	 
	 *      LEPTON_handle::install_droplets($module_name, $zip_names);
	 *
	 *	@endcode
	 *	@return void
	 */	
	static public function install_droplets(string $module_name='',string|array $zip_names=[]): void
    {
		droplets::getInstance();

        if (is_string($zip_names))
        {
			$zip_names = array($zip_names);
		}
		foreach ($zip_names as $to_install)	
		{
			$temp_path = LEPTON_PATH . "/modules/" . $module_name . "/install/".$to_install.".zip";
			if (file_exists($temp_path)) 
			{
				$result = droplet_install($temp_path, LEPTON_PATH . '/temp/unzip/');
                if (count($result['errors']) > 0)
				{
                    die ('ERROR: file is missing: <b> ' . (implode('<br />\n', $result['errors'])) . ' </b>.');
				}
			}
		}
		self::delete_obsolete_directories(array("/modules/" . $module_name . "/install"));
	}


	/**
	 *	uninstall droplets
	 *	@param string|array $droplet_names for module name
	 *
	 *	@code{.php}
	 *	$droplet_names = array(
	 *	'check-css'
	 *	);	 
	 *	LEPTON_handle::uninstall_droplets($droplet_names);
	 *
	 *	@endcode
	 *	@return void
	 */	
	static public function uninstall_droplets(string|array $droplet_names = []): void
    {
		if (is_string($droplet_names))
        {
			$droplet_names = [$droplet_names];
		}
		$database = LEPTON_database::getInstance();
		
		foreach ($droplet_names as $to_uninstall)
		{
			$to_delete = [];
			$database->execute_query(
				"SELECT `id` FROM ".TABLE_PREFIX."mod_droplets WHERE `name` = '".$to_uninstall."' ",
				true,
				$to_delete,
				false
            );
			if (isset($to_delete['id']))
			{
                $database->simple_query("DELETE FROM `" . TABLE_PREFIX . "mod_droplets` WHERE `id` = " . $to_delete['id']);
                $database->simple_query("DELETE FROM `" . TABLE_PREFIX . "mod_droplets_permissions` WHERE `id` = " . $to_delete['id']);
            }
		}
	}
	
    /**
     *  Static method to "require" a (LEPTON-) internal function file 
     *
     * @return bool
     *
     *  example given:
     *  @code{.php}
     *
     *      //  one single function
     *      LEPTON_handle::register( "get_menu_title" );
     *
     *      //  a set of functions
     *      LEPTON_handle::register( "get_menu_title", "page_tree", "get_page_link" );
     *
     *      //  a set of function names inside an array
     *      $all_needed= array("get_menu_title", "page_tree", "get_page_link" );
     *      LEPTON_handle::register( $all_needed, "rm_full_dir" );
     *
     *  @endcode
     *
     */
    static function register(): bool
    {
        if (0 === func_num_args())
		{
			return false;
		}
		
		$all_args = func_get_args();
		foreach($all_args as &$param)
		{	
			if (true === is_array($param))
			{
				foreach ($param as $ref)
				{
                    self::register($ref);
				}
			} 
			else 
			{
                if (!function_exists($param))
				{
                    $lookUpPath = LEPTON_PATH . "/framework/functions/function." . $param . ".php";
					if (file_exists($lookUpPath))
					{
					    require_once $lookUpPath;
					}
				}
			}
		}
		return true;
	}
	
	/**
	 *  Returns 'false' if the given string contains a non 'valid' char.
	 *  Accepted chars are "a" to "z" (case-insensitive) and "0" to "9" with "-", ".", "_".
	 *   
	 *  @param  string  $sEmail     Any string.
	 *  @return bool                True if all chars matches.
	 *
	 *	@code{.php}
	 *
	 *      $sEmailToCheck = "and 'OR'1'='1'OR'@mail.com";
	 *      $bEmailIsValid = LEPTON_handle::checkEmailChars( $sEmailToCheck );
	 *      // $bEmailIsValid will have boolean "false"
	 *
	 *      $sEmailToCheck = "example.php@toplevel.tld";
	 *      $bEmailIsValid = LEPTON_handle::checkEmailChars( $sEmailToCheck );
	 *      // $bEmailIsValid will have boolean "true"
	 *
	 *	@endcode
	 */
	public static function checkEmailChars(string $sEmail): bool
	{ 
		$add_custom = '';
		$ini_file_name = LEPTON_PATH."/config/lepton.ini.php";
			
        if (true === file_exists($ini_file_name))
		{
        	$config = parse_ini_string(";" . file_get_contents($ini_file_name), true);
            if ($config['custom_vars']['additional_email_chars'] != '')
			{
				$add_custom = $config['custom_vars']['additional_email_chars'];
			}
		}	
        return !((false === filter_var($sEmail, FILTER_VALIDATE_EMAIL)
            || (!preg_match('#^[' .LEPTON_core::getInstance()->email_chars.$add_custom.']+$#', $sEmail))));
    }
	
	/**
	 *  Returns 'false' if the given string contains a non 'valid' char.
	 *  Accepted chars see LEPTON_core->string_secure
	 *   
	 *  @param  string  $sString     Any string.
	 *  @return bool                True if all chars matches.
	 *
	 *	@code{.php}
	 *      $sStringToCheck = "any_string";
	 *	@endcode
	 */
	public static function checkStringSecure(string $sString): bool
	{ 	
		return (preg_match('/^['.LEPTON_core::getInstance()->string_secure.']+$/i', $sString) === 1);
    }		
	
	
	/**
	 *  Returns 'false' if the given string contains a non 'valid' char.
	 *  Accepted chars see LEPTON_core around line 26
	 *   
	 *  @param  string  $sPassword     Any string.
	 *  @return bool                True if all chars matches.
	 *
	 *	@code{.php}
	 *
	 *      $sPasswordToCheck = "and 'OR'1'='1'";
	 *      $bPasswordIsValid = LEPTON_handle::checkPasswordChars( $sPasswordToCheck );
	 *      // $bPasswordIsValid will have boolean "false"
	 *
	 *      $sPasswordToCheck = "1258Ab$yz?";
	 *      $bPasswordIsValid = LEPTON_handle::checkPasswordChars( $sPasswordToCheck );
	 *      // $bPasswordIsValid will have boolean "true"
	 *
	 *	@endcode
	 */
	public static function checkPasswordChars(string $sPassword): bool
	{ 
		$add_custom = '';
        $ini_file_name = LEPTON_PATH . "/config/lepton.ini.php";

        if (true === file_exists($ini_file_name))
		{
            $config = parse_ini_string(";" . file_get_contents($ini_file_name), true);
            if ($config['custom_vars']['additional_password_chars'] != '')
			{
				$add_custom = $config['custom_vars']['additional_password_chars'];
			}
		}	
        return !((false === !preg_match('/[^'.LEPTON_core::getInstance()->password_chars.$add_custom.']/', $sPassword)));
    }	
    

    
    /**
	 *  Returns 'false' if the given string contains a non 'valid' char.
	 *  Accepted chars are "a" to "z" (case-insensitive) and "0" to "9" with "-", ".", "_", " " (space).
	 *   
	 *  @param  string  $sName     Any string.
	 *  @return bool                True if all chars matches.
	 *
	 *	@code{.php}
	 *
	 *      $sNameToCheck = "and 'OR'1'='1'OR'@mail.com";
	 *      $bNameIsValid = LEPTON_handle::checkEmailChars( $sNameToCheck );
	 *      // $bNameIsValid will have boolean "false"
	 *
	 *      $sNameToCheck = "Aldus2 - from LEPTON-CMS";
	 *      $bNameIsValid = LEPTON_handle::checkEmailChars( $sNameToCheck );
	 *      // $bNameIsValid will have boolean "true"
	 *
	 *	@endcode
	 */
    public static function checkUsernameChars(string $sName): bool
    {    
        $add_custom = '';
        $ini_file_name = LEPTON_PATH."/config/lepton.ini.php";

        if (true === file_exists($ini_file_name))
        {
            $config = parse_ini_string(";".file_get_contents($ini_file_name), true);
            if ($config['custom_vars']['additional_usernames_chars'] != '')
            {
                $add_custom = $config['custom_vars']['additional_usernames_chars'];
            }
        }

        return !!preg_match("#^[" . LEPTON_core::getInstance()->username_chars . $add_custom . "]+$#", $sName);
    }

    /**
     *  NOTICE:
     *  Strings (hash) consisting only of integers can be checked with "intval()"
     *    example: $sValueToCheck = intval($StringToCheck);
     *    result: $sValueToCheck is forced to be an integer
     */

    /**
     *  Returns 'false' if the given string contains a non 'valid' char.
     *  Accepted chars are "a" to "f" (case-insensitive) and "0" to "9" with "-"
     *
     *  @param  string  $sHexHash   Any string.
     *  @return bool                True if all chars matches.
     *
     *  @code{.php}
     *
     *      $sHexHash = "and 'OR'1'='1'OR'@mail.com";
     *      $bHashIsValid = LEPTON_handle::checkHexChars( $sHexHash );
     *      // $bHashIsValid will have boolean "false"
     *
     *      $sHexHash = "936DA01F-9ABD-4D9D-80C7-02AF85C822A8";
     *      $bHashIsValid = LEPTON_handle::checkHexChars( $sHexHash );
     *      // $bHashIsValid will have boolean "true"
     *
     *  @endcode
     */
    public static function checkHexChars(string $sHexHash): bool
    { 
        return !!preg_match("~^[".LEPTON_core::getInstance()->hex_chars."]+$~", $sHexHash);
    }    
    
    /**
     *  To "restore" some given special chars. E.g. per default "&lt" will be restored to "<";
     *
     * @param string   $sAnyString     Any valid string. Pass|call by reference.
     * @param array    $aAnyAssocArray Optional an assoc. array with the char-pairs.
     *                                 Default is internal const SPECIAL_CHARS_RESTORE
     * @return void
     */
    public static function restoreSpecialChars(string &$sAnyString, array $aAnyAssocArray = self::SPECIAL_CHARS_RESTORE): void
    {
        $aLookUp  = array_keys($aAnyAssocArray);
        $aReplace = array_values($aAnyAssocArray);
        
        $sAnyString = str_replace($aLookUp, $aReplace, $sAnyString);
    }
    
    
    /**
     *  Create new passwordfiles in standard protected directory
     *
     *  Default directory is '/temp/secure/.htaccess'
     */
    public static function restoreStandardProtection() : bool
    {
        if (file_exists(self::HTACCESS_PATH))
        {
            unlink(self::HTACCESS_PATH);
            unlink(self::HTPASSWD_PATH);
        }
        $admin_username = LEPTON_database::getInstance()->get_one("SELECT username FROM " . TABLE_PREFIX . "users WHERE user_id = 1 ");
        $htuser = $admin_username;
        $random_value = random_int(100000,999999);
        $htpassword = password_hash((string)$random_value, PASSWORD_DEFAULT );

$htcontent = "# .htaccess-Datei
AuthType Basic
AuthName 'Protected area - Please insert password!'
AuthUserFile ".self::HTPASSWD_PATH."
require user ".$htuser."
";

$htpwcontent = "# Password file, user:".$htuser.", password: ".$random_value."
".$htuser.":".$htpassword."
";

        file_put_contents(self::HTACCESS_PATH,$htcontent);
        file_put_contents(self::HTPASSWD_PATH,$htpwcontent);

        return true;        
    }

    /**
     *  Create passwordfiles in chosen directory
     *
     * @param string $path Default directory is '/temp/secure/'.
     * @param string $username The username the htacess belongs to
     * @return bool
     * @throws Exception
     */
    public static function createStandardProtection(string $path = '/temp/secure/', string $username = ''): bool
    {
        if($username == '')
        {
            echo LEPTON_tools::display('USERNAME is mandantory as second parameter in this method', 'pre', 'ui red message');
            return false;
        }

        if (file_exists(LEPTON_PATH . $path . '/.htaccess'))
        {
            unlink(LEPTON_PATH.$path.'/.htaccess');
            unlink(LEPTON_PATH.$path.'/.htpasswd');
        }

        $htuser = $username;
        $random_value = random_int(100000,999999);
        $htpassword = password_hash((string)$random_value, PASSWORD_DEFAULT );

$htcontent = "# .htaccess-Datei
AuthType Basic
AuthName 'Protected area - Please insert password!'
AuthUserFile ".LEPTON_PATH.$path."/.htpasswd
require user ".$htuser."
";

$htpwcontent = "# Password file, user:".$htuser.", password: ".$random_value."
".$htuser.":".$htpassword."
";

        file_put_contents(LEPTON_PATH . $path . '/.htaccess', $htcontent);
        file_put_contents(LEPTON_PATH . $path . '/.htpasswd', $htpwcontent);
        
        return true;
    }

    /**
     *  Sort given array.
     *  @param  array   $array  Given array
     *  @param  string  $on     Key to sort, or comma separated list of keys.
     *  @param  int     $order  SORT_ASC or SORT_DESC
     *
     *  @return array
     *
     *  @code{.php}
     *      $array_sorted = LEPTON_handle::array_orderby($array_source,'field_to_sort', SORT_DESC);
     *
     *      $aArraySorted = LEPTON_handle::array_orderby($aArraySource,'fieldToSort, secondField', SORT_ASC);
     *
     *  @endcode
     */
    static function array_orderby(array $array, string $on, int $order = SORT_ASC): array
    {
        $new_array = [];
        $sortable_array = [];

        if (!empty($array)) 
        {
            // 1
            $lookUpKeys = explode(",", $on);
            $keys = array_map("trim", $lookUpKeys);
            $keepAssoc = false;
            foreach ($array as $k => $v)
            {
                if (is_array($v))
                {
                    if (isset($v[$keys[0]]))
                    {
                        $sortable_array[$k] = "";
                        foreach ($keys as $term)
                        {
                            if (isset($v[$term]))
                            {
                                $sortable_array[$k] .= $v[$term];
                            }
                        }
                    }
                } else {
                    $sortable_array[$k] = $v;
                    $keepAssoc = true;
                }
            }

            switch ($order)
            {
                case SORT_ASC:
                    asort($sortable_array, SORT_NATURAL|SORT_FLAG_CASE);
                    break;
                
                case SORT_DESC:
                    arsort($sortable_array, SORT_NATURAL|SORT_FLAG_CASE);
                    break;
                
                default:
                    LEPTON_tools::display(__CLASS__." [10023] No order match!", "pre", "ui message red");
                    break;
            }

            foreach ($sortable_array as $k => $v)
            {
                if ($keepAssoc === true)
                {
                    $new_array[$k] = $array[$k];
                } else {
                    $new_array[] = $array[$k];
                }
            }
        }

        return $new_array;
    }

    //  [5.4.1]
    static public function themeExists(string $sThemeName = ""): bool
    {
        $result = LEPTON_database::getInstance()->get_one("SELECT `directory` FROM `".TABLE_PREFIX."addons` WHERE `directory` ='".$sThemeName."'");
        return (!is_null($result));
    }
  
    /**
     * [5.4.2] Copies module-specific theme-files to the installed themes during  
     * installation and/or upgrade. The "basefiles" have to be inside the module-directory  
     * inside the subdirectory "backendthemes" e.g.
     *      ~modules/foldergallery/backendthemes/talgos
     *      ~modules/foldergallery/backendthemes/black_hole
     * 
     * @param   string  $sModuleDirectory   Module-Directory or path to a file inside the module.
     * @return void
     * @code
     *  // 1.1 direct by name
     *  LEPTON_handle::moveThemeFiles( "foldergallery" );
     * 
     *  // 1.2 via a var
     *  LEPTON_handle::moveThemeFiles( $modules_directory );
     * 
     *  // 1.3 the directory (path)
     *  LEPTON_handle::moveThemeFiles( __DIR__ );
     * 
     *  // 1.4 via the filepath of the current file
     *  LEPTON_handle::moveThemeFiles( __FILE__ ); // inside upgrade.php
     * 
     * @endcode
     */
    static public function moveThemeFiles(string $sModuleDirectory = ""): void
    { 
        $sSingleModuleDirectory = self::getModuleDirectory($sModuleDirectory);
        
        // get all themes
        $aAllThemes = self::getAllThemes();
        foreach ($aAllThemes as $sForTheme)
        {
            $sBaseSourcePath = LEPTON_PATH . "/modules/" . $sSingleModuleDirectory . "/backendthemes/" . $sForTheme;
            $sTargetPath = LEPTON_PATH . "/templates/" . $sForTheme . "/backend/" . $sSingleModuleDirectory;

            if (true === file_exists($sBaseSourcePath))
            {
                self::copyThemeFilesRecursive($sBaseSourcePath, $sTargetPath);
            }
        }
    }
    
    //  [5.4.3] Internal
    /**
     * @param string $dirsource
     * @param string $dirdest
     * @param int $deep
     * @return bool
     */
    static public function copyThemeFilesRecursive(string $dirsource, string $dirdest, int $deep = 0): bool
    {
        if (true === is_dir($dirsource))
        {
            if(($deep === 0) && (false === is_dir($dirdest)))
            {
                LEPTON_core::make_dir($dirdest);
            }
            $dir= dir($dirsource);
            while ( $file = $dir->read() )
            {
                if( $file[0] === "." )
                {
                    continue;
                }
                if( !is_dir($dirsource."/".$file) )
                {
                    copy($dirsource . "/" . $file, $dirdest . "/" . $file);
                    LEPTON_core::change_mode($dirdest . "/" . $file);
                } else {
                    LEPTON_core::make_dir($dirdest . "/" . $file);
                    self::copyThemeFilesRecursive($dirsource . "/" . $file, $dirdest . '/' . $file, $deep + 1);
                }
                
            }
            $dir->close();
        }
        if ($deep == 0)
        {
            // Is this a working copy? If not ... we try to remove the unneeded files here
            if (!file_exists(dirname($dirsource, 2) . "/.git"))
            {
                LEPTON_handle::register("rm_full_dir");
                rm_full_dir( $dirsource );
            }
        }
        return true;
    }

    //  [5.4.4] Deletes all "theme" specific files of a given module from all installed themes.
    /**
     * @param string $sModuleDirectory
     * @return void
     */
    static public function removeAllThemeFiles(string $sModuleDirectory = ""): void
    {
        $sSingleModuleDirectory = self::getModuleDirectory($sModuleDirectory);
        $aAllThemes = self::getAllThemes();

        foreach ($aAllThemes as $aTempThemeDirectory)
        {
            self::delete_obsolete_directories(["/templates/" . $aTempThemeDirectory . "/backend/" . $sSingleModuleDirectory]);
        }
    }

    //  [5.4.5] Try to get the module directory
    /**
     * @param string $anyNameOrPath
     * @return string
     */
    static public function getModuleDirectory(string $anyNameOrPath = ""): string
    {
        if (empty($anyNameOrPath))
        {
            return "";
        }
        
        /**
         *  [5.4.5.2] ist the method called from a file inside the given module-dir?
         *  see: https://stackoverflow.com/questions/2960805/php-determine-where-function-was-called-from
         *  This is to avoid the situation that module 'A' can copy/delete theme-files from module 'B'!
         */
        $backtrace = debug_backtrace();
        $aTempTest = [""];
        if (isset($backtrace[1]['file']))
        {
            $aTempTest = self::getPathElements($backtrace[1]['file']);
        }
        
        // [5.4.5.3] try to extract the module directory
        $aTemp = self::getPathElements($anyNameOrPath);
        
        return ($aTemp[0] === $aTempTest[0]) ? $aTemp[0] : "";
    }

    //  [5.4.6] Internal
    /**
     * @return array
     */
    static public function getAllThemes(): array
    {
        $aAllThemes = [];
        LEPTON_database::getInstance()->execute_query(
            "SELECT `directory` FROM `" . TABLE_PREFIX . "addons` WHERE `function` = 'theme'",
            true,
            $aAllThemes,
            true
        );
        $aReturnValues = [];
        foreach($aAllThemes as $aTempTheme)
        {
            $aReturnValues[] = $aTempTheme["directory"];
        }
        return $aReturnValues;
    }
    
    /**
     * [5.4.7] Internal
     * 
     * @param   string  $anyNameOrPath
     * @return  array
     * 
     */
    final static public function getPathElements(string $anyNameOrPath = ""): array
    {
        return explode(
            DIRECTORY_SEPARATOR,
            str_replace(
                LEPTON_PATH . DIRECTORY_SEPARATOR . "modules" . DIRECTORY_SEPARATOR,
                "",
                $anyNameOrPath
        ));   
    }

    /**
     * Generate a globally unique identifier (GUID)
     * Uses COM extension under Windows otherwise
     * create a random GUID in the same style.
     *
     * @return string GUID
     *
     * also generate a hash for form validation
     * @throws Exception
     */
    static public function createGUID(): string
    {
        if (function_exists('com_create_guid'))
        {
            $guid = com_create_guid();
            $guid = strtolower($guid);
            if (str_starts_with($guid, '{'))
            {
                $guid = substr($guid, 1);
            }
            if (strpos($guid, '}') == strlen($guid) - 1)
            {
                $guid = substr($guid, 0, strlen($guid) - 1);
            }
            return $guid;
        }
        else
        {
            return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
                random_int(0, 0xffff),
                random_int(0, 0xffff),
                random_int(0, 0xffff),
                random_int(0, 0xffff),
                random_int(0, 0x0fff) | 0x4000,
                random_int(0, 0x3fff) | 0x8000,
                random_int(0, 0xffff),
                random_int(0, 0xffff)
            );
        }
    }
}
