Subversion Repositories eFlore/Applications.del

Compare Revisions

Ignore whitespace Rev 1493 → Rev 1494

/trunk/services/modules/0.1/DelTk.php
28,7 → 28,7
define('_LISTE_OBS_MAX_BDTFX_NN', 1000000); // 120816 + 10000
 
class DelTk {
static $parametres_autorises = array(
static $parametres_autorises = array(
'masque', 'masque.famille', 'masque.nn', 'masque.referentiel', // taxon
'masque.genre', 'masque.espece', 'masque.ns', // nom_sel
'masque.commune', 'masque.departement', 'masque.id_zone_geo', // loc
37,9 → 37,9
'navigation.depart', 'navigation.limite',
'tri', 'ordre', // TODO: 'total=[yes]', 'fields=[x,y,...]'
// TODO: masque.annee, masque.insee (!= departement)
);
);
 
static $default_params = array(
static $default_params = array(
'navigation.depart' => 0, 'navigation.limite' => 10,
'tri' => 'date_transmission', 'ordre' => 'desc');
 
47,433 → 47,402
// input filtering
 
 
/* 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" ] ]
/* 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é.
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"
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"
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)
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/
// 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 $default_op: "AND" ou "OR"
@param $additional_sep: séparateur de mots:
*/
static function buildTagsAST($str = NULL, $default_op, $additional_sep = ',') {
if(!$str) return;
$words = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY);
@param $str: la chaîne à "parser"
@param $default_op: "AND" ou "OR"
@param $additional_sep: séparateur de mots:
*/
static function buildTagsAST($str = NULL, $default_op, $additional_sep = ',') {
if(!$str) return;
$words = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY);
 
if(preg_match('/\b(ET|AND)\b/', $str)) $op = 'AND';
elseif(preg_match('/\b(OU|OR)\b/', $str)) $op = 'OR';
else $op = $default_op;
if(preg_match('/\b(ET|AND)\b/', $str)) $op = 'AND';
elseif(preg_match('/\b(OU|OR)\b/', $str)) $op = 'OR';
else $op = $default_op;
 
if($additional_sep) {
array_walk($words,
create_function('&$v, $k, $sep', '$v = preg_split("/".$sep."/", $v, -1, PREG_SPLIT_NO_EMPTY);'),
$additional_sep);
}
$words = DelTk::array_flatten($words);
$words = array_map('trim', $words);
return array($op => array_filter($words));
if($additional_sep) {
array_walk($words,
create_function('&$v, $k, $sep', '$v = preg_split("/".$sep."/", $v, -1, PREG_SPLIT_NO_EMPTY);'),
$additional_sep);
}
$words = DelTk::array_flatten($words);
$words = array_map('trim', $words);
return array($op => array_filter($words));
}
 
 
static function array_flatten($arr) {
$arr = array_values($arr);
while (list($k,$v)=each($arr)) {
if (is_array($v)) {
array_splice($arr,$k,1,$v);
next($arr);
}
}
return $arr;
static function array_flatten($arr) {
$arr = array_values($arr);
while (list($k,$v)=each($arr)) {
if (is_array($v)) {
array_splice($arr,$k,1,$v);
next($arr);
}
}
return $arr;
}
 
// supprime l'index du tableau des paramètres si sa valeur ne correspond pas
// au spectre passé par $values.
static function unsetIfInvalid(&$var, $index, $values) {
if(array_key_exists($index, $var)) {
if(!in_array($var[$index], $values)) unset($var[$index]);
else return $var[$index];
}
return NULL;
// supprime l'index du tableau des paramètres si sa valeur ne correspond pas
// au spectre passé par $values.
static function unsetIfInvalid(&$var, $index, $values) {
if(array_key_exists($index, $var)) {
if(!in_array($var[$index], $values)) unset($var[$index]);
else return $var[$index];
}
return NULL;
}
 
 
 
 
/* 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 */
static function requestFilterParams(Array $params, $parametres_autorises = NULL, Conteneur $c = NULL /* pour la récup des départements */ ) {
if($parametres_autorises) { // filtrage de toute clef inconnue
$params = array_intersect_key($params, array_flip($parametres_autorises));
}
/* 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 */
static function requestFilterParams(Array $params, $parametres_autorises = NULL, Conteneur $c = NULL /* pour la récup des départements */ ) {
if($parametres_autorises) { // filtrage de toute clef inconnue
$params = array_intersect_key($params, array_flip($parametres_autorises));
}
 
$p['tri'] = DelTK::unsetIfInvalid($params, 'tri', array('date_observation'));
$p['ordre'] = DelTK::unsetIfInvalid($params, 'ordre', array('asc','desc'));
$p['masque.referentiel'] = DelTK::unsetIfInvalid($params, 'masque.referentiel', array('bdtfx','bdtxa','isfan'));
$p['tri'] = DelTK::unsetIfInvalid($params, 'tri', array('date_observation'));
$p['ordre'] = DelTK::unsetIfInvalid($params, 'ordre', array('asc','desc'));
$p['masque.referentiel'] = DelTK::unsetIfInvalid($params, 'masque.referentiel', array('bdtfx','bdtxa','isfan'));
 
// TODO: use filter_input(INPUT_GET);
// renvoie FALSE ou NULL si absent ou invalide
$p['navigation.limite'] = filter_var(@$params['navigation.limite'],
FILTER_VALIDATE_INT,
array('options' => array('default' => NULL,
'min_range' => 1,
'max_range' => _LISTE_OBS_MAX_RESULT_LIMIT)));
$p['navigation.depart'] = filter_var(@$params['navigation.depart'],
FILTER_VALIDATE_INT,
array('options' => array('default' => NULL,
'min_range' => 0,
'max_range' => _LISTE_OBS_MAX_ID_OBS)));
if(isset($params['masque.departement'])) {
// STRING: 0 -> 95, 971 -> 976, 2A + 2B (./services/configurations/config_departements_bruts.ini)
// accept leading 0 ?
// TODO; filter patterns like 555.
if(preg_match(';^(\d{2}|\d{3}|2a|2b)$;i', $params['masque.departement'])) {
$p['masque.departement'] = $params['masque.departement'];
}
// cf configurations/config_departements_bruts.ini
elseif( !is_null($c) && ( $x = $c->getParametre(
strtolower(str_replace(' ','-',iconv("UTF-8", "ASCII//TRANSLIT", $params['masque.departement'])))
))) {
$p['masque.departement'] = sprintf("INSEE-C:%02d___", $x);
}
}
// TODO: use filter_input(INPUT_GET);
// renvoie FALSE ou NULL si absent ou invalide
$p['navigation.limite'] = filter_var(@$params['navigation.limite'],
FILTER_VALIDATE_INT,
array('options' => array('default' => NULL,
'min_range' => 1,
'max_range' => _LISTE_OBS_MAX_RESULT_LIMIT)));
$p['navigation.depart'] = filter_var(@$params['navigation.depart'],
FILTER_VALIDATE_INT,
array('options' => array('default' => NULL,
'min_range' => 0,
'max_range' => _LISTE_OBS_MAX_ID_OBS)));
if(isset($params['masque.departement'])) {
// STRING: 0 -> 95, 971 -> 976, 2A + 2B (./services/configurations/config_departements_bruts.ini)
// accept leading 0 ?
// TODO; filter patterns like 555.
if(preg_match(';^(\d{2}|\d{3}|2a|2b)$;i', $params['masque.departement'])) {
$p['masque.departement'] = $params['masque.departement'];
}
// cf configurations/config_departements_bruts.ini
elseif( !is_null($c) && ( $x = $c->getParametre(
strtolower(str_replace(' ','-',iconv("UTF-8", "ASCII//TRANSLIT", $params['masque.departement'])))
))) {
$p['masque.departement'] = sprintf("INSEE-C:%02d___", $x);
}
}
 
if(isset($params['masque.date'])) {
// une année, TODO: masque.annee
if(is_numeric($params['masque.date'])) {
$p['masque.date'] = $params['masque.date'];
}
elseif(strpos($params['masque.date'], '/' !== false) &&
($x = strtotime(str_replace('/','-',$params['masque.date'])))) {
$p['masque.date'] = $x;
}
elseif(strpos($params['masque.date'], '-' !== false) &&
($x = strtotime($params['masque.date'])) ) {
$p['masque.date'] = $x;
}
}
if(isset($params['masque.date'])) {
// une année, TODO: masque.annee
if(is_numeric($params['masque.date'])) {
$p['masque.date'] = $params['masque.date'];
}
elseif(strpos($params['masque.date'], '/' !== false) &&
($x = strtotime(str_replace('/','-',$params['masque.date'])))) {
$p['masque.date'] = $x;
}
elseif(strpos($params['masque.date'], '-' !== false) &&
($x = strtotime($params['masque.date'])) ) {
$p['masque.date'] = $x;
}
}
 
$p['masque.nn'] = filter_var(@$params['masque.nn'],
FILTER_VALIDATE_INT,
array('options' => array('default' => NULL,
'min_range' => 0,
'max_range' => _LISTE_OBS_MAX_BDTFX_NN)));
$p['masque.nn'] = filter_var(@$params['masque.nn'],
FILTER_VALIDATE_INT,
array('options' => array('default' => NULL,
'min_range' => 0,
'max_range' => _LISTE_OBS_MAX_BDTFX_NN)));
 
$p['masque.nt'] = filter_var(@$params['masque.nt'],
FILTER_VALIDATE_INT,
array('options' => array('default' => NULL,
'min_range' => 0,
'max_range' => _LISTE_OBS_MAX_BDTFX_NT)));
$p['masque.nt'] = filter_var(@$params['masque.nt'],
FILTER_VALIDATE_INT,
array('options' => array('default' => NULL,
'min_range' => 0,
'max_range' => _LISTE_OBS_MAX_BDTFX_NT)));
 
 
// TODO: should we really trim() ?
// TODO: should we really trim() ?
 
if(isset($params['masque.ns'])) $p['masque.ns'] = trim($params['masque.ns']);
// if(isset($params['masque.texte'])) $p['masque.texte'] = trim($params['masque.texte']);
if(isset($params['masque.ns'])) $p['masque.ns'] = trim($params['masque.ns']);
// if(isset($params['masque.texte'])) $p['masque.texte'] = trim($params['masque.texte']);
 
if(isset($params['masque.famille'])) {
// mysql -N<<<"SELECT DISTINCT famille FROM bdtfx_v1_02;"|sed -r "s/(.)/\1\n/g"|sort -u|tr -d "\n"
$p['masque.famille'] = preg_replace('/[^a-zA-Z %_]/', '', iconv("UTF-8",
"ASCII//TRANSLIT",
$params['masque.famille']));
}
if(isset($params['masque.famille'])) {
// mysql -N<<<"SELECT DISTINCT famille FROM bdtfx_v1_02;"|sed -r "s/(.)/\1\n/g"|sort -u|tr -d "\n"
$p['masque.famille'] = preg_replace('/[^a-zA-Z %_]/', '', iconv("UTF-8",
"ASCII//TRANSLIT",
$params['masque.famille']));
}
 
// 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 '%<masque.genre>% %'.
// 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 '_'
if(isset($params['masque.genre'])) $p['masque.genre'] = trim($params['masque.genre']);
if(isset($params['masque.ns'])) $p['masque.ns'] = trim($params['masque.ns']);
// masque.espece n'était pas déclaré dans la "where" mais utilisé via config + switch//default
if(isset($params['masque.espece'])) $p['masque.espece'] = trim($params['masque.espece']);
// 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 '%<masque.genre>% %'.
// 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 '_'
if(isset($params['masque.genre'])) $p['masque.genre'] = trim($params['masque.genre']);
if(isset($params['masque.ns'])) $p['masque.ns'] = trim($params['masque.ns']);
// masque.espece n'était pas déclaré dans la "where" mais utilisé via config + switch//default
if(isset($params['masque.espece'])) $p['masque.espece'] = trim($params['masque.espece']);
 
// idem pour id_zone_geo qui mappait à ce_zone_geo:
if(isset($params['masque.id_zone_geo']) && preg_match(';^(INSEE-C:\d{5}|\d{2})$;', $params['masque.id_zone_geo'])) {
$p['masque.id_zone_geo'] = $params['masque.id_zone_geo'];
}
// idem pour id_zone_geo qui mappait à ce_zone_geo:
if(isset($params['masque.id_zone_geo']) && preg_match(';^(INSEE-C:\d{5}|\d{2})$;', $params['masque.id_zone_geo'])) {
$p['masque.id_zone_geo'] = $params['masque.id_zone_geo'];
}
 
// 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
if(isset($params['masque.commune'])) $p['masque.commune'] = str_replace(array('-',' '), '_', $params['masque.commune']);
// 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
if(isset($params['masque.commune'])) $p['masque.commune'] = str_replace(array('-',' '), '_', $params['masque.commune']);
 
// masque.auteur: peut-être un id, un courriel, ou un nom ou prénom, ...
if(isset($params['masque.auteur'])) $p['masque.auteur'] = trim($params['masque.auteur']);
// sera trimmé plus tard, cf sqlAddConstraint
if(isset($params['masque'])) $p['masque'] = trim($params['masque']);
// masque.auteur: peut-être un id, un courriel, ou un nom ou prénom, ...
if(isset($params['masque.auteur'])) $p['masque.auteur'] = trim($params['masque.auteur']);
// sera trimmé plus tard, cf sqlAddConstraint
if(isset($params['masque'])) $p['masque'] = trim($params['masque']);
 
// masque.tag, idem que pour masque.genre et masque.commune
if(isset($params['masque.tag'])) {
$x = explode(',',$params['masque.tag']);
$x = array_map('trim', $x);
$p['masque.tag'] = implode('|', array_filter($x));
}
// masque.tag, idem que pour masque.genre et masque.commune
if(isset($params['masque.tag'])) {
$x = explode(',',$params['masque.tag']);
$x = array_map('trim', $x);
$p['masque.tag'] = implode('|', array_filter($x));
}
 
// masque.type: ['adeterminer', 'aconfirmer', 'endiscussion', 'validees']
if(isset($params['masque.type'])) {
$p['masque.type'] = array_flip(array_intersect(array_filter(explode(';', $params['masque.type'])),
array('adeterminer', 'aconfirmer', 'endiscussion', 'validees')));
}
// masque.type: ['adeterminer', 'aconfirmer', 'endiscussion', 'validees']
if(isset($params['masque.type'])) {
$p['masque.type'] = array_flip(array_intersect(array_filter(explode(';', $params['masque.type'])),
array('adeterminer', 'aconfirmer', 'endiscussion', 'validees')));
}
 
 
// TODO: masque (général)
// TODO: masque (général)
 
 
// on filtre les NULL, FALSE et '', mais pas les 0, d'où le callback()
// TODO: PHP-5.3
return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
}
// on filtre les NULL, FALSE et '', mais pas les 0, d'où le callback()
// TODO: PHP-5.3
return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
}
 
 
 
// SQL helpers
 
/* Lorsque l'on concatène des champs, un seul NULL prend le dessus,
Il faut donc utiliser la syntaxe IFNULL(%s, "").
(Cette fonction effectue aussi l'implode() "final" */
static function sqlAddIfNullPourConcat($tab) {
// XXX: PHP-5.3
return implode(',',array_map(create_function('$a', 'return "IFNULL($a, \"\")";'), $tab));
}
/* Lorsque l'on concatène des champs, un seul NULL prend le dessus,
Il faut donc utiliser la syntaxe IFNULL(%s, "").
(Cette fonction effectue aussi l'implode() "final" */
static function sqlAddIfNullPourConcat($tab) {
// XXX: PHP-5.3
return implode(',',array_map(create_function('$a', 'return "IFNULL($a, \"\")";'), $tab));
}
 
 
 
/* Converti un tableau associatif et un préfix optionnel en une chaîne de champs adéquate
à un SELECT MySQL.
$select (optionnel) restreint les champs mappés aux valeurs de $select.
Si $select n'est pas fourni, toutes les clefs présentes dans $map seront présentes dans
le SELECT en sortie */
static function sqlFieldsToAlias($map, $select = NULL, $prefix = NULL) {
if($select) $arr = array_intersect_key($map, array_flip($select));
else $arr = $map;
$keys = array_keys($arr);
/* Converti un tableau associatif et un préfix optionnel en une chaîne de champs adéquate
à un SELECT MySQL.
$select (optionnel) restreint les champs mappés aux valeurs de $select.
Si $select n'est pas fourni, toutes les clefs présentes dans $map seront présentes dans
le SELECT en sortie */
static function sqlFieldsToAlias($map, $select = NULL, $prefix = NULL) {
if($select) $arr = array_intersect_key($map, array_flip($select));
else $arr = $map;
$keys = array_keys($arr);
 
if($prefix) array_walk($keys, create_function('&$val, $k, $prefix', '$val = sprintf("%s.`%s`", $prefix, $val);'), $prefix);
else array_walk($keys, create_function('&$val, $k', '$val = sprintf("`%s`", $val);'));
if($prefix) array_walk($keys, create_function('&$val, $k, $prefix', '$val = sprintf("%s.`%s`", $prefix, $val);'), $prefix);
else array_walk($keys, create_function('&$val, $k', '$val = sprintf("`%s`", $val);'));
 
return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
}
return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
}
 
 
 
/*
Retourne une clause where du style:
CONCAT(IF(du.prenom IS NULL, "", du.prenom), [...] vdi.i_nomutilisateur) REGEXP 'xxx'
Note; i_(nom|prenom_utilisateur), alias pour cel_images.(nom|prenom), n'est pas traité
car cette information est redondante dans cel_image et devrait être supprimée.
*/
static function addAuteursConstraint($val, $db, &$where) {
@list($a, $b) = explode(' ', $val, 2);
// un seul terme
$champs_n = array('du.prenom', // info user authentifié de l'obs depuis l'annuaire
'vdi.prenom_utilisateur', // info user anonyme de l'obs
/* 'vdi.i_prenom_utilisateur' */ ); // info user anonyme de l'image
$champs_p = array('du.nom', // idem pour le nom
'vdi.nom_utilisateur',
/* 'vdi.i_nom_utilisateur' */ );
 
/*
Retourne une clause where du style:
CONCAT(IF(du.prenom IS NULL, "", du.prenom), [...] vdi.i_nomutilisateur) REGEXP 'xxx'
Note; i_(nom|prenom_utilisateur), alias pour cel_images.(nom|prenom), n'est pas traité
car cette information est redondante dans cel_image et devrait être supprimée.
Note: pour l'heure, étant donnés:
- les CONVERT() de la VIEW del_utilisateur
- DEFAULT CHARSET=latin1 pour tela_prod_v4.annuaire_tela
- DEFAULT CHARSET=utf8 pour tb_cel.cel_obs
et l'âge du capitaine...
- REGEXP est case-sensitive, et collate les caractères accentués
- LIKE est case-insensitive, et collate les caractères accentués
*/
static function addAuteursConstraint($val, $db, &$where) {
@list($a, $b) = explode(' ', $val, 2);
// un seul terme
$champs_n = array('du.prenom', // info user authentifié de l'obs depuis l'annuaire
'vdi.prenom_utilisateur', // info user anonyme de l'obs
/* 'vdi.i_prenom_utilisateur' */ ); // info user anonyme de l'image
$champs_p = array('du.nom', // idem pour le nom
'vdi.nom_utilisateur',
/* 'vdi.i_nom_utilisateur' */ );
 
/*
Note: pour l'heure, étant donnés:
- les CONVERT() de la VIEW del_utilisateur
- DEFAULT CHARSET=latin1 pour tela_prod_v4.annuaire_tela
- DEFAULT CHARSET=utf8 pour tb_cel.cel_obs
et l'âge du capitaine...
- REGEXP est case-sensitive, et collate les caractères accentués
- LIKE est case-insensitive, et collate les caractères accentués
*/
if(! $b) {
$where[] = sprintf('CONCAT(%s,%s) LIKE %s',
DelTk::sqlAddIfNullPourConcat($champs_n),
DelTk::sqlAddIfNullPourConcat($champs_p),
$db->proteger("%".$val."%"));
}
else {
$where[] = sprintf('(CONCAT(%1$s,%2$s) LIKE %3$s AND CONCAT(%1$s,%2$s) LIKE %4$s)',
DelTk::sqlAddIfNullPourConcat($champs_n),
DelTk::sqlAddIfNullPourConcat($champs_p),
$db->proteger("%" . $a . "%"), $db->proteger("%" . $b . "%"));
}
if(! $b) {
$where[] = sprintf('CONCAT(%s,%s) LIKE %s',
DelTk::sqlAddIfNullPourConcat($champs_n),
DelTk::sqlAddIfNullPourConcat($champs_p),
$db->proteger("%".$val."%"));
}
else {
$where[] = sprintf('(CONCAT(%1$s,%2$s) LIKE %3$s AND CONCAT(%1$s,%2$s) LIKE %4$s)',
DelTk::sqlAddIfNullPourConcat($champs_n),
DelTk::sqlAddIfNullPourConcat($champs_p),
$db->proteger("%" . $a . "%"), $db->proteger("%" . $b . "%"));
}
}
 
 
 
 
 
/**
* - Rempli le tableau des contraintes "where" et "join" nécessaire
* à la *recherche* des observations demandées ($req) utilisées par self::getIdObs()
*
* Attention, cela signifie que toutes les tables ne sont pas *forcément*
* join'ées, par exemple si aucune contrainte ne le nécessite.
* $req tel qu'il est rempli ici est utile pour récupéré la seule liste des
* id d'observation qui match.
* Pour la récupération effective de "toutes" les données correspondante, il faut
* réinitialiser $req["join"] afin d'y ajouter toutes les autres tables.
*
* Note: toujours rajouter les préfixes de table (vdi,du,doi ou di), en fonction de ce que défini
* les JOIN qui sont utilisés.
* le préfix de v_del_image est "vdi" (cf: "FROM" de self::getIdObs())
* le préfix de del_utilisateur sur id_utilisateur = vdi.ce_utilisateur est "du"
*
* @param $p les paramètres (notamment de masque) passés par l'URL et déjà traités/filtrés (sauf quotes)
* @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
*/
static function sqlAddConstraint($p, $db, &$req) {
if(!empty($p['masque.auteur'])) {
// id du poster de l'obs
$req['join'][] = 'LEFT JOIN del_utilisateur AS du ON du.id_utilisateur = vdi.ce_utilisateur';
// id du poster de l'image... NON, c'est le même que le posteur de l'obs
// Cette jointure de table est ignoré ci-dessous pour les recherches d'auteurs
// $req['join'][] = 'LEFT JOIN del_utilisateur AS dui ON dui.id_utilisateur = vdi.i_ce_utilisateur';
/**
* - Rempli le tableau des contraintes "where" et "join" nécessaire
* à la *recherche* des observations demandées ($req) utilisées par self::getIdObs()
*
* Attention, cela signifie que toutes les tables ne sont pas *forcément*
* join'ées, par exemple si aucune contrainte ne le nécessite.
* $req tel qu'il est rempli ici est utile pour récupéré la seule liste des
* id d'observation qui match.
* Pour la récupération effective de "toutes" les données correspondante, il faut
* réinitialiser $req["join"] afin d'y ajouter toutes les autres tables.
*
* Note: toujours rajouter les préfixes de table (vdi,du,doi ou di), en fonction de ce que défini
* les JOIN qui sont utilisés.
* le préfix de v_del_image est "vdi" (cf: "FROM" de self::getIdObs())
* le préfix de del_utilisateur sur id_utilisateur = vdi.ce_utilisateur est "du"
*
* @param $p les paramètres (notamment de masque) passés par l'URL et déjà traités/filtrés (sauf quotes)
* @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
*/
static function sqlAddConstraint($p, $db, &$req) {
if(!empty($p['masque.auteur'])) {
// id du poster de l'obs
$req['join'][] = 'LEFT JOIN del_utilisateur AS du ON du.id_utilisateur = vdi.ce_utilisateur';
// id du poster de l'image... NON, c'est le même que le posteur de l'obs
// Cette jointure de table est ignoré ci-dessous pour les recherches d'auteurs
// $req['join'][] = 'LEFT JOIN del_utilisateur AS dui ON dui.id_utilisateur = vdi.i_ce_utilisateur';
 
if(is_numeric($p['masque.auteur'])) {
$req['where'][] = sprintf('(du.id_utilisateur = %1$d OR vdi.id_utilisateur = %1$d)', $p['masque.auteur']);
}
elseif(preg_match(';^.{5,}@[a-z0-9-.]{5,}$;i', $p['masque.auteur'])) {
$req['where'][] = sprintf('(du.courriel LIKE %1$s OR vdi.courriel LIKE %1$s )',
$db->proteger($p['masque.auteur'] . '%'));
}
else {
DelTk::addAuteursConstraint($p['masque.auteur'], $db, $req['where']);
}
}
if(is_numeric($p['masque.auteur'])) {
$req['where'][] = sprintf('(du.id_utilisateur = %1$d OR vdi.id_utilisateur = %1$d)', $p['masque.auteur']);
}
elseif(preg_match(';^.{5,}@[a-z0-9-.]{5,}$;i', $p['masque.auteur'])) {
$req['where'][] = sprintf('(du.courriel LIKE %1$s OR vdi.courriel LIKE %1$s )',
$db->proteger($p['masque.auteur'] . '%'));
}
else {
DelTk::addAuteursConstraint($p['masque.auteur'], $db, $req['where']);
}
}
 
if(!empty($p['masque.date'])) {
if(is_integer($p['masque.date']) && $p['masque.date'] < 2030 && $p['masque.date'] > 1600) {
$req['where'][] = sprintf("YEAR(vdi.date_observation) = %d", $p['masque.date']);
}
else {
$req['where'][] = sprintf("DATE_FORMAT(vdi.date_observation, '%%Y-%%m-%%d') = %s",
$db->proteger(strftime('%Y-%m-%d', $p['masque.date'])));
}
}
if(!empty($p['masque.date'])) {
if(is_integer($p['masque.date']) && $p['masque.date'] < 2030 && $p['masque.date'] > 1600) {
$req['where'][] = sprintf("YEAR(vdi.date_observation) = %d", $p['masque.date']);
}
else {
$req['where'][] = sprintf("DATE_FORMAT(vdi.date_observation, '%%Y-%%m-%%d') = %s",
$db->proteger(strftime('%Y-%m-%d', $p['masque.date'])));
}
}
 
// TODO: avoir des champs d'entrée distinct
if(!empty($p['masque.departement'])) {
$req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger('INSEE-C:'.$p['masque.departement']));
}
if(!empty($p['masque.id_zone_geo'])) {
$req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger($p['masque.id_zone_geo']));
}
if(!empty($p['masque.genre'])) {
$req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger('%' . $p['masque.genre'].'% %');
}
if(!empty($p['masque.famille'])) {
$req['where'][] = 'vdi.famille = '.$db->proteger($p['masque.famille']);
}
if(!empty($p['masque.ns'])) {
$req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger($p['masque.ns'].'%');
}
if(!empty($p['masque.nn'])) {
$req['where'][] = sprintf('vdi.nom_sel_nn = %1$d OR vdi.nom_ret_nn = %1$d', $p['masque.nn']);
}
if(!empty($p['masque.referentiel'])) {
$req['where'][] = sprintf('vdi.nom_referentiel LIKE %s', $db->proteger($p['masque.referentiel'].'%'));
}
if(!empty($p['masque.commune'])) {
$req['where'][] = 'vdi.zone_geo LIKE '.$db->proteger($p['masque.commune'].'%');
}
if(!empty($p['masque.tag'])) {
// TODO: remove LOWER() lorsqu'on est sur que les tags sont uniformés en minuscule
// i_mots_cles_texte provient de la VIEW v_del_image
if(isset($p['masque.tag']['AND'])) {
/* Lorsque nous interprêtons la chaîne provenant du masque général (cf: buildTagsAST($p['masque'], 'OR', ' ') dans sqlAddMasqueConstraint()),
nous sommes splittés par espace. Cependant, assurons que si une virgule à été saisie, nous n'aurons pas le motif
" AND CONCAT(mots_cles_texte, i_mots_cles_texte) REGEXP ',' " dans notre requête.
XXX: Au 12/11/2013, une recherche sur tag depuis le masque général implique un OU, donc le problème ne se pose pas ici */
$subwhere = array();
foreach($p['masque.tag']['AND'] as $tag) {
if(trim($tag) == ',') continue;
 
$subwhere[] = sprintf(
'LOWER(CONCAT(%s)) REGEXP %s',
DelTk::sqlAddIfNullPourConcat(array('vdi.mots_cles_texte', 'vdi.i_mots_cles_texte')),
$db->proteger(strtolower($tag)));
}
$req['where'][] = '(' . implode(' AND ', $subwhere) . ')';
}
else {
$req['where'][] = sprintf(
'LOWER(CONCAT(%s)) REGEXP %s',
DelTk::sqlAddIfNullPourConcat(array('vdi.mots_cles_texte', 'vdi.i_mots_cles_texte')),
$db->proteger(strtolower(implode('|', $p['masque.tag']['OR']))));
}
}
// TODO: avoir des champs d'entrée distinct
if(!empty($p['masque.departement'])) {
$req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger('INSEE-C:'.$p['masque.departement']));
}
if(!empty($p['masque.id_zone_geo'])) {
$req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger($p['masque.id_zone_geo']));
}
if(!empty($p['masque.genre'])) {
$req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger('%' . $p['masque.genre'].'% %');
}
if(!empty($p['masque.famille'])) {
$req['where'][] = 'vdi.famille = '.$db->proteger($p['masque.famille']);
}
if(!empty($p['masque.ns'])) {
$req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger($p['masque.ns'].'%');
}
if(!empty($p['masque.nn'])) {
$req['where'][] = sprintf('vdi.nom_sel_nn = %1$d OR vdi.nom_ret_nn = %1$d', $p['masque.nn']);
}
if(!empty($p['masque.referentiel'])) {
$req['where'][] = sprintf('vdi.nom_referentiel LIKE %s', $db->proteger($p['masque.referentiel'].'%'));
}
if(!empty($p['masque.commune'])) {
$req['where'][] = 'vdi.zone_geo LIKE '.$db->proteger($p['masque.commune'].'%');
}
}
 
 
 
 
 
 
 
// formatage de réponse HTTP
static function makeJSONHeader($total, $params, $url_service) {
$prev_url = $next_url = NULL;
$url_service_sans_slash = substr($url_service, 0, -1);
static function makeJSONHeader($total, $params, $url_service) {
$prev_url = $next_url = NULL;
$url_service_sans_slash = substr($url_service, 0, -1);
 
// aplatissons les params! - une seule couche cela dit, après débrouillez-vous
$params_a_plat = $params;
foreach ($params_a_plat as $cle_plate => $pap) {
if (is_array($pap)) {
$params_a_plat[$cle_plate] = implode(array_keys($pap), ',');
}
}
// aplatissons les params! - une seule couche cela dit, après débrouillez-vous
$params_a_plat = $params;
foreach ($params_a_plat as $cle_plate => $pap) {
if (is_array($pap)) {
$params_a_plat[$cle_plate] = implode(array_keys($pap), ',');
}
}
 
$next_offset = $params['navigation.depart'] + $params['navigation.limite'];
if($next_offset < $total) {
$next_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $next_offset)));
}
$next_offset = $params['navigation.depart'] + $params['navigation.limite'];
if($next_offset < $total) {
$next_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $next_offset)));
}
 
