<?php

declare(strict_types=1);

/**
 * This file is part of an ADDON for use with LEPTON Core.
 * This ADDON is released under the GNU GPL.
 * Additional license terms can be seen in the info.php of this module.
 *
 * @module          lib_search
 * @author          LEPTON Project
 * @copyright       2010-2026 LEPTON Project
 * @link            https://lepton-cms.org
 * @license         https://gnu.org/licenses/gpl.html
 * @license_terms   please see info.php of this module
 *
 */
 

// avoid missing constants
require_once dirname(__DIR__)."/search.constants.php";

class lib_search extends LEPTON_abstract
{
    const FRONTEND_RESULT_URL = LEPTON_URL."/modules/lib_search/frontend_result.php";
    const FRONTEND_PATH       = LEPTON_PATH.'/modules/lib_search/index.php';
    
    private string $error = '';
    private string $message = '';
    private string|null $lang = NULL;	// no further usage in this file! 20260205
    private bool $prompt = true;
        
    // the $setting array hold all settings for the search
    protected array $setting = [];
    
    // the $search_functions hold the module function for the search
    protected array $search_functions = [];
    
    // list of all LEPTON users
    protected array $users = [];
    
    // the search language
    protected string $search_language = 'FR';//LANGUAGE;
    
    // the search path
    protected string $search_path = '';
    
    // the search path, SQL
    protected string $search_path_SQL = '';
    
    // search language SQL query
    protected string $search_language_SQL = '';

    // search language SQL query with table
    protected string $search_language_SQL_table = '';
    
    // search type
    protected string|int $search_type = SEARCH_TYPE_ALL;
    
    // search string
    protected string $search_string = '';
    
    // search words for regex
    protected array $search_words = [];
    
    // search url array
    protected array $search_url_array = [];
    
    // search entities array
    protected array $search_entities_array = [];
    
    // search result array
    protected array $search_result = [];
    
    static public $instance;
    
    public function initialize()
    {
        // required by parent
    }


    /**
     * Constructor for LEPTON_Search
     *
     * @access public
     * @param bool $prompt
     */
    public function __construct(bool $prompt=true)
    {
        global $lang, $page_id;	
        $this->lang = $lang;	// no further usage in this file! 20260205  				
		$this->setPrompt($prompt);		
    }
    
    /**
     * Return a message
     * 
     * @access protected
     * @return string $message
     */
    protected function getMessage(): string
    {
        return $this->message;
    }

    /**
     * Set a message
     *
     * @access protected
     * @param string $message
     */
    protected function setMessage(string $message): void
    {
        $this->message = $message;
    }
    
    /**
     * Check if a message is set.
     * 
     * @access protected
     * @return boolean
     */
    protected function isMessage(): bool
    {
        return (bool) !empty($this->message);
    }

	/**
	 * Return the active error string
	 * 
	 * @access public
     * @return string Internal error message
     */
    public function getError(): string
    {
        return $this->error;
    }

	/**
	 * Set a error
	 * 
	 * @access protected
     * @param string $error
     */
    protected function setError(string $error): void
    {
        $this->error = $error;
    }

    /**
     * Check if an error occurred
     * 
     * @access public
     * @return boolean 
     */
    public function isError(): bool
    {
        return (bool) !empty($this->error);
    }
    
    /**
     * Return if the class should prompt the results or not
     * 
     * @access public
     * @return bool Internal $prompt
     */
    public function getPrompt(): bool
    {
        return $this->prompt;
    }
    
    /**
     * Set prompt for the class
     * 
     * @access public
     * @param boolean $prompt
     */
    public function setPrompt(bool $prompt=true): void
    {
        $this->prompt = $prompt;
    }
    
    /**
     * Return if the class should prompt the results or not
     * Alias for getPrompt()
     * 
     * @access public
     * @return boolean $prompt
     */
    public function isPrompt(): bool
	{
        return ($this->prompt == true);
    }

    /**
     * All stuff for frontend search
     *
     * @access public
     * @return void
     */
    public static function frontend_init(): void
    {
        global $page_id, $page_description, $page_keywords;

        $page_id = 0;
        $page_description = 'Search results';
        $page_keywords = 'Search results';

        define('PAGE_ID',       $page_id);
        define('ROOT_PARENT',   0);
        define('PARENT',        0);
        define('LEVEL',         0);
        define('PAGE_TITLE',    ($GLOBALS[ "TEXT" ]["SEARCH"] ?? "Search") );
        define('MENU_TITLE',    PAGE_TITLE);
        define('MODULE',        '');
        define('VISIBILITY',    'public');
        define('PAGE_CONTENT',  self::FRONTEND_PATH );

        // Find out what the search template is
        $database = LEPTON_database::getInstance();
        $fe_template = $database->get_one("SELECT `value` FROM `" . TABLE_PREFIX . "search` WHERE `name` = 'template'");

        if ($fe_template != '') {
            define('TEMPLATE', $fe_template);
        } else {
            define('TEMPLATE', DEFAULT_TEMPLATE);
        }
//        unset($template);

        // Get the referrer page ID if it exists
        if (isset($_REQUEST['referrer']) && is_numeric($_REQUEST['referrer']) && intval($_REQUEST['referrer']) > 0)
        {
            define('REFERRER_ID', intval($_REQUEST['referrer']));
        } else {
            define('REFERRER_ID', 0);
        }
    }
	
