<?php
/**
 * Ce script définit la classe 'om_table'.
 *
 * @package openads
 * @version SVN : $Id$
 */

require_once PATH_OPENMAIRIE."om_table.class.php";

/**
 * Définition de la classe 'om_table' (om_table).
 *
 * Cette classe est destinée à permettre la surcharge de certaines méthodes de
 * la classe 'om_table' du framework pour des besoins spécifiques de
 * l'application.
 */
class om_table extends table {

    /**
     * Clause de group by de la requête
     *
     * @var string
     */
    public $group_by;

    /**
     * Surcharge pour permettre aux modules d'ajouter la prise en charges des champs dynamiques
     */
    function __construct($aff, $table, $serie, $champAffiche, $champRecherche, $tri, $selection, $edition = "", $options = array() ,$advs_id = null, $om_validite = false) {
        $this->aff = $aff;
        $this->table = $table;
        $this->serie = $serie;
        $this->champAffiche = $champAffiche;
        $this->champRecherche = $champRecherche;
        $this->tri = $tri;
        $this->selection = $selection;
        $this->edition = $edition;
        $this->options = $options;
        $this->_advs_id = $advs_id;
        $this->_om_validite = $om_validite;
        $this->group_by = false;

        foreach ($options as $option) {
            if ($option['type'] == 'wildcard') {
                if (isset($option['left'])) {
                    $this->wildcard['left'] = $option['left'];
                }
                if (isset($option['right'])) {
                    $this->wildcard['right'] = $option['right'];
                }
            }
            if (isset($option['pagination_group_by'])) {
                $this->group_by = $option['pagination_group_by'];
            }
            // Types d'exports possible
            if (key_exists("export", $option)) {
                $this->export = $option['export'];
            }
        }
        $this->init($advs_id);
    }

    protected function init($advs_id) {

        // Initialisation de la classe 'application'.
        $this->init_om_application();

        // Logger
        $this->addToLog(__METHOD__."()", VERBOSE_MODE);

        $object_name = $this->f->get_submitted_get_value('obj');
        if (empty($object_name)) {
            $object_name = $this->absolute_object;
        }

        $data = array('table' => &$this);
        $modules = $this->f->module_manager->run_hooks_for_object_name('table_init_pre', $object_name, $data);

        // Sauvegarde de la recherche effectuée
        // Si le tableau est chargée avec un formulaire posté
        // alors on sérialise les valeurs postées et on les stocke
        if ($this->f->get_submitted_post_value() !== null
            && is_array($this->f->get_submitted_post_value())
            && count($this->f->get_submitted_post_value()) > 0) {
            //
            $this->serialize_criterions($this->f->get_submitted_post_value());
        }

        /* Support de la recherche avancee */
        if ($this->isAdvancedSearchEnabled()) {
            // initialisation de la liste des colonnes de la base
            $this->dbChampRechercheAv = array_keys($this->paramChampRechercheAv);
            
            // initialisation de la liste des champs HTML
            $this->htmlChampRechercheAv = $this->initHtmlChampRechercheAv();
        }


        if($advs_id != null && isset($_SESSION['advs_ids'][$advs_id])) {
            $_POST=$this->unserialize_criterions();
            $this->f->set_submitted_value();
        }

        // par defaut on considere qu'aucune recherche avancee n'a ete effectuee
        $this->_rechercheAvanceeFaite = false;

        $data = array('table' => &$this);
        $modules = $this->f->module_manager->run_hooks_for_object_name('table_init_post', $object_name, $data);
    }

