0136d6d698a3712ecceb5289349e9c7d2739b9f1
[garradin.git] / include / class.membres.php
1 <?php
2
3 namespace Garradin;
4
5 class Membres
6 {
7 const DROIT_AUCUN = 0;
8 const DROIT_ACCES = 1;
9 const DROIT_ECRITURE = 2;
10 const DROIT_ADMIN = 9;
11
12 const ITEMS_PER_PAGE = 50;
13
14 protected function _getSalt($length)
15 {
16 $str = str_split('./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
17 shuffle($str);
18
19 return implode('',
20 array_rand(
21 $str,
22 $length)
23 );
24 }
25
26 protected function _hashPassword($password)
27 {
28 $salt = '$2a$08$' . $this->_getSalt(22);
29 return crypt($password, $salt);
30 }
31
32 protected function _checkPassword($password, $stored_hash)
33 {
34 return crypt($password, $stored_hash) == $stored_hash;
35 }
36
37 protected function _sessionStart($force = false)
38 {
39 if (!isset($_SESSION) && ($force || isset($_COOKIE[session_name()])))
40 {
41 session_start();
42 }
43
44 return true;
45 }
46
47 public function keepSessionAlive()
48 {
49 $this->_sessionStart(true);
50 }
51
52 public function login($id, $passe)
53 {
54 $db = DB::getInstance();
55 $champ_id = Config::getInstance()->get('champ_identifiant');
56
57 $r = $db->simpleQuerySingle('SELECT id, passe, id_categorie FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id));
58
59 if (empty($r))
60 return false;
61
62 if (!$this->_checkPassword(trim($passe), $r['passe']))
63 return false;
64
65 $droits = $this->getDroits($r['id_categorie']);
66
67 if ($droits['connexion'] == self::DROIT_AUCUN)
68 return false;
69
70 $this->_sessionStart(true);
71 $db->simpleExec('UPDATE membres SET date_connexion = datetime(\'now\') WHERE id = ?;', $r['id']);
72
73 return $this->updateSessionData($r['id'], $droits);
74 }
75
76 public function recoverPasswordCheck($id)
77 {
78 $db = DB::getInstance();
79 $config = Config::getInstance();
80
81 $champ_id = $config->get('champ_identifiant');
82
83 $membre = $db->simpleQuerySingle('SELECT id, email FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id));
84
85 if (!$membre || trim($membre['email']) == '')
86 {
87 return false;
88 }
89
90 $this->_sessionStart(true);
91 $hash = sha1($membre['email'] . $membre['id'] . 'recover' . ROOT . time());
92 $_SESSION['recover_password'] = [
93 'id' => (int) $membre['id'],
94 'email' => $membre['email'],
95 'hash' => $hash
96 ];
97
98 $message = "Bonjour,\n\nVous avez oublié votre mot de passe ? Pas de panique !\n\n";
99 $message.= "Il vous suffit de cliquer sur le lien ci-dessous pour recevoir un nouveau mot de passe.\n\n";
100 $message.= WWW_URL . 'admin/password.php?c=' . substr($hash, -10);
101 $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé.";
102
103 return utils::mail($membre['email'], '['.$config->get('nom_asso').'] Mot de passe perdu ?', $message);
104 }
105
106 public function recoverPasswordConfirm($hash)
107 {
108 $this->_sessionStart();
109
110 if (empty($_SESSION['recover_password']['hash']))
111 return false;
112
113 if (substr($_SESSION['recover_password']['hash'], -10) != $hash)
114 return false;
115
116 $config = Config::getInstance();
117 $db = DB::getInstance();
118
119 $password = utils::suggestPassword();
120
121 $dest = $_SESSION['recover_password']['email'];
122 $id = (int)$_SESSION['recover_password']['id'];
123
124 $message = "Bonjour,\n\nVous avez demandé un nouveau mot de passe pour votre compte.\n\n";
125 $message.= "Votre adresse email : ".$dest."\n";
126 $message.= "Votre nouveau mot de passe : ".$password."\n\n";
127 $message.= "Si vous n'avez pas demandé à recevoir ce message, merci de nous le signaler.";
128
129 $password = $this->_hashPassword($password);
130
131 $db->simpleUpdate('membres', ['passe' => $password], 'id = '.(int)$id);
132
133 return utils::mail($dest, '['.$config->get('nom_asso').'] Nouveau mot de passe', $message);
134 }
135
136 public function updateSessionData($membre = null, $droits = null)
137 {
138 if (is_null($membre))
139 {
140 $membre = $this->get($_SESSION['logged_user']['id']);
141 }
142 elseif (is_int($membre))
143 {
144 $membre = $this->get($membre);
145 }
146
147 if (is_null($droits))
148 {
149 $droits = $this->getDroits($membre['id_categorie']);
150 }
151
152 $membre['droits'] = $droits;
153 $_SESSION['logged_user'] = $membre;
154 return true;
155 }
156
157 public function localLogin()
158 {
159 if (!defined('Garradin\LOCAL_LOGIN'))
160 return false;
161
162 if (trim(LOCAL_LOGIN) == '')
163 return false;
164
165 $db = DB::getInstance();
166 $config = Config::getInstance();
167 $champ_id = $config->get('champ_identifiant');
168
169 if (is_int(LOCAL_LOGIN) && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ? LIMIT 1;', true, LOCAL_LOGIN))
170 {
171 $this->_sessionStart(true);
172 return $this->updateSessionData(LOCAL_LOGIN);
173 }
174 elseif ($id = $db->simpleQuerySingle('SELECT id FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, LOCAL_LOGIN))
175 {
176 $this->_sessionStart(true);
177 return $this->updateSessionData($membre);
178 }
179
180 throw new UserException('Le membre ' . LOCAL_LOGIN . ' n\'existe pas, merci de modifier la directive Garradin\LOCAL_LOGIN.');
181 }
182
183 public function isLogged()
184 {
185 $this->_sessionStart();
186
187 if (empty($_SESSION['logged_user']))
188 {
189 if (defined('Garradin\LOCAL_LOGIN'))
190 {
191 return $this->localLogin();
192 }
193
194 return false;
195 }
196
197 return true;
198 }
199
200 public function getLoggedUser()
201 {
202 if (!$this->isLogged())
203 return false;
204
205 return $_SESSION['logged_user'];
206 }
207
208 public function logout()
209 {
210 $_SESSION = [];
211 setcookie(session_name(), '', 0, '/');
212 return true;
213 }
214
215 public function sessionStore($key, $value)
216 {
217 if (!isset($_SESSION['storage']))
218 {
219 $_SESSION['storage'] = [];
220 }
221
222 if ($value === null)
223 {
224 unset($_SESSION['storage'][$key]);
225 }
226 else
227 {
228 $_SESSION['storage'][$key] = $value;
229 }
230
231 return true;
232 }
233
234 public function sessionGet($key)
235 {
236 if (!isset($_SESSION['storage'][$key]))
237 {
238 return null;
239 }
240
241 return $_SESSION['storage'][$key];
242 }
243
244 public function sendMessage($dest, $sujet, $message, $copie = false)
245 {
246 if (!$this->isLogged())
247 {
248 throw new \LogicException('Cette fonction ne peut être appelée que par un utilisateur connecté.');
249 }
250
251 $from = $this->getLoggedUser();
252 $from = $from['email'];
253 // Uniquement adresse email pour le moment car faudrait trouver comment
254 // indiquer le nom mais qu'il soit correctement échappé FIXME
255
256 $config = Config::getInstance();
257
258 $message .= "\n\n--\nCe message a été envoyé par un membre de ".$config->get('nom_asso');
259 $message .= ", merci de contacter ".$config->get('email_asso')." en cas d'abus.";
260
261 if ($copie)
262 {
263 utils::mail($from, $sujet, $message);
264 }
265
266 return utils::mail($dest, $sujet, $message, ['From' => $from]);
267 }
268
269 // Gestion des données ///////////////////////////////////////////////////////
270
271 public function _checkFields(&$data, $check_editable = true, $check_password = true)
272 {
273 $champs = Config::getInstance()->get('champs_membres');
274
275 foreach ($champs->getAll() as $key=>$config)
276 {
277 if (!$check_editable && (!empty($config['private']) || empty($config['editable'])))
278 {
279 unset($data[$key]);
280 continue;
281 }
282
283 if (!isset($data[$key]) || (!is_array($data[$key]) && trim($data[$key]) === '')
284 || (is_array($data[$key]) && empty($data[$key])))
285 {
286 if (!empty($config['mandatory']) && ($check_password || $key != 'passe'))
287 {
288 throw new UserException('Le champ "' . $config['title'] . '" doit obligatoirement être renseigné.');
289 }
290 elseif (!empty($config['mandatory']))
291 {
292 continue;
293 }
294 }
295
296 if (isset($data[$key]))
297 {
298 if ($config['type'] == 'email' && trim($data[$key]) !== '' && !filter_var($data[$key], FILTER_VALIDATE_EMAIL))
299 {
300 throw new UserException('Adresse e-mail invalide dans le champ "' . $config['title'] . '".');
301 }
302 elseif ($config['type'] == 'url' && trim($data[$key]) !== '' && !filter_var($data[$key], FILTER_VALIDATE_URL))
303 {
304 throw new UserException('Adresse URL invalide dans le champ "' . $config['title'] . '".');
305 }
306 elseif ($config['type'] == 'date' && trim($data[$key]) !== '' && !utils::checkDate($data[$key]))
307 {
308 throw new UserException('Date invalide "' . $config['title'] . '", format attendu : AAAA-MM-JJ.');
309 }
310 elseif ($config['type'] == 'datetime' && trim($data[$key]) !== '')
311 {
312 if (!utils::checkDateTime($data[$key]) || !($dt = new DateTime($data[$key])))
313 {
314 throw new UserException('Date invalide "' . $config['title'] . '", format attendu : AAAA-MM-JJ HH:mm.');
315 }
316
317 $data[$key] = $dt->format('Y-m-d H:i');
318 }
319 elseif ($config['type'] == 'tel')
320 {
321 $data[$key] = utils::normalizePhoneNumber($data[$key]);
322 }
323 elseif ($config['type'] == 'country')
324 {
325 $data[$key] = strtoupper(substr($data[$key], 0, 2));
326 }
327 elseif ($config['type'] == 'checkbox')
328 {
329 $data[$key] = empty($data[$key]) ? 0 : 1;
330 }
331 elseif ($config['type'] == 'number' && trim($data[$key]) !== '')
332 {
333 if (empty($data[$key]))
334 {
335 $data[$key] = 0;
336 }
337
338 if (!is_numeric($data[$key]))
339 throw new UserException('Le champ "' . $config['title'] . '" doit contenir un chiffre.');
340 }
341 elseif ($config['type'] == 'select' && !in_array($data[$key], $config['options']))
342 {
343 throw new UserException('Le champ "' . $config['title'] . '" ne correspond pas à un des choix proposés.');
344 }
345 elseif ($config['type'] == 'multiple')
346 {
347 if (empty($data[$key]) || !is_array($data[$key]))
348 {
349 $data[$key] = 0;
350 continue;
351 }
352
353 $binary = 0;
354
355 foreach ($data[$key] as $k => $v)
356 {
357 if (array_key_exists($k, $config['options']) && !empty($v))
358 {
359 $binary |= 0x01 << $k;
360 }
361 }
362
363 $data[$key] = $binary;
364 }
365
366 // Un champ texte vide c'est un champ NULL
367 if (is_string($data[$key]) && trim($data[$key]) === '')
368 {
369 $data[$key] = null;
370 }
371 }
372 }
373
374 if (isset($data['code_postal']) && trim($data['code_postal']) != '')
375 {
376 if (!empty($data['pays']) && $data['pays'] == 'FR' && !preg_match('!^\d{5}$!', $data['code_postal']))
377 {
378 throw new UserException('Code postal invalide.');
379 }
380 }
381
382 if (!empty($data['passe']) && strlen($data['passe']) < 5)
383 {
384 throw new UserException('Le mot de passe doit faire au moins 5 caractères.');
385 }
386
387 return true;
388 }
389
390 public function add($data = [])
391 {
392 $this->_checkFields($data);
393 $db = DB::getInstance();
394 $config = Config::getInstance();
395 $id = $config->get('champ_identifiant');
396
397 if (!empty($data[$id])
398 && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE '.$id.' = ? LIMIT 1;', false, $data[$id]))
399 {
400 throw new UserException('La valeur du champ '.$id.' est déjà utilisée par un autre membre, hors ce champ doit être unique à chaque membre.');
401 }
402
403 if (isset($data['passe']) && trim($data['passe']) != '')
404 {
405 $data['passe'] = $this->_hashPassword($data['passe']);
406 }
407 else
408 {
409 unset($data['passe']);
410 }
411
412 if (empty($data['id_categorie']))
413 {
414 $data['id_categorie'] = Config::getInstance()->get('categorie_membres');
415 }
416
417 $db->simpleInsert('membres', $data);
418 return $db->lastInsertRowId();
419 }
420
421 public function edit($id, $data = [], $check_editable = true)
422 {
423 $db = DB::getInstance();
424 $config = Config::getInstance();
425
426 if (isset($data['id']) && ($data['id'] == $id || empty($data['id'])))
427 {
428 unset($data['id']);
429 }
430
431 $this->_checkFields($data, $check_editable, false);
432 $champ_id = $config->get('champ_identifiant');
433
434 if (!empty($data[$champ_id])
435 && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE '.$champ_id.' = ? AND id != ? LIMIT 1;', false, $data[$champ_id], (int)$id))
436 {
437 throw new UserException('La valeur du champ '.$champ_id.' est déjà utilisée par un autre membre, hors ce champ doit être unique à chaque membre.');
438 }
439
440 if (!empty($data['id']))
441 {
442 if ($db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ?;', false, (int)$data['id']))
443 {
444 throw new UserException('Ce numéro est déjà attribué à un autre membre.');
445 }
446
447 // Si on ne vérifie pas toutes les tables qui sont liées ici à un ID de membre
448 // la requête de modification provoquera une erreur de contrainte de foreign key
449 // ce qui est normal. Donc : il n'est pas possible de changer l'ID d'un membre qui
450 // a participé au wiki, à la compta, etc.
451 if ($db->simpleQuerySingle('SELECT 1 FROM wiki_revisions WHERE id_auteur = ?;', false, (int)$id)
452 || $db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_auteur = ?;', false, (int)$id))
453 # FIXME || $db->simpleQuerySingle('SELECT 1 FROM wiki_suivi WHERE id_membre = ?;', false, (int)$id))
454 {
455 throw new UserException('Le numéro n\'est pas modifiable pour ce membre car des contenus sont liés à ce numéro de membre (wiki, compta, etc.).');
456 }
457 }
458
459 if (!empty($data['passe']) && trim($data['passe']))
460 {
461 $data['passe'] = $this->_hashPassword($data['passe']);
462 }
463 else
464 {
465 unset($data['passe']);
466 }
467
468 if (isset($data['id_categorie']) && empty($data['id_categorie']))
469 {
470 $data['id_categorie'] = Config::getInstance()->get('categorie_membres');
471 }
472
473 if (empty($data))
474 {
475 return true;
476 }
477
478 return $db->simpleUpdate('membres', $data, 'id = '.(int)$id);
479 }
480
481 public function get($id)
482 {
483 $db = DB::getInstance();
484 $config = Config::getInstance();
485
486 return $db->simpleQuerySingle('SELECT *,
487 '.$config->get('champ_identite').' AS identite,
488 strftime(\'%s\', date_inscription) AS date_inscription,
489 strftime(\'%s\', date_connexion) AS date_connexion
490 FROM membres WHERE id = ? LIMIT 1;', true, (int)$id);
491 }
492
493 public function delete($ids)
494 {
495 if (!is_array($ids))
496 {
497 $ids = [(int)$ids];
498 }
499
500 if ($this->isLogged())
501 {
502 $user = $this->getLoggedUser();
503
504 foreach ($ids as $id)
505 {
506 if ($user['id'] == $id)
507 {
508 throw new UserException('Il n\'est pas possible de supprimer son propre compte.');
509 }
510 }
511 }
512
513 return self::_deleteMembres($ids);
514 }
515
516 public function getNom($id)
517 {
518 $db = DB::getInstance();
519 $config = Config::getInstance();
520
521 return $db->simpleQuerySingle('SELECT '.$config->get('champ_identite').' FROM membres WHERE id = ? LIMIT 1;', false, (int)$id);
522 }
523
524 public function getDroits($id)
525 {
526 $db = DB::getInstance();
527 $droits = $db->simpleQuerySingle('SELECT * FROM membres_categories WHERE id = ?;', true, (int)$id);
528
529 foreach ($droits as $key=>$value)
530 {
531 unset($droits[$key]);
532 $key = str_replace('droit_', '', $key, $found);
533
534 if ($found)
535 {
536 $droits[$key] = (int) $value;
537 }
538 }
539
540 return $droits;
541 }
542
543 public function search($field, $query)
544 {
545 $db = DB::getInstance();
546 $config = Config::getInstance();
547
548 $champs = $config->get('champs_membres');
549
550 if ($field != 'id' && !$champs->get($field))
551 {
552 throw new \UnexpectedValueException($field . ' is not a valid field');
553 }
554
555 $champ = $champs->get($field);
556
557 if ($champ['type'] == 'multiple')
558 {
559 $where = 'WHERE '.$field.' & (1 << '.(int)$query.')';
560 $order = false;
561 }
562 elseif ($champ['type'] == 'tel')
563 {
564 $query = utils::normalizePhoneNumber($query);
565 $query = preg_replace('!^0+!', '', $query);
566
567 if ($query == '')
568 {
569 return false;
570 }
571
572 $where = 'WHERE '.$field.' LIKE \'%'.$db->escapeString($query).'\'';
573 $order = $field;
574 }
575 elseif (!$champs->isText($field))
576 {
577 $where = 'WHERE '.$field.' = \''.$db->escapeString($query).'\'';
578 $order = $field;
579 }
580 else
581 {
582 $where = 'WHERE transliterate_to_ascii('.$field.') LIKE transliterate_to_ascii(\'%'.$db->escapeString($query).'%\')';
583 $order = 'transliterate_to_ascii('.$field.') COLLATE NOCASE';
584 }
585
586 $fields = array_keys($champs->getListedFields());
587
588 if (!in_array($field, $fields))
589 {
590 $fields[] = $field;
591 }
592
593 if (!in_array('email', $fields))
594 {
595 $fields[] = 'email';
596 }
597
598 return $db->simpleStatementFetch(
599 'SELECT id, id_categorie, ' . implode(', ', $fields) . ',
600 '.$config->get('champ_identite').' AS identite,
601 strftime(\'%s\', date_inscription) AS date_inscription
602 FROM membres ' . $where . ($order ? ' ORDER BY ' . $order : '') . '
603 LIMIT 1000;',
604 SQLITE3_ASSOC
605 );
606 }
607
608 public function listByCategory($cat, $fields, $page = 1, $order = null, $desc = false)
609 {
610 $begin = ($page - 1) * self::ITEMS_PER_PAGE;
611
612 $db = DB::getInstance();
613 $config = Config::getInstance();
614
615 $champs = $config->get('champs_membres');
616
617 if (is_int($cat) && $cat)
618 $where = 'WHERE id_categorie = '.(int)$cat;
619 elseif (is_array($cat))
620 $where = 'WHERE id_categorie IN ('.implode(',', $cat).')';
621 else
622 $where = '';
623
624 if (is_null($order) || !$champs->get($order))
625 $order = 'id';
626
627 if (!empty($fields) && $order != 'id' && $champs->isText($order))
628 {
629 $order = 'transliterate_to_ascii('.$order.') COLLATE NOCASE';
630 }
631
632 if ($desc)
633 {
634 $order .= ' DESC';
635 }
636
637 if (!in_array('email', $fields))
638 {
639 $fields []= 'email';
640 }
641
642 $fields = implode(', ', $fields);
643
644 $query = 'SELECT id, id_categorie, '.$fields.', '.$config->get('champ_identite').' AS identite,
645 strftime(\'%s\', date_inscription) AS date_inscription
646 FROM membres '.$where.'
647 ORDER BY '.$order.' LIMIT ?, ?;';
648
649 return $db->simpleStatementFetch($query, SQLITE3_ASSOC, $begin, self::ITEMS_PER_PAGE);
650 }
651
652 public function countByCategory($cat = 0)
653 {
654 $db = DB::getInstance();
655
656 if (is_int($cat) && $cat)
657 $where = 'WHERE id_categorie = '.(int)$cat;
658 elseif (is_array($cat))
659 $where = 'WHERE id_categorie IN ('.implode(',', $cat).')';
660 else
661 $where = '';
662
663 return $db->simpleQuerySingle('SELECT COUNT(*) FROM membres '.$where.';');
664 }
665
666 public function countAllButHidden()
667 {
668 $db = DB::getInstance();
669 return $db->simpleQuerySingle('SELECT COUNT(*) FROM membres WHERE id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1);');
670 }
671
672 static public function changeCategorie($id_cat, $membres)
673 {
674 foreach ($membres as &$id)
675 {
676 $id = (int) $id;
677 }
678
679 $db = DB::getInstance();
680 return $db->simpleUpdate('membres',
681 ['id_categorie' => (int)$id_cat],
682 'id IN ('.implode(',', $membres).')'
683 );
684 }
685
686 static protected function _deleteMembres($membres)
687 {
688 foreach ($membres as &$id)
689 {
690 $id = (int) $id;
691 }
692
693 $membres = implode(',', $membres);
694
695 $db = DB::getInstance();
696 $db->exec('UPDATE wiki_revisions SET id_auteur = NULL WHERE id_auteur IN ('.$membres.');');
697 $db->exec('UPDATE compta_journal SET id_auteur = NULL WHERE id_auteur IN ('.$membres.');');
698 //$db->exec('DELETE FROM wiki_suivi WHERE id_membre IN ('.$membres.');');
699 return $db->exec('DELETE FROM membres WHERE id IN ('.$membres.');');
700 }
701
702 public function sendMessageToCategory($dest, $sujet, $message, $subscribed_only = false)
703 {
704 $config = Config::getInstance();
705
706 $headers = [
707 'From' => '"'.$config->get('nom_asso').'" <'.$config->get('email_asso').'>',
708 ];
709 $message .= "\n\n--\n".$config->get('nom_asso')."\n".$config->get('site_asso');
710
711 if ($dest == 0)
712 $where = 'id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)';
713 else
714 $where = 'id_categorie = '.(int)$dest;
715
716 if ($subscribed_only)
717 {
718 $where .= ' AND lettre_infos = 1';
719 }
720
721 $db = DB::getInstance();
722 $res = $db->query('SELECT email FROM membres WHERE LENGTH(email) > 0 AND '.$where.' ORDER BY id;');
723
724 $sujet = '['.$config->get('nom_asso').'] '.$sujet;
725
726 while ($row = $res->fetchArray(SQLITE3_ASSOC))
727 {
728 utils::mail($row['email'], $sujet, $message, $headers);
729 }
730
731 return true;
732 }
733
734 public function searchSQL($query)
735 {
736 $db = DB::getInstance();
737
738 $st = $db->prepare($query);
739
740 if (!$st->readOnly())
741 {
742 throw new UserException('Seules les requêtes en lecture sont autorisées.');
743 }
744
745 if (!preg_match('/LIMIT\s+/', $query))
746 {
747 $query = preg_replace('/;?\s*$/', '', $query);
748 $query .= ' LIMIT 100';
749 }
750
751 if (!preg_match('/FROM\s+membres(?:\s+|$|;)/i', $query))
752 {
753 throw new UserException('Seules les requêtes sur la table membres sont autorisées.');
754 }
755
756 if (preg_match('/;\s*(.+?)$/', $query))
757 {
758 throw new UserException('Une seule requête peut être envoyée en même temps.');
759 }
760
761 $st = $db->prepare($query);
762
763 $res = $st->execute();
764 $out = [];
765
766 while ($row = $res->fetchArray(SQLITE3_ASSOC))
767 {
768 if (array_key_exists('passe', $row))
769 {
770 unset($row['passe']);
771 }
772
773 $out[] = $row;
774 }
775
776 return $out;
777 }
778
779 public function schemaSQL()
780 {
781 $db = DB::getInstance();
782
783 $tables = [
784 'membres' => $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'),
785 'categories'=> $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'),
786 ];
787
788 return $tables;
789 }
790 }
791
792 ?>