9 const DROIT_ECRITURE
= 2;
10 const DROIT_ADMIN
= 9;
12 const ITEMS_PER_PAGE
= 50;
14 protected function _getSalt($length)
16 $str = str_split('./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
26 protected function _hashPassword($password)
28 $salt = '$2a$08$' . $this->_getSalt(22);
29 return crypt($password, $salt);
32 protected function _checkPassword($password, $stored_hash)
34 return crypt($password, $stored_hash) == $stored_hash;
37 protected function _sessionStart($force = false)
39 if (!isset($_SESSION) && ($force ||
isset($_COOKIE[session_name()])))
47 public function keepSessionAlive()
49 $this->_sessionStart(true);
52 public function login($id, $passe)
54 $db = DB
::getInstance();
55 $champ_id = Config
::getInstance()->get('champ_identifiant');
57 $r = $db->simpleQuerySingle('SELECT id, passe, id_categorie FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id));
62 if (!$this->_checkPassword(trim($passe), $r['passe']))
65 $droits = $this->getDroits($r['id_categorie']);
67 if ($droits['connexion'] == self
::DROIT_AUCUN
)
70 $this->_sessionStart(true);
71 $db->simpleExec('UPDATE membres SET date_connexion = datetime(\'now\') WHERE id = ?;', $r['id']);
73 return $this->updateSessionData($r['id'], $droits);
76 public function recoverPasswordCheck($id)
78 $db = DB
::getInstance();
79 $config = Config
::getInstance();
81 $champ_id = $config->get('champ_identifiant');
83 $membre = $db->simpleQuerySingle('SELECT id, email FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id));
85 if (!$membre ||
trim($membre['email']) == '')
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'],
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é.";
103 return utils
::mail($membre['email'], '['.$config->get('nom_asso').'] Mot de passe perdu ?', $message);
106 public function recoverPasswordConfirm($hash)
108 $this->_sessionStart();
110 if (empty($_SESSION['recover_password']['hash']))
113 if (substr($_SESSION['recover_password']['hash'], -10) != $hash)
116 $config = Config
::getInstance();
117 $db = DB
::getInstance();
119 $password = utils
::suggestPassword();
121 $dest = $_SESSION['recover_password']['email'];
122 $id = (int)$_SESSION['recover_password']['id'];
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.";
129 $password = $this->_hashPassword($password);
131 $db->simpleUpdate('membres', ['passe' => $password], 'id = '.(int)$id);
133 return utils
::mail($dest, '['.$config->get('nom_asso').'] Nouveau mot de passe', $message);
136 public function updateSessionData($membre = null, $droits = null)
138 if (is_null($membre))
140 $membre = $this->get($_SESSION['logged_user']['id']);
142 elseif (is_int($membre))
144 $membre = $this->get($membre);
147 if (is_null($droits))
149 $droits = $this->getDroits($membre['id_categorie']);
152 $membre['droits'] = $droits;
153 $_SESSION['logged_user'] = $membre;
157 public function localLogin()
159 if (!defined('Garradin\LOCAL_LOGIN'))
162 if (trim(LOCAL_LOGIN
) == '')
165 $db = DB
::getInstance();
166 $config = Config
::getInstance();
167 $champ_id = $config->get('champ_identifiant');
169 if (is_int(LOCAL_LOGIN
) && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ? LIMIT 1;', true, LOCAL_LOGIN
))
171 $this->_sessionStart(true);
172 return $this->updateSessionData(LOCAL_LOGIN
);
174 elseif ($id = $db->simpleQuerySingle('SELECT id FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, LOCAL_LOGIN
))
176 $this->_sessionStart(true);
177 return $this->updateSessionData($membre);
180 throw new UserException('Le membre ' . LOCAL_LOGIN
. ' n\'existe pas, merci de modifier la directive Garradin\LOCAL_LOGIN.');
183 public function isLogged()
185 $this->_sessionStart();
187 if (empty($_SESSION['logged_user']))
189 if (defined('Garradin\LOCAL_LOGIN'))
191 return $this->localLogin();
200 public function getLoggedUser()
202 if (!$this->isLogged())
205 return $_SESSION['logged_user'];
208 public function logout()
211 setcookie(session_name(), '', 0, '/');
215 public function sessionStore($key, $value)
217 if (!isset($_SESSION['storage']))
219 $_SESSION['storage'] = [];
224 unset($_SESSION['storage'][$key]);
228 $_SESSION['storage'][$key] = $value;
234 public function sessionGet($key)
236 if (!isset($_SESSION['storage'][$key]))
241 return $_SESSION['storage'][$key];
244 public function sendMessage($dest, $sujet, $message, $copie = false)
246 if (!$this->isLogged())
248 throw new \
LogicException('Cette fonction ne peut être appelée que par un utilisateur connecté.');
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
256 $config = Config
::getInstance();
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.";
263 utils
::mail($from, $sujet, $message);
266 return utils
::mail($dest, $sujet, $message, ['From' => $from]);
269 // Gestion des données ///////////////////////////////////////////////////////
271 public function _checkFields(&$data, $check_editable = true, $check_password = true)
273 $champs = Config
::getInstance()->get('champs_membres');
275 foreach ($champs->getAll() as $key=>$config)
277 if (!$check_editable && (!empty($config['private']) ||
empty($config['editable'])))
283 if (!isset($data[$key]) ||
(!is_array($data[$key]) && trim($data[$key]) === '')
284 ||
(is_array($data[$key]) && empty($data[$key])))
286 if (!empty($config['mandatory']) && ($check_password ||
$key != 'passe'))
288 throw new UserException('Le champ "' . $config['title'] . '" doit obligatoirement être renseigné.');
290 elseif (!empty($config['mandatory']))
296 if (isset($data[$key]))
298 if ($config['type'] == 'email' && trim($data[$key]) !== '' && !filter_var($data[$key], FILTER_VALIDATE_EMAIL
))
300 throw new UserException('Adresse e-mail invalide dans le champ "' . $config['title'] . '".');
302 elseif ($config['type'] == 'url' && trim($data[$key]) !== '' && !filter_var($data[$key], FILTER_VALIDATE_URL
))
304 throw new UserException('Adresse URL invalide dans le champ "' . $config['title'] . '".');
306 elseif ($config['type'] == 'date' && trim($data[$key]) !== '' && !utils
::checkDate($data[$key]))
308 throw new UserException('Date invalide "' . $config['title'] . '", format attendu : AAAA-MM-JJ.');
310 elseif ($config['type'] == 'datetime' && trim($data[$key]) !== '')
312 if (!utils
::checkDateTime($data[$key]) ||
!($dt = new DateTime($data[$key])))
314 throw new UserException('Date invalide "' . $config['title'] . '", format attendu : AAAA-MM-JJ HH:mm.');
317 $data[$key] = $dt->format('Y-m-d H:i');
319 elseif ($config['type'] == 'tel')
321 $data[$key] = utils
::normalizePhoneNumber($data[$key]);
323 elseif ($config['type'] == 'country')
325 $data[$key] = strtoupper(substr($data[$key], 0, 2));
327 elseif ($config['type'] == 'checkbox')
329 $data[$key] = empty($data[$key]) ?
0 : 1;
331 elseif ($config['type'] == 'number' && trim($data[$key]) !== '')
333 if (empty($data[$key]))
338 if (!is_numeric($data[$key]))
339 throw new UserException('Le champ "' . $config['title'] . '" doit contenir un chiffre.');
341 elseif ($config['type'] == 'select' && !in_array($data[$key], $config['options']))
343 throw new UserException('Le champ "' . $config['title'] . '" ne correspond pas à un des choix proposés.');
345 elseif ($config['type'] == 'multiple')
347 if (empty($data[$key]) ||
!is_array($data[$key]))
355 foreach ($data[$key] as $k => $v)
357 if (array_key_exists($k, $config['options']) && !empty($v))
359 $binary |
= 0x01 << $k;
363 $data[$key] = $binary;
366 // Un champ texte vide c'est un champ NULL
367 if (is_string($data[$key]) && trim($data[$key]) === '')
374 if (isset($data['code_postal']) && trim($data['code_postal']) != '')
376 if (!empty($data['pays']) && $data['pays'] == 'FR' && !preg_match('!^\d{5}$!', $data['code_postal']))
378 throw new UserException('Code postal invalide.');
382 if (!empty($data['passe']) && strlen($data['passe']) < 5)
384 throw new UserException('Le mot de passe doit faire au moins 5 caractères.');
390 public function add($data = [])
392 $this->_checkFields($data);
393 $db = DB
::getInstance();
394 $config = Config
::getInstance();
395 $id = $config->get('champ_identifiant');
397 if (!empty($data[$id])
398 && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE '.$id.' = ? LIMIT 1;', false, $data[$id]))
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.');
403 if (isset($data['passe']) && trim($data['passe']) != '')
405 $data['passe'] = $this->_hashPassword($data['passe']);
409 unset($data['passe']);
412 if (empty($data['id_categorie']))
414 $data['id_categorie'] = Config
::getInstance()->get('categorie_membres');
417 $db->simpleInsert('membres', $data);
418 return $db->lastInsertRowId();
421 public function edit($id, $data = [], $check_editable = true)
423 $db = DB
::getInstance();
424 $config = Config
::getInstance();
426 if (isset($data['id']) && ($data['id'] == $id ||
empty($data['id'])))
431 $this->_checkFields($data, $check_editable, false);
432 $champ_id = $config->get('champ_identifiant');
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))
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.');
440 if (!empty($data['id']))
442 if ($db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ?;', false, (int)$data['id']))
444 throw new UserException('Ce numéro est déjà attribué à un autre membre.');
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))
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.).');
459 if (!empty($data['passe']) && trim($data['passe']))
461 $data['passe'] = $this->_hashPassword($data['passe']);
465 unset($data['passe']);
468 if (isset($data['id_categorie']) && empty($data['id_categorie']))
470 $data['id_categorie'] = Config
::getInstance()->get('categorie_membres');
478 return $db->simpleUpdate('membres', $data, 'id = '.(int)$id);
481 public function get($id)
483 $db = DB
::getInstance();
484 $config = Config
::getInstance();
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);
493 public function delete($ids)
500 if ($this->isLogged())
502 $user = $this->getLoggedUser();
504 foreach ($ids as $id)
506 if ($user['id'] == $id)
508 throw new UserException('Il n\'est pas possible de supprimer son propre compte.');
513 return self
::_deleteMembres($ids);
516 public function getNom($id)
518 $db = DB
::getInstance();
519 $config = Config
::getInstance();
521 return $db->simpleQuerySingle('SELECT '.$config->get('champ_identite').' FROM membres WHERE id = ? LIMIT 1;', false, (int)$id);
524 public function getDroits($id)
526 $db = DB
::getInstance();
527 $droits = $db->simpleQuerySingle('SELECT * FROM membres_categories WHERE id = ?;', true, (int)$id);
529 foreach ($droits as $key=>$value)
531 unset($droits[$key]);
532 $key = str_replace('droit_', '', $key, $found);
536 $droits[$key] = (int) $value;
543 public function search($field, $query)
545 $db = DB
::getInstance();
546 $config = Config
::getInstance();
548 $champs = $config->get('champs_membres');
550 if ($field != 'id' && !$champs->get($field))
552 throw new \
UnexpectedValueException($field . ' is not a valid field');
555 $champ = $champs->get($field);
557 if ($champ['type'] == 'multiple')
559 $where = 'WHERE '.$field.' & (1 << '.(int)$query.')';
562 elseif ($champ['type'] == 'tel')
564 $query = utils
::normalizePhoneNumber($query);
565 $query = preg_replace('!^0+!', '', $query);
572 $where = 'WHERE '.$field.' LIKE \'%'.$db->escapeString($query).'\'';
575 elseif (!$champs->isText($field))
577 $where = 'WHERE '.$field.' = \''.$db->escapeString($query).'\'';
582 $where = 'WHERE transliterate_to_ascii('.$field.') LIKE transliterate_to_ascii(\'%'.$db->escapeString($query).'%\')';
583 $order = 'transliterate_to_ascii('.$field.') COLLATE NOCASE';
586 $fields = array_keys($champs->getListedFields());
588 if (!in_array($field, $fields))
593 if (!in_array('email', $fields))
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 : '') . '
608 public function listByCategory($cat, $fields, $page = 1, $order = null, $desc = false)
610 $begin = ($page - 1) * self
::ITEMS_PER_PAGE
;
612 $db = DB
::getInstance();
613 $config = Config
::getInstance();
615 $champs = $config->get('champs_membres');
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).')';
624 if (is_null($order) ||
!$champs->get($order))
627 if (!empty($fields) && $order != 'id' && $champs->isText($order))
629 $order = 'transliterate_to_ascii('.$order.') COLLATE NOCASE';
637 if (!in_array('email', $fields))
642 $fields = implode(', ', $fields);
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 ?, ?;';
649 return $db->simpleStatementFetch($query, SQLITE3_ASSOC
, $begin, self
::ITEMS_PER_PAGE
);
652 public function countByCategory($cat = 0)
654 $db = DB
::getInstance();
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).')';
663 return $db->simpleQuerySingle('SELECT COUNT(*) FROM membres '.$where.';');
666 public function countAllButHidden()
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);');
672 static public function changeCategorie($id_cat, $membres)
674 foreach ($membres as &$id)
679 $db = DB
::getInstance();
680 return $db->simpleUpdate('membres',
681 ['id_categorie' => (int)$id_cat],
682 'id IN ('.implode(',', $membres).')'
686 static protected function _deleteMembres($membres)
688 foreach ($membres as &$id)
693 $membres = implode(',', $membres);
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.');');
702 public function sendMessageToCategory($dest, $sujet, $message, $subscribed_only = false)
704 $config = Config
::getInstance();
707 'From' => '"'.$config->get('nom_asso').'" <'.$config->get('email_asso').'>',
709 $message .= "\n\n--\n".$config->get('nom_asso')."\n".$config->get('site_asso');
712 $where = 'id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)';
714 $where = 'id_categorie = '.(int)$dest;
716 if ($subscribed_only)
718 $where .= ' AND lettre_infos = 1';
721 $db = DB
::getInstance();
722 $res = $db->query('SELECT email FROM membres WHERE LENGTH(email) > 0 AND '.$where.' ORDER BY id;');
724 $sujet = '['.$config->get('nom_asso').'] '.$sujet;
726 while ($row = $res->fetchArray(SQLITE3_ASSOC
))
728 utils
::mail($row['email'], $sujet, $message, $headers);
734 public function searchSQL($query)
736 $db = DB
::getInstance();
738 $st = $db->prepare($query);
740 if (!$st->readOnly())
742 throw new UserException('Seules les requêtes en lecture sont autorisées.');
745 if (!preg_match('/LIMIT\s+/', $query))
747 $query = preg_replace('/;?\s*$/', '', $query);
748 $query .= ' LIMIT 100';
751 if (!preg_match('/FROM\s+membres(?:\s+|$|;)/i', $query))
753 throw new UserException('Seules les requêtes sur la table membres sont autorisées.');
756 if (preg_match('/;\s*(.+?)$/', $query))
758 throw new UserException('Une seule requête peut être envoyée en même temps.');
761 $st = $db->prepare($query);
763 $res = $st->execute();
766 while ($row = $res->fetchArray(SQLITE3_ASSOC
))
768 if (array_key_exists('passe', $row))
770 unset($row['passe']);
779 public function schemaSQL()
781 $db = DB
::getInstance();
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\';'),