    /**
     * Méthode de composition du tri
     *
     * @return string
     */
    function composeTri() {
        $tri = $this->tri;
        if ((string) $this->getParam("tricol") !== ""
            && isset($this->champAffiche[abs((int) $this->getParam("tricol"))])) {

            // Tricol est la cle du tableau de champ, il faut recuperer le
            // champ pour l'integrer dans la requete on verifie si un 'as' est
            // present pour s'en servir
            $tricol = $this->champAffiche[abs((int) $this->getParam("tricol"))];
            $tab = preg_split("/[ )]as|AS /", $tricol);

            if (count($tab) > 1) {

                /* Permet de determiner si la colonne a trier est une date.
                   Si $tab[0] contient DD/MM/YYYY, c'est une date.
                   Fonctionne actuellement qu'avec PostgresSQL. */

                // dans le cas d'un champ non-date, on recupere table.colonne
                $tricol = $tab[0];
                if (strpos($tab[0], "'DD/MM/YYYY'")) {
                    // si c'est une date, on recupere le premier parametre
                    // de la fonction to_char
                    $tricol = str_replace("to_char(", "", $tab[0]);
                    $tricol = trim(str_replace(",'DD/MM/YYYY')", "", $tricol));
                    $tricol = trim(str_replace(", 'DD/MM/YYYY')", "", $tricol));
                } else if (str_contains($tab[0], "-- use_column_name_to_sort") !== false) {
                    // Si la mention [use_column_name_to_sort] est presente dans le
                    // select de la column alors c'est le nom de colonne qui
                    // sera utilisee dans le order by
                    $tricol = $tab[count($tab) - 1];
                }

                // seul un signe "-" en début de paramètre de tri effectue un tri
                // par ordre décroissant
                if (substr((string) $this->getParam("tricol"), 0, 1) !== "-") {

                    // tri croissant, nulls en dernier
                    if (OM_DB_PHPTYPE == 'pgsql') {
                        $tricol .= " ASC NULLS LAST";
                    } else {
                        $tricol = ' ISNULL('.$tricol.') ASC, '.$tricol.' ASC';
                    }
                } else {

                    // tri decroissant, nulls en dernier
                    if (OM_DB_PHPTYPE == 'pgsql') {
                        $tricol .= " DESC NULLS LAST";
                    } else {
                        $tricol = ' ISNULL('.$tricol.') ASC, '.$tricol.' DESC';
                    }
                }
            }

            // Si $tri n'est pas vide alors il faut inserer le nouveau champ
            // de tri en premier dans le ORDER BY
            if ($tri) {
                $tri = str_replace("order by ", "ORDER BY ".$tricol.",",
                                   strtolower($tri));
            } else {
                $tri = " order by ".$tricol."";
            }

        }
        return $tri;
    }