	/**
	 * Load the desired template for content(!) output, execute the template engine and returns the
	 * resulting template (for example -> search.results.lte). This is NOT the frontend-template!
	 *
	 *	@access	protected   
	 *	@param	string	$lte_template - the file name of the template.
	 *	@param	array	$template_data - the data for the template.
	 *	@return	string	The parsed template.
	 *
	 */
	protected function getTemplate(string $lte_template, array $template_data): string
    {      
		$oTWIG = lib_twig_box::getInstance();
		$oTWIG->registerModule('lib_search','lib_search');
		return $oTWIG->render('@lib_search/'.$lte_template, $template_data);
	} 
    
    /**
     * Get the settings for the LEPTON Search
     * 
     * @access protected
     * @return boolean
     */
    protected function getSettings() 
	{
        global $database;

        // set default values
        $this->setting = array(
            CFG_CONTENT_IMAGE => CONTENT_IMAGE_FIRST,
            CFG_SEARCH_DESCRIPTIONS => true,
            CFG_SEARCH_DROPLET => true,
            CFG_SEARCH_IMAGES => true,
            CFG_SEARCH_KEYWORDS => true,
            CFG_SEARCH_LIBRARY => 'lib_search',
            CFG_SEARCH_LINK_NON_PUBLIC_CONTENT => '',
            CFG_SEARCH_MAX_EXCERPTS => 15,
            CFG_SEARCH_MODULE_ORDER => 'wysiwyg',
            CFG_SEARCH_NON_PUBLIC_CONTENT => false,
            CFG_SEARCH_SHOW_DESCRIPTIONS => true,
            CFG_SEARCH_TIME_LIMIT => 0,
            CFG_SEARCH_USE_PAGE_ID => -1,
            CFG_THUMBS_WIDTH => 100
        );
        
        $aDBSettings = [];
        $database->execute_query(
            "SELECT * FROM `".TABLE_PREFIX."search`",
            true,
            $aDBSettings,
            true
        );
        
        if ($database->is_error())
        {
            $this->setError($database->get_error()); 
            return false;
        }

        foreach($aDBSettings as &$field )
        {
            if (isset($this->setting[ $field['name']] ) )
            {
                $this->setting[$field['name']] = $field['value'];
            }
        }
        return true;
    }
    
    /**
     * Walk through the modules and gather all search functions which should 
     * included in the LEPTON search in the this->search_functions array
     * 
     * @access protected
     * @return boolean - true on success and false on error
     */
    protected function checkForModuleSearchFunctions() 
	{
        global $database;

        $this->search_functions = [];

        // get all module directories
        $query_results = array();
		$database->execute_query(
			"SELECT directory FROM " . TABLE_PREFIX . "addons WHERE type = 'module'",
			true,
			$query_results,
			true
		);

        if ($database->is_error())
        { 
            $this->setError($database->get_error() ); 
            return false; 
        }
        
        if (count($query_results) > 0)
        {
            foreach ($query_results as &$module) {
                $file = LEPTON_PATH . '/modules/' . $module['directory'] . '/search.php';
                if (file_exists($file))
                {
                    include_once $file;
                    
                    // add standard search function
                    if (function_exists($module['directory'] . "_search"))
                    {
                        $this->search_functions[$module['directory']] = $module['directory'] . "_search";
                    }
                }
            }
        }
        return true;
    }
    
    /**
     * Create a list with all registered LEPTON users
     * 
     * @access protected
     * @return boolean - true on success
     */
    protected function getUsers() 
	{
        global $database;

        // get all users
        $aAllUsers = [];
        $database->execute_query(
            "SELECT user_id,username,display_name FROM " . TABLE_PREFIX . "users",
            true,
            $aAllUsers,
            true
        );

        if ($database->is_error())
        { 
            $this->setError($database->get_error() ); 
            return false; 
        }        
        // set a "unknown user"
        $this->users = array(
            '0' => array(
                'display_name' => $this->language['- unknown user -'],
                'username' => $this->language['- unknown user -']
            )
        );
        foreach($aAllUsers as &$user)
        {
            $this->users[$user['user_id']] = array(
                'display_name' => $user['display_name'],
                'username' => $user['username']
            );
        }
        return true;
    }
    
