1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
|
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
|
CREATE TABLE compta_exercices
-- Exercices
(
id INTEGER PRIMARY KEY,
libelle TEXT NOT NULL,
debut TEXT NOT NULL DEFAULT CURRENT_DATE,
fin TEXT NULL DEFAULT NULL,
clos INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE compta_comptes
-- Plan comptable
(
id TEXT PRIMARY KEY,
parent TEXT NOT NULL DEFAULT 0,
libelle TEXT NOT NULL,
position INTEGER NOT NULL, -- position actif/passif/charge/produit
plan_comptable INTEGER NOT NULL DEFAULT 1 -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur
);
CREATE INDEX compta_comptes_parent ON compta_comptes (parent);
CREATE TABLE compta_comptes_bancaires
-- Comptes bancaires
(
id TEXT PRIMARY KEY,
banque TEXT NOT NULL,
iban TEXT,
bic TEXT,
FOREIGN KEY(id) REFERENCES compta_comptes(id)
);
CREATE TABLE compta_journal
-- Journal des opérations comptables
(
id INTEGER PRIMARY KEY,
libelle TEXT NOT NULL,
remarques TEXT,
numero_piece TEXT, -- N° de pièce comptable
montant REAL,
date TEXT DEFAULT CURRENT_DATE,
moyen_paiement TEXT DEFAULT NULL,
numero_cheque TEXT DEFAULT NULL,
compte_debit INTEGER, -- N° du compte dans le plan
compte_credit INTEGER, -- N° du compte dans le plan
id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
id_auteur INTEGER NOT NULL,
id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
FOREIGN KEY(id_auteur) REFERENCES membres(id),
FOREIGN KEY(id_categorie) REFERENCES compta_categories(id)
);
CREATE INDEX compta_operations_exercice ON compta_journal (id_exercice);
CREATE INDEX compta_operations_date ON compta_journal (date);
CREATE INDEX compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
CREATE INDEX compta_operations_auteur ON compta_journal (id_auteur);
CREATE TABLE compta_moyens_paiement
-- Moyens de paiement
(
code TEXT PRIMARY KEY,
nom TEXT
);
INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue');
INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque');
INSERT INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces');
INSERT INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement');
INSERT INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP');
INSERT INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement');
CREATE TABLE compta_categories
-- Catégories pour simplifier le plan comptable
(
id INTEGER PRIMARY KEY,
type INTEGER DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface)
intitule TEXT NOT NULL,
description TEXT,
compte TEXT NOT NULL, -- Compte affecté par cette catégorie
FOREIGN KEY(compte) REFERENCES compta_comptes(id)
);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
|
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
|
DROP TABLE compta_exercices;
CREATE TABLE compta_exercices
-- Exercices
(
id INTEGER PRIMARY KEY,
libelle TEXT NOT NULL,
debut TEXT NOT NULL DEFAULT CURRENT_DATE,
fin TEXT NULL DEFAULT NULL,
cloture INTEGER NOT NULL DEFAULT 0
);
INSERT INTO compta_exercices (libelle, debut, fin, cloture)
VALUES (
'Premier exercice',
(CASE WHEN
(SELECT strftime('%Y-01-01', date) FROM compta_journal ORDER BY date ASC LIMIT 1)
IS NOT NULL THEN (SELECT strftime('%Y-01-01', date) FROM compta_journal ORDER BY date ASC LIMIT 1)
ELSE strftime('%Y-01-01', 'now') END
),
(CASE WHEN
(SELECT strftime('%Y-12-31', date) FROM compta_journal ORDER BY date DESC LIMIT 1)
IS NOT NULL THEN (SELECT strftime('%Y-12-31', date) FROM compta_journal ORDER BY date DESC LIMIT 1)
ELSE strftime('%Y-12-31', 'now') END
),
0
);
BEGIN;
ALTER TABLE compta_journal RENAME TO old_compta_journal;
DROP INDEX compta_operations_exercice;
DROP INDEX compta_operations_date;
DROP INDEX compta_operations_comptes;
DROP INDEX compta_operations_auteur;
CREATE TABLE compta_journal
-- Journal des opérations comptables
(
id INTEGER PRIMARY KEY,
libelle TEXT NOT NULL,
remarques TEXT,
numero_piece TEXT, -- N° de pièce comptable
montant REAL,
date TEXT DEFAULT CURRENT_DATE,
moyen_paiement TEXT DEFAULT NULL,
numero_cheque TEXT DEFAULT NULL,
compte_debit TEXT, -- N° du compte dans le plan
compte_credit TEXT, -- N° du compte dans le plan
id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
id_auteur INTEGER NULL,
id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
FOREIGN KEY(id_auteur) REFERENCES membres(id),
FOREIGN KEY(id_categorie) REFERENCES compta_categories(id)
);
CREATE INDEX compta_operations_exercice ON compta_journal (id_exercice);
CREATE INDEX compta_operations_date ON compta_journal (date);
CREATE INDEX compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
CREATE INDEX compta_operations_auteur ON compta_journal (id_auteur);
INSERT INTO compta_journal SELECT * FROM old_compta_journal;
UPDATE compta_journal SET id_exercice = 1;
DROP TABLE old_compta_journal;
END;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
|
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
|
CREATE TABLE cotisations
-- Types de cotisations et activités
(
id INTEGER PRIMARY KEY,
id_categorie_compta INTEGER NULL, -- NULL si le type n'est pas associé automatiquement à la compta
intitule TEXT NOT NULL,
description TEXT NULL,
montant REAL NOT NULL,
duree INTEGER NULL, -- En jours
debut TEXT NULL, -- timestamp
fin TEXT NULL,
FOREIGN KEY (id_categorie_compta) REFERENCES compta_categories (id)
);
CREATE TABLE cotisations_membres
-- Enregistrement des cotisations et activités
(
id INTEGER NOT NULL PRIMARY KEY,
id_membre INTEGER NOT NULL REFERENCES membres (id),
id_cotisation INTEGER NOT NULL REFERENCES cotisations (id),
date TEXT NOT NULL DEFAULT CURRENT_DATE
);
CREATE UNIQUE INDEX cm_unique ON cotisations_membres (id_membre, id_cotisation, date);
CREATE TABLE membres_operations
-- Liaision des enregistrement des paiements en compta
(
id_membre INTEGER NOT NULL REFERENCES membres (id),
id_operation INTEGER NOT NULL REFERENCES compta_journal (id),
id_cotisation INTEGER NULL REFERENCES cotisations_membres (id),
PRIMARY KEY (id_membre, id_operation)
);
CREATE TABLE rappels
-- Rappels de devoir renouveller une cotisation
(
id INTEGER PRIMARY KEY,
id_cotisation INTEGER NOT NULL REFERENCES cotisations (id),
delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel
sujet TEXT NOT NULL,
texte TEXT NOT NULL
);
CREATE TABLE rappels_envoyes
-- Enregistrement des rappels envoyés à qui et quand
(
id INTEGER PRIMARY KEY,
id_membre INTEGER NOT NULL REFERENCES membres (id),
id_cotisation INTEGER NOT NULL REFERENCES cotisations (id),
date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre
);
CREATE TABLE plugins
-- Plugins / extensions
(
id TEXT PRIMARY KEY,
officiel INTEGER NOT NULL DEFAULT 0,
nom TEXT NOT NULL,
description TEXT,
auteur TEXT,
url TEXT,
version TEXT NOT NULL,
menu INTEGER NOT NULL DEFAULT 0,
config TEXT
);
-- Mise à jour des catégories
CREATE TABLE membres_categories_tmp
-- Catégories de membres
(
id INTEGER PRIMARY KEY,
nom TEXT,
description TEXT,
droit_wiki INT DEFAULT 1,
droit_membres INT DEFAULT 1,
droit_compta INT DEFAULT 1,
droit_inscription INT DEFAULT 0,
droit_connexion INT DEFAULT 1,
droit_config INT DEFAULT 0,
cacher INT DEFAULT 0,
id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id)
);
-- Remise des anciennes infos
INSERT INTO membres_categories_tmp SELECT id, nom, description, droit_wiki, droit_membres,
droit_compta, droit_inscription, droit_connexion, droit_config, cacher, NULL FROM membres_categories;
-- Suppression de l'ancienne table et renommage de la nouvelle
DROP TABLE membres_categories;
ALTER TABLE membres_categories_tmp RENAME TO membres_categories;
-- Ajout désactivation compte
ALTER TABLE compta_comptes ADD COLUMN desactive INTEGER NOT NULL DEFAULT 0;
PRAGMA foreign_keys = ON;
|
︙ | | |
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
-
|
-- compta_categorie_dons => id_categorie
CREATE TABLE IF NOT EXISTS membres_categories
-- Catégories de membres
(
id INTEGER PRIMARY KEY NOT NULL,
nom TEXT NOT NULL,
description TEXT NULL,
droit_wiki INTEGER NOT NULL DEFAULT 1,
droit_membres INTEGER NOT NULL DEFAULT 1,
droit_compta INTEGER NOT NULL DEFAULT 1,
droit_inscription INTEGER NOT NULL DEFAULT 0,
droit_connexion INTEGER NOT NULL DEFAULT 1,
droit_config INTEGER NOT NULL DEFAULT 0,
|
︙ | | |
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
|
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
|
+
-
+
|
officiel INTEGER NOT NULL DEFAULT 0,
nom TEXT NOT NULL,
description TEXT NULL,
auteur TEXT NULL,
url TEXT NULL,
version TEXT NOT NULL,
menu INTEGER NOT NULL DEFAULT 0,
menu_condition TEXT NULL,
config TEXT NULL
);
CREATE TABLE IF NOT EXISTS plugins_signaux
-- Association entre plugins et signaux (hooks)
(
signal TEXT NOT NULL,
plugin TEXT NOT NULL REFERENCES plugins (id),
callback TEXT NOT NULL,
PRIMARY KEY (signal, plugin)
);
CREATE TABLE IF NOT EXISTS compta_rapprochement
-- Rapprochement entre compta et relevés de comptes
(
id_operation INTEGER NOT NULL PRIMARY KEY REFERENCES compta_journal (id) ON DELETE CASCADE,
date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),
id_auteur INTEGER NULL REFERENCES membres (id)
id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS fichiers
-- Données sur les fichiers
(
id INTEGER NOT NULL PRIMARY KEY,
nom TEXT NOT NULL, -- nom de fichier (par exemple image1234.jpeg)
|
︙ | | |
382
383
384
385
386
387
388
|
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
|
+
+
+
+
+
+
+
+
+
+
+
+
|
CREATE TABLE IF NOT EXISTS fichiers_compta_journal
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
fichier INTEGER NOT NULL REFERENCES fichiers (id),
id INTEGER NOT NULL REFERENCES compta_journal (id),
PRIMARY KEY(fichier, id)
);
CREATE TABLE IF NOT EXISTS recherches
-- Recherches enregistrées
(
id INTEGER NOT NULL PRIMARY KEY,
id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, -- Si non NULL, alors la recherche ne sera visible que par le membre associé
intitule TEXT NOT NULL,
creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(creation) IS NOT NULL AND datetime(creation) = creation),
cible TEXT NOT NULL, -- "membres" ou "compta_journal"
type TEXT NOT NULL, -- "json" ou "sql"
contenu TEXT NOT NULL
);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-
-
-
-
+
+
+
+
-
|
<?php
namespace Garradin\Compta;
use \Garradin\DB;
use \Garradin\Utils;
use \Garradin\UserException;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Config;
use KD2\ODSWriter;
class Import
{
protected $header = [
'Numéro mouvement',
'Date',
'Type de mouvement',
|
︙ | | |
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
-
+
|
'Numéro de chèque',
'Numéro de pièce',
'Remarques'
];
protected function export($exercice)
{
return DB::getInstance()->prepare('SELECT
return DB::getInstance()->iterate('SELECT
journal.id,
strftime(\'%d/%m/%Y\', date) AS date,
(CASE cat.type WHEN 1 THEN \'Recette\' WHEN -1 THEN \'Dépense\' ELSE \'Autre\' END) AS type,
(CASE cat.intitule WHEN NULL THEN \'\' ELSE cat.intitule END) AS cat,
journal.libelle,
montant,
compte_debit,
|
︙ | | |
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
-
+
-
+
-
-
+
-
-
+
-
-
-
-
-
-
+
-
-
-
+
+
-
-
-
+
+
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
|
FROM compta_journal AS journal
LEFT JOIN compta_categories AS cat ON cat.id = journal.id_categorie
LEFT JOIN compta_comptes AS debit ON debit.id = journal.compte_debit
LEFT JOIN compta_comptes AS credit ON credit.id = journal.compte_credit
LEFT JOIN compta_moyens_paiement AS moyen ON moyen.code = journal.moyen_paiement
WHERE id_exercice = '.(int)$exercice.'
ORDER BY journal.date;
')->execute();
');
}
public function toCSV($exercice)
protected function exportName()
{
$res = $this->export($exercice);
return sprintf('Export comptabilité - %s - %s', Config::getInstance()->get('nom_asso'), date('Y-m-d'));
$fp = fopen('php://output', 'w');
}
fputcsv($fp, $this->header);
while ($row = $res->fetchArray(SQLITE3_ASSOC))
{
fputcsv($fp, $row);
}
public function toCSV($exercice)
fclose($fp);
return true;
{
return Utils::toCSV($this->exportName(), $this->export($exercice), $this->header);
}
public function toODS($exercice)
{
$result = $this->export($exercice);
public function toODS($exercice)
{
return Utils::toODS($this->exportName(), $this->export($exercice), $this->header);
$ods = new ODSWriter;
$ods->table_name = 'Journal';
}
$ods->add($this->header);
while ($row = $result->fetchArray(SQLITE3_ASSOC))
{
unset($row->passe);
$ods->add($row);
}
$ods->output();
}
public function fromCSV($path)
{
if (!file_exists($path) || !is_readable($path))
{
throw new \RuntimeException('Fichier inconnu : '.$path);
}
|
︙ | | |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
|
<?php
namespace Garradin;
/**
* Pour procéder à l'installation de l'instance Garradin
* Utile pour automatiser l'installation sans passer par la page d'installation
*/
class Install
{
static public function reset(Membres\Session $session, $password, array $options = [])
{
$config = (object) Config::getInstance()->getConfig();
$user = $session->getUser();
if (!$session->checkPassword($password, $user->passe))
{
throw new UserException('Le mot de passe ne correspond pas.');
}
(new Sauvegarde)->create(date('Y-m-d-His-') . 'avant-remise-a-zero');
DB::getInstance()->close();
Config::deleteInstance();
unlink(DB_FILE);
return self::install($config->nom_asso, $config->adresse_asso, $config->email_asso, 'Bureau', $user->identite, $user->email, $password, $config->site_asso);
}
static public function install($nom_asso, $adresse_asso, $email_asso, $nom_categorie, $nom_membre, $email_membre, $passe_membre, $site_asso = WWW_URL)
{
$db = DB::getInstance(true);
// Taille de la page de DB, on force à 4096 (défaut dans les dernières
// versions de SQLite mais pas les vieilles)
$db->exec('PRAGMA page_size = 4096;');
|
︙ | | |
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
-
+
|
$config->setVersion(garradin_version());
$champs = Membres\Champs::importInstall();
$champs->save(false); // Pas de copie car pas de table membres existante
$config->set('champ_identifiant', 'email');
$config->set('champ_identite', 'nom');
// Création catégories
$cats = new Membres\Categories;
$id = $cats->add([
'nom' => 'Membres actifs',
]);
$config->set('categorie_membres', $id);
|
︙ | | |
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
|
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
+
|
$ex = new Compta\Exercices;
$ex->add([
'libelle' => 'Premier exercice',
'debut' => date('Y-01-01'),
'fin' => date('Y-12-31')
]);
// Ajout d'une recherche avancée en exemple
$query = [
'query' => [[
'operator' => 'AND',
'conditions' => [
[
'column' => 'lettre_infos',
'operator' => '= 1',
'values' => [],
],
],
]],
'order' => 'numero',
'desc' => true,
'limit' => '10000',
];
$recherche = new Recherche;
$recherche->add('Membres inscrits à la lettre d\'information', null, $recherche::TYPE_JSON, 'membres', $query);
return $config->save();
}
static public function checkAndCreateDirectories()
{
// Vérifier que les répertoires vides existent, sinon les créer
$paths = [DATA_ROOT, PLUGINS_ROOT, CACHE_ROOT, CACHE_ROOT . '/static', CACHE_ROOT . '/compiled'];
foreach ($paths as $path)
{
if (!file_exists($path))
{
mkdir($path, 0777, true);
}
Utils::safe_mkdir($path);
if (!is_dir($path))
{
throw new UserException('Le répertoire '.$path.' n\'existe pas ou n\'est pas un répertoire.');
}
if (!is_dir($path))
{
throw new UserException('Le répertoire '.$path.' n\'existe pas ou n\'est pas un répertoire.');
}
// On en profite pour vérifier qu'on peut y lire et écrire
if (!is_writable($path) || !is_readable($path))
{
throw new UserException('Le répertoire '.$path.' n\'est pas accessible en lecture/écriture.');
}
// On en profite pour vérifier qu'on peut y lire et écrire
if (!is_writable($path) || !is_readable($path))
{
throw new UserException('Le répertoire '.$path.' n\'est pas accessible en lecture/écriture.');
}
}
return true;
}
static public function setLocalConfig($key, $value)
{
$path = ROOT . DIRECTORY_SEPARATOR . 'config.local.php';
$new_line = sprintf('const %s = %s;', $key, var_export($value, true));
if (file_exists($path))
{
$config = file_get_contents($path);
$pattern = sprintf('/^.*(?:const\s+%s|define\s*\(.*%1$s).*$/m', $key);
$config = preg_replace($pattern, $new_line, $config, -1, $count);
if (!$count)
{
$config = preg_replace('/\?>.*/s', '', $config);
$config .= PHP_EOL . $new_line . PHP_EOL;
}
|
︙ | | |
︙ | | |
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
|
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
|
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
-
-
+
-
-
+
-
-
+
-
-
-
-
+
-
+
-
-
+
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
|
}
public function getIDWithNumero($numero)
{
return DB::getInstance()->firstColumn('SELECT id FROM membres WHERE numero = ?;', (int) $numero);
}
public function search($field, $query)
public function getSearchHeaderFields(array $result)
{
if (!count($result))
{
return false;
}
$db = DB::getInstance();
$champs = Config::getInstance()->get('champs_membres');
$fields = [];
foreach (reset($result) as $field=>$value)
{
if ($config = $champs->get($field))
{
$fields[$field] = $config;
}
}
return $fields;
}
public function sendMessage(array $recipients, $subject, $message, $send_copy)
{
$config = Config::getInstance();
$champs = $config->get('champs_membres');
foreach ($recipients as $recipient)
if (!$champs->get($field))
{
throw new \UnexpectedValueException($field . ' is not a valid field');
}
Utils::sendEmail(Utils::EMAIL_CONTEXT_BULK, $recipient->email, $subject, $message, $recipient->id);
$champ = $champs->get($field);
}
if ($champ->type == 'multiple')
{
$where = 'WHERE '.$field.' & (1 << '.(int)$query.')';
$order = false;
}
elseif ($champ->type == 'tel')
if ($send_copy)
{
$query = Utils::normalizePhoneNumber($query);
Utils::sendEmail(Utils::EMAIL_CONTEXT_BULK, $config->get('email_asso'), $subject, $message);
$query = preg_replace('!^0+!', '', $query);
}
if ($query == '')
{
return false;
}
return true;
}
$where = sprintf('WHERE %s LIKE %s', $field, $db->quote('%' . $query . '%'));
$order = $field;
}
elseif (!$champs->isText($field))
{
$where = sprintf('WHERE %s = %s', $field, $db->quote($query));
$order = $field;
}
else
{
// Si le champ est de type 'select' (sélecteur à choix unique), ne pas utiliser de LIKE mais valeur exacte
// @link https://fossil.kd2.org/garradin/info/587f730b661a7ce16bad215d4bd02195e754ec57
if ($champ->type != 'select')
{
$query = '%' . $query . '%';
}
public function listAllByCategory($id_categorie)
$where = sprintf('WHERE transliterate_to_ascii(%s) LIKE %s', $field, $db->quote(Utils::transliterateToAscii($query)));
$order = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE', $field);
}
{
$fields = array_keys((array)$champs->getListedFields());
return DB::getInstance()->get('SELECT id, email FROM membres WHERE id_categorie = ?;', (int)$id_categorie);
if (!in_array($field, $fields))
{
$fields[] = $field;
}
if (!in_array('email', $fields))
{
$fields[] = 'email';
}
$query = sprintf('SELECT id, id_categorie, %s, %s AS identite,
strftime(\'%%s\', date_inscription) AS date_inscription
FROM membres %s %s LIMIT 1000;',
implode(', ', $fields),
$config->get('champ_identite'),
$where,
$order ? 'ORDER BY ' . $order : ''
);
return $db->get($query);
}
public function listByCategory($cat, $fields, $page = 1, $order = null, $desc = false)
{
$begin = ($page - 1) * self::ITEMS_PER_PAGE;
$db = DB::getInstance();
|
︙ | | |
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
|
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
|
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
|
}
static protected function _deleteMembres($membres)
{
foreach ($membres as &$id)
{
$id = (int) $id;
// Suppression des fichiers liés
$files = Fichiers::listLinkedFiles(Fichiers::LIEN_MEMBRES, $id, null);
foreach ($files as $file)
{
$file = new Fichiers($file->id, $file);
$file->remove();
}
}
Plugin::fireSignal('membre.suppression', $membres);
$db = DB::getInstance();
// Suppression du membre
return $db->delete('membres', $db->where('id', $membres));
}
}
/**
* @deprecated remplacer par envoyer message à tableau de membres
*/
public function sendMessageToCategory($dest, $sujet, $message, $subscribed_only = false)
{
$config = Config::getInstance();
$message .= "\n\n--\n".$config->get('nom_asso')."\n".$config->get('site_asso');
if ($dest == 0)
$where = 'id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)';
else
$where = 'id_categorie = '.(int)$dest;
// FIXME: filtrage plus intelligent, car le champ lettre_infos peut ne pas exister
if ($subscribed_only)
{
$champs = Config::getInstance()->get('champs_membres');
if ($champs->get('lettre_infos'))
{
$where .= ' AND lettre_infos = 1';
}
}
$db = DB::getInstance();
$res = $db->query('SELECT email FROM membres WHERE LENGTH(email) > 0 AND '.$where.' ORDER BY id;');
$sujet = '['.$config->get('nom_asso').'] '.$sujet;
while ($row = $res->fetchArray(SQLITE3_ASSOC))
{
Utils::mail($row['email'], $sujet, $message);
}
return true;
}
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[] = $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;
}
}
|
︙ | | |
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
-
+
|
}
elseif ($config->type == 'number' || $config->type == 'multiple')
{
$rules[] = 'numeric';
}
elseif ($config->type == 'select')
{
$rules[] = 'in:' . range(0, count($this->options) - 1);
$rules[] = 'in:' . range(0, count($config->options) - 1);
}
elseif ($config->type == 'checkbox')
{
$rules[] = 'boolean';
}
if ($name == 'passe')
|
︙ | | |
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
|
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
|
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
|
/**
* Enregistre les changements de champs en base de données
* @param boolean $enable_copy Recopier les anciennes champs dans les nouveaux ?
* @return boolean true
*/
public function save($enable_copy = true)
{
$db = DB::getInstance();
$config = Config::getInstance();
$db = DB::getInstance();
$config = Config::getInstance();
// Champs à créer
$create = [
'id INTEGER PRIMARY KEY, -- Numéro attribué automatiquement',
'id_categorie INTEGER NOT NULL, -- Numéro de catégorie',
'date_connexion TEXT NULL, -- Date de dernière connexion',
'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE, -- Date d\'inscription',
// Champs à créer
$create = [
'id INTEGER PRIMARY KEY, -- Numéro attribué automatiquement',
'id_categorie INTEGER NOT NULL,',
'date_connexion TEXT NULL CHECK (date_connexion IS NULL OR datetime(date_connexion) = date_connexion), -- Date de dernière connexion',
'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date_inscription) IS NOT NULL AND date(date_inscription) = date_inscription), -- Date d\'inscription',
'secret_otp TEXT NULL, -- Code secret pour TOTP',
'clef_pgp TEXT NULL, -- Clé publique PGP'
];
];
// Clés à créer, permet aussi de clôturer la syntaxe du tableau, noter l'absence de virgule dans cette ligne
$create_keys = [
'FOREIGN KEY (id_categorie) REFERENCES membres_categories (id)'
];
// Champs à recopier
$copy = [
'id' => 'id',
|
︙ | | |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
+
|
<?php
namespace Garradin\Membres;
use Garradin\Membres;
use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;
use KD2\ODSWriter;
class Import
{
/**
* Champs du CSV de Galette
* les lignes vides ('') ne seront pas proposées à l'import
* @var array
*/
public $galette_fields = [
'Numéro',
1,
'Nom',
'Prénom',
'Pseudo',
'Société',
2,
'Date de naissance',
3,
'Adresse, ligne 1',
'Adresse, ligne 2',
'Code postal',
'Ville',
'Pays',
'Téléphone fixe',
'Téléphone mobile',
'E-Mail',
'Site web',
'ICQ',
'MSN',
'Jabber',
'Infos (réservé administrateur)',
'Infos (public)',
'Profession',
'Identifiant',
'Mot de passe',
'Date création fiche',
'Date modification fiche',
4, // activite_adh
5, // bool_admin_adh
6, // bool_exempt_adh
7, // bool_display_info
8, // date_echeance
9, // pref_lang
'Lieu de naissance',
10, // GPG id
11 // Fingerprint
];
/**
* Importer un CSV de la liste des membres depuis Galette
* @param string $path Chemin vers le CSV
* @param array $translation_table Tableau indiquant la correspondance à effectuer entre les champs
* de Galette et ceux de Garradin. Par exemple : ['Date création fiche' => 'date_inscription']
* @return boolean TRUE en cas de succès
*/
public function fromGalette($path, $translation_table)
public function getCSVAsArray($path)
{
if (!file_exists($path) || !is_readable($path))
{
throw new \RuntimeException('Fichier inconnu : '.$path);
}
$fp = fopen($path, 'r');
if (!$fp)
{
return false;
}
$db = DB::getInstance();
$db->begin();
$membres = new Membres;
$delim = Utils::find_csv_delim($fp);
$columns = array_flip($this->galette_fields);
Utils::skip_bom($fp);
$col = function($column) use (&$row, &$columns)
{
if (!isset($columns[$column]))
return null;
if (!isset($row[$columns[$column]]))
return null;
return $row[$columns[$column]];
};
$line = 0;
$delim = Utils::find_csv_delim($fp);
$out = [];
Utils::skip_bom($fp);
$nb_columns = null;
while (!feof($fp))
{
$row = fgetcsv($fp, 4096, $delim);
$line++;
if (empty($row))
{
continue;
}
if (null === $nb_columns)
{
$nb_columns = count($row);
}
if (count($row) != count($columns))
if (count($row) != $nb_columns)
{
throw new UserException('Erreur sur la ligne ' . $line . ' : incohérence dans le nombre de colonnes avec la première ligne.');
}
$out[$line] = $row;
}
fclose($fp);
return $out;
}
/**
* Importer un CSV générique
* @param string $path Chemin vers le CSV
* @param array $translation_table Tableau indiquant la correspondance à effectuer entre les colonnes
* du CSV et les champs de Garradin. Par exemple : ['Date création fiche' => 'date_inscription']
* @return boolean TRUE en cas de succès
*/
public function fromArray(array $table, $translation_table, $skip_lines = 0)
{
$db = DB::getInstance();
$db->begin();
$membres = new Membres;
$champs = Config::getInstance()->get('champs_membres');
$nb_columns = count($translation_table);
if ($skip_lines)
{
$table = array_slice($table, $skip_lines, null, true);
}
foreach ($table as $line => $row)
{
if (empty($row))
{
continue;
}
if (count($row) != $nb_columns)
{
$db->rollback();
throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.');
}
$data = [];
foreach ($translation_table as $galette=>$garradin)
foreach ($translation_table as $column_index => $garradin_field)
{
// Champs qu'on ne veut pas importer
if (empty($garradin))
if (empty($garradin_field))
{
continue;
// Concaténer plusieurs champs
if (isset($data[$garradin]))
$data[$garradin] .= "\n" . $col($galette);
else
$data[$garradin] = $col($galette);
}
// Concaténer plusieurs champs, si on choisit d'indiquer plusieurs fois
// le même champ pour plusieurs colonnes (par exemple pour mettre nom et prénom
// dans un seul champ)
if (isset($data[$garradin_field]))
{
$champ = $champs->get($garradin_field);
if ($champ->type == 'text')
{
$data[$garradin_field] .= ' ' . $row[$column_index];
}
elseif ($champ->type == 'textarea')
{
$data[$garradin_field] .= "\n" . $row[$column_index];
}
else
{
throw new UserException(sprintf('Erreur sur la ligne %d : impossible de concaténer des colonnes avec le champ %s : n\'est pas un champ de type texte', $line, $champ->title));
}
}
else
{
$data[$garradin_field] = $row[$column_index];
}
}
try {
$membres->add($data, false);
}
catch (UserException $e)
{
$db->rollback();
throw new UserException('Erreur sur la ligne ' . $line . ' : ' . $e->getMessage());
}
}
$db->commit();
fclose($fp);
return true;
}
/**
* Importer un CSV de la liste des membres depuis un export Garradin
* @param string $path Chemin vers le CSV
* @param string $path Chemin vers le CSV
* @param int $current_user_id
* @return boolean TRUE en cas de succès
*/
public function fromCSV($path, $current_user_id)
public function fromGarradinCSV($path, $current_user_id)
{
if (!file_exists($path) || !is_readable($path))
{
throw new \RuntimeException('Fichier inconnu : '.$path);
}
$fp = fopen($path, 'r');
|
︙ | | |
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
|
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
|
-
+
-
+
-
-
+
+
+
-
-
+
+
+
+
+
-
+
-
-
+
-
-
-
-
+
-
-
-
+
-
-
-
-
+
-
-
+
-
-
-
-
+
-
-
-
+
-
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
-
-
+
-
-
-
-
-
-
-
-
|
$db->commit();
fclose($fp);
return true;
}
protected function export()
protected function export(array $list = null)
{
$db = DB::getInstance();
$db = DB::getInstance();
$champs = Config::getInstance()->get('champs_membres')->getKeys();
$champs_sql = 'm.' . implode(', m.', $champs);
$champs = Config::getInstance()->get('champs_membres')->getKeys();
$champs_sql = 'm.' . implode(', m.', $champs);
$where = $list ? 'WHERE ' . $db->where('m.id', $list) : '';
$res = $db->prepare('SELECT ' . $champs_sql . ', c.nom AS categorie FROM membres AS m
LEFT JOIN membres_categories AS c ON m.id_categorie = c.id ORDER BY c.id;')->execute();
$res = $db->iterate('SELECT ' . $champs_sql . ', c.nom AS categorie FROM membres AS m
LEFT JOIN membres_categories AS c ON m.id_categorie = c.id
' . $where . '
ORDER BY c.id;');
return [
return [array_merge($champs, ['categorie']), $res];
array_merge($champs, ['categorie']),
}
$res,
public function toCSV()
{
list($champs, $result) = $this->export();
sprintf('Export membres - %s - %s', Config::getInstance()->get('nom_asso'), date('Y-m-d')),
$fp = fopen('php://output', 'w');
$header = false;
];
while ($row = $result->fetchArray(SQLITE3_ASSOC))
{
unset($row->passe);
}
if (!$header)
{
fputs($fp, Utils::row_to_csv(array_keys($row)));
$header = true;
}
public function toCSV(array $list = null)
fputs($fp, Utils::row_to_csv($row));
}
{
fclose($fp);
return true;
}
list($champs, $result, $name) = $this->export($list);
return Utils::toCSV($name, $result, $champs);
}
public function toODS()
{
list($champs, $result) = $this->export();
public function toODS(array $list = null)
{
list($champs, $result, $name) = $this->export($list);
$ods = new ODSWriter;
$ods->table_name = 'Membres';
return Utils::toODS($name, $result, $champs);
$ods->add($champs);
}
while ($row = $result->fetchArray(SQLITE3_ASSOC))
{
unset($row->passe);
$ods->add($row);
}
$ods->output();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
-
|
<?php
namespace Garradin\Membres;
use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;
use Garradin\Membres;
use Garradin\UserException;
use const Garradin\SECRET_KEY;
use const Garradin\WWW_URL;
use const Garradin\ADMIN_URL;
use const Garradin\FORCE_EMAIL_FROM;
use KD2\Security;
use KD2\Security_OTP;
use KD2\QRCode;
class Session extends \KD2\UserSession
{
|
︙ | | |
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
-
+
-
+
+
+
-
+
+
|
protected function deleteAllRememberMeSelectors($user_id)
{
return $this->db->delete('membres_sessions', $this->db->where('id_membre', $user_id));
}
// Ajout de la gestion de LOCAL_LOGIN
public function isLogged()
public function isLogged($disable_local_login = false)
{
$logged = parent::isLogged();
if (!$logged && defined('\Garradin\LOCAL_LOGIN')
if (!$disable_local_login && defined('\Garradin\LOCAL_LOGIN')
&& is_int(\Garradin\LOCAL_LOGIN) && \Garradin\LOCAL_LOGIN > 0)
{
if (!$logged || ($logged && $this->user->id != \Garradin\LOCAL_LOGIN))
{
$logged = $this->create(\Garradin\LOCAL_LOGIN);
$logged = $this->create(\Garradin\LOCAL_LOGIN);
}
}
return $logged;
}
// Ici checkOTP utilise NTP en second recours
public function checkOTP($secret, $code)
|
︙ | | |
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
|
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
|
-
+
-
|
$query = sprintf('%s.%s.%s', $id, $expire, $hash);
$message = "Bonjour,\n\nVous avez oublié votre mot de passe ? Pas de panique !\n\n";
$message.= "Il vous suffit de cliquer sur le lien ci-dessous pour recevoir un nouveau mot de passe.\n\n";
$message.= ADMIN_URL . 'password.php?c=' . $query;
$message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé.";
Utils::mail($membre->email, '['.$config->get('nom_asso').'] Mot de passe perdu ?', $message, [], $membre->clef_pgp);
return Utils::sendEmail(Utils::EMAIL_CONTEXT_SYSTEM, $membre->email, 'Mot de passe perdu ?', $message, $membre->id, $membre->clef_pgp);
return true;
}
static public function recoverPasswordConfirm($code)
{
if (substr_count($code, '.') !== 2)
{
return false;
|
︙ | | |
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
|
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
|
-
+
|
$message.= "Votre nouveau mot de passe : ".$password."\n\n";
$message.= "Si vous n'avez pas demandé à recevoir ce message, merci de nous le signaler.";
$password = Membres::hashPassword($password);
$db->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$id]);
return Utils::mail($membre->email, '['.$config->get('nom_asso').'] Nouveau mot de passe', $message, [], $membre->clef_pgp);
return Utils::sendEmail(Utils::EMAIL_CONTEXT_SYSTEM, $membre->email, 'Nouveau mot de passe', $message, $membre->id, $membre->clef_pgp);
}
public function editUser($data)
{
(new Membres)->edit($this->user->id, $data, false);
$this->refresh();
|
︙ | | |
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
|
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
|
-
+
-
-
+
+
-
-
+
-
+
-
+
|
return $out;
}
public function sendMessage($dest, $sujet, $message, $copie = false)
{
$user = $this->getUser();
$config = Config::getInstance();
$content = "Ce message vous a été envoyé par :\n";
$from = sprintf('"%s" <%s>', sprintf('=?UTF-8?B?%s?=', base64_encode($user->identite)), FORCE_EMAIL_FROM ?: $config->get('email_asso'));
$content.= sprintf("%s\n%s\n\n", $user->identite, $user->email);
$content.= str_repeat('=', 70) . "\n\n";
$message .= "\n\n--\nCe message a été envoyé par un membre de ".$config->get('nom_asso');
$message .= ", merci de contacter ".$config->get('email_asso')." en cas d'abus.";
$content.= $message;
if ($copie)
{
Utils::mail($from, $sujet, $message);
Utils::sendEmail(Utils::EMAIL_CONTEXT_PRIVATE, $user->email, $sujet, $content, $user->id);
}
return Utils::mail($dest, $sujet, $message, ['From' => $from, 'Reply-To' => $user->email]);
return Utils::sendEmail(Utils::EMAIL_CONTEXT_PRIVATE, $dest, $sujet, $content);
}
public function editSecurity(Array $data = [])
{
$allowed_fields = ['passe', 'clef_pgp', 'secret_otp'];
foreach ($data as $key=>$value)
|
︙ | | |
1
2
3
4
5
6
7
8
9
10
11
12
13
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
+
+
|
<?php
namespace Garradin;
class Plugin
{
const PLUGIN_ID_SYNTAX = '[a-z]+(?:_[a-z]+)*';
protected $id = null;
protected $plugin = null;
protected $config_changed = false;
protected $mimes = [
'css' => 'text/css',
'gif' => 'image/gif',
|
︙ | | |
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
-
-
-
+
+
+
-
+
+
+
+
|
'pdf' => 'application/pdf',
'png' => 'image/png',
'swf' => 'application/shockwave-flash',
'xml' => 'text/xml',
'svg' => 'image/svg+xml',
];
static protected $signal_files = [];
static public function getPath($id)
static public function getPath($id, $fail_with_exception = true)
{
if (file_exists(PLUGINS_ROOT . '/' . $id . '.tar.gz'))
{
return 'phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz';
}
elseif (is_dir(PLUGINS_ROOT . '/' . $id))
{
return PLUGINS_ROOT . '/' . $id;
}
if ($fail_with_exception)
{
throw new \LogicException(sprintf('Le plugin "%s" n\'existe pas dans le répertoire des plugins.', $id));
throw new \LogicException(sprintf('Le plugin "%s" n\'existe pas dans le répertoire des plugins.', $id));
}
return false;
}
/**
* Construire un objet Plugin pour un plugin
* @param string $id Identifiant du plugin
* @throws UserException Si le plugin n'est pas installé (n'existe pas en DB)
*/
|
︙ | | |
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
+
+
+
|
$this->plugin->config = json_decode($this->plugin->config);
if (!is_object($this->plugin->config))
{
$this->plugin->config = new \stdClass;
}
// Juste pour vérifier que le fichier source du plugin existe bien
self::getPath($id);
$this->id = $id;
}
/**
* Enregistrer les changements dans la config
*/
|
︙ | | |
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
|
-
+
|
$file = preg_replace('!^[./]*!', '', $file);
if (preg_match('!(?:\.\.|[/\\\\]\.|\.[/\\\\])!', $file))
{
throw new \RuntimeException('Chemin de fichier incorrect.');
}
$forbidden = ['install.php', 'garradin_plugin.ini', 'upgrade.php', 'uninstall.php', 'signals.php'];
$forbidden = ['install.php', 'garradin_plugin.ini', 'upgrade.php', 'uninstall.php'];
if (in_array($file, $forbidden))
{
throw new UserException('Le fichier ' . $file . ' ne peut être appelé par cette méthode.');
}
if (!file_exists($this->path() . '/www/' . $file))
|
︙ | | |
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
|
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
|
-
+
|
* Renvoie TRUE si le plugin a besoin d'être mis à jour
* (si la version notée dans la DB est différente de la version notée dans garradin_plugin.ini)
* @return boolean TRUE si le plugin doit être mis à jour, FALSE sinon
*/
public function needUpgrade()
{
$infos = (object) parse_ini_file($this->path() . '/garradin_plugin.ini', false);
if (version_compare($this->plugin->version, $infos->version, '!='))
return true;
return false;
}
/**
|
︙ | | |
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
|
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
|
-
-
+
+
+
+
+
+
-
-
+
+
+
-
+
+
+
+
+
+
+
+
|
{
$plugin = $this;
include $this->path() . '/upgrade.php';
}
$infos = (object) parse_ini_file($this->path() . '/garradin_plugin.ini', false);
return DB::getInstance()->update('plugins',
['version' => $infos->version],
return DB::getInstance()->update('plugins', [
'nom' => $infos->nom,
'description'=> $infos->description,
'auteur' => $infos->auteur,
'url' => $infos->url,
'version' => $infos->version,
'id = :id',
['id' => $this->id]
'menu' => (int)(bool)$infos->menu,
'menu_condition' => $infos->menu && isset($infos->menu_condition) ? trim($infos->menu_condition) : null,
], 'id = :id', ['id' => $this->id]);
);
}
/**
* Associer un signal à un callback du plugin
* @param string $signal Nom du signal (par exemple boucle.agenda pour la boucle de type AGENDA)
* @param mixed $callback Callback, sous forme d'un nom de fonction ou de méthode statique
* @return boolean TRUE
*/
public function registerSignal($signal, $callback)
{
$callable_name = '';
if (!is_callable($callback, true, $callable_name) || !is_string($callable_name))
{
throw new \LogicException('Le callback donné n\'est pas valide.');
}
// pour empêcher d'appeler des méthodes de Garradin après un import de base de données "hackée"
if (strpos($callable_name, 'Garradin\\Plugin\\') !== 0)
{
throw new \LogicException('Le callback donné n\'utilise pas le namespace Garradin\\Plugin');
}
$db = DB::getInstance();
// Signaux exclusifs, qui ne peuvent être attribués qu'à un seul plugin
if (strpos($signal, 'boucle.') === 0)
{
$registered = $db->firstColumn('SELECT plugin FROM plugins_signaux WHERE signal = ? AND plugin != ?;', $signal, $this->id);
if ($registered)
{
throw new \LogicException('Le signal ' . $signal . ' est exclusif et déjà associé au plugin "'.$registered.'"');
}
}
$callable_name = str_replace('Garradin\\Plugin\\', '', $callable_name);
$st = $db->prepare('INSERT OR REPLACE INTO plugins_signaux VALUES (:signal, :plugin, :callback);');
$st->bindValue(':signal', $signal);
$st->bindValue(':plugin', $this->id);
$st->bindValue(':callback', $callable_name);
return $st->execute();
}
|
︙ | | |
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
|
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
|
+
+
+
+
+
+
|
$db = DB::getInstance();
$plugins = $db->getGrouped('SELECT id, * FROM plugins ORDER BY nom;');
$system = explode(',', PLUGINS_SYSTEM);
foreach ($plugins as &$row)
{
$row->system = in_array($row->id, $system);
$row->disabled = !self::getPath($row->id, false);
}
return $plugins;
}
/**
* Vérifie que les plugins système sont bien installés et sinon les réinstalle
* @return void
*/
static public function checkAndInstallSystemPlugins()
{
if (!PLUGINS_SYSTEM)
{
return true;
}
$system = explode(',', PLUGINS_SYSTEM);
if (count($system) == 0)
{
return true;
}
|
︙ | | |
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
|
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
|
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
|
return true;
}
/**
* Liste les plugins qui doivent être affichés dans le menu
* @return array Tableau associatif id => nom (ou un tableau vide si aucun plugin ne doit être affiché)
*/
static public function listMenu()
static public function listMenu($user)
{
$db = DB::getInstance();
return $db->getAssoc('SELECT id, nom FROM plugins WHERE menu = 1 ORDER BY nom;');
$list = $db->getGrouped('SELECT id, nom, menu_condition FROM plugins WHERE menu = 1 ORDER BY nom;');
foreach ($list as $id => &$row)
{
if (!self::getPath($row->id, false))
{
// Ne pas lister les plugins dont le code a disparu
unset($list[$id]);
continue;
}
if (!$row->menu_condition)
{
$row = $row->nom;
continue;
}
$condition = strtr($row->menu_condition, [
'{Membres::DROIT_AUCUN}' => Membres::DROIT_AUCUN,
'{Membres::DROIT_ACCES}' => Membres::DROIT_ACCES,
'{Membres::DROIT_ECRITURE}' => Membres::DROIT_ECRITURE,
'{Membres::DROIT_ADMIN}' => Membres::DROIT_ADMIN,
]);
$condition = preg_replace_callback('/\{\$user\.(\w+)\}/', function ($m) use ($user) { return $user->{$m[1]}; }, $condition);
$query = 'SELECT 1 WHERE ' . $condition . ';';
$st = $db->prepare($query);
if (!$st->readOnly())
{
throw new \LogicException('Requête plugin pour affichage dans le menu n\'est pas en lecture : ' . $query);
}
$res = $st->execute();
if (!$res->fetchArray(\SQLITE3_NUM))
{
unset($list[$id]);
continue;
}
$row = $row->nom;
}
unset($row);
return $list;
}
/**
* Liste les plugins téléchargés mais non installés
* @return array Liste des plugins téléchargés
*/
static public function listDownloaded()
{
$installed = self::listInstalled();
$list = [];
$dir = dir(PLUGINS_ROOT);
while ($file = $dir->read())
{
if (substr($file, 0, 1) == '.')
continue;
if (preg_match('!^([a-zA-Z0-9_.-]+)\.tar\.gz$!i', $file, $match))
if (preg_match('!^(' . self::PLUGIN_ID_SYNTAX . ')\.tar\.gz$!', $file, $match))
{
// Sélectionner les archives PHAR
$file = $match[1];
}
elseif (is_dir(PLUGINS_ROOT . '/' . $file)
&& preg_match('!^([a-zA-Z0-9_.-]+)$!i', $file)
&& preg_match('!^' . self::PLUGIN_ID_SYNTAX . '$!', $file)
&& is_file(sprintf('%s/%s/garradin_plugin.ini', PLUGINS_ROOT, $file)))
{
// Rien à faire, le nom valide du plugin est déjà dans "$file"
}
else
{
// ignorer tout ce qui n'est pas un répertoire ou une archive PHAR valides
|
︙ | | |
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
|
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
|
-
+
|
catch (\Exception $e)
{
throw new UserException('Le téléchargement du plugin '.$id.' a échoué : ' . $e->getMessage());
}
if (!self::checkHash($id))
{
unlink(PLUGINS_ROOT . '/' . $id . '.tar.gz');
Utils::safe_unlink(PLUGINS_ROOT . '/' . $id . '.tar.gz');
throw new \RuntimeException('L\'archive du plugin '.$id.' est corrompue (le hash SHA1 ne correspond pas).');
}
self::install($id, true);
return true;
}
|
︙ | | |
635
636
637
638
639
640
641
642
643
644
645
646
647
648
|
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
|
+
|
'officiel' => (int)(bool)$official,
'nom' => $infos->nom,
'description'=> $infos->description,
'auteur' => $infos->auteur,
'url' => $infos->url,
'version' => $infos->version,
'menu' => (int)(bool)$infos->menu,
'menu_condition' => $infos->menu && isset($infos->menu_condition) ? trim($infos->menu_condition) : null,
'config' => $config,
]);
if (file_exists($path . '/install.php'))
{
$plugin = new Plugin($id);
require $plugin->path() . '/install.php';
|
︙ | | |
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
|
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
|
-
+
-
-
+
+
-
-
-
+
-
-
+
-
-
-
-
+
+
+
+
|
/**
* Déclenche le signal donné auprès des plugins enregistrés
* @param string $signal Nom du signal
* @param array $params Paramètres du callback (array ou null)
* @return NULL NULL si aucun plugin n'a été appelé, true sinon
*/
static public function fireSignal($signal, $params = null, &$return = null)
static public function fireSignal($signal, $params = null, &$callback_return = null)
{
$list = DB::getInstance()->get('SELECT * FROM plugins_signaux WHERE signal = ?;', $signal);
foreach ($list as $row)
{
if (!in_array($row->plugin, self::$signal_files))
{
$return = call_user_func_array('Garradin\\Plugin\\' . $row->callback, [&$params, &$callback_return]);
require_once self::getPath($row->plugin) . '/signals.php';
}
if ($return)
$return = call_user_func_array($row->callback, [&$params, &$return]);
{
if ($return)
return $return;
}
return !empty($list) ? true : null;
}
}
return !empty($list) ? false : null;
}
}
|