    /**
     * SURCHARGE
     *
     * Calcule et affiche la ligne d'entête du tableau.
     *
     * @param array $info
     * @param string $class Prefixe de la classe CSS a utiliser
     * @param boolean $onglet
     *
     * @return void
     */
    function displayHeader($info, $class = "tab", $onglet = false) {
        // Renomme le champ collectivité en service si l'option est activée
        if ($this->f->is_option_renommer_collectivite_enabled() === true) {
            foreach ($info as $key => $elem) {
                if ($elem["name"] === strtolower(__("collectivite"))
                    || $elem["name"] === strtolower(__("om_collectivite"))) {
                    //
                    $info[$key]["name"] = strtolower(__("service"));
                }
                if ($elem["name"] === strval(__("collectivite"))
                    || $elem["name"] === strval(__("om_collectivite"))) {
                    //
                    $info[$key]["name"] = strval(__("service"));
                }
            }
        }

        $getValues = $this->f->get_submitted_get_value();
        // Pour les sous-dossier, le clic sur le bouton "ajouter" entraine l'affichage
        // de la page de consultation du sous-dossier en tant que formulaire (sans ajax).
        //
        // Par défaut les actions de type "corner" affichées dans un onglet sont construite
        // pour utiliser un ajaxIt() afin d'accéder au lien paramétré. Pour contourner ce
        // problème si on est dans le contexte de l'onglet "sous-dossier" alors on modifie
        // la valeur du paramètre "onglet" pour faire comme si on était dans le contexte
        // d'un tab au lieu d'un soustab. Ainsi l'action va s'afficher sans ajaxIt et
        // permettre d'afficher un formulaire à la place d'un sous-formulaire.
        $specificUrl = '';
        $displayTri = true;
        if (! empty($getValues) && ! empty($getValues['obj'])) {
            if ($getValues['obj'] == 'sous_dossier') {
                $onglet = false;
                $displayTri = false;
            }
            if ($getValues['obj'] == 'document_numerise') {
                $displayTri = false;
            }
            // Gestion spécifique du tri pour les lien_dossier_tiers.
            // Il y a plusieurs listings sur l'onglet acteur donc cliquer sur le tri recharge
            // l'onglet avec uniquement un des tableaux triés.
            // Pour que le tri fonctionne correctement, la vue d'affichage des listings est chargée
            // avec 2 paramètres en plus :
            //   - tricol : indique la colonne à trier et le sens du tri
            //   - tritab : élément permettant d'identifier le tableau à trier. C'est la catégorie
            //               qui permet d'identifier quel tableau trier.
            elseif ($getValues['obj'] == 'lien_dossier_tiers' && ! empty($getValues['category'])) {
                $key = '1';
                $tricol = $key;
                // Récupération du tricol comme fais dans la classe parente
                if ((string) $this->getParam("tricol") !== "") {
                    $tricolParam = $this->getParam("tricol");
                    if (abs((int) $tricolParam) == $key) {
                        // si le tri est actuellement croissant
                        if (substr((string) $tricolParam, 0, 1) !== "-") {
                            $tricol = "-".$key;
                        // si le tri est actuellement decroissant
                        } elseif (substr((string) $tricolParam, 0, 1) === "-") {
                            $tricol = "";
                        }
                    }
                }
                // Constitution de l'url servant à afficher la vue avec le tri.
                // /!\ Le paramètre tritab est utilisé pour identifier sur quel tableau appliquer
                //     le tricol. Si ce paramètre est modifié, le modifier également dans :
                //       - lien_dossier_tiers::view_lien_dossier_tiers_tabs()
                // Initialisation des valeurs nécessaire à la construction de l'url
                $urlValues = array('obj', 'retourformulaire', 'idxformulaire', 'category');
                foreach($urlValues as $value) {
                    ${$value} = ! empty($getValues[$value]) ? $getValues[$value] : '';
                }
                $specificUrlTri = sprintf(
                    'a href="#" onclick="ajaxIt(\'%2$s\', \'%1$s&amp;obj=%2$s&amp;action=4&amp;retourformulaire=%3$s&amp;idxformulaire=%4$s&amp;tritab=%5$d&amp;tricol=%6$d\')',
                    OM_ROUTE_SOUSFORM,
                    $obj,
                    $retourformulaire,
                    $idxformulaire,
                    $category,
                    $tricol
                );
            }
        }
        // Récupération du header pour remplacer l'url lié au tri des colonne
        // par une url spécifique si elle a été spécifié
        ob_start();
        parent::displayHeader($info, $class, $onglet);
        $out = ob_get_contents();
        ob_end_clean();
        if (! empty($specificUrlTri)) {
            $out = preg_replace(
                "/a href=\"#\" onclick=\"ajaxIt\('\w*','(.*)'\)/",
                $specificUrlTri,
                $out
            );
        } elseif ($displayTri === false) {
            // Retire les redirections et les icônes de tri du header et ne conserve que
            // le nom à afficher en entete de colonne
            $out = preg_replace("/<a.*>.*ui-icon-triangle.*>(.*)<\/a>/", '$1', $out);
        }
        echo $out;
    }

    /**
     * SURCHARGE
     * 
     * Méthode de composition de la liste des champs du SELECT
     *
     * @return string
     */
    function composeChamp() {
        // traitement du libellé om_collectivite selon que l'on soit en mode multi-collectivités (par défaut) 
        // ou en mode multi-services (spécificité openADS)
        $champ = parent::composeChamp();
        if ($this->f->is_option_renommer_collectivite_enabled() === true) {
            $champ = str_ireplace(array(__('om_collectivite'), __('collectivite')), __('service'), $champ);
        }
        return $champ;
    }

    /**
     * SURCHARGE
     *
     * Gestion de la recherche.
     *
     * @return void
     */
    function composeSearchTab() {
        parent::composeSearchTab();

        // Renomme le champ collectivité en service si l'option est activée
        if ($this->f->is_option_renommer_collectivite_enabled() === true) {
            if (in_array(__("om_collectivite"), $this->searchTab["display"]) === true) {
                $om_collectivite_keys = array_keys($this->searchTab["display"], __("om_collectivite"));
                foreach ($om_collectivite_keys as $om_collectivite_key) {
                    $this->searchTab["display"][$om_collectivite_key] = __("service");
                }
            }
            if (in_array(__("collectivite"), $this->searchTab["display"]) === true) {
                $om_collectivite_keys = array_keys($this->searchTab["display"], __("collectivite"));
                foreach ($om_collectivite_keys as $om_collectivite_key) {
                    $this->searchTab["display"][$om_collectivite_key] = __("service");
                }
            }
        }
    }