    /**
     * Get the path to search into. 
     * The search path is normally not set and blank, you can set the path using
     * $_REQUEST['search_path']. 
     * Use a '%' as wildcard at the BEGINNING of the path, the search adds 
     * automatically a wildcard at the END of the path (SQL like style).
     * 
     * Possible values:
     * 
     *   a single path: 
     *     "/en/" - search only pages whose link starts with "/en/" like 
     *     "/en/search-me.php"
     *   a single path NOT to search into: 
     *     "-/help" - search for all pages but not for pages whose link starts 
     *     with "/help"
     *   a bunch of alternative pathes:
     *     "/en/,%/machinery/,/docs/" - alternative search paths, separated
     *     by comma
     *   a bunch of paths to exclude:
     *     "-/about,%/info,/docs/" - search all paths buth exclude these!
     *     
     * The different styles can't be mixed.
     * 
     * @access protected
     */
    protected function getSearchPath() 
	{
        $this->search_path_SQL = '';
        $this->search_path = '';
        if (isset($_REQUEST[REQUEST_SEARCH_PATH]))
        {
            $this->search_path = addslashes(strip_tags(stripslashes($_REQUEST[REQUEST_SEARCH_PATH])));
            if (!preg_match('~^%?[-a-zA-Z0-9_,/ ]+$~', $this->search_path))
            {
                $this->search_path = '';
            }
            
            if ($this->search_path != '')
            {
                $this->search_path_SQL = 'AND ( ';
                $not = '';
                $op = 'OR';
                
                if ($this->search_path[0] == '-')
                {
                    $not = 'NOT';
                    $op = 'AND';
                    $paths = explode(',', substr($this->search_path, 1));
                } else {
                    $paths = explode(',', $this->search_path);
                }
                
                $i = 0;
                
                foreach ($paths as $p)
                {
                    if ($i ++ > 0)
                    {
                        $this->search_path_SQL .= ' $op';
                    }
                    $this->search_path_SQL .= " link $not LIKE '" . $p . "%'";
                }
                $this->search_path_SQL .= ' )';
            }
        }
    }

    /**
     * Get the type of the search to execute, possible values are
     * - SEARCH_TYPE_ANY  = 'any' - search matches any words
     * - SEARCH_TYPE_ALL = 'all' - search match all words
     * - SEARCH_TYPE_EXACT = 'exact' - search for exact match
     * and set $this->search_type
     * 
     * @access protected
     */
    protected function getSearchType() 
	{
        if (isset($_REQUEST[REQUEST_SEARCH_TYPE])) {
            if ($_REQUEST[REQUEST_SEARCH_TYPE] == SEARCH_TYPE_ANY)
                $this->search_type = SEARCH_TYPE_ANY;
            elseif ($_REQUEST[REQUEST_SEARCH_TYPE] == SEARCH_TYPE_ALL)
                $this->search_type = SEARCH_TYPE_ALL;
            elseif ($_REQUEST[REQUEST_SEARCH_TYPE] == SEARCH_TYPE_EXACT)
                $this->search_type = SEARCH_TYPE_EXACT;
            elseif ($_REQUEST[REQUEST_SEARCH_TYPE] == SEARCH_TYPE_IMAGE)
                $this->search_type = SEARCH_TYPE_IMAGE;
            else
                $this->search_type = SEARCH_TYPE_ALL;
        } 
        else {
            $this->search_type = SEARCH_TYPE_ALL;
        }
    }
    