$prev_offset = $params['navigation.depart'] - $params['navigation.limite'];
if($prev_offset >= 0) {
$prev_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $prev_offset)));
}
 
return array(
'masque' => http_build_query(array_diff_key($params, array_flip(array('navigation.depart', 'navigation.limite')))),
'total' => $total,
'depart' => $params['navigation.depart'],
'limite' => $params['navigation.limite'],
'href.precedent' => $prev_url,
'href.suivant' => $next_url
);
$prev_offset = $params['navigation.depart'] - $params['navigation.limite'];
if($prev_offset >= 0) {
$prev_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $prev_offset)));
}
 
}
return array(
'masque' => http_build_query(array_diff_key($params, array_flip(array('navigation.depart', 'navigation.limite')))),
'total' => $total,
'depart' => $params['navigation.depart'],
'limite' => $params['navigation.limite'],
'href.precedent' => $prev_url,
'href.suivant' => $next_url
);
}
}
/trunk/services/modules/0.1/observations/ListeObservations.php
28,374 → 28,399
 
class ListeObservations {
 
private $conteneur;
private $gestionBdd;
private $bdd;
private $parametres = array();
private $ressources = array();
private $conteneur;
private $gestionBdd;
private $bdd;
private $parametres = array();
private $ressources = array();
 
static $tris_possibles = array('date_observation');
// paramètres autorisés
static $tris_possibles = array('date_observation');
// paramètres autorisés
 
static $sql_fields_liaisons = array(
'dob' => array('id_observation', 'nom_sel AS `determination.ns`', 'nt AS `determination.nt`',
'nom_sel_nn AS `determination.nn`', 'famille AS `determination.famille`',
'nom_referentiel AS `determination.referentiel`',
'ce_zone_geo AS id_zone_geo', 'zone_geo', 'lieudit',
'station', 'milieu', 'date_observation', 'mots_cles_texte', 'date_transmission',
'ce_utilisateur AS `auteur.id`', 'prenom_utilisateur AS `auteur.prenom`',
'nom_utilisateur AS `auteur.nom`', 'courriel_utilisateur AS observateur',
'commentaire'),
'di' => array('id_image', 'date_prise_de_vue AS `date`', 'hauteur',/* 'largeur','nom_original' // apparemment inutilisés */),
'du' => array('prenom', 'nom', 'courriel'),
'dc' => array('commentaire')
);
static $sql_fields_liaisons = array(
'dob' => array('id_observation', 'nom_sel AS `determination.ns`', 'nt AS `determination.nt`',
'nom_sel_nn AS `determination.nn`', 'famille AS `determination.famille`',
'nom_referentiel AS `determination.referentiel`',
'ce_zone_geo AS id_zone_geo', 'zone_geo', 'lieudit',
'station', 'milieu', 'date_observation', 'mots_cles_texte', 'date_transmission',
'ce_utilisateur AS `auteur.id`', 'prenom_utilisateur AS `auteur.prenom`',
'nom_utilisateur AS `auteur.nom`', 'courriel_utilisateur AS observateur',
'commentaire'),
'di' => array('id_image', 'date_prise_de_vue AS `date`', 'hauteur',/* 'largeur','nom_original' // apparemment inutilisés */),
'du' => array('prenom', 'nom', 'courriel'),
'dc' => array('commentaire')
);
 
 
public function __construct(Conteneur $conteneur = null) {
$this->conteneur = $conteneur == null ? new Conteneur() : $conteneur;
$this->conteneur->chargerConfiguration('config_departements_bruts.ini');
$this->conteneur->chargerConfiguration('config_observations.ini');
$this->conteneur->chargerConfiguration('config_mapping_votes.ini');
$this->conteneur->chargerConfiguration('config_mapping_commentaires.ini');
$this->navigation = $conteneur->getNavigation();
$this->masque = $conteneur->getMasque();
$this->gestionBdd = $conteneur->getGestionBdd();
$this->bdd = $this->gestionBdd->getBdd();
}
public function __construct(Conteneur $conteneur = null) {
$this->conteneur = $conteneur == null ? new Conteneur() : $conteneur;
$this->conteneur->chargerConfiguration('config_departements_bruts.ini');
$this->conteneur->chargerConfiguration('config_observations.ini');
$this->conteneur->chargerConfiguration('config_mapping_votes.ini');
$this->conteneur->chargerConfiguration('config_mapping_commentaires.ini');
$this->navigation = $conteneur->getNavigation();
$this->masque = $conteneur->getMasque();
$this->gestionBdd = $conteneur->getGestionBdd();
$this->bdd = $this->gestionBdd->getBdd();
}
 
static function reformateObservation($obs, $url_pattern = '') {
$obs = array_map('array_filter', $obs);
$obs_merged = array();
foreach($obs as $o) {
$id = $o['id_observation'];
static function reformateObservation($obs, $url_pattern = '') {
$obs = array_map('array_filter', $obs);
$obs_merged = array();
foreach($obs as $o) {
$id = $o['id_observation'];
 
// car auteur.id peut être un email, un hash, ou un annuaire_tela.U_ID
// mais dans les deux premiers cas SELECT courriel AS observateur fait déjà l'affaire
if(!is_numeric($o['auteur.id'])) $o['auteur.id'] = "0";
if(!isset($o['auteur.nom'])) $o['auteur.nom'] = '[inconnu]';
// car auteur.id peut être un email, un hash, ou un annuaire_tela.U_ID
// mais dans les deux premiers cas SELECT courriel AS observateur fait déjà l'affaire
if(!is_numeric($o['auteur.id'])) $o['auteur.id'] = "0";
if(!isset($o['auteur.nom'])) $o['auteur.nom'] = '[inconnu]';
 
$image = array_intersect_key($o, array_flip(array('id_image', 'date', 'hauteur' , 'largeur', 'nom_original')));
$image['binaire.href'] = sprintf($url_pattern, $image['id_image']);
unset($o['id_image'], $o['date'], $o['hauteur'], $o['largeur'], $o['nom_original']);
if(!isset($obs_merged['"' . $id . '"'])) $obs_merged['"' . $id . '"'] = $o;
$obs_merged['"' . $id . '"']['images'][] = $image;
}
return $obs_merged;
$image = array_intersect_key($o, array_flip(array('id_image', 'date', 'hauteur' , 'largeur', 'nom_original')));
$image['binaire.href'] = sprintf($url_pattern, $image['id_image']);
unset($o['id_image'], $o['date'], $o['hauteur'], $o['largeur'], $o['nom_original']);
if(!isset($obs_merged['"' . $id . '"'])) $obs_merged['"' . $id . '"'] = $o;
$obs_merged['"' . $id . '"']['images'][] = $image;
}
return $obs_merged;
}
 
/**
* Méthode principale de la classe.
* Lance la récupération des images dans la base et les place dans un objet ResultatService
* pour l'afficher.
* @param array $ressources les ressources situées après l'url de base (ex : http://url/ressource1/ressource2)
* @param array $parametres les paramètres situés après le ? dans l'url
**/
public function consulter($ressources, $parametres) {
// SELECT, à terme, pourrait affecter getInfos(), mais en aucune manière getIdObs()
$req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'having' => array());
/**
* Méthode principale de la classe.
* Lance la récupération des images dans la base et les place dans un objet ResultatService
* pour l'afficher.
* @param array $ressources les ressources situées après l'url de base (ex : http://url/ressource1/ressource2)
* @param array $parametres les paramètres situés après le ? dans l'url
**/
public function consulter($ressources, $parametres) {
// SELECT, à terme, pourrait affecter getInfos(), mais en aucune manière getIdObs()
$req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'having' => array());
 
// toujours nécessaire puisque nous tapons sur v_del_image qui INNER JOIN cel_images, or nous voulons certes
// toutes les images, mais nous voulons $limite observations uniques.
$req['groupby'][] = 'vdi.id_observation';
// toujours nécessaire puisque nous tapons sur v_del_image qui INNER JOIN cel_images, or nous voulons certes
// toutes les images, mais nous voulons $limite observations uniques.
$req['groupby'][] = 'vdi.id_observation';
 
$db = $this->bdd;
$db = $this->bdd;
 
// filtrage de l'INPUT
$params = DelTk::requestFilterParams($parametres, DelTk::$parametres_autorises, $this->conteneur);
// filtrage de l'INPUT
$params = DelTk::requestFilterParams($parametres, DelTk::$parametres_autorises, $this->conteneur);
 
$params['masque.tag'] = DelTk::buildTagsAST(@$parametres['masque.tag'], 'OR', ',');
$params['masque.tag'] = DelTk::buildTagsAST(@$parametres['masque.tag'], 'OR', ',');
 
// ... et paramètres par défaut
$params = array_merge(DelTk::$default_params, $params);
// ... et paramètres par défaut
$params = array_merge(DelTk::$default_params, $params);
 
// création des contraintes (masques)
DelTk::sqlAddConstraint($params, $db, $req);
self::sqlAddConstraint($params, $db, $req, $this->conteneur);
self::sqlAddMasqueConstraint($params, $db, $req, $this->conteneur);
// création des contraintes (masques)
DelTk::sqlAddConstraint($params, $db, $req);
self::sqlAddConstraint($params, $db, $req, $this->conteneur);
self::sqlAddMasqueConstraint($params, $db, $req, $this->conteneur);
 
// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
$idobs_tab = self::getIdObs($params, $req, $db);
// idobs est une liste (toujours ordonnée) des id d'observations recherchées
$idobs = array_values(array_map(create_function('$a', 'return $a["id_observation"];'), $idobs_tab));
// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
$idobs_tab = self::getIdObs($params, $req, $db);
// idobs est une liste (toujours ordonnée) des id d'observations recherchées
$idobs = array_values(array_map(create_function('$a', 'return $a["id_observation"];'), $idobs_tab));
 
if($idobs) {
$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
if($idobs) {
$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
 
// 2) récupération des données nécessaires pour ces observations (obs + images)
// ici les champs récupérés sont issus de self::$sql_fields_liaisons mais sans préfixes
// car tout provient de v_del_image
$obs_unfmt = self::getInfos($idobs, $db);
// 2) récupération des données nécessaires pour ces observations (obs + images)
// ici les champs récupérés sont issus de self::$sql_fields_liaisons mais sans préfixes
// car tout provient de v_del_image
$obs_unfmt = self::getInfos($idobs, $db);
 
// 3) suppression, merge des données en tableau assez représentatif du futur JSON en output
$observations = self::reformateObservation($obs_unfmt, $this->conteneur->getParametre('url_images'));
// 3) suppression, merge des données en tableau assez représentatif du futur JSON en output
$observations = self::reformateObservation($obs_unfmt, $this->conteneur->getParametre('url_images'));
 
// 4) récupération des données nécessaires pour ces observations (commentaires + votes)
// modifie $observations
$this->configurer();
$this->chargerDeterminations($observations);
// 4) récupération des données nécessaires pour ces observations (commentaires + votes)
// modifie $observations
$this->configurer();
$this->chargerDeterminations($observations);
 
// 5) restauration de l'ordre souhaité initialement
$observations = self::sortArrayByArray($observations, $idobs);
} else {
$observations = array();
$total = 0;
}
// 5) restauration de l'ordre souhaité initialement
$observations = self::sortArrayByArray($observations, $idobs);
} else {
$observations = array();
$total = 0;
}
 
// 6) JSON output
$resultat = new ResultatService();
$resultat->corps = array('entete' => DelTk::makeJSONHeader($total, $params, Config::get('url_service')),
'resultats' => $observations);
// 6) JSON output
$resultat = new ResultatService();
$resultat->corps = array('entete' => DelTk::makeJSONHeader($total, $params, Config::get('url_service')),
'resultats' => $observations);
return $resultat;
}
return $resultat;
}
 