    /**
     * Affichage principal.
     * 
     * @override Surcharge pour retirer les espaces non sécables (&nbsp;) des icônes
     *           via capture du buffer d'output.
     */
    public function display($params = array(), $actions = array(), $db = NULL, $class = "tab", $onglet = false) {
        ob_start();
        // Vérification que le paramètre "premier" est valué dans l'URL
        if ($params["premier"] === "" || is_null($params["premier"])) {
            $params["premier"] = 0;
        }
        
        parent::display($params, $actions, $db, $class, $onglet);

        $out = ob_get_contents();
        ob_end_clean();
        echo str_replace(
            array('</a>&nbsp;</td>', '</a>&nbsp;<a', '<td class="icons">&nbsp;'),
            array('</a></td>', '</a><a', '<td class="icons">'),
            $out
        );
    }

    /**
     * Supprime les chaînes 'AND ()' ou 'WHERE ()' de la fin d'une chaîne,
     * sans tenir compte de la casse.
     *
     * Cette fonction vérifie si la chaîne donnée se termine par 'AND ()' ou 'WHERE ()',
     * sans tenir compte de la casse (ex. 'AND ()', 'and ()', 'AnD ()', etc.). Si l'une de ces
     * chaînes est trouvée à la fin, elle est supprimée de la chaîne originale.
     *
     * @param string $text La chaîne à vérifier et à modifier si nécessaire.
     * @return string La chaîne modifiée sans 'AND ()' ou 'WHERE ()' à la fin, si elles étaient présentes.
     */
    private function removeTrailingConditions($text) {
        // Convertir la chaîne en minuscules pour vérifier les termes de manière insensible à la casse
        $lowerText = strtolower($text);

        // Supprime 'and ()' à la fin de la chaîne, si présent
        if (substr($lowerText, -7) == "and ( )") {
            $text = substr($text, 0, -7);
        }

        // Supprime 'where ()' à la fin de la chaîne, si présent
        if (substr($lowerText, -9) == 'where ( )') {
            $text = substr($text, 0, -9);
        }

        // Retourne la chaîne modifiée
        return $text;
    }

