* @author Jean-Pascal MILCENT * @author Aurelien PERONNET * @license GPL v3 * @license CECILL v2 * @copyright 1999-2014 Tela Botanica */ class ParametresFiltrage { const APPLI_IMG = 'IMG'; const APPLI_OBS = 'OBS'; const LISTE_OBS_MAX_RESULT_LIMIT = 1000; const LISTE_OBS_MAX_ID_OBS = 10e7; const LISTE_OBS_MAX_BDTFX_NT = 1000000; // SELECT MAX(num_taxonomique) FROM bdtfx_v2_00; // 44378 + 1000 const LISTE_OBS_MAX_BDTFX_NN = 1000000; // SELECT MAX(num_nom) FROM bdtfx_v2_00;// 120816 + 10000 private $conteneur; private $contexte; private $parametres = array(); private $parametresFiltres = array(); private $appli; public function __construct($conteneur) { $this->conteneur = $conteneur; $this->contexte = $this->conteneur->getContexte(); $this->parametres = $this->contexte->getQS(); } private function etreAppliImg() { return $this->appli === 'IMG' ? true : false; } private function etreAppliObs() { return $this->appli === 'OBS' ? true : false; } public function filtrerUrlParamsAppliImg() { $this->appli = self::APPLI_IMG; $this->maintenirCompatibilitesParametres(); $parametresAutorises = $this->conteneur->getParametreTableau('images.masques_possibles'); $this->eliminerParametresInconnus($parametresAutorises); $this->repartirMasqueGeneral(); $paramsParDefaut = $this->conteneur->getParametreTableau('images.parametres_valeurs_defaut'); $this->definirParametresDefauts($paramsParDefaut); $this->filtrerUrlParamsGeneraux(); $trisPossibles = $this->conteneur->getParametreTableau('appli_img.tris_possibles'); $this->detruireParametreInvalide('tri', $trisPossibles); $formatsImgPossibles = $this->conteneur->getParametreTableau('appli_img.img_formats_possibles'); $this->detruireParametreInvalide('format', $formatsImgPossibles); $this->filtrerProtocole(); $this->supprimerParametresFiltresInvalides(); return $this->parametresFiltres; } public function filtrerUrlParamsAppliObs() { $this->appli = self::APPLI_OBS; $this->maintenirCompatibilitesParametres(); $parametresAutorises = $this->conteneur->getParametreTableau(('observations.masques_possibles')); $this->eliminerParametresInconnus($parametresAutorises); $this->repartirMasqueGeneral(); $paramsParDefaut = $this->conteneur->getParametreTableau('observations.parametres_valeurs_defaut'); $this->definirParametresDefauts($paramsParDefaut); $this->filtrerUrlParamsGeneraux(); $trisPossibles = $this->conteneur->getParametreTableau('appli_obs.tris_possibles'); $this->detruireParametreInvalide('tri', $trisPossibles); $this->supprimerParametresFiltresInvalides(); return $this->parametresFiltres; } private function maintenirCompatibilitesParametres() { $this->renommerParametres(); if ($this->etreAppliImg() && !isset($this->parametres['masque.tag_del']) && isset($this->parametres['masque.tag'])) { $this->parametres['masque.tag_del'] = $this->parametres['masque.tag']; unset($this->parametres['masque.tag']); } if ($this->etreAppliobs() && !isset($this->parametres['masque.tag_cel']) && isset($this->parametres['masque.tag'])) { $this->parametres['masque.tag_cel'] = $this->parametres['masque.tag']; unset($this->parametres['masque.tag']); } } private function renommerParametres() { $renomages = array('masque.tag_pictoflora' => 'masque.tag_del'); foreach ($renomages as $ancienNom => $nouveauNom) { if (isset($this->parametres[$ancienNom])) { $this->parametres[$nouveauNom] = $this->parametres[$ancienNom]; unset($this->parametres[$ancienNom]); } } } /** * Suppression de toutes les clefs NON présentes dans le paramètre de config : images|observations.masques_possibles * @param array $parametresAutorises tableau des paramètres pouvant être utilisé dans l'url. */ private function eliminerParametresInconnus(Array $parametresAutorises = null) { if ($parametresAutorises) { $this->parametres = array_intersect_key($this->parametres, array_flip($parametresAutorises)); } } /** * Les paramètres par défaut sont écrasés par ceux passés dans l'url. * * @param array $paramsParDefaut tableau associatif des paramètres d'url par défaut */ private function definirParametresDefauts(Array $paramsParDefaut) { $this->parametres = array_merge($paramsParDefaut, $this->parametres); } /** * "masque" ne fait jamais que faire une requête sur la plupart des champs, (presque) tous traités * de manière identique à la seule différence que: * 1) ils sont combinés par des "OU" logiques plutôt que des "ET". * 2) les tags sont traités différemment pour conserver la compatibilité avec l'utilisation historique: * Tous les mots-clefs doivent matcher et sont séparés par des espaces. */ private function repartirMasqueGeneral() { if (isset($this->parametres['masque']) && !empty(trim($this->parametres['masque']))) { $masqueGeneral = trim($this->parametres['masque']); $masquesDetailCles = array('masque.auteur', 'masque.departement', 'masque.commune', 'masque.id_zone_geo', 'masque.ns', 'masque.famille', 'masque.date', 'masque.genre', 'masque.milieu'); // Suppression de la génération de SQL du masque général sur les champ spécifiques qui sont traités avec leur valeur propre. foreach ($masquesDetailCles as $cle) { if (isset($this->parametres[$cle]) === false) { $this->parametres[$cle] = $masqueGeneral; $this->parametresFiltres['_parametres_condition_or_'][] = $cle; } } } } /** * Filtre et valide les paramètres reconnus. Effectue *toute* la sanitization *sauf* l'escape-string * Cette fonction est appelée: * - une fois sur les champs de recherche avancées * - une fois sur le masque général si celui-ci à été spécifié. Dans ce cas, * la chaîne générale saisie est utilisée comme valeur pour chacun des champs particuliers * avec les traitements particuliers qui s'imposent * Par exemple: si l'on cherche "Languedoc", cela impliquera: * WHERE (nom_sel like "Languedoc" OR nom_ret ... OR ...) mais pas masque.date ou masque.departement * qui s'assure d'un pattern particulier * * masque.genre est un alias pour masque.ns (nom_sel), mais permet de rajouter une clause supplémentaire * sur nom_sel. Précédemment: WHERE nom_sel LIKE '%% %'. * Désormais masque.genre doit être intégralement spécifié, les caractères '%' et '_' seront interprétés. * Attention toutefois car la table del_observation intègre des nom_sel contenant '_' */ // TODO: ajouter un filtre sur le masque (général) private function filtrerUrlParamsGeneraux() { $this->detruireParametreInvalide('ordre', $this->conteneur->getParametreTableau('valeurs_ordre')); $this->detruireParametreInvalide('masque.referentiel', $this->conteneur->getParametreTableau('valeurs_referentiel')); $this->filtrerNavigationLimite(); $this->filtrerNavigationDepart(); $this->filtrerDepartement(); $this->filtrerDate(); $this->filtrerNn(); $this->filtrerNt(); $parametresATrimer = array('masque', 'masque.ns', 'masque.genre', 'masque.espece', 'masque.auteur', 'masque.milieu'); $this->supprimerCaracteresInvisibles($parametresATrimer); $this->filtrerFamille(); $this->filtrerPays(); $this->filtrerIdZoneGeo(); $this->filtrerCommune(); $this->filtrerType(); $this->filtrerPnInscrits(); $this->filtrerTagCel(); $this->filtrerTagDel(); } /** * Supprime l'index du tableau des paramètres si sa valeur ne correspond pas * au spectre passé par $values. */ private function detruireParametreInvalide($index, Array $valeursAutorisees) { if (array_key_exists($index, $this->parametres)) { if (!in_array($this->parametres[$index], $valeursAutorisees)) { unset($this->parametres[$index]); } else { $this->parametresFiltres[$index] = $this->parametres[$index]; } } } private function filtrerNavigationLimite() { if (isset($this->parametres['navigation.limite'])) { $options = array( 'options' => array( 'default' => null, 'min_range' => 1, 'max_range' => self::LISTE_OBS_MAX_RESULT_LIMIT)); $paramFiltre = filter_var($this->parametres['navigation.limite'], FILTER_VALIDATE_INT, $options); $this->parametresFiltres['navigation.limite'] = $paramFiltre; } } private function filtrerNavigationDepart() { if (isset($this->parametres['navigation.depart'])) { $options = array( 'options' => array( 'default' => null, 'min_range' => 0, 'max_range' => self::LISTE_OBS_MAX_ID_OBS)); $paramFiltre = filter_var($this->parametres['navigation.depart'], FILTER_VALIDATE_INT, $options); $this->parametresFiltres['navigation.depart'] = $paramFiltre; } } /** * STRING: 0 -> 95, 971 -> 976, 2A + 2B (./services/configurations/config_departements_bruts.ini) * accept leading 0 ? * TODO; filter patterns like 555. * * @return type */ private function filtrerDepartement() { if (isset($this->parametres['masque.departement'])) { $dept = $this->parametres['masque.departement']; $paramFiltre = null; if (preg_match('/^(\d{2}|\d{3}|2a|2b)$/i', $dept) != 0) { $paramFiltre = is_numeric($dept) ? str_pad($dept, 5, '_') : $dept; } else { $dept_translit = iconv('UTF-8', 'ASCII//TRANSLIT', $dept); $dpt_chaine = strtolower(str_replace(' ', '-', $dept_translit)); $this->conteneur->chargerConfiguration('config_departements_bruts.ini'); $dpt_numero = $this->conteneur->getParametre($dpt_chaine); if (!empty($dpt_numero)) { $paramFiltre = str_pad($dpt_numero, 5, '_'); } } $this->parametresFiltres['masque.departement'] = $paramFiltre; } } private function filtrerDate() { if (isset($this->parametres['masque.date'])) { $date = $this->parametres['masque.date']; $paramFiltre = null; if (preg_match('/^\d{4}$/', $date)) { $paramFiltre = $date; } else if (strpos($date, '/') !== false) { // Format d'entrée DEL : jj/mm/yyyy list($jour, $mois, $annee) = explode('/', $date); $paramFiltre = "$annee-$mois-$jour"; } else if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { $paramFiltre = $date; } $this->parametresFiltres['masque.date'] = $paramFiltre; } } private function filtrerNn() { if (isset($this->parametres['masque.nn'])) { $options = array( 'options' => array( 'default' => null, 'min_range' => 0, 'max_range' => self::LISTE_OBS_MAX_BDTFX_NN)); $paramFiltre = filter_var($this->parametres['masque.nn'], FILTER_VALIDATE_INT, $options); $this->parametresFiltres['masque.nn'] = $paramFiltre; } } private function filtrerNt() { if (isset($this->parametres['masque.nt'])) { $options = array( 'options' => array( 'default' => null, 'min_range' => 0, 'max_range' => self::LISTE_OBS_MAX_BDTFX_NT)); $paramFiltre = filter_var($this->parametres['masque.nt'], FILTER_VALIDATE_INT, $options); $this->parametresFiltres['masque.nt'] = $paramFiltre; } } private function supprimerCaracteresInvisibles(Array $liste_params) { foreach ($liste_params as $param) { if (isset($this->parametres[$param])) { $this->parametresFiltres[$param] = trim($this->parametres[$param]); } } } private function filtrerFamille() { if (isset($this->parametres['masque.famille'])) { // mysql -N<<<"SELECT DISTINCT famille FROM bdtfx_v1_02;"|sed -r "s/(.)/\1\n/g"|sort -u|tr -d "\n" $familleTranslit = iconv('UTF-8', 'ASCII//TRANSLIT',$this->parametres['masque.famille']); $paramFiltre = preg_replace('/[^a-zA-Z %_]/', '', $familleTranslit); $this->parametresFiltres['masque.famille'] = $paramFiltre; } } // Idem pour id_zone_geo qui mappait à ce_zone_geo: private function filtrerIdZoneGeo() { if (isset($this->parametres['masque.id_zone_geo'])) { if (preg_match('/^(INSEE-C:\d{5}|\d{2})$/', $this->parametres['masque.id_zone_geo'])) { $paramFiltre = $this->parametres['masque.id_zone_geo']; $this->parametresFiltres['masque.id_zone_geo'] = $paramFiltre; } } } // Idem pour id_zone_geo qui mappait à ce_zone_geo: private function filtrerPays() { if (isset($this->parametres['masque.pays'])) { // une liste de pays séparés par des virgules est acceptable if (preg_match('/^([a-zA-Z]{2},)*[a-zA-Z]{2}$/', $this->parametres['masque.pays'])) { // Nettoyage d'une virgule terminale au cas ou $this->parametres['masque.pays'] = rtrim($this->parametres['masque.pays'], ','); $paramFiltre = $this->parametres['masque.pays']; $this->parametresFiltres['masque.pays'] = $paramFiltre; } } } protected function filtrerPnInscrits() { if (isset($this->parametres['masque.pninscritsseulement'])) { if ($this->parametres['masque.pninscritsseulement'] == 1) { $this->parametresFiltres['masque.pninscritsseulement'] = 1; } } } /** masque.commune (zone_geo) * TODO: que faire avec des '%' en INPUT ? * Le masque doit *permettre* une regexp et non l'imposer. Charge au client de faire son travail. */ private function filtrerCommune() { if (isset($this->parametres['masque.commune'])) { $paramFiltre = str_replace(array('-',' '), '_', $this->parametres['masque.commune']); $this->parametresFiltres['masque.commune'] = $paramFiltre; } } private function filtrerTagCel() { if (isset($this->parametres['masque.tag_cel'])) { $this->parametresFiltres['masque.tag_cel'] = $this->construireTableauTags($this->parametres['masque.tag_cel'], 'OR', ','); } else if (isset($this->parametres['masque'])) { $this->parametresFiltres['masque.tag_cel'] = $this->construireTableauTags($this->parametres['masque'], 'AND', ' '); $this->parametresFiltres['_parametres_condition_or_'][] = 'masque.tag_cel'; } } private function filtrerTagDel() { if (isset($this->parametres['masque.tag_del'])) { $this->parametresFiltres['masque.tag_del'] = $this->construireTableauTags($this->parametres['masque.tag_del'], 'OR', ','); } else if (isset($this->parametres['masque'])) { $this->parametresFiltres['masque.tag_del'] = $this->construireTableauTags($this->parametres['masque'], 'AND', ' '); $this->parametresFiltres['_parametres_condition_or_'][] = 'masque.tag_del'; } } /** * Construit un (vulgaire) abstract syntax tree: * "AND" => [ "tag1", "tag2" ] * Idéalement (avec un parser simple comme proposé par http://hoa-project.net/Literature/Hack/Compiler.html#Langage_PP) * nous aurions: * "AND" => [ "tag1", "tag2", "OR" => [ "tag3", "tag4" ] ] * * Ici nous devons traiter les cas suivants: * tags séparés par des "ET/AND OU/OR", séparés par des espaces ou des virgules. * Mais la chaîne peut aussi avoir été issue du "masque général" (la barre de recherche générique). * ce qui implique des comportement par défaut différents afin de préserver la compatibilité. * * Théorie: * 1) tags passés par "champ tag": * - support du ET/OU, et explode par virgule. * - si pas d'opérande détectée: "OU" * * 2) tags passés par "recherche générale": * - support du ET/OU, et explode par whitespace. * - si pas d'opérande détectée: "ET" * * La présence de $additional_sep s'explique car ET/OU sous-entendent une séparation par des espaces. * Mais ce n'est pas toujours pertinent car: 1) la compatibilité suggère de considérer parfois * la virgule comme séparateur et 2) les tags *peuvent* contenir des espaces. Par conséquent: * * a,b,c => "a" $default_op "b" $default_op "c" * * a,b AND c => "a" AND "b" AND "c" * * a OR b AND c,d => "a" AND "b" AND "c" AND "d" * C'est à dire par ordre décroissant de priorité: * 1) opérande contenu dans la chaîne * 2) opérande par défaut * 3) les séparateurs présents sont substitués par l'opérande déterminée par 1) ou 2) * * // TODO: support des parenthèses, imbrications & co: "(", ")" * // http://codehackit.blogspot.fr/2011/08/expression-parser-in-php.html * // http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/ * * @param $str: la chaîne à "parser" * @param $operateur_par_defaut: "AND" ou "OR" * @param $separateur_additionnel: séparateur de mots: */ public function construireTableauTags($str = null, $operateur_par_defaut, $separateur_additionnel = ',') { if (!$str) return; $op = $this->definirOperateurParDefaut($str, $operateur_par_defaut); $mots = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY); if ($separateur_additionnel) { foreach ($mots as $index => $mot) { $mot = trim($mot); $mots_separes = preg_split("/$separateur_additionnel/", $mot, -1, PREG_SPLIT_NO_EMPTY); $mots[$index] = array_shift($mots_separes); $mots = array_merge($mots, $mots_separes); } } $mots = array_filter($mots); return array($op => $mots); } private function definirOperateurParDefaut($str, $operateur_par_defaut) { $op = $operateur_par_defaut; if (preg_match('/\b(ET|AND)\b/', $str)) { $op = 'AND'; } else if(preg_match('/\b(OU|OR)\b/', $str)) { $op = 'OR'; } return $op; } // masque.type: ['adeterminer', 'aconfirmer', 'endiscussion', 'validees', 'monactivite'] private function filtrerType() { if (isset($this->parametres['masque.type'])) { $typesArray = explode(';', $this->parametres['masque.type']); $typesFiltres = array_filter($typesArray); $typesAutorises = $this->conteneur->getParametreTableau('valeurs_type'); $typesValides = array_intersect($typesFiltres, $typesAutorises); $paramFiltre = array_flip($typesValides); $this->parametresFiltres['masque.type'] = $paramFiltre; } } private function filtrerProtocole() { // ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné if (!isset($this->parametres['protocole']) || !is_numeric($this->parametres['protocole'])) { $this->parametresFiltres['protocole'] = $this->conteneur->getParametre('appli_img.protocole_defaut'); } else { $this->parametresFiltres['protocole'] = intval($this->parametres['protocole']); } } private function supprimerParametresFiltresInvalides() { // Suppression des NULL, FALSE et '', mais pas des 0, d'où l'utilisation de 'strlen'. // La fonction 'strlen' permet de supprimer les NULL, FALSE et chaines vides mais gardent les valeurs 0 (zéro). // Les valeurs spéciales contenant des tableaux (tag, _parametres_condition_or_) ne sont pas prise en compte foreach ($this->parametresFiltres as $cle => $valeur) { if (is_array($valeur) || strlen($valeur) !== 0) { $this->parametresFiltres[$cle] = $valeur; } } } }