Index: src/include/lib/Garradin/Membres.php ================================================================== --- src/include/lib/Garradin/Membres.php +++ src/include/lib/Garradin/Membres.php @@ -292,144 +292,10 @@ public function getIDWithNumero($numero) { return DB::getInstance()->firstColumn('SELECT id FROM membres WHERE numero = ?;', (int) $numero); } - public function buildSQLSearchQuery(array $groups, $order, $desc = false, $limit = 100) - { - $db = DB::getInstance(); - $config = Config::getInstance(); - - $champs = $config->get('champs_membres'); - $colonnes = []; - - $query_groups = []; - - foreach ($groups as $group) - { - if (!isset($group['conditions'], $group['operator']) - || !is_array($group['conditions']) - || ($group['operator'] != 'AND' && $group['operator'] != 'OR')) - { - // Ignorer les groupes de conditions invalides - continue; - } - - $query_group_conditions = []; - - foreach ($group['conditions'] as $condition) - { - if (!isset($condition['column'], $condition['operator']) - || (isset($condition['values']) && !is_array($condition['values']))) - { - // Ignorer les conditions invalides - continue; - } - - if (!$champs->get($condition['column'])) - { - // Ignorer une condition qui se rapporte à une colonne - // qui n'existe pas, cas possible si on reprend une recherche - // après avoir modifié les fiches de membres - continue; - } - - $colonnes[] = $condition['column']; - $champ = $champs->get($condition['column']); - - if ($champs->isText($condition['column'])) - { - $query = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE %s', $db->quoteIdentifier($condition['column']), $condition['operator']); - } - else - { - $query = sprintf('%s %s', $db->quoteIdentifier($condition['column']), $condition['operator']); - } - - $values = isset($condition['values']) ? $condition['values'] : []; - - $values = array_map(['Garradin\Utils', 'transliterateToAscii'], $values); - - if ($champ->type == 'tel') - { - // Normaliser le numéro de téléphone - $values = array_map(['Garradin\Utils', 'normalizePhoneNumber'], $values); - } - - if ($condition['operator'] == '&') - { - $new_query = []; - - foreach ($values as $value) - { - $new_query[] = sprintf('%s (1 << %d)', $query, (int) $value); - } - - $query = '(' . implode(' AND ', $new_query) . ')'; - } - elseif (strpos($query, '??') !== false) - { - $values = array_map([$db, 'quote'], $values); - $query = str_replace('??', implode(', ', $values), $query); - } - elseif (preg_match('/%\?%|%\?|\?%/', $query, $match)) - { - $value = str_replace(['%_'], ['\\%', '\\_'], reset($values)); - $value = str_replace('?', $value, $match[0]); - $query = str_replace($match[0], sprintf('%s ESCAPE \'\\\'', $db->quote($value)), $query); - } - elseif (strpos($query, '?') !== false) - { - $expected = substr_count($query, '?'); - $found = count($values); - - if ($expected != $found) - { - throw new \RuntimeException(sprintf('Operator %s expects at least %d parameters, only %d supplied', $condition['operator'], $expected, $found)); - } - - for ($i = 0; $i < $expected; $i++) - { - $pos = strpos($query, '?'); - $query = substr_replace($query, $db->quote(array_shift($values)), $pos, 1); - } - } - - $query_group_conditions[] = $query; - } - - $query_groups[] = implode(' ' . $group['operator'] . ' ', $query_group_conditions); - } - - $colonnes = array_unique($colonnes); - - if (!in_array($config->get('champ_identite'), $colonnes)) - { - array_unshift($colonnes, $config->get('champ_identite')); - } - - $colonnes = array_map([$db, 'quoteIdentifier'], $colonnes); - - if ($champs->isText($order)) - { - $order = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE', $db->quoteIdentifier($order)); - } - else - { - $order = $db->quoteIdentifier($order); - } - - $sql_query = sprintf('SELECT id, %s FROM membres WHERE %s ORDER BY %s %s LIMIT %d;', - implode(', ', $colonnes), - '(' . implode(') AND (', $query_groups) . ')', - $order, - $desc ? 'DESC' : 'ASC', - (int) $limit); - - return $sql_query; - } - public function getSearchHeaderFields(array $result) { if (!count($result)) { return false; @@ -447,59 +313,10 @@ } return $fields; } - public function searchSQL($query) - { - $db = DB::getInstance(); - - if (!preg_match('/LIMIT\s+/i', $query)) - { - $query = preg_replace('/;?\s*$/', '', $query); - $query .= ' LIMIT 100'; - } - - if (preg_match('/;\s*(.+?)$/', $query)) - { - throw new UserException('Une seule requête peut être envoyée en même temps.'); - } - - $st = $db->prepare($query); - - if (!$st->readOnly()) - { - throw new UserException('Seules les requêtes en lecture sont autorisées.'); - } - - $res = $st->execute(); - $out = []; - - while ($row = $res->fetchArray(SQLITE3_ASSOC)) - { - if (array_key_exists('passe', $row)) - { - unset($row['passe']); - } - - $out[] = (object) $row; - } - - return $out; - } - - public function schemaSQL() - { - $db = DB::getInstance(); - - $tables = [ - 'membres' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'), - 'categories'=> $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'), - ]; - - return $tables; - } public function listByCategory($cat, $fields, $page = 1, $order = null, $desc = false) { $begin = ($page - 1) * self::ITEMS_PER_PAGE; $db = DB::getInstance(); ADDED src/include/lib/Garradin/Recherche.php Index: src/include/lib/Garradin/Recherche.php ================================================================== --- src/include/lib/Garradin/Recherche.php +++ src/include/lib/Garradin/Recherche.php @@ -0,0 +1,391 @@ +test('membres', 'id = ?', $id_membre)) + { + throw new \InvalidArgumentException('Numéro d\'utilisateur inconnu.'); + } + + if ($type !== self::TYPE_SQL && $type !== self::TYPE_JSON) + { + throw new \InvalidArgumentException('Type de recherche inconnu.'); + } + + if (!in_array($cible, self::TARGETS, true)) + { + throw new \InvalidArgumentException('Cible de recherche invalide.'); + } + + if ($type == self::TYPE_SQL && !is_string($query)) + { + throw new \InvalidArgumentException('Recherche invalide pour le type SQL'); + } + + if ($type == self::TYPE_JSON) + { + if (!is_array($query)) + { + throw new \InvalidArgumentException('Recherche invalide pour le type JSON'); + } + + $query = json_encode($query); + + if (!json_decode($query)) + { + throw new \InvalidArgumentException('JSON invalide pour le type JSON'); + } + } + + return true; + } + + public function edit($id, $intitule, $id_membre, $type, $cible, $contenu) + { + $this->_checkFields($intitule, $id_membre, $type, $cible, $contenu); + + return DB::getInstance()->update('recherches', + compact('intitule', 'id_membre', 'type', 'cible', 'contenu'), + 'id = ' . (int)$id); + } + + public function add($intitule, $id_membre, $type, $cible, $contenu) + { + $this->_checkFields($intitule, $id_membre, $type, $cible, $contenu); + + $db = DB::getInstance(); + + $db->insert('recherches', + compact('intitule', 'id_membre', 'type', 'cible', 'contenu')); + + return $db->lastInsertRowId(); + } + + public function remove($id) + { + return DB::getInstance()->delete('recherches', 'id = ?', (int) $id); + } + + public function get($id) + { + return DB::getInstance()->first('SELECT * FROM recherches WHERE id = ?;', (int) $id); + } + + public function getList($id_membre) + { + return DB::getInstance()->get('SELECT id, type, intitule, type, id_membre FROM recherches + WHERE id_membre IS NULL OR id_membre = ? ORDER BY intitule;', (int)$id_membre); + } + + /** + * Lancer une recherche enregistrée + */ + public function search($id) + { + $search = $this->get($id); + + if (!$search) + { + return false; + } + + if ($search->type == self::TYPE_JSON) + { + $search->contenu = $this->buildQuery($search->target, json_decode($search->contenu)); + } + + return $this->searchSQL($search->target, $query); + } + + /** + * Renvoie la liste des colonnes d'une cible + */ + public function getColumns($target) + { + $columns = []; + + if ($target == 'membres') + { + $champs = Config::getInstance()->get('champs_membres'); + + foreach ($champs->getList() as $champ => $config) + { + $column = (object) [ + 'realType' => $config->type, + 'textMatch'=> $champs->isText($champ), + 'label' => $config->title, + 'type' => 'text', + 'null' => true, + ]; + + if ($config->type == 'checkbox') + { + $column->type = 'boolean'; + } + elseif ($config->type == 'select') + { + $column->type = 'enum'; + $column->values = $config->options; + } + elseif ($config->type == 'multiple') + { + $column->type = 'bitwise'; + $column->values = $config->options; + } + elseif ($config->type == 'date' || $config->type == 'datetime') + { + $column->type = $config->type; + } + elseif ($config->type == 'number' || $champ == 'numero') + { + $column->type = 'integer'; + } + + $columns[$champ] = $column; + } + } + + return $columns; + } + + /** + * Construire une recherche SQL à partir d'un objet généré par QueryBuilder + * @param string $target Cible de la requête : membres, compta_journal, etc. + * @param array $groups Groupes de critères + * @param string $order Ordre de tri + * @param boolean $desc Inverser le tri + * @param integer $limit Limite + * @return string Chaîne SQL + */ + public function buildQuery($target, array $groups, $order, $desc = false, $limit = 100) + { + if (!in_array($target, self::TARGETS, true)) + { + throw new \InvalidArgumentException('Cible inconnue : ' . $target); + } + + if ($target == 'membres') + { + $config = Config::getInstance(); + $champs = $config->get('champs_membres'); + } + + $db = DB::getInstance(); + $target_columns = $this->getColumns($target); + $query_columns = []; + + $query_groups = []; + + foreach ($groups as $group) + { + if (!isset($group['conditions'], $group['operator']) + || !is_array($group['conditions']) + || ($group['operator'] != 'AND' && $group['operator'] != 'OR')) + { + // Ignorer les groupes de conditions invalides + continue; + } + + $query_group_conditions = []; + + foreach ($group['conditions'] as $condition) + { + if (!isset($condition['column'], $condition['operator']) + || (isset($condition['values']) && !is_array($condition['values']))) + { + // Ignorer les conditions invalides + continue; + } + + if (!array_key_exists($condition['column'], $target_columns)) + { + // Ignorer une condition qui se rapporte à une colonne + // qui n'existe pas, cas possible si on reprend une recherche + // après avoir modifié les fiches de membres + continue; + } + + $query_columns[] = $condition['column']; + $column = $target_columns[$condition['column']]; + + if ($column->textMatch == 'text') + { + $query = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE %s', $db->quoteIdentifier($condition['column']), $condition['operator']); + } + else + { + $query = sprintf('%s %s', $db->quoteIdentifier($condition['column']), $condition['operator']); + } + + $values = isset($condition['values']) ? $condition['values'] : []; + + $values = array_map(['Garradin\Utils', 'transliterateToAscii'], $values); + + if ($column->type == 'tel') + { + // Normaliser le numéro de téléphone + $values = array_map(['Garradin\Utils', 'normalizePhoneNumber'], $values); + } + + // L'opérateur binaire est un peu spécial + if ($condition['operator'] == '&') + { + $new_query = []; + + foreach ($values as $value) + { + $new_query[] = sprintf('%s (1 << %d)', $query, (int) $value); + } + + $query = '(' . implode(' AND ', $new_query) . ')'; + } + // Remplacement de liste + elseif (strpos($query, '??') !== false) + { + $values = array_map([$db, 'quote'], $values); + $query = str_replace('??', implode(', ', $values), $query); + } + // Remplacement de recherche LIKE + elseif (preg_match('/%\?%|%\?|\?%/', $query, $match)) + { + $value = str_replace(['%_'], ['\\%', '\\_'], reset($values)); + $value = str_replace('?', $value, $match[0]); + $query = str_replace($match[0], sprintf('%s ESCAPE \'\\\'', $db->quote($value)), $query); + } + // Remplacement de paramètre + elseif (strpos($query, '?') !== false) + { + $expected = substr_count($query, '?'); + $found = count($values); + + if ($expected != $found) + { + throw new \RuntimeException(sprintf('Operator %s expects at least %d parameters, only %d supplied', $condition['operator'], $expected, $found)); + } + + for ($i = 0; $i < $expected; $i++) + { + $pos = strpos($query, '?'); + $query = substr_replace($query, $db->quote(array_shift($values)), $pos, 1); + } + } + + $query_group_conditions[] = $query; + } + + if ($query_group_conditions) + { + $query_groups[] = implode(' ' . $group['operator'] . ' ', $query_group_conditions); + } + } + + $query_columns = array_unique($query_columns); + + // Ajout du champ identité si pas présent + if ($target == 'membres' && !in_array($config->get('champ_identite'), $query_columns)) + { + array_unshift($query_columns, $config->get('champ_identite')); + } + + if ($target_columns[$order]->textMatch) + { + $order = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE', $db->quoteIdentifier($order)); + } + else + { + $order = $db->quoteIdentifier($order); + } + + $query_columns = array_map([$db, 'quoteIdentifier'], $query_columns); + + $sql_query = sprintf('SELECT id, %s FROM %s WHERE %s ORDER BY %s %s LIMIT %d;', + implode(', ', $query_columns), + $target, + '(' . implode(') AND (', $query_groups) . ')', + $order, + $desc ? 'DESC' : 'ASC', + (int) $limit); + + return $sql_query; + } + + /** + * Lancer une recherche SQL + */ + public function searchSQL($target, $query) + { + if (!in_array($target, self::TARGETS, true)) + { + throw new \InvalidArgumentException('Cible inconnue : ' . $target); + } + + $db = DB::getInstance(); + + if (!preg_match('/LIMIT\s+/i', $query)) + { + $query = preg_replace('/;?\s*$/', '', $query); + $query .= ' LIMIT 100'; + } + + if (preg_match('/;\s*(.+?)$/', $query)) + { + throw new UserException('Une seule requête peut être envoyée en même temps.'); + } + + $st = $db->prepare($query); + + if (!$st->readOnly()) + { + throw new UserException('Seules les requêtes en lecture sont autorisées.'); + } + + $res = $st->execute(); + $out = []; + + while ($row = $res->fetchArray(SQLITE3_ASSOC)) + { + $out[] = (object) $row; + } + + return $out; + } + + public function schema($target) + { + $db = DB::getInstance(); + + if ($target == 'membres') + { + $tables = [ + 'membres' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'), + 'categories'=> $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'), + ]; + } + + return $tables; + } +} Index: src/templates/admin/membres/import.tpl ================================================================== --- src/templates/admin/membres/import.tpl +++ src/templates/admin/membres/import.tpl @@ -1,8 +1,10 @@ {include file="admin/_head.tpl" title="Import & export des membres" current="membres" js=1} -
Votre message a été envoyé.
{/if} Index: src/templates/admin/membres/recherche.tpl ================================================================== --- src/templates/admin/membres/recherche.tpl +++ src/templates/admin/membres/recherche.tpl @@ -1,14 +1,8 @@ -{include file="admin/_head.tpl" title="Recherche de membre" current="membres" js=1 custom_js=['sql_query_builder.js']} - -{if $session->canAccess('membres', Garradin\Membres::DROIT_ADMIN)} - -{/if} +{include file="admin/_head.tpl" title="Recherche de membre" current="membres" js=1 custom_js=['sql_query_builder.min.js']} + +{include file="admin/membres/_nav.tpl" current="recherche"}