    /**
     * SURCHARGE/OVERLOAD
     * Méthode de composition de la recherche avancée.
     *
     * @param [type] $champ [description]
     * @param [type] $tri   [description]
     *
     * @return [type] [description]
     */
    function composeAdvancedSearch($champ, $tri){
        /* Dans le cas d'une recherche avancee, l'operateur binaire OR
           utilise dans la requete SQL de rechreche devient AND */

        $opp = "or";
        if ($this->isAdvancedSearchEnabled() &&
            $this->f->get_submitted_post_value("advanced-search-submit") !== null) {
            $opp = "and";
        }

        // Traitement des filtrages sur tablejoin : on cherche à filtrer les données
        // de table qui vérifient au moins une fois le critère de recherche
        // portant sur la table liée
        // l'option tabletype est alors positionnée sur related par opposition à
        // reference pour une table de référence (le cas par défaut pour les critères
        // portant sur des tables liées)
        //
        // Ce traitement se fait avant traitement de la requête car les critères sont
        // ajoutés à la variable table sous la forme de jointure filtrante
        // de type
        // ... INNER JOIN related ON table.table = related.table
        // AND related.field = $value
        //
        // Exemple :
        //    $champs['parcelle'] = array(
        //    'table' => 'dossier_parcelle',
        //    'where' => 'injoin',
        //    'tablejoin' => 'INNER JOIN (SELECT DISTINCT dossier FROM '.DB_PREFIXE.'dossier_parcelle WHERE lower(dossier_parcelle.libelle) like %s ) AS A1 ON A1.dossier = dossier.dossier' ,
        //    'colonne' => 'libelle',
        //    'type' => 'text',
        //    'taille' => 30,
        //    'libelle' => __('parcelle'));

        // test si on est bien en recherche avancée
        if ($this->isAdvancedSearchEnabled() &&
            $this->f->get_submitted_post_value("advanced-search-submit") !== null) {
            // Boucle sur les champs de recherche
            foreach ($this->searchTab["query"] as $key => $elem) {
                // le champ a-t-il une clé tablejoin ? et si la valeur est-elle renseignée ?
                if ( key_exists("tablejoin", $this->paramChampRechercheAv[$elem])
                    && key_exists($elem, $this->f->get_submitted_post_value())
                    ) {
                    // récupération et retraitement de la valeur du POST
                    $value = $this->composeSearchValue(
                        $this->f->get_submitted_post_value($elem)
                    );

                    // retraitement de join pour remplacer %s / %d par la valeur recherchée ;
                    $tablejoin = $this->paramChampRechercheAv[$elem]["tablejoin"];
                    // la chaine de tablejoin doit contenir un unique %s ou %d qui représente le champ recherche
                    if (substr_count($tablejoin, '%s') >= 1 ) {
                        $tablejoin = sprintf($tablejoin, $value);
                    } else if (substr_count($tablejoin, '%d') >= 1 ) {
                        // SURCHARGE pour les ids numériques
                        // si on recherche sur un sélect avec un identifiant numérique
                        $tablejoin = sprintf($tablejoin, intval($this->f->get_submitted_post_value($elem)));
                    }

                    // Ajout du SQL de la jointure de la table liée dans $this->table
                    $this->table .= " ".$tablejoin." ";
                }


            }
        }


        // Construction de la requete
        // Requête de récupération des données du listing
        $this->sql = "SELECT ".$champ." FROM ".$this->table." ";
        // Requête de calcul du nombre de résultat pour la pagignation
        $this->sqlC = "SELECT count(*) FROM ".$this->table." ";
        if ($this->selection == '') {
            $this->sql .= "WHERE (";
            $this->sqlC .= "WHERE (";
        } else {
            $this->sql .= $this->selection . " AND (";
            $this->sqlC .= $this->selection . " AND (";
        }

        // Construction de la clause WHERE avec les champs de recherche
        $sqlw = "";

        $aggregate_filters = array();

        foreach ($this->searchTab["query"] as $key => $elem) {
            $value = $this->f->get_submitted_post_value("recherche");

            /* Si la recherche avancee est activee on recupere les
               valeurs postees */

            $champ = $elem;

            /* Dans le cas ou des critères sont de type tablejoin on ne
               rajoute pas de WHERE */
            if (key_exists("tablejoin", $this->paramChampRechercheAv[$elem])) {
                continue;
            }
            /* Dans le cas ou des criteres de recherche avancee
               utilisent des fonctions d'aggregation, on n'ajoute
               pas ces criteres a la clause WHERE. Ils seront
               ajoute plus tard a la clause HAVING. */

            if (key_exists("aggregate", $this->paramChampRechercheAv[$champ])) {
                array_push($aggregate_filters, $champ);
                continue;
            }

            /* Gestion des clauses WHERE particulieres */

            if (key_exists("where", $this->paramChampRechercheAv[$champ])) {

                // CAS - intervalle de date
                if ($this->paramChampRechercheAv[$champ]["where"] == "intervaldate") {

                    /* Dans le cas d'un intervalle de dates, deux
                       valeurs sont soumisent: le minimum et le
                       maximum */

                    // le format de date recu : jj/mm/aaaa
                    // avec 0 initiaux

                    if (key_exists($champ."_min", $this->f->get_submitted_post_value())) {
                        $min_date_tab = explode(
                                "/",
                                $this->f->get_submitted_post_value($champ."_min")
                            );

                        if (key_exists(2, $min_date_tab)) {
                            $min_date = $min_date_tab[2]."-".$min_date_tab[1]."-".$min_date_tab[0];
                            $sqlw .= " ".$this->paramChampRechercheAv[$champ]["table"].".".$this->paramChampRechercheAv[$champ]["colonne"]. " >= DATE('".$min_date."') ".$opp." ";
                        }
                    }

                    if (key_exists($champ."_max", $this->f->get_submitted_post_value())) {
                        $max_date_tab = explode(
                                "/",
                                $this->f->get_submitted_post_value($champ."_max")
                            );

                        if (key_exists(2, $max_date_tab)) {
                            $max_date = $max_date_tab[2]."-".$max_date_tab[1]."-".$max_date_tab[0];
                            $sqlw .= " ".$this->paramChampRechercheAv[$champ]["table"].".".$this->paramChampRechercheAv[$champ]["colonne"]. " <= DATE('".$max_date."') ".$opp." ";
                        }
                    }

                } elseif (
                    // CAS - valeurs booleennes
                    $this->paramChampRechercheAv[$champ]["where"] == "booleansubquery" ||
                        $this->paramChampRechercheAv[$champ]["where"] == "insubquery") {

                    if ($this->paramChampRechercheAv[$champ]["subtype"] == "manualselect" &&
                        key_exists($champ, $this->f->get_submitted_post_value())) {

                        $sqlw .= " ".$this->paramChampRechercheAv[$champ]["table"].".".
                                     $this->paramChampRechercheAv[$champ]["colonne"];

                        if ($this->paramChampRechercheAv[$champ]["where"] == "insubquery" &&
                            $this->f->get_submitted_post_value($champ) == "false") {
                            $sqlw .= " NOT IN";
                        } else {
                            $sqlw .= " IN";
                        }

                        $sqlw .= " (".$this->paramChampRechercheAv[$champ]["subquery"];

                        if ($this->paramChampRechercheAv[$champ]["where"] == "booleansubquery") {
                            $sqlw .= " ".$this->f->get_submitted_post_value($champ);
                        }

                        $sqlw .= " ) ".$opp;

                        if ($this->paramChampRechercheAv[$champ]["where"] == "insubquery") {
                            continue;
                        }

                        if ($this->paramChampRechercheAv[$champ]["val_".$this->f->get_submitted_post_value($champ)] == "strict") {

                            $sqlw .= " ".$this->paramChampRechercheAv[$champ]["table"].".".
                                         $this->paramChampRechercheAv[$champ]["colonne"];

                            $sqlw .= " NOT IN";
                            $sqlw .= " (".$this->paramChampRechercheAv[$champ]["subquery"];

                            if ($this->paramChampRechercheAv[$champ]["where"] == "booleansubquery") {

                                $val = "true";
                                if ($this->f->get_submitted_post_value($champ) == "true") {
                                    $val = "false";
                                }

                                $sqlw .= " ".$val;
                            }

                            $sqlw .= " ) ".$opp;
                        }
                    }
                }

                // fin du traitement des valeurs particulieres du WHERE
                continue;
            }

            /*
             * Gestion de la clause TYPE select
             *
             * Si le champ est de type select alors on bypass les wildcards
             * pour faire une recherche strictement égale.
             */

            if (key_exists("type", $this->paramChampRechercheAv[$champ])
                && $this->paramChampRechercheAv[$champ]["type"] == "select") {

                // Création de la condition du where (stricte puisque select)
                $sqlw .= " ".$this->paramChampRechercheAv[$champ]["table"].
                ".".$this->paramChampRechercheAv[$champ]["colonne"].
                " = '".$this->f->get_submitted_post_value($champ)."' ".$opp;

                // On sort de la boucle pour traiter le champ suivant
                continue;
            } // Fin du traitement du type select

            $value = $this->f->get_submitted_post_value($champ);
            if ( is_array($this->paramChampRechercheAv[$champ]["colonne"])){

                $elem = array();
                foreach ($this->paramChampRechercheAv[$champ]["colonne"] as  $data) {

                    $elem[] = $this->paramChampRechercheAv[$champ]["table"].".". $data;
                }
            }
            else{

                $elem = $this->paramChampRechercheAv[$champ]["table"].".".
                    $this->paramChampRechercheAv[$champ]["colonne"];
            }

            // gestion des champs date simple, sans intervalle
            if ($this->paramChampRechercheAv[$champ]["type"] == "date") {
                $sqlw .= " ".$elem." = DATE('".$value."')".$opp." ";
                continue;
            }

            // Caractère d'échappement d'élément dans une recherche
            $separator = ',';
            /*
             * Si on passe plusieurs colonnes dans le champ de recherche,
             * on boucle sur chaque colonne de table liée à ce champ de recherche
             * */
            if ( is_array($elem) ){
                $sqlw .= ' ( ';
                foreach ($elem as $data) {
                    $sqlw .= $this->createSQLW($data, $value, ' OR ');
                }
                $sqlw = substr($sqlw, 0, strlen($sqlw) - 3 ) . " )".$opp." ";
            } else {
                // recherche dans un champ unique
                $sqlw .= $this->createSQLW($elem, $value, $opp).' ';
            }
        }
        // Suppression du dernier $opp
        $sqlw = substr($sqlw, 0, strlen($sqlw) - strlen($opp)-1).")";
        // Complétion de la clause WHERE
        $this->sql .= " ".$sqlw;
        $this->sqlC .= " ".$sqlw;

        // Suppression des AND ou WHERE si pas utilisés
        $this->sql = $this->removeTrailingConditions($this->sql);
        $this->sqlC = $this->removeTrailingConditions($this->sqlC);
        // Construction de la requête triée pour les résultats affichés
        $this->sql .= " ".$tri;

        /* Dans le cas ou des criteres de recherche avancee utilisent des
           fonctions d'aggregation, on construit la clause HAVING adequate */

        if (isset($aggregate_filters) &&
            count($aggregate_filters) != 0) {

            /* Si la requete contient deja la clause HAVING, on ajoute
               les criteres de recherche utilisant des fonctions d'aggregation
               apres le HAVING */

            if (stristr($this->sql, "having") != false) {

                //
                $filters = "HAVING"; /* pas d'espace apres ce having svp*/
                $filters .= $this->add_aggregate_filters($aggregate_filters, $opp);
                $this->sql = str_ireplace("having", $filters, $this->sql);
                $this->sqlC = str_ireplace("having", $filters, $this->sqlC);

            } elseif (stristr($this->sql, "order by") != false) {
                /* Si la requete ne contient pas la clause HAVING, mais une clause
                   ORDER BY, on ajoute les criteres de recherche utilisant des fonctions
                   d'aggregation avant le ORDER BY */

                $filters = "HAVING"; /* pas d'espace apres ce having svp*/
                $filters .= $this->add_aggregate_filters($aggregate_filters, $opp);
                $filters .= " ORDER BY";

                //
                $this->sql = str_ireplace("order by", $filters, $this->sql);

            } else {
                /* Si la requete ne contient ni la clause HAVING, ni ORDER BY, on ajoute
                   les criteres de recherche utilisant des fonctions d'aggregation
                   a la fin de $this->sqlC */
                //
                $this->sql .= " HAVING"; /* pas d'espace apres ce having svp*/
                $this->sql .= $this->add_aggregate_filters($aggregate_filters, $opp);
                $this->sqlC .= " HAVING"; /* pas d'espace apres ce having svp*/
                $this->sqlC .= $this->add_aggregate_filters($aggregate_filters, $opp);
            }
        }
    }