    /**
     * Prepare the search before really executing
     * 
     * @access protected
     */
    protected function prepareSearch() 
	{
        
        $search_entities_string = ''; // for SQL's LIKE
        $search_display_string = ''; // for displaying
        $search_url_string = ''; // for $_GET -- ATTN: unquoted! Will become urldecoded later
        $string = '';
        if (isset($_REQUEST[REQUEST_SEARCH_STRING])) {
            if ($this->search_type != SEARCH_TYPE_EXACT) {
                // remove all comma's
                $string = str_replace(',', '', $_REQUEST[REQUEST_SEARCH_STRING]);
            } else {
                $string = $_REQUEST[REQUEST_SEARCH_STRING];
            }
            // redo possible magic quotes
            $string = stripslashes($string);
            $string = preg_replace('/[ \r\n\t]+/', ' ', $string);
            $string = trim($string);
            // remove some bad chars
            $string = str_replace(array('[[', ']]'), '', $string);
            $string = preg_replace('/(^|\s+)[|.]+(?=\s+|$)/', '', $string);
            $search_display_string = $string;
            
            $search_entities_string = addslashes($string);
            
            // mySQL needs four backslashes to match one in LIKE comparisons)
            $search_entities_string = str_replace('\\\\', '\\\\\\\\', $search_entities_string);
            
            // convert string to utf-8
            // $string = entities_to_umlauts($string, 'UTF-8');
            
            $search_url_string = $string;
//            $search_entities_string = addslashes(htmlentities($string, ENT_COMPAT, 'UTF-8'));
            // mySQL needs four backslashes to match one in LIKE comparisons)
            $search_entities_string = str_replace('\\\\', '\\\\\\\\', $search_entities_string);
            $string = preg_quote($string);
            // quote ' " and /  -we need quoted / for regex
            $this->search_string = str_replace(array('\'', '"', '/'), array('\\\'', '\"', '\/'), $string);
        }
        // make arrays from the search_..._strings above
        if ($this->search_type == SEARCH_TYPE_EXACT) {
            $this->search_url_array[] = $search_url_string;
        }
        else {
            $this->search_url_array = explode(' ', $search_url_string);
        }
        $search_normal_array = array();
        $this->search_entities_array = array();
        
        if ($this->search_type == SEARCH_TYPE_EXACT) {
            $search_normal_array[] = $this->search_string;
            $this->search_entities_array[] = $search_entities_string;
        } 
        else {
            $exploded_string = explode(' ', $this->search_string);
            // Make sure there is no blank values in the array
            foreach ($exploded_string as $each_exploded_string) {
                if ($each_exploded_string != '') {
                    $search_normal_array[] = $each_exploded_string;
                }
            }
            $exploded_string = explode(' ', $search_entities_string);
            // Make sure there is no blank values in the array
            foreach ($exploded_string as $each_exploded_string) {
                if ($each_exploded_string != '') {
                    $this->search_entities_array[] = $each_exploded_string;
                }
            }
        }
        
        // make an extra copy of search_normal_array for use in regex
        $this->search_words = array();
        
        // include the translation tables for special chars
        $search_language = $this->search_language;
        //include_once LEPTON_PATH.'/modules/lib_search/search.convert.php';
        //global $search_table_umlauts_local;
        
        //include_once LEPTON_PATH.'/modules/lib_search/search.convert.umlaute.php';
        //global $search_table_ul_umlauts;
        
        foreach ($search_normal_array as $str) {
            //$str = strtr($str, $search_table_umlauts_local);
            //$str = strtr($str, $search_table_ul_umlauts);
            $this->search_words[] = $str;
        }    
            
    }
    
    protected function getSearchForm() 
	{
        $data = array(
            'action'        => self::FRONTEND_RESULT_URL,
            'search_path'   => array(
                    'name'  => REQUEST_SEARCH_PATH,
                    'value' => $this->search_path
            ),
            'search_string' => array(
                    'name'  => REQUEST_SEARCH_STRING,
                    'value' => $this->search_string
            ),
            'search_type'   => array(
                    'name'  => REQUEST_SEARCH_TYPE,
                    'match_all' => array(
                            'value' => SEARCH_TYPE_ALL,
                            'checked' => ($this->search_type == SEARCH_TYPE_ALL) ? 1 : 0
                    ),
                    'match_any' => array(
                            'value' => SEARCH_TYPE_ANY,
                            'checked' => ($this->search_type == SEARCH_TYPE_ANY) ? 1 : 0
                    ),
                    'match_exact' => array(
                            'value' => SEARCH_TYPE_EXACT,
                            'checked' => ($this->search_type == SEARCH_TYPE_EXACT) ? 1 : 0
                    ),
                    'match_image' => array(
                            'value' => SEARCH_TYPE_IMAGE,
                            'checked' => ($this->search_type == SEARCH_TYPE_IMAGE) ? 1 : 0
                    ),
            )
        );    
        return $data;
    }
    