static function sortArrayByArray($array, $orderArray) {
$ordered = array();
foreach($orderArray as $key) {
if(array_key_exists('"' . $key . '"', $array)) {
$ordered['"' . $key . '"'] = $array['"' . $key . '"'];
unset($array['"' . $key . '"']);
}
}
return $ordered + $array;
static function sortArrayByArray($array, $orderArray) {
$ordered = array();
foreach($orderArray as $key) {
if(array_key_exists('"' . $key . '"', $array)) {
$ordered['"' . $key . '"'] = $array['"' . $key . '"'];
unset($array['"' . $key . '"']);
}
}
return $ordered + $array;
}
 
// SQL helpers
/*
* Retourne une liste ordonnée d'id d'observation correspondant aux critères
* passés dans p et aux clauses where/join présentes dans le tableau $req
*
* @param p: $params (filtrés sauf escape-string)
* @param req: le tableau représentant les composants de la requete SQL
* @param db: l'instance de db
*/
static function getIdObs($p, $req, $db) {
$req_s = sprintf('SELECT SQL_CALC_FOUND_ROWS id_observation' .
' FROM v_del_image vdi'.
' %s' . // LEFT JOIN if any
' WHERE %s'. // where-clause ou TRUE
' %s'. // group-by
' %s'. // having (si commentaires)
' ORDER BY %s %s %s'.
' LIMIT %d, %d -- %s',
// SQL helpers
/*
* Retourne une liste ordonnée d'id d'observation correspondant aux critères
* passés dans p et aux clauses where/join présentes dans le tableau $req
*
* @param p: $params (filtrés sauf escape-string)
* @param req: le tableau représentant les composants de la requete SQL
* @param db: l'instance de db
*/
static function getIdObs($p, $req, $db) {
$req_s = sprintf('SELECT SQL_CALC_FOUND_ROWS id_observation' .
' FROM v_del_image vdi'.
' %s' . // LEFT JOIN if any
' WHERE %s'. // where-clause ou TRUE
' %s'. // group-by
' %s'. // having (si commentaires)
' ORDER BY %s %s %s'.
' LIMIT %d, %d -- %s',
$req['join'] ? implode(' ', $req['join']) : '',
$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
$req['join'] ? implode(' ', $req['join']) : '',
$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
 
$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
$req['having'] ? ('HAVING ' . implode(' AND ', $req['having'])) : '',
$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
$req['having'] ? ('HAVING ' . implode(' AND ', $req['having'])) : '',
 
$p['tri'], strtoupper($p['ordre']),
// date_transmission peut-être NULL et nous voulons de la consistence
// (sauf après r1860 de Cel)
$p['tri'] == 'date_transmission' ? ', id_observation' : '',
$p['tri'], strtoupper($p['ordre']),
// date_transmission peut-être NULL et nous voulons de la consistence
// (sauf après r1860 de Cel)
$p['tri'] == 'date_transmission' ? ', id_observation' : '',
 
$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__);
$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__);
 
$res = $db->recupererTous($req_s);
$err = mysql_error();
if(!$res && $err) {
// http_response_code(400);
// if(defined('DEBUG') && DEBUG) header("X-Debug: $req_s");
throw new Exception('not found', 400);
}
// ordre préservé, à partir d'ici c'est important.
return $res;
$res = $db->recupererTous($req_s);
$err = mysql_error();
if(!$res && $err) {
// http_response_code(400);
// if(defined('DEBUG') && DEBUG) header("X-Debug: $req_s");
throw new Exception('not found', 400);
}
// ordre préservé, à partir d'ici c'est important.
return $res;
}
 
/*
Champs récupérés:
Pour del_images, la vue retourne déjà ce que nous recherchons de cel_obs et cel_images
(cel_obs.* et cel_[obs_]images.{id_observation, id_image, date_prise_de_vue AS date, hauteur, largeur})
Pour del_commentaires: nous voulons *
Reste ensuite à formatter.
Note: le préfixe de table utilisé ici (vdi) n'impacte *aucune* autre partie du code car rien
n'en dépend pour l'heure. (inutilisation de $req['select'])
*/
static function getInfos($idobs, $db) {
/*$select_fields = implode(',', array_merge(
array_map(create_function('$a', 'return "vdi.".$a;'), self::$sql_fields_liaisons['dob']),
array_map(create_function('$a', 'return "vdi.".$a;'), self::$sql_fields_liaisons['di']),
array_map(create_function('$a', 'return "du.".$a;'), self::$sql_fields_liaisons['du'])));*/
$select_fields = array_merge(self::$sql_fields_liaisons['dob'],
self::$sql_fields_liaisons['di']);
$req_s = sprintf('SELECT %s FROM v_del_image vdi'.
// ' LEFT JOIN del_commentaire AS dc ON di.id_observation = dc.ce_observation AND dc.nom_sel IS NOT NULL'.
' WHERE id_observation IN (%s)',
implode(',', $select_fields),
implode(',', $idobs));
return $db->recupererTous($req_s);
}
/*
Champs récupérés:
Pour del_images, la vue retourne déjà ce que nous recherchons de cel_obs et cel_images
(cel_obs.* et cel_[obs_]images.{id_observation, id_image, date_prise_de_vue AS date, hauteur, largeur})
Pour del_commentaires: nous voulons *
Reste ensuite à formatter.
Note: le préfixe de table utilisé ici (vdi) n'impacte *aucune* autre partie du code car rien
n'en dépend pour l'heure. (inutilisation de $req['select'])
*/
static function getInfos($idobs, $db) {
/*$select_fields = implode(',', array_merge(
array_map(create_function('$a', 'return "vdi.".$a;'), self::$sql_fields_liaisons['dob']),
array_map(create_function('$a', 'return "vdi.".$a;'), self::$sql_fields_liaisons['di']),
array_map(create_function('$a', 'return "du.".$a;'), self::$sql_fields_liaisons['du'])));*/
$select_fields = array_merge(self::$sql_fields_liaisons['dob'],
self::$sql_fields_liaisons['di']);
$req_s = sprintf('SELECT %s FROM v_del_image vdi'.
// ' LEFT JOIN del_commentaire AS dc ON di.id_observation = dc.ce_observation AND dc.nom_sel IS NOT NULL'.
' WHERE id_observation IN (%s)',
implode(',', $select_fields),
implode(',', $idobs));
return $db->recupererTous($req_s);
}
 
 
/**
* Complément à DelTk::sqlAddConstraint()
*
/**
* Complément à DelTk::sqlAddConstraint()
*
* @param $p les paramètres (notamment de masque) passés par l'URL et déjà traités/filtrés (sauf quotes)
* @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
* @param $c conteneur, utilisé soit pour l'appel récursif à requestFilterParams() en cas de param "masque"
* soit pour la définition du type (qui utilise la variable nb_commentaires_discussion)
*/
static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
if(!empty($p['masque.type'])) {
self::addTypeConstraints($p['masque.type'], $db, $req, $c);
* @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
* @param $c conteneur, utilisé soit pour l'appel récursif à requestFilterParams() en cas de param "masque"
* soit pour la définition du type (qui utilise la variable nb_commentaires_discussion)
*/
static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
if(!empty($p['masque.tag'])) {
// TODO: remove LOWER() lorsqu'on est sur que les tags sont uniformés en minuscule
// i_mots_cles_texte provient de la VIEW v_del_image
if(isset($p['masque.tag']['AND'])) {
/* Lorsque nous interprêtons la chaîne provenant du masque général (cf: buildTagsAST($p['masque'], 'OR', ' ') dans sqlAddMasqueConstraint()),
nous sommes splittés par espace. Cependant, assurons que si une virgule à été saisie, nous n'aurons pas le motif
" AND CONCAT(mots_cles_texte, i_mots_cles_texte) REGEXP ',' " dans notre requête.
XXX: Au 12/11/2013, une recherche sur tag depuis le masque général implique un OU, donc le problème ne se pose pas ici */
$subwhere = array();
foreach($p['masque.tag']['AND'] as $tag) {
if(trim($tag) == ',') continue;
 
$subwhere[] = sprintf('LOWER(CONCAT(%s)) REGEXP %s',
DelTk::sqlAddIfNullPourConcat(array('vdi.mots_cles_texte', 'vdi.i_mots_cles_texte')),
$db->proteger(strtolower($tag)));
}
$req['where'][] = '(' . implode(' AND ', $subwhere) . ')';
}
else {
$req['where'][] = sprintf('LOWER(CONCAT(%s)) REGEXP %s',
DelTk::sqlAddIfNullPourConcat(array('vdi.mots_cles_texte', 'vdi.i_mots_cles_texte')),
$db->proteger(strtolower(implode('|', $p['masque.tag']['OR']))));
}
}
 
/* Le masque fait une recherche générique parmi de nombreux champs ci-dessus.
Nous initialisons donc ces paramètres (excepté masque biensur), et nous rappelons
récursivement. À la seule différence que nous n'utiliserons que $or_req['where']
imploded par des " OR ". */
static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
if(!empty($p['masque'])) {
$or_params = array('masque.auteur' => $p['masque'],
'masque.departement' => $p['masque'],
'masque.id_zone_geo' => $p['masque'],
'masque.tag' => $p['masque'],
'masque.ns' => $p['masque'],
'masque.famille' => $p['masque'],
'masque.date' => $p['masque'],
'masque.genre' => $p['masque'],
/* milieu: TODO ? */ );
$or_masque = DelTk::requestFilterParams($or_params, array_keys($or_params), $c);
$or_masque['masque.tag'] = DelTk::buildTagsAST($p['masque'], 'OR', ' ');
// $or_req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'having' => array());
$or_req = array('join' => array(), 'where' => array());
DelTk::sqlAddConstraint($or_masque, $db, $or_req);
self::sqlAddConstraint($or_masque, $db, $or_req);
 
if($or_req['where']) {
$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
// utile au cas ou des jointures seraient rajoutées
$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
}
}
if(!empty($p['masque.type'])) {
self::addTypeConstraints($p['masque.type'], $db, $req, $c);
}
}
 
/* Le masque fait une recherche générique parmi de nombreux champs ci-dessus.
Nous initialisons donc ces paramètres (excepté masque biensur), et nous rappelons
récursivement. À la seule différence que nous n'utiliserons que $or_req['where']
imploded par des " OR ". */
static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
if(!empty($p['masque'])) {
$or_params = array('masque.auteur' => $p['masque'],
'masque.departement' => $p['masque'],
'masque.id_zone_geo' => $p['masque'],
'masque.tag' => $p['masque'],
'masque.ns' => $p['masque'],
'masque.famille' => $p['masque'],
'masque.date' => $p['masque'],
'masque.genre' => $p['masque'],
/* milieu: TODO ? */ );
$or_masque = DelTk::requestFilterParams($or_params, array_keys($or_params), $c);
$or_masque['masque.tag'] = DelTk::buildTagsAST($p['masque'], 'OR', ' ');
// $or_req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'having' => array());
$or_req = array('join' => array(), 'where' => array());
DelTk::sqlAddConstraint($or_masque, $db, $or_req);
self::sqlAddConstraint($or_masque, $db, $or_req);
 
private function configurer() {
$this->mappingVotes = $this->conteneur->getParametre('mapping_votes');
$this->mappingCommentaire = $this->conteneur->getParametre('mapping_commentaire');
if($or_req['where']) {
$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
// utile au cas ou des jointures seraient rajoutées
$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
}
}
}
 
 
/*
* @param $req: la représentation de la requête MySQL complète, à amender.
*/
static function addTypeConstraints($val, $db, &$req, Conteneur $c) {
if(array_key_exists('adeterminer', $val)) {
//On récupère toutes les observations qui on le tag "aDeterminer" *ou* qui n'ont pas de nom d'espèce
$req['where'][] = '(' . implode(' OR ', array(
'vdi.certitude = "aDeterminer"',
'vdi.mots_cles_texte LIKE "%aDeterminer%"',
'vdi.nom_sel_nn IS NULL', // TODO: ensure pas d'entrée à 0
)) . ')';
}
if(array_key_exists('aconfirmer', $val)) {
//On récupère toutes les observations qui ne sont pas "aDeterminer" *et* qui ont un nom d'espèce
$req['where'][] = '(' . implode(' AND ', array(
'vdi.nom_sel IS NOT NULL',
'vdi.certitude != "aDeterminer"',
'(vdi.mots_cles_texte IS NULL OR vdi.mots_cles_texte NOT LIKE "%aDeterminer%"',
)) . ')';
}
if(array_key_exists('validees', $val)) {
//On récupère toutes les observations ayant un commentaire doté de proposition_retenue = 1
$req['join'][] = 'INNER JOIN del_commentaire AS dc ON vdi.id_observation = dc.ce_observation AND dc.proposition_retenue = 1';
}
private function configurer() {
$this->mappingVotes = $this->conteneur->getParametre('mapping_votes');
$this->mappingCommentaire = $this->conteneur->getParametre('mapping_commentaire');
}
 
// solution n°1: impraticable
if(false && array_key_exists('endiscussion', $val)) {
//Si on veut les observations en discussion,
// on va récupérer les ids des observations dont le nombre de commentaire est supérieur à N
$req['select'][] = 'COUNT(dc.id_commentaire) AS comm_count';
$req['join'][] = 'INNER JOIN del_commentaire AS dc ON vdi.id_observation = dc.ce_observation';
$req['groupby'][] = 'vdi.id_observation';
$req['having'][] = "COUNT(id_commentaire) > " . $c->getParametre('nb_commentaires_discussion');
}
 
if(array_key_exists('endiscussion', $val)) {
$req['where'][] = '(SELECT COUNT(id_commentaire) FROM del_commentaire AS dc'.
' WHERE ce_observation = id_observation) > ' . intval($c->getParametre('nb_commentaires_discussion'));
}
/*
* @param $req: la représentation de la requête MySQL complète, à amender.
*/
static function addTypeConstraints($val, $db, &$req, Conteneur $c) {
if(array_key_exists('adeterminer', $val)) {
//On récupère toutes les observations qui on le tag "aDeterminer" *ou* qui n'ont pas de nom d'espèce
$req['where'][] = '(' . implode(' OR ', array(
'vdi.certitude = "aDeterminer"',
'vdi.mots_cles_texte LIKE "%aDeterminer%"',
'vdi.nom_sel_nn IS NULL', // TODO: ensure pas d'entrée à 0
)) . ')';
}
if(array_key_exists('aconfirmer', $val)) {
//On récupère toutes les observations qui ne sont pas "aDeterminer" *et* qui ont un nom d'espèce
$req['where'][] = '(' . implode(' AND ', array(
'vdi.nom_sel IS NOT NULL',
'vdi.certitude != "aDeterminer"',
'(vdi.mots_cles_texte IS NULL OR vdi.mots_cles_texte NOT LIKE "%aDeterminer%"',
)) . ')';
}
if(array_key_exists('validees', $val)) {
//On récupère toutes les observations ayant un commentaire doté de proposition_retenue = 1
$req['join'][] = 'INNER JOIN del_commentaire AS dc ON vdi.id_observation = dc.ce_observation AND dc.proposition_retenue = 1';
}
 
// solution n°1: impraticable
if(false && array_key_exists('endiscussion', $val)) {
//Si on veut les observations en discussion,
// on va récupérer les ids des observations dont le nombre de commentaire est supérieur à N
$req['select'][] = 'COUNT(dc.id_commentaire) AS comm_count';
$req['join'][] = 'INNER JOIN del_commentaire AS dc ON vdi.id_observation = dc.ce_observation';
$req['groupby'][] = 'vdi.id_observation';
$req['having'][] = "COUNT(id_commentaire) > " . $c->getParametre('nb_commentaires_discussion');
}
 
/**
* Récupérer toutes les déterminations et le nombre de commentaire au total
* @param array $observations la liste des observations à mettre à jour
* */
private function chargerDeterminations(&$observations) {
$idObs = array_values(array_map(create_function('$a', 'return $a["id_observation"];'),
$observations));
$r = sprintf('SELECT * FROM del_commentaire AS dc WHERE dc.nom_sel IS NOT NULL AND ce_observation IN (%s) -- %s',
implode(',',$idObs),
__FILE__ . ':' . __LINE__);
$propositions = $this->bdd->recupererTous($r);
if(!$propositions) return;
foreach ($propositions as $proposition) {
$idObs = $proposition['ce_observation'];
$idComment = $proposition['id_commentaire'];
$comment = $this->formaterDetermination($idComment, $proposition);
if($comment) $observations['"' . $idObs . '"']['commentaires'][$idComment] = $comment;
if(array_key_exists('endiscussion', $val)) {
$req['where'][] = '(SELECT COUNT(id_commentaire) FROM del_commentaire AS dc'.
' WHERE ce_observation = id_observation) > ' . intval($c->getParametre('nb_commentaires_discussion'));
}
}
 
 
/**
* Récupérer toutes les déterminations et le nombre de commentaire au total
* @param array $observations la liste des observations à mettre à jour
* */
private function chargerDeterminations(&$observations) {
$idObs = array_values(array_map(create_function('$a', 'return $a["id_observation"];'),
$observations));
$r = sprintf('SELECT * FROM del_commentaire AS dc WHERE dc.nom_sel IS NOT NULL AND ce_observation IN (%s) -- %s',
implode(',',$idObs),
__FILE__ . ':' . __LINE__);
$propositions = $this->bdd->recupererTous($r);
if(!$propositions) return;
foreach ($propositions as $proposition) {
$idObs = $proposition['ce_observation'];
$idComment = $proposition['id_commentaire'];
$comment = $this->formaterDetermination($idComment, $proposition);
if($comment) $observations['"' . $idObs . '"']['commentaires'][$idComment] = $comment;
}
}
}
 
private function formaterDetermination($commentId, $proposition) {
if(!$proposition) return NULL;
private function formaterDetermination($commentId, $proposition) {
if(!$proposition) return NULL;
 
$proposition_formatee = array('nb_commentaires' => '0');
foreach ($this->mappingCommentaire as $nomOriginal => $nomFinal) {
if (isset($proposition[$nomOriginal])) {
$proposition_formatee[$nomFinal] = $proposition[$nomOriginal];
}
}
$proposition_formatee = array('nb_commentaires' => '0');
foreach ($this->mappingCommentaire as $nomOriginal => $nomFinal) {
if (isset($proposition[$nomOriginal])) {
$proposition_formatee[$nomFinal] = $proposition[$nomOriginal];
}
}
 
// Charger les votes sur les déterminations
$resultatsVotes = $this->bdd->recupererTous(
sprintf('SELECT * FROM del_commentaire_vote WHERE ce_proposition = %d', $commentId));
// Charger les votes sur les déterminations
$resultatsVotes = $this->bdd->recupererTous(
sprintf('SELECT * FROM del_commentaire_vote WHERE ce_proposition = %d', $commentId));
foreach ($resultatsVotes as $vote) {
$proposition_formatee['votes'][$vote['id_vote']] = $this->formaterVote($vote);
}
foreach ($resultatsVotes as $vote) {
$proposition_formatee['votes'][$vote['id_vote']] = $this->formaterVote($vote);
}
 
 
// chargerNombreCommentaire()
// Charger le nombre de commentaires (sans détermination) associé à l'observation
$listeCommentaires = $this->bdd->recupererTous(sprintf(
'SELECT ce_commentaire_parent, ce_proposition, COUNT( id_commentaire ) AS nb '.
'FROM del_commentaire WHERE ce_proposition = %d GROUP BY ce_proposition -- %s',
$commentId, __FILE__ . ':' . __LINE__));
foreach ($listeCommentaires as $ligneProposition) {
// ce test sert à exclure les proposition de 1er niveau qui sont elles aussi des commentaires
if($ligneProposition['ce_commentaire_parent']) {
// TODO/debug: id_commentaire_parent != $commentId ??
// reprendre la "logique" du code... moins de boucles, moins de requêtes, ...
if($ligneProposition['ce_commentaire_parent'] != $commentId) {
// restore_error_handler();
error_log(sprintf("possible error: nb_commentaires = %s: comment = %d, parent = %d, %s",
$ligneProposition['nb'], $commentId, $ligneProposition['ce_commentaire_parent'], __FILE__));
}
$proposition_formatee['nb_commentaires'] = $ligneProposition['nb'];
} else {
$proposition_formatee['observation']['nb_commentaires'] = $ligneProposition['nb'];
}
// chargerNombreCommentaire()
// Charger le nombre de commentaires (sans détermination) associé à l'observation
$listeCommentaires = $this->bdd->recupererTous(sprintf(
'SELECT ce_commentaire_parent, ce_proposition, COUNT( id_commentaire ) AS nb '.
'FROM del_commentaire WHERE ce_proposition = %d GROUP BY ce_proposition -- %s',
$commentId, __FILE__ . ':' . __LINE__));
foreach ($listeCommentaires as $ligneProposition) {
// ce test sert à exclure les proposition de 1er niveau qui sont elles aussi des commentaires
if($ligneProposition['ce_commentaire_parent']) {
// TODO/debug: id_commentaire_parent != $commentId ??
// reprendre la "logique" du code... moins de boucles, moins de requêtes, ...
if($ligneProposition['ce_commentaire_parent'] != $commentId) {
// restore_error_handler();
error_log(sprintf("possible error: nb_commentaires = %s: comment = %d, parent = %d, %s",
$ligneProposition['nb'], $commentId, $ligneProposition['ce_commentaire_parent'], __FILE__));
}
 
return $proposition_formatee;
$proposition_formatee['nb_commentaires'] = $ligneProposition['nb'];
} else {
$proposition_formatee['observation']['nb_commentaires'] = $ligneProposition['nb'];
}
}
 
/**
* Formater un vote en fonction du fichier de configuration config_votes.ini
* @param $votes array()
* */
private function formaterVote($vote) {
$retour = array();
foreach ($vote as $param=>$valeur) {
$retour[$this->mappingVotes[$param]] = $valeur;
}
return $retour;
return $proposition_formatee;
}
 
/**
* Formater un vote en fonction du fichier de configuration config_votes.ini
* @param $votes array()
* */
private function formaterVote($vote) {
$retour = array();
foreach ($vote as $param=>$valeur) {
$retour[$this->mappingVotes[$param]] = $valeur;
}
return $retour;
}
}