    /**
     * Composition et stockage de la requête dans le cas d'une recherche simple.
     *
     * @param string $champ Critère SELECT de la requête.
     * @param string $tri Critère TRI de la requête.
     *
     * @return void
     */
    function composeSimpleSearch($champ, $tri) {
        // préparation du critère de filtrage
        // sur le champ sélectionné ou tous les champs
        //
        $sqlw = $this->selection;
        if ($this->f->get_submitted_post_value("recherche") == '') {
            // pas de valeur dans le champ de recherche
            // on prend $selection en critère par défaut
            $this->addToLog(__METHOD__."(): no field", EXTRA_VERBOSE_MODE);
        } elseif (isset($this->searchTab["query"][$this->f->get_submitted_post_value("selectioncol")])) {
            $this->addToLog(__METHOD__."(): mono field", EXTRA_VERBOSE_MODE);
            // on a un champ sélectionné
            $champrecherche = $this->searchTab["query"][$this->f->get_submitted_post_value("selectioncol")];
            // Teste si un critère de sélection avait été renseigné
            $sqlw .= (($sqlw=="")?"WHERE ":" AND ");
            $sqlw .= $this->normalizeFieldValue($champrecherche);
            $sqlw .= " LIKE ".$this->composeSearchValue($this->f->get_submitted_post_value("recherche"));
        } else {
            $this->addToLog(__METHOD__."(): multi-fields", EXTRA_VERBOSE_MODE);
            //
            if (count($this->searchTab["query"]) != 0) {
                // pas de champ sélectionné : on recherche dans tous les champs disponibles
                // Teste si un critère de sélection avait été renseigné
                $sqlw .= (($sqlw=="")?"WHERE ":" AND ")."(";
                // on crée une recherche OR pour chaque champ de recherche
                foreach ($this->searchTab["query"] as $searchField) {
                    $sqlw .= $this->normalizeFieldValue($searchField);
                    $sqlw .= " LIKE ".$this->composeSearchValue($this->f->get_submitted_post_value("recherche"));
                    $sqlw .= " OR ";
                }
                // suppression du dernier OR
                $sqlw = substr($sqlw, 0, strlen($sqlw)-3).")";
            } else {
                $this->addToLog(__METHOD__."(): Problème d'intégration - Aucun champ de recherche défini", EXTRA_VERBOSE_MODE);
            }
        }

        // Construction de la requete
        $this->sql = "SELECT ".$champ." FROM ".$this->table." ".$sqlw." ".$tri;
        // Construction de la requete qui compte le nombre
        // d'enregistrement
        $this->sqlC = "SELECT count(*) FROM ".$this->table." ".$sqlw;

    }