    protected function execSearch() 
	{
        $database = LEPTON_database::getInstance();
        $admin = LEPTON_admin::getInstance("Pages", "start", false, false);
        
        $data = [];
        if ($this->search_string == '')
        {
            // empty search string - just return the dialog
            $this->search_result = [
                'form' => $this->getSearchForm()
            ];
            return true;
        }
        
        // Get the modules from module table
        /**
         *  Aldus: this is tricky: we want only modules that are used in the frontend!
         *
         */
        $aAllModules = [];
        $database->execute_query(
            "SELECT DISTINCT module FROM " . TABLE_PREFIX . "sections WHERE module != '' ",
            true,
            $aAllModules,
            true
        );
        
        if ($database->is_error()) {
            $this->setError( $database->get_error() );
            return false;
        }
        
        $modules = [];
        foreach ($aAllModules as $module)
        {
            $modules[] = $module['module'];
        }
        
        // get the modules for the Droplet search
        $get_droplets = [];
        $database->execute_query(
            "SELECT * FROM `".TABLE_PREFIX."search` WHERE `name`='droplet'",
            true,
            $get_droplets,
            true
        );
        

        $droplets = [];
        $droplet_array = [];
        
        if (!empty($get_droplets))
		{
            foreach ($get_droplets as &$module)
            {    
                $value = unserialize($module['extra']);
                if (isset($value['page_id']) && isset($value['module_directory'])) 
				{
                    $droplets[] = [
                        'module_directory' => $value['module_directory'],
                        'page_id' => $value['page_id'],
                        'droplet_name' => $module['value']
                    ];
                    if (!isset($droplet_array[$value['module_directory']]))
                    {
                        $modules[] = $value['module_directory'];
                        $droplet_array[$value['module_directory']] = $value['module_directory'];
                    }
                }
            }
        }

        // sort module search order - first modules from CFG_MODULE_ORDER
        $sorted_modules = [];
        $search_modules = explode(',', $this->setting[CFG_SEARCH_MODULE_ORDER]);
        foreach ($search_modules as $item) 
		{
            $item = trim($item);
            for ($i=0; $i < count($modules); $i++) 
			{
                if (isset($modules[$i]) && $modules[$i] == $item) 
				{
                    $sorted_modules[] = $modules[$i];
                    unset($modules[$i]);
                    break;
                }
            }
        }
        // ... then add the rest
        foreach ($modules as $item) 
		{
            $sorted_modules[] = $item;
        }

        // Use the module's search-extensions.
        // This is somewhat slower than the orginial method.
        
        // init the $_SESSION for the search result items
        $_SESSION[SESSION_SEARCH_RESULT_ITEMS] = [];
                

        // now call module-based $search_funcs[]
        $seen_pages = []; // seen pages per module.
        $pages_listed = []; // seen pages.
        // skip this search if $search_max_excerpt == 0
        if ($this->setting[CFG_SEARCH_MAX_EXCERPTS] != 0)
        {
            foreach ($sorted_modules as $module_name)
            {
                $start_time = time();	// get start-time to check time-limit; not very accurate, but ok
                $seen_pages[$module_name] = array();
                if (!isset($this->search_functions[$module_name]))
                {
                    // there is no search_func for this module
                    continue; 
                }
                if (isset($droplet_array[$module_name]))
                {
                    // don't look for sections - call droplets search function
                    $pids = [];
                    foreach ($droplets as $dl)
                    {
                        if ($dl['module_directory'] == $module_name)
                        {
                            $pids[] = $dl['page_id'];
                        }
                    }
                    foreach ($pids as $pid)
                    {
                        $SQL = sprintf("SELECT * FROM %spages WHERE page_id='%s'", TABLE_PREFIX, $pid);
                        $pages_query = [];
                        $database->execute_query(
                            $SQL,
                            true,
                            $pages_query,
                            true
                        );
                            
                        if (true === $database->is_error())
                        {
                            $this->setError( $database->get_error() );
                            return false;
                        }   
                        if (!empty($pages_query))
                        {
                            foreach($pages_query as $res)
                            {
                                // check if time-limit is exceeded for this module
                                if ($this->setting[CFG_SEARCH_TIME_LIMIT] > 0 && (time()-$start_time > $this->setting[CFG_SEARCH_TIME_LIMIT]))
                                {
                                    break;
                                }
                                $search_func_vars = [
                                    'database' => $database,
                                    'page_id' => $res['page_id'],
                                    'section_id' => -1, // no section_id's for droplets needed
                                    'page_title' => $res['page_title'],
                                    'page_menu_title' => $res['menu_title'],
                                    'page_description' => $this->setting[CFG_SEARCH_SHOW_DESCRIPTIONS] ? $res['description'] : "",
                                    'page_keywords' => $res['keywords'],
                                    'page_link' => $res['link'],
                                    'page_visibility' => $res['visibility'],
                                    'page_modified_when' => $res['modified_when'],
                                    'page_modified_by' => $res['modified_by'],
                                    'users' => $this->users,
                                    'search_words' => $this->search_words, // needed for preg_match
                                    'search_match' => $this->search_type,
                                    'search_url_array' => $this->search_url_array, // needed for url-string only
                                    'search_entities_array' => $this->search_entities_array, // entities
                                    'default_max_excerpt' => $this->setting[CFG_SEARCH_MAX_EXCERPTS],
                                    'time_limit' => $this->setting[CFG_SEARCH_TIME_LIMIT], // time-limit in secs
                                    'settings' => $this->setting
                                ];
                                // Only show this page if we are allowed to see it
                                if ($admin->page_is_visible($res) == false)
                                {
                                    if ($res['visibility'] == 'registered')
                                    {
                                        if (!$this->setting[CFG_SEARCH_NON_PUBLIC_CONTENT])
                                        {
                                            // don't show excerpt
                                            $search_func_vars['default_max_excerpt'] = 0;
                                            $search_func_vars['page_description'] = $this->language['This content is reserved for registered users.'];
                                        } else {
                                            // show non public content so set $_SESSIONs for print_excerpt2()
                                            $_SESSION[SESSION_SEARCH_NON_PUBLIC_CONTENT] = true;
                                            $_SESSION[SESSION_SEARCH_LINK_NON_PUBLIC_CONTENT] = $this->setting[CFG_SEARCH_LINK_NON_PUBLIC_CONTENT];
                                        }
                                    } else { // private
                                        continue;
                                    }
                                }
                                // call the module search function
                                $uf_res = call_user_func($this->search_functions[$module_name], $search_func_vars);
                                if ($uf_res)
                                {
                                    $pages_listed[$res['page_id']] = true;
                					$seen_pages[$module_name][$res['page_id']] = true;
                				} else {
                					$seen_pages[$module_name][$res['page_id']] = true;
                				}
                            } 
                        }
                    }
                }
                else 
				{           
                    // get each section for $module_name
                    $table_s = TABLE_PREFIX."sections";
                    $table_p = TABLE_PREFIX."pages";
                    $SQL = 
                        "SELECT s.section_id, s.page_id, s.module, s.publ_start, 
                        s.publ_end, p.page_title, p.menu_title, p.link, p.description, 
                        p.keywords, p.modified_when, p.modified_by, p.visibility, 
                        p.viewing_groups FROM $table_s AS s 
                        INNER JOIN $table_p AS p ON s.page_id = p.page_id WHERE 
                        s.module = '$module_name' AND p.visibility NOT IN 
                        ('none','deleted','private','registered') AND p.searching = '1' ".$this->search_path_SQL." ". 
                        $this->search_language_SQL." ORDER BY s.page_id, s.position ASC";
                    // a: 22
                    $aTempSections = [];
                    $database->execute_query(
                        $SQL,
                        true,
                        $aTempSections,
                        true
                    );
                    if (true === $database->is_error() )
                    {
                        $this->setError( $database->get_error() );
                        return false;
                    }
                    
                    if (count($aTempSections) > 0) {
                        //while (false !== ($res = $sections_query->fetchRow())) {
                        foreach($aTempSections as $res)
                        {    
                            // check if time-limit is exceeded for this module
                            if ($this->setting[CFG_SEARCH_TIME_LIMIT] > 0 && (time()-$start_time > $this->setting[CFG_SEARCH_TIME_LIMIT])) {
                                break;
                            }
                            // Only show this section if it is not "out of publication-date"
                            $now = time();
                            if (!($now < $res['publ_end'] && ($now > $res['publ_start'] || $res['publ_start'] == 0) ||
                                $now > $res['publ_start'] && $res['publ_end'] == 0)) {
                                continue;
                            }
                            $search_func_vars = array(
                                'database' => $database,
                                'page_id' => $res['page_id'],
                                'section_id' => $res['section_id'],
                                'page_title' => $res['page_title'],
                                'page_menu_title' => $res['menu_title'],
                                'page_description' => $this->setting[CFG_SEARCH_SHOW_DESCRIPTIONS] ? $res['description'] : "",
                                'page_keywords' => $res['keywords'],
                                'page_link' => $res['link'],
                                'page_visibility' => $res['visibility'],
                                'page_modified_when' => $res['modified_when'],
                                'page_modified_by' => $res['modified_by'],
                                'users' => $this->users,
                                'search_words' => $this->search_words, // needed for preg_match
                                'search_match' => $this->search_type,
                                'search_url_array' => $this->search_url_array, // needed for url-string only
                                'search_entities_array' => $this->search_entities_array, // entities
                                'default_max_excerpt' => $this->setting[CFG_SEARCH_MAX_EXCERPTS],
                                'time_limit' => $this->setting[CFG_SEARCH_TIME_LIMIT], // time-limit in secs
                                'settings' => $this->setting
                            );
                            // Only show this page if we are allowed to see it
                            if ($admin->page_is_visible($res) == false) {
                                if ($res['visibility'] == 'registered') {
                                    if (!$this->setting[CFG_SEARCH_NON_PUBLIC_CONTENT]) {
                                        // don't show excerpt
                                        $search_func_vars['default_max_excerpt'] = 0;
                                        $search_func_vars['page_description'] = $this->language['This content is reserved for registered users.'];
                                    } else {
                                        // show non public content so set $_SESSIONs for print_excerpt2()
                                        $_SESSION[SESSION_SEARCH_NON_PUBLIC_CONTENT] = true;
                                        $_SESSION[SESSION_SEARCH_LINK_NON_PUBLIC_CONTENT] = $this->setting[CFG_SEARCH_LINK_NON_PUBLIC_CONTENT];
                                    }
                                } else 
								{ // private
                                    continue;
                                }
                            }
                            // call the module search function
                            $uf_res = call_user_func($this->search_functions[$module_name], $search_func_vars);
                            if ($uf_res) {
                                $pages_listed[$res['page_id']] = true;
            					$seen_pages[$module_name][$res['page_id']] = true;
            				} else {
            					$seen_pages[$module_name][$res['page_id']] = true;
            				}
            			} // while
                    } // if
                }
        	} // foreach
        } // max_excerpts
        

        // Search page details only, such as description, keywords, etc, but only of unseen pages.
        $max_excerpt_num = 3; // we don't want excerpt here ???
        $divider = ".";
        $table = TABLE_PREFIX."pages";
        $SQL = "
            SELECT
                page_id, page_title, menu_title, link, description, 
                keywords, modified_when, modified_by, visibility, viewing_groups, language
            FROM
                ".$table." 
            WHERE
                visibility NOT IN ('none','deleted') 
            AND
                searching = '1' ".$this->search_path_SQL." ".$this->search_language_SQL;
        
        $query_pages = [];
        $database->execute_query(
            $SQL,
            true,
            $query_pages,
            true
        );
        if (true === $database->is_error())
        {
            $this->setError( $database->get_error() );
            return false;
        }
        if (!empty($query_pages)) {
            //while(false !== ($page = $query_pages->fetchRow())) {
            foreach($query_pages as $page)
            {
                if (isset($pages_listed[$page['page_id']])) continue;
                $func_vars = array(
                    'database' => $database,
                    'page_id' => $page['page_id'],
                    'page_title' => $page['page_title'],
                    'page_menu_title' => $page['menu_title'],
                    'page_description' => $this->setting[CFG_SEARCH_SHOW_DESCRIPTIONS] ? $page['description'] : '',
                    'page_keywords' => $page['keywords'],
                    'page_link' => $page['link'],
                    'page_visibility' => $page['visibility'],
                    'page_modified_when' => $page['modified_when'],
                    'page_modified_by' => $page['modified_by'],
                    'users' => $this->users,
                    'search_words' => $this->search_words, // needed for preg_match_all
                    'search_match' => $this->search_type,
                    'search_url_array' => $this->search_url_array, // needed for url-string only
                    'search_entities_array' => $this->search_entities_array, // entities
                    'default_max_excerpt' => $max_excerpt_num,
                    'settings' => $this->setting
                );
                // Only show this page if we are allowed to see it
                if ($admin->page_is_visible($page) == false) {
                    if($page['visibility'] != 'registered') {
                        continue;
                    } else { 
                        // page: registered, user: access denied
                        $func_vars['page_description'] = $this->language['This content is reserved for registered users.'];
                    }
                }
                if($admin->page_is_active($page) == false) 
				{
                    continue;
                }
                $text = $func_vars['page_title'].$divider
                    .$func_vars['page_menu_title'].$divider
                    .($this->setting[CFG_SEARCH_DESCRIPTIONS] ? $func_vars['page_description'] : '')
                    .$divider.($this->setting[CFG_SEARCH_DESCRIPTIONS] ? $func_vars['page_keywords'] : '').$divider;
                $mod_vars = array(
                    'page_link' => $func_vars['page_link'],
                    'page_link_target' => "",
                    'page_title' => $func_vars['page_title'],
                    'page_description' => $func_vars['page_description'],
                    'page_modified_when' => $func_vars['page_modified_when'],
                    'page_modified_by' => $func_vars['page_modified_by'],
                    'text' => $text,
                    'max_excerpt_num' => $func_vars['default_max_excerpt']
                );
                if (print_excerpt2($mod_vars, $func_vars)) {
                    $pages_listed[$page['page_id']] = true;
                }
            }
        }
        
        // ok - all done ...
        $src = LEPTON_PATH.'/modules/lib_search/images/content-locked.gif';
	    list($width, $height) = getimagesize($src);
	    
	    $this->search_result = array(
            'form' => $this->getSearchForm(),
            'result' => array(
                'count' => count($_SESSION[SESSION_SEARCH_RESULT_ITEMS]),
                'items' => $_SESSION[SESSION_SEARCH_RESULT_ITEMS],
                ),
            'images' => array(
               'locked' => array(
                    'src' => LEPTON_URL.'/modules/lib_search/images/content-locked.gif',
                    'width' => $width,
                    'height' => $height,
                )
            )            
        );
        
	    return true;
    } // execSearch()
    
