Merge branch '10400' into module_i18n

This commit is contained in:
Fred Tempez 2020-11-14 16:09:57 +01:00
commit 3c4e1f1294
38 changed files with 1673 additions and 461 deletions

View File

@ -1,5 +1,21 @@
# Changelog
## version 10.4.00
- Modifications :
- Captcha arithmétique, activation recommandée dans la configuration.
- Module User
- Pour les articles de blog et de news, choix de la signature, nom+prenom ; nom+prenom ; id ; pseudo
- Importation d'un liste d'utilisateur dans un fichier plat (CSV).
- Module Blog :
- Texte du commentaire enrichi.
- Nombre maximal de caractère par commentaire.
- Gestion des commentaires article par article.
- Suppression des commentaires en masse.
- Limiter l'édition des articles et des commentaires à l'id de l'éditeur
- Approbation des commentaires
- Gestion des thèmes :
- Bouton de réinitialisation avec confirmation
## version 10.3.06
- Correction :
- Edition de page avec module, le changement de mise en page désactive le bouton d'option du module.
@ -40,11 +56,13 @@
- Barre de membre déplacée à droite de la barre de menu.
## version 10.3.02
- Correction :
- Corrections :
- Icône de pied de page github manquante.
- Mauvaise redirection après changement de mot de passe d'un membre.
- Modifications :
- Nouvelles images de captcha.
- Option de configuration, captcha demandé à la connexion.
- Méthode d'encodage UTF8.
## version 10.3.01

View File