    /**
     * SURCHARGE -
     *
     * Calcul du nombre total de resultats de la requete du
     * listing.
     *
     * Recupere la requete de calcul du nombre de resultat
     * du listing ainsi que le tri devant etre applique.
     * Verifie si le tri contiens une clause GROUP BY et
     * si il n'y a pas de recherche avancee en cours :
     *   - si oui : ajoute le tri à la requete et compte
     *              le nombre de ligne renvoye
     *   - si non : execute la requete et recupere le resultat
     *
     * @param string $query   : requete de calcul du total
     * @param string $tri     : clause de tri des resultats
     * @param string $timeout : temps d'execution autorise pour la requete
     *
     * @return boolean|string false en cas d'erreur et le nombre de resultat sinon
     */
    protected function get_nombre_resultat(string $query, string $tri, $timeout = null) {
        // XXX : lorsque le bypass du group by sera integre dans le framework cette
        //       surcharge pourra etre retiree
        $is_timeout_set = false;
        if (! empty($timeout) && is_integer($timeout)) {
            $this->f->set_statement_timeout($timeout);
            $is_timeout_set = true;
        }
        // Defini la methode d'execution de la requete ainsi que
        // l'entree du tableau ou est stockee la donnee attendue
        $cle_resultat = 'result';
        $methode = 'get_one_result_from_db_query';
        // Si il y a un groupe by l'ajoute a la requete et indique
        // que l'on souhaite recuperer le nombre de ligne
        // Dans ce cas on fait un get_all pour recuperer plusieurs
        // ligne
        if (preg_match("/group by/i", $tri) == 1
            && ! $this->_rechercheAvanceeFaite
            && $this->group_by !== false) {
            $query = $query.$tri;
            $methode = 'get_all_results_from_db_query';
            $cle_resultat = 'row_count';  
        }
        // Execute la requete et si un timeout a ete applique,
        // effectue un traitement specifique de l'erreur pour ne pas
        // stopper l'execution si l'erreur est liee au timeout.
        $qres = $this->f->$methode(
            $query,
            array(
                'origin' => __METHOD__,
                'force_return' => $is_timeout_set,
                'return_error_log' => $is_timeout_set
            )
        );
        // Si une erreur est catché et qu'elle ne concerne pas le timeout
        // alors elle est réintégrée au log pour être affichée
        if ($is_timeout_set && ! $this->is_timeout_error_in_log($qres['log'])) {
            $this->f->add_logs_to_log_storage($qres['log']);
        }
        return $is_timeout_set && $qres['code'] !== 'OK' ?
            false :
            $qres[$cle_resultat];
    }

    /**
     * Charge le JS pour étendre les listings d'instructions
     *
     * @return void
     */
    function displayGlobalContainerEnd() {
        $lazy_script_option = null;
        foreach($this->options as $option) {
            if (isset($option['type']) && $option['type'] == 'lazy_script') {
                $lazy_script_option = $option;
                break;
            }
        }
        if (! empty($lazy_script_option)) {
            if (! isset($lazy_script_option['src']) || empty($lazy_script_option['src'])) {
                $this->f->log(__METHOD__, "Missing key 'src' for lazy_script option ("
                    .var_export($lazy_script_option, true).")", 'WARNING');
                return;
            }
            $js = $lazy_script_option['src'];
            if (file_exists($js) || strpos($js, "http") === 0) {
                $version = $this->f->get_application_version();
                $version_urlencoded = urlencode($version);
                // On ajoute le numero de version du logiciel en parametre du fichier
                // pour que le fichier ne soit pas récupéré dans le cache des navigateurs
                // lors d'une mise à jour
                $js .= (strpos($js, "?") === false ? "?" : "&")."v=$version_urlencoded";
                $html = '<script type="text/javascript" src="'.$js.'"></script>'."\n";
                echo $html;
            }
        }
    }
}