    /**
     * Execute the LEPTON Search
     * 
     * @access protected
     * @return string result
     */
    public function exec(): string
    {
        if (!SHOW_SEARCH) {
            // the lepton search is not active
            $this->setMessage($this->language['The LEPTON Search is disabled!']);
            return $this->Output([$this->getMessage()]);
        }
        
        // get the settings for the search
        if (!$this->getSettings()) return $this->Output();
                 
        // gather the modules search functions
        if (!$this->checkForModuleSearchFunctions()) return $this->Output();
        
        // get all users
        if (!$this->getUsers()) return $this->Output();
        
        // check if a search language is set, used for special umlaut handling
        if (isset($_REQUEST[REQUEST_SEARCH_LANG])) {
            $this->search_language = $_REQUEST[REQUEST_SEARCH_LANG];
            if (!preg_match('~^[A-Z]{2}$~', $this->search_language)) $this->search_language = LANGUAGE;
        } 
        
        // get the search path
        $this->getSearchPath();
        
        // use page languages?
        if (PAGE_LANGUAGES) {
            $table = TABLE_PREFIX . "pages";
            $this->search_language_SQL_table = "AND $table.`language` = '" . LANGUAGE . "'";
            $this->search_language_SQL = "AND `language` = '" . LANGUAGE . "'";
        }
        
        // get the search type
        $this->getSearchType();
        
        // prepare the search
        $this->prepareSearch();
        
        // create temporary directory for the search
        $tmp = LEPTON_PATH.'/temp/search';
        if (!file_exists($tmp)) {
            if (!mkdir($tmp, 0755, true)) {
                $this->setError(
                  str_replace("{{ directory }}", "temp/search", $this->language[ 'Error creating the directory <b>{{ directory }}</b>.'] )
                );
                return $this->Output();
            }
		}	
        // cleanup the temporary directory
        $oDir = dir($tmp);        
        while (false !== ($strFile = $oDir->read())) {
            if ($strFile != '.' && $strFile != '..'
                && !is_link($tmp.'/'.$strFile)
                && is_file($tmp.'/'.$strFile)) unlink($tmp.'/'.$strFile);
        }        
        $oDir->close();

        // process the search
        if (!$this->execSearch()) return $this->Output();

        return $this->Output($this->search_result);
    } 
    