@ -1,10 +1,10 @@
![](https://img.shields.io/github/last-commit/fredtempez/ZwiiCMS/master) ![](https://img.shields.io/github/release-date/fredtempez/ZwiiCMS)
# ZwiiCMS 10.3.05
# ZwiiCMS 10.4.00
Zwii est un CMS sans base de données (flat-file) qui permet de créer et gérer facilement un site web sans aucune connaissance en programmation.
ZwiiCMS a été créé par un développeur de talent, [Rémi Jean](https://remijean.fr/). Il est désormais maintenu par Fred Tempez aidé de la communauté.
ZwiiCMS a été créé par un développeur de talent, [Rémi Jean](https://remijean.fr/). Il est désormais maintenu par Frédéric Tempez.
[Site](http://zwiicms.fr/) - [Forum](http://forum.zwiicms.com/) - [Version initiale](https://github.com/remijean/ZwiiCMS/) - [GitHub](https://github.com/fredtempez/ZwiiCMS)

View File

@ -32,7 +32,7 @@ class template {
);
}
/**
/**
* Crée un champ captcha
* @param string $nameId Nom et id du champ
* @param array $attributes Attributs ($key => $value)
@ -47,29 +47,85 @@ class template {
'id' => $nameId,
'name' => $nameId,
'value' => '',
'limit' => false
'limit' => false // captcha simple
], $attributes);
// Génère deux nombres pour le captcha
$numbers = array(0,1,2,3,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,20);
$letters = array('u','t','s','r','q','p','o','n','m','l','k','j','i','h','g','f','e','d','c','b','a');
$limit = $attributes['limit'] ? count($letters)-1 : 10 ;
mt_srand((float) microtime()*1000000);
$firstNumber = mt_rand ( 0 , $limit );
$secondNumber = mt_rand ( 0 , $limit );
$result = $firstNumber + $secondNumber;
// Captcha quatre opérations
// Limite addition et soustraction selon le type de captcha
$numbers = [0,1,2,3,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,20];
$letters = ['u','t','s','r','q','p','o','n','m','l','k','j','i','h','g','f','e','d','c','b','a'];
$limit = $attributes['limit'] ? count($letters)-1 : 10;
// Tirage de l'opération
mt_srand((float) microtime()*1000000);
// Captcha simple limité à l'addition
$operator = $attributes['limit'] ? mt_rand (1, 4) : 1;
// Limite si multiplication ou division
if ($operator > 2) {
$limit = 10;
}
// Tirage des nombres
mt_srand((float) microtime()*1000000);
$firstNumber = mt_rand (1, $limit);
mt_srand((float) microtime()*1000000);
$secondNumber = mt_rand (1, $limit);
// Permutation si addition ou soustraction
if (($operator < 3) and ($firstNumber < $secondNumber)) {
$temp = $firstNumber;
$firstNumber = $secondNumber;
$secondNumber = $temp;
}
// Icône de l'opérateur et calcul du résultat
switch ($operator) {
case 1:
$operator = template::ico('plus');
$result = $firstNumber + $secondNumber;
break;
case 2:
$operator = template::ico('minus');
$result = $firstNumber - $secondNumber;
break;
case 3:
$operator = template::ico('cancel');
$result = $firstNumber * $secondNumber;
break;
case 4:
$operator = template::ico('divide');
$limit2 = [10, 10, 6, 5, 4, 3, 2, 2, 2, 2];
for ($i = 1; $i <= $firstNumber; $i++) {
$limit = $limit2[$i-1];
}
mt_srand((float) microtime()*1000000);
$secondNumber = mt_rand(1, $limit);
$firstNumber = $firstNumber * $secondNumber;
$result = $firstNumber / $secondNumber;
break;
}
// Hashage du résultat
$result = password_hash($result, PASSWORD_BCRYPT);
// Codage des valeurs de l'opération
$firstLetter = uniqid();
$secondLetter = uniqid();
// Masquage image source
// Masquage image source pour éviter un décodage
copy ('core/vendor/zwiico/png/'.$letters[$firstNumber] . '.png', 'site/tmp/' . $firstLetter . '.png');
copy ('core/vendor/zwiico/png/'.$letters[$secondNumber] . '.png', 'site/tmp/' . $secondLetter . '.png');
// Début du wrapper
$html = '<div class="captcha" id="' . $attributes['id'] . 'Wrapper" class="inputWrapper ' . $attributes['classWrapper'] . '">';
// Label
$html .= self::label($attributes['id'],
'<img src="' . helper::baseUrl(false) . 'site/tmp/' . $firstLetter . '.png" />' . template::ico('plus') . '<img class="captchaNumber" src="' . helper::baseUrl(false) . 'site/tmp/' . $secondLetter . '.png" /> en chiffres ?', [
'<img src="' . helper::baseUrl(false) . 'site/tmp/' . $firstLetter . '.png" />&nbsp;<strong>' . $operator . '</strong>&nbsp;<img class="captchaNumber" src="' . helper::baseUrl(false) . 'site/tmp/' . $secondLetter . '.png" /> en chiffres ?', [
'help' => $attributes['help']
]);
// Notice
$notice = '';
if(array_key_exists($attributes['id'], common::$inputNotices)) {
@ -77,29 +133,22 @@ class template {
$attributes['class'] .= ' notice';
}
$html .= self::notice($attributes['id'], $notice);
// captcha
$html .= sprintf(
'<input type="text" %s>',
helper::sprintAttributes($attributes)
);
// Champ résultat caché
// Champ résultat codé
$html .= self::hidden($attributes['id'] . 'Result', [
'value' => $result,
'before' => false
]);
// Champs cachés contenant les nombres
/*
$html .= self::hidden($attributes['id'] . 'FirstNumber', [
'value' => $firstNumber,
'before' => false
]);
$html .= self::hidden($attributes['id'] . 'SecondNumber', [
'value' => $secondNumber,
'before' => false
]);
*/
// Fin du wrapper
$html .= '</div>';
// Retourne le html
return $html;
}

View File

@ -25,6 +25,10 @@ class common {
const GROUP_MEMBER = 1;
const GROUP_MODERATOR = 2;
const GROUP_ADMIN = 3;
const SIGNATURE_ID = 1;
const SIGNATURE_PSEUDO = 2;
const SIGNATURE_FIRSTLASTNAME = 3;
const SIGNATURE_LASTFIRSTNAME = 4;
// Dossier de travail
const BACKUP_DIR = 'site/backup/';
const DATA_DIR = 'site/data/';
@ -39,7 +43,7 @@ class common {
const ACCESS_TIMER = 1800;
// Numéro de version
const ZWII_VERSION = '10.3.06';
const ZWII_VERSION = '10.4.00';
const ZWII_UPDATE_CHANNEL = "v10";
public static $actions = [];
@ -53,15 +57,6 @@ class common {
'user',
'translate'
];
public static $dataStage = [
'config',
'core',
'module',
'page',
'user',
'theme',
'admin'
];
public static $accessList = [
'user',
'theme',
@ -148,9 +143,24 @@ class common {
private $url = '';
// Données de site
private $user = [];
private $core = [];
private $config = [];
private $page = [];
private $module = [];
// Descripteur de données Entrées / Sorties
// Liste ici tous les fichiers de données
private $dataFiles = [
'page' => '',
'module' => '',
'core' => '',
'config' => '',
'page' => '',
'user' => '',
'theme' => '',
'admin' => '',
'blacklist' => ''
];
/**
* Constructeur commun
@ -164,20 +174,32 @@ class common {
$this->input['_COOKIE'] = $_COOKIE;
}
// Instanciation de la classe des entrées / sorties
// Récupére les descripteurs
foreach ($this->dataFiles as $keys => $value) {
// Constructeur JsonDB
$this->dataFiles[$keys] = new \Prowebcraft\JsonDb([
'name' => $keys . '.json',
'dir' => $this->dirData ($keys,'fr')
]);;
}
// Import version 9
if (file_exists(self::DATA_DIR . 'core.json') === true &&
$this->getData(['core','dataVersion']) < 10000) {
$keepUsers = isset($_SESSION['KEEP_USERS']) ? $_SESSION['KEEP_USERS'] : false;
$this->importData($keepUsers);
unset ($_SESSION['KEEP_USERS']);
// Réinstaller htaccess
copy('core/module/install/ressource/.htaccess', self::DATA_DIR . '.htaccess');
common::$importNotices [] = "Importation réalisée avec succès" ;
//echo '<script>window.location.replace("' . helper::baseUrl() . $this->getData(['config','homePageId']) . '")</script>';
$this->getData(['core','dataVersion']) < 10000) {
$keepUsers = isset($_SESSION['KEEP_USERS']) ? $_SESSION['KEEP_USERS'] : false;
$this->importData($keepUsers);
unset ($_SESSION['KEEP_USERS']);
// Réinstaller htaccess
copy('core/module/install/ressource/.htaccess', self::DATA_DIR . '.htaccess');
common::$importNotices [] = "Importation réalisée avec succès" ;
//echo '<script>window.location.replace("' . helper::baseUrl() . $this->getData(['config','homePageId']) . '")</script>';
}
// Installation fraîche, initialisation des modules manquants
// La langue d'installation par défaut est fr
foreach (self::$dataStage as $stageId) {
foreach ($this->dataFiles as $stageId => $item) {
$folder = $this->dirData ($stageId, 'fr');
if (file_exists($folder . $stageId .'.json') === false) {
$this->initData($stageId,'fr');
@ -194,6 +216,7 @@ class common {
$this->page = $this->getCache('page');
$this->module = $this->getCache('module');
// Auto traduction
if ( $this->getData(['translate','active'])) {
// Lire la langue du navigateur
$lan = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
@ -334,13 +357,9 @@ class common {
* @param array $keys Clé(s) des données
*/
public function deleteData($keys) {
//Retourne une chaine contenant le dossier à créer
$folder = $this->dirData ($keys[0],'fr');
// Constructeur JsonDB
$db = new \Prowebcraft\JsonDb([
'name' => $keys[0] . '.json',
'dir' => $folder
]);
// Descripteur
$db = $this->dataFiles[$keys[0]];
// Aiguillage
switch(count($keys)) {
case 1:
$db->delete($keys[0]);
@ -381,38 +400,10 @@ class common {
public function getData($keys = []) {
if (count($keys) >= 1) {
/**
* Lecture dans le cache, page et module
*/
if ($keys[0] === 'page' ||
$keys[0] === 'module' ) {
// Décent dans les niveaux de la variable $data
$data = array_merge ($this->page , $this->module);
foreach($keys as $key) {
// Si aucune donnée n'existe retourne null
if(isset($data[$key]) === false) {
return null;
}
// Sinon décent dans les niveaux
else {
$data = $data[$key];
}
}
// Retourne les données
return $data;
}
/**
* Lecture directe
*/
//Retourne une chaine contenant le dossier à créer
$folder = $this->dirData ($keys[0],'fr');
// Constructeur JsonDB
$db = new \Prowebcraft\JsonDb([
'name' => $keys[0] . '.json',
'dir' => $folder
]);
$db = $this->dataFiles[$keys[0]];
switch(count($keys)) {
case 1:
$tempData = $db->get($keys[0]);
@ -440,22 +431,6 @@ class common {
}
}
/**
* Lecture des fichiers de données de page et mise ne cache
* @param @return string données des pages
*/
public function getCache($data) {
$folder = $this->dirData ($data,'fr');
// Constructeur JsonDB
$db = new \Prowebcraft\JsonDb([
'name' => $data . '.json',
'dir' => $folder
]);
$tempData = $db->get($data);
return [$data => $tempData];
}
/*
* Dummy function
* Compatibilité des modules avec v8 et v9
@ -970,14 +945,10 @@ class common {
return false;
}
//Retourne une chaine contenant le dossier à créer
$folder = $this->dirData ($keys[0],'fr');
// Constructeur JsonDB
$db = new \Prowebcraft\JsonDb([
'name' => $keys[0] . '.json',
'dir' => $folder
]);
// Descripteur
$db = $this->dataFiles[$keys[0]];
// Aiguillage
switch(count($keys)) {
case 2:
$db->set($keys[0],$keys[1]);
@ -1022,12 +993,7 @@ class common {
if (!file_exists(self::DATA_DIR . '/' . $lang)) {
mkdir (self::DATA_DIR . '/' . $lang);
}
$folder = $this->dirData ($module,$lang);
// Constructeur JsonDB
$db = new \Prowebcraft\JsonDb([
'name' => $module . '.json',
'dir' => $folder
]);
$db = $this->dataFiles[$module];
if ($sampleSite === true) {
$db->set($module,init::$siteData[$module]);
} else {
@ -1463,6 +1429,51 @@ class common {
}
$this->setData(['core', 'dataVersion', 10304]);
}
// Version 10.4.00
if ($this->getData(['core', 'dataVersion']) < 10400) {
// Ajouter le prénom comme pseudo et le pseudo comme signature
foreach($this->getData(['user']) as $userId => $userIds){
$this->setData(['user',$userId,'pseudo',$this->getData(['user',$userId,'firstname'])]);
$this->setData(['user',$userId,'signature',2]);
}
// Ajouter les champs de blog v3
// Liste des pages dans pageList
$pageList = array();
foreach ($this->getHierarchy(null,null,null) as $parentKey=>$parentValue) {
$pageList [] = $parentKey;
foreach ($parentValue as $childKey) {
$pageList [] = $childKey;
}
}
// Parcourir pageList et rechercher les modules de blog
foreach ($pageList as $parentKey => $parent) {
//La page a une galerie
if ($this->getData(['page',$parent,'moduleId']) === 'blog' ) {
$articleIds = array_keys(helper::arrayCollumn($this->getData(['module',$parent]), 'publishedOn', 'SORT_DESC'));
foreach ($articleIds as $key => $article) {
// Droits les deux groupes
$this->setData(['module', $parent, $article,'editConsent', 3]);
// Limite de taille 500
$this->setData(['module', $parent, $article,'commentMaxlength', '500']);
// Pas d'approbation des commentaires
$this->setData(['module', $parent, $article,'commentApproved', false ]);
// pas de notification
$this->setData(['module', $parent, $article,'commentNotification', false ]);
// groupe de notification
$this->setData(['module', $parent, $article,'commentGroupNotification', 3 ]);
}
// Traitement des commentaires
if ( is_array($this->getData(['module', $parent, $article,'comment'])) ) {
foreach($this->getData(['module', $parent, $article,'comment']) as $commentId => $comment) {
// Approbation
$this->setData(['module', $parent, $article,'comment', $commentId, 'approval', true ]);
}
}
}
}
$this->setData(['core', 'dataVersion', 10400]);
}
}
}
@ -1566,7 +1577,7 @@ class core extends common {
//$css .= '.button.buttonGrey,.button.buttonGrey:hover{color:' . $this->getData(['theme', 'text', 'textColor']) . '}';
$css .= '.container{max-width:' . $this->getData(['theme', 'site', 'width']) . '}';
$margin = $this->getData(['theme', 'site', 'margin']) ? '0' : '20px';
$css .= $this->getData(['theme', 'site', 'width']) === '100%' ? '#site.light{margin:150px auto !important;}#site{margin:0 auto !important;} body{margin:0 auto !important;} #bar{margin:0 auto !important;} body > header{margin:0 auto !important;} body > nav {margin: 0 auto !important;} body > footer {margin:0 auto !important;}': "#site.light{margin: 100px auto !important;}#site{margin: " . $margin . " auto !important;} body{margin:0px 10px;} #bar{margin: 0 -10px;} body > header{margin: 0 -10px;} body > nav {margin: 0 -10px;} body > footer {margin: 0 -10px;} ";
$css .= $this->getData(['theme', 'site', 'width']) === '100%' ? '#site.light{margin:5% auto !important;}#site{margin:0 auto !important;} body{margin:0 auto !important;} #bar{margin:0 auto !important;} body > header{margin:0 auto !important;} body > nav {margin: 0 auto !important;} body > footer {margin:0 auto !important;}': "#site.light{margin: 5% auto !important;}#site{margin: " . $margin . " auto !important;} body{margin:0px 10px;} #bar{margin: 0 -10px;} body > header{margin: 0 -10px;} body > nav {margin: 0 -10px;} body > footer {margin: 0 -10px;} ";
$css .= $this->getData(['theme', 'site', 'width']) === '750px' ? '.button, button{font-size:0.8em;}' : '';
$css .= '#site{background-color:' . $this->getData(['theme', 'site', 'backgroundColor']) . ';border-radius:' . $this->getData(['theme', 'site', 'radius']) . ';box-shadow:' . $this->getData(['theme', 'site', 'shadow']) . ' #212223;}';
$css .= '.editorWysiwyg {background-color:' . $this->getData(['theme', 'site', 'backgroundColor']) . ';}';
@ -1700,7 +1711,7 @@ class core extends common {
$colors = helper::colorVariants($this->getData(['admin','backgroundColorButtonRed']));
$css .= '.button.buttonRed {background-color: ' . $colors['normal'] . ';color: ' . $colors['text'] . ';}.button.buttonRed:hover {background-color:' . $colors['darken'] . ';color:' . $colors['text'] . ';}.button.buttonRed:active {background-color:' . $colors['veryDarken'] . ';color:' . $colors['text'] . ';}';
$colors = helper::colorVariants($this->getData(['admin','backgroundColorButtonGreen']));
$css .= 'button[type=submit] {background-color: ' . $colors['normal'] . ';color: ' . $colors['text'] . ';}button[type=submit]:hover {background-color: ' . $colors['darken'] . ';color: ' . $colors['text'] .';}button[type=submit]:active {background-color: ' . $colors['darken'] . ';color: ' .$colors['text'] .';}';
$css .= '.button.buttonGreen, button[type=submit] {background-color: ' . $colors['normal'] . ';color: ' . $colors['text'] . ';}.button.buttonGreen:hover, button[type=submit]:hover {background-color: ' . $colors['darken'] . ';color: ' . $colors['text'] .';}.button.buttonGreen:active, button[type=submit]:active {background-color: ' . $colors['darken'] . ';color: ' .$colors['text'] .';}';
$colors = helper::colorVariants($this->getData(['admin','backgroundBlockColor']));
$css .= '.block {border: 1px solid ' . $this->getData(['admin','borderBlockColor']) . ';}.block h4 {background-color: ' . $colors['normal'] . ';color:' . $colors['text'] . ';}';
$css .= 'table tr,input[type=email],input[type=text],input[type=password],select:not(#barSelectPage),textarea:not(.editorWysiwyg),.inputFile{background-color: ' . $colors['normal'] . ';color:' . $colors['text'] . ';border: 1px solid ' . $this->getData(['admin','borderBlockColor']) . ';}';
@ -2868,4 +2879,4 @@ class layout extends common {
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@
<?php echo template::textarea('configMetaDescription', [
'label' => 'Description du site',
'value' => $this->getData(['config', 'metaDescription']),
'help' => 'La description participe au référencement, n\'oubliez pas de personnaliser la description de chaque page sans un copié collé.'
'help' => 'La description d\'une page participe à son référencement, chaque page doit disposer d\'une description différente.'
]); ?>
</div>
</div>
@ -102,7 +102,7 @@
<div class="col4 verticalAlignBottom">
<?php echo template::checkbox('configCaptchaStrong', true, 'Captcha renforcé', [
'checked' => $this->getData(['config','captchaStrong']),
'help' => 'Option recommandée pour sécuriser la connexion. S\'applique à tous les captchas du site.'
'help' => 'Option recommandée pour sécuriser la connexion. S\'applique à tous les captchas du site. Le captcha simple se limite à une addition de nombres de 0 à 10. Le captcha renforcé utilise quatre opérations de nombres de 0 à 20.'
]); ?>
</div>
</div>

View File

@ -61,6 +61,8 @@ class install extends common {
'forgot' => 0,
'group' => self::GROUP_ADMIN,
'lastname' => $userLastname,
'pseudo' => 'Admin',
'signature' => 1,
'mail' => $userMail,
'password' => $this->getInput('installPassword', helper::FILTER_PASSWORD, true)
]

View File

@ -649,13 +649,13 @@ class init extends common {
'module' => [
'blog' => [
'mon-premier-article' => [
'closeComment' => false,
'comment' => [
'58e11d09e5aff' => [
'author' => 'Rémi',
'content' => 'Article bien rédigé et très pertinent, bravo !',
'createdOn' => 1421748000,
'userId' => ''
'userId' => '',
'approval' => true
]
],
'content' => '<p>Et eodem impetu Domitianum praecipitem per scalas itidem funibus constrinxerunt, eosque coniunctos per ampla spatia civitatis acri raptavere discursu. iamque artuum et membrorum divulsa conpage superscandentes corpora mortuorum ad ultimam truncata deformitatem velut exsaturati mox abiecerunt in flumen.</p><p>Ex his quidam aeternitati se commendari posse per statuas aestimantes eas ardenter adfectant quasi plus praemii de figmentis aereis sensu carentibus adepturi, quam ex conscientia honeste recteque factorum, easque auro curant inbracteari, quod Acilio Glabrioni delatum est primo, cum consiliis armisque regem superasset Antiochum. quam autem sit pulchrum exigua haec spernentem et minima ad ascensus verae gloriae tendere longos et arduos, ut memorat vates Ascraeus, Censorius Cato monstravit. qui interrogatus quam ob rem inter multos... statuam non haberet malo inquit ambigere bonos quam ob rem id non meruerim, quam quod est gravius cur inpetraverim mussitare.</p><p>Latius iam disseminata licentia onerosus bonis omnibus Caesar nullum post haec adhibens modum orientis latera cuncta vexabat nec honoratis parcens nec urbium primatibus nec plebeiis.</p>',
@ -665,10 +665,15 @@ class init extends common {
'publishedOn' => 1548790902,
'state' => true,
'title' => 'Mon premier article',
'userId' => '' // Géré au moment de l'installation
'userId' => '', // Géré au moment de l'installation
'editConsent' => 'all',
'commentMaxlength' => '500',
'commentApproved' => false,
'commentClose' => false,
'commentNotification' => false,
'commentGroupNotification' => 3
],
'mon-deuxieme-article' => [
'closeComment' => false,
'comment' => [],
'content' => '<p>Et prima post Osdroenam quam, ut dictum est, ab hac descriptione discrevimus, Commagena, nunc Euphratensis, clementer adsurgit, Hierapoli, vetere Nino et Samosata civitatibus amplis inlustris.</p><p>Ob haec et huius modi multa, quae cernebantur in paucis, omnibus timeri sunt coepta. et ne tot malis dissimulatis paulatimque serpentibus acervi crescerent aerumnarum, nobilitatis decreto legati mittuntur: Praetextatus ex urbi praefecto et ex vicario Venustus et ex consulari Minervius oraturi, ne delictis supplicia sint grandiora, neve senator quisquam inusitato et inlicito more tormentis exponeretur.</p><p>Sed ut tum ad senem senex de senectute, sic hoc libro ad amicum amicissimus scripsi de amicitia. Tum est Cato locutus, quo erat nemo fere senior temporibus illis, nemo prudentior; nunc Laelius et sapiens (sic enim est habitus) et amicitiae gloria excellens de amicitia loquetur. Tu velim a me animum parumper avertas, Laelium loqui ipsum putes. C. Fannius et Q. Mucius ad socerum veniunt post mortem Africani; ab his sermo oritur, respondet Laelius, cuius tota disputatio est de amicitia, quam legens te ipse cognosces.</p>',
'picture' => 'galerie/landscape/desert.jpg',
@ -677,10 +682,15 @@ class init extends common {
'publishedOn' => 1550432502,
'state' => true,
'title' => 'Mon deuxième article',
'userId' => '' // Géré au moment de l'installation
'userId' => '', // Géré au moment de l'installation
'editConsent' => 'all',
'commentMaxlength' => '500',
'commentApproved' => false,
'commentClose' => false,
'commentNotification' => false,
'commentGroupNotification' => 3
],
'mon-troisieme-article' => [
'closeComment' => true,
'comment' => [],
'content' => '<p>Rogatus ad ultimum admissusque in consistorium ambage nulla praegressa inconsiderate et leviter proficiscere inquit ut praeceptum est, Caesar sciens quod si cessaveris, et tuas et palatii tui auferri iubebo prope diem annonas. hocque solo contumaciter dicto subiratus abscessit nec in conspectum eius postea venit saepius arcessitus.</p><p>Proinde concepta rabie saeviore, quam desperatio incendebat et fames, amplificatis viribus ardore incohibili in excidium urbium matris Seleuciae efferebantur, quam comes tuebatur Castricius tresque legiones bellicis sudoribus induratae.</p><p>Inter has ruinarum varietates a Nisibi quam tuebatur accitus Vrsicinus, cui nos obsecuturos iunxerat imperiale praeceptum, dispicere litis exitialis certamina cogebatur abnuens et reclamans, adulatorum oblatrantibus turmis, bellicosus sane milesque semper et militum ductor sed forensibus iurgiis longe discretus, qui metu sui discriminis anxius cum accusatores quaesitoresque subditivos sibi consociatos ex isdem foveis cerneret emergentes, quae clam palamve agitabantur, occultis Constantium litteris edocebat inplorans subsidia, quorum metu tumor notissimus Caesaris exhalaret.</p>',
'picture' => 'galerie/landscape/iceberg.jpg',
@ -689,7 +699,13 @@ class init extends common {
'publishedOn' => 1550864502,
'state' => true,
'title' => 'Mon troisième article',
'userId' => '' // Géré au moment de l'installation
'userId' => '', // Géré au moment de l'installation
'editConsent' => 'all',
'commentMaxlength' => '500',
'commentApproved' => false,
'commentClose' => true,
'commentNotification' => false,
'commentGroupNotification' => 3
]
],
'galeries' => [

View File

@ -24,7 +24,6 @@ class theme extends common {
'index' => self::GROUP_ADMIN,
'menu' => self::GROUP_ADMIN,
'reset' => self::GROUP_ADMIN,
'resetAdmin' => self::GROUP_ADMIN,
'site' => self::GROUP_ADMIN,
'admin' => self::GROUP_ADMIN,
'manage' => self::GROUP_ADMIN,
@ -535,29 +534,32 @@ class theme extends common {
*/
public function reset() {
// Supprime le fichier de personnalisation avancée
unlink(self::DATA_DIR.'custom.css');
$redirect ='';
switch ($this->getUrl(2)) {
case 'admin':
$this->initData('admin');
$redirect = helper::baseUrl() . 'theme/admin';
break;
case 'manage':
$this->initData('theme');
$redirect = helper::baseUrl() . 'theme/manage';
break;
case 'custom':
unlink(self::DATA_DIR.'custom.css');
$redirect = helper::baseUrl() . 'theme/advanced';
break;
default :
$redirect = helper::baseUrl() . 'theme';
}
// Valeurs en sortie
$this->addOutput([
'notification' => 'Personnalisation avancée réinitialisée',
'redirect' => helper::baseUrl() . 'theme/advanced',
'notification' => 'Réinitialisation effectuée',
'redirect' => $redirect,
'state' => true
]);
}
/**
* Réinitialisation de la personnalisation avancée
*/
public function resetAdmin() {
// Supprime le fichier de personnalisation avancée
//unlink(self::DATA_DIR.'admin.json');
$this->initData('admin');
// Valeurs en sortie
$this->addOutput([
'notification' => 'Thème réinitialisé',
'redirect' => helper::baseUrl() . 'theme/admin',
'state' => true
]);
}
/**
* Options du site
@ -635,7 +637,7 @@ class theme extends common {
) {
$modele = 'admin';
}
if (!empty($modele)
if (!empty($modele)
) {
// traiter l'archive
$success = $zip->extractTo('.');

View File

@ -32,7 +32,7 @@ $("input, select").on("change", function() {
var colors = core.colorVariants($("#adminColorRed").val());
css += ".button.buttonRed {background-color: " + colors.normal + ";color:" + colors.text + ";}.button.buttonRed:hover {background-color:" + colors.darken + ";color:" + colors.text + "}.button.buttonRed:active {background-color:" + colors.veryDarken + ";color:" + colors.text + "}";
var colors = core.colorVariants($("#adminColorGreen").val());
css += "button[type=submit] {background-color: " + colors.normal + ";color: " + ";color:" + colors.text + "}button[type=submit]:hover {background-color: " + colors.darken + ";color:" + colors.text + ";}button[type=submit]:active {background-color:" + colors.veryDarken + ";color:" + colors.text + "}";
css += ".button.buttonGreen, button[type=submit] {background-color: " + colors.normal + ";color: " + ";color:" + colors.text + "}.button.buttonGreen:hover, button[type=submit]:hover {background-color: " + colors.darken + ";color:" + colors.text + ";}.button.buttonGreen:active, button[type=submit]:active {background-color:" + colors.veryDarken + ";color:" + colors.text + "}";
var colors = core.colorVariants($("#adminBackGroundBlockColor").val());
css += ".block {border: 1px solid " + $("#adminBorderBlockColor").val() + ";}.block h4 {background-color: " + colors.normal + ";color:" + colors.text + ";}";
css += "input[type=email],input[type=text],input[type=password],select:not(#barSelectPage),textarea:not(.editorWysiwyg),.inputFile{background-color: " + colors.normal + ";color:" + colors.text + ";border: 1px solid " + $("#adminBorderBlockColor").val() + ";}";
@ -46,3 +46,13 @@ $("input, select").on("change", function() {
.appendTo("head");
});
/**
* Confirmation de réinitialisation
*/
$("#configAdminReset").on("click", function() {
var _this = $(this);
return core.confirm("Êtes-vous sûr de vouloir réinitialiser à son état d'origine le thème de l\'administration ?", function() {
$(location).attr("href", _this.attr("href"));
});
});

View File

@ -16,7 +16,7 @@
<div class="col2 offset">
<?php echo template::button('configAdminReset', [
'class' => 'buttonRed',
'href' => helper::baseUrl() . 'theme/resetAdmin',
'href' => helper::baseUrl() . 'theme/reset/admin',
'value' => 'Réinitialiser',
'ico' => 'cancel'
]); ?>

View File

@ -10,7 +10,7 @@
</div>
<div class="col2 offset6">
<?php echo template::button('themeAdvancedReset', [
'href' => helper::baseUrl() . 'theme/reset',
'href' => helper::baseUrl() . 'theme/reset/custom',
'class' => 'buttonRed',
'ico' => 'cancel',
'value' => 'Réinitialiser'

View File

@ -0,0 +1,21 @@
/**
* This file is part of Zwii.
*
* For full copyright and license information, please see the LICENSE
* file that was distributed with this source code.
*
* @author Rémi Jean <remi.jean@outlook.com>
* @copyright Copyright (C) 2008-2018, Rémi Jean
* @license GNU General Public License, version 3
* @link http://zwiicms.fr/
*/
/**
* Confirmation de réinitialisation
*/
$("#configManageReset").on("click", function() {
var _this = $(this);
return core.confirm("Êtes-vous sûr de vouloir réinitialiser à son état d'origine le thème du site ?", function() {
$(location).attr("href", _this.attr("href"));
});
});

View File

@ -8,53 +8,59 @@
'value' => 'Retour'
]); ?>
</div>
<div class="col2 offset6">
<?php echo template::button('configManageReset', [
'class' => 'buttonRed',
'href' => helper::baseUrl() . 'theme/reset/manage',
'value' => 'Réinitialiser',
'ico' => 'cancel'
]); ?>
</div>
<div class="col2">
<?php echo template::submit('themeImportSubmit', [
'value' => 'Appliquer'
]); ?>
</div>
</div>
<div class="row">
<div class="col6">
<div class="col12">
<div class="block">
<h4>Installer un thème archivé</h4>
<h4>Installer un thème archivé (site ou administration)</h4>
<div class="row">
<div class="col12">
<div class="col6 offset3">
<?php echo template::file('themeManageImport', [
'label' => 'Archive ZIP :',
'type' => 2
]); ?>
</div>
</div>
<div class="row">
<div class="col5 offset3">
<?php echo template::submit('themeImportSubmit', [
'value' => 'Appliquer'
]); ?>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col6">
<div class="block">
<h4>Sauvegarder le thème</h4>
<h4>Sauvegarde du thème dans les <a href="<?php echo helper::baseUrl(false); ?>core/vendor/filemanager/dialog.php?fldr=theme&type=0&akey=<?php echo md5_file(self::DATA_DIR.'core.json'); ?>" data-lity>fichiers</a> du site</h4>
<div class="row">
<div class="col6">
<?php echo template::button('themeSave', [
'href' => helper::baseUrl() . 'theme/save/theme',
'ico' => 'download-cloud',
'value' => 'Thème site'
'value' => 'Thème du site'
]); ?>
</div>
<div class="col6">
<?php echo template::button('themeSaveAdmin', [
'href' => helper::baseUrl() . 'theme/save/admin',
'ico' => 'download-cloud',
'value' => 'Thème administration'
'value' => 'Thème de l\'administration'
]); ?>
</div>
</div>
<div class="row">
<div class="col12">
<em>Le fichier de sauvegarde est généré dans <a href="<?php echo helper::baseUrl(false); ?>core/vendor/filemanager/dialog.php?fldr=theme&type=0&akey=<?php echo md5_file(self::DATA_DIR.'core.json'); ?>" data-lity>le dossier Thème</a> du gestionnaire de fichiers.</em>
</div>
</div>
</div>
</div>
<div class="col6">
<div class="block">
<h4>Télécharger le thème</h4>
<div class="row">
@ -62,14 +68,14 @@
<?php echo template::button('themeExport', [
'href' => helper::baseUrl() . 'theme/export/theme',
'ico' => 'download',
'value' => 'Thème site'
'value' => 'Thème du site'
]); ?>
</div>
<div class="col6">
<?php echo template::button('themeExport', [
'href' => helper::baseUrl() . 'theme/export/admin',
'ico' => 'download',
'value' => 'Thème administration'
'value' => 'Thème de l\'administration'
]); ?>
</div>
</div>

View File

@ -178,8 +178,7 @@
<div class="row">
<div class="col4">
<?php echo template::checkbox('themeMenuLoginLink', true, 'Lien de connexion', [
'checked' => $this->getData(['theme', 'menu', 'loginLink']),
'help' => 'L\'activation de cette option n\'est pas recommandée'
'checked' => $this->getData(['theme', 'menu', 'loginLink'])
]); ?>
</div>
<div class="col4">

View File

@ -0,0 +1,8 @@
# Bloque l'accès aux données
Order deny,allow
Deny from all
# Sauf l'accès au modèle csv
<Files template.csv>
Order Allow,Deny
Allow from all
</Files>

View File

@ -0,0 +1,2 @@
id;nom;prenom;email;groupe
jbon;Bon;Jean;jean.bon@email.fr;1
1 id nom prenom email groupe
2 jbon Bon Jean jean.bon@email.fr 1

View File

@ -17,20 +17,35 @@ class user extends common {
public static $actions = [
'add' => self::GROUP_ADMIN,
'delete' => self::GROUP_ADMIN,
'edit' => self::GROUP_MEMBER,
'forgot' => self::GROUP_VISITOR,
'import' => self::GROUP_ADMIN,
'index' => self::GROUP_ADMIN,
'login' => self::GROUP_VISITOR,
'edit' => self::GROUP_MEMBER,
'logout' => self::GROUP_MEMBER,
'forgot' => self::GROUP_VISITOR,
'login' => self::GROUP_VISITOR,
'reset' => self::GROUP_VISITOR
];
public static $users = [];
//Paramètres pour choix de la signature
public static $signature = [
self::SIGNATURE_ID => 'Identifiant',
self::SIGNATURE_PSEUDO => 'Pseudo',
self::SIGNATURE_FIRSTLASTNAME => 'Prénom Nom',
self::SIGNATURE_LASTFIRSTNAME => 'Nom Prénom'
];
public static $userId = '';
public static $userLongtime = false;
public static $separators = [
';' => ';',
',' => ',',
':' => ':'
];
/**
* Ajout
*/
@ -63,8 +78,15 @@ class user extends common {
'forgot' => 0,
'group' => $this->getInput('userAddGroup', helper::FILTER_INT, true),
'lastname' => $userLastname,
'pseudo' => $this->getInput('userAddPseudo', helper::FILTER_STRING_SHORT, true),
'signature' => $this->getInput('userAddSignature', helper::FILTER_INT, true),
'mail' => $userMail,
'password' => $this->getInput('userAddPassword', helper::FILTER_PASSWORD, true),
"connectFail" => null,
"connectTimeout" => null,
"accessUrl" => null,
"accessTimer" => null,
"accessCsrf" => null
]
]);
@ -77,7 +99,6 @@ class user extends common {
'Bonjour <strong>' . $userFirstname . ' ' . $userLastname . '</strong>,<br><br>' .
'Un administrateur vous a créé un compte sur le site ' . $this->getData(['config', 'title']) . '. Vous trouverez ci-dessous les détails de votre compte.<br><br>' .
'<strong>Identifiant du compte :</strong> ' . $this->getInput('userAddId') . '<br>' .
'<strong>Mot de passe du compte :</strong> ' . $this->getInput('userAddPassword') . '<br><br>' .
'<small>Nous ne conservons pas les mots de passe, en conséquence nous vous conseillons de conserver ce message tant que vous ne vous êtes pas connecté. Vous pourrez modifier votre mot de passe après votre première connexion.</small>',
null
);
@ -208,15 +229,26 @@ class user extends common {
else {
$newGroup = $this->getData(['user', $this->getUrl(2), 'group']);
}
// Modification de nom Prénom
if($this->getUser('group') === self::GROUP_ADMIN){
$newfirstname = $this->getInput('userEditFirstname', helper::FILTER_STRING_SHORT, true);
$newlastname = $this->getInput('userEditLastname', helper::FILTER_STRING_SHORT, true);
}
else{
$newfirstname = $this->getData(['user', $this->getUrl(2), 'firstname']);
$newlastname = $this->getData(['user', $this->getUrl(2), 'lastname']);
}
// Modifie l'utilisateur
$this->setData([
'user',
$this->getUrl(2),
[
'firstname' => $this->getInput('userEditFirstname', helper::FILTER_STRING_SHORT, true),
'firstname' => $newfirstname,
'forgot' => 0,
'group' => $newGroup,
'lastname' => $this->getInput('userEditLastname', helper::FILTER_STRING_SHORT, true),
'lastname' => $newlastname,
'pseudo' => $this->getInput('userEditPseudo', helper::FILTER_STRING_SHORT, true),
'signature' => $this->getInput('userEditSignature', helper::FILTER_INT, true),
'mail' => $this->getInput('userEditMail', helper::FILTER_MAIL, true),
'password' => $newPassword,
'connectFail' => $this->getData(['user',$this->getUrl(2),'connectFail']),
@ -413,7 +445,8 @@ class user extends common {
// Valeurs en sortie
$this->addOutput([
'notification' => 'Connexion réussie',
'redirect' => helper::baseUrl() . str_replace('_', '/', str_replace('__', '#', $this->getUrl(2))),
'redirect' => helper::baseUrl(),
//'redirect' => helper::baseUrl() . str_replace('_', '/', str_replace('__', '#', $this->getUrl(2))),
'state' => true
]);
}
@ -532,9 +565,137 @@ class user extends common {
}
// Valeurs en sortie
$this->addOutput([
'title' => 'Réinitialisation du mot de passe',
'display' => self::DISPLAY_LAYOUT_LIGHT,
'title' => 'Réinitialisation de votre mot de passe',
'view' => 'reset'
]);
}
}
/**
* Importation CSV d'utilisateurs
*/
public function import() {
// Soumission du formulaire
$notification = '';
$success = true;
if($this->isPost()) {
// Lecture du CSV et construction du tableau
$file = $this->getInput('userImportCSVFile',helper::FILTER_STRING_SHORT, true);
$filePath = self::FILE_DIR . 'source/' . $file;
if ($file AND file_exists($filePath)) {
// Analyse et extraction du CSV
$rows = array_map(function($row) { return str_getcsv($row, $this->getInput('userImportSeparator') ); }, file($filePath));
$header = array_shift($rows);
$csv = array();
foreach($rows as $row) {
$csv[] = array_combine($header, $row);
}
// Traitement des données
foreach($csv as $item ) {
// Données valides
if( array_key_exists('id', $item)
AND array_key_exists('prenom',$item)
AND array_key_exists('nom',$item)
AND array_key_exists('groupe',$item)
AND array_key_exists('email',$item)
AND $item['nom']
AND $item['prenom']
AND $item['id']
AND $item['email']
AND $item['groupe']
) {
// Validation du groupe
$item['groupe'] = (int) $item['groupe'];
$item['groupe'] = ( $item['groupe'] >= self::GROUP_BANNED AND $item['groupe'] <= self::GROUP_ADMIN )
? $item['groupe'] : 1;
// L'utilisateur existe
if ( $this->getData(['user',helper::filter($item['id'] , helper::FILTER_ID)]))
{
// Notification du doublon
$item['notification'] = template::ico('cancel');
// Création du tableau de confirmation
self::$users[] = [
helper::filter($item['id'] , helper::FILTER_ID),
$item['nom'],
$item['prenom'],
self::$groups[$item['groupe']],
$item['prenom'],
$item['email'],
$item['notification']
];
// L'utilisateur n'existe pas
} else {
// Nettoyage de l'identifiant
$userId = helper::filter($item['id'] , helper::FILTER_ID);
// Enregistre le user
$create = $this->setData([
'user',
$userId, [
'firstname' => $item['prenom'],
'forgot' => 0,
'group' => $item['groupe'] ,
'lastname' => $item['nom'],
'mail' => $item['email'],
'pseudo' => $item['prenom'],
'signature' => 1, // Pseudo
'password' => uniqid(), // A modifier à la première connexion
"connectFail" => null,
"connectTimeout" => null,
"accessUrl" => null,
"accessTimer" => null,
"accessCsrf" => null
]]);
// Icône de notification
$item['notification'] = $create ? template::ico('check') : template::ico('cancel');
// Envoi du mail
if ($create
AND $this->getInput('userImportNotification',helper::FILTER_BOOLEAN) === true) {
$sent = $this->sendMail(
$item['email'],
'Compte créé sur ' . $this->getData(['config', 'title']),
'Bonjour <strong>' . $item['prenom'] . ' ' . $item['nom'] . '</strong>,<br><br>' .
'Un administrateur vous a créé un compte sur le site ' . $this->getData(['config', 'title']) . '. Vous trouverez ci-dessous les détails de votre compte.<br><br>' .
'<strong>Identifiant du compte :</strong> ' . $userId . '<br>' .
'<small>Un mot de passe provisoire vous été attribué, à la première connexion cliquez sur Mot de passe Oublié.</small>'
);
if ($sent === true) {
// Mail envoyé changement de l'icône
$item['notification'] = template::ico('mail') ;
}
}
// Création du tableau de confirmation
self::$users[] = [
$userId,
$item['nom'],
$item['prenom'],
self::$groups[$item['groupe']],
$item['prenom'],
$item['email'],
$item['notification']
];
}
}
}
if (empty(self::$users)) {
$notification = 'Rien à importer, erreur de format ou fichier incorrect' ;
$success = false;
} else {
$notification = 'Importation effectuée' ;
$success = true;
}
} else {
$notification = 'Erreur de lecture, vérifiez les permissions';
$success = false;
}
}
// Valeurs en sortie
$this->addOutput([
'title' => 'Importation',
'view' => 'import',
'notification' => $notification,
'state' => $success
]);
}
}

View File

@ -30,6 +30,14 @@
]); ?>
</div>
</div>
<?php echo template::text('userAddPseudo', [
'autocomplete' => 'off',
'label' => 'Pseudo'
]); ?>
<?php echo template::select('userAddSignature', $module::$signature, [
'label' => 'Signature',
'selected' => 1
]); ?>
<?php echo template::mail('userAddMail', [
'autocomplete' => 'off',
'label' => 'Adresse mail'
@ -73,9 +81,9 @@
'label' => 'Confirmation'
]); ?>
<?php echo template::checkbox('userAddSendMail', true,
'Prévenir l\'utilisateur par mail');
?>
'Prévenir l\'utilisateur par mail');
?>
</div>
</div>
</div>
<?php echo template::formClose(); ?>
<?php echo template::formClose(); ?>

View File

@ -1,7 +1,7 @@
<?php echo template::formOpen('userEditForm'); ?>
<div class="row">
<div class="col2">
<?php if($this->getUser('group') === self::GROUP_ADMIN): ?>
<?php if($this->getUser('group') === self::GROUP_ADMIN): ?>
<?php echo template::button('userEditBack', [
'class' => 'buttonGrey',
'href' => helper::baseUrl() . 'user',
@ -29,6 +29,7 @@
<div class="col6">
<?php echo template::text('userEditFirstname', [
'autocomplete' => 'off',
'disabled' => $this->getUser('group') > 2 ? false : true,
'label' => 'Prénom',
'value' => $this->getData(['user', $this->getUrl(2), 'firstname'])
]); ?>
@ -36,11 +37,21 @@
<div class="col6">
<?php echo template::text('userEditLastname', [
'autocomplete' => 'off',
'disabled' => $this->getUser('group') > 2 ? false : true,
'label' => 'Nom',
'value' => $this->getData(['user', $this->getUrl(2), 'lastname'])
]); ?>
</div>
</div>
<?php echo template::text('userEditPseudo', [
'autocomplete' => 'off',
'label' => 'Pseudo',
'value' => $this->getData(['user', $this->getUrl(2), 'pseudo'])
]); ?>
<?php echo template::select('userEditSignature', $module::$signature, [
'label' => 'Signature',
'selected' => $this->getData(['user', $this->getUrl(2), 'signature'])
]); ?>
<?php echo template::mail('userEditMail', [
'autocomplete' => 'off',
'label' => 'Adresse mail',
@ -98,4 +109,4 @@
</div>
</div>
</div>
<?php echo template::formClose(); ?>
<?php echo template::formClose(); ?>

View File

@ -5,7 +5,6 @@
<div class="row">
<div class="col3 offset6">
<?php echo template::button('userForgotBack', [
'class' => 'buttonGrey',
'href' => helper::baseUrl() . 'user/login/' . $this->getUrl(2),
'ico' => 'left',
'value' => 'Retour'

16
core/module/user/view/import/import.css vendored Normal file
View File

@ -0,0 +1,16 @@
/**
* This file is part of Zwii.
*
* For full copyright and license information, please see the LICENSE
* file that was distributed with this source code.
*
* @author Rémi Jean <remi.jean@outlook.com>
* @copyright Copyright (C) 2008-2018, Rémi Jean
* @author Frédéric Tempez <frederic.tempez@outlook.com>
* @copyright Copyright (C) 2018-2020, Frédéric Tempez
* @license GNU General Public License, version 3
* @link http://zwiicms.fr/
*/
@import url("site/data/admin.css");

View File

<
@ -0,0 +1,62 @@
<?php echo template::formOpen('userImportForm'); ?>
<div class="row">
<div class="col2">
<?php echo template::button('userImportBack', [
'class' => 'buttonGrey',
'href' => helper::baseUrl() . 'user',
'ico' => 'left',
'value' => 'Retour'
]); ?>
</div>
<div class="col2 offset8">
<?php echo template::submit('userImportSubmit', [
'value' => 'Importer'
]); ?>
</div>
</div>
<div class="row">
<div class="col12">
<div class="block">
<h4>Importation de fichier plat CSV</h4>
<div class="row">
<div class="col6">
<?php echo template::file('userImportCSVFile', [
'label' => 'Liste d\'utilisateurs :'
]); ?>
</div>
<div class="col2">
<?php echo template::select('userImportSeparator', $module::$separators, [
'label' => 'Séparateur'
]); ?>
</div>
</div>
<div class="row">
<div class="col12">
<?php echo template::checkbox('userImportNotification', true, 'Envoyer un message de confirmation', [
'checked' => false
]); ?>
</div>
</div>
<div class="row">
<div class="col1">
<p>Aide :</p>
</div>
<div class="col11">
<p>Les en-têtes obligatoires sont : id, nom, prenom, email et groupe