    /**
     * Prompt or return the results of the LEPTON Search
     * 
     * @access protected
     * @param array $data - template data
     * @return string|bool - prompt the result or return the results
     */
    protected function Output(array $data = []): string|bool
    {
        if (!isset($data['MOD_SEARCH']))
        {
			$data['MOD_SEARCH'] = $this->language;
			$data['LANG'] = LANGUAGE;
        }
        
        if ($this->isError()) {
            // prompt error
            $data = [
                    'error' => [
                            'header' => $this->language['LEPTON Search Error'],
                            'text' => $this->getError()
                            ]
                    ];
            $result = $this->getTemplate('error.lte', $data);            
        }
        elseif ($this->isMessage()) 
		{
            // prompt a message
            $data = [
                'message' => [
                    'header'    => $this->language['LEPTON Search Message'],
                    'text'      => $this->getMessage()
                ]
            ];
            $result = $this->getTemplate('message.lte', $data);
        }
        else 
		{
            if (isset($data['result']['items']))
            {
                foreach($data['result']['items'] as &$ref)
                {
                    $aTemp = explode("?", $ref['page']['link']);
                    $sTemplink = array_shift($aTemp);
                    // Anker
                    $sAnker = "";
                    if (count($aTemp) > 0)
                    {
                        $a2 = explode("#", $aTemp[0]);
                        if (count($a2) > 1)
                        {
                            $sAnker = "#" . $a2[1];
                        }
                    }
                    $ref['page']['link']=$sTemplink.$sAnker;
                    // echo "<p>".$ref['page']['link']."</p><hr />";
                }
            }
			
            // return the search result
			$sOutputTemplate = 'search.results.lte';
			if(file_exists(LEPTON_PATH.'/modules/lib_search/templates/search.results_multilanguage.lte'))
			{
				$sOutputTemplate = 'search.results_multilanguage.lte';
			}
			
			$result = $this->getTemplate($sOutputTemplate, $data);
        }
		
        // Add in L* 5.3
        LEPTON_handle::restoreSpecialChars( $result );        
		
		if ($this->isPrompt())
		{
			echo($result);
            return "true";
		} 
		else 
		{
			return $result;
       	}
    }	

}