Changes In Branch dev Excluding Merge-Ins
This is equivalent to a diff from a1a07cf6fa to c9e600ebc1
2021-03-01
| ||
02:29 | Transform debt and credit payments in advanced transactions Leaf check-in: 5595d3ca25 user: bohwaz tags: trunk, stable | |
01:57 | Bring FileSystem backend up to date, fix migrateStorage Leaf check-in: c9e600ebc1 user: bohwaz tags: dev | |
01:57 | Fix function name typo in encrypted page check-in: 8a7cea7e6a user: bohwaz tags: dev | |
2021-02-26
| ||
01:09 | Merge changes from trunk check-in: 49124e29be user: bohwaz tags: dev | |
2021-02-25
| ||
20:54 | Add "first connection" to label for lost password check-in: a1a07cf6fa user: bohwaz tags: trunk, stable | |
2021-02-22
| ||
23:52 | CSV custom import: Error message when all translation rows are unselected check-in: 2593b9def3 user: bohwaz tags: trunk, stable | |
Modified src/.htaccess.www from [34b16e1e96] to [3ae6d113cf].
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Options -MultiViews -Indexes DirectoryIndex disabled DirectoryIndex index.php index.html # Au cas où <IfModule mod_alias.c> RedirectMatch 403 /include/ RedirectMatch 403 /cache/ RedirectMatch 403 /plugins/ RedirectMatch 403 /templates/ RedirectMatch 403 ^/scripts/ RedirectMatch 403 /.*\.sqlite RedirectMatch 403 /.*\.log RedirectMatch 403 /(README|VERSION|COPYING|Makefile|cron\.php) RedirectMatch 403 /config\.(.*)\.php RedirectMatch 403 /sous-domaine\.html RedirectMatch 403 _inc\.php </IfModule> |
< < | |
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Options -MultiViews -Indexes
DirectoryIndex disabled
DirectoryIndex index.php index.html
# Au cas où
<IfModule mod_alias.c>
RedirectMatch 403 /include/
RedirectMatch 403 /templates/
RedirectMatch 403 ^/scripts/
RedirectMatch 403 /data/
RedirectMatch 403 /.*\.log
RedirectMatch 403 /(README|VERSION|COPYING|Makefile|cron\.php)
RedirectMatch 403 /config\.(.*)\.php
RedirectMatch 403 /sous-domaine\.html
RedirectMatch 403 _inc\.php
</IfModule>
|
Modified src/VERSION from [4f24748557] to [c7f4566bfd].
1 |
1.0.5
|
| |
1 |
1.1.0-alpha1
|
Deleted src/cache/index.html version [abe89f9bfb].
1 |
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>
|
< |
Modified src/config.dist.php from [35e054b963] to [fb69fce6e6].
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
...
362
363
364
365
366
367
368
|
* Défaut : répertoire racine de Garradin (__DIR__) */ //const ROOT = __DIR__; /** * Répertoire où sont situées les données de Garradin * (incluant la base de données SQLite, les sauvegardes, le cache et les fichiers locaux) * * Défaut : identique à ROOT */ //const DATA_ROOT = ROOT; /** * Répertoire où est situé le cache (fichiers temporaires utilisés pour accélérer le chargement des pages) * * Défaut : sous-répertoire 'cache' de DATA_ROOT */ //const CACHE_ROOT = ROOT . '/cache'; /** * Emplacement du fichier de base de données de Garradin * * Défaut : DATA_ROOT . '/association.sqlite' */ ................................................................................ * * sinon la personnalisation des couleurs ne fonctionnera pas * * Défaut : [ADMIN_URL]static/gdin_bg.png */ //const ADMIN_BACKGROUND_IMAGE = 'http://mon-asso.fr/fond_garradin.png'; |
|
|
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
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
...
362
363
364
365
366
367
368
369
370
371
372
373
374
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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
|
* Défaut : répertoire racine de Garradin (__DIR__) */ //const ROOT = __DIR__; /** * Répertoire où sont situées les données de Garradin * (incluant la base de données SQLite, les sauvegardes, le cache, les fichiers locaux et les plugins) * * Défaut : sous-répertoire "data" de la racine */ //const DATA_ROOT = ROOT . '/data'; /** * Répertoire où est situé le cache (fichiers temporaires utilisés pour accélérer le chargement des pages) * * Défaut : sous-répertoire 'cache' de DATA_ROOT */ //const CACHE_ROOT = DATA_ROOT . '/cache'; /** * Emplacement du fichier de base de données de Garradin * * Défaut : DATA_ROOT . '/association.sqlite' */ ................................................................................ * * sinon la personnalisation des couleurs ne fonctionnera pas * * Défaut : [ADMIN_URL]static/gdin_bg.png */ //const ADMIN_BACKGROUND_IMAGE = 'http://mon-asso.fr/fond_garradin.png'; /** * Stockage des fichiers * * Indiquer ici le nom d'une classe de stockage de fichiers * (parmis celles disponibles dans lib/Garradin/Files/Backend) * * Indiquer NULL si vous souhaitez stocker les fichier dans la base * de données SQLite (valeur par défaut). * * Classes de stockage possibles : * - SQLite : enregistre dans la base de données (défaut) * - FileSystem : enregistrement des fichiers dans le système de fichier * * Défaut : null */ //const FILE_STORAGE_BACKEND = null; /** * Configuration du stockage des fichiers * * Indiquer dans cette constante la configuration de la classe de stockage * des fichiers. * * Valeurs possibles : * - SQLite : aucune configuration possible * - FileSystem : [ * 'path' => (string) chemin du répertoire où doivent être stockés les fichiers * 'scan' => (bool) chemin * * Défaut : null */ //const FILE_STORAGE_CONFIG = null; /** * Forcer le quota disponible pour les fichiers * * Si cette constante est renseignée (en octets) alors il ne sera * pas possible de stocker plus que cette valeur. * Tout envoi de fichier sera refusé. * * Défaut : null (dans ce cas c'est le stockage qui détermine la taille disponible) */ //const FILE_STORAGE_QUOTA = 10000; // Forcer le quota alloué à 10 Mo, quel que soit le backend de stockage /** * Mise à jour automatique des métadonnées du système de fichier * * Si activé (true) Garradin effectuera un scan intégral * des fichiers pour détecter les modifications lors de l'ouverture * d'une page qui utilise des fichiers. * * Exemples : * - page de détails d'une écriture n°42 = scan du répertoire "transaction/42" * - liste des pages web = scan du répertoire "web" * etc. * * Si activé, ce réglage peut avoir un impact sur les performances avec le backend * "FileSystem" (aucune différence avec le backend "SQLite"). * * Dans ce cas il est recommandé de désactiver la mise à jour automatique (false) * et de lancer le script "scripts/watch_filesystem.sh" qui utilise inotify * pour ne mettre à jour que si un fichier a été modifié/ajouté/supprimé. */ //const FILE_STORAGE_SYNC_AUTO = true; |
Added src/data/cache/index.html version [abe89f9bfb].
> |
1 |
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>
|
Added src/data/plugins/index.html version [abe89f9bfb].
> |
1 |
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>
|
Added src/include/data/1.1.0_migration.sql version [1e80e5f88d].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
-- Remove triggers in case they interact with the migration DROP TRIGGER IF EXISTS wiki_recherche_delete; DROP TRIGGER IF EXISTS wiki_recherche_update; DROP TRIGGER IF EXISTS wiki_recherche_contenu_insert; DROP TRIGGER IF EXISTS wiki_recherche_contenu_chiffre; ALTER TABLE membres_categories RENAME TO membres_categories_old; ALTER TABLE config RENAME TO config_old; .read 1.1.0_schema.sql INSERT INTO config SELECT * FROM config_old; DROP TABLE config_old; -- This is not used anymore DELETE FROM config WHERE key = 'version'; DELETE FROM config WHERE key = 'accueil_wiki'; -- New config key INSERT INTO config (key, value) VALUES ('telephone_asso', NULL); -- Create directories INSERT INTO files (path, name, type) VALUES ('', 'documents', 2); INSERT INTO files (path, name, type) VALUES ('', 'config', 2); INSERT INTO files (path, name, type) VALUES ('', 'transaction', 2); INSERT INTO files (path, name, type) VALUES ('', 'skel', 2); INSERT INTO files (path, name, type) VALUES ('', 'user', 2); INSERT INTO files (path, name, type) VALUES ('', 'web', 2); -- Copy droit_wiki value to droit_web and droit_documents INSERT INTO users_categories SELECT id, nom, droit_wiki, -- perm_web droit_wiki, -- perm_documents droit_membres, droit_compta, droit_inscription, droit_connexion, droit_config, cacher FROM membres_categories_old; DROP TABLE membres_categories_old; UPDATE recherches SET contenu = REPLACE(contenu, 'id_categorie', 'category_id') WHERE cible = 'membres' AND contenu LIKE '%id_categorie%'; -- Copy existing files for transactions INSERT INTO files (path, name, type, mime, modified, size, image) SELECT 'transaction/' || t.id, f.nom, 1, f.type, f.datetime, c.taille, f.image FROM fichiers f INNER JOIN fichiers_contenu c ON c.id = f.id_contenu INNER JOIN fichiers_acc_transactions t ON t.fichier = f.id; INSERT INTO files_contents (id, compressed, content) SELECT f2.id, 0, c.contenu FROM fichiers f INNER JOIN files f2 ON f2.name = f.nom AND f2.path = 'transaction/' || t.id INNER JOIN fichiers_contenu c ON c.id = f.id_contenu INNER JOIN fichiers_acc_transactions t ON t.fichier = f.id; -- Copy wiki pages content CREATE TEMP TABLE wiki_as_files (old_id, new_id, path, content, title, uri, old_parent, new_parent, created, modified, author_id, encrypted, type, public); INSERT INTO wiki_as_files SELECT id, NULL, '', contenu, titre, uri, parent, parent, date_creation, date_modification, id_auteur, chiffrement, CASE WHEN (SELECT 1 FROM wiki_pages pp WHERE pp.parent = p.id LIMIT 1) THEN 1 ELSE 2 END, -- Type, 1 = category, 2 = page CASE WHEN droit_lecture = -1 THEN 1 ELSE 0 END -- public FROM wiki_pages p INNER JOIN wiki_revisions r ON r.id_page = p.id AND r.revision = p.revision; -- Build back path, up to ten levels UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files AS waf SET path = (SELECT uri FROM wiki_as_files WHERE old_id = waf.new_parent) || '/' || path, new_parent = (SELECT new_parent FROM wiki_as_files WHERE old_id = waf.new_parent) WHERE new_parent > 0; UPDATE wiki_as_files SET path = TRIM(path, '/'); UPDATE wiki_as_files SET path = NULL WHERE path = ''; -- Copy into files INSERT INTO files (path, name, type, mime, modified, size) SELECT 'web/' || (CASE WHEN path IS NOT NULL THEN path || '/' ELSE '' END) || uri, 'index.txt', 1, 'text/plain', modified, 0 -- size will be set after FROM wiki_as_files; UPDATE wiki_as_files SET new_id = (SELECT id FROM files WHERE path = 'web/' || (CASE WHEN path IS NOT NULL THEN path || '/' ELSE '' END) || uri); -- x'0a' == \n INSERT INTO files_contents (id, compressed, content) SELECT new_id, 0, 'Title: ' || title || x'0a' || 'Published: ' || created || x'0a' || 'Status: ' || (CASE WHEN public THEN 'Online' ELSE 'Draft' END) || x'0a' || 'Format: ' || (CASE WHEN encrypted THEN 'Skriv/Encrypted' ELSE 'Skriv' END) || x'0a' || x'0a' || '----' || x'0a' || x'0a' || content FROM wiki_as_files; -- Set file size UPDATE files SET size = (SELECT LENGTH(content) FROM files_contents WHERE id = files.id); UPDATE wiki_as_files SET new_parent = (SELECT w.new_id FROM wiki_as_files w WHERE w.old_id = wiki_as_files.old_parent); -- Copy to search INSERT INTO files_search (id, title, content) SELECT new_id, title, content FROM wiki_as_files WHERE encrypted = 0; -- Copy to search INSERT INTO files_search (id, title, content) SELECT new_id, title, 'Contenu chiffré' FROM wiki_as_files WHERE encrypted = 1; -- Copy to web_pages INSERT INTO web_pages (id, parent, path, name, type, status, title, published, modified, format, content) SELECT new_id, path, (CASE WHEN path IS NOT NULL THEN path || '/' ELSE '' END) || uri, 'index.txt', type, CASE WHEN public THEN 'online' ELSE 'draft' END, title, created, modified, CASE WHEN encrypted THEN 'skriv/encrypted' ELSE 'skriv' END, content FROM wiki_as_files; -- Copy files linked to wiki pages INSERT INTO files (path, name, type, mime, modified, size, image) SELECT 'web/' || (CASE WHEN waf.path IS NOT NULL THEN waf.path || '/' ELSE '' END) || uri, f.nom, 1, f.type, f.datetime, c.taille, f.image FROM fichiers f INNER JOIN fichiers_contenu c ON c.id = f.id_contenu INNER JOIN fichiers_wiki_pages w ON w.fichier = f.id INNER JOIN wiki_as_files waf ON w.id = waf.old_id; INSERT INTO files_contents (id, compressed, content) SELECT f2.id, 0, c.contenu FROM fichiers f INNER JOIN fichiers_contenu c ON c.id = f.id_contenu INNER JOIN fichiers_wiki_pages w ON w.fichier = f.id INNER JOIN wiki_as_files waf ON w.id = waf.old_id INNER JOIN files AS f2 ON f2.name = f.nom AND f2.path = 'web/' || (CASE WHEN waf.path IS NOT NULL THEN waf.path || '/' ELSE '' END) || waf.uri; -- Create parent directories INSERT INTO files (type, path, name) SELECT 2, CASE WHEN waf.path IS NOT NULL THEN 'web/' || waf.path ELSE 'web' END, waf.uri FROM wiki_as_files waf GROUP BY waf.old_id; -- Copy existing config files INSERT INTO files (path, name, type, mime, modified, size, image) SELECT 'config', 'admin_bg.png', 1, type, datetime, c.taille, image FROM fichiers f INNER JOIN fichiers_contenu c ON c.id = f.id_contenu WHERE f.id = (SELECT c.value FROM config c WHERE key = 'image_fond') LIMIT 1; INSERT INTO files_contents (id, compressed, content) SELECT f2.id, 0, c.contenu FROM files AS f2 INNER JOIN fichiers f ON f2.name = 'admin_bg.png' AND f2.path = 'config' INNER JOIN fichiers_contenu c ON c.id = f.id_contenu WHERE f.id = (SELECT c.value FROM config c WHERE key = 'image_fond') LIMIT 1; -- Rename UPDATE config SET key = 'admin_background', value = 'config/admin_bg.png' WHERE key = 'image_fond'; -- Copy connection page as a single file INSERT INTO files (path, name, type, mime, modified, size, image) SELECT 'config', 'admin_homepage.skriv', 1, 'text/plain', datetime(), LENGTH(content), 0 FROM wiki_as_files WHERE uri = (SELECT value FROM config WHERE key = 'accueil_connexion'); INSERT INTO files_contents (id, compressed, content) SELECT f.id, 0, waf.content FROM files f INNER JOIN wiki_as_files waf ON waf.uri = (SELECT value FROM config WHERE key = 'accueil_connexion') WHERE f.path = 'config' AND f.name = 'admin_homepage.skriv'; -- Rename UPDATE config SET key = 'admin_homepage', value = 'config/admin_homepage.skriv' WHERE key = 'accueil_connexion'; -- Create transaction directories INSERT INTO files (path, name, type) SELECT 'transaction', id, 2 FROM fichiers_acc_transactions GROUP BY id; DELETE FROM plugins_signaux WHERE signal LIKE 'boucle.%'; DROP TABLE wiki_recherche; DROP TABLE wiki_pages; DROP TABLE wiki_revisions; DROP TABLE fichiers_wiki_pages; DROP TABLE fichiers_acc_transactions; DROP TABLE fichiers_membres; DROP TABLE fichiers; DROP TABLE fichiers_contenu; |
Added src/include/data/1.1.0_schema.sql version [e4495d01df].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 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 |
CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY NOT NULL, value TEXT NULL ); CREATE TABLE IF NOT EXISTS users_categories -- Users categories, mainly used to manage rights ( id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, -- Permissions, 0 = no access, 1 = read-only, 2 = read-write, 9 = admin perm_web INTEGER NOT NULL DEFAULT 1, perm_documents INTEGER NOT NULL DEFAULT 1, perm_users INTEGER NOT NULL DEFAULT 1, perm_accounting INTEGER NOT NULL DEFAULT 1, perm_subscribe INTEGER NOT NULL DEFAULT 0, perm_connect INTEGER NOT NULL DEFAULT 1, perm_config INTEGER NOT NULL DEFAULT 0, hidden INTEGER NOT NULL DEFAULT 0 ); -- Membres de l'asso -- Table dynamique générée par l'application -- voir Garradin\Membres\Champs.php CREATE TABLE IF NOT EXISTS membres_sessions -- Sessions ( selecteur TEXT NOT NULL, hash TEXT NOT NULL, id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE, expire INT NOT NULL, PRIMARY KEY (selecteur, id_membre) ); CREATE TABLE IF NOT EXISTS services -- Types de services (cotisations) ( id INTEGER PRIMARY KEY NOT NULL, label TEXT NOT NULL, description TEXT NULL, duration INTEGER NULL CHECK (duration IS NULL OR duration > 0), -- En jours start_date TEXT NULL CHECK (start_date IS NULL OR date(start_date) = start_date), end_date TEXT NULL CHECK (end_date IS NULL OR (date(end_date) = end_date AND date(end_date) >= date(start_date))) ); CREATE TABLE IF NOT EXISTS services_fees ( id INTEGER PRIMARY KEY NOT NULL, label TEXT NOT NULL, description TEXT NULL, amount INTEGER NULL, formula TEXT NULL, -- Formule de calcul du montant de la cotisation, si cotisation dynamique (exemple : membres.revenu_imposable * 0.01) id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE, id_account INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL CHECK (id_account IS NULL OR id_year IS NOT NULL), -- NULL si le type n'est pas associé automatiquement à la compta id_year INTEGER NULL REFERENCES acc_years (id) ON DELETE SET NULL -- NULL si le type n'est pas associé automatiquement à la compta ); CREATE TABLE IF NOT EXISTS services_users -- Enregistrement des cotisations et activités ( id INTEGER NOT NULL PRIMARY KEY, id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE, id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE, id_fee INTEGER NULL REFERENCES services_fees (id) ON DELETE CASCADE, paid INTEGER NOT NULL DEFAULT 0, expected_amount INTEGER NULL, date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date), expiry_date TEXT NULL CHECK (date(expiry_date) IS NULL OR date(expiry_date) = expiry_date) ); CREATE UNIQUE INDEX IF NOT EXISTS su_unique ON services_users (id_user, id_service, date); CREATE INDEX IF NOT EXISTS su_service ON services_users (id_service); CREATE INDEX IF NOT EXISTS su_fee ON services_users (id_fee); CREATE INDEX IF NOT EXISTS su_paid ON services_users (paid); CREATE INDEX IF NOT EXISTS su_expiry ON services_users (expiry_date); CREATE TABLE IF NOT EXISTS services_reminders -- Rappels de devoir renouveller une cotisation ( id INTEGER NOT NULL PRIMARY KEY, id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE, delay INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel subject TEXT NOT NULL, body TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS services_reminders_sent -- Enregistrement des rappels envoyés à qui et quand ( id INTEGER NOT NULL PRIMARY KEY, id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE, id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE, id_reminder INTEGER NOT NULL REFERENCES services_reminders (id) ON DELETE CASCADE, date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date) ); CREATE UNIQUE INDEX IF NOT EXISTS srs_index ON services_reminders_sent (id_user, id_service, id_reminder, date); CREATE INDEX IF NOT EXISTS srs_reminder ON services_reminders_sent (id_reminder); CREATE INDEX IF NOT EXISTS srs_user ON services_reminders_sent (id_user); -- -- COMPTA -- CREATE TABLE IF NOT EXISTS acc_charts -- Plans comptables : il peut y en avoir plusieurs ( id INTEGER NOT NULL PRIMARY KEY, country TEXT NOT NULL, code TEXT NULL, -- NULL = plan comptable créé par l'utilisateur label TEXT NOT NULL, archived INTEGER NOT NULL DEFAULT 0 -- 1 = archivé, non-modifiable ); CREATE TABLE IF NOT EXISTS acc_accounts -- Comptes des plans comptables ( id INTEGER NOT NULL PRIMARY KEY, id_chart INTEGER NOT NULL REFERENCES acc_charts ON DELETE CASCADE, code TEXT NOT NULL, -- peut contenir des lettres, eg. 53A, 53B, etc. label TEXT NOT NULL, description TEXT NULL, position INTEGER NOT NULL, -- position actif/passif/charge/produit type INTEGER NOT NULL DEFAULT 0, -- Type de compte spécial : banque, caisse, en attente d'encaissement, etc. user INTEGER NOT NULL DEFAULT 1 -- 1 = fait partie du plan comptable original, 0 = a été ajouté par l'utilisateur ); CREATE UNIQUE INDEX IF NOT EXISTS acc_accounts_codes ON acc_accounts (code, id_chart); CREATE INDEX IF NOT EXISTS acc_accounts_type ON acc_accounts (type); CREATE INDEX IF NOT EXISTS acc_accounts_position ON acc_accounts (position); CREATE TABLE IF NOT EXISTS acc_years -- Exercices ( id INTEGER NOT NULL PRIMARY KEY, label TEXT NOT NULL, start_date TEXT NOT NULL CHECK (date(start_date) IS NOT NULL AND date(start_date) = start_date), end_date TEXT NOT NULL CHECK (date(end_date) IS NOT NULL AND date(end_date) = end_date), closed INTEGER NOT NULL DEFAULT 0, id_chart INTEGER NOT NULL REFERENCES acc_charts (id) ); CREATE INDEX IF NOT EXISTS acc_years_closed ON acc_years (closed); CREATE TABLE IF NOT EXISTS acc_transactions -- Opérations comptables ( id INTEGER PRIMARY KEY NOT NULL, type INTEGER NOT NULL DEFAULT 0, -- Type d'écriture, 0 = avancée (normale) status INTEGER NOT NULL DEFAULT 0, -- Statut (bitmask) label TEXT NOT NULL, notes TEXT NULL, reference TEXT NULL, -- N° de pièce comptable date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date), validated INTEGER NOT NULL DEFAULT 0, -- 1 = écriture validée, non modifiable hash TEXT NULL, prev_hash TEXT NULL, id_year INTEGER NOT NULL REFERENCES acc_years(id), id_creator INTEGER NULL REFERENCES membres(id) ON DELETE SET NULL, id_related INTEGER NULL REFERENCES acc_transactions(id) ON DELETE SET NULL -- écriture liée (par ex. remboursement d'une dette) ); CREATE INDEX IF NOT EXISTS acc_transactions_year ON acc_transactions (id_year); CREATE INDEX IF NOT EXISTS acc_transactions_date ON acc_transactions (date); CREATE INDEX IF NOT EXISTS acc_transactions_related ON acc_transactions (id_related); CREATE INDEX IF NOT EXISTS acc_transactions_type ON acc_transactions (type); CREATE INDEX IF NOT EXISTS acc_transactions_status ON acc_transactions (status); CREATE TABLE IF NOT EXISTS acc_transactions_lines -- Lignes d'écritures d'une opération ( id INTEGER PRIMARY KEY NOT NULL, id_transaction INTEGER NOT NULL REFERENCES acc_transactions (id) ON DELETE CASCADE, id_account INTEGER NOT NULL REFERENCES acc_accounts (id), -- N° du compte dans le plan comptable credit INTEGER NOT NULL, debit INTEGER NOT NULL, reference TEXT NULL, -- Référence de paiement, eg. numéro de chèque label TEXT NULL, reconciled INTEGER NOT NULL DEFAULT 0, id_analytical INTEGER NULL REFERENCES acc_accounts(id) ON DELETE SET NULL, CONSTRAINT line_check1 CHECK ((credit * debit) = 0), CONSTRAINT line_check2 CHECK ((credit + debit) > 0) ); CREATE INDEX IF NOT EXISTS acc_transactions_lines_account ON acc_transactions_lines (id_account); CREATE INDEX IF NOT EXISTS acc_transactions_lines_analytical ON acc_transactions_lines (id_analytical); CREATE INDEX IF NOT EXISTS acc_transactions_lines_reconciled ON acc_transactions_lines (reconciled); CREATE TABLE IF NOT EXISTS acc_transactions_users -- Liaison des écritures et des membres ( id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE, id_transaction INTEGER NOT NULL REFERENCES acc_transactions (id) ON DELETE CASCADE, id_service_user INTEGER NULL REFERENCES services_users (id) ON DELETE SET NULL, PRIMARY KEY (id_user, id_transaction) ); CREATE INDEX IF NOT EXISTS acc_transactions_users_service ON acc_transactions_users (id_service_user); CREATE TABLE IF NOT EXISTS plugins ( id TEXT NOT NULL PRIMARY KEY, 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) ); ---------- FILES ---------------- CREATE TABLE IF NOT EXISTS files -- Files metadata ( id INTEGER NOT NULL PRIMARY KEY, path TEXT NOT NULL, name TEXT NOT NULL, -- File name type INTEGER NOT NULL, -- File type, 1 = file, 2 = directory mime TEXT NULL, size INT NULL, modified TEXT NULL CHECK (modified IS NULL OR datetime(modified) = modified), image INT NOT NULL DEFAULT 0, CHECK (type = 2 OR (mime IS NOT NULL AND modified IS NOT NULL AND size IS NOT NULL)) ); -- Unique index as this is used to make up a file path CREATE UNIQUE INDEX IF NOT EXISTS files_unique ON files (path, name); CREATE INDEX IF NOT EXISTS files_modified ON files (modified); CREATE TABLE IF NOT EXISTS files_contents -- Files contents (empty if using another storage backend) ( id INTEGER NOT NULL PRIMARY KEY REFERENCES files(id), compressed INT NOT NULL DEFAULT 0, content BLOB NOT NULL ); CREATE VIRTUAL TABLE IF NOT EXISTS files_search USING fts4 -- Search inside files content ( tokenize=unicode61, -- Available from SQLITE 3.7.13 (2012) id INTEGER PRIMARY KEY NOT NULL, path TEXT NOT NULL, title TEXT NULL, content TEXT NOT NULL -- Text content ); CREATE TABLE IF NOT EXISTS web_pages ( id INTEGER NOT NULL PRIMARY KEY, parent TEXT NULL, -- Parent path, NULL = web root path TEXT NOT NULL, -- Full page directory name name TEXT NOT NULL, -- File name type INTEGER NOT NULL, -- 1 = Category, 2 = Page status TEXT NOT NULL, format TEXT NOT NULL, published TEXT NOT NULL CHECK (datetime(published) = published), modified TEXT NOT NULL CHECK (datetime(modified) = modified), title TEXT NOT NULL, content TEXT NOT NULL ); CREATE UNIQUE INDEX web_pages_path ON web_pages (path); CREATE INDEX web_pages_parent ON web_pages (parent); CREATE TABLE IF NOT EXISTS web_attachments ( id INTEGER NOT NULL PRIMARY KEY, page_id INTEGER NOT NULL REFERENCES web_pages(id), name TEXT NOT NULL, -- File name image INTEGER NOT NULL DEFAULT 0 ); -- FIXME: rename to english 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" type TEXT NOT NULL, -- "json" ou "sql" contenu TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS compromised_passwords_cache -- Cache des hash de mots de passe compromis ( hash TEXT NOT NULL PRIMARY KEY ); CREATE TABLE IF NOT EXISTS compromised_passwords_cache_ranges -- Cache des préfixes de mots de passe compromis ( prefix TEXT NOT NULL PRIMARY KEY, date INTEGER NOT NULL ); |
Modified src/include/data/schema.sql from [bdddcd8200] to [c56886c575].
1 |
1.0.0_schema.sql
|
| |
1 |
1.1.0_schema.sql
|
Modified src/include/init.php from [43f2c1664b] to [52bc1a8101].
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 ... 162 163 164 165 166 167 168 169 170 171 172 173 174 175 ... 320 321 322 323 324 325 326 327 328 329 330 331 332 333 |
} if (file_exists($path)) { require_once $path; } }, true); if (!defined('Garradin\DATA_ROOT')) { define('Garradin\DATA_ROOT', ROOT); } if (!defined('Garradin\WWW_URI')) { try { $uri = \KD2\HTTP::getRootURI(ROOT); } ................................................................................ 'SMTP_PORT' => 587, 'SMTP_SECURITY' => 'STARTTLS', 'ADMIN_URL' => WWW_URL . 'admin/', 'NTP_SERVER' => 'fr.pool.ntp.org', 'ENABLE_AUTOMATIC_BACKUPS' => true, 'ADMIN_COLOR1' => '#9c4f15', 'ADMIN_COLOR2' => '#d98628', ]; foreach ($default_config as $const => $value) { $const = sprintf('Garradin\\%s', $const); if (!defined($const)) ................................................................................ if (in_array('install.php', get_included_files())) { die('Erreur de redirection en boucle : problème de configuration ?'); } Utils::redirect(ADMIN_URL . 'install.php'); } $config = Config::getInstance(); if (version_compare($config->getVersion(), garradin_version(), '<')) { Utils::redirect(ADMIN_URL . 'upgrade.php'); } } |
| > > > > | | > > > | | |
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 ... 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 ... 327 328 329 330 331 332 333 334 335 336 337 338 339 340 |
} if (file_exists($path)) { require_once $path; } }, true); if (!defined('Garradin\DATA_ROOT')) { // Migrate plugins, cache and SQLite to data/ subdirectory (version 1.1) if (!file_exists(ROOT . '/data/association.sqlite') && file_exists(ROOT . '/association.sqlite')) { Upgrade::moveDataRoot(); } define('Garradin\DATA_ROOT', ROOT . '/data'); } if (!defined('Garradin\WWW_URI')) { try { $uri = \KD2\HTTP::getRootURI(ROOT); } ................................................................................ 'SMTP_PORT' => 587, 'SMTP_SECURITY' => 'STARTTLS', 'ADMIN_URL' => WWW_URL . 'admin/', 'NTP_SERVER' => 'fr.pool.ntp.org', 'ENABLE_AUTOMATIC_BACKUPS' => true, 'ADMIN_COLOR1' => '#9c4f15', 'ADMIN_COLOR2' => '#d98628', 'FILE_STORAGE_BACKEND' => null, 'FILE_STORAGE_CONFIG' => null, 'FILE_STORAGE_QUOTA' => null, ]; foreach ($default_config as $const => $value) { $const = sprintf('Garradin\\%s', $const); if (!defined($const)) ................................................................................ if (in_array('install.php', get_included_files())) { die('Erreur de redirection en boucle : problème de configuration ?'); } Utils::redirect(ADMIN_URL . 'install.php'); } $v = DB::getInstance()->version(); if (version_compare($v, garradin_version(), '<')) { Utils::redirect(ADMIN_URL . 'upgrade.php'); } } |
Modified src/include/lib/Garradin/Config.php from [e6ef6cc161] to [e3dead6a86].
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 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 |
<?php namespace Garradin; use KD2\SMTP; class Config { protected $fields_types = null; protected $config = null; protected $modified = []; static protected $_instance = null; /** * Singleton simple * @return Config */ static public function getInstance() { return self::$_instance ?: self::$_instance = new Config; } static public function deleteInstance() { self::$_instance = null; } /** * Empêche de cloner l'objet * @return void */ private function __clone() { } protected function __construct() { // Définition des types de données stockées $string = ''; $int = 0; $float = 0.0; $array = []; $bool = false; $object = new \stdClass; $this->fields_types = [ 'nom_asso' => $string, 'adresse_asso' => $string, 'email_asso' => $string, 'site_asso' => $string, 'monnaie' => $string, 'pays' => $string, 'champs_membres' => $object, 'categorie_membres' => $int, 'accueil_wiki' => $string, 'accueil_connexion' => $string, 'frequence_sauvegardes' => $int, 'nombre_sauvegardes' => $int, 'champ_identifiant' => $string, 'champ_identite' => $string, 'version' => $string, 'last_chart_change' => $int, 'last_version_check' => $string, 'couleur1' => $string, 'couleur2' => $string, 'image_fond' => $string, 'desactiver_site' => $bool, ]; $db = DB::getInstance(); $this->config = $db->getAssoc('SELECT cle, valeur FROM config ORDER BY cle;'); foreach ($this->config as $key=>&$value) { if (!array_key_exists($key, $this->fields_types)) { // Ancienne clé de config qui n'est plus utilisée continue; } if (is_array($this->fields_types[$key])) { $value = explode(',', $value); } elseif ($key == 'champs_membres') { $value = new Membres\Champs((string)$value); } else { settype($value, gettype($this->fields_types[$key])); } } } public function __destruct() { if (!empty($this->modified)) { $this->save(); } } public function save() { if (empty($this->modified)) return true; $values = []; $db = DB::getInstance(); // Image files if (isset($this->modified['image_fond'])) { $key = 'image_fond'; $value =& $this->config[$key]; if ($current = $db->firstColumn('SELECT valeur FROM config WHERE cle = ?;', $key)) { try { $f = new Fichiers($current); $f->remove(); } catch (\InvalidArgumentException $e) { // Ignore: the file has already been deleted } } if (strlen($value) > 0) { $content = $value; $value = null; $f = Fichiers::storeFromBase64($key . '.png', $content); $value = $f->id; unset($f); } } unset($value, $key); $db->begin(); foreach ($this->modified as $key=>$modified) { $value = $this->config[$key]; if (is_array($value)) { $value = implode(',', $value); } elseif (is_object($value)) { $value = (string) $value; } $db->preparedQuery('INSERT OR REPLACE INTO config (cle, valeur) VALUES (?, ?);', [$key, $value]); } if (!empty($this->modified['champ_identifiant'])) { // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique $db->exec('UPDATE membres SET '.$this->get('champ_identifiant').' = NULL WHERE '.$this->get('champ_identifiant').' = "";'); // Création de l'index unique $db->exec('DROP INDEX IF EXISTS membres_identifiant;'); $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$this->get('champ_identifiant').');'); } $db->commit(); $this->modified = []; return true; } public function get($key) { if (!array_key_exists($key, $this->fields_types)) { throw new \OutOfBoundsException('Ce champ est inconnu.'); } if (!array_key_exists($key, $this->config)) { return null; } return $this->config[$key]; } public function getVersion() { if (!array_key_exists('version', $this->config)) { return '0'; } return $this->config['version']; } public function setVersion($version) { $this->config['version'] = $version; $db = DB::getInstance(); $db->preparedQuery('INSERT OR REPLACE INTO config (cle, valeur) VALUES (?, ?);', ['version', $version]); return true; } public function set($key, $value) { if (!array_key_exists($key, $this->fields_types)) { throw new \OutOfBoundsException('Ce champ est inconnu.'); } if (is_array($this->fields_types[$key])) { $value = !empty($value) ? (array) $value : []; } elseif (is_int($this->fields_types[$key])) { $value = (int) $value; } elseif (is_float($this->fields_types[$key])) { $value = (float) $value; } elseif (is_bool($this->fields_types[$key])) { $value = (bool) $value; } elseif (is_string($this->fields_types[$key])) { $value = (string) $value; } switch ($key) { case 'nom_asso': { if (!trim($value)) { throw new UserException('Le nom de l\'association ne peut rester vide.'); } break; } case 'accueil_wiki': case 'accueil_connexion': { $value = trim($value); $name = str_replace('accueil_', '', $key); if ($value === '') { throw new UserException(sprintf('Le nom de la page d\'accueil %s ne peut rester vide.', $name)); } $db = DB::getInstance(); if (!$db->test('wiki_pages', $db->where('uri', $value))) { throw new UserException(sprintf('Le nom de la page d\'accueil %s ne correspond à aucune page existante, merci de la créer auparavant.', $name)); } break; } case 'email_asso': { if (!SMTP::checkEmailIsValid($value, false)) { throw new UserException('Adresse e-mail invalide.'); } break; } case 'champs_membres': { if (!($value instanceOf Membres\Champs)) { throw new \UnexpectedValueException('$value doit être de type Membres\Champs'); } break; } case 'champ_identite': case 'champ_identifiant': { $champs = $this->get('champs_membres'); $db = DB::getInstance(); // Vérification que le champ existe bien if (!$champs->get($value)) { throw new UserException('Le champ '.$value.' n\'existe pas pour la configuration de '.$key); } // Vérification que le champ est unique pour l'identifiant if ($key == 'champ_identifiant' && !$db->firstColumn('SELECT (COUNT(DISTINCT lower('.$value.')) = COUNT(*)) FROM membres WHERE '.$value.' IS NOT NULL AND '.$value.' != \'\';')) { throw new UserException('Le champ '.$value.' comporte des doublons et ne peut donc pas servir comme identifiant pour la connexion.'); } break; } case 'categorie_membres': { $db = DB::getInstance(); if (!$db->firstColumn('SELECT 1 FROM membres_categories WHERE id = ?;', $value)) { throw new UserException('La catégorie de membres par défaut numéro \''.$value.'\' ne semble pas exister.'); } break; } case 'monnaie': { if (!trim($value)) { throw new UserException('La monnaie doit être renseignée.'); } break; } case 'pays': { if (!trim($value) || !Utils::getCountryName($value)) { throw new UserException('Le pays renseigné est invalide.'); } break; } default: break; } if (!isset($this->config[$key]) || $value !== $this->config[$key]) { $this->config[$key] = $value; $this->modified[$key] = true; } return true; } public function getFieldsTypes() { return $this->fields_types; } public function getConfig() { return $this->config; } } |
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 |
<?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Membres\Champs; use KD2\SMTP; class Config extends Entity { protected $nom_asso; protected $adresse_asso; protected $email_asso; protected $telephone_asso; protected $site_asso; protected $monnaie; protected $pays; protected $champs_membres; protected $categorie_membres; protected $admin_homepage; protected $frequence_sauvegardes; protected $nombre_sauvegardes; protected $champ_identifiant; protected $champ_identite; protected $last_chart_change; protected $last_version_check; protected $couleur1; protected $couleur2; protected $admin_background; protected $desactiver_site; protected $_types = [ 'nom_asso' => 'string', 'adresse_asso' => '?string', 'email_asso' => 'string', 'telephone_asso' => '?string', 'site_asso' => '?string', 'monnaie' => 'string', 'pays' => 'string', 'champs_membres' => Champs::class, 'categorie_membres' => 'int', 'admin_homepage' => '?string', 'frequence_sauvegardes' => '?int', 'nombre_sauvegardes' => '?int', 'champ_identifiant' => 'string', 'champ_identite' => 'string', 'last_chart_change' => '?int', 'last_version_check' => '?string', 'couleur1' => '?string', 'couleur2' => '?string', 'admin_background' => '?string', 'desactiver_site' => 'bool', ]; protected $_special_types = [ 'admin_homepage' => 'Garradin\Entities\Files\File', 'admin_background' => 'Garradin\Entities\Files\File', ]; protected $_deleted_files = []; static protected $_instance = null; static public function getInstance() { return self::$_instance ?: self::$_instance = new self; } static public function deleteInstance() { self::$_instance = null; } public function __clone() { throw new \LogicException('Cannot clone config'); } protected function __construct() { parent::__construct(); $db = DB::getInstance(); $config = $db->getAssoc('SELECT key, value FROM config ORDER BY key;'); if (empty($config)) { return; } $default = array_fill_keys(array_keys($this->_types), null); $config = array_merge($default, $config); $config['champs_membres'] = new Champs($config['champs_membres']); foreach ($this->_types as $key => $type) { $value = $config[$key]; if ($type[0] == '?' && $value === null) { continue; } } $this->load($config); $this->champs_membres = new Membres\Champs((string)$this->champs_membres); } public function get(string $key) { $type = $this->_special_types[$key] ?? null; if ($type == File::class && is_string($this->$key)) { return Files::get($this->$key); } return parent::get($key); } public function set(string $key, $value, bool $loose = false, bool $check_for_changes = true) { $type = $this->_special_types[$key] ?? null; // Append to deleted files queue if it's set to null if ($type == File::class && null === $value && null !== $this->$key) { $this->_deleted_files[] = $this->$key; } parent::set($key, $value, $loose, $check_for_changes); } public function save(): bool { if (!count($this->_modified)) { return true; } $this->selfCheck(); $values = []; $db = DB::getInstance(); foreach ($this->_modified as $key => $modified) { $value = $this->$key; $type = ltrim($this->_types[$key], '?'); if ($type == Champs::class) { $value = $value->toString(); } elseif (is_object($value)) { throw new \UnexpectedValueException('Unexpected object as value: ' . get_class($value)); } $values[$key] = $value; } unset($value, $key, $modified); $db->begin(); foreach ($values as $key => $value) { $db->preparedQuery('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?);', $key, $value); } if (!empty($values['champ_identifiant'])) { // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique $db->exec('UPDATE membres SET '.$this->get('champ_identifiant').' = NULL WHERE '.$this->get('champ_identifiant').' = "";'); // Création de l'index unique $db->exec('DROP INDEX IF EXISTS membres_identifiant;'); $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$this->get('champ_identifiant').');'); } foreach ($this->_deleted_files as $file) { $file->delete(); } $db->commit(); $this->_modified = []; return true; } public function delete(): bool { throw new \LogicException('Cannot delete config'); } public function importForm($source = null): void { if (null === $source) { $source = $_POST; } // N'enregistrer les couleurs que si ce ne sont pas les couleurs par défaut if (!isset($source['couleur1'], $source['couleur2']) || ($source['couleur1'] == ADMIN_COLOR1 && $source['couleur2'] == ADMIN_COLOR2)) { $source['couleur1'] = null; $source['couleur2'] = null; } if (isset($source['admin_background']) && trim($source['admin_background']) == 'RESET') { $source['admin_background'] = null; } elseif (isset($source['admin_background']) && strlen($source['admin_background'])) { if ($this->admin_background) { $this->admin_background->storeFromBase64($source['admin_background']); $this->admin_background->save(); } else { $file = File::createFromBase64(File::CONTEXT_CONFIG, 'admin_background.png', $source['admin_background']); $this->set('admin_background', $file->pathname()); } unset($source['admin_background']); } parent::importForm($source); } protected function _filterType(string $key, $value) { switch ($this->_types[$key]) { case 'int': return (int) $value; case 'bool': return (bool) $value; case 'string': return (string) $value; case Champs::class: if (!is_object($value) || !($value instanceof $this->_types[$key])) { throw new \InvalidArgumentException(sprintf('"%s" is not of type "%s"', $key, $this->_types[$key])); } return $value; default: throw new \InvalidArgumentException(sprintf('"%s" has unknown type "%s"', $key, $this->_types[$key])); } } public function selfCheck(): void { $this->assert(trim($this->nom_asso) != '', 'Le nom de l\'association ne peut rester vide.'); $this->assert(trim($this->monnaie) != '', 'La monnaie ne peut rester vide.'); $this->assert(trim($this->pays) != '' && Utils::getCountryName($this->pays), 'Le pays ne peut rester vide.'); $this->assert(null === $this->site_asso || filter_var($this->site_asso, FILTER_VALIDATE_URL), 'L\'adresse URL du site web est invalide.'); $this->assert(trim($this->email_asso) != '' && SMTP::checkEmailIsValid($this->email_asso, false), 'L\'adresse e-mail de l\'association est invalide.'); $this->assert(null === $this->admin_homepage || $this->admin_homepage instanceof File, 'Page d\'accueil invalide'); $this->assert($this->champs_membres instanceof Champs, 'Objet champs membres invalide'); $champs = $this->champs_membres; $this->assert(!empty($champs->get($this->champ_identite)), sprintf('Le champ spécifié pour identité, "%s" n\'existe pas', $this->champ_identite)); $this->assert(!empty($champs->get($this->champ_identifiant)), sprintf('Le champ spécifié pour identifiant, "%s" n\'existe pas', $this->champ_identifiant)); $db = DB::getInstance(); $sql = sprintf('SELECT (COUNT(DISTINCT %s) = COUNT(*)) FROM membres WHERE %1$s IS NOT NULL AND %1$s != \'\';', $this->champ_identifiant); $is_unique = $db->firstColumn($sql); $this->assert($is_unique, sprintf('Le champ "%s" comporte des doublons et ne peut donc pas servir comme identifiant unique de connexion.', $this->champ_identifiant)); $this->assert($db->test('users_categories', 'id = ?', $this->categorie_membres), 'Catégorie de membres inconnue'); } public function getConfig() { return $this->asArray(); } } |
Modified src/include/lib/Garradin/DB.php from [dad60ad267] to [0fbc5c09f2].
9 10 11 12 13 14 15 16 17 18 19 20 21 22 .. 45 46 47 48 49 50 51 52 53 54 55 56 57 58 .. 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
/** * Application ID pour SQLite * @link https://www.sqlite.org/pragma.html#pragma_application_id */ const APPID = 0x5da2d811; static protected $_instance = null; static public function getInstance($create = false, $readonly = false) { if (null === self::$_instance) { self::$_instance = new DB('sqlite', ['file' => DB_FILE]); } ................................................................................ // Performance enhancement // see https://www.cs.utexas.edu/~jaya/slides/apsys17-sqlite-slides.pdf // https://ericdraken.com/sqlite-performance-testing/ $this->exec(sprintf('PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA journal_size_limit = %d;', 32 * 1024 * 1024)); $this->db->createFunction('transliterate_to_ascii', ['Garradin\Utils', 'transliterateToAscii']); } public function close(): void { parent::close(); self::$_instance = null; } ................................................................................ } public function commitSchemaUpdate() { $this->commit(); $this->toggleForeignKeys(true); } /** * @see https://www.sqlite.org/lang_altertable.html */ public function toggleForeignKeys($enable) { assert(is_bool($enable)); |
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 .. 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 ... 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
/** * Application ID pour SQLite * @link https://www.sqlite.org/pragma.html#pragma_application_id */ const APPID = 0x5da2d811; static protected $_instance = null; protected $_version = -1; static public function getInstance($create = false, $readonly = false) { if (null === self::$_instance) { self::$_instance = new DB('sqlite', ['file' => DB_FILE]); } ................................................................................ // Performance enhancement // see https://www.cs.utexas.edu/~jaya/slides/apsys17-sqlite-slides.pdf // https://ericdraken.com/sqlite-performance-testing/ $this->exec(sprintf('PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA journal_size_limit = %d;', 32 * 1024 * 1024)); $this->db->createFunction('transliterate_to_ascii', ['Garradin\Utils', 'transliterateToAscii']); } public function version(): ?string { if (-1 === $this->_version) { $this->connect(); $v = (int) $this->db->querySingle('PRAGMA user_version;'); $v = self::parseVersion($v); if (null === $v) { // For legacy version before 1.1.0 $v = $this->db->querySingle('SELECT valeur FROM config WHERE cle = \'version\';'); } $this->_version = $v ?: null; } return $this->_version; } static public function parseVersion(int $v): ?string { if ($v > 0) { $major = intval($v / 1000000); $v -= $major * 1000000; $minor = intval($v / 10000); $v -= $minor * 10000; $release = intval($v / 100); $v -= $release * 100; $type = $v; if ($type == 0) { $type = ''; } // Corrective release: 1.2.3.1 elseif ($type > 75) { $type = '.' . ($type - 75); } // RC release elseif ($type > 50) { $type = '-rc' . ($type - 50); } // Beta elseif ($type > 25) { $type = '-beta' . ($type - 25); } // Alpha else { $type = '-alpha' . $type; } $v = sprintf('%d.%d.%d%s', $major, $minor, $release, $type); } return $v ?: null; } /** * Save version to database * rc, alpha, beta and corrective release (4th number) are limited to 24 versions each * @param string $version Version string, eg. 1.2.3-rc2 */ public function setVersion(string $version): void { if (!preg_match('/^(\d+)\.(\d+)\.(\d+)(?:(?:-(alpha|beta|rc)|\.)(\d+)|)?$/', $version, $match)) { throw new \InvalidArgumentException('Invalid version number: ' . $version); } $version = ($match[1] * 100 * 100 * 100) + ($match[2] * 100 * 100) + ($match[3] * 100); if (isset($match[5])) { if ($match[5] > 24) { throw new \InvalidArgumentException('Invalid version number: cannot have a 4th component larger than 24: ' . $version); } if ($match[4] == 'rc') { $version += $match[5] + 50; } elseif ($match[4] == 'beta') { $version += $match[5] + 25; } elseif ($match[4] == 'alpha') { $version += $match[5]; } else { $version += $match[5] + 75; } } $this->db->exec(sprintf('PRAGMA user_version = %d;', $version)); } public function close(): void { parent::close(); self::$_instance = null; } ................................................................................ } public function commitSchemaUpdate() { $this->commit(); $this->toggleForeignKeys(true); } public function lastErrorMsg() { return $this->db->lastErrorMsg(); } /** * @see https://www.sqlite.org/lang_altertable.html */ public function toggleForeignKeys($enable) { assert(is_bool($enable)); |
Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [cabf196b95] to [e7a7ef2155].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ... 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 ... 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 ... 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 |
<?php namespace Garradin\Entities\Accounting; use KD2\DB\EntityManager; use Garradin\Entity; use Garradin\Fichiers; use Garradin\Accounting\Accounts; use Garradin\ValidationException; use Garradin\DB; use Garradin\Config; use Garradin\Utils; use Garradin\UserException; class Transaction extends Entity { const TABLE = 'acc_transactions'; const TYPE_ADVANCED = 0; const TYPE_REVENUE = 1; const TYPE_EXPENSE = 2; ................................................................................ $db = DB::getInstance(); if ($db->test(Year::TABLE, 'id = ? AND closed = 1', $this->id_year)) { throw new ValidationException('Il n\'est pas possible de supprimer une écriture qui fait partie d\'un exercice clôturé'); } Fichiers::deleteLinkedFiles(Fichiers::LIEN_COMPTA, $this->id()); return parent::delete(); } public function selfCheck(): void { parent::selfCheck(); ................................................................................ else { $details = self::getTypesDetails(); if (!array_key_exists($type, $details)) { throw new ValidationException('Type d\'écriture inconnu'); } if (!$this->exists() && ($type == self::TYPE_DEBT || $type == self::TYPE_CREDIT)) { $this->addStatus(self::STATUS_WAITING); } if (empty($source['amount'])) { throw new UserException('Montant non précisé'); } $amount = $source['amount']; ................................................................................ public function year() { return EntityManager::findOneById(Year::class, $this->id_year); } public function listFiles() { return Fichiers::listLinkedFiles(Fichiers::LIEN_COMPTA, $this->id()); } public function linkToUser(int $user_id, ?int $service_id = null) { $db = EntityManager::getInstance(self::class)->DB(); return $db->preparedQuery('INSERT OR IGNORE INTO acc_transactions_users (id_transaction, id_user, id_service_user) VALUES (?, ?, ?);', |
> < < < > > > > > > | | > > > | > > > > > |
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 ... 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 ... 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 ... 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 |
<?php namespace Garradin\Entities\Accounting; use KD2\DB\EntityManager; use Garradin\Entity; use Garradin\DB; use Garradin\Config; use Garradin\Utils; use Garradin\UserException; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Accounting\Accounts; use Garradin\ValidationException; class Transaction extends Entity { const TABLE = 'acc_transactions'; const TYPE_ADVANCED = 0; const TYPE_REVENUE = 1; const TYPE_EXPENSE = 2; ................................................................................ $db = DB::getInstance(); if ($db->test(Year::TABLE, 'id = ? AND closed = 1', $this->id_year)) { throw new ValidationException('Il n\'est pas possible de supprimer une écriture qui fait partie d\'un exercice clôturé'); } Files::deleteLinkedFiles(File::CONTEXT_TRANSACTION, $this->id()); return parent::delete(); } public function selfCheck(): void { parent::selfCheck(); ................................................................................ else { $details = self::getTypesDetails(); if (!array_key_exists($type, $details)) { throw new ValidationException('Type d\'écriture inconnu'); } if (empty($this->_related) && ($type == self::TYPE_DEBT || $type == self::TYPE_CREDIT)) { $this->addStatus(self::STATUS_WAITING); } else { $this->removeStatus(self::STATUS_WAITING); } if (empty($source['amount'])) { throw new UserException('Montant non précisé'); } $amount = $source['amount']; ................................................................................ public function year() { return EntityManager::findOneById(Year::class, $this->id_year); } public function listFiles() { return Files::list($this->getAttachementsDirectory()); } public function getAttachementsDirectory(): string { return File::CONTEXT_TRANSACTION . '/' . $this->id(); } public function linkToUser(int $user_id, ?int $service_id = null) { $db = EntityManager::getInstance(self::class)->DB(); return $db->preparedQuery('INSERT OR IGNORE INTO acc_transactions_users (id_transaction, id_user, id_service_user) VALUES (?, ?, ?);', |
Modified src/include/lib/Garradin/Entities/Accounting/Year.php from [82f91d0f84] to [42cf3b1884].
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
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
|
<?php namespace Garradin\Entities\Accounting; use KD2\DB\EntityManager; use Garradin\DB; use Garradin\Entity; use Garradin\Fichiers; use Garradin\UserException; use Garradin\Accounting\Accounts; class Year extends Entity { const TABLE = 'acc_years'; protected $id; protected $label; ................................................................................ throw new \LogicException('Cet exercice est déjà clôturé'); } if ($target->closed) { throw new \LogicException('L\'exercice cible est déjà clôturé'); } if ($target->id_chart != $this->id_chart) { throw new UserException('Il n\'est pas possible de déplacer les écritures dans un exercices dont le plan comptable est différent'); } DB::getInstance()->preparedQuery('UPDATE acc_transactions SET id_year = ? WHERE id_year = ? AND date > ?;', $target->id(), $this->id(), $date->format('Y-m-d')); } public function delete(): bool { // Manual delete of transactions, as there is a voluntary safeguard in SQL: no cascade DB::getInstance()->preparedQuery('DELETE FROM acc_transactions WHERE id_year = ?;', $this->id()); // Clean up files Fichiers::deleteUnlinkedFiles(); return parent::delete(); } public function countTransactions(): int { $db = DB::getInstance(); |
<
>
<
<
<
<
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
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
|
<?php namespace Garradin\Entities\Accounting; use KD2\DB\EntityManager; use Garradin\DB; use Garradin\Entity; use Garradin\UserException; use Garradin\Accounting\Accounts; use Garradin\Files\Files; class Year extends Entity { const TABLE = 'acc_years'; protected $id; protected $label; ................................................................................ throw new \LogicException('Cet exercice est déjà clôturé'); } if ($target->closed) { throw new \LogicException('L\'exercice cible est déjà clôturé'); } DB::getInstance()->preparedQuery('UPDATE acc_transactions SET id_year = ? WHERE id_year = ? AND date > ?;', $target->id(), $this->id(), $date->format('Y-m-d')); } public function delete(): bool { // Manual delete of transactions, as there is a voluntary safeguard in SQL: no cascade DB::getInstance()->preparedQuery('DELETE FROM acc_transactions WHERE id_year = ?;', $this->id()); // Clean up files Files::deleteUnlinkedFiles(); return parent::delete(); } public function countTransactions(): int { $db = DB::getInstance(); |
Added src/include/lib/Garradin/Entities/Files/File.php version [0129e9f614].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 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 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 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 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 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 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 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 |
<?php namespace Garradin\Entities\Files; use KD2\Graphics\Image; use KD2\DB\EntityManager as EM; use Garradin\DB; use Garradin\Entity; use Garradin\UserException; use Garradin\ValidationException; use Garradin\Membres\Session; use Garradin\Static_Cache; use Garradin\Utils; use Garradin\Entities\Web\Page; use Garradin\Files\Files; use const Garradin\{WWW_URL, ENABLE_XSENDFILE}; /** * This is a virtual entity, it cannot be saved to a SQL table */ class File extends Entity { const TABLE = 'files'; protected $id; /** * Parent directory of file */ protected $path; /** * File name */ protected $name; protected $type = self::TYPE_FILE; protected $mime; protected $size; protected $modified; protected $image; protected $_types = [ 'id' => 'int', 'path' => 'string', 'name' => 'string', 'type' => 'int', 'mime' => '?string', 'size' => '?int', 'modified' => '?DateTime', 'image' => 'int', ]; const TYPE_FILE = 1; const TYPE_DIRECTORY = 2; const TYPE_LINK = 3; /** * Tailles de miniatures autorisées, pour ne pas avoir 500 fichiers générés avec 500 tailles différentes * @var array */ const ALLOWED_THUMB_SIZES = [200, 500]; const THUMB_CACHE_ID = 'file.thumb.%s.%d'; const THUMB_SIZE_TINY = 200; const THUMB_SIZE_SMALL = 500; const FILE_EXT_ENCRYPTED = '.skriv.enc'; const FILE_EXT_SKRIV = '.skriv'; const EDITOR_WEB = 'web'; const EDITOR_ENCRYPTED = 'encrypted'; const EDITOR_CODE = 'code'; const CONTEXT_DOCUMENTS = 'documents'; const CONTEXT_USER = 'user'; const CONTEXT_TRANSACTION = 'transaction'; const CONTEXT_CONFIG = 'config'; const CONTEXT_WEB = 'web'; const CONTEXT_SKELETON = 'skel'; const CONTEXTS_NAMES = [ self::CONTEXT_DOCUMENTS => 'Documents', self::CONTEXT_USER => 'Membre', self::CONTEXT_TRANSACTION => 'Écriture comptable', self::CONTEXT_CONFIG => 'Configuration', self::CONTEXT_WEB => 'Site web', self::CONTEXT_SKELETON => 'Squelettes', ]; const IMAGE_TYPES = [ 'image/png', 'image/gif', 'image/jpeg', ]; const PREVIEW_TYPES = [ 'application/pdf', 'audio/mpeg', 'audio/ogg', 'audio/wave', 'audio/wav', 'audio/x-wav', 'audio/x-pn-wav', 'audio/webm', 'video/webm', 'video/ogg', 'application/ogg', 'video/mp4', 'image/png', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml', 'text/plain', 'text/html', ]; static public function getColumns(): array { return array_keys((new self)->_types); } public function pathname(): string { $path = $this->path; if ($path) { $path .= '/'; } return $path . $this->name; } public function context(): string { return strtok($this->path, '/'); } public function canPreview(): bool { return in_array($this->mime, self::PREVIEW_TYPES); } public function delete(): bool { Files::callStorage('checkLock'); // Delete recursively if ($this->type == self::TYPE_DIRECTORY) { foreach (Files::list($this->path()) as $file) { $file->delete(); } } // Delete actual file content $return = Files::callStorage('delete', $this); // clean up thumbnails foreach (self::ALLOWED_THUMB_SIZES as $size) { Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $size)); } return parent::delete(); } public function rename(string $new_path): bool { self::validatePath($new_path); $current_path = $this->path(); $return = Files::callStorage('move', $this, $new_path); $this->set('path', dirname($new_path)); $this->set('name', basename($new_path)); $this->save(); if ($this->type == self::TYPE_DIRECTORY) { // Move sub-directories and sub-files DB::getInstance()->preparedQuery('UPDATE files SET path = ? WHERE path = ?;', $new_path, $current_path); } return $return; } public function setContent(string $content): self { return $this->store(null, rtrim($content)); } protected function store(string $source_path = null, $source_content = null): self { if ($source_path && !$source_content) { $this->set('size', filesize($source_path)); } else { $this->set('size', strlen($source_content)); } // Check that it's a real image if ($this->image) { try { if ($source_path && !$source_content) { $i = new Image($source_path); } else { $i = Image::createFromBlob($source_content); } // Recompress PNG files from base64, assuming they are coming // from JS canvas which doesn't know how to gzip (d'oh!) if ($i->format() == 'png' && null !== $source_content) { $source_content = $i->output('png', true); $this->set('size', strlen($source_content)); } unset($i); } catch (\RuntimeException $e) { if (strstr($e->getMessage(), 'No suitable image library found')) { throw new \RuntimeException('Le serveur n\'a aucune bibliothèque de gestion d\'image installée, et ne peut donc pas accepter les images. Installez Imagick ou GD.'); } throw new UserException('Fichier image invalide'); } } Files::callStorage('checkLock'); // If a file of the same name already exists, define a new name if (Files::callStorage('exists', $this->pathname()) && !$this->exists()) { $pos = strrpos($this->name, '.'); $new_name = substr($this->name, 0, $pos) . '.' . substr(sha1(random_bytes(16)), 0, 10) . substr($this->name, $pos); $this->set('name', $new_name); } if (!$this->exists()) { $this->set('modified', new \DateTime); $this->save(); } if (null !== $source_path) { $return = Files::callStorage('storePath', $this, $source_path); } else { $return = Files::callStorage('storeContent', $this, $source_content); } if (!$return) { throw new UserException('Le fichier n\'a pas pu être enregistré.'); } // Store content in search table if (substr($this->mime, 0, 5) == 'text/') { $content = $source_content !== null ? $source_content : Files::callStorage('fetch', $this->path()); if ($this->customType() == self::FILE_EXT_ENCRYPTED) { $content = 'Contenu chiffré'; } else if ($this->mime === 'text/html' || $this->mime == 'text/xml') { $content = strip_tags($content); } // Only index contents up to 150KB and valid UTF-8 if (strlen($content) <= 150*1024 && preg_match('//u', $content)) { DB::getInstance()->preparedQuery('INSERT OR REPLACE INTO files_search (path, content) VALUES (?, ?);', $this->path(), $content); } } return $this; } static public function createAndStore(string $path, string $name, string $source_path = null, string $source_content = null): self { $file = self::create($path, $name, $source_path, $source_content); $file->store($source_path, $source_content); return $file; } static public function createDirectory(string $path, string $name): self { $file = new self; $file->set('name', $name); $file->set('path', $path); $file->set('type', self::TYPE_DIRECTORY); $file->set('image', 0); $file->save(); Files::callStorage('mkdir', $file); return $file; } static public function create(string $path, string $name, string $source_path = null, string $source_content = null): self { if (isset($source_path, $source_content)) { throw new \InvalidArgumentException('Either source path or source content should be set but not both'); } $finfo = \finfo_open(\FILEINFO_MIME_TYPE); $file = new self; $file->set('name', $name); $file->set('path', $path); $db = DB::getInstance(); if ($source_path && !$source_content) { $file->set('mime', finfo_file($finfo, $source_path)); } else { $file->set('mime', finfo_buffer($finfo, $source_content)); } $file->set('image', (int) in_array($file->mime, self::IMAGE_TYPES)); return $file; } /** * Upload de fichier à partir d'une chaîne en base64 * @param string $name * @param string $content * @return File */ static public function createFromBase64(string $path, string $name, string $encoded_content): self { $content = base64_decode($encoded_content); return self::createAndStore($path, $name, null, $content); } public function storeFromBase64(string $encoded_content): self { $content = base64_decode($encoded_content); return $this->store(null, $content); } /** * Upload du fichier par POST */ static public function upload(string $path, string $key): self { if (!isset($_FILES[$key]) || !is_array($_FILES[$key])) { throw new UserException('Aucun fichier reçu'); } $file = $_FILES[$key]; if (!empty($file['error'])) { throw new UserException(self::getErrorMessage($file['error'])); } if (empty($file['size']) || empty($file['name'])) { throw new UserException('Fichier reçu invalide : vide ou sans nom de fichier.'); } if (!is_uploaded_file($file['tmp_name'])) { throw new \RuntimeException('Le fichier n\'a pas été envoyé de manière conventionnelle.'); } $name = preg_replace('/\s+/', '_', $file['name']); $name = preg_replace('/[^\d\w._-]/ui', '', $name); return self::createAndStore($path, strtolower($name), $file['tmp_name']); } /** * Récupération du message d'erreur * @param integer $error Code erreur du $_FILE * @return string Message d'erreur */ static public function getErrorMessage($error) { switch ($error) { case UPLOAD_ERR_INI_SIZE: return 'Le fichier excède la taille permise par la configuration du serveur.'; case UPLOAD_ERR_FORM_SIZE: return 'Le fichier excède la taille permise par le formulaire.'; case UPLOAD_ERR_PARTIAL: return 'L\'envoi du fichier a été interrompu.'; case UPLOAD_ERR_NO_FILE: return 'Aucun fichier n\'a été reçu.'; case UPLOAD_ERR_NO_TMP_DIR: return 'Pas de répertoire temporaire pour stocker le fichier.'; case UPLOAD_ERR_CANT_WRITE: return 'Impossible d\'écrire le fichier sur le disque du serveur.'; case UPLOAD_ERR_EXTENSION: return 'Une extension du serveur a interrompu l\'envoi du fichier.'; default: return 'Erreur inconnue: ' . $error; } } public function url($download = false): string { if ($this->context() == self::CONTEXT_WEB) { $path = substr($this->path, strlen(self::CONTEXT_WEB . '/')); } else { $path = $this->path; } $url = WWW_URL . $path . '/' . $this->name; if ($download) { $url .= '?download'; } return $url; } public function thumb_url(?int $size = null): string { $size = $size ? self::_findNearestThumbSize($size) : min(self::ALLOWED_THUMB_SIZES); return sprintf('%s?%dpx', $this->url(), $size); } /** * Renvoie la taille de miniature la plus proche de la taille demandée * @param integer $size Taille demandée * @return integer Taille possible */ static protected function _findNearestThumbSize($size) { $size = (int) $size; if (in_array($size, self::ALLOWED_THUMB_SIZES)) { return $size; } foreach (self::ALLOWED_THUMB_SIZES as $s) { if ($s >= $size) { return $s; } } return max(self::ALLOWED_THUMB_SIZES); } /** * Envoie le fichier au client HTTP */ public function serve(?Session $session = null, bool $download = false): void { if (!$this->checkReadAccess($session)) { header('HTTP/1.1 403 Forbidden', true, 403); throw new UserException('Vous n\'avez pas accès à ce fichier.'); return; } $path = Files::callStorage('getFullPath', $this); $content = null === $path ? Files::callStorage('fetch', $this) : null; $this->_serve($path, $content, $download); } /** * Envoie une miniature à la taille indiquée au client HTTP */ public function serveThumbnail(?Session $session = null, ?int $width = null): void { if (!$this->checkReadAccess($session)) { header('HTTP/1.1 403 Forbidden', true, 403); throw new UserException('Accès interdit'); return; } if (!$this->image) { throw new UserException('Il n\'est pas possible de fournir une miniature pour un fichier qui n\'est pas une image.'); } if (!$width) { $width = reset(self::ALLOWED_THUMB_SIZES); } if (!in_array($width, self::ALLOWED_THUMB_SIZES)) { throw new UserException('Cette taille de miniature n\'est pas autorisée.'); } $cache_id = sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $width); $destination = Static_Cache::getPath($cache_id); // La miniature n'existe pas dans le cache statique, on la crée if (!Static_Cache::exists($cache_id)) { try { if ($path = Files::callStorage('getFullPath', $this)) { (new Image($path))->resize($width)->save($destination); } elseif ($content = Files::callStorage('fetch', $this)) { Image::createFromBlob($content)->resize($width)->save($destination); } else { throw new \RuntimeException('Unable to fetch file'); } } catch (\RuntimeException $e) { throw new UserException('Impossible de créer la miniature'); } } $this->_serve($destination, null); } /** * Servir un fichier local en HTTP * @param string $path Chemin vers le fichier local * @param string $type Type MIME du fichier * @param string $name Nom du fichier avec extension * @param integer $size Taille du fichier en octets (facultatif) * @return boolean TRUE en cas de succès */ protected function _serve(?string $path, ?string $content, bool $download = false): void { if ($this->isPublic()) { Utils::HTTPCache(null, $this->modified->getTimestamp()); } else { // Disable browser cache header('Pragma: private'); header('Expires: -1'); header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0'); } $type = $this->mime; // Force CSS mimetype if (substr($this->name, -4) == '.css') { $type = 'text/css'; } elseif (substr($this->name, -3) == '.js') { $type = 'text/javascript'; } if (substr($type, 0, 5) == 'text/') { $type .= ';charset=utf-8'; } header(sprintf('Content-Type: %s', $type)); header(sprintf('Content-Disposition: %s; filename="%s"', $download ? 'attachment' : 'inline', $this->name)); // Utilisation de XSendFile si disponible if (null !== $path && ENABLE_XSENDFILE && isset($_SERVER['SERVER_SOFTWARE'])) { if (stristr($_SERVER['SERVER_SOFTWARE'], 'apache') && function_exists('apache_get_modules') && in_array('mod_xsendfile', apache_get_modules())) { header('X-Sendfile: ' . $path); return; } else if (stristr($_SERVER['SERVER_SOFTWARE'], 'lighttpd')) { header('X-Sendfile: ' . $path); return; } } // Désactiver gzip if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', 1); } @ini_set('zlib.output_compression', 'Off'); header(sprintf('Content-Length: %d', $path ? filesize($path) : strlen($content))); if (@ob_get_length()) { @ob_clean(); } flush(); if (null !== $path) { readfile($path); } else { echo $content; } } public function fetch() { return Files::callStorage('fetch', $this); } public function render(array $options = []) { $type = $this->customType(); /* if (substr($this->name, -strlen(self::FILE_EXT_HTML)) == self::FILE_EXT_HTML) { return \Garradin\Web\Render\HTML::render($this, null, $options); }*/ if ($type == self::FILE_EXT_SKRIV) { return \Garradin\Web\Render\Skriv::render($this, null, $options); } else if ($type == self::FILE_EXT_ENCRYPTED) { return \Garradin\Web\Render\EncryptedSkriv::render($this, null, $options); } else if (substr($this->mime, 0, 5) == 'text/') { return $this->fetch(); } throw new \LogicException('Cannot render file of this type'); } public function checkReadAccess(?Session $session): bool { // Web pages and config files are always public if ($this->isPublic()) { return true; } $context = $this->context(); if (null === $session) { return false; } if ($context == self::CONTEXT_TRANSACTION && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)) { return true; } // The user can access his own profile files else if ($context == self::CONTEXT_USER && $ref == $session->getUser()->id) { return true; } // Only users able to manage users can see their profile files else if ($context == self::CONTEXT_USER && $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)) { return true; } // Only users with right to access documents can read documents else if ($context == self::CONTEXT_DOCUMENTS && $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_READ)) { return true; } return false; } public function checkWriteAccess(?Session $session): bool { if (null === $session) { return false; } switch ($this->context()) { case self::CONTEXT_WEB: return $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE); case self::CONTEXT_DOCUMENTS: // Only admins can delete files return $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_WRITE); case self::CONTEXT_CONFIG: return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN); case self::CONTEXT_TRANSACTION: return $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE); case self::CONTEXT_SKELETON: return $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN); case self::CONTEXT_USER: return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE); } return false; } public function checkDeleteAccess(?Session $session): bool { if (null === $session) { return false; } switch ($this->context()) { case self::CONTEXT_WEB: return $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE); case self::CONTEXT_DOCUMENTS: // Only admins can delete files return $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_ADMIN); case self::CONTEXT_CONFIG: return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN); case self::CONTEXT_TRANSACTION: return $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); case self::CONTEXT_SKELETON: return $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN); case self::CONTEXT_USER: return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE); } return false; } static public function checkCreateAccess(string $path, ?Session $session): bool { if (null === $session) { return false; } $context = strtok($path, '/'); switch ($context) { case self::CONTEXT_WEB: return $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE); case self::CONTEXT_DOCUMENTS: return $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_WRITE); case self::CONTEXT_CONFIG: return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN); case self::CONTEXT_TRANSACTION: return $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE); case self::CONTEXT_SKELETON: return $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN); case self::CONTEXT_USER: return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE); } return false; } public function path(): string { return $this->path . '/' . $this->name; } public function pathHash(): string { return sha1($this->path()); } public function isPublic(): bool { $context = $this->context(); if ($context == self::CONTEXT_CONFIG || $context == self::CONTEXT_WEB || $context == self::CONTEXT_SKELETON) { return true; } return false; } public function getEditor(): ?string { if ($this->customType() == self::FILE_EXT_SKRIV) { return self::EDITOR_WEB; } elseif ($this->customType() == self::FILE_EXT_ENCRYPTED) { return self::EDITOR_ENCRYPTED; } elseif (substr($this->mime, 0, 5) == 'text/') { return self::EDITOR_CODE; } return null; } static public function validatePath(string $path): array { $path = explode('/', $path); if (count($path) < 1) { throw new ValidationException('Invalid file path'); } if (!array_key_exists($path[0], self::CONTEXTS_NAMES)) { throw new ValidationException('Chemin invalide'); } $context = array_shift($path); foreach ($path as $part) { if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!iu', $part)) { throw new ValidationException('Chemin invalide'); } } $name = array_pop($path); $ref = implode('/', $path); return [$context, $ref ?: null, $name]; } public function customType(): ?string { static $extensions = [self::FILE_EXT_ENCRYPTED, self::FILE_EXT_SKRIV]; foreach ($extensions as $ext) { if (substr($this->name, -strlen($ext)) == $ext) { return $ext; } } return null; } } |
Modified src/include/lib/Garradin/Entities/Services/Fee.php from [7ce45f39a7] to [2759012752].
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 |
$tables = 'services_users su INNER JOIN membres m ON m.id = su.id_user INNER JOIN services_fees sf ON sf.id = su.id_fee INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_fee) AS su2 ON su2.id = su.id LEFT JOIN acc_transactions_users tu ON tu.id_service_user = su.id LEFT JOIN acc_transactions_lines l ON l.id_transaction = tu.id_transaction'; $conditions = sprintf('su.id_fee = %d AND su.paid = 1 AND (su.expiry_date >= date() OR su.expiry_date IS NULL) AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)', $this->id()); $list = new DynamicList($columns, $tables, $conditions); $list->groupBy('su.id_user'); $list->orderBy('date', true); $list->setCount('COUNT(DISTINCT su.id_user)'); return $list; } public function unpaidUsersList(): DynamicList { $list = $this->paidUsersList(); $conditions = sprintf('su.id_fee = %d AND su.paid = 0 AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)', $this->id()); $list->setConditions($conditions); return $list; } public function expiredUsersList(): DynamicList { $list = $this->paidUsersList(); $conditions = sprintf('su.id_fee = %d AND su.expiry_date < date() AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)', $this->id()); $list->setConditions($conditions); return $list; } } |
| | | |
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 |
$tables = 'services_users su INNER JOIN membres m ON m.id = su.id_user INNER JOIN services_fees sf ON sf.id = su.id_fee INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_fee) AS su2 ON su2.id = su.id LEFT JOIN acc_transactions_users tu ON tu.id_service_user = su.id LEFT JOIN acc_transactions_lines l ON l.id_transaction = tu.id_transaction'; $conditions = sprintf('su.id_fee = %d AND su.paid = 1 AND (su.expiry_date >= date() OR su.expiry_date IS NULL) AND m.category_id NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list = new DynamicList($columns, $tables, $conditions); $list->groupBy('su.id_user'); $list->orderBy('date', true); $list->setCount('COUNT(DISTINCT su.id_user)'); return $list; } public function unpaidUsersList(): DynamicList { $list = $this->paidUsersList(); $conditions = sprintf('su.id_fee = %d AND su.paid = 0 AND m.category_id NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list->setConditions($conditions); return $list; } public function expiredUsersList(): DynamicList { $list = $this->paidUsersList(); $conditions = sprintf('su.id_fee = %d AND su.expiry_date < date() AND m.category_id NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list->setConditions($conditions); return $list; } } |
Modified src/include/lib/Garradin/Entities/Services/Service.php from [04841ea319] to [1e0b3eb58f].
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 |
$tables = 'services_users su INNER JOIN membres m ON m.id = su.id_user INNER JOIN services s ON s.id = su.id_service INNER JOIN services_fees sf ON sf.id = su.id_fee INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_service) AS su2 ON su2.id = su.id'; $conditions = sprintf('su.id_service = %d AND su.paid = 1 AND (su.expiry_date >= date() OR su.expiry_date IS NULL) AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)', $this->id()); $list = new DynamicList($columns, $tables, $conditions); $list->groupBy('su.id_user'); $list->orderBy('date', true); $list->setCount('COUNT(DISTINCT su.id_user)'); return $list; } public function unpaidUsersList(): DynamicList { $list = $this->paidUsersList(); $conditions = sprintf('su.id_service = %d AND su.paid = 0 AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)', $this->id()); $list->setConditions($conditions); return $list; } public function expiredUsersList(): DynamicList { $list = $this->paidUsersList(); $conditions = sprintf('su.id_service = %d AND su.expiry_date < date() AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)', $this->id()); $list->setConditions($conditions); return $list; } } |
| | | |
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 |
$tables = 'services_users su INNER JOIN membres m ON m.id = su.id_user INNER JOIN services s ON s.id = su.id_service INNER JOIN services_fees sf ON sf.id = su.id_fee INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_service) AS su2 ON su2.id = su.id'; $conditions = sprintf('su.id_service = %d AND su.paid = 1 AND (su.expiry_date >= date() OR su.expiry_date IS NULL) AND m.category_id NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list = new DynamicList($columns, $tables, $conditions); $list->groupBy('su.id_user'); $list->orderBy('date', true); $list->setCount('COUNT(DISTINCT su.id_user)'); return $list; } public function unpaidUsersList(): DynamicList { $list = $this->paidUsersList(); $conditions = sprintf('su.id_service = %d AND su.paid = 0 AND m.category_id NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list->setConditions($conditions); return $list; } public function expiredUsersList(): DynamicList { $list = $this->paidUsersList(); $conditions = sprintf('su.id_service = %d AND su.expiry_date < date() AND m.category_id NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list->setConditions($conditions); return $list; } } |
Added src/include/lib/Garradin/Entities/Users/Category.php version [19219a4cf7].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 |
<?php namespace Garradin\Entities\Users; use Garradin\Membres\Session; use Garradin\Config; use Garradin\DB; use Garradin\UserException; use Garradin\Entity; class Category extends Entity { const TABLE = 'users_categories'; protected $id; protected $name; protected $hidden = 0; protected $perm_web = 0; protected $perm_documents = 0; protected $perm_users = 0; protected $perm_accounting = 0; protected $perm_subscribe = 0; protected $perm_connect = 0; protected $perm_config = 0; protected $_types = [ 'id' => 'int', 'name' => 'string', 'hidden' => 'int', 'perm_web' => 'int', 'perm_documents' => 'int', 'perm_users' => 'int', 'perm_accounting' => 'int', 'perm_subscribe' => 'int', 'perm_connect' => 'int', 'perm_config' => 'int', ]; const PERMISSIONS = [ 'connect' => [ 'label' => 'Les membres de cette catégorie peuvent-ils se connecter ?', 'shape' => 'C', 'options' => [ Session::ACCESS_NONE => 'N\'a pas le droit de se connecter', Session::ACCESS_READ => 'A le droit de se connecter', ], ], 'users' => [ 'label' => 'Gestion des membres', 'shape' => 'M', 'options' => [ Session::ACCESS_NONE => 'Pas d\'accès', Session::ACCESS_READ => 'Lecture uniquement (peut voir les informations personnelles de tous les membres, y compris leurs inscriptions à des activités)', Session::ACCESS_WRITE => 'Lecture & écriture (peut ajouter et modifier des membres, mais pas les supprimer ni les changer de catégorie, peut inscrire des membres à des activités)', Session::ACCESS_ADMIN => 'Administration (peut tout faire)', ], ], 'accounting' => [ 'label' => 'Comptabilité', 'shape' => '€', 'options' => [ Session::ACCESS_NONE => 'Pas d\'accès', Session::ACCESS_READ => 'Lecture uniquement (peut lire toutes les informations de tous les exercices)', Session::ACCESS_WRITE => 'Lecture & écriture (peut ajouter des écritures, mais pas les modifier ni les supprimer)', Session::ACCESS_ADMIN => 'Administration (peut modifier et supprimer des écritures, gérer les comptes, les exercices, etc.)', ], ], 'documents' => [ 'label' => 'Documents', 'shape' => 'D', 'options' => [ Session::ACCESS_NONE => 'Pas d\'accès', Session::ACCESS_READ => 'Lecture uniquement (peut lire tous les fichiers)', Session::ACCESS_WRITE => 'Lecture & écriture (peut lire, ajouter, modifier et déplacer des fichiers, mais pas les supprimer)', Session::ACCESS_ADMIN => 'Administration (peut tout faire)', ], ], 'web' => [ 'label' => 'Gestion du site web', 'shape' => 'W', 'options' => [ Session::ACCESS_NONE => 'Pas d\'accès', Session::ACCESS_READ => 'Lecture uniquement (peut lire les pages)', Session::ACCESS_WRITE => 'Lecture & écriture (peut ajouter, modifier et supprimer des pages et catégories, mais pas modifier la configuration)', Session::ACCESS_ADMIN => 'Administration (peut tout faire)', ], ], 'config' => [ 'label' => 'Les membres de cette catégorie peuvent-ils modifier la configuration ?', 'shape' => '☑', 'options' => [ Session::ACCESS_NONE => 'Ne peut pas modifier la configuration', Session::ACCESS_ADMIN => 'Peut modifier la configuration', ], ], ]; public function selfCheck(): void { parent::selfCheck(); $this->assert(trim($this->name) !== '', 'Le nom de catégorie ne peut rester vide.'); $this->assert($this->hidden === 0 || $this->hidden === 1, 'Wrong value for hidden'); foreach (self::PERMISSIONS as $key => $perm) { $this->assert(array_key_exists($this->{'perm_' . $key}, $perm['options']), 'Invalid value for perm_' . $key); } } public function delete(): bool { $db = DB::getInstance(); $config = Config::getInstance(); if ($this->id() == $config->get('categorie_membres')) { throw new UserException('Il est interdit de supprimer la catégorie définie par défaut dans la configuration.'); } if ($db->test('membres', 'category_id = ?', $this->id())) { throw new UserException('La catégorie contient encore des membres, il n\'est pas possible de la supprimer.'); } return parent::delete(); } public function setAllPermissions(int $access): void { foreach (self::PERMISSIONS as $key => $perm) { // Restrict to the maximum access level, as some permissions only allow up to READ $perm_access = min($access, max(array_keys($perm['options']))); $this->set('perm_' . $key, $perm_access); } } } |
Added src/include/lib/Garradin/Entities/Web/Page.php version [546a528094].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 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 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 418 419 |
<?php namespace Garradin\Entities\Web; use Garradin\DB; use Garradin\Entity; use Garradin\UserException; use Garradin\Utils; use Garradin\Entities\Files\File; use Garradin\Files\Files; use Garradin\Web\Render\Skriv; use KD2\DB\EntityManager as EM; use const Garradin\WWW_URL; class Page extends Entity { const TABLE = 'web_pages'; protected $id; protected $parent; protected $path; protected $name; protected $title; protected $type; protected $status; protected $format; protected $published; protected $modified; protected $content; protected $_types = [ 'id' => 'int', 'parent' => '?string', 'path' => 'string', 'name' => 'string', 'title' => 'string', 'type' => 'int', 'status' => 'string', 'format' => 'string', 'published' => 'DateTime', 'modified' => 'DateTime', 'content' => 'string', ]; const FORMAT_SKRIV = 'skriv'; const FORMAT_ENCRYPTED = 'skriv/encrypted'; const FORMATS_LIST = [ self::FORMAT_SKRIV => 'SkrivML', self::FORMAT_ENCRYPTED => 'Chiffré', ]; const STATUS_ONLINE = 'online'; const STATUS_DRAFT = 'draft'; const STATUS_LIST = [ self::STATUS_ONLINE => 'En ligne', self::STATUS_DRAFT => 'Brouillon', ]; const TYPE_CATEGORY = 1; const TYPE_PAGE = 2; const TEMPLATES = [ self::TYPE_PAGE => 'article.html', self::TYPE_CATEGORY => 'category.html', ]; protected $_file; protected $_attachments; static public function create(int $type, ?string $parent, string $title, string $status = self::STATUS_ONLINE): self { $page = new self; $data = compact('type', 'parent', 'title', 'status'); $data['content'] = ''; $page->importForm($data); $page->published = new \DateTime; $page->modified = new \DateTime; $page->name = 'index.txt'; return $page; } public function file(bool $force_reload = false) { if (null === $this->_file || $force_reload) { $this->_file = Files::get($this->filepath()); if (null === $this->_file) { throw new \LogicException('This file does not exist: ' . $this->filepath()); } } return $this->_file; } public function load(array $data): void { parent::load($data); if ($this->file()->modified > $this->modified) { $this->loadFromFile($this->file()); $this->save(); } } public function url(): string { return WWW_URL . $this->path(); } public function uri(): string { return basename($this->path); } public function template(): string { return self::TEMPLATES[$this->type]; } public function asTemplateArray(): array { $out = $this->asArray(); $out['url'] = $this->url(); $out['html'] = $this->render(); $out['uri'] = $this->uri(); return $out; } public function render(array $options = []): string { if ($this->format == self::FORMAT_SKRIV) { return \Garradin\Web\Render\Skriv::render($this->file(), $this->content, $options); } else if ($this->format == self::FORMAT_ENCRYPTED) { return \Garradin\Web\Render\EncryptedSkriv::render($this->file(), $this->content, $options); } } public function preview(string $content): string { return Skriv::render($this->file(), $content, ['prefix' => '#']); } public function filepath(): string { $parts = [ File::CONTEXT_WEB, $this->path, $this->name, ]; $parts = array_filter($parts); return implode('/', $parts); } public function path(): string { return $this->path; } public function save(): bool { $file = $this->file(); $exists = $this->exists(); $realpath = $this->filepath(); if (!$exists && !$file) { $file = $this->_file = File::createAndStore(dirname($realpath), basename($realpath), null, $this->export()); } $edit_file = (bool) count(array_intersect(['title', 'status', 'published', 'format', 'content'], array_keys($this->_modified))); if ($edit_file) { $this->set('modified', new \DateTime); } parent::save(); if ($exists && (isset($this->_modified['parent']) || isset($this->_modified['name']) || isset($this->_modified['path']))) { // Rename parent directory $dir = Files::get($file->path); $dir->rename(dirname($realpath)); $file = $this->file(true); } if ($edit_file) { $file->setContent($this->export()); } return true; } public function delete(): bool { $this->file()->delete(); return parent::delete(); } public function selfCheck(): void { $this->assert($this->type === self::TYPE_CATEGORY || $this->type === self::TYPE_PAGE, 'Unknown page type'); $this->assert(array_key_exists($this->status, self::STATUS_LIST), 'Unknown page status'); $this->assert(array_key_exists($this->format, self::FORMATS_LIST), 'Unknown page format'); $this->assert(trim($this->title) !== '', 'Le titre ne peut rester vide'); $this->assert(trim($this->path) !== '', 'Le chemin ne peut rester vide'); $this->assert((bool) $this->file(), 'Fichier manquant'); $db = DB::getInstance(); $where = $this->exists() ? sprintf(' AND id != %d', $this->id()) : ''; $this->assert(!$db->test(self::TABLE, 'path = ?' . $where, $this->path), 'Cette adresse URI est déjà utilisée par une autre page, merci d\'en choisir une autre'); } public function importForm(array $source = null) { if (null === $source) { $source = $_POST; } if (isset($source['date']) && isset($source['date_time'])) { $source['published'] = $source['date'] . ' ' . $source['date_time']; } if (isset($source['title']) && is_null($this->path)) { $source['path'] = trim($this->parent . '/' . Utils::transformTitleToURI($source['title']), '/'); } if (isset($source['uri'])) { $source['path'] = trim($this->parent . '/' . Utils::transformTitleToURI($source['uri']), '/'); } if (!empty($source['encryption']) ) { $this->set('format', self::FORMAT_ENCRYPTED); } else { $this->set('format', self::FORMAT_SKRIV); } return parent::importForm($source); } public function getBreadcrumbs(): array { $sql = ' WITH RECURSIVE parents(title, parent, path, level) AS ( SELECT title, parent, path, 1 FROM web_pages WHERE id = ? UNION ALL SELECT p.title, p.parent, p.path, level + 1 FROM web_pages p JOIN parents ON parents.parent = p.path ) SELECT path, title FROM parents ORDER BY level DESC;'; return DB::getInstance()->getAssoc($sql, $this->id()); } public function listAttachments(): array { if (null === $this->_attachments) { $list = Files::list(dirname($this->filepath())); // Remove the page itself $list = array_filter($list, function ($a) { return $a->name != $this->name && $a->type != $a::TYPE_DIRECTORY; }); $this->_attachments = $list; } return $this->_attachments; } static public function findTaggedAttachments(string $text): array { preg_match_all('/<<?(?:fichier|image)\s*(?:\|\s*)?([\w\d_.-]+)/ui', $text, $match, PREG_PATTERN_ORDER); preg_match_all('/(?:fichier|image):\/\/([\w\d_.-]+)/ui', $text, $match2, PREG_PATTERN_ORDER); return array_merge($match[1], $match2[1]); } /** * Return list of images * If $all is FALSE then this will only return images that are not present in the content */ public function getImageGallery(bool $all = true): array { return $this->getAttachmentsGallery($all, true); } /** * Return list of files * If $all is FALSE then this will only return files that are not present in the content */ public function getAttachmentsGallery(bool $all = true, bool $images = false): array { $out = []; if (!$all) { $tagged = $this->findTaggedAttachments($this->content); } foreach ($this->listAttachments() as $a) { if ($images && !$a->image) { continue; } elseif (!$images && $a->image) { continue; } // Skip if (!$all && in_array($a->name, $tagged)) { continue; } $out[] = $a; } return $out; } public function export(): string { $meta = [ 'Title' => str_replace("\n", '', trim($this->title)), 'Status' => $this->status, 'Published' => $this->published->format('Y-m-d H:i:s'), 'Format' => $this->format, ]; $out = ''; foreach ($meta as $key => $value) { $out .= sprintf("%s: %s\n", $key, $value); } $out .= "\n----\n\n" . $this->content; return $out; } public function importFromRaw(string $str): bool { $str = preg_replace("/\r\n|\r|\n/", "\n", $str); $str = explode("\n\n----\n\n", $str, 2); if (count($str) !== 2) { return false; } list($meta, $content) = $str; $meta = explode("\n", trim($meta)); foreach ($meta as $line) { $key = strtolower(trim(strtok($line, ':'))); $value = trim(strtok('')); if ($key == 'title') { $this->set('title', $value); } elseif ($key == 'published') { $this->set('published', new \DateTime($value)); } elseif ($key == 'format') { $value = strtolower($value); if (!array_key_exists($value, self::FORMATS_LIST)) { throw new \LogicException('Unknown format: ' . $value); } $this->set('format', $value); } elseif ($key == 'status') { $value = strtolower($value); if (!array_key_exists($value, self::STATUS_LIST)) { throw new \LogicException('Unknown status: ' . $value); } $this->set('status', $value); } else { // Ignore other metadata } } $this->set('content', trim($content, "\n\r")); return true; } public function loadFromFile(File $file): void { // Path is relative to web root $this->set('parent', substr(dirname($file->path), strlen(File::CONTEXT_WEB . '/')) ?: null); if (!$this->importFromRaw($file->fetch())) { throw new \LogicException('Invalid page content: ' . $file->pathname()); } $this->set('modified', $file->modified); $this->set('type', self::TYPE_PAGE); // Default foreach (Files::list($file->path) as $subfile) { if ($subfile->type == File::TYPE_DIRECTORY) { $this->set('type', self::TYPE_CATEGORY); break; } } } } |
Modified src/include/lib/Garradin/Entity.php from [4ef751c359] to [a696ddf36e].
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
elseif (preg_match('!^\d{2}/\d{2}/\d{4}$!', $value)) {
return \DateTime::createFromFormat('d/m/Y', $value);
}
elseif (null !== $value) {
throw new ValidationException('Format de date invalide (merci d\'utiliser le format JJ/MM/AAAA) : ' . $value);
}
}
else {
return parent::filterUserValue($type, $value, $key);
}
}
protected function assert(?bool $test, string $message = null): void
{
if ($test) {
return;
}
|
| | > | > > > |
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
elseif (preg_match('!^\d{2}/\d{2}/\d{4}$!', $value)) { return \DateTime::createFromFormat('d/m/Y', $value); } elseif (null !== $value) { throw new ValidationException('Format de date invalide (merci d\'utiliser le format JJ/MM/AAAA) : ' . $value); } } elseif ($type == 'DateTime') { if (preg_match('!^\d{2}/\d{2}/\d{4}\s\d{2}:\d{2}$!', $value)) { return \DateTime::createFromFormat('d/m/Y H:i', $value); } } return parent::filterUserValue($type, $value, $key); } protected function assert(?bool $test, string $message = null): void { if ($test) { return; } |
Deleted src/include/lib/Garradin/Fichiers.php version [76103311d2].
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 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 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 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 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 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 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 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 |
<?php namespace Garradin; use KD2\Graphics\Image; use Garradin\Membres\Session; class Fichiers { public $id; public $nom; public $type; public $image; public $datetime; public $hash; public $taille; public $id_contenu; /** * Tailles de miniatures autorisées, pour ne pas avoir 500 fichiers générés avec 500 tailles différentes * @var array */ protected static $allowed_thumb_sizes = [200, 500]; const LIEN_COMPTA = 'acc_transactions'; const LIEN_WIKI = 'wiki_pages'; const LIEN_MEMBRES = 'membres'; /** * Renvoie l'URL vers un fichier * @param integer $id Numéro du fichier * @param string $nom Nom de fichier avec extension * @param integer $size Taille de la miniature désirée (pour les images) * @return string URL du fichier */ static public function _getURL(int $id, string $nom, string $hash, $size = false): string { $url = sprintf('%sf/%s/%s?', WWW_URL, base_convert((int)$id, 10, 36), $nom); if ($size) { $url .= self::_findThumbSize($size) . 'px&'; } $url .= substr($hash, 0, 10); return $url; } /** * Renvoie la taille de miniature la plus proche de la taille demandée * @param integer $size Taille demandée * @return integer Taille possible */ static protected function _findThumbSize($size) { $size = (int) $size; if (in_array($size, self::$allowed_thumb_sizes)) { return $size; } foreach (self::$allowed_thumb_sizes as $s) { if ($s >= $size) { return $s; } } return max(self::$allowed_thumb_sizes); } /** * Constructeur de l'objet pour un fichier * @param integer $id Numéro unique du fichier * @param $data array|object File data to populate object */ public function __construct(int $id, $data = null) { if (is_null($data)) { $data = DB::getInstance()->first('SELECT fichiers.*, fc.hash, fc.taille, strftime(\'%s\', datetime) AS datetime FROM fichiers INNER JOIN fichiers_contenu AS fc ON fc.id = fichiers.id_contenu WHERE fichiers.id = ?;', (int)$id); } if (!$data) { throw new \InvalidArgumentException('Ce fichier n\'existe pas.'); } foreach ((array)$data as $key => $value) { $this->$key = $value; } } /** * Renvoie l'adresse d'accès au fichier * @param boolean $size Taille éventuelle de la miniature demandée * @return string URL d'accès au fichier */ public function getURL($size = false) { return self::_getURL($this->id, $this->nom, $this->hash, $size); } /** * Lier un fichier à un contenu * @param string $type Type de contenu (constantes LIEN_*) * @param integer $foreign_id ID du contenu lié * @return boolean TRUE en cas de succès */ public function linkTo($type, $foreign_id) { $db = DB::getInstance(); $check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA]; if (!in_array($type, $check)) { throw new \LogicException('Type de lien de fichier inconnu.'); } // Vérifier que le fichier n'est pas déjà lié à un autre type $query = []; foreach ($check as $check_type) { // Ne pas chercher dans le type qu'on veut lier if ($check_type == $type) { continue; } $query[] = sprintf('SELECT 1 FROM fichiers_%s WHERE fichier = %d', $check_type, $this->id); } $query = implode(' UNION ', $query) . ';'; if ($db->firstColumn($query)) { throw new \LogicException('Ce fichier est déjà lié à un autre contenu : ' . $check_type); } return $db->preparedQuery('INSERT OR IGNORE INTO fichiers_' . $type . ' (fichier, id) VALUES (?, ?);', [(int)$this->id, (int)$foreign_id]); } public function getLinkedId(string $type) { $check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA]; if (!in_array($type, $check)) { throw new \LogicException('Type de lien de fichier inconnu.'); } return DB::getInstance()->firstColumn(sprintf('SELECT id FROM fichiers_%s WHERE fichier = %d;', $type, $this->id)); } public function isPublic(&$wiki = null): bool { $config = Config::getInstance(); if ($config->get('image_fond') == $this->id) { return true; } // On regarde déjà si le fichier n'est pas lié au wiki $query = sprintf('SELECT wp.droit_lecture FROM fichiers_%s AS link INNER JOIN wiki_pages AS wp ON wp.id = link.id WHERE link.fichier = ? LIMIT 1;', self::LIEN_WIKI); $wiki = DB::getInstance()->firstColumn($query, (int)$this->id); // Page wiki publique, aucune vérification à faire, seul cas d'accès à un fichier en dehors de l'espace admin if ($wiki !== false && $wiki == Wiki::LECTURE_PUBLIC) { return true; } return false; } /** * Vérifie que l'utilisateur a bien le droit d'accéder à ce fichier * @param mixed $user Tableau contenant les infos sur l'utilisateur connecté, provenant de Session::getUser, ou false * @return boolean TRUE si l'utilisateur a le droit d'accéder au fichier, sinon FALSE */ public function checkAccess(Session $session, bool $require_admin = false) { $wiki = null; if ($this->isPublic($wiki) && !$require_admin) { return true; } // Pas d'utilisateur connecté, pas d'accès aux fichiers de l'espace admin if (!$session->isLogged()) { return false; } $user = $session->getUser(); if ($wiki !== false) { // S'il n'a même pas droit à accéder au wiki c'est mort if (!$session->canAccess('wiki', Membres::DROIT_ACCES)) { return false; } // On renvoie à l'objet Wiki pour savoir si l'utilisateur a le droit de lire ce fichier $_w = new Wiki; $_w->setRestrictionCategorie($user->id_categorie, $user->droit_wiki); return $require_admin ? $_w->canWritePage($wiki) : $_w->canReadPage($wiki); } $level = $require_admin ? Membres::DROIT_ADMIN : Membres::DROIT_ACCES; $db = DB::getInstance(); // On regarde maintenant si le fichier est lié à la compta $query = sprintf('SELECT 1 FROM fichiers_%s WHERE fichier = ? LIMIT 1;', self::LIEN_COMPTA); $compta = $db->firstColumn($query, (int)$this->id); if ($compta) { // OK si accès à la compta return $session->canAccess('compta', $level); } // Enfin, si le fichier est lié à un membre $query = sprintf('SELECT id FROM fichiers_%s WHERE fichier = ? LIMIT 1;', self::LIEN_MEMBRES); $membre = $db->firstColumn($query, (int)$this->id); if ($membre !== false) { // De manière évidente, l'utilisateur a le droit d'accéder aux fichiers liés à son profil if ((int)$membre == $user->id) { return true; } // Pour voir les fichiers des membres il faut pouvoir les gérer if ($level == Membres::DROIT_ACCES) { $level = Membres::DROIT_ECRITURE; } if ($session->canAccess('membres', $level)) { return true; } } return false; } /** * Supprime le fichier * @return boolean TRUE en cas de succès */ public function remove() { $db = DB::getInstance(); $db->begin(); $db->delete('fichiers_' . self::LIEN_COMPTA, 'fichier = ?', (int)$this->id); $db->delete('fichiers_' . self::LIEN_WIKI, 'fichier = ?', (int)$this->id); $db->delete('fichiers_' . self::LIEN_MEMBRES, 'fichier = ?', (int)$this->id); $db->delete('fichiers', 'id = ?', (int)$this->id); // Suppression du contenu s'il n'est pas utilisé par un autre fichier if (!$db->firstColumn('SELECT 1 FROM fichiers WHERE id_contenu = ? AND id != ? LIMIT 1;', (int)$this->id_contenu, (int)$this->id)) { $db->delete('fichiers_contenu', 'id = ?', (int)$this->id_contenu); } $cache_id = 'fichiers.' . $this->id_contenu; Static_Cache::remove($cache_id); foreach (self::$allowed_thumb_sizes as $size) { Static_Cache::remove($cache_id . '.thumb.' . (int)$size); } return $db->commit(); } /** * Renvoie le chemin vers le fichier local en cache, et le crée s'il n'existe pas * @return string Chemin local */ protected function getFilePathFromCache() { // Le cache est géré par ID contenu, pas ID fichier, pour minimiser l'espace disque utilisé $cache_id = 'fichiers.' . $this->id_contenu; // Le fichier n'existe pas dans le cache statique, on l'enregistre if (!Static_Cache::exists($cache_id)) { $blob = DB::getInstance()->openBlob('fichiers_contenu', 'contenu', (int)$this->id_contenu); Static_Cache::storeFromPointer($cache_id, $blob); fclose($blob); } return Static_Cache::getPath($cache_id); } /** * Envoie le fichier au client HTTP * @return void */ public function serve() { return $this->_serve($this->getFilePathFromCache(), $this->type, ($this->image ? false : $this->nom), $this->taille, $this->isPublic()); } /** * Envoie une miniature à la taille indiquée au client HTTP * @return void */ public function serveThumbnail($width = null) { if (!$this->image) { throw new UserException('Il n\'est pas possible de fournir une miniature pour un fichier qui n\'est pas une image.'); } if (!$width) { $width = reset(self::$allowed_thumb_sizes); } if (!in_array($width, self::$allowed_thumb_sizes)) { throw new UserException('Cette taille de miniature n\'est pas autorisée.'); } $cache_id = 'fichiers.' . $this->id_contenu . '.thumb.' . (int)$width; $path = Static_Cache::getPath($cache_id); // La miniature n'existe pas dans le cache statique, on la crée if (!Static_Cache::exists($cache_id)) { $source = $this->getFilePathFromCache(); try { (new Image($source))->resize($width)->save($path); } catch (\RuntimeException $e) { throw new UserException('Impossible de créer la miniature'); } } return $this->_serve($path, $this->type); } /** * Servir un fichier local en HTTP * @param string $path Chemin vers le fichier local * @param string $type Type MIME du fichier * @param string $name Nom du fichier avec extension * @param integer $size Taille du fichier en octets (facultatif) * @return boolean TRUE en cas de succès */ protected function _serve($path, $type, $name = false, $size = null, bool $public = false) { if ($public) { Utils::HTTPCache($this->hash, $this->datetime); } else { // Désactiver le cache header('Pragma: public'); header('Expires: -1'); header('Cache-Control: public, must-revalidate, post-check=0, pre-check=0'); } header('Content-Type: '.$type); if ($name) { header('Content-Disposition: attachment; filename="' . $name . '"'); } // Utilisation de XSendFile si disponible if (ENABLE_XSENDFILE && isset($_SERVER['SERVER_SOFTWARE'])) { if (stristr($_SERVER['SERVER_SOFTWARE'], 'apache') && function_exists('apache_get_modules') && in_array('mod_xsendfile', apache_get_modules())) { header('X-Sendfile: ' . $path); return true; } else if (stristr($_SERVER['SERVER_SOFTWARE'], 'lighttpd')) { header('X-Sendfile: ' . $path); return true; } } // Désactiver gzip if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', 1); } @ini_set('zlib.output_compression', 'Off'); if ($size) { header('Content-Length: '. (int)$size); } if (@ob_get_length()) { @ob_clean(); } flush(); // Sinon on envoie le fichier à la mano return readfile($path); } /** * Vérifie si le hash fourni n'est pas déjà stocké * Utile pour par exemple reconnaître un ficher dont le contenu est déjà stocké, et éviter un nouvel upload * @param string $hash Hash SHA1 * @return boolean TRUE si le hash est déjà présent dans fichiers_contenu, FALSE sinon */ static public function checkHash($hash) { return (boolean) DB::getInstance()->firstColumn( 'SELECT 1 FROM fichiers_contenu WHERE hash = ?;', trim(strtolower($hash)) ); } /** * Retourne un tableau de hash trouvés dans la DB parmi une liste de hash fournis * @param array $list Liste de hash à vérifier * @return array Liste des hash trouvés */ static public function checkHashList($list) { $db = DB::getInstance(); array_walk($list, function (&$a) use ($db) { $a = $db->quote($a); }); $query = sprintf('SELECT hash, 1 FROM fichiers_contenu WHERE hash IN (%s);', implode(', ', $list)); return $db->getAssoc($query); } /** * Récupération du message d'erreur * @param integer $error Code erreur du $_FILE * @return string Message d'erreur */ static public function getErrorMessage($error) { switch ($error) { case UPLOAD_ERR_INI_SIZE: return 'Le fichier excède la taille permise par la configuration du serveur.'; case UPLOAD_ERR_FORM_SIZE: return 'Le fichier excède la taille permise par le formulaire.'; case UPLOAD_ERR_PARTIAL: return 'L\'envoi du fichier a été interrompu.'; case UPLOAD_ERR_NO_FILE: return 'Aucun fichier n\'a été reçu.'; case UPLOAD_ERR_NO_TMP_DIR: return 'Pas de répertoire temporaire pour stocker le fichier.'; case UPLOAD_ERR_CANT_WRITE: return 'Impossible d\'écrire le fichier sur le disque du serveur.'; case UPLOAD_ERR_EXTENSION: return 'Une extension du serveur a interrompu l\'envoi du fichier.'; default: return 'Erreur inconnue: ' . $error; } } /** * Upload du fichier par POST * @param array $file Caractéristiques du fichier envoyé * @return Fichiers */ static public function upload($file) { if (!empty($file['error'])) { throw new UserException(self::getErrorMessage($file['error'])); } if (empty($file['size']) || empty($file['name'])) { throw new UserException('Fichier reçu invalide : vide ou sans nom de fichier.'); } if (!is_uploaded_file($file['tmp_name'])) { throw new \RuntimeException('Le fichier n\'a pas été envoyé de manière conventionnelle.'); } $name = preg_replace('/\s+/', '_', $file['name']); $name = preg_replace('/[^\d\w._-]/ui', '', $name); return self::storeFile($name, $file['tmp_name']); } /** * Upload de fichier à partir d'une chaîne en base64 * @param string $name * @param string $content * @return Fichiers */ static public function storeFromBase64($name, $content) { $content = base64_decode($content); return self::storeFile($name, null, $content); } /** * Upload de fichier (interne) * * @param string $name * @param string $path Chemin du fichier * @param string $content Ou contenu du fichier * @return Fichiers */ static protected function storeFile($name, $path = null, $content = null) { assert($path || $content); if ($path && !$content) { $hash = sha1_file($path); $size = filesize($path); $bytes = file_get_contents($path, false, null, -1, 1024); } else { $hash = sha1($content); $size = strlen($content); $bytes = substr($content, 0, 1024); } $type = \KD2\FileInfo::guessMimeType($bytes); if (!$type) { $ext = substr($name, strrpos($name, '.')+1); $ext = strtolower($ext); $type = \KD2\FileInfo::getMimeTypeFromFileExtension($ext); } $is_image = preg_match('/^image\/(?:png|jpe?g|gif)$/', $type); // Check that it's a real image if ($is_image) { try { if ($path && !$content) { $i = new Image($path); } else { $i = Image::createFromBlob($content); } // Recompress PNG files from base64, assuming they are coming // from JS canvas which doesn't know how to gzip (d'oh!) if ($i->format() == 'png' && null !== $content) { $content = $i->output('png', true); $hash = sha1($content); $size = strlen($content); } unset($i); } catch (\RuntimeException $e) { if (strstr($e->getMessage(), 'No suitable image library found')) { throw new UserException('Le serveur n\'a aucune bibliothèque de gestion d\'image installée, et ne peut donc pas accepter les images. Installez Imagick ou GD.'); } throw new UserException('Fichier image invalide'); } } $db = DB::getInstance(); $db->begin(); // Il peut arriver que l'on renvoie ici un fichier déjà stocké, auquel cas, ne pas le re-stocker if (!($id_contenu = $db->firstColumn('SELECT id FROM fichiers_contenu WHERE hash = ?;', $hash))) { $db->preparedQuery('INSERT INTO fichiers_contenu (hash, taille, contenu) VALUES (?, ?, zeroblob(?));', [$hash, (int)$size, (int)$size]); $id_contenu = $db->lastInsertRowID(); // Écrire le contenu $blob = $db->openBlob('fichiers_contenu', 'contenu', $id_contenu, 'main', SQLITE3_OPEN_READWRITE); if (null !== $content) { fwrite($blob, $content); } else{ fwrite($blob, file_get_contents($path)); } fclose($blob); } $db->insert('fichiers', [ 'id_contenu' => (int)$id_contenu, 'nom' => $name, 'type' => $type, 'image' => (int)$is_image, ]); $db->commit(); return new Fichiers($db->lastInsertRowID()); } /** * Envoie un fichier déjà stocké * * @param string $name Nom du fichier * @param string $hash Hash SHA1 du contenu du fichier * @return object Un objet Fichiers en cas de succès */ static public function uploadExistingHash($name, $hash) { $db = DB::getInstance(); $name = preg_replace('/[^\d\w._-]/ui', '', $name); $file = $db->first('SELECT * FROM fichiers INNER JOIN fichiers_contenu AS fc ON fc.id = fichiers.id_contenu AND fc.hash = ?;', trim($hash)); if (!$file) { throw new UserException('Le fichier à copier n\'existe pas (aucun hash ne correspond à '.$hash.').'); } $db->insert('fichiers', [ 'id_contenu' => (int)$file->id_contenu, 'nom' => $name, 'type' => $file->type, 'image' => (int)$file->image, ]); return new Fichiers($db->lastInsertRowID()); } /** * Récupère la liste des fichiers liés à une ressource * * @param string $type Type de ressource * @param integer $id Numéro de ressource * @param boolean|null $images TRUE pour retourner seulement les images, * FALSE pour retourner les fichiers sans images, NULL pour tout retourner * @return array Liste des fichiers */ static public function listLinkedFiles($type, $id, $images = null) { $check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA]; if (!in_array($type, $check)) { throw new \LogicException('Type de lien de fichier inconnu.'); } $images = is_null($images) ? '' : ' AND image = ' . (int)$images; $query = sprintf('SELECT fichiers.*, c.hash, c.taille FROM fichiers INNER JOIN fichiers_%s AS fwp ON fwp.fichier = fichiers.id INNER JOIN fichiers_contenu AS c ON c.id = fichiers.id_contenu WHERE fwp.id = ? %s ORDER BY fichiers.nom COLLATE NOCASE;', $type, $images); $files = DB::getInstance()->get($query, (int)$id); foreach ($files as &$file) { $file->url = self::_getURL($file->id, $file->nom, $file->hash); $file->thumb = $file->image ? self::_getURL($file->id, $file->nom, $file->hash, 200) : false; } return $files; } static public function deleteLinkedFiles($type, int $id) { static $check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA]; if (!in_array($type, $check)) { throw new \LogicException('Type de lien de fichier inconnu.'); } $files = DB::getInstance()->delete('fichiers_' . $type, 'id = ?', $id); return self::deleteUnlinkedFiles(); } static public function deleteUnlinkedFiles() { static $all = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA]; $id_background = Config::getInstance()->get('image_fond'); $list = DB::getInstance()->iterate(sprintf('SELECT f.id, f.id_contenu FROM fichiers f LEFT JOIN fichiers_%s a ON a.fichier = f.id LEFT JOIN fichiers_%s b ON b.fichier = f.id LEFT JOIN fichiers_%s c ON c.fichier = f.id WHERE a.id IS NULL AND b.id IS NULL AND c.id IS NULL;', self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA)); foreach ($list as $file) { if ($file->id == $id_background) { // FIXME: want to use something cleaner here! continue; } $f = new Fichiers($file->id, (array) $file); $f->remove(); } } /** * Enlève d'une liste de fichiers ceux qui sont mentionnés dans un texte wiki * @param array $files Liste de fichiers * @param string $text texte wiki * @return array Un tableau qui ne contient pas les fichiers mentionnés dans $text */ static public function filterFilesUsedInText($files, $text) { $used = self::listFilesUsedInText($text); return array_filter($files, function ($row) use ($used) { return !in_array($row->id, $used); }); } /** * Renvoie une liste d'ID de fichiers mentionnées dans un texte wiki * @param string $text Texte wiki * @return array Liste des IDs de fichiers mentionnés */ static public function listFilesUsedInText($text) { preg_match_all('/<<?(?:fichier|image)\s*(?:\|\s*)?(\d+)/', $text, $match, PREG_PATTERN_ORDER); preg_match_all('/(?:fichier|image):\/\/(\d+)/', $text, $match2, PREG_PATTERN_ORDER); return array_merge($match[1], $match2[1]); } /** * Callback utilisé pour l'extension <<fichier>> dans le wiki-texte * @param array $args Arguments passés à l'extension * @param string $content Contenu éventuel (en mode bloc) * @param object $skriv Objet SkrivLite */ static public function SkrivFichier($args, $content, $skriv) { $id = $caption = null; foreach ($args as $value) { if (preg_match('/^\d+$/', $value) && !$id) { $id = (int)$value; break; } else { $caption = trim($value); } } if (empty($id)) { return $skriv->parseError('/!\ Tag fichier : aucun numéro de fichier indiqué.'); } try { $file = new Fichiers($id); } catch (\InvalidArgumentException $e) { return $skriv->parseError('/!\ Tag fichier : ' . $e->getMessage()); } if (empty($caption)) { $caption = $file->nom; } $out = '<aside class="fichier" data-type="'.$skriv->escape($file->type).'">'; $out.= '<a href="'.$file->getURL().'" class="internal-file">'.$skriv->escape($caption).'</a> '; $out.= '<small>('.$skriv->escape(($file->type ? $file->type . ', ' : '') . Utils::format_bytes($file->taille)).')</small>'; $out.= '</aside>'; return $out; } /** * Callback utilisé pour l'extension <<image>> dans le wiki-texte * @param array $args Arguments passés à l'extension * @param string $content Contenu éventuel (en mode bloc) * @param object $skriv Objet SkrivLite */ static public function SkrivImage($args, $content, $skriv) { static $align_values = ['droite', 'gauche', 'centre']; $align = ''; $id = $caption = null; foreach ($args as $value) { if (preg_match('/^\d+$/', $value) && !$id) { $id = (int)$value; } else if (in_array($value, $align_values) && !$align) { $align = $value; } else { $caption = $value; } } if (!$id) { return $skriv->parseError('/!\ Tag image : aucun numéro de fichier indiqué.'); } try { $file = new Fichiers($id); } catch (\InvalidArgumentException $e) { return $skriv->parseError('/!\ Tag image : ' . $e->getMessage()); } if (!$file->image) { return $skriv->parseError('/!\ Tag image : ce fichier n\'est pas une image.'); } $out = '<a href="'.$file->getURL().'" class="internal-image">'; $out .= '<img src="'.$file->getURL($align == 'centre' ? 500 : 200).'" alt="'; if ($caption) { $out .= htmlspecialchars($caption, ENT_QUOTES, 'UTF-8'); } $out .= '" /></a>'; if (!empty($align)) { $out = '<figure class="image ' . $align . '">' . $out; if ($caption) { $out .= '<figcaption>' . htmlspecialchars($caption, ENT_QUOTES, 'UTF-8') . '</figcaption>'; } $out .= '</figure>'; } return $out; } } |
< < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added src/include/lib/Garradin/Files/Files.php version [af6eef26f4].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
<?php namespace Garradin\Files; use Garradin\Static_Cache; use Garradin\DB; use Garradin\Utils; use Garradin\ValidationException; use Garradin\Membres\Session; use Garradin\Entities\Files\File; use Garradin\Entities\Web\Page; use KD2\DB\EntityManager as EM; use const Garradin\{FILE_STORAGE_BACKEND, FILE_STORAGE_QUOTA, FILE_STORAGE_CONFIG}; class Files { static public function redirectOldWikiPage(string $uri): ?File { $uri = Utils::transformTitleToURI($uri); $db = DB::getInstance(); if ($db->test(Page::TABLE, 'uri = ?')) { Utils::redirect(ADMIN_URL . 'web/page.php?uri=' . $uri); } $file = self::get('documents/wiki/' . $uri . '.skriv'); if ($file) { Utils::redirect(ADMIN_URL . 'documents/?p=' . $file->path); } return null; } static public function list(string $path = null): array { File::validatePath($path); // Update this path self::callStorage('sync', $path); return EM::getInstance(File::class)->all('SELECT * FROM @TABLE WHERE path = ? ORDER BY type DESC, name COLLATE NOCASE ASC;', $path); } /** * Creates a new temporary table files_tmp containg all files from the path argument */ static public function listToSQL(string $path): int { $db = DB::getInstance(); $db->begin(); $columns = File::getColumns(); $db->exec(sprintf('CREATE TEMP TABLE IF NOT EXISTS files_tmp (%s);', implode(',', $columns))); $i = 0; foreach (self::list($path) as $file) { $file = $file->asArray(); unset($file['id']); $db->insert('files_tmp', $file); $i++; } $db->commit(); return $i; } static public function callStorage(string $function, ...$args) { $storage = FILE_STORAGE_BACKEND ?? 'SQLite'; $class_name = __NAMESPACE__ . '\\Storage\\' . $storage; call_user_func([$class_name, 'configure'], FILE_STORAGE_CONFIG); // Check that we can store this data if ($function == 'store') { $quota = FILE_STORAGE_QUOTA ?: self::callStorage('getQuota'); $used = self::callStorage('getTotalSize'); $size = $args[0] ? filesize($args[1]) : strlen($args[2]); if (($used + $size) >= $quota) { throw new \OutOfBoundsException('File quota has been exhausted'); } } return call_user_func_array([$class_name, $function], $args); } /** * Copy all files from a storage backend to another one * This can be used to move from SQLite to FileSystem for example * Note that this only copies files, and is not removing them from the source storage backend. */ static public function migrateStorage(string $from, string $to, $from_config = null, $to_config = null, ?callable $callback = null): void { $from = __NAMESPACE__ . '\\Storage\\' . $from; $to = __NAMESPACE__ . '\\Storage\\' . $to; if (!class_exists($from)) { throw new \InvalidArgumentException('Invalid storage: ' . $from); } if (!class_exists($to)) { throw new \InvalidArgumentException('Invalid storage: ' . $to); } call_user_func([$from, 'configure'], $from_config); call_user_func([$to, 'configure'], $to_config); try { call_user_func([$from, 'checkLock']); call_user_func([$to, 'checkLock']); //call_user_func([$from, 'lock']); //call_user_func([$to, 'lock']); $db = DB::getInstance(); $db->begin(); foreach ($db->iterate('SELECT * FROM files ORDER BY type DESC, path ASC, name ASC;') as $file) { $f = new File; $f->load((array) $file); if ($f->type == File::TYPE_DIRECTORY) { call_user_func([$to, 'mkdir'], $f); } else { $from_path = call_user_func([$from, 'getFullPath'], $f); call_user_func([$to, 'storePath'], $f, $from_path); } if (null !== $callback) { $callback($f); } } $db->commit(); } finally { call_user_func([$from, 'unlock']); call_user_func([$to, 'unlock']); } } /** * Delete all files from a storage backend */ static public function truncateStorage(string $backend, $config = null): void { $backend = __NAMESPACE__ . '\\Storage\\' . $backend; call_user_func([$backend, 'configure'], $config); if (!class_exists($backend)) { throw new \InvalidArgumentException('Invalid storage: ' . $from); } call_user_func([$backend, 'truncate']); } static public function getContextJoinClause(string $context): ?string { switch ($context) { case File::CONTEXT_TRANSACTION: return 'acc_transactions c ON c.id = f.context_ref'; case File::CONTEXT_USER: return 'membres c ON c.id = f.context_ref'; case File::CONTEXT_FILE: return 'files c ON c.id = f.context_ref'; case File::CONTEXT_CONFIG: return 'config c ON c.key = f.context_ref AND c.value = f.id'; case File::CONTEXT_WEB: case File::CONTEXT_DOCUMENTS: case File::CONTEXT_SKELETON: default: return null; } } static public function get(?string $path, ?string $name = null): ?File { if (null === $path) { return null; } $fullpath = $path; if ($name) { $fullpath .= '/' . $name; } try { File::validatePath($fullpath); } catch (ValidationException $e) { return null; } $path = dirname($fullpath); $name = basename($fullpath); $file = EM::findOne(File::class, 'SELECT * FROM @TABLE WHERE path = ? AND name = ? LIMIT 1;', $path, $name); if (null !== $file) { $file = self::callStorage('update', $file); } return $file; } static public function getFromURI(string $uri): ?File { $uri = trim($uri, '/'); $uri = rawurldecode($uri); $context = substr($uri, 0, strpos($uri, '/')); // Use alias for web files if (!array_key_exists($context, File::CONTEXTS_NAMES)) { $uri = File::CONTEXT_WEB . '/' . $uri; } return self::get($uri); } static public function getContext(string $path): ?string { $context = strtok($path, '/'); if (!array_key_exists($context, File::CONTEXTS_NAMES)) { return null; } return $context; } } |
Added src/include/lib/Garradin/Files/Storage/FileSystem.php version [df8a604177].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 296 |
<?php namespace Garradin\Files\Storage; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\DB; use Garradin\Utils; use const Garradin\FILE_STORAGE_CONFIG; /** * This class provides storage in the file system * You need to configure FILE_STORAGE_CONFIG to give a file path */ class FileSystem implements StorageInterface { static protected $_size; static protected $_root; static public function configure(?string $config): void { if (!$config) { throw new \RuntimeException('Le stockage de fichier n\'a pas été configuré (FILE_STORAGE_CONFIG est vide).'); } if (!is_writable($config) && !Utils::safe_mkdir($config)) { throw new \RuntimeException('Le répertoire de stockage des fichiers est protégé contre l\'écriture.'); } $target = rtrim($config, DIRECTORY_SEPARATOR); self::$_root = realpath($target); } static protected function _getRoot() { if (!self::$_root) { throw new \RuntimeException('Le stockage de fichier n\'a pas été configuré (FILE_STORAGE_CONFIG est vide ?).'); } return self::$_root; } static protected function ensureDirectoryExists(string $path): void { if (is_dir($path)) { return; } $permissions = fileperms(self::_getRoot(null)); Utils::safe_mkdir($path, $permissions & 0777, true); } static public function storePath(File $file, string $path): bool { $target = self::getFullPath($file); self::ensureDirectoryExists(dirname($target)); return copy($path, $target); } static public function storeContent(File $file, string $content): bool { $target = self::getFullPath($file); self::ensureDirectoryExists(dirname($target)); return file_put_contents($target, $content) === false ? false : true; } static public function mkdir(File $file): bool { return Utils::safe_mkdir(self::getFullPath($file)); } static protected function _getRealPath(string $path) { return self::_getRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path); } static public function getFullPath(File $file): ?string { return self::_getRealPath($file->pathname()); } static public function display(File $file): void { readfile(self::getFullPath($file)); } static public function fetch(File $file): string { return file_get_contents(self::getFullPath($file)); } static public function delete(File $file): bool { $path = self::getFullPath($file); if (is_dir($path)) { return rmdir($path); } else { return unlink($path); } } static public function move(File $file, string $new_path): bool { $source = self::getFullPath($file); $target = self::_getRealPath($new_path); self::ensureDirectoryExists(dirname($target)); return rename($source, $target); } static public function exists(string $path): bool { return (bool) file_exists(self::_getRealPath($path)); } static public function modified(File $file): ?int { return filemtime(self::getFullPath($path)) ?: null; } static public function getTotalSize(): int { if (null !== self::$_size) { return self::$_size; } $total = 0; $path = self::_getRoot(); foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)) as $p) { $total += $p->getSize(); } self::$_size = (int) $total; return self::$_size; } /** * @see https://www.crazyws.fr/dev/fonctions-php/fonction-disk-free-space-et-disk-total-space-pour-ovh-2JMH9.html * @see https://github.com/jdel/sspks/commit/a890e347f32e9e3e50a0dd82398947633872bf38 */ static public function getQuota(): int { return @disk_total_space(self::_getRoot()) ?: \PHP_INT_MAX; } static public function truncate(): void { Utils::deleteRecursive(self::_getRoot()); } static public function lock(): void { touch(self::_getRoot() . DIRECTORY_SEPARATOR . '.lock'); } static public function unlock(): void { Utils::safe_unlink(self::_getRoot() . DIRECTORY_SEPARATOR . '.lock'); } static public function checkLock(): void { $lock = file_exists(self::_getRoot() . DIRECTORY_SEPARATOR . '.lock'); if ($lock) { throw new \RuntimeException('File storage is locked'); } } static public function sync(?string $path): void { $fullpath = self::_getRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path); $fullpath = rtrim($fullpath, DIRECTORY_SEPARATOR); if (!file_exists($fullpath)) { return; } $db = DB::getInstance(); $saved_files = $db->getGrouped('SELECT name, size, modified, type FROM files WHERE path = ?;', $path); $added = []; $deleted = []; $modified = []; $exists = []; foreach (new \FilesystemIterator($fullpath, \FilesystemIterator::SKIP_DOTS) as $file) { $name = $file->getFilename(); $data = [ 'path' => $path, 'name' => $name, 'modified' => null, 'size' => null, ]; if ($file->isDir()) { $data['type'] = File::TYPE_DIRECTORY; } else { $data['type'] = File::TYPE_FILE; $data['modified'] = $file->getMTime(); $data['size'] = $file->getSize(); $data['mime'] = mime_content_type($file->getRealpath()); } $exists[$name] = null; if (!array_key_exists($name, $saved_files)) { $added[] = $data; } elseif ($saved_files[$name]->modified < $data['modified']) { $modified[] = $data; } } foreach ($modified as $file) { // This will call 'update' method Files::get($file['path'], $file['name']); } $deleted = array_diff_key($saved_files, $exists); foreach ($deleted as $file) { $type = $saved_files[$file['name']]->type; if ($type == File::TYPE_DIRECTORY) { $sql = 'DELETE FROM files WHERE path = ? OR path LIKE ? OR (path = ? AND name = ?);'; $path = $file['path'] . '/' . $file['name']; $params = [$path, $path . '/%', $file['path'], $file['name']]; } else { $sql = 'DELETE FROM files WHERE path = ? AND name = ?;'; $params = [$file['path'], $file['name']]; } $db->exec($sql); } } static public function update(File $file): ?File { $path = self::getFullPath($file); // File has disappeared if (!file_exists($path)) { return null; } $type = is_dir($path) ? File::TYPE_DIRECTORY : File::TYPE_FILE; // Directories don't have a modified time here if ($type == File::TYPE_DIRECTORY && $file->type == File::TYPE_DIRECTORY) { return $file; } $modified = filemtime($path); if ($modified <= $file->modified->getTimestamp()) { return $file; } if ($type == File::TYPE_DIRECTORY) { $file->modified = null; $file->size = null; $file->mime = null; $file->image = null; } else { // Short trick to return a local timezone date time $file->modified = \DateTime::createFromFormat('!Y-m-d H:i:s', date('Y-m-d H:i:s', $modified)); $file->size = filesize($path); $finfo = \finfo_open(\FILEINFO_MIME_TYPE); $file->mime = finfo_file($finfo, $path); if ($type != $file->type) { $file->type = $type; } } $file->save(); return $file; } } |
Added src/include/lib/Garradin/Files/Storage/SQLite.php version [a201174305].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 |
<?php namespace Garradin\Files\Storage; use Garradin\Entities\Files\File; use Garradin\Static_Cache; use Garradin\DB; use KD2\DB\EntityManager as EM; use const Garradin\{DB_FILE, DATA_ROOT}; class SQLite implements StorageInterface { static public function configure(?string $config): void { } /** * Renvoie le chemin vers le fichier local en cache, et le crée s'il n'existe pas * @return string Chemin local */ static protected function _getFilePathFromCache(File $file): string { $cache_id = 'files.' . $file->pathHash(); if (!Static_Cache::exists($cache_id)) { $db = DB::getInstance(); try { $blob = $db->openBlob('files_contents', 'content', $file->id()); } catch (\Exception $e) { if (!strstr($e->getMessage(), 'no such rowid')) { throw $e; } throw new \RuntimeException('File does not exist in DB: ' . $file->pathname()); } Static_Cache::storeFromPointer($cache_id, $blob); fclose($blob); } return Static_Cache::getPath($cache_id); } static public function storePath(File $file, string $source_path): bool { return self::store($file, $source_path, null); } static public function storeContent(File $file, string $source_content): bool { return self::store($file, null, $source_content); } static protected function store(File $file, ?string $source_path, ?string $source_content): bool { if (!isset($source_path) && !isset($source_content)) { throw new \InvalidArgumentException('Either source_path or source_content must be supplied'); } $db = DB::getInstance(); $st = $db->preparedQuery('INSERT OR REPLACE INTO files_contents (id, content) VALUES (?, zeroblob(?));', $file->id(), $file->size); $blob = $db->openBlob('files_contents', 'content', $file->id(), 'main', \SQLITE3_OPEN_READWRITE); if (null !== $source_content) { fwrite($blob, $source_content); } else { fwrite($blob, file_get_contents($source_path)); } fclose($blob); $cache_id = 'files.' . $file->pathHash(); Static_Cache::remove($cache_id); return true; } static public function getFullPath(File $file): ?string { return self::_getFilePathFromCache($file); } static public function display(File $file): void { readfile(self::getFullPath($file)); } static public function fetch(File $file): string { return file_get_contents(self::getFullPath($file)); } static public function modified(File $file): ?int { return $file->modified ?? time(); } static public function exists(string $path): bool { return DB::getInstance()->test('files', 'path = ? AND name = ?', dirname($path), basename($path)); } static public function delete(File $file): bool { $db = DB::getInstance(); $cache_id = 'files.' . $file->pathHash(); Static_Cache::remove($cache_id); return $db->delete('files_contents', 'id = ?', $file->id()); } static public function move(File $file, string $new_path): bool { return true; } static public function mkdir(File $file): bool { $db = DB::getInstance(); $path = $file->pathname(); // Recursive mkdir of parent directories while ($test_path = dirname($path)) { if (!$db->test('files', 'path = ? AND name = ?', dirname($test_path), basename($test_path))) { self::mkdir($test_path); } } return $db->insert('files', [ 'type' => File::TYPE_DIRECTORY, 'path' => dirname($path), 'name' => basename($path), ]); } static public function getTotalSize(): int { return (int) DB::getInstance()->firstColumn('SELECT SUM(LENGTH(content)) FROM files_contents;'); } /** * @see https://www.crazyws.fr/dev/fonctions-php/fonction-disk-free-space-et-disk-total-space-pour-ovh-2JMH9.html * @see https://github.com/jdel/sspks/commit/a890e347f32e9e3e50a0dd82398947633872bf38 */ static public function getQuota(): int { return @disk_total_space(DATA_ROOT) ?: \PHP_INT_MAX; } static public function truncate(): void { $db = DB::getInstance(); $db->exec('DELETE FROM files_contents; VACUUM;'); } static public function lock(): void { DB::getInstance()->exec('INSERT INTO files (name, path) VALUES (\'.lock\', \'.lock\');'); } static public function unlock(): void { DB::getInstance()->exec('DELETE FROM files WHERE path = \'.lock\';'); } static public function checkLock(): void { $lock = DB::getInstance()->firstColumn('SELECT 1 FROM files WHERE path = \'.lock\';'); if ($lock) { throw new \RuntimeException('File storage is locked'); } } /** * We don't need to do anything there as everything is already in DB */ static public function sync(?string $path): void { return; } static public function update(File $file): ?File { return $file; } } |
Added src/include/lib/Garradin/Files/Storage/StorageInterface.php version [d5b48a6174].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 |
<?php namespace Garradin\Files\Storage; use Garradin\Entities\Files\File; interface StorageInterface { /** * Configures the storage backend for subsequent calls */ static public function configure(?string $config): void; /** * Stores a file in this backend from a local path */ static public function storePath(File $file, string $source_path): bool; /** * Stores a file from a content binary string */ static public function storeContent(File $file, string $source_content): bool; /** * Create an empty directory */ static public function mkdir(File $file): bool; /** * Should return full local file access path. * If storage backend cannot store the file locally, return NULL. * In that case a subsequent call to fetch() will be done. */ static public function getFullPath(File $file): ?string; /** * Returns the binary of a content to php://output */ static public function display(File $file): void; /** * Returns the binary content of a file */ static public function fetch(File $file): string; /** * Delete a file */ static public function delete(File $file): bool; /** * Gets modified timestamp */ static public function modified(File $file): ?int; /** * Return TRUE if file exists */ static public function exists(string $path): bool; /** * Moves a file to a new path, when its name or path has changed */ static public function move(File $file, string $new_path): bool; /** * Return total size of used space by files stored in this backed */ static public function getTotalSize(): int; /** * Return available disk space * This will only be called if FILE_STORAGE_QUOTA constant is null */ static public function getQuota(): int; /** * Delete all stored content in this backend */ static public function truncate(): void; /** * Lock storage backend against changes * This is used when migrating from one storage to another */ static public function lock(): void; /** * Unlock storage backend against changes */ static public function unlock(): void; /** * Throw a \RuntimeException if the lock is active */ static public function checkLock(): void; /** * Update metadata in database from local directory * This is called before listing a directory * * The backend must list files for this path and update/add/delete them * using File entities. * * @param string $path Parent path * @return void */ static public function sync(?string $path): void; /** * Update metadata of a file if needed, before getting it * * This is called before getting any metadata of a file * * @param File $file * @return File modified File object, or NULL if the file no longer exists */ static public function update(File $file): ?File; } |
Modified src/include/lib/Garradin/Form.php from [8f5c821c3d] to [a495b167f2].
35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
return false; } try { call_user_func($fn); if (null !== $redirect) { Utils::redirect($redirect); } return true; } catch (UserException $e) { $this->addError($e->getMessage()); |
> > > > |
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
return false; } try { call_user_func($fn); if (null !== $redirect) { if (array_key_exists('_dialog', $_GET)) { Utils::reloadParentFrame($redirect); } Utils::redirect($redirect); } return true; } catch (UserException $e) { $this->addError($e->getMessage()); |
Modified src/include/lib/Garradin/Install.php from [1e3ce59b3f] to [ca3b2fcfa3].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .. 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 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 ... 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
<?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Chart; use Garradin\Entities\Accounting\Year; /** * Pour procéder à l'installation de l'instance Garradin * Utile pour automatiser l'installation sans passer par la page d'installation */ class Install { ................................................................................ // Force l'installation de plugin système Plugin::checkAndInstallSystemPlugins(); } return $ok; } static public function install($nom_asso, $nom_membre, $email_membre, $passe_membre) { $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;'); $db->exec('VACUUM;'); // Création de la base de données $db->begin(); $db->exec('PRAGMA application_id = ' . DB::APPID . ';'); $db->exec(file_get_contents(DB_SCHEMA)); $db->commit(); // Configuration de base // c'est dans Config::set que sont vérifiées les données utilisateur (renvoie UserException) $config = Config::getInstance(); $config->set('nom_asso', $nom_asso); $config->set('email_asso', $email_membre); $config->set('site_asso', WWW_URL); $config->set('monnaie', '€'); $config->set('pays', 'FR'); $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); $id = $cats->add([ 'nom' => 'Anciens membres', 'droit_inscription' => Membres::DROIT_AUCUN, 'droit_wiki' => Membres::DROIT_AUCUN, 'droit_membres' => Membres::DROIT_AUCUN, 'droit_compta' => Membres::DROIT_AUCUN, 'droit_config' => Membres::DROIT_AUCUN, 'droit_connexion' => Membres::DROIT_AUCUN, 'cacher' => 1, ]); $id = $cats->add([ 'nom' => 'Administrateurs', 'droit_inscription' => Membres::DROIT_AUCUN, 'droit_wiki' => Membres::DROIT_ADMIN, 'droit_membres' => Membres::DROIT_ADMIN, 'droit_compta' => Membres::DROIT_ADMIN, 'droit_config' => Membres::DROIT_ADMIN, ]); // Création premier membre $membres = new Membres; $id_membre = $membres->add([ 'id_categorie' => $id, 'nom' => $nom_membre, 'email' => $email_membre, 'passe' => $passe_membre, 'pays' => 'FR', ]); // Création wiki $page = Wiki::transformTitleToURI($nom_asso); $wiki = new Wiki; $id_page = $wiki->create([ 'titre' => $nom_asso, 'uri' => $page, ]); $wiki->editRevision($id_page, 0, [ 'id_auteur' => $id_membre, 'contenu' => "Bienvenue dans le wiki de ".$nom_asso." !\n\nCliquez sur le bouton « éditer » pour modifier cette page.", ]); // Création page wiki connexion $page = Wiki::transformTitleToURI('Bienvenue'); $id_page = $wiki->create([ 'titre' => 'Bienvenue', 'uri' => $page, ]); $config->set('accueil_wiki', $page); $wiki->editRevision($id_page, 0, [ 'id_auteur' => $id_membre, 'contenu' => "Bienvenue dans l'administration de ".$nom_asso." !\n\n" . "Utilisez le menu à gauche pour accéder aux différentes rubriques.", ]); $config->set('accueil_connexion', $page); // Import plan comptable $chart = new Chart; $chart->label = 'Plan comptable associatif 2020 (Règlement ANC n°2018-06)'; $chart->country = 'FR'; $chart->code = 'PCA2018'; $chart->save(); $chart->accounts()->importCSV(ROOT . '/include/data/charts/fr_2018.csv'); // Premier exercice $year = new Year; $year->label = sprintf('Exercice %d', date('Y')); $year->start_date = new \DateTime('January 1st'); $year->end_date = new \DateTime('December 31'); $year->id_chart = $chart->id(); $year->save(); // Compte bancaire $account = new Account; $account->import([ 'label' => 'Compte courant', 'code' => '512A', 'type' => Account::TYPE_BANK, 'position' => Account::ASSET_OR_LIABILITY, 'id_chart' => $chart->id(), 'user' => 1, ]); $account->save(); // Ajout d'une recherche avancée en exemple (membres) $query = (object) [ 'query' => [[ 'operator' => 'AND', 'conditions' => [ [ 'column' => 'lettre_infos', 'operator' => '= 1', ................................................................................ ]], 'order' => 'numero', 'desc' => true, 'limit' => '10000', ]; $recherche = new Recherche; $recherche->add('Membres inscrits à la lettre d\'information', null, $recherche::TYPE_JSON, 'membres', $query); // Ajout d'une recherche avancée en exemple (compta) $query = (object) [ 'query' => [[ 'operator' => 'AND', 'conditions' => [ [ 'column' => 'a2.code', 'operator' => 'IS NULL', |
> > > > > > > > > > | < > > | < < < < > > > > > > > > > > > > > > > > > > > > > > > > | | < < > | > | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < | | | | | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .. 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 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 ... 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
<?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Chart; use Garradin\Entities\Accounting\Year; use Garradin\Entities\Users\Category; use Garradin\Entities\Files\File; use Garradin\Membres\Session; /** * Pour procéder à l'installation de l'instance Garradin * Utile pour automatiser l'installation sans passer par la page d'installation */ class Install { ................................................................................ // Force l'installation de plugin système Plugin::checkAndInstallSystemPlugins(); } return $ok; } static protected function assert(bool $assertion, string $message) { if (!$assertion) { throw new ValidationException($message); } } static public function installFromForm(array $source = null) { if (null === $source) { $source = $_POST; } self::assert(isset($source['name']) && trim($source['name']) !== '', 'Le nom de l\'association n\'est pas renseigné'); self::assert(isset($source['user_name']) && trim($source['user_name']) !== '', 'Le nom du membre n\'est pas renseigné'); self::assert(isset($source['user_email']) && trim($source['user_email']) !== '', 'L\'adresse email du membre n\'est pas renseignée'); self::assert(isset($source['user_password']) && isset($source['user_password_confirm']) && trim($source['user_password']) !== '', 'Le mot de passe n\'est pas renseigné'); self::assert((bool)filter_var($source['user_email'], FILTER_VALIDATE_EMAIL), 'Adresse email invalide'); self::assert(strlen($source['user_password']) >= Session::MINIMUM_PASSWORD_LENGTH, 'Le mot de passe est trop court'); self::assert($source['user_password'] === $source['user_password_confirm'], 'La vérification du mot de passe ne correspond pas'); try { return self::install($source['name'], $source['user_name'], $source['user_email'], $source['user_password']); } catch (\Exception $e) { @unlink(DB_FILE); throw $e; } } static public function install(string $name, string $user_name, string $user_email, string $user_password, ?string $welcome_text = null) { $db = DB::getInstance(true); // Création de la base de données $db->begin(); $db->exec('PRAGMA application_id = ' . DB::APPID . ';'); $db->setVersion(garradin_version()); $db->exec(file_get_contents(DB_SCHEMA)); $db->commit(); // Configuration de base // c'est dans Config::set que sont vérifiées les données utilisateur (renvoie UserException) $config = Config::getInstance(); $config->set('nom_asso', $name); $config->set('email_asso', $user_email); $config->set('monnaie', '€'); $config->set('pays', 'FR'); $config->set('desactiver_site', true); $champs = Membres\Champs::importInstall(); $champs->create(); // Pas de copie car pas de table membres existante $config->set('champs_membres', $champs); $config->set('champ_identifiant', 'email'); $config->set('champ_identite', 'nom'); // Create default category for common users $cat = new Category; $cat->setAllPermissions(Session::ACCESS_NONE); $cat->importForm([ 'name' => 'Membres actifs', 'perm_connect' => Session::ACCESS_READ, ]); $cat->save(); $config->set('categorie_membres', $cat->id()); // Create default category for ancient users $cat = new Category; $cat->importForm([ 'name' => 'Anciens membres', 'hidden' => 1, ]); $cat->setAllPermissions(Session::ACCESS_NONE); $cat->save(); // Create default category for admins $cat = new Category; $cat->importForm([ 'name' => 'Administrateurs', ]); $cat->setAllPermissions(Session::ACCESS_ADMIN); $cat->save(); // Create first user $membres = new Membres; $id_membre = $membres->add([ 'category_id' => $cat->id(), 'nom' => $user_name, 'email' => $user_email, 'passe' => $user_password, 'pays' => 'FR', ]); $welcome_text = $welcome_text ?? sprintf("Bienvenue dans l'administration de %s !\n\nUtilisez le menu à gauche pour accéder aux différentes sections.", $name); $file = File::createAndStore(File::CONTEXT_CONFIG, 'admin_homepage.skriv', null, $welcome_text); $config->set('admin_homepage', $file); // Import accounting chart $chart = new Chart; $chart->label = 'Plan comptable associatif 2020 (Règlement ANC n°2018-06)'; $chart->country = 'FR'; $chart->code = 'PCA2018'; $chart->save(); $chart->accounts()->importCSV(ROOT . '/include/data/charts/fr_2018.csv'); // Create first accounting year $year = new Year; $year->label = sprintf('Exercice %d', date('Y')); $year->start_date = new \DateTime('January 1st'); $year->end_date = new \DateTime('December 31'); $year->id_chart = $chart->id(); $year->save(); // Create a first bank account $account = new Account; $account->import([ 'label' => 'Compte courant', 'code' => '512A', 'type' => Account::TYPE_BANK, 'position' => Account::ASSET_OR_LIABILITY, 'id_chart' => $chart->id(), 'user' => 1, ]); $account->save(); // Create an example saved search (users) $query = (object) [ 'query' => [[ 'operator' => 'AND', 'conditions' => [ [ 'column' => 'lettre_infos', 'operator' => '= 1', ................................................................................ ]], 'order' => 'numero', 'desc' => true, 'limit' => '10000', ]; $recherche = new Recherche; $recherche->add('Inscrits à la lettre d\'information', null, $recherche::TYPE_JSON, 'membres', $query); // Create an example saved search (accounting) $query = (object) [ 'query' => [[ 'operator' => 'AND', 'conditions' => [ [ 'column' => 'a2.code', 'operator' => 'IS NULL', |
Modified src/include/lib/Garradin/Membres.php from [084ee701a4] to [0a092a3ef6].
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ... 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 ... 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 ... 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 ... 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 ... 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 ... 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 |
namespace Garradin; use KD2\Security; use KD2\SMTP; use Garradin\Membres\Session; class Membres { const DROIT_AUCUN = 0; const DROIT_ACCES = 1; const DROIT_ECRITURE = 2; const DROIT_ADMIN = 9; const ITEMS_PER_PAGE = 50; // Gestion des données /////////////////////////////////////////////////////// public function _checkFields(&$data, $check_editable = true, $check_password = true) { $champs = Config::getInstance()->get('champs_membres'); ................................................................................ $data['passe'] = Session::hashPassword($data['passe']); } else { unset($data['passe']); } if (empty($data['id_categorie'])) { $data['id_categorie'] = Config::getInstance()->get('categorie_membres'); } $db->insert('membres', $data); $id = $db->lastInsertRowId(); Plugin::fireSignal('membre.nouveau', array_merge(['id' => $id], $data)); ................................................................................ $data['passe'] = Session::hashPassword($data['passe']); } else { unset($data['passe']); } if (isset($data['id_categorie']) && empty($data['id_categorie'])) { $data['id_categorie'] = Config::getInstance()->get('categorie_membres'); } if (empty($data)) { return true; } ................................................................................ public function delete($ids) { if (!is_array($ids)) { $ids = [(int)$ids]; } $session = new Session; if ($session->isLogged()) { $user = $session->getUser(); foreach ($ids as $id) { ................................................................................ { Utils::sendEmail(Utils::EMAIL_CONTEXT_BULK, $config->get('email_asso'), $subject, $message); } return true; } public function listAllByCategory($id_categorie, $only_with_email = false) { $where = $only_with_email ? ' AND email IS NOT NULL' : ''; return DB::getInstance()->get('SELECT id, email FROM membres WHERE id_categorie = ?' . $where, (int)$id_categorie); } public function listByCategory(?int $category_id): DynamicList { $config = Config::getInstance(); $db = DB::getInstance(); $identity = $config->get('champ_identite'); ................................................................................ if ($champs->isText($key)) { $columns[$key]['order'] = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE %%s', $db->quoteIdentifier($key)); } } $tables = 'membres'; $conditions = $category_id ? sprintf('id_categorie = %d', $category_id) : sprintf('id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)'); $order = $identity; if (!isset($columns[$order])) { $order = $champs->getFirstListed(); } ................................................................................ { $db = DB::getInstance(); $query = 'SELECT COUNT(*) FROM membres '; if (is_int($cat) && $cat) { $query .= sprintf('WHERE id_categorie = %d', $cat); } elseif (is_array($cat)) { $query .= sprintf('WHERE id_categorie IN (%s)', implode(',', $cat)); } $query .= ';'; return $db->firstColumn($query); } public function countAllButHidden() { $db = DB::getInstance(); return $db->firstColumn('SELECT COUNT(*) FROM membres WHERE id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1);'); } static public function changeCategorie($id_cat, $membres) { foreach ($membres as &$id) { $id = (int) $id; } $db = DB::getInstance(); return $db->update('membres', ['id_categorie' => (int)$id_cat], sprintf('id IN (%s)', implode(',', $membres)) ); } 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)); } } |
| > | < | < < | | | | | | | | | | | | > > > > > | | < < < < < < < |
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ... 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 ... 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 ... 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 ... 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 ... 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 ... 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 |
namespace Garradin; use KD2\Security; use KD2\SMTP; use Garradin\Membres\Session; use Garradin\Files\Files; use Garradin\Entities\Files\File; class Membres { const ITEMS_PER_PAGE = 50; // Gestion des données /////////////////////////////////////////////////////// public function _checkFields(&$data, $check_editable = true, $check_password = true) { $champs = Config::getInstance()->get('champs_membres'); ................................................................................ $data['passe'] = Session::hashPassword($data['passe']); } else { unset($data['passe']); } if (empty($data['category_id'])) { $data['category_id'] = Config::getInstance()->get('categorie_membres'); } $db->insert('membres', $data); $id = $db->lastInsertRowId(); Plugin::fireSignal('membre.nouveau', array_merge(['id' => $id], $data)); ................................................................................ $data['passe'] = Session::hashPassword($data['passe']); } else { unset($data['passe']); } if (isset($data['category_id']) && empty($data['category_id'])) { $data['category_id'] = Config::getInstance()->get('categorie_membres'); } if (empty($data)) { return true; } ................................................................................ public function delete($ids) { if (!is_array($ids)) { $ids = [(int)$ids]; } $session = Session::getInstance(); if ($session->isLogged()) { $user = $session->getUser(); foreach ($ids as $id) { ................................................................................ { Utils::sendEmail(Utils::EMAIL_CONTEXT_BULK, $config->get('email_asso'), $subject, $message); } return true; } public function listAllByCategory($category_id, $only_with_email = false) { $where = $only_with_email ? ' AND email IS NOT NULL' : ''; return DB::getInstance()->get('SELECT id, email FROM membres WHERE category_id = ?' . $where, (int)$category_id); } public function listByCategory(?int $category_id): DynamicList { $config = Config::getInstance(); $db = DB::getInstance(); $identity = $config->get('champ_identite'); ................................................................................ if ($champs->isText($key)) { $columns[$key]['order'] = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE %%s', $db->quoteIdentifier($key)); } } $tables = 'membres'; $conditions = $category_id ? sprintf('category_id = %d', $category_id) : sprintf('category_id NOT IN (SELECT id FROM users_categories WHERE hidden = 1)'); $order = $identity; if (!isset($columns[$order])) { $order = $champs->getFirstListed(); } ................................................................................ { $db = DB::getInstance(); $query = 'SELECT COUNT(*) FROM membres '; if (is_int($cat) && $cat) { $query .= sprintf('WHERE category_id = %d', $cat); } elseif (is_array($cat)) { $query .= sprintf('WHERE category_id IN (%s)', implode(',', $cat)); } $query .= ';'; return $db->firstColumn($query); } public function countAllButHidden() { $db = DB::getInstance(); return $db->firstColumn('SELECT COUNT(*) FROM membres WHERE category_id NOT IN (SELECT id FROM users_categories WHERE hidden = 1);'); } public function getFilesPath(int $id) { return File::CONTEXT_USER . '/' . $id; } static public function changeCategorie($id_cat, $membres) { foreach ($membres as &$id) { $id = (int) $id; } $db = DB::getInstance(); return $db->update('membres', ['category_id' => (int)$id_cat], sprintf('id IN (%s)', implode(',', $membres)) ); } static protected function _deleteMembres($membres) { foreach ($membres as &$id) { $id = (int) $id; Files::deleteLinkedFiles(File::CONTEXT_USER, $id); } Plugin::fireSignal('membre.suppression', $membres); $db = DB::getInstance(); // Suppression du membre return $db->delete('membres', $db->where('id', $membres)); } } |
Deleted src/include/lib/Garradin/Membres/Categories.php version [2ef27a72a7].
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 |
<?php namespace Garradin\Membres; use Garradin\Membres; use Garradin\Config; use Garradin\DB; use Garradin\Wiki; use Garradin\UserException; class Categories { protected $droits = [ 'inscription'=> Membres::DROIT_AUCUN, 'connexion' => Membres::DROIT_ACCES, 'membres' => Membres::DROIT_AUCUN, 'compta' => Membres::DROIT_AUCUN, 'wiki' => Membres::DROIT_ACCES, 'config' => Membres::DROIT_AUCUN, ]; protected function _checkData(&$data) { $db = DB::getInstance(); if (!isset($data['nom']) || !trim($data['nom'])) { throw new UserException('Le nom de catégorie ne peut rester vide.'); } } public function add($data) { $this->_checkData($data); foreach ($this->droits as $key=>$value) { if (!isset($data['droit_'.$key])) $data['droit_'.$key] = $value; else $data['droit_'.$key] = (int)$data['droit_'.$key]; } $db = DB::getInstance(); $db->insert('membres_categories', $data); return $db->lastInsertRowId(); } public function edit($id, $data) { $this->_checkData($data); foreach ($this->droits as $key=>$value) { if (isset($data['droit_'.$key])) $data['droit_'.$key] = (int)$data['droit_'.$key]; } if (!isset($data['cacher']) || $data['cacher'] != 1) $data['cacher'] = 0; $db = DB::getInstance(); return $db->update('membres_categories', $data, 'id = '.(int)$id); } public function get($id) { $db = DB::getInstance(); return $db->first('SELECT * FROM membres_categories WHERE id = ?;', (int) $id); } public function remove($id) { $db = DB::getInstance(); $config = Config::getInstance(); if ($id == $config->get('categorie_membres')) { throw new UserException('Il est interdit de supprimer la catégorie définie par défaut dans la configuration.'); } if ($db->test('membres', 'id_categorie = ?', (int)$id)) { throw new UserException('La catégorie contient encore des membres, il n\'est pas possible de la supprimer.'); } $db->update( 'wiki_pages', [ 'droit_lecture' => Wiki::LECTURE_NORMAL, 'droit_ecriture' => Wiki::ECRITURE_NORMAL, ], 'droit_lecture = '.(int)$id.' OR droit_ecriture = '.(int)$id ); return $db->delete('membres_categories', 'id = ?', (int) $id); } public function listSimple() { $db = DB::getInstance(); return $db->getAssoc('SELECT id, nom FROM membres_categories ORDER BY nom;'); } public function listComplete() { $db = DB::getInstance(); return $db->get('SELECT * FROM membres_categories ORDER BY nom;'); } public function listCompleteWithStats() { $db = DB::getInstance(); return $db->get('SELECT *, (SELECT COUNT(*) FROM membres WHERE id_categorie = membres_categories.id) AS nombre FROM membres_categories ORDER BY nom;'); } public function listHidden() { $db = DB::getInstance(); return $db->getAssoc('SELECT id, nom FROM membres_categories WHERE cacher = 1;'); } public function listNotHidden() { $db = DB::getInstance(); return $db->getAssoc('SELECT id, nom FROM membres_categories WHERE cacher = 0;'); } } |
< < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/include/lib/Garradin/Membres/Champs.php from [5d64a9e45c] to [e89e12fd7c].
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 ... 173 174 175 176 177 178 179 180 181 182 183 184 185 186 ... 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 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 |
use Garradin\Config; use Garradin\DB; use Garradin\Utils; use Garradin\UserException; class Champs { protected $champs = null; protected $system_fields = [ 'date_connexion', 'date_inscription', 'clef_pgp', 'secret_otp', 'id', 'id_categorie', ]; protected $types = [ 'email' => 'Adresse E-Mail', 'url' => 'Adresse URL', 'checkbox' => 'Case à cocher', 'date' => 'Date', 'datetime' => 'Date et heure', //'file' => 'Fichier', 'password' => 'Mot de passe', 'number' => 'Numéro', 'tel' => 'Numéro de téléphone', 'select' => 'Sélecteur à choix unique', 'multiple' => 'Sélecteur à choix multiple', 'country' => 'Sélecteur de pays', 'text' => 'Texte', ................................................................................ public function getList() { $champs = clone $this->champs; unset($champs->passe); return $champs; } public function getMultiples() { $out = []; foreach ($this->champs as $id => $champ) { if ($champ->type == 'multiple') { ................................................................................ } $this->champs = $champs; return true; } /** * 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(); // 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', 'id_categorie' => 'id_categorie', 'date_connexion' => 'date_connexion', 'date_inscription' => 'date_inscription', ]; $anciens_champs = $config->get('champs_membres'); $anciens_champs = is_null($anciens_champs) ? $this->champs : $anciens_champs->getAll(); if (property_exists($anciens_champs, 'secret_otp')) { $copy['secret_otp'] = 'secret_otp'; $copy['clef_pgp'] = 'clef_pgp'; } foreach ($this->champs as $key=>$cfg) { if ($cfg->type == 'number' || $cfg->type == 'multiple' || $cfg->type == 'checkbox') $type = 'INTEGER'; elseif ($cfg->type == 'file') $type = 'BLOB'; else $type = 'TEXT'; $line = sprintf('"%s" %s,', $key, $type); if (!empty($cfg->title)) { $line .= ' -- ' . str_replace(["\n", "\r"], '', $cfg->title); } $create[] = $line; if (property_exists($anciens_champs, $key)) { $copy[$key] = $key; } elseif ($key == 'numero') { // Copie des numéros de membre à partir du champ ID $copy[$key] = 'id'; } } $create = array_merge($create, $create_keys); $create = 'CREATE TABLE membres_tmp (' . "\n\t" . implode("\n\t", $create) . "\n);"; $copy = 'INSERT INTO membres_tmp (' . implode(', ', array_keys($copy)) . ') SELECT ' . implode(', ', $copy) . ' FROM membres;'; $db->exec('PRAGMA foreign_keys = OFF;'); $db->begin(); $db->exec($create); if ($enable_copy) { $db->exec($copy); } $db->exec('DROP TABLE IF EXISTS membres;'); $db->exec('ALTER TABLE membres_tmp RENAME TO membres;'); $db->exec('CREATE INDEX membres_id_categorie ON membres (id_categorie);'); // Index if ($config->get('champ_identifiant')) { // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique $db->exec('UPDATE membres SET '.$config->get('champ_identifiant').' = NULL WHERE '.$config->get('champ_identifiant').' = "";'); // Création de l'index unique $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$config->get('champ_identifiant').');'); } if (isset($this->champs->numero)) { $db->exec('CREATE UNIQUE INDEX membres_numero ON membres (numero);'); } // Création des index pour les champs affichés dans la liste des membres $listed_fields = array_keys((array) $this->getListedFields()); foreach ($listed_fields as $field) { if ($field === $config->get('champ_identifiant')) { // Il y a déjà un index continue; } $db->exec('CREATE INDEX membres_liste_' . $field . ' ON membres (' . $field . ');'); } $db->commit(); $db->exec('PRAGMA foreign_keys = ON;'); $config->set('champs_membres', $this); $config->save(); return true; } } |
> > | | > > > > > > > > > > > > > > > < < < < < < > | | | | | < < < < < < < < < < < < < < < < < < < | | | | | | | | < > > > > > | > > > > > > > > > > > > > > > | > > | > > > > > > | | < > | < < > | | > | < > > | < < < > > > > > > > > > > > > > > > > | < > > > | < < | > | < < < > | < | > > > > > > > > < > | < > < < < < > | < | < | > > > > > > > > > > > > > > > > > > > > |
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 ... 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 ... 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 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 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 |
use Garradin\Config; use Garradin\DB; use Garradin\Utils; use Garradin\UserException; class Champs { const TABLE = 'membres'; protected $champs = null; protected $system_fields = [ 'date_connexion', 'date_inscription', 'clef_pgp', 'secret_otp', 'id', 'category_id', ]; protected $types = [ 'email' => 'Adresse E-Mail', 'url' => 'Adresse URL', 'checkbox' => 'Case à cocher', 'date' => 'Date', 'datetime' => 'Date et heure', 'file' => 'Fichier', 'password' => 'Mot de passe', 'number' => 'Numéro', 'tel' => 'Numéro de téléphone', 'select' => 'Sélecteur à choix unique', 'multiple' => 'Sélecteur à choix multiple', 'country' => 'Sélecteur de pays', 'text' => 'Texte', ................................................................................ public function getList() { $champs = clone $this->champs; unset($champs->passe); return $champs; } public function listAssocNames() { $out = []; foreach ($this->champs as $key => $config) { if ($key == 'passe') { continue; } $out[$key] = $config->title; } return $out; } public function getMultiples() { $out = []; foreach ($this->champs as $id => $champ) { if ($champ->type == 'multiple') { ................................................................................ } $this->champs = $champs; return true; } public function getSQLSchema(string $table_name = self::TABLE): string { $config = Config::getInstance(); $db = DB::getInstance(); // Champs à créer $create = [ 'id INTEGER PRIMARY KEY, -- Numéro attribué automatiquement', 'category_id INTEGER NOT NULL REFERENCES users_categories(id),', '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' ]; end($this->champs); $last_one = key($this->champs); foreach ($this->champs as $key=>$cfg) { if ($cfg->type == 'number' || $cfg->type == 'multiple' || $cfg->type == 'checkbox') $type = 'INTEGER'; elseif ($cfg->type == 'file') $type = 'BLOB'; else $type = 'TEXT'; $line = sprintf('%s %s', $db->quoteIdentifier($key), $type); if ($last_one != $key) { $line .= ','; } if (!empty($cfg->title)) { $line .= ' -- ' . str_replace(["\n", "\r"], '', $cfg->title); } $create[] = $line; } $sql = sprintf("CREATE TABLE %s\n(\n\t%s\n);", $table_name, implode("\n\t", $create)); return $sql; } public function getCopyFields(): array { $config = Config::getInstance(); // Champs à recopier $copy = [ 'id' => 'id', 'category_id' => 'category_id', 'date_connexion' => 'date_connexion', 'date_inscription' => 'date_inscription', 'secret_otp' => 'secret_otp', 'clef_pgp' => 'clef_pgp', ]; $anciens_champs = $config->get('champs_membres'); $anciens_champs = is_null($anciens_champs) ? $this->champs : $anciens_champs->getAll(); foreach ($this->champs as $key=>$cfg) { if (property_exists($anciens_champs, $key)) { $copy[$key] = $key; } } return $copy; } public function getSQLCopy(string $old_table_name, string $new_table_name = self::TABLE, array $fields = null): string { if (null === $fields) { $fields = $this->getCopyFields(); } return sprintf('INSERT INTO %s (%s) SELECT %s FROM %s;', $new_table_name, implode(', ', $fields), implode(', ', array_keys($fields)), $old_table_name ); } public function copy(string $old_table_name, string $new_table_name = self::TABLE, array $fields = null): void { DB::getInstance()->exec($this->getSQLCopy($old_table_name, $new_table_name, $fields)); } public function create(string $table_name = self::TABLE) { $db = DB::getInstance(); $db->begin(); $this->createTable($table_name); $this->createIndexes($table_name); $db->commit(); } public function createTable(string $table_name = self::TABLE): void { DB::getInstance()->exec($this->getSQLSchema($table_name)); } public function createIndexes(string $table_name = self::TABLE): void { $db = DB::getInstance(); $config = Config::getInstance(); $db->exec(sprintf('CREATE INDEX users_category ON %s (category_id);', $table_name)); // Index if ($config->get('champ_identifiant')) { // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique $db->exec(sprintf('UPDATE %s SET %s = NULL WHERE %2$s = \'\';', $table_name, $config->get('champ_identifiant'))); // Création de l'index unique $db->exec(sprintf('CREATE UNIQUE INDEX users_id_field ON %s (%s);', $table_name, $config->get('champ_identifiant'))); } $db->exec(sprintf('CREATE UNIQUE INDEX user_number ON %s (numero);', $table_name)); // Création des index pour les champs affichés dans la liste des membres $listed_fields = array_keys((array) $this->getListedFields()); foreach ($listed_fields as $field) { if ($field === $config->get('champ_identifiant')) { // Il y a déjà un index continue; } $db->exec(sprintf('CREATE INDEX users_list_%s ON %s (%1$s);', $field, $table_name)); } } /** * Enregistre les changements de champs en base de données * @return boolean true */ public function save() { $db = DB::getInstance(); $config = Config::getInstance(); $db->exec('PRAGMA foreign_keys = OFF;'); $db->begin(); $this->createTable(self::TABLE . '_tmp'); $this->copy(self::TABLE, self::TABLE . '_tmp'); $db->exec(sprintf('DROP TABLE IF EXISTS %s;', self::TABLE)); $db->exec(sprintf('ALTER TABLE %s_tmp RENAME TO %1$s;', self::TABLE)); $this->createIndexes(self::TABLE); $db->commit(); $db->exec('PRAGMA foreign_keys = ON;'); $config->set('champs_membres', $this); $config->save(); return true; } } |
Modified src/include/lib/Garradin/Membres/Import.php from [807575f8f7] to [7ba001f460].
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
...
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
|
$membres = new Membres; // On récupère les champs qu'on peut importer $champs_membres = Config::getInstance()->get('champs_membres'); $champs_multiples = $champs_membres->getMultiples(); $champs = $champs_membres->getKeys(); $champs[] = 'date_inscription'; //$champs[] = 'date_connexion'; //$champs[] = 'id'; //$champs[] = 'id_categorie'; $line = 0; $delim = CSV::findDelimiter($fp); CSV::skipBOM($fp); while (!feof($fp)) { ................................................................................ $list = array_map('intval', $list); $where = sprintf('WHERE m.%s', $db->where('id', $list)); } else { $where = ''; } $sql = sprintf('SELECT %s, c.nom AS "Catégorie membre" FROM membres AS m INNER JOIN membres_categories AS c ON m.id_categorie = c.id %s ORDER BY c.id;', $fields, $where); $res = $db->iterate($sql); return [ array_keys((array) $res->current()), $res, |
<
<
<
|
|
|
86
87
88
89
90
91
92
93
94
95
96
97
98
99
...
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
|
$membres = new Membres; // On récupère les champs qu'on peut importer $champs_membres = Config::getInstance()->get('champs_membres'); $champs_multiples = $champs_membres->getMultiples(); $champs = $champs_membres->getKeys(); $champs[] = 'date_inscription'; $line = 0; $delim = CSV::findDelimiter($fp); CSV::skipBOM($fp); while (!feof($fp)) { ................................................................................ $list = array_map('intval', $list); $where = sprintf('WHERE m.%s', $db->where('id', $list)); } else { $where = ''; } $sql = sprintf('SELECT %s, c.name AS "Catégorie membre" FROM membres AS m INNER JOIN users_categories AS c ON m.category_id = c.id %s ORDER BY c.id;', $fields, $where); $res = $db->iterate($sql); return [ array_keys((array) $res->current()), $res, |
Modified src/include/lib/Garradin/Membres/Session.php from [dd76dcbfbe] to [b642e4ef90].
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 .. 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 ... 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 ... 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
use KD2\Security; use KD2\Security_OTP; use KD2\Graphics\QRCode; use KD2\HTTP; class Session extends \KD2\UserSession { // Personalisation de la config de UserSession protected $cookie_name = 'gdin'; protected $remember_me_cookie_name = 'gdinp'; protected $remember_me_expiry = '+3 months'; const MINIMUM_PASSWORD_LENGTH = 8; static public function checkPasswordValidity($password) { if (strlen($password) < self::MINIMUM_PASSWORD_LENGTH) { throw new UserException(sprintf('Le mot de passe doit faire au moins %d caractères.', self::MINIMUM_PASSWORD_LENGTH)); } $session = new Session(); $session->http = new HTTP; if ($session->isPasswordCompromised($password)) { throw new UserException('Ce mot de passe figure dans une liste de mots de passe compromis. Si vous l\'avez utilisé sur d\'autres sites il est recommandé de le changer sur ces autres sites également.'); } } public function isPasswordCompromised($password) { // Vérifier s'il n'y a pas un plugin qui gère déjà cet aspect // notamment en installation mutualisée c'est plus efficace ................................................................................ if ($called !== null) { return $return['is_compromised']; } return parent::isPasswordCompromised($password); } // Extension des méthodes de UserSession public function __construct() { $url = parse_url(ADMIN_URL); parent::__construct(DB::getInstance(), [ 'cookie_domain' => $url['host'], 'cookie_path' => preg_replace('!/admin/$!', '/', $url['path']), 'cookie_secure' => (\Garradin\PREFER_HTTPS >= 2) ? true : false, ]); } protected function getUserForLogin($login) { $champ_id = Config::getInstance()->get('champ_identifiant'); // Ne renvoie un membre que si celui-ci a le droit de se connecter $query = 'SELECT m.id, m.%1$s AS login, m.passe AS password, m.secret_otp AS otp_secret FROM membres AS m INNER JOIN membres_categories AS mc ON mc.id = m.id_categorie WHERE m.%1$s = ? COLLATE NOCASE AND mc.droit_connexion >= %2$d LIMIT 1;'; $query = sprintf($query, $champ_id, Membres::DROIT_ACCES); return $this->db->first($query, $login); } protected function getUserDataForSession($id) { // Mettre à jour la date de connexion $this->db->preparedQuery('UPDATE membres SET date_connexion = datetime() WHERE id = ?;', [$id]); $config = Config::getInstance(); return $this->db->first('SELECT m.*, m.'.$config->get('champ_identite').' AS identite, c.droit_connexion, c.droit_wiki, c.droit_membres, c.droit_compta, c.droit_config, c.droit_membres FROM membres AS m INNER JOIN membres_categories AS c ON m.id_categorie = c.id WHERE m.id = ? LIMIT 1;', $id); } protected function storeRememberMeSelector($selector, $hash, $expiry, $user_id) { return $this->db->insert('membres_sessions', [ 'selecteur' => $selector, ................................................................................ if (!$disable_local_login && defined('\Garradin\LOCAL_LOGIN')) { $login_id = \Garradin\LOCAL_LOGIN; // On va chercher le premier membre avec le droit de gérer la config if (-1 === $login_id) { $login_id = $this->db->firstColumn('SELECT id FROM membres WHERE id_categorie IN (SELECT id FROM membres_categories WHERE droit_config = ?) LIMIT 1', Membres::DROIT_ADMIN); } if ($login_id > 0 && (!$logged || ($logged && $this->user->id != $login_id))) { $logged = $this->create($login_id); } } return $logged; } // Ici checkOTP utilise NTP en second recours public function checkOTP($secret, $code) { if (Security_OTP::TOTP($secret, $code)) { return true; ................................................................................ $this->refresh(false); return true; } public function canAccess($category, $permission) { if (!$this->user) { return false; } return ($this->user->{'droit_' . $category} >= $permission); } public function requireAccess($category, $permission) { if (!$this->canAccess($category, $permission)) { throw new UserException('Vous n\'avez pas le droit d\'accéder à cette page.'); |
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | < < < < < < < < < < < < | | | | | | | | > > > > > | > | |
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 .. 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 ... 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 ... 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 |
use KD2\Security; use KD2\Security_OTP; use KD2\Graphics\QRCode; use KD2\HTTP; class Session extends \KD2\UserSession { const SECTION_WEB = 'web'; const SECTION_DOCUMENTS = 'documents'; const SECTION_USERS = 'users'; const SECTION_ACCOUNTING = 'accounting'; const SECTION_CONNECT = 'connect'; const SECTION_CONFIG = 'config'; const SECTION_SUBSCRIBE = 'subscribe'; const ACCESS_NONE = 0; const ACCESS_READ = 1; const ACCESS_WRITE = 2; const ACCESS_ADMIN = 9; // Personalisation de la config de UserSession protected $cookie_name = 'gdin'; protected $remember_me_cookie_name = 'gdinp'; protected $remember_me_expiry = '+3 months'; const MINIMUM_PASSWORD_LENGTH = 8; static protected $_instance = null; static public function getInstance() { return self::$_instance ?: self::$_instance = new self; } public function __clone() { throw new \LogicException('Cannot clone'); } public function __construct() { if (self::$_instance !== null) { throw new \LogicException('Wrong call, use getInstance'); } $url = parse_url(ADMIN_URL); parent::__construct(DB::getInstance(), [ 'cookie_domain' => $url['host'], 'cookie_path' => preg_replace('!/admin/$!', '/', $url['path']), 'cookie_secure' => (\Garradin\PREFER_HTTPS >= 2) ? true : false, ]); } static public function checkPasswordValidity($password) { if (strlen($password) < self::MINIMUM_PASSWORD_LENGTH) { throw new UserException(sprintf('Le mot de passe doit faire au moins %d caractères.', self::MINIMUM_PASSWORD_LENGTH)); } $session = self::getInstance(); $session->http = new HTTP; if ($session->isPasswordCompromised($password)) { throw new UserException('Ce mot de passe figure dans une liste de mots de passe compromis, il ne peut donc être utilisé ici. Si vous l\'avez utilisé sur d\'autres sites il est recommandé de le changer sur ces autres sites également.'); } } public function isPasswordCompromised($password) { // Vérifier s'il n'y a pas un plugin qui gère déjà cet aspect // notamment en installation mutualisée c'est plus efficace ................................................................................ if ($called !== null) { return $return['is_compromised']; } return parent::isPasswordCompromised($password); } protected function getUserForLogin($login) { $champ_id = Config::getInstance()->get('champ_identifiant'); // Ne renvoie un membre que si celui-ci a le droit de se connecter $query = 'SELECT m.id, m.%1$s AS login, m.passe AS password, m.secret_otp AS otp_secret FROM membres AS m INNER JOIN users_categories AS c ON c.id = m.category_id WHERE m.%1$s = ? COLLATE NOCASE AND c.perm_connect >= %2$d LIMIT 1;'; $query = sprintf($query, $champ_id, self::ACCESS_READ); return $this->db->first($query, $login); } protected function getUserDataForSession($id) { // Mettre à jour la date de connexion $this->db->preparedQuery('UPDATE membres SET date_connexion = datetime() WHERE id = ?;', [$id]); $config = Config::getInstance(); return $this->db->first('SELECT m.*, m.'.$config->get('champ_identite').' AS identite, c.perm_connect, c.perm_web, c.perm_users, c.perm_documents, c.perm_subscribe, c.perm_accounting, c.perm_config FROM membres AS m INNER JOIN users_categories AS c ON m.category_id = c.id WHERE m.id = ? LIMIT 1;', $id); } protected function storeRememberMeSelector($selector, $hash, $expiry, $user_id) { return $this->db->insert('membres_sessions', [ 'selecteur' => $selector, ................................................................................ if (!$disable_local_login && defined('\Garradin\LOCAL_LOGIN')) { $login_id = \Garradin\LOCAL_LOGIN; // On va chercher le premier membre avec le droit de gérer la config if (-1 === $login_id) { $login_id = $this->db->firstColumn('SELECT id FROM membres WHERE category_id IN (SELECT id FROM users_categories WHERE perm_config = ?) LIMIT 1', self::ACCESS_ADMIN); } if ($login_id > 0 && (!$logged || ($logged && $this->user->id != $login_id))) { $logged = $this->create($login_id); } } return $logged; } public function forceLogin(int $id) { return $this->create($id); } // Ici checkOTP utilise NTP en second recours public function checkOTP($secret, $code) { if (Security_OTP::TOTP($secret, $code)) { return true; ................................................................................ $this->refresh(false); return true; } public function canAccess($category, $permission) { if (!$this->getUser()) { return false; } return ($this->getUser()->{'perm_' . $category} >= $permission); } public function requireAccess($category, $permission) { if (!$this->canAccess($category, $permission)) { throw new UserException('Vous n\'avez pas le droit d\'accéder à cette page.'); |
Modified src/include/lib/Garradin/Plugin.php from [5aa9774a1a] to [1f8c21a59a].
1 2 3 4 5 6 7 8 9 10 11 ... 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 ... 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 |
<?php namespace Garradin; class Plugin { const PLUGIN_ID_SYNTAX = '[a-z]+(?:_[a-z]+)*'; protected $id = null; protected $plugin = null; protected $config_changed = false; ................................................................................ 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($user) { self::checkAndInstallSystemPlugins(); $db = DB::getInstance(); $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]); ................................................................................ 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 . ';'; $res = $db->protectSelect(['membres' => []], $query); if (!$db->firstColumn($query)) { unset($list[$id]); |
> > | > > > > > > > > > > > > > > > > > > > > | < > | | | < > | > > > | > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 ... 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 ... 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 |
<?php namespace Garradin; use Garradin\Membres\Session; class Plugin { const PLUGIN_ID_SYNTAX = '[a-z]+(?:_[a-z]+)*'; protected $id = null; protected $plugin = null; protected $config_changed = false; ................................................................................ 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(Session $session) { self::checkAndInstallSystemPlugins(); $db = DB::getInstance(); $list = $db->getGrouped('SELECT id, nom, menu_condition FROM plugins WHERE menu = 1 ORDER BY nom;'); // FIXME deprecated $fix_legacy = [ '{Membres::DROIT_AUCUN}' => '{ACCESS_NONE}', '{Membres::DROIT_ACCES}' => '{ACCESS_READ}', '{Membres::DROIT_ECRITURE}' => '{ACCESS_WRITE}', '{Membres::DROIT_ADMIN}' => '{ACCESS_ADMIN}', '{$user.droit_compta}' => '{$user.perm_accounting}', '{$user.droit_membres}' => '{$user.perm_users}', '{$user.droit_config}' => '{$user.perm_config}', '{$user.droit_wiki}' => '{$user.perm_documents}', ]; $user = $session->getUser(); $permissions = [ '{ACCESS_NONE}' => $session::ACCESS_NONE, '{ACCESS_READ}' => $session::ACCESS_READ, '{ACCESS_WRITE}' => $session::ACCESS_WRITE, '{ACCESS_ADMIN}' => $session::ACCESS_ADMIN, ]; foreach ($list as $id => &$row) { if (!self::getPath($row->id, false)) { // Ne pas lister les plugins dont le code a disparu unset($list[$id]); ................................................................................ if (!$row->menu_condition) { $row = $row->nom; continue; } $new_condition = strtr($row->menu_condition, $fix_legacy); // FIXME: legacy if ($new_condition != $row->menu_condition) { $db->update('plugins', ['menu_condition' => $new_condition], 'id = :id', ['id' => $id]); $row->menu_condition = $new_condition; } $condition = strtr($row->menu_condition, $permissions); $condition = preg_replace_callback('/\{\$user\.(\w+)\}/', function ($m) use ($user, $db) { return property_exists($user, $m[1]) ? $db->quote($user->{$m[1]}) : 'NULL'; }, $condition); $query = 'SELECT 1 WHERE ' . $condition . ';'; $res = $db->protectSelect(['membres' => []], $query); if (!$db->firstColumn($query)) { unset($list[$id]); |
Modified src/include/lib/Garradin/Recherche.php from [e91118fdd9] to [25cb8d4a11].
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 ... 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 ... 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 |
$columns = []; $db = DB::getInstance(); if ($target == 'membres') { $champs = Config::getInstance()->get('champs_membres'); $columns['id_categorie'] = (object) [ 'textMatch'=> false, 'label' => 'Catégorie', 'type' => 'enum', 'null' => false, 'values' => $db->getAssoc('SELECT id, nom FROM membres_categories ORDER BY nom;'), ]; foreach ($champs->getList() as $champ => $config) { $column = (object) [ 'textMatch'=> $champs->isText($champ), 'label' => $config->title, ................................................................................ $query .= ' LIMIT 100'; } try { $db = DB::getInstance(); static $allowed = [ 'compta' => ['acc_transactions' => null, 'acc_transactions_lines' => null, 'acc_accounts' => null, 'acc_charts' => null, 'acc_years' => null, 'acc_transactions_users' => null], 'membres' => ['membres' => null, 'membres_categories' => null], ]; if ($unprotected) { $allowed_tables = null; } else { $allowed_tables = $allowed[$target]; ................................................................................ public function schema(string $target) { $db = DB::getInstance(); if ($target == 'membres') { $tables = [ 'membres' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'), 'categories' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'), ]; } elseif ($target == 'compta') { $tables = [ 'acc_transactions' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'acc_transactions\';'), 'acc_transactions_lines' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'acc_transactions_lines\';'), ]; |
| | | | |
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 ... 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 ... 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 |
$columns = []; $db = DB::getInstance(); if ($target == 'membres') { $champs = Config::getInstance()->get('champs_membres'); $columns['category_id'] = (object) [ 'textMatch'=> false, 'label' => 'Catégorie', 'type' => 'enum', 'null' => false, 'values' => $db->getAssoc('SELECT id, name FROM users_categories ORDER BY name COLLATE NOCASE;'), ]; foreach ($champs->getList() as $champ => $config) { $column = (object) [ 'textMatch'=> $champs->isText($champ), 'label' => $config->title, ................................................................................ $query .= ' LIMIT 100'; } try { $db = DB::getInstance(); static $allowed = [ 'compta' => ['acc_transactions' => null, 'acc_transactions_lines' => null, 'acc_accounts' => null, 'acc_charts' => null, 'acc_years' => null, 'acc_transactions_users' => null], 'membres' => ['membres' => null, 'users_categories' => null], ]; if ($unprotected) { $allowed_tables = null; } else { $allowed_tables = $allowed[$target]; ................................................................................ public function schema(string $target) { $db = DB::getInstance(); if ($target == 'membres') { $tables = [ 'membres' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'), 'categories' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'users_categories\';'), ]; } elseif ($target == 'compta') { $tables = [ 'acc_transactions' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'acc_transactions\';'), 'acc_transactions_lines' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'acc_transactions_lines\';'), ]; |
Modified src/include/lib/Garradin/Sauvegarde.php from [d455e0ab72] to [63babef5f6].
1 2 3 4 5 6 7 8 9 10 11 .. 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 ... 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 ... 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 ... 505 506 507 508 509 510 511 512 513 514 |
<?php namespace Garradin; class Sauvegarde { const NEED_UPGRADE = 0x01 << 2; const NOT_AN_ADMIN = 0x01 << 3; const INTEGRITY_FAIL = 41; const NOT_A_DB = 42; ................................................................................ // Skip non-auto files if ($auto_only && !$auto) { continue; } $db = new \SQLite3(DATA_ROOT . '/' . $file, \SQLITE3_OPEN_READONLY); $version = $db->querySingle('SELECT valeur FROM config WHERE cle = \'version\';'); $db->close(); $out[$file] = (object) [ 'filename' => $file, 'date' => filemtime(DATA_ROOT . '/' . $file), 'name' => $name != $file ? $name : null, ................................................................................ { throw new UserException('Ce fichier n\'est pas une sauvegarde Garradin (application_id ne correspond pas).', self::NO_APP_ID); } if ($user_id) { // Empêchons l'admin de se tirer une balle dans le pied $is_still_admin = $db->querySingle('SELECT 1 FROM membres_categories WHERE id = (SELECT id_categorie FROM membres WHERE id = ' . (int) $user_id . ') AND droit_config >= ' . Membres::DROIT_ADMIN . ' AND droit_connexion >= ' . Membres::DROIT_ACCES); if (!$is_still_admin) { $return |= self::NOT_AN_ADMIN; } } ................................................................................ unlink($backup); if ($return & self::NOT_AN_ADMIN) { // Forcer toutes les catégories à pouvoir gérer les droits $db = DB::getInstance(); $db->update('membres_categories', [ 'droit_membres' => Membres::DROIT_ADMIN, 'droit_connexion' => Membres::DROIT_ACCES ]); } if ($version != garradin_version()) { $return |= self::NEED_UPGRADE; } ................................................................................ /** * Taille occupée par les fichiers dans la base de données * @return integer Taille en octets */ public function getDBFilesSize() { $db = DB::getInstance(); return (int) $db->firstColumn('SELECT SUM(taille) FROM fichiers_contenu;'); } } |
> > > > > > | > | | | | | | | | |
1 2 3 4 5 6 7 8 9 10 11 12 13 .. 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 ... 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 ... 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 ... 512 513 514 515 516 517 518 519 520 521 |
<?php namespace Garradin; use Garradin\Membres\Session; class Sauvegarde { const NEED_UPGRADE = 0x01 << 2; const NOT_AN_ADMIN = 0x01 << 3; const INTEGRITY_FAIL = 41; const NOT_A_DB = 42; ................................................................................ // Skip non-auto files if ($auto_only && !$auto) { continue; } $db = new \SQLite3(DATA_ROOT . '/' . $file, \SQLITE3_OPEN_READONLY); $version = DB::parseVersion($db->querySingle('PRAGMA user_version;')); if (null === $version) { // for versions prior to 1.1.0 $version = $db->querySingle('SELECT valeur FROM config WHERE cle = \'version\';'); } $db->close(); $out[$file] = (object) [ 'filename' => $file, 'date' => filemtime(DATA_ROOT . '/' . $file), 'name' => $name != $file ? $name : null, ................................................................................ { throw new UserException('Ce fichier n\'est pas une sauvegarde Garradin (application_id ne correspond pas).', self::NO_APP_ID); } if ($user_id) { // Empêchons l'admin de se tirer une balle dans le pied $is_still_admin = $db->querySingle('SELECT 1 FROM users_categories WHERE id = (SELECT category_id FROM membres WHERE id = ' . (int) $user_id . ') AND perm_config >= ' . Session::ACCESS_ADMIN . ' AND perm_connect >= ' . Session::ACCESS_READ); if (!$is_still_admin) { $return |= self::NOT_AN_ADMIN; } } ................................................................................ unlink($backup); if ($return & self::NOT_AN_ADMIN) { // Forcer toutes les catégories à pouvoir gérer les droits $db = DB::getInstance(); $db->update('users_categories', [ 'perm_users' => Session::ACCESS_ADMIN, 'perm_connect' => Session::ACCESS_READ ]); } if ($version != garradin_version()) { $return |= self::NEED_UPGRADE; } ................................................................................ /** * Taille occupée par les fichiers dans la base de données * @return integer Taille en octets */ public function getDBFilesSize() { $db = DB::getInstance(); return (int) $db->firstColumn('SELECT SUM(size) FROM files;'); } } |
Modified src/include/lib/Garradin/Services/Fees.php from [5a3f0428c1] to [8c034cc764].
1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
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
|
<?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\Membres\Categories; use Garradin\Entities\Services\Fee; use Garradin\Entities\Accounting\Year; use KD2\DB\EntityManager; class Fees { protected $service_id; ................................................................................ return $result; } public function listWithStats() { $db = DB::getInstance(); $hidden_cats = array_keys((new Categories)->listHidden()); $condition = sprintf('SELECT COUNT(DISTINCT su.id_user) FROM services_users su INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_fee) su2 ON su2.id = su.id INNER JOIN membres m ON m.id = su.id_user WHERE su.id_fee = f.id AND m.id_categorie NOT IN (%s)', implode(',', $hidden_cats)); $sql = sprintf('SELECT f.*, (%s AND (expiry_date IS NULL OR expiry_date >= date()) AND paid = 1) AS nb_users_ok, (%1$s AND expiry_date < date()) AS nb_users_expired, (%1$s AND paid = 0) AS nb_users_unpaid FROM services_fees f WHERE id_service = ? ORDER BY amount, transliterate_to_ascii(label) COLLATE NOCASE;', $condition); return $db->get($sql, $this->service_id); } } |
|
|
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
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
|
<?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\Users\Categories; use Garradin\Entities\Services\Fee; use Garradin\Entities\Accounting\Year; use KD2\DB\EntityManager; class Fees { protected $service_id; ................................................................................ return $result; } public function listWithStats() { $db = DB::getInstance(); $hidden_cats = array_keys(Categories::listHidden()); $condition = sprintf('SELECT COUNT(DISTINCT su.id_user) FROM services_users su INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_fee) su2 ON su2.id = su.id INNER JOIN membres m ON m.id = su.id_user WHERE su.id_fee = f.id AND m.category_id NOT IN (%s)', implode(',', $hidden_cats)); $sql = sprintf('SELECT f.*, (%s AND (expiry_date IS NULL OR expiry_date >= date()) AND paid = 1) AS nb_users_ok, (%1$s AND expiry_date < date()) AS nb_users_expired, (%1$s AND paid = 0) AS nb_users_unpaid FROM services_fees f WHERE id_service = ? ORDER BY amount, transliterate_to_ascii(label) COLLATE NOCASE;', $condition); return $db->get($sql, $this->service_id); } } |
Modified src/include/lib/Garradin/Services/Reminders.php from [2eefb08a6c] to [065ce72ab4].
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
m.email, m.%s AS identity FROM services_users su INNER JOIN services s ON s.id = su.id_service INNER JOIN services_reminders sr ON sr.id_service = su.id_service -- Join with users, but not ones part of a hidden category INNER JOIN membres m ON su.id_user = m.id AND m.email IS NOT NULL AND (m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)) -- Join with sent reminders to exclude users that already have received this reminder LEFT JOIN services_reminders_sent srs ON srs.id_reminder = sr.id AND srs.id_user = su.id_user WHERE date() > date(su.expiry_date, sr.delay || \' days\') AND srs.id IS NULL GROUP BY su.id_user, s.id ORDER BY su.id_user;'; |
| |
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
m.email, m.%s AS identity FROM services_users su INNER JOIN services s ON s.id = su.id_service INNER JOIN services_reminders sr ON sr.id_service = su.id_service -- Join with users, but not ones part of a hidden category INNER JOIN membres m ON su.id_user = m.id AND m.email IS NOT NULL AND (m.category_id NOT IN (SELECT id FROM users_categories WHERE hidden = 1)) -- Join with sent reminders to exclude users that already have received this reminder LEFT JOIN services_reminders_sent srs ON srs.id_reminder = sr.id AND srs.id_user = su.id_user WHERE date() > date(su.expiry_date, sr.delay || \' days\') AND srs.id IS NULL GROUP BY su.id_user, s.id ORDER BY su.id_user;'; |
Modified src/include/lib/Garradin/Services/Services.php from [99a3b0f3b3] to [743242c68f].
1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
<?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\Membres\Categories; use Garradin\Entities\Services\Service; use KD2\DB\EntityManager; class Services { static public function get(int $id) { ................................................................................ return $out; } static public function listWithStats() { $db = DB::getInstance(); $hidden_cats = array_keys((new Categories)->listHidden()); $condition = sprintf('SELECT COUNT(DISTINCT su.id_user) FROM services_users su INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_service) su2 ON su2.id = su.id INNER JOIN membres m ON m.id = su.id_user WHERE su.id_service = s.id AND m.id_categorie NOT IN (%s)', implode(',', $hidden_cats)); $sql = sprintf('SELECT s.*, (%s AND (expiry_date IS NULL OR expiry_date >= date()) AND paid = 1) AS nb_users_ok, (%1$s AND expiry_date < date()) AS nb_users_expired, (%1$s AND paid = 0) AS nb_users_unpaid FROM services s ORDER BY transliterate_to_ascii(s.label) COLLATE NOCASE;', $condition); return $db->get($sql); } } |
|
|
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
<?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\Users\Categories; use Garradin\Entities\Services\Service; use KD2\DB\EntityManager; class Services { static public function get(int $id) { ................................................................................ return $out; } static public function listWithStats() { $db = DB::getInstance(); $hidden_cats = array_keys(Categories::listHidden()); $condition = sprintf('SELECT COUNT(DISTINCT su.id_user) FROM services_users su INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_service) su2 ON su2.id = su.id INNER JOIN membres m ON m.id = su.id_user WHERE su.id_service = s.id AND m.category_id NOT IN (%s)', implode(',', $hidden_cats)); $sql = sprintf('SELECT s.*, (%s AND (expiry_date IS NULL OR expiry_date >= date()) AND paid = 1) AS nb_users_ok, (%1$s AND expiry_date < date()) AS nb_users_expired, (%1$s AND paid = 0) AS nb_users_unpaid FROM services s ORDER BY transliterate_to_ascii(s.label) COLLATE NOCASE;', $condition); return $db->get($sql); } } |
Deleted src/include/lib/Garradin/Squelette.php version [1036393224].
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 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 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 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 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 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 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 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 |
<?php namespace Garradin; class Squelette_Snippet { const TEXT = 0; const PHP = 1; const GUESS = 2; const OBJ = 3; protected $_content = []; protected function _getType($type, $value) { if ($type == self::GUESS) { if ($value instanceof Squelette_Snippet) return self::OBJ; else return self::TEXT; } return $type; } public function __construct($type = self::TEXT, $value = '') { $type = $this->_getType($type, $value); if ($type == self::OBJ) { $this->_content = $value->get(); } else { $this->_content[] = (string) (int) $type . $value; } unset($value); } public function prepend($type = self::TEXT, $value, $pos = false) { $type = $this->_getType($type, $value); if ($type == self::OBJ) { if ($pos) { array_splice($this->_content, $pos, 0, $value->get()); } else { $this->_content = array_merge($value->get(), $this->_content); } } else { $value = (string) (int) $type . $value; if ($pos) { array_splice($this->_content, $pos, 0, $value); } else { array_unshift($this->_content, $value); } } unset($value); } public function append($type = self::TEXT, $value, $pos = false) { $type = $this->_getType($type, $value); if ($type == self::OBJ) { if ($pos) { array_splice($this->_content, $pos + 1, 0, $value->get()); } else { $this->_content = array_merge($this->_content, $value->get()); } } else { $value = (string) (int) $type . $value; if ($pos) { array_splice($this->_content, $pos + 1, 0, $value); } else { array_push($this->_content, $value); } } unset($value); } public function output($in_php = false) { $out = ''; $php = $in_php ?: false; foreach ($this->_content as $line) { if ($line[0] == self::PHP && !$php) { $php = true; $out .= '<?php '; } elseif ($line[0] == self::TEXT && $php) { $php = false; $out .= ' ?>'; } $out .= substr($line, 1); if ($line[0] == self::PHP) { $out .= "\n"; } } if ($php && !$in_php) { $out .= ' ?>'; } $this->_content = []; return $out; } public function __toString() { return $this->output(false); } public function get() { return $this->_content; } public function replace($key, $type = self::TEXT, $value) { $type = $this->_getType($type, $value); if ($type == self::OBJ) { array_splice($this->_content, $key, 1, $value->get()); } else { $this->_content[$key] = (string) (int) $type . $value; } unset($value); } } class Squelette extends \KD2\MiniSkel { private $parent = null; private $current = null; private $_vars = []; private function _registerDefaultModifiers() { foreach (Squelette_Filtres::$filtres_php as $func=>$name) { if (is_string($func)) $this->register_modifier($name, $func); else $this->register_modifier($name, $name); } foreach (get_class_methods('Garradin\Squelette_Filtres') as $name) { $this->register_modifier($name, ['Garradin\Squelette_Filtres', $name]); } foreach (Squelette_Filtres::$filtres_alias as $name=>$func) { $this->register_modifier($name, ['Garradin\Squelette_Filtres', $func]); } if (file_exists(DATA_ROOT . '/www/squelettes/mes_filtres.php')) { require_once DATA_ROOT . '/www/squelettes/mes_filtres.php'; } } private function _registerDefaultTags() { $config = Config::getInstance(); $this->assign('nom_asso', $config->get('nom_asso')); $this->assign('adresse_asso', $config->get('adresse_asso')); $this->assign('email_asso', $config->get('email_asso')); $this->assign('site_asso', $config->get('site_asso')); $this->assign('url_racine', WWW_URL); $this->assign('url_site', WWW_URL); $this->assign('url_atom', WWW_URL . 'feed/atom/'); $this->assign('url_elements', WWW_URL . 'squelettes/'); $this->assign('url_admin', ADMIN_URL); $url = file_exists(DATA_ROOT . '/www/squelettes/default.css') ? WWW_URL . 'squelettes/default.css' : WWW_URL . 'squelettes-dist/default.css'; $this->assign('url_css_defaut', $url); if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { if (function_exists('locale_accept_from_http')) { $lang = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']); } else { $lang = preg_replace('/[^a-z]/i', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']); $lang = strtolower(substr($lang, 0, 2)); } $lang = strtolower(substr($lang, 0, 2)); } else { $lang = ''; } $this->assign('langue_visiteur', $lang); } public function __construct() { $this->_registerDefaultModifiers(); $this->_registerDefaultTags(); } protected function processInclude($args) { if (empty($args)) throw new \KD2\MiniSkelMarkupException("Le tag INCLURE demande à préciser le fichier à inclure."); $file = key($args); if (empty($file) || !preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $file)) throw new \KD2\MiniSkelMarkupException("INCLURE: le nom de fichier ne peut contenir que des caractères alphanumériques."); return new Squelette_Snippet(1, '$this->fetch("'.$file.'", false);'); } protected function processVariable($name, $applyDefault, $modifiers, $pre, $post, $context) { if ($context == self::CONTEXT_IN_ARG) { $out = new Squelette_Snippet(1, '$this->getVariable(\''.$name.'\')'); if ($pre) { $out->prepend(2, $pre); } if ($post) { $out->append(2, $post); } return $out; } $out = new Squelette_Snippet(1, '$value = $this->getVariable(\''.$name.'\');'); // We process modifiers foreach ($modifiers as &$modifier) { if (!isset($this->modifiers[$modifier['name']])) { throw new \KD2\MiniSkelMarkupException('Filtre '.$modifier['name'].' inconnu !'); } $out->append(1, '$value = call_user_func_array('.var_export($this->modifiers[$modifier['name']], true).', [$value, '); foreach ($modifier['arguments'] as $arg) { if ($arg == 'debut_liste') { $out->append(1, '$this->getVariable(\'debut_liste\')'); } elseif ($arg instanceOf Squelette_Snippet) { $out->append(3, $arg); } else { //if (preg_match('!getVariable!', $arg)) throw new Exception("lol"); $out->append(1, '"'.str_replace('"', '\\"', $arg).'"'); } $out->append(1, ', '); } $out->append(1, ']);'); if (in_array($modifier['name'], Squelette_Filtres::$desactiver_defaut)) { $applyDefault = false; } } if ($applyDefault) { $out->append(1, 'if (is_string($value) && trim($value)) $value = htmlspecialchars($value, ENT_QUOTES, \'UTF-8\', false);'); } $out->append(1, 'if ($value === true || trim($value) !== \'\'):'); // Getting pre-content if ($pre) { $out->append(2, $pre); } $out->append(1, 'echo is_bool($value) ? "" : $value;'); // Getting post-content if ($post) { $out->append(2, $post); } $out->append(1, 'endif;'); return $out; } protected function processLoop($loopName, $loopType, $loopCriterias, $loopContent, $preContent, $postContent, $altContent) { $query = $loop_start = ''; $query_args = []; $db = DB::getInstance(); $statement_code = ''; // Types de boucles natifs if ($loopType == 'articles' || $loopType == 'rubriques' || $loopType == 'pages') { $where = $order = ''; $limit = $begin = 0; $query = 'SELECT w.*, '; $query.= 'strftime(\'%s\', w.date_creation) AS date_creation, '; $query.= 'strftime(\'%s\', w.date_modification) AS date_modification'; if (trim($loopContent)) { $query .= ', r.contenu AS texte FROM wiki_pages AS w LEFT JOIN wiki_revisions AS r ON (w.id = r.id_page AND w.revision = r.revision) '; } else { $query .= '\'\' AS texte FROM wiki_pages AS w '; } $where = 'WHERE w.droit_lecture = -1 '; if ($loopType == 'articles') { $where .= 'AND (SELECT COUNT(id) FROM wiki_pages WHERE parent = w.id AND droit_lecture = -1) = 0 '; } elseif ($loopType == 'rubriques') { $where .= 'AND (SELECT COUNT(id) FROM wiki_pages WHERE parent = w.id AND droit_lecture = -1) > 0 '; } $allowed_fields = ['id', 'uri', 'titre', 'date', 'date_creation', 'date_modification', 'parent', 'rubrique', 'revision', 'points', 'recherche', 'texte', 'age']; $search = $search_rank = false; foreach ($loopCriterias as $criteria_id => $criteria) { if (isset($criteria['field'])) { if (!in_array($criteria['field'], $allowed_fields)) { throw new \KD2\MiniSkelMarkupException("Critère '".$criteria['field']."' invalide pour la boucle ".$loopName." de type ".$loopType."."); } elseif ($criteria['field'] == 'rubrique') { $criteria['field'] = 'parent'; } elseif ($criteria['field'] == 'date') { $criteria['field'] = 'date_creation'; } elseif ($criteria['field'] == 'points') { if ($criteria['action'] != \KD2\MiniSkel::ACTION_ORDER_BY) { throw new \KD2\MiniSkelMarkupException("Le critère 'points' n\'est pas valide dans ce contexte."); } $search_rank = true; } elseif ($criteria['field'] == 'age') { $criteria['field'] = 'julianday() - julianday(date_creation)'; $criteria['value'] = (int)$criteria['value']; } } switch ($criteria['action']) { case \KD2\MiniSkel::ACTION_ORDER_BY: if (!$order) $order = 'ORDER BY '.$criteria['field'].''; else $order .= ', '.$criteria['field'].''; break; case \KD2\MiniSkel::ACTION_ORDER_DESC: if ($order) $order .= ' DESC'; break; case \KD2\MiniSkel::ACTION_LIMIT: $begin = $criteria['begin']; $limit = $criteria['number']; break; case \KD2\MiniSkel::ACTION_MATCH_FIELD_BY_VALUE: $where .= ' AND '.$criteria['field'].' '.$criteria['comparison'].' ?'; $query_args[] = $criteria['value']; break; case \KD2\MiniSkel::ACTION_MATCH_FIELD: { if ($criteria['field'] == 'recherche') { $query = 'SELECT w.*, r.contenu AS texte, rank(matchinfo(wiki_recherche), 0, 1.0, 1.0) AS points FROM wiki_pages AS w INNER JOIN wiki_recherche AS r ON (w.id = r.id) '; $where .= ' AND wiki_recherche MATCH ?'; $search = true; } else { $where .= ' AND '.$criteria['field'].' = ?'; if ($criteria['field'] == 'parent') { $criteria['field'] = 'id'; } } $query_args[] = ['$this->getVariable(\'' . $criteria['field'] . '\')']; break; } default: break; } } if ($search_rank && !$search) { throw new \KD2\MiniSkelMarkupException("Le critère par points n'est possible que dans les boucles de recherche."); } if (trim($loopContent)) { $loop_start .= '$row[\'url\'] = WWW_URL . $row[\'uri\']; '; } $query .= $where . ' ' . $order; if (!$limit || $limit > 100) $limit = 100; if ($limit) { $query .= ' LIMIT '; if (is_numeric($begin)) { $query .= (int) $begin; } else { $query .= '?'; $query_args[] = ['\'.$this->variables[\'debut_liste\'].\'']; } $query .= ','.(int)$limit; } } else if ($loopType == 'documents' || $loopType == 'images' || $loopType == 'fichiers') { $where = $order = ''; $limit = $begin = 0; $query = 'SELECT f.*, fc.hash, fc.taille, strftime(\'%s\', f.datetime) AS date '; $query.= ' FROM fichiers AS f INNER JOIN fichiers_contenu AS fc ON fc.id = f.id_contenu '; $query.= ' INNER JOIN fichiers_wiki_pages AS fwp ON fwp.fichier = f.id '; $query.= ' INNER JOIN wiki_pages AS w ON w.id = fwp.id AND w.droit_lecture = -1 '; $where = 'WHERE 1 '; $allowed_fields = ['id', 'nom', 'type', 'date', 'image', 'hash', 'taille', 'parent', 'page', 'rubrique', 'article', 'sauf_mention']; if ($loopType == 'images') { $where .= ' AND image = 1 '; } else if ($loopType == 'documents') { $where .= ' AND image = 0 '; } foreach ($loopCriterias as $criteria_id => $criteria) { if (isset($criteria['field'])) { if (!in_array($criteria['field'], $allowed_fields)) { throw new \KD2\MiniSkelMarkupException("Critère '".$criteria['field']."' invalide pour la boucle ".$loopName." de type ".$loopType."."); } elseif ($criteria['field'] == 'rubrique' || $criteria['field'] == 'page' || $criteria['field'] == 'article' || $criteria['field'] == 'parent') { $criteria['field'] = 'w.id'; } elseif ($criteria['field'] == 'date') { $criteria['field'] = 'datetime'; } } switch ($criteria['action']) { case \KD2\MiniSkel::ACTION_ORDER_BY: if (!$order) $order = 'ORDER BY '.$criteria['field'].''; else $order .= ', '.$criteria['field'].''; break; case \KD2\MiniSkel::ACTION_ORDER_DESC: if ($order) $order .= ' DESC'; break; case \KD2\MiniSkel::ACTION_LIMIT: $begin = $criteria['begin']; $limit = $criteria['number']; break; case \KD2\MiniSkel::ACTION_MATCH_FIELD_BY_VALUE: $where .= ' AND '.$criteria['field'].' '.$criteria['comparison'].' ?'; $query_args[] = $criteria['value']; break; case \KD2\MiniSkel::ACTION_MATCH_FIELD: { if ($criteria['field'] == 'sauf_mention') { $where .= " AND f.id NOT IN (:php_implode) "; $query_args[':php_implode'] = '\'.implode(",", Fichiers::listFilesUsedInText($this->getVariable(\'texte\'))).\''; break; } $where .= ' AND '.$criteria['field'].' = ?'; if ($criteria['field'] == 'w.id') { $criteria['field'] = 'id'; } $query_args[] = ['$this->getVariable(\'' . $criteria['field'] . '\')']; break; } default: break; } } if (trim($loopContent)) { $loop_start .= '$row[\'url\'] = Fichiers::_getURL($row[\'id\'], $row[\'nom\'], $row[\'hash\']); '; $loop_start .= '$row[\'miniature\'] = $row[\'image\'] ? Fichiers::_getURL($row[\'id\'], $row[\'nom\'], $row[\'hash\'], 200) : \'\'; '; $loop_start .= '$row[\'moyenne\'] = $row[\'image\'] ? Fichiers::_getURL($row[\'id\'], $row[\'nom\'], $row[\'hash\'], 500) : \'\'; '; } $query .= $where . ' ' . $order; if (!$limit || $limit > 100) $limit = 100; if ($limit) { $query .= ' LIMIT '; if (is_numeric($begin)) { $query .= (int) $begin; } else { $query .= '?'; $query_args[] = ['\'.$this->variables[\'debut_liste\'].\'']; } $query .= ','.(int)$limit; } } else { $params = [ 'loopName' => $loopName, 'loopType' => $loopType, 'loopCriterias' => $loopCriterias, 'loopContent' => $loopContent, 'preContent' => $preContent, 'postContent' => $postContent, 'altContent' => $altContent, ]; $callback_return = [ 'query' => &$query, 'query_args' => &$query_args, 'loop_start' => &$loop_start, 'code' => &$statement_code, ]; // Appel du plugin lié à cette boucle, si ça existe $return = Plugin::fireSignal('boucle.' . $loopType, $params, $callback_return); // Si aucun plugin n'a été appelé c'est que le type de boucle n'est pas défini if (!$return) { throw new \KD2\MiniSkelMarkupException("Le type de boucle '".$loopType."' est inconnu."); } if (empty($query)) { $query = 'SELECT 0 LIMIT 0;'; } } try { // Sécurité anti injection, à la compilation seulement $statement = $db->protectSelect(null, $query); } catch (\Exception $e) { throw new \KD2\MiniSkelMarkupException("Erreur SQL dans la requête : ".$e->getMessage() . "\n " . $query); } $hash = sha1(uniqid(mt_rand(), true)); $out = new Squelette_Snippet(); $out->append(1, '$parent_hash = @$this->current[\'_self_hash\'];'); $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$parent_hash] : null;'); if (!empty($search)) { $out->append(1, 'if (trim($this->getVariable(\'recherche\'))) { '); } $query = var_export($query, true); foreach ($query_args as $k=>$arg) { if (substr($k, 0, 4) == ':php') { $query = str_replace($k, $arg, $query); unset($query_args[$k]); } } // Si le plugin a donné du code à exécuter à la place d'une requête SQL if ($statement_code) { $out->append(1, str_replace('$OBJ_VAR', '$result_' . $hash, $statement_code)); $out->append(1, '$nb_rows = $result_'.$hash.'->countRows();'); } else { $out->append(1, '$statement = $db->prepare('.$query.'); '); foreach ($query_args as $k=>$arg) { $out->append(1, '$value = ' . (is_array($arg) ? $arg[0] : var_export($arg, true)) . ';'); $out->append(1, '$statement->bindValue(' . ($k+1) . ', $value, $db->getArgType($value));'); } $out->append(1, '$result_'.$hash.' = $statement->execute(); '); $out->append(1, '$nb_rows = $db->countRows($result_'.$hash.'); '); } if (!empty($search)) { $out->append(1, '} else { $result_'.$hash.' = false; $nb_rows = 0; }'); } $out->append(1, '$this->_vars[\''.$hash.'\'] = [\'_self_hash\' => \''.$hash.'\', \'_parent_hash\' => $parent_hash, \'total_boucle\' => $nb_rows, \'compteur_boucle\' => 0];'); $out->append(1, '$this->current =& $this->_vars[\''.$hash.'\']; '); $out->append(1, 'if ($nb_rows > 0):'); // Ajout contenu avant tag if ($preContent) { $out->append(2, $this->parse($preContent, $loopName, self::PRE_CONTENT)); } $out->append(1, 'while ($row = $result_'.$hash.'->fetchArray(SQLITE3_ASSOC)): '); $out->append(1, '$this->_vars[\''.$hash.'\'][\'compteur_boucle\'] += 1; '); $out->append(1, $loop_start); $out->append(1, '$this->_vars[\''.$hash.'\'] = array_merge($this->_vars[\''.$hash.'\'], $row); '); $out->append(2, $this->parseVariables($loopContent)); $out->append(1, 'endwhile;'); // we put the post-content after the loop content if ($postContent) { $out->append(2, $this->parse($postContent, $loopName, self::POST_CONTENT)); } if ($altContent) { $out->append(1, 'else:'); $out->append(2, $this->parse($altContent, $loopName, self::ALT_CONTENT)); } $out->append(1, 'endif; '); $out->append(1, '$parent_hash = $this->_vars[\''.$hash.'\'][\'_parent_hash\']; '); $out->append(1, 'unset($result_'.$hash.', $nb_rows, $this->_vars[\''.$hash.'\']); '); $out->append(1, 'if ($parent_hash) { $this->current =& $this->_vars[$parent_hash]; $parent_hash = $this->current[\'_parent_hash\']; } '); $out->append(1, 'else { $this->current = null; }'); $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$parent_hash] : null;'); return $out; } public function fetch($template, $no_display = false) { $this->currentTemplate = $template; $path = file_exists(DATA_ROOT . '/www/squelettes/' . $template) ? DATA_ROOT . '/www/squelettes/' . $template : ROOT . '/www/squelettes-dist/' . $template; $tpl_id = basename(dirname($path)) . '/' . $template; if (!self::compile_check($tpl_id, $path)) { if (!file_exists($path)) { throw new \KD2\MiniSkelMarkupException('Le squelette "'.$tpl_id.'" n\'existe pas.'); } $content = file_get_contents($path); $content = strtr($content, ['<?php' => '<?php', '<?' => '<?php echo \'<?\'; ?>']); $out = new Squelette_Snippet(2, $this->parse($content)); $out->prepend(1, '/* '.$tpl_id.' */ '. 'namespace Garradin; $db = DB::getInstance(); '. 'if ($this->parent) $parent_hash = $this->parent[\'_self_hash\']; '. // For included files 'else $parent_hash = false;'); if (!$no_display) { self::compile_store($tpl_id, $out); } } if (!$no_display) { require self::compile_get_path($tpl_id); } else { eval($tpl_id); } return null; } public function dispatchURI() { $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; header('HTTP/1.1 200 OK', 200, true); if ($pos = strpos($uri, '?')) { $uri = substr($uri, 0, $pos); } else { // WWW_URI inclus toujours le slash final, mais on veut le conserver ici $uri = substr($uri, strlen(WWW_URI) - 1); } if ($uri == '/') { $skel = 'sommaire.html'; } elseif ($uri == '/feed/atom/') { header('Content-Type: application/atom+xml'); $skel = 'atom.xml'; } elseif ($uri == '/favicon.ico') { header('Location: ' . ADMIN_URL . 'static/icon.png'); exit; } elseif (substr($uri, -1) == '/') { $skel = 'rubrique.html'; $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1, -1); } elseif (preg_match('!^/admin/!', $uri)) { http_response_code(404); throw new UserException('Cette page n\'existe pas.'); } else { $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1); if (preg_match('!^[\w\d_-]+$!i', $_GET['uri']) && file_exists(DATA_ROOT . '/www/squelettes/' . strtolower($_GET['uri']) . '.html')) { $skel = strtolower($_GET['uri']) . '.html'; } else { $skel = 'article.html'; } } $this->display($skel); } static private function compile_get_path($path) { $hash = sha1($path); return CACHE_ROOT . '/compiled/s_' . $hash . '.php'; } static private function compile_check($tpl, $check) { if (!file_exists(self::compile_get_path($tpl))) return false; $time = filemtime(self::compile_get_path($tpl)); if (empty($time)) { return false; } if ($time < filemtime($check)) return false; return $time; } static private function compile_store($tpl, $content) { $path = self::compile_get_path($tpl); if (!file_exists(dirname($path))) { Utils::safe_mkdir(dirname($path), 0777, true); } file_put_contents($path, $content); return true; } static public function compile_clear($tpl) { $path = self::compile_get_path($tpl); if (file_exists($path)) { Utils::safe_unlink($path); } return true; } protected function getVariable($var) { if (isset($this->current[$var])) { return $this->current[$var]; } elseif (isset($this->parent[$var])) { return $this->parent[$var]; } elseif (isset($this->variables[$var])) { return $this->variables[$var]; } elseif (isset($_REQUEST[$var])) { return $_REQUEST[$var]; } else { return null; } } static public function getSource($template) { if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $template)) { return null; } $path = file_exists(DATA_ROOT . '/www/squelettes/' . $template) ? DATA_ROOT . '/www/squelettes/' . $template : ROOT . '/www/squelettes-dist/' . $template; if (!file_exists($path)) { return null; } return file_get_contents($path); } static public function editSource($template, $content) { if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $template)) return false; $path = DATA_ROOT . '/www/squelettes/' . $template; return file_put_contents($path, $content); } static public function resetSource($template) { if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $template)) return false; if (file_exists(DATA_ROOT . '/www/squelettes/' . $template)) { return Utils::safe_unlink(DATA_ROOT . '/www/squelettes/' . $template); } return false; } static public function listSources() { if (!file_exists(DATA_ROOT . '/www/squelettes')) { Utils::safe_mkdir(DATA_ROOT . '/www/squelettes', 0775, true); } $sources = []; $dir = dir(ROOT . '/www/squelettes-dist'); while ($file = $dir->read()) { if ($file[0] == '.') continue; if (!preg_match('/\.(?:css|x?html?|atom|rss|xml|svg|txt)$/i', $file)) continue; $sources[$file] = false; } $dir->close(); $dir = dir(DATA_ROOT . '/www/squelettes'); while ($file = $dir->read()) { if ($file[0] == '.') continue; if (!preg_match('/\.(?:css|x?html?|atom|rss|xml|svg|txt)$/i', $file)) continue; $sources[$file] = [ // Est-ce que le fichier fait partie de la distribution de Garradin? // (Si non pas possible de faire un reset) 'dist' => array_key_exists($file, $sources), 'mtime' => filemtime($dir->path.'/'.$file), ]; } $dir->close(); ksort($sources); return $sources; } } |
< < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted src/include/lib/Garradin/Squelette_Filtres.php version [4a8a3bb75b].
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 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 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
<?php namespace Garradin; class Squelette_Filtres { static private $alt = []; static public $filtres_php = [ 'strtolower', 'strtoupper', 'ucfirst', 'ucwords', 'str_rot13', 'str_shuffle', 'htmlentities', 'htmlspecialchars', 'trim', 'ltrim', 'rtrim', 'lcfirst', 'md5', 'sha1', 'metaphone', 'nl2br', 'soundex', 'str_split', 'str_word_count', 'strrev', 'strlen', 'wordwrap', 'strip_tags' => 'supprimer_tags', 'var_dump', ]; static public $filtres_alias = [ '!=' => 'different_de', '==' => 'egal_a', '?' => 'choixsivide', '>' => 'superieur_a', '>=' => 'superieur_ou_egal_a', '<' => 'inferieur_a', '<=' => 'inferieur_ou_egal_a', 'yes' => 'oui', 'no' => 'non', 'and' => 'et', 'or' => 'ou', 'xor' => 'xou', 'supprimer_spip' => 'supprimer_skriv', ]; static public $desactiver_defaut = [ 'formatter_texte', 'entites_html', 'proteger_contact', 'echapper_xml', ]; static public function date($date, $format) { return strftime($format, $date); } static public function date_en_francais($date) { return ucfirst(strtolower(Utils::strftime_fr($date, '%A %e %B %Y'))); } static public function heure_en_francais($date) { return Utils::strftime_fr($date, '%Hh%M'); } static public function mois_en_francais($date) { return Utils::strftime_fr($date, '%B %Y'); } static public function date_perso($date, $format) { return Utils::strftime_fr($date, $format); } static public function date_intelligente($date, $avec_heure = true) { $jour = null; $heure = date('H\hi', $date); if (date('Ymd', $date) == date('Ymd')) { $jour = 'aujourd\'hui'; } elseif (date('Ymd', $date) == date('Ymd', strtotime('yesterday'))) { $jour = 'hier'; } elseif (date('Ymd', $date) == date('Ymd', strtotime('tomorrow'))) { $jour = 'demain'; } elseif (date('Y', $date) == date('Y')) { $jour = strtolower(Utils::strftime_fr($date, '%A %e %B')); } else { $jour = strtolower(Utils::strftime_fr($date, '%e %B %Y')); } if ($avec_heure) { return sprintf('%s, %s', $jour, $heure); } return $jour; } static public function date_atom($date) { return date(DATE_ATOM, $date); } static public function alterner($v, $name, $valeur1, $valeur2) { if (!array_key_exists($name, self::$alt)) { self::$alt[$name] = 0; } if (self::$alt[$name]++ % 2 == 0) return $valeur1; else return $valeur2; } static public function proteger_contact($contact) { if (!trim($contact)) return ''; if (strpos($contact, '@')) { $reversed = strrev($contact); // https://unicode-table.com/en/FF20/ $reversed = strtr($reversed, ['@' => '@']); return sprintf('<a href="#error" onclick="this.href = (this.innerText + \':otliam\').split(\'\').reverse().join(\'\').replace(/@/, \'@\');"><span style="unicode-bidi:bidi-override;direction: rtl;">%s</span></a>', htmlspecialchars($reversed)); } else { return '<a href="'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'">'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'</a>'; } } static public function entites_html($texte) { return htmlspecialchars($texte, ENT_QUOTES, 'UTF-8'); } static public function echapper_xml($texte) { return str_replace(''', ''', htmlspecialchars($texte, ENT_QUOTES, 'UTF-8')); } static public function formatter_texte($texte) { $texte = Utils::SkrivToHTML($texte); $texte = self::typo_fr($texte); // Liens wiki $texte = preg_replace_callback('!<a href="([^/.:@]+)">!i', function ($matches) { return '<a href="' . WWW_URL . Wiki::transformTitleToURI($matches[1]) . '">'; }, $texte); return $texte; } static public function typo_fr($str, $html = true) { $space = $html ? ' ' : ' '; $str = preg_replace('/(?:[\h]| )*([?!:»])(\s+|$)/u', $space.'\\1\\2', $str); $str = preg_replace('/(^|\s+)([«])(?:[\h]| )*/u', '\\1\\2'.$space, $str); return $str; } static public function pagination($total, $debut, $par_page) { $max_page = ceil($total / $par_page); $current = ($debut > 0) ? ceil($debut / $par_page) + 1 : 1; $out = ''; if ($current > 1) { $out .= '<a href="./'.($current > 2 ? '+' . ($debut - $par_page) : '').'">« Page précédente</a> - '; } for ($i = 1; $i <= $max_page; $i++) { $link = ($i == 1) ? './' : './+' . (($i - 1) * $par_page); if ($i == $current) $out .= '<strong>'.$i.'</strong> - '; else $out .= '<a href="'.$link.'">'.$i.'</a> - '; } if ($current < $max_page) { $out .= '<a href="./+'.($debut + $par_page).'">Page suivante »</a>'; } else { $out = substr($out, 0, -3); } return $out; } // Compatibilité SPIP static public function egal_a($value, $test) { if ($value == $test) return true; else return false; } static public function different_de($value, $test) { if ($value != $test) return true; else return false; } // disponible aussi avec : | ?{sioui, sinon} static public function choixsivide($value, $un, $deux = '') { if (empty($value) || !trim($value)) return $deux; else return $un; } static public function sinon($value, $sinon = '') { if ($value) return $value; else return $sinon; } static public function choixsiegal($value, $test, $un, $deux) { return ($value == $test) ? $un : $deux; } static public function supprimer_tags($value, $replace = '') { return preg_replace('!<[^>]*>!', $replace, $value); } static public function supprimer_skriv($value) { $value = self::formatter_texte($value); return self::supprimer_tags($value); } static public function couper($texte, $taille, $etc = ' (...)') { if (strlen($texte) > $taille) { $texte = substr($texte, 0, $taille); $taille -= ($taille * 0.1); $texte = preg_replace('!([\s.,;:\!?])[^\s.,;:\!?]*?$!', '\\1', $texte); $texte.= $etc; } return $texte; } static public function replace($texte, $expression, $replace, $modif='UsimsS') { return preg_replace('/'.$expression.'/'.$modif, $replace, $texte); } static public function plus($a, $b) { return $a + $b; } static public function moins($a, $b) { return $a - $b; } static public function mult($a, $b) { return $a * $b; } static public function div($a, $b) { return $b ? $a / $b : 0; } static public function modulo($a, $mod, $add) { return ($mod ? $a % $mod : 0) + $add; } static public function vide($value) { return ''; } static public function concat() { return implode('', func_get_args()); } static public function singulier_ou_pluriel($nb, $singulier, $pluriel, $var = null) { if (!$nb) return ''; if ($nb == 1) return str_replace('@'.$var.'@', $nb, $singulier); else return str_replace('@'.$var.'@', $nb, $pluriel); } static public function date_w3c($date) { return date(DATE_W3C, $date); } static public function et($value, $test) { return ($value && $test) ? true : false; } static public function ou($value, $test) { return ($value || $test) ? true : false; } static public function xou($value, $test) { return ($value XOR $test) ? true : false; } static public function oui($value) { return $value ? true : false; } static public function non($value) { return !$value ? true : false; } static public function superieur_a($value, $test) { return ($value > $test) ? true : false; } static public function superieur_ou_egal_a($value, $test) { return ($value >= $test) ? true : false; } static public function inferieur_a($value, $test) { return ($value < $test) ? true : false; } static public function inferieur_ou_egal_a($value, $test) { return ($value <= $test) ? true : false; } static public function euros($value) { return str_replace(' ', ' ', number_format($value, (round($value) == round($value, 2) ? 0 : 2), ',', ' ')) . ' €'; } static public function taille_en_octets($value) { return Utils::format_bytes($value); } } |
< < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/include/lib/Garradin/Template.php from [cf65df4d0f] to [01bab329ab].
3 4 5 6 7 8 9 10 11 12 13 14 15 16 .. 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 .. 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 ... 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 ... 225 226 227 228 229 230 231 232 233 234 235 236 237 238 ... 245 246 247 248 249 250 251 252 253 254 255 256 257 258 ... 276 277 278 279 280 281 282 283 284 285 286 287 288 289 ... 382 383 384 385 386 387 388 389 390 391 392 393 394 395 ... 409 410 411 412 413 414 415 416 417 418 419 420 421 422 ... 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 ... 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 ... 565 566 567 568 569 570 571 572 573 574 575 576 577 578 ... 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 |
namespace Garradin; use KD2\Form; use KD2\HTTP; use KD2\Translate; use Garradin\Membres\Session; use Garradin\Entities\Accounting\Account; class Template extends \KD2\Smartyer { static protected $_instance = null; static public function getInstance() { ................................................................................ $this->register_compile_function('continue', function ($pos, $block, $name, $raw_args) { if ($block == 'continue') { return 'continue;'; } }); $this->register_function('form_errors', [$this, 'formErrors']); $this->register_function('show_error', [$this, 'showError']); $this->register_function('form_field', [$this, 'formField']); $this->register_function('html_champ_membre', [$this, 'formChampMembre']); $this->register_function('input', [$this, 'formInput']); $this->register_function('custom_colors', [$this, 'customColors']); $this->register_function('plugin_url', ['Garradin\Utils', 'plugin_url']); $this->register_function('diff', [$this, 'diff']); $this->register_function('pagination', [$this, 'pagination']); $this->register_function('format_droits', [$this, 'formatDroits']); $this->register_function('csrf_field', function ($params) { return Form::tokenHTML($params['key']); }); $this->register_function('icon', [$this, 'widgetIcon']); $this->register_function('button', [$this, 'widgetButton']); ................................................................................ $this->register_modifier('strlen', 'strlen'); $this->register_modifier('dump', ['KD2\ErrorManager', 'dump']); $this->register_modifier('get_country_name', ['Garradin\Utils', 'getCountryName']); $this->register_modifier('format_tel', [$this, 'formatPhoneNumber']); $this->register_modifier('abs', 'abs'); $this->register_modifier('display_champ_membre', [$this, 'displayChampMembre']); $this->register_modifier('format_bytes', ['Garradin\Utils', 'format_bytes']); $this->register_modifier('strftime_fr', [Utils::class, 'strftime_fr']); $this->register_modifier('date_fr', [Utils::class, 'date_fr']); $this->register_modifier('date_long', [Utils::class, 'date_fr']); $this->register_modifier('date_short', function ($dt) { return Utils::date_fr($dt, 'd/m/Y'); }); $this->register_modifier('html_money', [$this, 'htmlMoney']); $this->register_modifier('money_currency', [$this, 'htmlMoneyCurrency']); $this->register_modifier('format_wiki', function ($str) { $str = Utils::SkrivToHTML($str); $str = Squelette_Filtres::typo_fr($str); return $str; }); $this->register_modifier('liens_wiki', function ($str, $prefix) { return preg_replace_callback('!<a href="([^/.:@]+)">!i', function ($matches) use ($prefix) { return '<a href="' . $prefix . Wiki::transformTitleToURI($matches[1]) . '">'; }, $str); }); } protected function htmlMoney($number, bool $hide_empty = true): string { if ($hide_empty && !$number) { return ''; } return sprintf('<b class="money">%s</b>', Utils::money_format($number, ',', ' ', $hide_empty)); } protected function htmlMoneyCurrency($number, bool $hide_empty = true): string { $out = $this->htmlMoney($number, $hide_empty); if ($out !== '') { $out .= ' ' . Config::getInstance()->get('monnaie'); } return $out; } protected function formErrors($params) { $form = $this->getTemplateVars('form'); if (!$form->hasErrors()) { ................................................................................ $shape = $params['shape']; $label = $params['label']; // href can be prefixed with '!' to make the URL relative to ADMIN_URL if (substr($href, 0, 1) == '!') { $href = ADMIN_URL . substr($params['href'], 1); } unset($params['href'], $params['shape'], $params['label']); array_walk($params, function (&$v, $k) { $v = sprintf('%s="%s"', $k, $this->escape($v)); }); $params = implode(' ', $params); return sprintf('<a class="icn-btn" data-icon="%s" href="%s" %s>%s</a>', Utils::iconUnicode($shape), $this->escape($href), $params, $this->escape($label)); } protected function formInput(array $params) { static $params_list = ['value', 'default', 'type', 'help', 'label', 'name', 'options', 'source']; // Extract params and keep attributes separated ................................................................................ $attributes = array_diff_key($params, array_flip($params_list)); $params = array_intersect_key($params, array_flip($params_list)); extract($params, \EXTR_SKIP); if (!isset($name, $type)) { throw new \InvalidArgumentException('Missing name or type'); } $current_value = null; $current_value_from_user = false; if (isset($_POST[$name])) { $current_value = $_POST[$name]; $current_value_from_user = true; ................................................................................ } elseif (isset($default) && ($type != 'checkbox' || empty($_POST))) { $current_value = $default; } if ($type == 'date' && is_object($current_value) && $current_value instanceof \DateTimeInterface) { $current_value = $current_value->format('d/m/Y'); } elseif ($type == 'date' && is_string($current_value)) { if ($v = \DateTime::createFromFormat('!Y-m-d', $current_value)) { $current_value = $v->format('d/m/Y'); } } ................................................................................ $type = 'text'; $attributes['placeholder'] = 'JJ/MM/AAAA'; $attributes['data-input'] = 'date'; $attributes['size'] = 12; $attributes['maxlength'] = 10; $attributes['pattern'] = '\d\d?/\d\d?/\d{4}'; } // Create attributes string if (array_key_exists('required', $attributes)) { $attributes['required'] = 'required'; } if (!empty($attributes['disabled'])) { ................................................................................ return $input; } if ($type == 'file') { $input .= sprintf('<input type="hidden" name="MAX_FILE_SIZE" value="%d" id="f_maxsize" />', Utils::return_bytes(Utils::getMaxUploadSize())); } $label = sprintf('<label for="%s">%s</label>', $attributes['id'], $this->escape($label)); if ($type == 'radio' || $type == 'checkbox') { $out = sprintf('<dd>%s %s', $input, $label); if (isset($help)) { ................................................................................ $out .= sprintf('<dd class="help">%s</dd>', $this->escape($help)); } } return $out; } protected function formField(array $params, $escape = true) { if (!isset($params['name'])) { throw new \BadFunctionCallException('name argument is mandatory'); } ................................................................................ protected function customColors() { $config = Config::getInstance(); $couleur1 = $config->get('couleur1') ?: ADMIN_COLOR1; $couleur2 = $config->get('couleur2') ?: ADMIN_COLOR2; $image_fond = ADMIN_BACKGROUND_IMAGE; if ($config->get('image_fond')) { try { $f = new Fichiers($config->get('image_fond')); $image_fond = $f->getURL(); } catch (\InvalidArgumentException $e) { // Fichier qui n'existe pas/plus } } // Transformation Hexa vers décimal $couleur1 = implode(', ', sscanf($couleur1, '#%02x%02x%02x')); $couleur2 = implode(', ', sscanf($couleur2, '#%02x%02x%02x')); $out = ' ................................................................................ :root { --gMainColor: %s; --gSecondColor: %s; --gBgImage: url("%s"); } </style>'; return sprintf($out, $couleur1, $couleur2, $image_fond); } protected function displayChampMembre($v, $config = null) { if (is_string($config)) { $config = Config::getInstance()->get('champs_membres')->get($config); } ................................................................................ $config = $params['config']; $type = $config->type; if ($params['name'] == 'passe' || (!empty($params['user_mode']) && !empty($config->private))) { return ''; } $options = []; if ($type == 'select' || $type == 'multiple') { if (empty($config->options)) { ................................................................................ $prev = $i; } $out .= '</table>'; return $out; } protected function pagination(array $params) { if (!isset($params['url']) || !isset($params['page']) || !isset($params['bypage']) || !isset($params['total'])) throw new \BadFunctionCallException("Paramètre manquant pour pagination"); if ($params['total'] == -1) return ''; $pagination = Utils::getGenericPagination($params['page'], $params['total'], $params['bypage']); if (empty($pagination)) return ''; $out = '<ul class="pagination">'; $encoded_url = rawurlencode('[ID]'); foreach ($pagination as &$page) { $attributes = ''; if (!empty($page['class'])) $attributes .= ' class="' . htmlspecialchars($page['class']) . '" '; $out .= '<li'.$attributes.'>'; $attributes = ''; if (!empty($page['accesskey'])) $attributes .= ' accesskey="' . htmlspecialchars($page['accesskey']) . '" '; $out .= '<a' . $attributes . ' href="' . str_replace(['[ID]', $encoded_url], htmlspecialchars($page['id']), $params['url']) . '">'; $out .= htmlspecialchars($page['label']); $out .= '</a>'; $out .= '</li>' . "\n"; } $out .= '</ul>'; return $out; } protected function formatDroits(array $params) { $droits = $params['droits']; $out = ['connexion' => '', 'inscription' => '', 'membres' => '', 'compta' => '', 'wiki' => '', 'config' => '']; $classes = [ Membres::DROIT_AUCUN => 'aucun', Membres::DROIT_ACCES => 'acces', Membres::DROIT_ECRITURE=> 'ecriture', Membres::DROIT_ADMIN => 'admin', ]; foreach ($droits as $cle=>$droit) { $cle = str_replace('droit_', '', $cle); if (array_key_exists($cle, $out)) { $class = $classes[$droit]; $desc = false; $s = false; if ($cle == 'connexion') { if ($droit == Membres::DROIT_AUCUN) $desc = 'N\'a pas le droit de se connecter'; else $desc = 'A le droit de se connecter'; } elseif ($cle == 'inscription') { if ($droit == Membres::DROIT_AUCUN) $desc = 'N\'a pas le droit de s\'inscrire seul'; else $desc = 'A le droit de s\'inscrire seul'; } elseif ($cle == 'config') { $s = '☑'; if ($droit == Membres::DROIT_AUCUN) $desc = 'Ne peut modifier la configuration'; else $desc = 'Peut modifier la configuration'; } elseif ($cle == 'compta') { $s = '€'; } if (!$s) $s = strtoupper($cle[0]); if (!$desc) { $desc = ucfirst($cle). ' : '; if ($droit == Membres::DROIT_AUCUN) $desc .= 'Pas accès'; elseif ($droit == Membres::DROIT_ACCES) $desc .= 'Lecture uniquement'; elseif ($droit == Membres::DROIT_ECRITURE) $desc .= 'Lecture & écriture'; else $desc .= 'Administration'; } $out[$cle] = '<b class="'.$class.' '.$cle.'" title="' .htmlspecialchars($desc, ENT_QUOTES, 'UTF-8').'">'.$s.'</b>'; } } return implode(' ', $out); } } |
> > > > > > > > > > > | < < < < < < < < < < < < < | < < < < < < < < | > > | < < < < > > < > < < < < < < < < < < > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | < < < < < < < < < > | > > > > > | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < | < < < < < < < < > > > > | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .. 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 .. 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 ... 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 ... 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 ... 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 ... 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 ... 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 ... 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 ... 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 ... 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 ... 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 ... 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 |
namespace Garradin; use KD2\Form; use KD2\HTTP; use KD2\Translate; use Garradin\Membres\Session; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Users\Category; use Garradin\UserTemplate\CommonModifiers; use Garradin\Web\Render\Skriv; class Template extends \KD2\Smartyer { static protected $_instance = null; static public function getInstance() { ................................................................................ $this->register_compile_function('continue', function ($pos, $block, $name, $raw_args) { if ($block == 'continue') { return 'continue;'; } }); $this->register_compile_function('use', function ($pos, $block, $name, $raw_args) { if ($name == 'use') { return sprintf('use %s;', $raw_args); } }); $this->register_function('form_errors', [$this, 'formErrors']); $this->register_function('show_error', [$this, 'showError']); $this->register_function('form_field', [$this, 'formField']); $this->register_function('html_champ_membre', [$this, 'formChampMembre']); $this->register_function('input', [$this, 'formInput']); $this->register_function('password_change', [$this, 'passwordChangeInput']); $this->register_function('custom_colors', [$this, 'customColors']); $this->register_function('plugin_url', ['Garradin\Utils', 'plugin_url']); $this->register_function('diff', [$this, 'diff']); $this->register_function('display_permissions', [$this, 'displayPermissions']); $this->register_function('csrf_field', function ($params) { return Form::tokenHTML($params['key']); }); $this->register_function('icon', [$this, 'widgetIcon']); $this->register_function('button', [$this, 'widgetButton']); ................................................................................ $this->register_modifier('strlen', 'strlen'); $this->register_modifier('dump', ['KD2\ErrorManager', 'dump']); $this->register_modifier('get_country_name', ['Garradin\Utils', 'getCountryName']); $this->register_modifier('format_tel', [$this, 'formatPhoneNumber']); $this->register_modifier('abs', 'abs'); $this->register_modifier('display_champ_membre', [$this, 'displayChampMembre']); $this->register_modifier('format_skriv', function ($str) { return Skriv::render(null, $str); }); foreach (CommonModifiers::MODIFIERS_LIST as $key => $name) { $this->register_modifier(is_int($key) ? $name : $key, is_int($key) ? [CommonModifiers::class, $name] : $name); } foreach (CommonModifiers::FUNCTIONS_LIST as $key => $name) { $this->register_function(is_int($key) ? $name : $key, is_int($key) ? [CommonModifiers::class, $name] : $name); } $this->register_modifier('local_url', [Utils::class, 'getLocalURL']); } protected function formErrors($params) { $form = $this->getTemplateVars('form'); if (!$form->hasErrors()) { ................................................................................ $shape = $params['shape']; $label = $params['label']; // href can be prefixed with '!' to make the URL relative to ADMIN_URL if (substr($href, 0, 1) == '!') { $href = ADMIN_URL . substr($params['href'], 1); } if (!isset($params['class'])) { $params['class'] = ''; } $params['class'] .= ' icn-btn'; unset($params['href'], $params['shape'], $params['label']); array_walk($params, function (&$v, $k) { $v = sprintf('%s="%s"', $k, $this->escape($v)); }); $params = implode(' ', $params); return sprintf('<a data-icon="%s" href="%s" %s>%s</a>', Utils::iconUnicode($shape), $this->escape($href), $params, $this->escape($label)); } protected function passwordChangeInput(array $params) { $out = $this->formInput(array_merge($params, [ 'type' => 'password', 'help' => sprintf('(Minimum %d caractères)', Session::MINIMUM_PASSWORD_LENGTH), 'minlength' => Session::MINIMUM_PASSWORD_LENGTH, ])); $out.= '<dd class="help">Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr et plus simple à retenir qu\'un mot de passe composé de 10 lettres et chiffres.</dd>'; $suggestion = Utils::suggestPassword(); $out .= sprintf('<dd class="help">Pas d\'idée ? Voici une suggestion choisie au hasard : <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="f_%s_suggest" value="%s" autocomplete="off" size="%d" /></dd>', $params['name'], $suggestion, strlen($suggestion)); $out .= $this->formInput([ 'type' => 'password', 'label' => 'Répéter le mot de passe', 'required' => true, 'name' => $params['name'] . '_confirm', 'minlength' => Session::MINIMUM_PASSWORD_LENGTH, ]); return $out; } protected function formInput(array $params) { static $params_list = ['value', 'default', 'type', 'help', 'label', 'name', 'options', 'source']; // Extract params and keep attributes separated ................................................................................ $attributes = array_diff_key($params, array_flip($params_list)); $params = array_intersect_key($params, array_flip($params_list)); extract($params, \EXTR_SKIP); if (!isset($name, $type)) { throw new \InvalidArgumentException('Missing name or type'); } $suffix = null; if ($type == 'datetime') { $type = 'date'; $tparams = func_get_arg(0); $tparams['type'] = 'time'; $tparams['name'] = sprintf('%s_time', $name); unset($tparams['label']); $suffix = self::formInput($tparams); } $current_value = null; $current_value_from_user = false; if (isset($_POST[$name])) { $current_value = $_POST[$name]; $current_value_from_user = true; ................................................................................ } elseif (isset($default) && ($type != 'checkbox' || empty($_POST))) { $current_value = $default; } if ($type == 'date' && is_object($current_value) && $current_value instanceof \DateTimeInterface) { $current_value = $current_value->format('d/m/Y'); } elseif ($type == 'time' && is_object($current_value) && $current_value instanceof \DateTimeInterface) { $current_value = $current_value->format('H:i'); } elseif ($type == 'date' && is_string($current_value)) { if ($v = \DateTime::createFromFormat('!Y-m-d', $current_value)) { $current_value = $v->format('d/m/Y'); } } ................................................................................ $type = 'text'; $attributes['placeholder'] = 'JJ/MM/AAAA'; $attributes['data-input'] = 'date'; $attributes['size'] = 12; $attributes['maxlength'] = 10; $attributes['pattern'] = '\d\d?/\d\d?/\d{4}'; } elseif ($type == 'time') { $type = 'text'; $attributes['placeholder'] = 'HH:MM'; $attributes['data-input'] = 'time'; $attributes['size'] = 8; $attributes['maxlength'] = 5; $attributes['pattern'] = '\d\d?:\d\d?'; } // Create attributes string if (array_key_exists('required', $attributes)) { $attributes['required'] = 'required'; } if (!empty($attributes['disabled'])) { ................................................................................ return $input; } if ($type == 'file') { $input .= sprintf('<input type="hidden" name="MAX_FILE_SIZE" value="%d" id="f_maxsize" />', Utils::return_bytes(Utils::getMaxUploadSize())); } $input .= $suffix; $label = sprintf('<label for="%s">%s</label>', $attributes['id'], $this->escape($label)); if ($type == 'radio' || $type == 'checkbox') { $out = sprintf('<dd>%s %s', $input, $label); if (isset($help)) { ................................................................................ $out .= sprintf('<dd class="help">%s</dd>', $this->escape($help)); } } return $out; } /** * @deprecated */ protected function formField(array $params, $escape = true) { if (!isset($params['name'])) { throw new \BadFunctionCallException('name argument is mandatory'); } ................................................................................ protected function customColors() { $config = Config::getInstance(); $couleur1 = $config->get('couleur1') ?: ADMIN_COLOR1; $couleur2 = $config->get('couleur2') ?: ADMIN_COLOR2; $admin_background = ADMIN_BACKGROUND_IMAGE; if ($f = $config->get('admin_background')) { $admin_background = $f->url(); } // Transformation Hexa vers décimal $couleur1 = implode(', ', sscanf($couleur1, '#%02x%02x%02x')); $couleur2 = implode(', ', sscanf($couleur2, '#%02x%02x%02x')); $out = ' ................................................................................ :root { --gMainColor: %s; --gSecondColor: %s; --gBgImage: url("%s"); } </style>'; return sprintf($out, $couleur1, $couleur2, $admin_background); } protected function displayChampMembre($v, $config = null) { if (is_string($config)) { $config = Config::getInstance()->get('champs_membres')->get($config); } ................................................................................ $config = $params['config']; $type = $config->type; if ($params['name'] == 'passe' || (!empty($params['user_mode']) && !empty($config->private))) { return ''; } // Files are managed out of the form if ($config->type == 'file') { return ''; } $options = []; if ($type == 'select' || $type == 'multiple') { if (empty($config->options)) { ................................................................................ $prev = $i; } $out .= '</table>'; return $out; } protected function displayPermissions(array $params): string { $perms = $params['permissions']; $out = []; foreach (Category::PERMISSIONS as $name => $config) { $access = $perms->{'perm_' . $name}; $label = $config['options'][$access]; $out[$name] = sprintf('<b class="access_%s %s" title="%s">%s</b>', $access, $name, htmlspecialchars($label), $config['shape']); } return implode(' ', $out); } } |
Modified src/include/lib/Garradin/Upgrade.php from [f128060934] to [4f1247ba58].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 .. 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 ... 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 ... 167 168 169 170 171 172 173 174 |
<?php namespace Garradin; use Garradin\Membres\Session; class Upgrade { const MIN_REQUIRED_VERSION = '0.9.8'; static public function preCheck(): bool { $config = Config::getInstance(); $v = $config->getVersion(); if (version_compare($v, garradin_version(), '>=')) { return false; } if (!$v || version_compare($v, self::MIN_REQUIRED_VERSION, '<')) ................................................................................ throw new UserException('Une mise à jour est déjà en cours.' . PHP_EOL . 'Si celle-ci a échouée et que vous voulez ré-essayer, supprimez le fichier suivant:' . PHP_EOL . $path); } // Voir si l'utilisateur est loggé, on le fait ici pour le cas où // il y aurait déjà eu des entêtes envoyés au navigateur plus bas $session = new Session; $user_is_logged = $session->isLogged(true); return true; } static public function upgrade() { $config = Config::getInstance(); $v = $config->getVersion(); $session = new Session; $user_is_logged = $session->isLogged(true); Static_Cache::store('upgrade', 'Mise à jour en cours.'); $db = DB::getInstance(); // reset last version check $db->exec('UPDATE config SET valeur = NULL WHERE cle = \'last_version_check\';'); // Créer une sauvegarde automatique $backup_name = (new Sauvegarde)->create('pre-upgrade-' . garradin_version()); try { if (version_compare($v, '1.0.0-alpha1', '<')) { $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0_migration.sql'); $db->commitSchemaUpdate(); // Import nouveau plan comptable $chart = new \Garradin\Entities\Accounting\Chart; $chart->label = 'Plan comptable associatif 2018'; $chart->country = 'FR'; $chart->code = 'PCA2018'; $chart->save(); $chart->accounts()->importCSV(ROOT . '/include/data/charts/fr_2018.csv'); } if (version_compare($v, '1.0.0-beta1', '>=') && version_compare($v, '1.0.0-beta6', '<')) { $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0-beta6_migration.sql'); $db->commitSchemaUpdate(); } if (version_compare($v, '1.0.0-beta8', '<')) { $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0-beta8_migration.sql'); $db->commitSchemaUpdate(); } if (version_compare($v, '1.0.0-beta1', '>=') && version_compare($v, '1.0.0-rc3', '<')) { $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0-rc3_migration.sql'); $db->commitSchemaUpdate(); } if (version_compare($v, '1.0.0-rc10', '<')) { $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0-rc10_migration.sql'); $db->commitSchemaUpdate(); } if (version_compare($v, '1.0.0-beta1', '>=') && version_compare($v, '1.0.0-rc11', '<')) { // Missing trigger $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0_schema.sql'); $db->commitSchemaUpdate(); } if (version_compare($v, '1.0.0-rc14', '<')) { // Missing trigger $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0-rc14_migration.sql'); $db->commitSchemaUpdate(); } if (version_compare($v, '1.0.0-rc16', '<')) { // Missing trigger $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0-rc16_migration.sql'); $db->commitSchemaUpdate(); } if (version_compare($v, '1.0.1', '<')) { // Missing trigger $db->begin(); $db->import(ROOT . '/include/data/1.0.1_migration.sql'); $db->commit(); } ................................................................................ if (version_compare($v, '1.0.3', '<')) { // Missing trigger $db->begin(); $db->import(ROOT . '/include/data/1.0.3_migration.sql'); $db->commit(); } // Vérification de la cohérence des clés étrangères $db->foreignKeyCheck(); Utils::clearCaches(); $config->setVersion(garradin_version()); Static_Cache::remove('upgrade'); // Réinstaller les plugins système si nécessaire Plugin::checkAndInstallSystemPlugins(); Plugin::upgradeAllIfRequired(); ................................................................................ // Forcer à rafraîchir les données de la session si elle existe if ($user_is_logged) { $session->refresh(); } } } |
> | | < | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > | > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 .. 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 .. 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 ... 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 |
<?php namespace Garradin; use Garradin\Membres\Session; use Garradin\Membres\Champs; class Upgrade { const MIN_REQUIRED_VERSION = '1.0.0'; static public function preCheck(): bool { $v = DB::getInstance()->version(); if (version_compare($v, garradin_version(), '>=')) { return false; } if (!$v || version_compare($v, self::MIN_REQUIRED_VERSION, '<')) ................................................................................ throw new UserException('Une mise à jour est déjà en cours.' . PHP_EOL . 'Si celle-ci a échouée et que vous voulez ré-essayer, supprimez le fichier suivant:' . PHP_EOL . $path); } // Voir si l'utilisateur est loggé, on le fait ici pour le cas où // il y aurait déjà eu des entêtes envoyés au navigateur plus bas $session = Session::getInstance(); $user_is_logged = $session->isLogged(true); return true; } static public function upgrade() { $db = DB::getInstance(); $v = $db->version(); $session = Session::getInstance(); $user_is_logged = $session->isLogged(true); Static_Cache::store('upgrade', 'Mise à jour en cours.'); // Créer une sauvegarde automatique $backup_name = (new Sauvegarde)->create('pre-upgrade-' . garradin_version()); try { if (version_compare($v, '1.0.1', '<')) { // Missing trigger $db->begin(); $db->import(ROOT . '/include/data/1.0.1_migration.sql'); $db->commit(); } ................................................................................ if (version_compare($v, '1.0.3', '<')) { // Missing trigger $db->begin(); $db->import(ROOT . '/include/data/1.0.3_migration.sql'); $db->commit(); } if (version_compare($v, '1.1.0', '<=')) { // Missing trigger $db->beginSchemaUpdate(); $attachments = $db->getAssoc('SELECT id, nom FROM fichiers;'); // Update Skriv content for attachments foreach ($db->iterate('SELECT rowid, contenu FROM wiki_revisions;') as $r) { $content = preg_replace_callback('!<<(image|fichier)\|(\d+)\|(gauche|droite|centre)>>!', function ($match) use ($attachments) { $name = $attachments[$match[2]] ?? '_ERREUR_fichier_inconnu_' . $match[2]; $align = ($match[3] == 'centre' ? 'center' : ($match[3] == 'gauche' ? 'left' : 'right')); return sprintf('<<%s|%s|%s>>', $match[1] == 'fichier' ? 'file' : 'image', $name, $align); }, $r->contenu); $content = preg_replace_callback('!(image|fichier)://(\d+)!', function ($match) use ($attachments) { $name = $attachments[$match[2]] ?? '_ERREUR_fichier_inconnu_' . $match[2]; return sprintf('#file:[%s]', $name); }, $content); if ($content != $r->contenu) { $db->update('wiki_revisions', ['contenu' => $content], 'rowid = :id', ['id' => $r->rowid]); } } $champs = new Champs($db->firstColumn('SELECT valeur FROM config WHERE cle = \'champs_membres\';')); $db->import(ROOT . '/include/data/1.1.0_migration.sql'); // Rename membres table $champs->createTable($champs::TABLE .'_tmp'); $fields = $champs->getCopyFields(); unset($fields['category_id']); $fields['id_categorie'] = 'category_id'; $champs->copy($champs::TABLE, $champs::TABLE . '_tmp', $fields); $db->exec(sprintf('DROP TABLE IF EXISTS %s;', $champs::TABLE)); $db->exec(sprintf('ALTER TABLE %s_tmp RENAME TO %1$s;', $champs::TABLE)); $champs->createIndexes($champs::TABLE); $db->commitSchemaUpdate(); } // Vérification de la cohérence des clés étrangères $db->foreignKeyCheck(); Utils::clearCaches(); $db->setVersion(garradin_version()); // reset last version check $db->exec('UPDATE config SET value = NULL WHERE key = \'last_version_check\';'); Static_Cache::remove('upgrade'); // Réinstaller les plugins système si nécessaire Plugin::checkAndInstallSystemPlugins(); Plugin::upgradeAllIfRequired(); ................................................................................ // Forcer à rafraîchir les données de la session si elle existe if ($user_is_logged) { $session->refresh(); } } /** * Move data from root to data/ subdirectory * (migration from 1.0 to 1.1 version) */ static public function moveDataRoot(): void { Utils::safe_mkdir(ROOT . '/data'); file_put_contents(ROOT . '/data/index.html', '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>'); rename(ROOT . '/cache', ROOT . '/data/cache'); rename(ROOT . '/plugins', ROOT . '/data/plugins'); $files = glob(ROOT . '/*.sqlite'); foreach ($files as $file) { rename($file, ROOT . '/data/' . basename($file)); } } } |
Added src/include/lib/Garradin/UserTemplate/CommonModifiers.php version [3d0edb4ff5].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
<?php namespace Garradin\UserTemplate; use Garradin\Config; use Garradin\Utils; /** * Common modifiers and functions used by Template (Smartyer) and UserTemplate */ class CommonModifiers { const MODIFIERS_LIST = [ 'money', 'money_currency', 'relative_date', 'date_short', 'date_long', 'date_hour', 'date', 'strftime', 'size_in_bytes' => [Utils::class, 'format_bytes'], 'typo', ]; const FUNCTIONS_LIST = [ 'pagination', ]; static public function money($number, bool $hide_empty = true): string { if ($hide_empty && !$number) { return ''; } return sprintf('<b class="money">%s</b>', Utils::money_format($number, ',', ' ', $hide_empty)); } static public function money_currency($number, bool $hide_empty = true): string { $out = $this->htmlMoney($number, $hide_empty); if ($out !== '') { $out .= ' ' . Config::getInstance()->get('monnaie'); } return $out; } static public function date_long($ts, bool $with_hour = false): ?string { return Utils::strftime_fr($ts, '%A %e %B %Y' . ($with_hour ? ' à %Hh%M' : '')); } static public function date_short($ts, bool $with_hour = false): ?string { return Utils::date_fr($ts, 'd/m/Y' . ($with_hour ? ' à H\hi' : '')); } static public function date_hour($ts): ?string { return Utils::date_fr($ts, 'H\hi'); } static public function strftime($ts, string $format, string $locale = 'fr'): ?string { if ($locale == 'fr') { return Utils::strftime_fr($ts, $format); } $ts = Utils::get_datetime($ts); if (!$ts) { return $ts; } return strftime($format, $ts->getTimestamp()); } static public function date($ts, string $format = null, string $locale = 'fr'): ?string { if (preg_match('/^DATE_[\w\d]+$/', $format)) { $format = constant('DateTime::' . $format); } if (null === $format) { $format = 'd/m/Y à H:i'; } if ($locale == 'fr') { return Utils::date_fr($ts, $format); } $ts = Utils::get_datetime($ts); return date($format, $ts); } static public function relative_date($ts, bool $with_hour = false): string { $day = null; if (null === $ts) { return ''; } $date = Utils::get_datetime($ts); if ($date->format('Ymd') == date('Ymd')) { $day = 'aujourd\'hui'; } elseif ($date->format('Ymd') == date('Ymd', strtotime('yesterday'))) { $day = 'hier'; } elseif ($date->format('Ymd') == date('Ymd', strtotime('tomorrow'))) { $day = 'demain'; } elseif ($date->format('Y') == date('Y')) { $day = strtolower(Utils::strftime_fr($date, '%A %e %B')); } else { $day = strtolower(Utils::strftime_fr($date, '%e %B %Y')); } if ($with_hour) { $hour = $date->format('H\hi'); return sprintf('%s, %s', $day, $hour); } return $day; } static public function typo($str, $locale = 'fr') { $str = preg_replace('/[\h]*([?!:»])(\s+|$)/u', ' \\1\\2', $str); $str = preg_replace('/(^|\s+)([«])[\h]*/u', '\\1\\2 ', $str); return $str; } static public function pagination(array $params): string { if (!isset($params['url'], $params['page'], $params['bypage'], $params['total'])) { throw new \BadFunctionCallException("Paramètre manquant pour pagination"); } if ($params['total'] == -1) return ''; $pagination = self::getGenericPagination($params['page'], $params['total'], $params['bypage']); if (empty($pagination)) return ''; $out = '<ul class="pagination">'; $encoded_url = rawurlencode('[ID]'); foreach ($pagination as &$page) { $attributes = ''; if (!empty($page['class'])) $attributes .= ' class="' . htmlspecialchars($page['class']) . '" '; $out .= '<li'.$attributes.'>'; $attributes = ''; if (!empty($page['accesskey'])) $attributes .= ' accesskey="' . htmlspecialchars($page['accesskey']) . '" '; $out .= '<a' . $attributes . ' href="' . str_replace(['[ID]', $encoded_url], htmlspecialchars($page['id']), $params['url']) . '">'; $out .= htmlspecialchars($page['label']); $out .= '</a>'; $out .= '</li>' . "\n"; } $out .= '</ul>'; return $out; } /** * Génération pagination à partir de la page courante ($current), * du nombre d'items total ($total), et du nombre d'items par page ($bypage). * $listLength représente la longueur d'items de la pagination à génerer * * @param int $current * @param int $total * @param int $bypage * @param int $listLength * @param bool $showLast Toggle l'affichage du dernier élément de la pagination * @return array|null */ static public function getGenericPagination($current, $total, $bypage, $listLength = 11, $showLast = true) { if ($total <= $bypage) return null; $total = ceil($total / $bypage); if ($total < $current) return null; $length = ($listLength / 2); $begin = $current - ceil($length); if ($begin < 1) { $begin = 1; } $end = $begin + $listLength; if($end > $total) { $begin -= ($end - $total); $end = $total; } if ($begin < 1) { $begin = 1; } if($end==($total-1)) { $end = $total; } if($begin == 2) { $begin = 1; } $out = []; if ($current > 1) { $out[] = ['id' => $current - 1, 'label' => '« ' . 'Page précédente', 'class' => 'prev', 'accesskey' => 'a']; } if ($begin > 1) { $out[] = ['id' => 1, 'label' => '1 ...', 'class' => 'first']; } for ($i = $begin; $i <= $end; $i++) { $out[] = ['id' => $i, 'label' => $i, 'class' => ($i == $current) ? 'current' : '']; } if ($showLast && $end < $total) { $out[] = ['id' => $total, 'label' => '... ' . $total, 'class' => 'last']; } if ($current < $total) { $out[] = ['id' => $current + 1, 'label' => 'Page suivante' . ' »', 'class' => 'next', 'accesskey' => 'z']; } return $out; } } |
Added src/include/lib/Garradin/UserTemplate/Functions.php version [6793282125].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 |
<?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use KD2\ErrorManager; use Garradin\Web\Skeleton; class Functions { const FUNCTIONS_LIST = [ 'include', 'http', 'dump', ]; static public function dump(array $params, Brindille $tpl) { if (!count($params)) { $params = $tpl->getAllVariables(); } $dump = htmlspecialchars(ErrorManager::dump($params)); // FIXME: only send back HTML when content-type is text/html, or send raw text return sprintf('<pre style="background: yellow; padding: 5px; overflow: auto">%s</pre>', $dump); } static public function include(array $params, UserTemplate $ut, int $line): void { if (empty($params['file'])) { throw new Brindille_Exception(sprintf('Ligne %d: argument "file" manquant pour la fonction "include"', $line)); } // Avoid recursive loops $from = $ut->get('included_from') ?? []; if (in_array($params['file'], $from)) { throw new Brindille_Exception(sprintf('Ligne %d : boucle infinie d\'inclusion détectée : %s', $line, $params['file'])); } $s = new Skeleton($params['file']); if (!$s->exists()) { throw new Brindille_Exception(sprintf('Ligne %d : fonction "include" : le fichier à inclure "%s" n\'existe pas', $line, $params['file'])); } $params['included_from'] = array_merge($from, [$params['file']]); $s->display($params); } static public function http(array $params): void { if (headers_sent()) { return; } if (isset($params['code'])) { static $codes = [ 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 449 => 'Retry With', 450 => 'Blocked by Windows Parental Controls', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 509 => 'Bandwidth Limit Exceeded', 510 => 'Not Extended', ]; if (!isset($codes[$params['code']])) { throw new Brindille_Exception('Code HTTP inconnu'); } header(sprintf('HTTP/1.1 %d %s', $params['code'], $codes[$params['code']]), true); } elseif (isset($params['redirect'])) { header('Location: ' . WWW_URL . $params['redirect'], true); } elseif (isset($params['type'])) { header('Content-Type: ' . $params['type'], true); } else { throw new Brindille_Exception('No valid parameter found for http function'); } } } |
Added src/include/lib/Garradin/UserTemplate/Modifiers.php version [f62964524d].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 |
<?php namespace Garradin\UserTemplate; use Garradin\Utils; class Modifiers { const PHP_MODIFIERS_LIST = [ 'strtolower', 'strtoupper', 'ucfirst', 'ucwords', 'htmlentities', 'htmlspecialchars', 'trim', 'ltrim', 'rtrim', 'lcfirst', 'md5', 'sha1', 'metaphone', 'nl2br', 'soundex', 'str_split', 'str_word_count', 'strrev', 'strlen', 'wordwrap', 'strip_tags', 'strlen', ]; const MODIFIERS_LIST = [ 'truncate', 'excerpt', 'protect_contact', 'atom_date', 'xml_escape', 'replace', 'regexp_replace', ]; static public function replace($str, $find, $replace): string { return str_replace($find, $replace, $str); } static public function regexp_replace($str, $pattern, $replace) { return preg_replace($pattern, $replace, $str); } /** * UTF-8 aware intelligent substr * @param string $str UTF-8 string * @param integer $length Maximum string length * @param string $placeholder Placeholder text to append at the string if it has been cut * @param boolean $strict_cut If true then will cut in the middle of words * @return string String cut to $length or shorter * @example |truncate:10:" (click to read more)":true */ static public function truncate($str, $length = 80, $placeholder = '…', $strict_cut = false): string { // Don't try to use unicode if the string is not valid UTF-8 $u = preg_match('//u', $str) ? 'u' : ''; // Shorter than $length + 1 if (!preg_match('/^.{' . ((int)$length + 1) . '}/s' . $u, $str)) { return $str; } // Cut at 80 characters $str = preg_replace('/^(.{0,' . (int)$length . '}).*$/s' . $u, '$1', $str); if (!$strict_cut) { $cut = preg_replace('/[^\s.,:;!?]*?$/s' . $u, '', $str); if (trim($cut) == '') { $cut = $str; } $str = $cut; } return trim($str) . $placeholder; } static public function excerpt($str, $length = 600): string { $str = strip_tags($str); $str = self::truncate($str, $length); $str = preg_replace("/\n{2,}/", '</p><p>', $str); return '<p>' . $str . '</p>'; } static public function protect_contact(string $contact): string { if (!trim($contact)) return ''; if (strpos($contact, '@')) { $reversed = strrev($contact); // https://unicode-table.com/en/FF20/ $reversed = strtr($reversed, ['@' => '@']); return sprintf('<a href="#error" onclick="this.href = (this.innerText + \':otliam\').split(\'\').reverse().join(\'\').replace(/@/, \'@\');"><span style="unicode-bidi:bidi-override;direction: rtl;">%s</span></a>', htmlspecialchars($reversed)); } else { return '<a href="'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'">'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'</a>'; } } static public function atom_date($date) { return Utils::date_fr(DATE_ATOM, $date); } static public function xml_escape($str) { return htmlspecialchars($str, ENT_XML1); } } |
Added src/include/lib/Garradin/UserTemplate/Sections.php version [eddf71a8f9].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
<?php namespace Garradin\UserTemplate; use KD2\Brindille_Exception; use Garradin\DB; use Garradin\Entities\Web\Page; use Garradin\Web\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; class Sections { const SECTIONS_LIST = [ 'categories', 'articles', 'pages', 'images', 'documents', 'files', 'sql', ]; static protected $_cache = []; static protected function cache(string $id, callable $callback) { if (!array_key_exists($id, self::$_cache)) { self::$_cache[$id] = $callback(); } return self::$_cache[$id]; } static public function categories(array $params, UserTemplate $tpl, int $line): \Generator { if (!array_key_exists('where', $params)) { $params['where'] = ''; } $params['where'] .= ' AND w.type = ' . Page::TYPE_CATEGORY; return self::pages($params, $tpl, $line); } static public function articles(array $params, UserTemplate $tpl, int $line): \Generator { if (!array_key_exists('where', $params)) { $params['where'] = ''; } $params['where'] .= ' AND w.type = ' . Page::TYPE_PAGE; return self::pages($params, $tpl, $line); } static public function pages(array $params, UserTemplate $tpl, int $line): \Generator { if (!array_key_exists('where', $params)) { $params['where'] = ''; } $params['select'] = 'w.*'; $params['tables'] = 'web_pages w'; $params['where'] .= ' AND status = :status'; $params[':status'] = Page::STATUS_ONLINE; if (isset($params['search'])) { $params['tables'] .= ' INNER JOIN files_search s USING (path)'; $params['select'] .= ', rank(matchinfo(s), 0, 1.0, 1.0) AS points'; $params['where'] .= ' AND s MATCH :search'; if (!isset($params['order'])) { $params['order'] = 'points DESC'; } $params[':search'] = $params['search']; unset($params['search']); } if (isset($params['uri'])) { $params['where'] .= ' AND w.uri = :uri'; $params['limit'] = 1; $params[':uri'] = $params['uri']; unset($params['uri']); } if (array_key_exists('parent', $params)) { if (null === $params['parent']) { $params['where'] .= ' AND w.parent IS NULL'; } else { $params['where'] .= ' AND w.parent = :parent'; $params[':parent'] = $params['parent']; } unset($params['parent']); } if (isset($params['future'])) { if (!$params['future']) { $params['where'] .= ' AND w.published <= datetime()'; } unset($params['future']); } //var_dump($params); exit; foreach (self::sql($params, $tpl, $line) as $row) { $data = $row; unset($data['points']); $page = new Page; $page->load($data); $page->exists(true); $row = array_merge($row, $page->asTemplateArray()); yield $row; } } static public function images(array $params, UserTemplate $tpl, int $line): \Generator { if (!array_key_exists('where', $params)) { $params['where'] = ''; } $params['where'] .= ' AND f.image = 1'; return self::files($params, $tpl, $line); } static public function documents(array $params, UserTemplate $tpl, int $line): \Generator { if (!array_key_exists('where', $params)) { $params['where'] = ''; } $params['where'] .= ' AND f.image = 0'; return self::files($params, $tpl, $line); } static public function files(array $params, UserTemplate $tpl, int $line): \Generator { if (!array_key_exists('where', $params)) { $params['where'] = ''; } if (empty($params['parent'])) { throw new Brindille_Exception('La section "files" doit obligatoirement comporter un paramètre "parent"'); } $parent = $params['parent']; // Fetch page $page = self::cache('page_' . md5($parent), function () use ($parent) { return Web::getByURI($parent); }); if (!$page) { return; } $params['select'] = 'f.*'; $params['tables'] = 'files f'; $params['where'] .= ' AND f.path = :path AND f.name != :p_name AND f.type = ' . File::TYPE_FILE; $params[':path'] = dirname($page->filepath()); $params[':p_name'] = basename($page->filepath()); unset($params['parent']); // Generate a temporary table containing the list of files included in the text if (!empty($params['except_in_text'])) { // Don't regenerate that table for each section called in the page, // we assume the content and list of files will not change between sections self::cache('page_files_text_' . $parent, function () use ($page) { $db = DB::getInstance(); $db->begin(); // Put files mentioned in the text in a temporary table $db->exec('CREATE TEMP TABLE IF NOT EXISTS files_tmp_in_text (page_id, name);'); foreach (Page::findTaggedAttachments($page->content) as $name) { $db->insert('files_tmp_in_text', ['page_id' => $page->id(), 'name' => $name]); } $db->commit(); }); $params['where'] .= sprintf(' AND f.name NOT IN (SELECT name FROM files_tmp_in_text WHERE page_id = %d)', $page->id()); } if (empty($params['order'])) { $params['order'] = 'name'; } if ($params['order'] == 'name') { $params['order'] .= ' COLLATE NOCASE'; } foreach (self::sql($params, $tpl, $line) as $row) { $file = new File; $file->load($row); $row['url'] = $file->url(); $row['download_url'] = $file->url(true); $row['thumb_url'] = $file->thumb_url(); $row['small_url'] = $file->thumb_url(File::THUMB_SIZE_SMALL); yield $row; } } static public function sql(array $params, UserTemplate $tpl, int $line): \Generator { static $defaults = [ 'select' => '*', 'order' => '1', 'begin' => 0, 'limit' => 1000, 'where' => '', ]; if (!isset($params['tables'])) { throw new Brindille_Exception('Missing parameter "tables"'); } foreach ($defaults as $key => $default_value) { if (!isset($params[$key])) { $params[$key] = $default_value; } } // Allow for count=true, count=1 and also count="DISTINCT user_id" count="id" if (isset($params['count'])) { $params['select'] = sprintf('COUNT(%s) AS count', $params['count'] == 1 ? '*' : $params['count']); } $sql = sprintf('SELECT %s FROM %s WHERE 1 %s %s ORDER BY %s LIMIT %d,%d;', $params['select'], $params['tables'], $params['where'] ?? '', isset($params['group']) ? 'GROUP BY ' . $params['group'] : '', $params['order'], $params['begin'], $params['limit'] ); try { $db = DB::getInstance(); $statement = $db->protectSelect(null, $sql); $args = []; foreach ($params as $key => $value) { if (substr($key, 0, 1) == ':') { $args[$key] = $value; } } foreach ($args as $key => $value) { $statement->bindValue($key, $value, $db->getArgType($value)); } if (!empty($params['debug'])) { echo sprintf('<pre style="padding: 5px; background: yellow;">%s</pre>', htmlspecialchars($statement->getSQL(true))); } unset($params, $sql); $result = $statement->execute(); } catch (\Exception $e) { throw new Brindille_Exception(sprintf("Erreur SQL à la ligne %d : %s\nRequête exécutée : %s", $line, $db->lastErrorMsg(), $sql)); } while ($row = $result->fetchArray(\SQLITE3_ASSOC)) { yield $row; } } } |
Added src/include/lib/Garradin/UserTemplate/UserTemplate.php version [c6276879b4].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 163 164 165 166 167 168 169 |
<?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use Garradin\Config; use Garradin\Plugin; use Garradin\Utils; use Garradin\Web\Skeleton; use Garradin\Entities\Files\File; use Garradin\UserTemplate\Modifiers; use Garradin\UserTemplate\Functions; use Garradin\UserTemplate\Sections; use const Garradin\{WWW_URL, ADMIN_URL, CACHE_ROOT, DATA_ROOT}; class UserTemplate extends Brindille { protected $path; protected $modified; protected $file; static protected $root_variables; static public function getRootVariables() { if (null !== self::$root_variables) { return self::$root_variables; } static $keys = ['adresse_asso', 'champ_identifiant', 'champ_identite', 'couleur1', 'couleur2', 'email_asso', 'monnaie', 'nom_asso', 'pays', 'site_asso', 'telephone_asso']; if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { if (function_exists('locale_accept_from_http')) { $lang = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']); } else { $lang = preg_replace('/[^a-z]/i', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']); $lang = strtolower(substr($lang, 0, 2)); } $lang = strtolower(substr($lang, 0, 2)); } else { $lang = ''; } $config = Config::getInstance(); $config = array_intersect_key($config->asArray(), array_flip($keys)); self::$root_variables = [ 'root_url' => WWW_URL, 'admin_url' => ADMIN_URL, '_GET' => &$_GET, '_POST' => &$_POST, 'visitor_lang' => $lang, 'config' => $config, ]; return self::$root_variables; } public function __construct(?File $file = null) { if ($file) { $this->file = $file; $this->modified = $file->modified->getTimestamp(); } $this->assignArray(self::getRootVariables()); $this->registerAll(); Plugin::fireSignal('usertemplate.init', ['template' => $this]); } public function registerAll() { // Register default Brindille modifiers $this->registerDefaults(); // Common modifiers foreach (CommonModifiers::MODIFIERS_LIST as $key => $name) { $this->registerModifier(is_int($key) ? $name : $key, is_int($key) ? [CommonModifiers::class, $name] : $name); } foreach (CommonModifiers::FUNCTIONS_LIST as $key => $name) { $this->registerFunction(is_int($key) ? $name : $key, is_int($key) ? [CommonModifiers::class, $name] : $name); } // PHP modifiers foreach (Modifiers::PHP_MODIFIERS_LIST as $name) { $this->registerModifier($name, $name); } // Local modifiers foreach (Modifiers::MODIFIERS_LIST as $name) { $this->registerModifier($name, [Modifiers::class, $name]); } // Local functions foreach (Functions::FUNCTIONS_LIST as $name) { $this->registerFunction($name, [Functions::class, $name]); } // Local sections foreach (Sections::SECTIONS_LIST as $name) { $this->registerSection($name, [Sections::class, $name]); } } public function setSource(string $path) { $this->path = $path; $this->modified = filemtime($path); } public function display(): void { $compiled_path = CACHE_ROOT . '/compiled/s_' . sha1($this->file ? $this->file->path() : $this->path) . '.php'; if (file_exists($compiled_path) && filemtime($compiled_path) >= $this->modified) { require $compiled_path; return; } $tmp_path = $compiled_path . '.tmp'; $source = $this->file ? $this->file->fetch() : file_get_contents($this->path); try { $code = $this->compile($source); file_put_contents($tmp_path, $code); require $tmp_path; } catch (Brindille_Exception $e) { throw new Brindille_Exception(sprintf("Erreur de syntaxe dans '%s' : %s", $this->file ? $this->file->name : basename($this->path), $e->getMessage()), 0, $e); } catch (\Throwable $e) { // Don't delete temporary file as it can be used to debug throw $e; } if (!file_exists(dirname($compiled_path))) { Utils::safe_mkdir(dirname($compiled_path), 0777, true); } rename($tmp_path, $compiled_path); } public function fetch(): string { ob_start(); $this->display(); return ob_get_clean(); } } |
Added src/include/lib/Garradin/Users/Categories.php version [3a8d8a43a9].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 |
<?php namespace Garradin\Users; use Garradin\DB; use Garradin\Entities\Users\Category; use KD2\DB\EntityManager as EM; class Categories { static public function get(int $id): ?Category { return EM::findOneById(Category::class, $id); } static public function listSimple(): array { return DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s ORDER BY name COLLATE NOCASE;', Category::TABLE)); } static public function listWithStats(): array { return DB::getInstance()->getGrouped(sprintf('SELECT c.id, c.*, (SELECT COUNT(*) FROM membres WHERE category_id = c.id) AS count FROM %s c ORDER BY c.name COLLATE NOCASE;', Category::TABLE)); } static public function listHidden(): array { return DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s WHERE hidden = 1 ORDER BY name COLLATE NOCASE;', Category::TABLE)); } static public function listNotHidden(): array { return DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s WHERE hidden = 0 ORDER BY name COLLATE NOCASE;', Category::TABLE)); } } |
Modified src/include/lib/Garradin/Utils.php from [9771f61ae9] to [5af259e567].
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ... 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 ... 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 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 ... 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 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 418 419 420 421 422 ... 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 ... 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 ... 914 915 916 917 918 919 920 921 |
class Utils { const EMAIL_CONTEXT_BULK = 'bulk'; const EMAIL_CONTEXT_PRIVATE = 'private'; const EMAIL_CONTEXT_SYSTEM = 'system'; static protected $skriv = null; const FRENCH_DATE_NAMES = [ 'January'=>'Janvier', 'February'=>'Février', 'March'=>'Mars', 'April'=>'Avril', 'May'=>'Mai', 'June'=>'Juin', 'July'=>'Juillet', 'August'=>'Août', 'September'=>'Septembre', 'October'=>'Octobre', 'November'=>'Novembre', 'December'=>'Décembre', 'Monday'=>'Lundi', 'Tuesday'=>'Mardi', 'Wednesday'=>'Mercredi', 'Thursday'=>'Jeudi','Friday'=>'Vendredi','Saturday'=>'Samedi','Sunday'=>'Dimanche', 'Feb'=>'Fév','Apr'=>'Avr','Jun'=>'Juin', 'Jul'=>'Juil','Aug'=>'Aout','Dec'=>'Déc', 'Mon'=>'Lun','Tue'=>'Mar','Wed'=>'Mer','Thu'=>'Jeu','Fri'=>'Ven','Sat'=>'Sam','Sun'=>'Dim']; ................................................................................ } static public function getModifiedURL(string $new) { return HTTP::mergeURLs(self::getSelfURL(), $new); } public static function redirect($destination=false, $exit=true) { $destination = self::getLocalURL($destination); if (PHP_SAPI == 'cli') { echo 'Please visit ' . $destination . PHP_EOL; exit; } ................................................................................ if (!isset($list[$code])) return false; return $list[$code]; } /** * Génération pagination à partir de la page courante ($current), * du nombre d'items total ($total), et du nombre d'items par page ($bypage). * $listLength représente la longueur d'items de la pagination à génerer * * @param int $current * @param int $total * @param int $bypage * @param int $listLength * @param bool $showLast Toggle l'affichage du dernier élément de la pagination * @return array|null */ public static function getGenericPagination($current, $total, $bypage, $listLength=11, $showLast = true) { if ($total <= $bypage) return null; $total = ceil($total / $bypage); if ($total < $current) return null; $length = ($listLength / 2); $begin = $current - ceil($length); if ($begin < 1) { $begin = 1; } $end = $begin + $listLength; if($end > $total) { $begin -= ($end - $total); $end = $total; } if ($begin < 1) { $begin = 1; } if($end==($total-1)) { $end = $total; } if($begin == 2) { $begin = 1; } $out = []; if ($current > 1) { $out[] = ['id' => $current - 1, 'label' => '« ' . 'Page précédente', 'class' => 'prev', 'accesskey' => 'a']; } if ($begin > 1) { $out[] = ['id' => 1, 'label' => '1 ...', 'class' => 'first']; } for ($i = $begin; $i <= $end; $i++) { $out[] = ['id' => $i, 'label' => $i, 'class' => ($i == $current) ? 'current' : '']; } if ($showLast && $end < $total) { $out[] = ['id' => $total, 'label' => '... ' . $total, 'class' => 'last']; } if ($current < $total) { $out[] = ['id' => $current + 1, 'label' => 'Page suivante' . ' »', 'class' => 'next', 'accesskey' => 'z']; } return $out; } static public function transliterateToAscii($str, $charset='UTF-8') { // Don't process empty strings if (!trim($str)) return $str; // We only process non-ascii strings ................................................................................ $str = preg_replace('#&[^;]+;#', '', $str); // supprime les autres caractères $str = preg_replace('![^[:ascii:]]+!', '', $str); return $str; } /** * Transforme un texte SkrivML en HTML * @param string $str Texte SkrivML * @return string Texte HTML */ static public function SkrivToHTML($str) { if (!self::$skriv) { self::$skriv = new \KD2\SkrivLite; self::$skriv->registerExtension('fichier', ['\\Garradin\\Fichiers', 'SkrivFichier']); self::$skriv->registerExtension('image', ['\\Garradin\\Fichiers', 'SkrivImage']); // Enregistrer d'autres extensions éventuellement Plugin::fireSignal('skriv.init', ['skriv' => self::$skriv]); } $skriv =& self::$skriv; $str = preg_replace_callback('/(fichier|image):\/\/(\d+)/', function ($match) use ($skriv) { try { $file = new Fichiers((int)$match[2]); } catch (\InvalidArgumentException $e) { return $skriv->parseError('/!\ Lien fichier : ' . $e->getMessage()); } return $file->getURL(); }, $str); $str = self::$skriv->render($str); return $str; } /** * Transforme les tags de base SPIP en tags SkrivML * @param string $str Texte d'entrée * @return string Texte transformé */ static public function SpipToSkriv($str) { $str = preg_replace('/(?<!\\\\)\{{3}(\V*)\}{3}/', '=== $1 ===', $str); $str = preg_replace('/(?<!\\\\)\{{2}(\V*)\}{2}/', '**$1**', $str); $str = preg_replace('/(?<!\\\\)\{(\V*)\}/', '\'\'$1\'\'', $str); $str = preg_replace('/(?<!\\\\)\[(.+?)->(.+?)\]/', '[[$1 | $2]]', $str); $str = preg_replace('/(?<!\[)\[([^\[\]]+?)\]/', '[[$1]]', $str); return $str; } /** * Transforme les tags HTML basiques en tags SkrivML * @param string $str Texte d'entrée * @return string Texte transformé */ static public function HTMLToSkriv($str) { ................................................................................ return number_format(round($size / 1024 / 1024, 2), 2, ',', '') . ' Mo'; elseif ($size > 1024) return round($size / 1024) . ' Ko'; else return $size . ' o'; } static public function deleteRecursive($path) { if (!file_exists($path)) return false; $dir = dir($path); if (!$dir) return false; ................................................................................ } $h /= 6; } return array($h * 360, $s, $v); } static public function HTTPCache(string $hash, int $last_change): bool { $etag = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : null; $last_modified = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : null; if ($etag === $hash && $last_modified >= $last_change) { header('HTTP/1.1 304 Not Modified', true, 304); exit; } header(sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $last_change))); header(sprintf('Etag: %s', $hash)); header('Cache-Control: private'); return false; } static public function getLatestVersion(): ?string { ................................................................................ } $config->set('last_version_check', json_encode($last)); $config->save(); return $last->version; } } |
< < > > > > > > > > > > > > > > > > > > > > > | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | > > | > > | > > > > > > > > > > > |
10 11 12 13 14 15 16 17 18 19 20 21 22 23 ... 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 ... 284 285 286 287 288 289 290 291 292 293 294 295 296 297 ... 305 306 307 308 309 310 311 312 313 314 315 316 317 318 ... 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 ... 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 ... 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 |
class Utils { const EMAIL_CONTEXT_BULK = 'bulk'; const EMAIL_CONTEXT_PRIVATE = 'private'; const EMAIL_CONTEXT_SYSTEM = 'system'; const FRENCH_DATE_NAMES = [ 'January'=>'Janvier', 'February'=>'Février', 'March'=>'Mars', 'April'=>'Avril', 'May'=>'Mai', 'June'=>'Juin', 'July'=>'Juillet', 'August'=>'Août', 'September'=>'Septembre', 'October'=>'Octobre', 'November'=>'Novembre', 'December'=>'Décembre', 'Monday'=>'Lundi', 'Tuesday'=>'Mardi', 'Wednesday'=>'Mercredi', 'Thursday'=>'Jeudi','Friday'=>'Vendredi','Saturday'=>'Samedi','Sunday'=>'Dimanche', 'Feb'=>'Fév','Apr'=>'Avr','Jun'=>'Juin', 'Jul'=>'Juil','Aug'=>'Aout','Dec'=>'Déc', 'Mon'=>'Lun','Tue'=>'Mar','Wed'=>'Mer','Thu'=>'Jeu','Fri'=>'Ven','Sat'=>'Sam','Sun'=>'Dim']; ................................................................................ } static public function getModifiedURL(string $new) { return HTTP::mergeURLs(self::getSelfURL(), $new); } static public function reloadParentFrame(string $destination): void { $destination = self::getLocalURL($destination); echo ' <!DOCTYPE html> <html> <head> <script type="text/javascript"> window.parent.location.reload(); </script> </head> <body> <p><a href="' . htmlspecialchars($destination) . '">Cliquer ici pour continuer</a> </body> </html>'; exit; } public static function redirect($destination = '', $exit=true) { $destination = self::getLocalURL($destination); if (PHP_SAPI == 'cli') { echo 'Please visit ' . $destination . PHP_EOL; exit; } ................................................................................ if (!isset($list[$code])) return false; return $list[$code]; } static public function transliterateToAscii($str, $charset='UTF-8') { // Don't process empty strings if (!trim($str)) return $str; // We only process non-ascii strings ................................................................................ $str = preg_replace('#&[^;]+;#', '', $str); // supprime les autres caractères $str = preg_replace('![^[:ascii:]]+!', '', $str); return $str; } /** * Transforme les tags HTML basiques en tags SkrivML * @param string $str Texte d'entrée * @return string Texte transformé */ static public function HTMLToSkriv($str) { ................................................................................ return number_format(round($size / 1024 / 1024, 2), 2, ',', '') . ' Mo'; elseif ($size > 1024) return round($size / 1024) . ' Ko'; else return $size . ' o'; } static public function deleteRecursive(string $path): bool { if (!file_exists($path)) return false; $dir = dir($path); if (!$dir) return false; ................................................................................ } $h /= 6; } return array($h * 360, $s, $v); } static public function HTTPCache(?string $hash, int $last_change): bool { $etag = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : null; $last_modified = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : null; if ($etag === $hash && $last_modified >= $last_change) { header('HTTP/1.1 304 Not Modified', true, 304); exit; } header(sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $last_change))); if ($etag) { header(sprintf('Etag: %s', $hash)); } header('Cache-Control: private'); return false; } static public function getLatestVersion(): ?string { ................................................................................ } $config->set('last_version_check', json_encode($last)); $config->save(); return $last->version; } static public function transformTitleToURI($str) { $str = Utils::transliterateToAscii($str); $str = preg_replace('![^\w\d_-]!i', '-', $str); $str = preg_replace('!-{2,}!', '-', $str); $str = trim($str, '-'); return $str; } } |
Added src/include/lib/Garradin/Web/Render/EncryptedSkriv.php version [8b803e3b49].
> > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; use Garradin\Template; use const Garradin\ADMIN_URL; class EncryptedSkriv { static public function render(File $file, ?string $content = null): string { $tpl = Template::getInstance(); $content = $content ?? $file->fetch(); $tpl->assign('admin_url', ADMIN_URL); $tpl->assign(compact('file', 'content')); return $tpl->fetch('common/files/_file_render_encrypted.tpl'); } } |
Added src/include/lib/Garradin/Web/Render/HTML.php version [e2b502a15f].
> > > > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php namespace Garradin\Files\Render; use Garradin\Entities\Files\File; use KD2\Garbage2xhtml; class HTML { static protected $g2x; static public function render(File $file, ?string $content = null): string { if (null === self::$g2x) { $g2x = self::$g2x = new Garbage2xhtml; $g2x->auto_br = false; } return self::$g2x->process($content ?? $file->fetch()); } } |
Added src/include/lib/Garradin/Web/Render/Skriv.php version [0829b3ce29].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 |
<?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; use Garradin\Squelette_Filtres; use Garradin\Plugin; use Garradin\Utils; use Garradin\Files\Files; use Garradin\UserTemplate\CommonModifiers; use KD2\SkrivLite; use const Garradin\WWW_URL; class Skriv { static protected $skriv; static public function render(File $file, ?string $content = null, array $options = []): string { if (!isset($options['prefix'])) { $options['prefix'] = WWW_URL; } if (!self::$skriv) { self::$skriv = new \KD2\SkrivLite; self::$skriv->registerExtension('file', [self::class, 'SkrivFile']); self::$skriv->registerExtension('image', [self::class, 'SkrivImage']); // Enregistrer d'autres extensions éventuellement Plugin::fireSignal('skriv.init', ['skriv' => self::$skriv]); } $skriv =& self::$skriv; $skriv->_currentPath = str_replace(File::CONTEXT_WEB . '/', '', $file->path); $str = $content ?? $file->fetch(); $str = preg_replace_callback('/#file:\[([^\]\h]+)\]/', function ($match) use ($skriv) { return WWW_URL . $skriv->_currentPath . '/' . $match[1]; }, $str); $str = self::$skriv->render($str); $str = CommonModifiers::typo($str); $str = preg_replace_callback('!<a href="/(.+)">!i', function ($matches) use ($options) { return sprintf('<a href="%s">', $options['prefix'], Utils::transformTitleToURI($matches[1])); }, $str); return sprintf('<div class="web-content">%s</div>', $str); } /** * Callback utilisé pour l'extension <<file>> dans le wiki-texte * @param array $args Arguments passés à l'extension * @param string $content Contenu éventuel (en mode bloc) * @param object $skriv Objet SkrivLite */ static public function SkrivFile(array $args, ?string $content, SkrivLite $skriv): string { $name = $args[0] ?? null; $caption = $args[1] ?? null; if (!$name || !$skriv->_currentPath) { return $skriv->parseError('/!\ Tag file : aucun nom de fichier indiqué.'); } if (empty($caption)) { $caption = $name; } $url = WWW_URL . $skriv->_currentPath . '/' . $name; $ext = substr($name, strrpos($name, '.')+1); return sprintf( '<aside class="file" data-type="%s"><a href="%s" class="internal-file">%s</a> <small>(%s)</small></aside>', htmlspecialchars($ext), htmlspecialchars($url), htmlspecialchars($caption), htmlspecialchars(strtoupper($ext)) ); } /** * Callback utilisé pour l'extension <<image>> dans le wiki-texte * @param array $args Arguments passés à l'extension * @param string $content Contenu éventuel (en mode bloc) * @param object $skriv Objet SkrivLite */ static public function SkrivImage(array $args, ?string $content, SkrivLite $skriv): string { static $align_values = ['left', 'right', 'center']; $name = $args[0] ?? null; $align = $args[1] ?? null; $caption = $args[2] ?? null; if (!$name || !$skriv->_currentPath) { return $skriv->parseError('/!\ Tag image : aucun nom de fichier indiqué.'); } $url = WWW_URL . $skriv->_currentPath . '/' . $name; $thumb_url = sprintf('%s?%dpx', $url, $align == 'center' ? 500 : 200); $out = sprintf('<a href="%s" class="internal-image" target="_image"><img src="%s" alt="%s" loading="lazy" /></a>', htmlspecialchars($url), htmlspecialchars($thumb_url), htmlspecialchars($caption ?? $name) ); if (!empty($align)) { if ($caption) { $caption = sprintf('<figcaption>%s</figcaption>', htmlspecialchars($caption)); } $out = sprintf('<figure class="image img-%s">%s%s</figure>', $align, $out, $caption); } return $out; } } |
Added src/include/lib/Garradin/Web/Skeleton.php version [376165e7c2].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
<?php namespace Garradin\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\UserException; use Garradin\UserTemplate\UserTemplate; use KD2\Brindille_Exception; use KD2\DB\EntityManager as EM; use const Garradin\ROOT; class Skeleton { const TEMPLATE_TYPES = '!^(?:text/(?:html|plain)|\w+/(?:\w+\+)?xml)$!'; protected $name; protected $file; public function __construct(string $tpl) { if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $tpl)) { throw new \InvalidArgumentException('Invalid skeleton name'); } $this->file = Files::get(File::CONTEXT_SKELETON, $tpl); $this->name = $tpl; } public function defaultPath(): ?string { $path = ROOT . '/www/skel-dist/' . $this->name; if (file_exists($path)) { return $path; } return null; } public function serve(array $params = []): bool { header('Content-Type: text/html;charset=utf-8', true); if (!$this->exists()) { header('HTTP/1.1 404 Not Found', true); $tpl = new self('404.html'); if (!$tpl->serve()) { throw new UserException('Cette page n\'existe pas.'); } } if (preg_match(self::TEMPLATE_TYPES, $this->type())) { $ut = new UserTemplate($this->file); if (!$this->file) { $ut->setSource($this->defaultPath()); } header(sprintf('Content-Type: %s;charset=utf-8', $this->type())); try { $ut->assignArray($params); $ut->display(); } catch (Brindille_Exception $e) { printf('<div style="border: 5px solid orange; padding: 10px; background: yellow;"><h2>Erreur dans le squelette</h2><p>%s</p></div>', nl2br(htmlspecialchars($e->getMessage()))); } } elseif ($this->file) { $this->file->serve(); } else { header(sprintf('Content-Type: %s;charset=utf-8', $this->type())); readfile($this->defaultPath()); } return true; } public function fetch(array $params = []): string { if (!$this->exists()) { return ''; } if (preg_match(self::TEMPLATE_TYPES, $this->type())) { $ut = new UserTemplate($this->file); if (!$this->file) { $ut->setSource($this->defaultPath()); } $ut->assignArray($params); return $ut->fetch(); } elseif ($this->file) { $this->file->fetch(); } else { return file_get_contents($this->defaultPath()); } } public function display(array $params = []): void { if (!$this->exists()) { return; } if (preg_match(self::TEMPLATE_TYPES, $this->type())) { $ut = new UserTemplate($this->file); if (!$this->file) { $ut->setSource($this->defaultPath()); } $ut->assignArray($params); $ut->display(); } elseif ($this->file) { $this->file->display(); } else { readfile($this->defaultPath()); } } public function exists() { return $this->file ? true : ($this->defaultPath() ? true : false); } public function raw(): string { return $this->file ? $this->file->fetch() : file_get_contents($this->defaultPath()); } public function edit(string $content) { File::createAndStore(File::CONTEXT_SKELETON, $this->name, null, $content); } public function type(): string { $name = $this->file->name ?? $this->defaultPath(); $ext = substr($name, strrpos($name, '.')+1); if ($ext == 'css') { return 'text/css'; } elseif ($ext == 'html') { return 'text/html'; } elseif ($ext == 'js') { return 'text/javascript'; } if ($this->file) { return $this->file->type; } $finfo = \finfo_open(\FILEINFO_MIME_TYPE); return finfo_file($finfo, $this->defaultPath()); } public function reset() { if ($this->file) { $this->file->delete(); } } static public function resetSelected(array $selected) { foreach ($selected as $file) { $f = new self($file); $f->reset(); } } static public function list(): array { $sources = []; $dir = dir(ROOT . '/www/skel-dist/'); while ($file = $dir->read()) { if ($file[0] == '.') continue; $sources[$file] = null; } $dir->close(); $list = Files::list(File::CONTEXT_SKELETON); foreach ($list as $file) { $sources[$file->name] = $file; } ksort($sources); return $sources; } static public function upload(string $name, ?string $file): void { if (empty($_FILES[$file]['tmp_name'])) { File::createAndStore($name, File::CONTEXT_SKELETON, null, null, 'À modifier…'); } else { $f = File::upload($file, File::CONTEXT_SKELETON, null); if ($f->name != $name) { $f->importForm(['name' => $name]); $f->save(); } } } } |
Added src/include/lib/Garradin/Web/Web.php version [46ab4edbd5].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
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 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 |
<?php namespace Garradin\Web; use Garradin\Entities\Web\Page; use Garradin\Web\Skeleton; use Garradin\Files\Files; use Garradin\Config; use Garradin\Utils; use Garradin\UserException; use Garradin\Membres\Session; use KD2\DB\EntityManager as EM; use const Garradin\{WWW_URI, ADMIN_URL}; class Web { static public function search(string $search, bool $online_only = true): array { if (strlen($search) > 100) { throw new UserException('Recherche trop longue : maximum 100 caractères'); } $where = ''; if ($online_only) { $where = sprintf('p.status = %d AND ', Page::STATUS_ONLINE); } $query = sprintf('SELECT p.*, snippet(files_search, \'<b>\', \'</b>\', \'...\', -1, -50) AS snippet, rank(matchinfo(files_search), 0, 1.0, 1.0) AS points FROM files_search AS s INNER JOIN web_pages AS p USING (id) WHERE %s files_search MATCH ? ORDER BY points DESC LIMIT 0,50;', $where); return DB::getInstance()->get($query, $search); } static public function listCategories(?Page $parent): array { $params = []; if (null === $parent) { $where = 'parent IS NULL'; } else { $where = 'parent = ?'; $params[] = $parent->path(); } $sql = sprintf('SELECT * FROM @TABLE WHERE %s AND type = %d ORDER BY title COLLATE NOCASE;', $where, Page::TYPE_CATEGORY); return EM::getInstance(Page::class)->all($sql, ...$params); } static public function listPages(?Page $parent, bool $order_by_date = true): array { $params = []; if (null === $parent) { $where = 'parent IS NULL'; } else { $where = 'parent = ?'; $params[] = $parent->path(); } $order = $order_by_date ? 'published DESC' : 'title COLLATE NOCASE'; $sql = sprintf('SELECT * FROM @TABLE WHERE %s AND type = %d ORDER BY %s;', $where, Page::TYPE_PAGE, $order); return EM::getInstance(Page::class)->all($sql, ...$params); } static public function listCategoriesTree(?int $current = null): array { $db = DB::getInstance(); $flat = $db->get('SELECT id, parent_id, title FROM web_pages ORDER BY title COLLATE NOCASE;'); $parents = []; foreach ($flat as $node) { if (!isset($parents[$node->parent_id])) { $parents[$node->parent_id] = []; } $parents[(int) $node->parent_id][] = $node; } $build_tree = function (int $parent_id, int $level) use ($build_tree, $parents): array { $nodes = []; if (!isset($parents[$parent_id])) { return $nodes; } foreach ($parents[$parent_id] as $node) { $node->level = $level; $node->children = $build_tree($node->id, $level + 1); $nodes[] = $node; } return $nodes; }; return $build_tree(0, 0); } static public function getByURI(string $uri): ?Page { return EM::findOne(Page::class, 'SELECT * FROM @TABLE WHERE path = ?;', $uri); } static public function get(int $id): ?Page { return EM::findOneById(Page::class, $id); } static public function dispatchURI() { $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; if ($pos = strpos($uri, '?')) { $uri = substr($uri, 0, $pos); } else { // WWW_URI inclus toujours le slash final, mais on veut le conserver ici $uri = substr($uri, strlen(WWW_URI) - 1); } if (Config::getInstance()->get('desactiver_site')) { Utils::redirect(ADMIN_URL); } header('HTTP/1.1 200 OK', 200, true); $skel = null; $page = null; $uri = substr($uri, 1); if ($uri == '') { $skel = 'index.html'; } // Redirect old URLs (pre-1.1) elseif ($uri == 'feed/atom/') { Utils::redirect('/atom.xml'); } elseif (substr($uri, 0, 6) === 'admin/') { http_response_code(404); throw new UserException('Cette page n\'existe pas.'); } elseif ($page = self::getByURI($uri, 1)) { $skel = $page->template(); $page = $page->asTemplateArray(); } elseif ($file = Files::getFromURI($uri)) { $size = null; foreach ($_GET as $key => $value) { if (substr($key, -2) == 'px') { $size = (int)substr($key, 0, -2); break; } } $session = Session::getInstance(); if ($size) { $file->serveThumbnail($session, $size); } else { $file->serve($session, isset($_GET['download']) ? true : false); } return; } else { // Trying to see if a custom template with this name exists if (preg_match('!^[\w\d_.-]+$!i', $uri)) { $s = new Skeleton($uri); if ($s->exists()) { $s->serve(); return; } } $skel = '404.html'; } $s = new Skeleton($skel); $s->serve(compact('uri', 'page', 'skel')); } } |
Deleted src/include/lib/Garradin/Wiki.php version [6ab9be5a8f].
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 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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 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 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 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 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 |
<?php namespace Garradin; class Wiki { const LECTURE_PUBLIC = -1; const LECTURE_NORMAL = 0; const LECTURE_CATEGORIE = 1; const ECRITURE_NORMAL = 0; const ECRITURE_CATEGORIE = 1; const ITEMS_PER_PAGE = 25; protected $restriction_categorie = null; protected $restriction_droit = null; static public function transformTitleToURI($str) { $str = Utils::transliterateToAscii($str); $str = preg_replace('![^\w\d_-]!i', '-', $str); $str = preg_replace('!-{2,}!', '-', $str); $str = trim($str, '-'); return $str; } // Gestion des données /////////////////////////////////////////////////////// public function _checkFields(&$data) { $db = DB::getInstance(); if (array_key_exists('titre', $data) && !trim($data['titre'])) { throw new UserException('Le titre ne peut rester vide.'); } if (array_key_exists('uri', $data) && !trim($data['uri'])) { throw new UserException('L\'adresse de la page ne peut rester vide.'); } if (array_key_exists('droit_lecture', $data)) { $data['droit_lecture'] = (int) $data['droit_lecture']; if ($data['droit_lecture'] < -1) { $data['droit_lecture'] = 0; } } if (array_key_exists('droit_ecriture', $data)) { $data['droit_ecriture'] = (int) $data['droit_ecriture']; if ($data['droit_ecriture'] < 0) { $data['droit_ecriture'] = 0; } } if (array_key_exists('parent', $data)) { $data['parent'] = (int) $data['parent']; if ($data['parent'] < 0) { $data['parent'] = 0; } if (!$db->firstColumn('SELECT 1 FROM wiki_pages WHERE id = ?;', $data['parent'])) { $data['parent'] = 0; } } return true; } public function create($data = []) { $this->_checkFields($data); $db = DB::getInstance(); if (!empty($data['uri'])) { $data['uri'] = self::transformTitleToURI($data['uri']); if ($db->firstColumn('SELECT 1 FROM wiki_pages WHERE uri = ? LIMIT 1;', $data['uri'])) { throw new UserException('Cette adresse de page est déjà utilisée pour une autre page, il faut en choisir une autre.'); } } else { $data['uri'] = self::transformTitleToURI($data['titre']); if (!trim($data['uri']) || $db->firstColumn('SELECT 1 FROM wiki_pages WHERE uri = ? LIMIT 1;', $data['uri'])) { $data['uri'] .= '_' . date('d-m-Y_H-i-s'); } } $db->insert('wiki_pages', $data); $id = $db->lastInsertRowId(); // On ne peut utiliser un trigger pour insérer dans la recherche // car les tables virtuelles font des opérations qui modifient // last_insert_rowid() et donc résultat incohérent $db->insert('wiki_recherche', ['id' => $id, 'titre' => $data['titre']]); return $id; } public function edit($id, $data = []) { $db = DB::getInstance(); $this->_checkFields($data); // Modification de la date de création: vérification que le format est bien conforme SQLite if (isset($data['date_creation'])) { if (!Utils::checkDateTime($data['date_creation'])) { throw new UserException('Date invalide: '.($data['date_creation'] ?: 'date non reconnue')); } // On stocke la date en UTC, pas dans le fuseau local $data['date_creation'] = gmdate('Y-m-d H:i:s', strtotime($data['date_creation'])); } if (isset($data['uri'])) { $data['uri'] = self::transformTitleToURI($data['uri']); if ($db->firstColumn('SELECT 1 FROM wiki_pages WHERE uri = ? AND id != ? LIMIT 1;', $data['uri'], (int)$id)) { throw new UserException('Cette adresse de page est déjà utilisée pour une autre page, il faut en choisir une autre.'); } } if (isset($data['droit_lecture']) && $data['droit_lecture'] >= self::LECTURE_CATEGORIE) { $data['droit_ecriture'] = $data['droit_lecture']; } if (isset($data['parent']) && (int)$data['parent'] == (int)$id) { $data['parent'] = 0; } $data['date_modification'] = gmdate('Y-m-d H:i:s'); $db->update('wiki_pages', $data, 'id = :id', ['id' => (int)$id]); return true; } public function delete($id) { $db = DB::getInstance(); // Ne pas permettre de supprimer une page qui a des sous-pages if ($db->firstColumn('SELECT 1 FROM wiki_pages WHERE parent = ? LIMIT 1;', (int)$id)) { return false; } // Suppression des fichiers liés $files = Fichiers::listLinkedFiles(Fichiers::LIEN_WIKI, $id, null); foreach ($files as $file) { $file = new Fichiers($file->id, $file); $file->remove(); } $db->delete('wiki_revisions', 'id_page = ?', (int)$id); $db->delete('wiki_recherche', 'id = ?', (int)$id); $db->delete('wiki_pages', 'id = ?', (int)$id); return true; } public function get($id) { $db = DB::getInstance(); return $db->first('SELECT *, strftime(\'%s\', date_creation) AS date_creation, strftime(\'%s\', date_modification) AS date_modification FROM wiki_pages WHERE id = ? LIMIT 1;', (int)$id); } public function getTitle($id) { $db = DB::getInstance(); return $db->firstColumn('SELECT titre FROM wiki_pages WHERE id = ? LIMIT 1;', (int)$id); } public function getRevision($id, $rev) { $db = DB::getInstance(); $champ_id = Config::getInstance()->get('champ_identite'); return $db->first('SELECT r.revision, r.modification, r.id_auteur, r.contenu, strftime(\'%s\', r.date) AS date, LENGTH(r.contenu) AS taille, m.'.$champ_id.' AS nom_auteur, r.chiffrement FROM wiki_revisions AS r LEFT JOIN membres AS m ON m.id = r.id_auteur WHERE r.id_page = ? AND revision = ? LIMIT 1;', (int) $id, (int) $rev); } public function listRevisions($id) { $db = DB::getInstance(); $champ_id = Config::getInstance()->get('champ_identite'); // FIXME pagination au lieu de bloquer à 1000 return $db->get('SELECT r.revision, r.modification, r.id_auteur, strftime(\'%s\', r.date) AS date, LENGTH(r.contenu) AS taille, m.'.$champ_id.' AS nom_auteur, LENGTH(r.contenu) - (SELECT LENGTH(contenu) FROM wiki_revisions WHERE id_page = r.id_page AND revision < r.revision ORDER BY revision DESC LIMIT 1) AS diff_taille, r.chiffrement FROM wiki_revisions AS r LEFT JOIN membres AS m ON m.id = r.id_auteur WHERE r.id_page = ? ORDER BY r.revision DESC LIMIT 1000;', (int) $id); } public function editRevision($id, $revision_edition = 0, $data) { $db = DB::getInstance(); $revision = $db->firstColumn('SELECT revision FROM wiki_pages WHERE id = ?;', (int)$id); // ?! L'ID fournit ne correspond à rien ? if ($revision === false) { throw new \RuntimeException('La page demandée n\'existe pas.'); } // Pas de révision if ($revision == 0 && !trim($data['contenu'])) { return true; } // Il faut obligatoirement fournir un ID d'auteur if (empty($data['id_auteur']) && $data['id_auteur'] !== null) { throw new \BadMethodCallException('Aucun ID auteur de fourni.'); } $contenu = $db->firstColumn('SELECT contenu FROM wiki_revisions WHERE revision = ? AND id_page = ?;', (int)$revision, (int)$id); // Pas de changement au contenu, pas la peine d'enregistrer une nouvelle révision if (trim($contenu) == trim($data['contenu'])) { return true; } // Révision sur laquelle est basée la nouvelle révision // utilisé pour vérifier que le contenu n'a pas été modifié depuis qu'on // a chargé la page d'édition if ($revision > $revision_edition) { throw new UserException('La page a été modifiée depuis le début de votre modification.'); } if (empty($data['chiffrement'])) $data['chiffrement'] = 0; if (!isset($data['modification']) || !trim($data['modification'])) $data['modification'] = null; // Incrémentons le numéro de révision $revision++; $data['id_page'] = $id; $data['revision'] = $revision; $db->insert('wiki_revisions', $data); $db->update('wiki_pages', [ 'revision' => $revision, 'date_modification' => gmdate('Y-m-d H:i:s'), ], 'id = :id', ['id' => (int)$id]); return true; } public function search($search) { if (strlen($search) > 100) { throw new UserException('Recherche trop longue : maximum 100 caractères'); } $query = sprintf('SELECT p.uri, r.*, snippet(wiki_recherche, \'<b>\', \'</b>\', \'...\', -1, -50) AS snippet, rank(matchinfo(wiki_recherche), 0, 1.0, 1.0) AS points FROM wiki_recherche AS r INNER JOIN wiki_pages AS p ON p.id = r.id WHERE %s AND wiki_recherche MATCH ? ORDER BY points DESC LIMIT 0,50;', $this->_getLectureClause('p.')); return DB::getInstance()->get($query, $search); } public function setRestrictionCategorie($id, $droit_wiki) { $this->restriction_categorie = $id; $this->restriction_droit = $droit_wiki; return true; } protected function _getLectureClause($prefix = '') { if (is_null($this->restriction_categorie)) { throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.'); } if ($this->restriction_droit == Membres::DROIT_AUCUN) { throw new UserException('Vous n\'avez pas accès au wiki.'); } if ($this->restriction_droit == Membres::DROIT_ADMIN) return '1'; return '('.$prefix.'droit_lecture = '.self::LECTURE_NORMAL.' OR '.$prefix.'droit_lecture = '.self::LECTURE_PUBLIC.' OR '.$prefix.'droit_lecture = '.(int)$this->restriction_categorie.')'; } public function canReadPage($lecture) { if (is_null($this->restriction_categorie)) { throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.'); } if ($this->restriction_droit < Membres::DROIT_ACCES) { return false; } if ($this->restriction_droit == Membres::DROIT_ADMIN || $lecture == self::LECTURE_NORMAL || $lecture == self::LECTURE_PUBLIC || $lecture == $this->restriction_categorie) return true; return false; } public function canWritePage($ecriture) { if (is_null($this->restriction_categorie)) { throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.'); } if ($this->restriction_droit < Membres::DROIT_ECRITURE) { return false; } if ($this->restriction_droit == Membres::DROIT_ADMIN || $ecriture == self::ECRITURE_NORMAL || $ecriture == $this->restriction_categorie) return true; return false; } public function getList($parent = 0, $order_by_date = false) { $order = ($order_by_date ? 'date_creation DESC' : 'transliterate_to_ascii(titre) COLLATE NOCASE'); $query = sprintf('SELECT id, revision, uri, titre, strftime(\'%%s\', date_creation) AS date_creation, strftime(\'%%s\', date_modification) AS date_modification FROM wiki_pages WHERE parent = ? AND %s ORDER BY %s LIMIT 500;', $this->_getLectureClause(), $order); return DB::getInstance()->get($query, (int) $parent); } public function hasChildren($parent, $public_only = false) { $db = DB::getInstance(); $public = !$public_only ? '' : ' AND ' . $db->where('droit_lecture', self::LECTURE_PUBLIC); return $db->test('wiki_pages', $db->where('parent', (int)$parent) . $public); } public function getById($id) { $db = DB::getInstance(); $page = $db->first('SELECT *, strftime(\'%s\', date_creation) AS date_creation, strftime(\'%s\', date_modification) AS date_modification FROM wiki_pages WHERE id = ?;', (int)$id); if (!$page) { return false; } $page->contenu = false; if ($page->revision > 0) { $page->contenu = $db->first('SELECT * FROM wiki_revisions WHERE id_page = ? AND revision = ?;', (int)$page->id, (int)$page->revision); } return $page; } public function getByURI($uri) { $id = DB::getInstance()->firstColumn('SELECT id FROM wiki_pages WHERE uri = ?;', $uri); if (!$id) { return false; } return $this->getByID($id); } public function listRecentModifications($page = 1) { $begin = ($page - 1) * self::ITEMS_PER_PAGE; $db = DB::getInstance(); return $db->get('SELECT *, strftime(\'%s\', date_creation) AS date_creation, strftime(\'%s\', date_modification) AS date_modification FROM wiki_pages WHERE '.$this->_getLectureClause().' ORDER BY date_modification DESC LIMIT ?,?;', $begin, self::ITEMS_PER_PAGE); } public function countRecentModifications() { $db = DB::getInstance(); return $db->firstColumn('SELECT COUNT(*) FROM wiki_pages WHERE '.$this->_getLectureClause().';'); } public function listBackBreadCrumbs($id) { if ($id == 0) return []; $db = DB::getInstance(); $flat = []; $max = 0; while ($id > 0 && $max++ < 10) { $res = $db->first('SELECT parent, titre, uri FROM wiki_pages WHERE id = ? LIMIT 1;', (int)$id); $flat[] = [ 'id' => $id, 'titre' => $res->titre, 'uri' => $res->uri, ]; if ($id == $res->parent) { throw new \Exception('Parent! ' . $id . '/' . $res->parent); } $id = (int)$res->parent; } return array_reverse($flat); } public function listBackParentTree($id) { $db = DB::getInstance(); $flat = [ (object) [ 'id' => 0, 'parent' => null, 'titre' => 'Racine', 'children' => $db->getGrouped('SELECT id, parent, titre FROM wiki_pages WHERE parent = ? ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE;', 0) ] ]; $max = 0; do { $parent = $db->get('SELECT parent FROM wiki_pages WHERE id = ? LIMIT 1;', (int)$id); $flat[$id] = (object) [ 'id' => $id, 'parent' => $id ? (int)$parent : null, 'titre' => $id ? $this->getTitle($id) : 'Racine', 'children' => $db->getGrouped('SELECT id, parent, titre FROM wiki_pages WHERE parent = ? ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE;', (int)$id) ]; $id = (int)$parent; } while ($id != 0 && $max++ < 20); $tree = []; foreach ($flat as $id=>&$node) { if (is_null($node->parent)) { $tree[$id] = &$node; } else { if (!isset($flat[$node->parent]->children)) { $flat[$node->parent]->children = []; } $flat[$node->parent]->children[$id] = &$node; } } return $tree; } } |
< < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/include/lib/dependencies.list from [b716c7d0a7] to [9fc3eee5ea].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
KD2/data/
KD2/DB/AbstractEntity.php
KD2/DB/DB.php
KD2/DB/EntityManager.php
KD2/DB/SQLite3.php
KD2/ErrorManager.php
KD2/FileInfo.php
KD2/Form.php
KD2/HTTP.php
KD2/Graphics/Image.php
KD2/Graphics/QRCode.php
KD2/Graphics/SVG/Pie.php
KD2/Graphics/SVG/Plot.php
KD2/MiniSkel.php
KD2/Office/Calc/Writer.php
KD2/Security.php
KD2/Security_OTP.php
KD2/SimpleDiff.php
KD2/SkrivLite.php
KD2/Smartyer.php
KD2/SMTP.php
KD2/Translate.php
KD2/UserSession.php
KD2/ZipWriter.php
|
> < |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
KD2/data/
KD2/DB/AbstractEntity.php
KD2/DB/DB.php
KD2/DB/EntityManager.php
KD2/DB/SQLite3.php
KD2/Brindille.php
KD2/ErrorManager.php
KD2/FileInfo.php
KD2/Form.php
KD2/HTTP.php
KD2/Graphics/Image.php
KD2/Graphics/QRCode.php
KD2/Graphics/SVG/Pie.php
KD2/Graphics/SVG/Plot.php
KD2/Office/Calc/Writer.php
KD2/Security.php
KD2/Security_OTP.php
KD2/SimpleDiff.php
KD2/SkrivLite.php
KD2/Smartyer.php
KD2/SMTP.php
KD2/Translate.php
KD2/UserSession.php
KD2/ZipWriter.php
|
Modified src/include/test_required.php from [a96dd46521] to [3b6f01c207].
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
echo "Pour plus d'informations consulter http://dev.kd2.org/garradin/Probl%C3%A8mes%20fr%C3%A9quents\n"; } exit; } test_requis( version_compare(phpversion(), '7.2', '>='), 'PHP 7.2 ou supérieur requis. PHP version ' . phpversion() . ' installée.' ); test_requis( defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH, 'L\'algorithme de hashage de mot de passe Blowfish n\'est pas présent (pas installé ou pas compilé).' ); |
| | |
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
echo "Pour plus d'informations consulter http://dev.kd2.org/garradin/Probl%C3%A8mes%20fr%C3%A9quents\n"; } exit; } test_requis( version_compare(phpversion(), '7.4', '>='), 'PHP 7.4 ou supérieur requis. PHP version ' . phpversion() . ' installée.' ); test_requis( defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH, 'L\'algorithme de hashage de mot de passe Blowfish n\'est pas présent (pas installé ou pas compilé).' ); |
Deleted src/plugins/index.html version [abe89f9bfb].
1 |
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>
|
< |
Modified src/templates/acc/accounts/deposit.tpl from [1de1606a11] to [216770f1fc].
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 |
</tr> </thead> <tbody> {foreach from=$journal item="line"} {if isset($line.sum)} <tr> <td colspan="5"></td> <td class="money">{if $line.sum > 0}-{/if}{$line.sum|abs|raw|html_money:false}</td> <th>Solde au {$line.date|date_short}</th> <td colspan="2"></td> </tr> {else} <tr> <td class="check"> {input type="checkbox" name="deposit[%d]"|args:$line.id value="1" data-debit=$line.debit|abs data-credit=$line.credit default=$line.checked} </td> <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> <td>{$line.reference}</td> <td>{$line.line_reference}</td> <th>{$line.label}</th> <td class="money">{$line.debit|raw|html_money}</td> <td class="money">{if $line.running_sum > 0}-{/if}{$line.running_sum|abs|raw|html_money:false}</td> </tr> {/if} {/foreach} </tbody> </table> <fieldset> |
| | | |
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 |
</tr> </thead> <tbody> {foreach from=$journal item="line"} {if isset($line.sum)} <tr> <td colspan="5"></td> <td class="money">{if $line.sum > 0}-{/if}{$line.sum|abs|raw|money:false}</td> <th>Solde au {$line.date|date_short}</th> <td colspan="2"></td> </tr> {else} <tr> <td class="check"> {input type="checkbox" name="deposit[%d]"|args:$line.id value="1" data-debit=$line.debit|abs data-credit=$line.credit default=$line.checked} </td> <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> <td>{$line.reference}</td> <td>{$line.line_reference}</td> <th>{$line.label}</th> <td class="money">{$line.debit|raw|money}</td> <td class="money">{if $line.running_sum > 0}-{/if}{$line.running_sum|abs|raw|money:false}</td> </tr> {/if} {/foreach} </tbody> </table> <fieldset> |
Modified src/templates/acc/accounts/index.tpl from [346db46cee] to [9f769dfcbf].
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
..
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
<nav class="tabs"> <aside> {linkbutton shape="search" href="!acc/search.php?year=%d"|args:$current_year.id label="Recherche"} </aside> <ul> <li class="current"><a href="{$admin_url}acc/accounts/">Comptes favoris</a></li> <li><a href="{$admin_url}acc/reports/trial_balance.php?year={$current_year.id}">Balance générale (tous les comptes)</a></li> {if $session->canAccess('compta', Membres::DROIT_ADMIN)} <li><a href="{$admin_url}acc/charts/accounts/?id={$chart_id}">Modifier les comptes</a></li> <li><a href="{$admin_url}acc/charts/accounts/all.php?id={$chart_id}">Plan comptable complet</a></li> {/if} </ul> </nav> {if isset($_GET['chart_change'])} ................................................................................ {elseif $account.sum > 0}(Créance) {/if} </em> {/if} </td> <td class="actions"> {linkbutton label="Journal" shape="menu" href="journal.php?id=%d&year=%d"|args:$account.id,$current_year.id} {if $session->canAccess('compta', Membres::DROIT_ADMIN)} {if $account.type == Entities\Accounting\Account::TYPE_BANK} {linkbutton label="Rapprochement" shape="check" href="reconcile.php?id=%d"|args:$account.id} {elseif $account.type == Entities\Accounting\Account::TYPE_OUTSTANDING} {linkbutton label="Dépôt en banque" shape="check" href="deposit.php?id=%d"|args:$account.id} {/if} {/if} </td> |
|
|
|
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
..
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
<nav class="tabs"> <aside> {linkbutton shape="search" href="!acc/search.php?year=%d"|args:$current_year.id label="Recherche"} </aside> <ul> <li class="current"><a href="{$admin_url}acc/accounts/">Comptes favoris</a></li> <li><a href="{$admin_url}acc/reports/trial_balance.php?year={$current_year.id}">Balance générale (tous les comptes)</a></li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <li><a href="{$admin_url}acc/charts/accounts/?id={$chart_id}">Modifier les comptes</a></li> <li><a href="{$admin_url}acc/charts/accounts/all.php?id={$chart_id}">Plan comptable complet</a></li> {/if} </ul> </nav> {if isset($_GET['chart_change'])} ................................................................................ {elseif $account.sum > 0}(Créance) {/if} </em> {/if} </td> <td class="actions"> {linkbutton label="Journal" shape="menu" href="journal.php?id=%d&year=%d"|args:$account.id,$current_year.id} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {if $account.type == Entities\Accounting\Account::TYPE_BANK} {linkbutton label="Rapprochement" shape="check" href="reconcile.php?id=%d"|args:$account.id} {elseif $account.type == Entities\Accounting\Account::TYPE_OUTSTANDING} {linkbutton label="Dépôt en banque" shape="check" href="deposit.php?id=%d"|args:$account.id} {/if} {/if} </td> |
Modified src/templates/acc/accounts/journal.tpl from [6182c4c368] to [8864e5930b].
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 .. 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 ... 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
<p class="alert block">Ce compte présente un solde négatif de <strong>{$sum|raw|money_currency}</strong>. Est-ce normal ? Cette situation ne devrait se produire que si vous avez reçu des remboursements par exemple, et que ceux-ci couvrent des dépenses réglées sur un exercice précédent.</p> {/if} {/if} <nav class="tabs"> <aside> {if $session->canAccess('compta', Membres::DROIT_ADMIN)} {linkbutton href="%s&export=csv"|args:$self_url label="Export CSV" shape="export"} {linkbutton href="%s&export=ods"|args:$self_url label="Export tableur" shape="export"} {/if} {linkbutton shape="search" href="!acc/search.php?year=%d&account=%s"|args:$year.id,$account.code label="Recherche"} {if $year.id == CURRENT_YEAR_ID} {linkbutton href="!acc/transactions/new.php?account=%d"|args:$account.id label="Saisir une écriture dans ce compte" shape="plus"} {/if} ................................................................................ <td class="check"> {input type="checkbox" name="check[%s]"|args:$line.id_line value=$line.id default=0} </td> {/if} <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> {if $simple} <td class="money">{if $line.change > 0}+{else}-{/if}{$line.change|abs|raw|html_money}</td> {else} <td class="money">{$line.debit|raw|html_money}</td> <td class="money">{$line.credit|raw|html_money}</td> {/if} {if isset($line->sum)} <td class="money">{$line.sum|raw|html_money:false}</td> {/if} <td>{$line.reference}</td> <th>{$line.label}</th> {if !$simple}<td>{$line.line_label}</td>{/if} <td>{$line.line_reference}</td> <td class="num">{if $line.id_analytical}<a href="{$admin_url}acc/reports/statement.php?analytical={$line.id_analytical}">{$line.code_analytical}</a>{/if}</td> <td class="actions"> ................................................................................ <tfoot> <tr> {if $can_edit} <td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td> {/if} {if !$simple}<td></td>{/if} <td colspan="3">Solde</td> <td class="money">{$sum|raw|html_money:false}</td> {if !$simple}<td></td>{/if} <td class="actions" colspan="5"> {if $can_edit} <em>Pour les écritures cochées :</em> <input type="hidden" name="from" value="{$self_url}" /> <input type="hidden" name="year" value="{$year.id}" /> {csrf_field key="projects_action"} |
| | | | | | |
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 .. 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 ... 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
<p class="alert block">Ce compte présente un solde négatif de <strong>{$sum|raw|money_currency}</strong>. Est-ce normal ? Cette situation ne devrait se produire que si vous avez reçu des remboursements par exemple, et que ceux-ci couvrent des dépenses réglées sur un exercice précédent.</p> {/if} {/if} <nav class="tabs"> <aside> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton href="%s&export=csv"|args:$self_url label="Export CSV" shape="export"} {linkbutton href="%s&export=ods"|args:$self_url label="Export tableur" shape="export"} {/if} {linkbutton shape="search" href="!acc/search.php?year=%d&account=%s"|args:$year.id,$account.code label="Recherche"} {if $year.id == CURRENT_YEAR_ID} {linkbutton href="!acc/transactions/new.php?account=%d"|args:$account.id label="Saisir une écriture dans ce compte" shape="plus"} {/if} ................................................................................ <td class="check"> {input type="checkbox" name="check[%s]"|args:$line.id_line value=$line.id default=0} </td> {/if} <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> {if $simple} <td class="money">{if $line.change > 0}+{else}-{/if}{$line.change|abs|raw|money}</td> {else} <td class="money">{$line.debit|raw|money}</td> <td class="money">{$line.credit|raw|money}</td> {/if} {if isset($line->sum)} <td class="money">{$line.sum|raw|money:false}</td> {/if} <td>{$line.reference}</td> <th>{$line.label}</th> {if !$simple}<td>{$line.line_label}</td>{/if} <td>{$line.line_reference}</td> <td class="num">{if $line.id_analytical}<a href="{$admin_url}acc/reports/statement.php?analytical={$line.id_analytical}">{$line.code_analytical}</a>{/if}</td> <td class="actions"> ................................................................................ <tfoot> <tr> {if $can_edit} <td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td> {/if} {if !$simple}<td></td>{/if} <td colspan="3">Solde</td> <td class="money">{$sum|raw|money:false}</td> {if !$simple}<td></td>{/if} <td class="actions" colspan="5"> {if $can_edit} <em>Pour les écritures cochées :</em> <input type="hidden" name="from" value="{$self_url}" /> <input type="hidden" name="year" value="{$year.id}" /> {csrf_field key="projects_action"} |
Modified src/templates/acc/accounts/reconcile.tpl from [cc53da36ae] to [aa2066a69f].
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
..
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
|
<form method="get" action="{$self_url_no_qs}"> {if $prev || $next} <fieldset class="shortFormRight"> <legend>Rapprochement par mois</legend> <p> {if $prev} {linkbutton shape="left" href=$prev.url label=$prev.date|date_fr:'F Y'} {/if} {if $next} {linkbutton shape="right" href=$next.url label=$next.date|date_fr:'F Y'} {/if} </p> </fieldset> {/if} <fieldset class="shortFormLeft"> <legend>Période de rapprochement</legend> <p> ................................................................................ </tr> </thead> <tbody> {foreach from=$journal item="line"} {if isset($line->sum)} <tr> <td colspan="5"></td> <td class="money">{if $line.sum > 0}-{/if}{$line.sum|abs|raw|html_money:false}</td> <td class="money">{if $line.reconciled_sum > 0}-{/if}{$line.reconciled_sum|abs|raw|html_money}</td> <th>Solde au {$line.date|date_short}</th> <td colspan="2"></td> </tr> {else} <tr> <td class="check"> {input type="checkbox" name="reconcile[%d]"|args:$line.id_line value="1" default=$line.reconciled} </td> <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> <td class="money">{$line.credit|raw|html_money}</td> <td class="money">{$line.debit|raw|html_money}</td> {* Not a bug! Credit/debit is reversed here to reflect the bank statement *} <td class="money">{if $line.running_sum > 0}-{/if}{$line.running_sum|abs|raw|html_money:false}</td> <td class="money">{if $line.reconciled_sum > 0}-{/if}{$line.reconciled_sum|abs|raw|html_money:false}</td> <th>{$line.label}</th> <td>{$line.reference}</td> <td>{$line.line_reference}</td> </tr> {/if} {/foreach} </tbody> |
|
|
|
|
|
|
|
|
|
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
..
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
|
<form method="get" action="{$self_url_no_qs}">
{if $prev || $next}
<fieldset class="shortFormRight">
<legend>Rapprochement par mois</legend>
<p>
{if $prev}
{linkbutton shape="left" href=$prev.url label=$prev.date|date:'F Y'}
{/if}
{if $next}
{linkbutton shape="right" href=$next.url label=$next.date|date:'F Y'}
{/if}
</p>
</fieldset>
{/if}
<fieldset class="shortFormLeft">
<legend>Période de rapprochement</legend>
<p>
................................................................................
</tr>
</thead>
<tbody>
{foreach from=$journal item="line"}
{if isset($line->sum)}
<tr>
<td colspan="5"></td>
<td class="money">{if $line.sum > 0}-{/if}{$line.sum|abs|raw|money:false}</td>
<td class="money">{if $line.reconciled_sum > 0}-{/if}{$line.reconciled_sum|abs|raw|money}</td>
<th>Solde au {$line.date|date_short}</th>
<td colspan="2"></td>
</tr>
{else}
<tr>
<td class="check">
{input type="checkbox" name="reconcile[%d]"|args:$line.id_line value="1" default=$line.reconciled}
</td>
<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td>
<td>{$line.date|date_short}</td>
<td class="money">{$line.credit|raw|money}</td>
<td class="money">{$line.debit|raw|money}</td> {* Not a bug! Credit/debit is reversed here to reflect the bank statement *}
<td class="money">{if $line.running_sum > 0}-{/if}{$line.running_sum|abs|raw|money:false}</td>
<td class="money">{if $line.reconciled_sum > 0}-{/if}{$line.reconciled_sum|abs|raw|money:false}</td>
<th>{$line.label}</th>
<td>{$line.reference}</td>
<td>{$line.line_reference}</td>
</tr>
{/if}
{/foreach}
</tbody>
|
Modified src/templates/acc/accounts/reconcile_assist.tpl from [4e7ccd5117] to [80420a98b7].
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 ... 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 ... 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
</tr> </thead> <tbody> {foreach from=$lines key="line_id" item="line"} {if isset($line->journal->sum)} <tr> <td colspan="4"></td> <td class="money">{if $line.journal.sum > 0}-{/if}{$line.journal.sum|abs|raw|html_money:false}</td> <th style="text-align: right">Solde au {$line.journal.date|date_short}</th> <td class="separator"></td> <td class="separator"></td> <td colspan="3"></td> </tr> {else} <tr> ................................................................................ {input type="checkbox" name="reconcile[%d]"|args:$line.journal.id_line value="1" default=$line.journal.reconciled} </td> <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.journal.id}">#{$line.journal.id}</a></td> <td>{$line.journal.date|date_short}</td> <td class="money"> {if $line.journal.credit} {* Not a bug! Credit/debit is reversed here to reflect the bank statement *} -{$line.journal.credit|raw|html_money} {else} {$line.journal.debit|raw|html_money} {/if} </td> <td class="money">{if $line.journal.running_sum > 0}-{/if}{$line.journal.running_sum|abs|raw|html_money:false}</td> <th style="text-align: right">{$line.journal.label}</th> {else} <td colspan="5"></td> <td style="text-align: right"> {if $line.add} {* FIXME later add ability to pre-fill multi-line transactions in new.php {linkbutton label="Créer cette écriture" target="_blank" href="%s&create=%s"|args:$self_url,$line_id shape="plus"} ................................................................................ {else} <b class="icn">⚠</b> {/if} </td> {if isset($line->csv)} <th class="separator">{$line.csv.label}</th> <td class="money"> {$line.csv.amount|raw|html_money} </td> <td class="money">{$line.csv.running_sum|raw|html_money}</td> <td>{$line.csv.date|date_short}</td> {else} <td colspan="4" class="separator"></td> {/if} </tr> {/if} {/foreach} |
| | | | | | |
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 ... 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 ... 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
</tr> </thead> <tbody> {foreach from=$lines key="line_id" item="line"} {if isset($line->journal->sum)} <tr> <td colspan="4"></td> <td class="money">{if $line.journal.sum > 0}-{/if}{$line.journal.sum|abs|raw|money:false}</td> <th style="text-align: right">Solde au {$line.journal.date|date_short}</th> <td class="separator"></td> <td class="separator"></td> <td colspan="3"></td> </tr> {else} <tr> ................................................................................ {input type="checkbox" name="reconcile[%d]"|args:$line.journal.id_line value="1" default=$line.journal.reconciled} </td> <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.journal.id}">#{$line.journal.id}</a></td> <td>{$line.journal.date|date_short}</td> <td class="money"> {if $line.journal.credit} {* Not a bug! Credit/debit is reversed here to reflect the bank statement *} -{$line.journal.credit|raw|money} {else} {$line.journal.debit|raw|money} {/if} </td> <td class="money">{if $line.journal.running_sum > 0}-{/if}{$line.journal.running_sum|abs|raw|money:false}</td> <th style="text-align: right">{$line.journal.label}</th> {else} <td colspan="5"></td> <td style="text-align: right"> {if $line.add} {* FIXME later add ability to pre-fill multi-line transactions in new.php {linkbutton label="Créer cette écriture" target="_blank" href="%s&create=%s"|args:$self_url,$line_id shape="plus"} ................................................................................ {else} <b class="icn">⚠</b> {/if} </td> {if isset($line->csv)} <th class="separator">{$line.csv.label}</th> <td class="money"> {$line.csv.amount|raw|money} </td> <td class="money">{$line.csv.running_sum|raw|money}</td> <td>{$line.csv.date|date_short}</td> {else} <td colspan="4" class="separator"></td> {/if} </tr> {/if} {/foreach} |
Modified src/templates/acc/accounts/simple.tpl from [55f085e703] to [97c4fe6c8a].
1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
{include file="admin/_head.tpl" title="Suivi : %s"|args:$types[$type] current="acc/simple"} {include file="acc/_year_select.tpl"} <nav class="tabs"> <aside> {if $session->canAccess('compta', Membres::DROIT_ADMIN)} {linkbutton href="?type=%d&export=csv"|args:$type label="Export CSV" shape="export"} {linkbutton href="?type=%d&export=ods"|args:$type label="Export tableur" shape="export"} {/if} {linkbutton shape="search" href="!acc/search.php?year=%d&type=%d"|args:$year.id,$type label="Recherche"} </aside> <ul> {foreach from=$types key="key" item="label"} ................................................................................ {if $can_edit} <td class="check"> {input type="checkbox" name="check[%s]"|args:$line.id_line value=$line.id default=0} </td> {/if} <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> <td class="money">{$line.change|abs|raw|html_money}</td> <td>{$line.reference}</td> <th>{$line.label}</th> <td>{$line.line_reference}</td> <td class="num">{if $line.id_analytical}<a href="{$admin_url}acc/reports/statement.php?analytical={$line.id_analytical}">{$line.code_analytical}</a>{/if}</td> <td class="actions"> {if $line.type == Entities\Accounting\Transaction::TYPE_DEBT && ($line.status & Entities\Accounting\Transaction::STATUS_WAITING)} {linkbutton shape="check" label="Régler cette dette" href="!acc/transactions/new.php?payoff_for=%d"|args:$line.id} |
|
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
{include file="admin/_head.tpl" title="Suivi : %s"|args:$types[$type] current="acc/simple"} {include file="acc/_year_select.tpl"} <nav class="tabs"> <aside> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton href="?type=%d&export=csv"|args:$type label="Export CSV" shape="export"} {linkbutton href="?type=%d&export=ods"|args:$type label="Export tableur" shape="export"} {/if} {linkbutton shape="search" href="!acc/search.php?year=%d&type=%d"|args:$year.id,$type label="Recherche"} </aside> <ul> {foreach from=$types key="key" item="label"} ................................................................................ {if $can_edit} <td class="check"> {input type="checkbox" name="check[%s]"|args:$line.id_line value=$line.id default=0} </td> {/if} <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> <td class="money">{$line.change|abs|raw|money}</td> <td>{$line.reference}</td> <th>{$line.label}</th> <td>{$line.line_reference}</td> <td class="num">{if $line.id_analytical}<a href="{$admin_url}acc/reports/statement.php?analytical={$line.id_analytical}">{$line.code_analytical}</a>{/if}</td> <td class="actions"> {if $line.type == Entities\Accounting\Transaction::TYPE_DEBT && ($line.status & Entities\Accounting\Transaction::STATUS_WAITING)} {linkbutton shape="check" label="Régler cette dette" href="!acc/transactions/new.php?payoff_for=%d"|args:$line.id} |
Modified src/templates/acc/charts/accounts/_nav.tpl from [ec091420e5] to [982004d151].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<nav class="tabs"> <ul> <li class="current"><a href="{$admin_url}acc/charts/">Plans comptables</a></li> {if $session->canAccess('compta', Membres::DROIT_ADMIN)} <li><a href="{$admin_url}acc/charts/import.php">Importer un plan comptable</a></li> {/if} </ul> <ul class="sub"> <li class="title">{$chart.label}</li> <li{if $current == 'favorites'} class="current"{/if}><a href="{$admin_url}acc/charts/accounts/?id={$chart.id}">Comptes favoris</a></li> <li{if $current == 'all'} class="current"{/if}><a href="{$admin_url}acc/charts/accounts/all.php?id={$chart.id}">Tous les comptes</a></li> {if $session->canAccess('compta', Membres::DROIT_ADMIN)} <li{if $current == 'new'} class="current"{/if}><a href="{$admin_url}acc/charts/accounts/new.php?id={$chart.id}"><strong>Ajouter un compte</strong></a></li> {/if} </ul> </nav> |
| | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<nav class="tabs"> <ul> <li class="current"><a href="{$admin_url}acc/charts/">Plans comptables</a></li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <li><a href="{$admin_url}acc/charts/import.php">Importer un plan comptable</a></li> {/if} </ul> <ul class="sub"> <li class="title">{$chart.label}</li> <li{if $current == 'favorites'} class="current"{/if}><a href="{$admin_url}acc/charts/accounts/?id={$chart.id}">Comptes favoris</a></li> <li{if $current == 'all'} class="current"{/if}><a href="{$admin_url}acc/charts/accounts/all.php?id={$chart.id}">Tous les comptes</a></li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <li{if $current == 'new'} class="current"{/if}><a href="{$admin_url}acc/charts/accounts/new.php?id={$chart.id}"><strong>Ajouter un compte</strong></a></li> {/if} </ul> </nav> |
Modified src/templates/acc/charts/accounts/all.tpl from [532b7dcf92] to [ff6fa9acdc].
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
{icon shape="star"} <?=Entities\Accounting\Account::TYPES_NAMES[$account->type]?>
{/if}
</td>
<td>
{if $account.user}<em>Ajouté</em>{/if}
</td>
<td class="actions">
{if $session->canAccess('compta', Membres::DROIT_ADMIN) && !$chart.archived}
{if $account.user || !$chart.code}
{linkbutton shape="delete" label="Supprimer" href="!acc/charts/accounts/delete.php?id=%d"|args:$account.id}
{/if}
{linkbutton shape="edit" label="Modifier" href="!acc/charts/accounts/edit.php?id=%d"|args:$account.id}
{/if}
</td>
</tr>
{/foreach}
</tbody>
</table>
{include file="admin/_foot.tpl"}
|
| |
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
{icon shape="star"} <?=Entities\Accounting\Account::TYPES_NAMES[$account->type]?>
{/if}
</td>
<td>
{if $account.user}<em>Ajouté</em>{/if}
</td>
<td class="actions">
{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$chart.archived}
{if $account.user || !$chart.code}
{linkbutton shape="delete" label="Supprimer" href="!acc/charts/accounts/delete.php?id=%d"|args:$account.id}
{/if}
{linkbutton shape="edit" label="Modifier" href="!acc/charts/accounts/edit.php?id=%d"|args:$account.id}
{/if}
</td>
</tr>
{/foreach}
</tbody>
</table>
{include file="admin/_foot.tpl"}
|
Modified src/templates/acc/charts/accounts/index.tpl from [d986fd5237] to [1453ceb936].
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
{foreach from=$group.accounts item="account"}
<tr>
<td class="num">{$account.code}</td>
<th>{$account.label}</th>
<td class="desc">{$account.description}</td>
<td class="actions">
{if $session->canAccess('compta', Membres::DROIT_ADMIN) && !$chart.archived}
{if $account.user || !$chart.code}
{linkbutton shape="delete" label="Supprimer" href="!acc/charts/accounts/delete.php?id=%d"|args:$account.id}
{/if}
{linkbutton shape="edit" label="Modifier" href="!acc/charts/accounts/edit.php?id=%d"|args:$account.id}
{/if}
</td>
</tr>
{/foreach}
</tbody>
{/foreach}
</table>
{include file="admin/_foot.tpl"}
|
| |
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
{foreach from=$group.accounts item="account"}
<tr>
<td class="num">{$account.code}</td>
<th>{$account.label}</th>
<td class="desc">{$account.description}</td>
<td class="actions">
{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$chart.archived}
{if $account.user || !$chart.code}
{linkbutton shape="delete" label="Supprimer" href="!acc/charts/accounts/delete.php?id=%d"|args:$account.id}
{/if}
{linkbutton shape="edit" label="Modifier" href="!acc/charts/accounts/edit.php?id=%d"|args:$account.id}
{/if}
</td>
</tr>
{/foreach}
</tbody>
{/foreach}
</table>
{include file="admin/_foot.tpl"}
|
Modified src/templates/acc/charts/accounts/selector.tpl from [6cbc945144] to [4fc94408d5].
1 2 3 4 5 6 7 8 |
{include file="admin/_head.tpl" title="Sélectionner un compte" body_id="popup" is_popup=true}
{if empty($grouped_accounts) && empty($accounts)}
<p class="block alert">Le plan comptable ne comporte aucun compte de ce type. Pour afficher des comptes ici, les <a href="{$www_url}admin/acc/charts/accounts/all.php?id={$chart.id}" target="_blank">modifier dans le plan comptable</a> en sélectionnant le type de compte favori voulu.</td>
{elseif isset($grouped_accounts)}
<?php $index = 1; ?>
|
| |
1 2 3 4 5 6 7 8 |
{include file="admin/_head.tpl" title="Sélectionner un compte"} {if empty($grouped_accounts) && empty($accounts)} <p class="block alert">Le plan comptable ne comporte aucun compte de ce type. Pour afficher des comptes ici, les <a href="{$www_url}admin/acc/charts/accounts/all.php?id={$chart.id}" target="_blank">modifier dans le plan comptable</a> en sélectionnant le type de compte favori voulu.</td> {elseif isset($grouped_accounts)} <?php $index = 1; ?> |
Modified src/templates/acc/charts/index.tpl from [edc5b4d9b1] to [0e0aa6ffba].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .. 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 |
{include file="admin/_head.tpl" title="Gestion des plans comptables" current="acc/charts"} <nav class="tabs"> <ul> <li class="current"><a href="{$admin_url}acc/charts/">Plans comptables</a></li> {if $session->canAccess('compta', Membres::DROIT_ADMIN)} <li><a href="{$admin_url}acc/charts/import.php">Importer un plan comptable</a></li> {/if} </ul> </nav> {if $_GET.msg == 'OPEN'} <p class="block alert"> Il n'existe aucun exercice ouvert. {if $session->canAccess('compta', Membres::DROIT_ADMIN)} Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures. {/if} </p> {/if} {if count($list)} <table class="list"> ................................................................................ <td>{$item.country|get_country_name}</td> <th><a href="{$admin_url}acc/charts/accounts/?id={$item.id}">{$item.label}</a></th> <td>{if $item.code}Officiel{else}Personnel{/if}</td> <td>{if $item.archived}<em>Archivé</em>{/if}</td> <td class="actions"> {linkbutton shape="star" label="Comptes favoris" href="!acc/charts/accounts/?id=%d"|args:$item.id} {linkbutton shape="menu" label="Tous les comptes" href="!acc/charts/accounts/all.php?id=%d"|args:$item.id} {if $session->canAccess('compta', Membres::DROIT_ADMIN)} {linkbutton shape="edit" label="Modifier" href="!acc/charts/edit.php?id=%d"|args:$item.id} {linkbutton shape="export" label="Export CSV" href="!acc/charts/export.php?id=%d"|args:$item.id} {linkbutton shape="export" label="Export tableur" href="!acc/charts/export.php?id=%d&ods"|args:$item.id} {if !$item.code && !$item.archived} {linkbutton shape="delete" label="Supprimer" href="!acc/charts/delete.php?id=%d"|args:$item.id} {/if} {/if} ................................................................................ </td> </tr> {/foreach} </tbody> </table> {/if} {if $session->canAccess('compta', Membres::DROIT_ADMIN)} <form method="post" action="{$self_url_no_qs}"> <fieldset> <legend>Créer un nouveau plan comptable</legend> <dl> {input type="select_groups" name="copy" options=$charts_groupped label="Recopier depuis" required=1 default=$from} {input type="text" name="label" label="Libellé" required=1} {input type="select" name="country" label="Pays" required=1 options=$country_list default=$config.pays} |
| | | | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .. 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 |
{include file="admin/_head.tpl" title="Gestion des plans comptables" current="acc/charts"} <nav class="tabs"> <ul> <li class="current"><a href="{$admin_url}acc/charts/">Plans comptables</a></li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <li><a href="{$admin_url}acc/charts/import.php">Importer un plan comptable</a></li> {/if} </ul> </nav> {if $_GET.msg == 'OPEN'} <p class="block alert"> Il n'existe aucun exercice ouvert. {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures. {/if} </p> {/if} {if count($list)} <table class="list"> ................................................................................ <td>{$item.country|get_country_name}</td> <th><a href="{$admin_url}acc/charts/accounts/?id={$item.id}">{$item.label}</a></th> <td>{if $item.code}Officiel{else}Personnel{/if}</td> <td>{if $item.archived}<em>Archivé</em>{/if}</td> <td class="actions"> {linkbutton shape="star" label="Comptes favoris" href="!acc/charts/accounts/?id=%d"|args:$item.id} {linkbutton shape="menu" label="Tous les comptes" href="!acc/charts/accounts/all.php?id=%d"|args:$item.id} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton shape="edit" label="Modifier" href="!acc/charts/edit.php?id=%d"|args:$item.id} {linkbutton shape="export" label="Export CSV" href="!acc/charts/export.php?id=%d"|args:$item.id} {linkbutton shape="export" label="Export tableur" href="!acc/charts/export.php?id=%d&ods"|args:$item.id} {if !$item.code && !$item.archived} {linkbutton shape="delete" label="Supprimer" href="!acc/charts/delete.php?id=%d"|args:$item.id} {/if} {/if} ................................................................................ </td> </tr> {/foreach} </tbody> </table> {/if} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <form method="post" action="{$self_url_no_qs}"> <fieldset> <legend>Créer un nouveau plan comptable</legend> <dl> {input type="select_groups" name="copy" options=$charts_groupped label="Recopier depuis" required=1 default=$from} {input type="text" name="label" label="Libellé" required=1} {input type="select" name="country" label="Pays" required=1 options=$country_list default=$config.pays} |
Modified src/templates/acc/index.tpl from [10bfa27777] to [63dacb5997].
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{foreach from=$years item="year"}
<section class="year-infos">
<h2 class="ruler">{$year.label} —
Du {$year.start_date|date_short} au {$year.end_date|date_short}</h2>
<nav class="tabs">
<aside>
{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
{linkbutton shape="upload" href="!acc/years/import.php?id=%d"|args:$year.id label="Import & export"}
{/if}
{linkbutton shape="search" href="!acc/search.php?year=%d"|args:$year.id label="Recherche"}
</aside>
<ul>
<li><a href="{$admin_url}acc/reports/graphs.php?year={$year.id}">Graphiques</a></li>
<li><a href="{$admin_url}acc/reports/trial_balance.php?year={$year.id}">Balance générale</a></li>
|
| |
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{foreach from=$years item="year"}
<section class="year-infos">
<h2 class="ruler">{$year.label} —
Du {$year.start_date|date_short} au {$year.end_date|date_short}</h2>
<nav class="tabs">
<aside>
{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
{linkbutton shape="upload" href="!acc/years/import.php?id=%d"|args:$year.id label="Import & export"}
{/if}
{linkbutton shape="search" href="!acc/search.php?year=%d"|args:$year.id label="Recherche"}
</aside>
<ul>
<li><a href="{$admin_url}acc/reports/graphs.php?year={$year.id}">Graphiques</a></li>
<li><a href="{$admin_url}acc/reports/trial_balance.php?year={$year.id}">Balance générale</a></li>
|
Modified src/templates/acc/reports/_journal.tpl from [4ecba1da68] to [ae9d01ba2d].
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<tr> <td rowspan="{$transaction.lines|count}" class="num"><a href="{$admin_url}acc/transactions/details.php?id={$transaction.id}">#{$transaction.id}</a></td> <td rowspan="{$transaction.lines|count}">{$transaction.reference}</td> <td rowspan="{$transaction.lines|count}">{$transaction.date|date_short}</td> <th rowspan="{$transaction.lines|count}">{$transaction.label}</th> {foreach from=$transaction.lines item="line"} <td>{$line.account_code} - {$line.account_label}</td> <td class="money">{$line.debit|raw|html_money}</td> <td class="money">{$line.credit|raw|html_money}</td> <td>{$line.label}</td> <td>{$line.reference}</td> </tr> <tr> {/foreach} </tr> </tbody> {/foreach} </table> |
| | |
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<tr> <td rowspan="{$transaction.lines|count}" class="num"><a href="{$admin_url}acc/transactions/details.php?id={$transaction.id}">#{$transaction.id}</a></td> <td rowspan="{$transaction.lines|count}">{$transaction.reference}</td> <td rowspan="{$transaction.lines|count}">{$transaction.date|date_short}</td> <th rowspan="{$transaction.lines|count}">{$transaction.label}</th> {foreach from=$transaction.lines item="line"} <td>{$line.account_code} - {$line.account_label}</td> <td class="money">{$line.debit|raw|money}</td> <td class="money">{$line.credit|raw|money}</td> <td>{$line.label}</td> <td>{$line.reference}</td> </tr> <tr> {/foreach} </tr> </tbody> {/foreach} </table> |
Modified src/templates/acc/reports/_statement.tpl from [2408ac0c3b] to [674c33b3c6].
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 |
<tfoot> <tr> <td> <table> <tfoot> <tr> <th>Total</th> <td class="money">{$statement.expense_sum|raw|html_money:false}</td> </tr> </tfoot> </table> </td> <td> <table> <tfoot> <tr> <th>Total</th> <td class="money">{$statement.revenue_sum|raw|html_money:false}</td> </tr> </tfoot> </table> </td> </tr> {if $statement.result} <tr> <td> {if ($statement.result < 0)} <table> <tfoot> <tr> <th>Résultat (perte)</th> <td class="money">{$statement.result|raw|html_money:false}</td> </tr> </tfoot> </table> {/if} </td> <td> {if ($statement.result >= 0)} <table> <tfoot> <tr> <th>Résultat (excédent)</th> <td class="money">{$statement.result|raw|html_money:false}</td> </tr> </tfoot> </table> {/if} </td> </tr> {/if} </tfoot> </table> |
| | | | |
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 |
<tfoot> <tr> <td> <table> <tfoot> <tr> <th>Total</th> <td class="money">{$statement.expense_sum|raw|money:false}</td> </tr> </tfoot> </table> </td> <td> <table> <tfoot> <tr> <th>Total</th> <td class="money">{$statement.revenue_sum|raw|money:false}</td> </tr> </tfoot> </table> </td> </tr> {if $statement.result} <tr> <td> {if ($statement.result < 0)} <table> <tfoot> <tr> <th>Résultat (perte)</th> <td class="money">{$statement.result|raw|money:false}</td> </tr> </tfoot> </table> {/if} </td> <td> {if ($statement.result >= 0)} <table> <tfoot> <tr> <th>Résultat (excédent)</th> <td class="money">{$statement.result|raw|money:false}</td> </tr> </tfoot> </table> {/if} </td> </tr> {/if} </tfoot> </table> |
Modified src/templates/acc/reports/_statement_table.tpl from [59dd236cd8] to [cb39c0e999].
7 8 9 10 11 12 13 14 15 16 17 18 |
<tr class="compte">
<td class="num">
{if !empty($year)}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$year.id}">{$account.code}</a>
{else}{$account.code}
{/if}
</td>
<th>{$account.label}</th>
<td class="money">{$account.sum|raw|html_money}</td>
</tr>
{/foreach}
</tbody>
</table>
|
| |
7 8 9 10 11 12 13 14 15 16 17 18 |
<tr class="compte"> <td class="num"> {if !empty($year)}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$year.id}">{$account.code}</a> {else}{$account.code} {/if} </td> <th>{$account.label}</th> <td class="money">{$account.sum|raw|money}</td> </tr> {/foreach} </tbody> </table> |
Modified src/templates/acc/reports/balance_sheet.tpl from [a7a5928f43] to [dc88e4cf11].
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 |
<tfoot> <tr> <td> <table> <tfoot> <tr> <th>Total actif</th> <td class="money">{$asset_sum|raw|html_money:false}</td> </tr> </tfoot> </table> </td> <td> <table> <tfoot> <tr> <th>Total passif</th> <td class="money">{$liability_sum|raw|html_money:false}</td> </tr> </tfoot> </table> </td> </tr> </tfoot> </table> <p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p> {include file="admin/_foot.tpl"} |
| | |
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 |
<tfoot> <tr> <td> <table> <tfoot> <tr> <th>Total actif</th> <td class="money">{$asset_sum|raw|money:false}</td> </tr> </tfoot> </table> </td> <td> <table> <tfoot> <tr> <th>Total passif</th> <td class="money">{$liability_sum|raw|money:false}</td> </tr> </tfoot> </table> </td> </tr> </tfoot> </table> <p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/reports/ledger.tpl from [a06ebc88ec] to [2309772ddd].
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
..
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
{foreach from=$account.lines item="line"} <tr> <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.reference}</td> <td>{$line.line_reference}</td> <td>{$line.date|date_short}</td> <th>{$line.label}{if $line.line_label} <em>({$line.line_label})</em>{/if}</th> <td class="money">{$line.debit|raw|html_money}</td> <td class="money">{$line.credit|raw|html_money}</td> <td class="money">{$line.running_sum|raw|html_money:false}</td> </tr> {/foreach} </tbody> <tfoot> <tr> <td colspan="4"></td> <th>Solde final</th> <td class="money">{$account.debit|raw|html_money}</td> <td class="money">{$account.credit|raw|html_money}</td> <td class="money">{$account.sum|raw|html_money:false}</td> </tr> </tfoot> </table> </details> {if isset($account->all_debit)} ................................................................................ <col width="10%" /> <col width="10%" /> <col width="10%" /> </colgroup> <tfoot> <tr> <td><strong>Totaux</strong></td> <td class="money">{$account.all_debit|raw|html_money:false}</td> <td class="money">{$account.all_credit|raw|html_money:false}</td> <td></td> </tr> </tfoot> </table> {/if} {/foreach} |
|
|
|
|
|
|
|
|
|
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
..
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
{foreach from=$account.lines item="line"}
<tr>
<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td>
<td>{$line.reference}</td>
<td>{$line.line_reference}</td>
<td>{$line.date|date_short}</td>
<th>{$line.label}{if $line.line_label} <em>({$line.line_label})</em>{/if}</th>
<td class="money">{$line.debit|raw|money}</td>
<td class="money">{$line.credit|raw|money}</td>
<td class="money">{$line.running_sum|raw|money:false}</td>
</tr>
{/foreach}
</tbody>
<tfoot>
<tr>
<td colspan="4"></td>
<th>Solde final</th>
<td class="money">{$account.debit|raw|money}</td>
<td class="money">{$account.credit|raw|money}</td>
<td class="money">{$account.sum|raw|money:false}</td>
</tr>
</tfoot>
</table>
</details>
{if isset($account->all_debit)}
................................................................................
<col width="10%" />
<col width="10%" />
<col width="10%" />
</colgroup>
<tfoot>
<tr>
<td><strong>Totaux</strong></td>
<td class="money">{$account.all_debit|raw|money:false}</td>
<td class="money">{$account.all_credit|raw|money:false}</td>
<td></td>
</tr>
</tfoot>
</table>
{/if}
{/foreach}
|
Modified src/templates/acc/reports/projects.tpl from [c1c139c152] to [4800025ba6].
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
..
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
<aside> {linkbutton label="Créer un nouveau compte de projet" href="!acc/charts/accounts/new.php?id=%d&type=%d"|args:$current_year.id_chart,$analytical_type shape="plus"} </aside> {/if} <ul> <li><a href="{$admin_url}acc/years/">Exercices</a></li> {if $session->canAccess('compta', Membres::DROIT_ADMIN)} <li><a href="{$admin_url}acc/years/new.php">Nouvel exercice</a></li> {/if} <li class="current"><a href="{$admin_url}acc/reports/projects.php">Projets <em>(compta analytique)</em></a></li> </ul> <ul class="sub"> <li{if !$by_year} class="current"{/if}><a href="{$self_url_no_qs}">Par projet</a></li> ................................................................................ | <a href="{$admin_url}acc/reports/trial_balance.php?analytical={$item.id_account}&year={$item.id_year}">Balance générale</a> | <a href="{$admin_url}acc/reports/journal.php?analytical={$item.id_account}&year={$item.id_year}">Journal général</a> | <a href="{$admin_url}acc/reports/ledger.php?analytical={$item.id_account}&year={$item.id_year}">Grand livre</a> | <a href="{$admin_url}acc/reports/statement.php?analytical={$item.id_account}&year={$item.id_year}">Compte de résultat</a> | <a href="{$admin_url}acc/reports/balance_sheet.php?analytical={$item.id_account}&year={$item.id_year}">Bilan</a> </span> </td> <td class="money">{$item.sum_expense|raw|html_money}</td> <td class="money">{$item.sum_revenue|raw|html_money}</td> <td class="money">{$item.debit|raw|html_money:false}</td> <td class="money">{$item.credit|raw|html_money:false}</td> <td class="money">{$item.sum|raw|html_money:false}</td> </tr> {/foreach} </tbody> {/foreach} </table> {else} |
|
|
|
|
|
|
|
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
..
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
<aside> {linkbutton label="Créer un nouveau compte de projet" href="!acc/charts/accounts/new.php?id=%d&type=%d"|args:$current_year.id_chart,$analytical_type shape="plus"} </aside> {/if} <ul> <li><a href="{$admin_url}acc/years/">Exercices</a></li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <li><a href="{$admin_url}acc/years/new.php">Nouvel exercice</a></li> {/if} <li class="current"><a href="{$admin_url}acc/reports/projects.php">Projets <em>(compta analytique)</em></a></li> </ul> <ul class="sub"> <li{if !$by_year} class="current"{/if}><a href="{$self_url_no_qs}">Par projet</a></li> ................................................................................ | <a href="{$admin_url}acc/reports/trial_balance.php?analytical={$item.id_account}&year={$item.id_year}">Balance générale</a> | <a href="{$admin_url}acc/reports/journal.php?analytical={$item.id_account}&year={$item.id_year}">Journal général</a> | <a href="{$admin_url}acc/reports/ledger.php?analytical={$item.id_account}&year={$item.id_year}">Grand livre</a> | <a href="{$admin_url}acc/reports/statement.php?analytical={$item.id_account}&year={$item.id_year}">Compte de résultat</a> | <a href="{$admin_url}acc/reports/balance_sheet.php?analytical={$item.id_account}&year={$item.id_year}">Bilan</a> </span> </td> <td class="money">{$item.sum_expense|raw|money}</td> <td class="money">{$item.sum_revenue|raw|money}</td> <td class="money">{$item.debit|raw|money:false}</td> <td class="money">{$item.credit|raw|money:false}</td> <td class="money">{$item.sum|raw|money:false}</td> </tr> {/foreach} </tbody> {/foreach} </table> {else} |
Modified src/templates/acc/reports/trial_balance.tpl from [98a27d9282] to [14933bb3ab].
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<tr> <td class="num"> {if !empty($year)}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$year.id}">{$account.code}</a> {else}{$account.code} {/if} </td> <th>{$account.label}</th> <td class="money">{$account.debit|raw|html_money}</td> <td class="money">{$account.credit|raw|html_money}</td> <td class="money">{if $account.sum < 0}{$account.sum|abs|escape|html_money}{/if}</td> <td class="money">{if $account.sum > 0}{$account.sum|abs|escape|html_money}{/if}</td> </tr> {/foreach} </tbody> </table> <p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p> {include file="admin/_foot.tpl"} |
| | | | |
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<tr> <td class="num"> {if !empty($year)}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$year.id}">{$account.code}</a> {else}{$account.code} {/if} </td> <th>{$account.label}</th> <td class="money">{$account.debit|raw|money}</td> <td class="money">{$account.credit|raw|money}</td> <td class="money">{if $account.sum < 0}{$account.sum|abs|escape|money}{/if}</td> <td class="money">{if $account.sum > 0}{$account.sum|abs|escape|money}{/if}</td> </tr> {/foreach} </tbody> </table> <p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/search.tpl from [8068c4405e] to [f2a1ec386b].
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
..
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
<li><a href="saved_searches.php">Recherches enregistrées</a></li> </ul> </nav> {include file="common/search/advanced.tpl" action_url=$self_url} {if !empty($result)} {*if $session->canAccess('compta', Membres::DROIT_ECRITURE)} <form method="post" action="{$admin_url}membres/action.php" class="memberList"> {/if*} <p class="help">{$result|count} écritures trouvées pour cette recherche.</p> <table class="list search"> <thead> <tr> {*if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" /></td>{/if*} {foreach from=$result_header item="label"} <td>{$label}</td> {/foreach} <td></td> </tr> </thead> <tbody> {foreach from=$result item="row"} <tr> {*if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" name="selected[]" value="{$row.id}" /></td>{/if*} {foreach from=$row key="key" item="value"} {if $key == 'transaction_id'} <td class="num"> <a href="{$admin_url}acc/transactions/details.php?id={$value}">{$value}</a> </td> {else} <td> {if $key == 'credit' || $key == 'debit'} {$value|raw|html_money:false} {elseif $key == 'date'} {$value|date_short} {elseif null == $value} <em>(nul)</em> {else} {$value} {/if} ................................................................................ {if $row.transaction_id} {linkbutton shape="search" label="Détails" href="!acc/transactions/details.php?id=%d"|args:$row.transaction_id} {/if} </td> </tr> {/foreach} </tbody> {*if $session->canAccess('membres', Membres::DROIT_ADMIN)} {include file="admin/membres/_list_actions.tpl" colspan=count($result_header)+1} {/if*} </table> {*if $session->canAccess('membres', Membres::DROIT_ECRITURE)} </form> {/if*} {elseif $result !== null} <p class="block alert"> Aucun résultat trouvé. |
|
|
|
|
|
|
|
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
..
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
<li><a href="saved_searches.php">Recherches enregistrées</a></li> </ul> </nav> {include file="common/search/advanced.tpl" action_url=$self_url} {if !empty($result)} {*if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} <form method="post" action="{$admin_url}membres/action.php" class="memberList"> {/if*} <p class="help">{$result|count} écritures trouvées pour cette recherche.</p> <table class="list search"> <thead> <tr> {*if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" /></td>{/if*} {foreach from=$result_header item="label"} <td>{$label}</td> {/foreach} <td></td> </tr> </thead> <tbody> {foreach from=$result item="row"} <tr> {*if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}<td class="check"><input type="checkbox" name="selected[]" value="{$row.id}" /></td>{/if*} {foreach from=$row key="key" item="value"} {if $key == 'transaction_id'} <td class="num"> <a href="{$admin_url}acc/transactions/details.php?id={$value}">{$value}</a> </td> {else} <td> {if $key == 'credit' || $key == 'debit'} {$value|raw|money:false} {elseif $key == 'date'} {$value|date_short} {elseif null == $value} <em>(nul)</em> {else} {$value} {/if} ................................................................................ {if $row.transaction_id} {linkbutton shape="search" label="Détails" href="!acc/transactions/details.php?id=%d"|args:$row.transaction_id} {/if} </td> </tr> {/foreach} </tbody> {*if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} {include file="admin/membres/_list_actions.tpl" colspan=count($result_header)+1} {/if*} </table> {*if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} </form> {/if*} {elseif $result !== null} <p class="block alert"> Aucun résultat trouvé. |
Modified src/templates/acc/transactions/details.tpl from [42225313b2] to [361a38a61b].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .. 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 .. 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 ... 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
{include file="admin/_head.tpl" title="Écriture n°%d"|args:$transaction.id current="acc"} {if $session->canAccess('compta', Membres::DROIT_ADMIN) && !$transaction->validated && !$tr_year->closed} <nav class="tabs"> <ul> <li><a href="edit.php?id={$transaction.id}">Modifier cette écriture</a></li> <li><a href="delete.php?id={$transaction.id}">Supprimer cette écriture</a></li> </ul> </nav> {/if} {if $session->canAccess('compta', Membres::DROIT_ECRITURE) && $transaction.status & $transaction::STATUS_WAITING} <div class="block alert"> <form method="post" action="{$self_url}"> {if $transaction.type == $transaction::TYPE_DEBT} <h3>Dette en attente</h3> {linkbutton shape="check" label="Enregistrer le règlement de cette dette" href="!acc/transactions/new.php?payoff_for=%d"|args:$transaction.id} {else} <h3>Créance en attente</h3> ................................................................................ <dt>Type</dt> <dd> {$transaction->getTypeName()} </dd> <dt>Libellé</dt> <dd><h2>{$transaction.label}</h2></dd> <dt>Date</dt> <dd>{$transaction.date|date_fr:'l j F Y (d/m/Y)'}</dd> <dt>Numéro pièce comptable</dt> <dd>{if trim($transaction.reference)}{$transaction.reference}{else}-{/if}</dd> <dt>Exercice</dt> <dd> <a href="{$admin_url}acc/reports/ledger.php?year={$transaction.id_year}">{$tr_year.label}</a> | Du {$tr_year.start_date|date_short} au {$tr_year.end_date|date_short} | <strong>{if $tr_year.closed}Clôturé{else}En cours{/if}</strong> </dd> <dt>Écriture créée par</dt> <dd> {if $transaction.id_creator} {if $session->canAccess('compta', Membres::DROIT_ACCES)} <a href="{$admin_url}membres/fiche.php?id={$transaction.id_creator}">{$creator_name}</a> {else} {$creator_name} {/if} {else} <em>membre supprimé</em> {/if} ................................................................................ {if $u.id_service_user}— en règlement d'une <a href="{$admin_url}services/user.php?id={$u.id}&only={$u.id_service_user}">activité</a>{/if} </dd> {/foreach} {/if} <dt>Remarques</dt> <dd>{if trim($transaction.notes)}{$transaction.notes|escape|nl2br}{else}-{/if}</dd> <dt>Fichiers joints</dt> {foreach from=$files item="file"} <dd> <aside class="file"> <a target="_blank" href="{$file.url}">{$file.nom}</a> <small>({$file.type}, {$file.taille|format_bytes})</small> {linkbutton shape="download" href=$file.url target="_blank" label="Télécharger"} {linkbutton shape="delete" href="!acc/transactions/delete_file.php?id=%d&from=%d"|args:$file.id,$transaction.id label="Supprimer"} </aside> </dd> {foreachelse} <dd>-</dd> {/foreach} </dl> <table class="list"> <thead> <tr> <td class="num">N° compte</td> <th>Compte</th> ................................................................................ </tr> </thead> <tbody> {foreach from=$transaction->getLinesWithAccounts(false) item="line"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$line.id_account}&year={$transaction.id_year}">{$line.account_code}</a></td> <td>{$line.account_name}</td> <td class="money">{if $line.debit}{$line.debit|escape|html_money}{/if}</td> <td class="money">{if $line.credit}{$line.credit|escape|html_money}{/if}</td> <td>{$line.label}</td> <td>{$line.reference}</td> <td> {if $line.id_analytical} <a href="{$admin_url}acc/reports/statement.php?analytical={$line.id_analytical}">{$line.analytical_name}</a> {/if} </td> </tr> {/foreach} </tbody> </table> {include file="admin/_foot.tpl"} |
| | | | < < < < < < < < < < < < < < | | > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .. 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 .. 82 83 84 85 86 87 88 89 90 91 92 93 94 95 ... 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 |
{include file="admin/_head.tpl" title="Écriture n°%d"|args:$transaction.id current="acc"} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$transaction->validated && !$tr_year->closed} <nav class="tabs"> <ul> <li><a href="edit.php?id={$transaction.id}">Modifier cette écriture</a></li> <li><a href="delete.php?id={$transaction.id}">Supprimer cette écriture</a></li> </ul> </nav> {/if} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE) && $transaction.status & $transaction::STATUS_WAITING} <div class="block alert"> <form method="post" action="{$self_url}"> {if $transaction.type == $transaction::TYPE_DEBT} <h3>Dette en attente</h3> {linkbutton shape="check" label="Enregistrer le règlement de cette dette" href="!acc/transactions/new.php?payoff_for=%d"|args:$transaction.id} {else} <h3>Créance en attente</h3> ................................................................................ <dt>Type</dt> <dd> {$transaction->getTypeName()} </dd> <dt>Libellé</dt> <dd><h2>{$transaction.label}</h2></dd> <dt>Date</dt> <dd>{$transaction.date|date:'l j F Y (d/m/Y)'}</dd> <dt>Numéro pièce comptable</dt> <dd>{if trim($transaction.reference)}{$transaction.reference}{else}-{/if}</dd> <dt>Exercice</dt> <dd> <a href="{$admin_url}acc/reports/ledger.php?year={$transaction.id_year}">{$tr_year.label}</a> | Du {$tr_year.start_date|date_short} au {$tr_year.end_date|date_short} | <strong>{if $tr_year.closed}Clôturé{else}En cours{/if}</strong> </dd> <dt>Écriture créée par</dt> <dd> {if $transaction.id_creator} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)} <a href="{$admin_url}membres/fiche.php?id={$transaction.id_creator}">{$creator_name}</a> {else} {$creator_name} {/if} {else} <em>membre supprimé</em> {/if} ................................................................................ {if $u.id_service_user}— en règlement d'une <a href="{$admin_url}services/user.php?id={$u.id}&only={$u.id_service_user}">activité</a>{/if} </dd> {/foreach} {/if} <dt>Remarques</dt> <dd>{if trim($transaction.notes)}{$transaction.notes|escape|nl2br}{else}-{/if}</dd> </dl> <table class="list"> <thead> <tr> <td class="num">N° compte</td> <th>Compte</th> ................................................................................ </tr> </thead> <tbody> {foreach from=$transaction->getLinesWithAccounts(false) item="line"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$line.id_account}&year={$transaction.id_year}">{$line.account_code}</a></td> <td>{$line.account_name}</td> <td class="money">{if $line.debit}{$line.debit|escape|money}{/if}</td> <td class="money">{if $line.credit}{$line.credit|escape|money}{/if}</td> <td>{$line.label}</td> <td>{$line.reference}</td> <td> {if $line.id_analytical} <a href="{$admin_url}acc/reports/statement.php?analytical={$line.id_analytical}">{$line.analytical_name}</a> {/if} </td> </tr> {/foreach} </tbody> </table> {if $can_upload || count($files)} <div class="attachments"> <h3 class="ruler">Fichiers joints</h3> {include file="common/files/_context_list.tpl" files=$files can_upload=$can_upload parent_path=$file_parent} </div> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/acc/transactions/edit.tpl from [9472a8358a] to [e2fa247466].
1
2
3
4
5
6
7
8
9
10
..
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
{include file="admin/_head.tpl" title="Modification d'une écriture" current="acc/simple"} <form method="post" action="{$self_url}" enctype="multipart/form-data" data-focus="#f_date"> {form_errors} {if $has_reconciled_lines} <p class="alert block"> Attention, cette écriture contient des lignes qui ont été rapprochées. La modification de cette écriture entraînera la perte du rapprochement. </p> {/if} ................................................................................ <legend>Détails facultatifs</legend> <dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}"> {input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." default=$first_line.reference} </dl> <dl> {input type="list" multiple=true name="users" label="Membres associés" target="membres/selector.php" default=$linked_users} {input type="textarea" name="notes" label="Remarques" rows=4 cols=30 source=$transaction} {input type="file" name="file" label="Ajouter un fichier joint"} </dl> <dl data-types="all-but-advanced"> {if count($analytical_accounts) > 1} {input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts default=$first_line.id_analytical} {/if} </dl> </fieldset> |
|
<
<
|
1
2
3
4
5
6
7
8
9
10
..
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
{include file="admin/_head.tpl" title="Modification d'une écriture" current="acc/simple"}
<form method="post" action="{$self_url}" data-focus="#f_date">
{form_errors}
{if $has_reconciled_lines}
<p class="alert block">
Attention, cette écriture contient des lignes qui ont été rapprochées. La modification de cette écriture entraînera la perte du rapprochement.
</p>
{/if}
................................................................................
<legend>Détails facultatifs</legend>
<dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}">
{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." default=$first_line.reference}
</dl>
<dl>
{input type="list" multiple=true name="users" label="Membres associés" target="membres/selector.php" default=$linked_users}
{input type="textarea" name="notes" label="Remarques" rows=4 cols=30 source=$transaction}
</dl>
<dl data-types="all-but-advanced">
{if count($analytical_accounts) > 1}
{input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts default=$first_line.id_analytical}
{/if}
</dl>
</fieldset>
|
Modified src/templates/acc/transactions/new.tpl from [7fde859c77] to [5dd8c5b01f].
1
2
3
4
5
6
7
8
9
10
11
12
..
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
{include file="admin/_head.tpl" title="Saisie d'une écriture" current="acc/new"} {include file="acc/_year_select.tpl"} <form method="post" action="{$self_url}" enctype="multipart/form-data" data-focus="1"> {form_errors} {if $ok} <p class="block confirm"> L'écriture numéro <a href="details.php?id={$ok}">{$ok}</a> a été ajoutée. (<a href="details.php?id={$ok}">Voir l'écriture</a>) </p> ................................................................................ <legend>Détails facultatifs</legend> <dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}"> {input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." source=$transaction} </dl> <dl> {input type="list" multiple=true name="users" label="Membres associés" target="membres/selector.php"} {input type="textarea" name="notes" label="Remarques" rows=4 cols=30} {input type="file" name="file" label="Fichier joint"} </dl> <dl data-types="all-but-advanced"> {if count($analytical_accounts) > 1} {input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts} {/if} </dl> </fieldset> |
|
<
<
|
1
2
3
4
5
6
7
8
9
10
11
12
..
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
{include file="admin/_head.tpl" title="Saisie d'une écriture" current="acc/new"}
{include file="acc/_year_select.tpl"}
<form method="post" action="{$self_url}" data-focus="1">
{form_errors}
{if $ok}
<p class="block confirm">
L'écriture numéro <a href="details.php?id={$ok}">{$ok}</a> a été ajoutée.
(<a href="details.php?id={$ok}">Voir l'écriture</a>)
</p>
................................................................................
<legend>Détails facultatifs</legend>
<dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}">
{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." source=$transaction}
</dl>
<dl>
{input type="list" multiple=true name="users" label="Membres associés" target="membres/selector.php"}
{input type="textarea" name="notes" label="Remarques" rows=4 cols=30}
</dl>
<dl data-types="all-but-advanced">
{if count($analytical_accounts) > 1}
{input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts}
{/if}
</dl>
</fieldset>
|
Modified src/templates/acc/transactions/service_user.tpl from [a8761edf6b] to [31416dfe77].
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
</tr> </thead> <tbody> {foreach from=$balance item="account"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}">{$account.code}</a></td> <th>{$account.label}</th> <td class="money">{if $account.sum < 0}{$account.sum|raw|html_money}{/if}</td> <td class="money">{if $account.sum > 0}{$account.sum|raw|html_money}{/if}</td> </tr> {/foreach} </tbody> </table> {include file="admin/_foot.tpl"} |
| | |
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
</tr> </thead> <tbody> {foreach from=$balance item="account"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}">{$account.code}</a></td> <th>{$account.label}</th> <td class="money">{if $account.sum < 0}{$account.sum|raw|money}{/if}</td> <td class="money">{if $account.sum > 0}{$account.sum|raw|money}{/if}</td> </tr> {/foreach} </tbody> </table> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/transactions/user.tpl from [fca819aadf] to [4c90bde47d].
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
</tr> </thead> <tbody> {foreach from=$balance item="account"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}">{$account.code}</a></td> <th>{$account.label}</th> <td class="money">{if $account.sum < 0}{$account.sum|raw|html_money}{/if}</td> <td class="money">{if $account.sum > 0}{$account.sum|raw|html_money}{/if}</td> </tr> {/foreach} </tbody> </table> {include file="admin/_foot.tpl"} |
| | |
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
</tr> </thead> <tbody> {foreach from=$balance item="account"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}">{$account.code}</a></td> <th>{$account.label}</th> <td class="money">{if $account.sum < 0}{$account.sum|raw|money}{/if}</td> <td class="money">{if $account.sum > 0}{$account.sum|raw|money}{/if}</td> </tr> {/foreach} </tbody> </table> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/years/index.tpl from [fd39c3ba37] to [b7ac0ffc9e].
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
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
<nav class="tabs"> <aside> {linkbutton shape="search" href="!acc/search.php" label="Recherche"} </aside> <ul> <li class="current"><a href="{$self_url}">Exercices</a></li> {if $session->canAccess('compta', Membres::DROIT_ADMIN)} <li><a href="{$admin_url}acc/years/new.php">Nouvel exercice</a></li> {/if} <li><a href="{$admin_url}acc/reports/projects.php">Projets <em>(compta analytique)</em></a></li> </ul> </nav> {if $_GET.msg == 'OPEN'} <p class="block error"> Il n'existe aucun exercice ouvert. {if $session->canAccess('compta', Membres::DROIT_ADMIN)} Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures. {/if} </p> {/if} {if $_GET.msg == 'UPDATE_FEES'} <p class="block error"> ................................................................................ | <a href="{$admin_url}acc/reports/statement.php?year={$year.id}">Compte de résultat</a> | <a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a> </td> </tr> <tr> <td><em>{if $year.closed}Clôturé{else}En cours{/if}</em></td> <td> {if $session->canAccess('compta', Membres::DROIT_ADMIN)} {linkbutton label="Export CSV" shape="export" href="import.php?id=%d&export=csv"|args:$year.id} {linkbutton label="Export tableur" shape="export" href="import.php?id=%d&export=ods"|args:$year.id} {if !$year.closed} {linkbutton label="Import" shape="upload" href="import.php?id=%d"|args:$year.id} {linkbutton label="Balance d'ouverture" shape="reset" href="balance.php?id=%d"|args:$year.id} {linkbutton label="Modifier" shape="edit" href="edit.php?id=%d"|args:$year.id} {linkbutton label="Clôturer" shape="lock" href="close.php?id=%d"|args:$year.id} |
|
|
|
|
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
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
<nav class="tabs"> <aside> {linkbutton shape="search" href="!acc/search.php" label="Recherche"} </aside> <ul> <li class="current"><a href="{$self_url}">Exercices</a></li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <li><a href="{$admin_url}acc/years/new.php">Nouvel exercice</a></li> {/if} <li><a href="{$admin_url}acc/reports/projects.php">Projets <em>(compta analytique)</em></a></li> </ul> </nav> {if $_GET.msg == 'OPEN'} <p class="block error"> Il n'existe aucun exercice ouvert. {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures. {/if} </p> {/if} {if $_GET.msg == 'UPDATE_FEES'} <p class="block error"> ................................................................................ | <a href="{$admin_url}acc/reports/statement.php?year={$year.id}">Compte de résultat</a> | <a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a> </td> </tr> <tr> <td><em>{if $year.closed}Clôturé{else}En cours{/if}</em></td> <td> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton label="Export CSV" shape="export" href="import.php?id=%d&export=csv"|args:$year.id} {linkbutton label="Export tableur" shape="export" href="import.php?id=%d&export=ods"|args:$year.id} {if !$year.closed} {linkbutton label="Import" shape="upload" href="import.php?id=%d"|args:$year.id} {linkbutton label="Balance d'ouverture" shape="reset" href="balance.php?id=%d"|args:$year.id} {linkbutton label="Modifier" shape="edit" href="edit.php?id=%d"|args:$year.id} {linkbutton label="Clôturer" shape="lock" href="close.php?id=%d"|args:$year.id} |
Modified src/templates/admin/_head.tpl from [b134b1fcd2] to [9c3d24a63b].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 .. 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 .. 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 |
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr"> <head> <meta charset="utf-8" /> <title>{$title}</title> <link rel="icon" type="image/png" href="{$admin_url}static/icon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" /> <link rel="stylesheet" type="text/css" href="{$admin_url}static/admin.css?{$version_hash}" media="all" /> <script type="text/javascript" src="{$admin_url}static/scripts/global.js?{$version_hash}"></script> {if isset($custom_js)} {foreach from=$custom_js item="js"} <script type="text/javascript" src="{$admin_url}static/scripts/{$js}?{$version_hash}"></script> {/foreach} {/if} {if isset($custom_css)} {foreach from=$custom_css item="css"} <link rel="stylesheet" type="text/css" href="{$admin_url}static/{$css}?{$version_hash}" media="all" /> {/foreach} {/if} {if isset($plugin_css)} {foreach from=$plugin_css item="css"} <link rel="stylesheet" type="text/css" href="{plugin_url file=$css}?{$version_hash}" /> {/foreach} {/if} ................................................................................ <link rel="stylesheet" type="text/css" href="{$admin_url}static/print.css?{$version_hash}" media="print" /> <link rel="stylesheet" type="text/css" href="{$admin_url}static/handheld.css?{$version_hash}" media="handheld,screen and (max-width:981px)" /> {if isset($config)} {custom_colors config=$config} {/if} </head> <body{if !empty($body_id)} id="{$body_id}"{/if}> {if empty($is_popup)} <header class="header"> <nav class="menu"> <ul> {if !$is_logged} <li><a href="{$www_url}">← Retour au site</a></li> <li><a href="{$admin_url}">Connexion</a> <ul> ................................................................................ <ul> {foreach from=$plugins_menu key="plugin_id" item="name"} <li class="plugins {if $current == sprintf("plugin_%s", $plugin_id)} current{/if}"><a href="{plugin_url id=$plugin_id}">{$name}</a></li> {/foreach} </ul> {/if} </li> {if $session->canAccess('membres', Membres::DROIT_ACCES)} <li class="member list{if $current == 'membres'} current{elseif $current_parent == 'membres'} current_parent{/if}"><a href="{$admin_url}membres/"><b class="icn">👪</b><i> Membres</i></a> <ul> {if $session->canAccess('membres', Membres::DROIT_ECRITURE)} <li class="member new{if $current == 'membres/ajouter'} current{/if}"><a href="{$admin_url}membres/ajouter.php">Ajouter</a></li> {/if} <li class="{if $current == 'membres/services'} current{/if}"><a href="{$admin_url}services/">Activités & cotisations</a></li> {if $session->canAccess('membres', Membres::DROIT_ECRITURE)} <li class="member message{if $current == 'membres/message'} current{/if}"><a href="{$admin_url}membres/message_collectif.php">Message collectif</a></li> {/if} </ul> </li> {/if} {if $session->canAccess('compta', Membres::DROIT_ACCES)} <li class="{if $current == 'acc'} current{elseif $current_parent == 'acc'} current_parent{/if}"><a href="{$admin_url}acc/"><b>€</b><i> Comptabilité</i></a> <ul> {if $session->canAccess('compta', Membres::DROIT_ECRITURE)} <li class="{if $current == 'acc/new'} current{/if}"><a href="{$admin_url}acc/transactions/new.php">Saisie</a></li> {/if} <li class="{if $current == 'acc/accounts'} current{/if}"><a href="{$admin_url}acc/accounts/">Comptes</a></li> <li class="{if $current == 'acc/simple'} current{/if}"><a href="{$admin_url}acc/accounts/simple.php">Suivi des écritures</a></li> <li class="{if $current == 'acc/years'} current{/if}"><a href="{$admin_url}acc/years/">Exercices & rapports</a></li> {if $session->canAccess('compta', Membres::DROIT_ECRITURE)} <li class="{if $current == 'acc/charts'} current{/if}"><a href="{$admin_url}acc/charts/">Plans comptables</a></li> {/if} </ul> </li> {/if} {if $session->canAccess('wiki', Membres::DROIT_ACCES)} <li class="wiki{if $current == 'wiki'} current{elseif $current_parent == 'wiki'} current_parent{/if}"><a href="{$admin_url}wiki/"><b class="icn">✎</b><i> Wiki</i></a> <ul> <li class="wiki list{if $current == 'wiki/recent'} current{/if}"><a href="{$admin_url}wiki/recent.php">Dernières modifications</a> <li class="wiki search{if $current == 'wiki/chercher'} current{/if}"><a href="{$admin_url}wiki/chercher.php">Recherche</a> </ul> </li> {/if} {if $session->canAccess('config', Membres::DROIT_ADMIN)} <li class="main config{if $current == 'config'} current{elseif $current_parent == 'config'} current_parent{/if}"><a href="{$admin_url}config/"><b class="icn">☸</b><i> Configuration</i></a> {/if} <li class="{if $current == 'mes_infos'} current{elseif $current_parent == 'mes_infos'} current_parent{/if}"> <a href="{$admin_url}mes_infos.php"><b class="icn">👤</b><i> Mes infos personnelles</i></a> <ul> <li{if $current == 'my_services'} class="current"{/if}><a href="{$admin_url}my_services.php">Mes activités & cotisations</a></li> </ul> </li> {if !defined('Garradin\LOCAL_LOGIN') || !LOCAL_LOGIN} <li class="logout"><a href="{$admin_url}logout.php"><b class="icn">⤝</b><i> Déconnexion</i></a></li> {/if} {/if} </ul> </nav> <h1>{$title}</h1> </header> {/if} <main> |
| | | | | | | | | | | > | < | < < | > > > > > | > > | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 .. 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 .. 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 |
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr"{if array_key_exists('_dialog', $_GET)} class="dialog"{/if}> <head> <meta charset="utf-8" /> <title>{$title}</title> <link rel="icon" type="image/png" href="{$admin_url}static/icon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" /> <link rel="stylesheet" type="text/css" href="{$admin_url}static/admin.css?{$version_hash}" media="all" /> <script type="text/javascript" src="{$admin_url}static/scripts/global.js?{$version_hash}"></script> {if isset($custom_js)} {foreach from=$custom_js item="js"} <script type="text/javascript" src="{$admin_url}static/scripts/{$js}?{$version_hash}"></script> {/foreach} {/if} {if isset($custom_css)} {foreach from=$custom_css item="css_url"} <link rel="stylesheet" type="text/css" href="{$css_url|local_url:"!static/styles/"}?{$version_hash}" media="all" /> {/foreach} {/if} {if isset($plugin_css)} {foreach from=$plugin_css item="css"} <link rel="stylesheet" type="text/css" href="{plugin_url file=$css}?{$version_hash}" /> {/foreach} {/if} ................................................................................ <link rel="stylesheet" type="text/css" href="{$admin_url}static/print.css?{$version_hash}" media="print" /> <link rel="stylesheet" type="text/css" href="{$admin_url}static/handheld.css?{$version_hash}" media="handheld,screen and (max-width:981px)" /> {if isset($config)} {custom_colors config=$config} {/if} </head> <body> {if !array_key_exists('_dialog', $_GET)} <header class="header"> <nav class="menu"> <ul> {if !$is_logged} <li><a href="{$www_url}">← Retour au site</a></li> <li><a href="{$admin_url}">Connexion</a> <ul> ................................................................................ <ul> {foreach from=$plugins_menu key="plugin_id" item="name"} <li class="plugins {if $current == sprintf("plugin_%s", $plugin_id)} current{/if}"><a href="{plugin_url id=$plugin_id}">{$name}</a></li> {/foreach} </ul> {/if} </li> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)} <li class="member list{if $current == 'membres'} current{elseif $current_parent == 'membres'} current_parent{/if}"><a href="{$admin_url}membres/"><b class="icn">👪</b><i> Membres</i></a> <ul> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} <li class="member new{if $current == 'membres/ajouter'} current{/if}"><a href="{$admin_url}membres/ajouter.php">Ajouter</a></li> {/if} <li class="{if $current == 'membres/services'} current{/if}"><a href="{$admin_url}services/">Activités & cotisations</a></li> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} <li class="member message{if $current == 'membres/message'} current{/if}"><a href="{$admin_url}membres/message_collectif.php">Message collectif</a></li> {/if} </ul> </li> {/if} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)} <li class="{if $current == 'acc'} current{elseif $current_parent == 'acc'} current_parent{/if}"><a href="{$admin_url}acc/"><b>€</b><i> Comptabilité</i></a> <ul> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} <li class="{if $current == 'acc/new'} current{/if}"><a href="{$admin_url}acc/transactions/new.php">Saisie</a></li> {/if} <li class="{if $current == 'acc/accounts'} current{/if}"><a href="{$admin_url}acc/accounts/">Comptes</a></li> <li class="{if $current == 'acc/simple'} current{/if}"><a href="{$admin_url}acc/accounts/simple.php">Suivi des écritures</a></li> <li class="{if $current == 'acc/years'} current{/if}"><a href="{$admin_url}acc/years/">Exercices & rapports</a></li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} <li class="{if $current == 'acc/charts'} current{/if}"><a href="{$admin_url}acc/charts/">Plans comptables</a></li> {/if} </ul> </li> {/if} {if $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_READ)} <li class="{if $current == 'docs'} current{elseif $current_parent == 'docs'} current_parent{/if}"><a href="{$admin_url}docs/"><b class="icn">🗀</b><i> Documents</i></a> </li> {/if} {if $session->canAccess($session::SECTION_WEB, $session::ACCESS_READ)} <li class="{if $current == 'web'} current{elseif $current_parent == 'web'} current_parent{/if}"><a href="{$admin_url}web/"><b class="icn">🖻</b><i> Site web</i></a> </li> {/if} {if $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN)} <li class="main config{if $current == 'config'} current{elseif $current_parent == 'config'} current_parent{/if}"><a href="{$admin_url}config/"><b class="icn">☸</b><i> Configuration</i></a> {/if} <li class="{if $current == 'mes_infos'} current{elseif $current_parent == 'mes_infos'} current_parent{/if}"> <a href="{$admin_url}mes_infos.php"><b class="icn">👤</b><i> Mes infos personnelles</i></a> <ul> <li{if $current == 'my_services'} class="current"{/if}><a href="{$admin_url}my_services.php">Mes activités & cotisations</a></li> </ul> </li> {if !defined('Garradin\LOCAL_LOGIN') || !LOCAL_LOGIN} <li class="logout"><a href="{$admin_url}logout.php"><b class="icn">⤝</b><i> Déconnexion</i></a></li> {/if} {/if} </ul> </nav> <h1>{$title}</h1> </header> {/if} <main> |
Modified src/templates/admin/config/_menu.tpl from [1062a04378] to [6b04ba763d].
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<nav class="tabs">
<ul>
<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Général</a></li>
<li{if $current == 'categories'} class="current"{/if}><a href="{$admin_url}config/categories/">Catégories de membres</a></li>
<li{if $current == 'fiches_membres'} class="current"{/if}><a href="{$admin_url}config/membres.php">Fiche des membres</a></li>
<li{if $current == 'site'} class="current"{/if}><a href="{$admin_url}config/site.php">Site public</a></li>
<li{if $current == 'donnees'} class="current"{/if}><a href="{$admin_url}config/donnees/">Sauvegardes</a></li>
<li{if $current == 'plugins'} class="current"{/if}><a href="{$admin_url}config/plugins.php">Extensions</a></li>
<li{if $current == 'advanced'} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li>
</ul>
{if $current == 'advanced'}
<ul class="sub">
|
< |
1 2 3 4 5 6 7 8 9 10 11 12 |
<nav class="tabs"> <ul> <li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Général</a></li> <li{if $current == 'categories'} class="current"{/if}><a href="{$admin_url}config/categories/">Catégories de membres</a></li> <li{if $current == 'fiches_membres'} class="current"{/if}><a href="{$admin_url}config/membres.php">Fiche des membres</a></li> <li{if $current == 'donnees'} class="current"{/if}><a href="{$admin_url}config/donnees/">Sauvegardes</a></li> <li{if $current == 'plugins'} class="current"{/if}><a href="{$admin_url}config/plugins.php">Extensions</a></li> <li{if $current == 'advanced'} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li> </ul> {if $current == 'advanced'} <ul class="sub"> |
Modified src/templates/admin/config/advanced/errors.tpl from [db5622e3ef] to [39b2ff6b31].
1 2 3 4 5 6 7 8 .. 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 .. 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
{include file="admin/_head.tpl" title="Journaux" current="config" custom_css=["styles/config.css"]} {include file="admin/config/_menu.tpl" current="advanced" sub_current="errors"} {if isset($reports) && isset($id)} <section class="error"> {foreach from=$main.errors item="error"} <h2 class="ruler">{$error.type}: {$error.message} [Code: {$error.errorCode}]</h2> ................................................................................ </article> {/foreach} {/if} {/foreach} {foreach from=$reports item=report} <article class="event"> <h2 class="ruler">Occurence du {$report.context.date|date_fr}</h2> <table class="list"> {foreach from=$report.context key="k" item="v"} <tr> <th>{$k}</th> <td>{if $k == 'date'}{$v|date_fr}{else}{$v}{/if}</td> </tr> {/foreach} </table> </article> {/foreach} </section> {elseif isset($errors)} ................................................................................ <tr> <th><a href="?type=errors&id={$ref}">{$ref}</a></th> <td> {$error.message}<br /> <tt>{$error.source}</tt> </td> <td>{$error.count}</td> <td>{$error.last_seen|date_fr}</td> <td class="actions"> {linkbutton shape="menu" label="Voir les détails" href="%s?type=errors&id=%s"|args:$self_url_no_qs,$ref} </td> </tr> {/foreach} </tbody> </table> {/if} {/if} {include file="admin/_foot.tpl"} |
| | | | |
1 2 3 4 5 6 7 8 .. 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 .. 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
{include file="admin/_head.tpl" title="Journaux" current="config" custom_css=["config.css"]} {include file="admin/config/_menu.tpl" current="advanced" sub_current="errors"} {if isset($reports) && isset($id)} <section class="error"> {foreach from=$main.errors item="error"} <h2 class="ruler">{$error.type}: {$error.message} [Code: {$error.errorCode}]</h2> ................................................................................ </article> {/foreach} {/if} {/foreach} {foreach from=$reports item=report} <article class="event"> <h2 class="ruler">Occurence du {$report.context.date|date}</h2> <table class="list"> {foreach from=$report.context key="k" item="v"} <tr> <th>{$k}</th> <td>{if $k == 'date'}{$v|date}{else}{$v}{/if}</td> </tr> {/foreach} </table> </article> {/foreach} </section> {elseif isset($errors)} ................................................................................ <tr> <th><a href="?type=errors&id={$ref}">{$ref}</a></th> <td> {$error.message}<br /> <tt>{$error.source}</tt> </td> <td>{$error.count}</td> <td>{$error.last_seen|date}</td> <td class="actions"> {linkbutton shape="menu" label="Voir les détails" href="%s?type=errors&id=%s"|args:$self_url_no_qs,$ref} </td> </tr> {/foreach} </tbody> </table> {/if} {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/advanced/index.tpl from [96b57f6bc4] to [b3ebdb69a6].
1 2 3 4 5 6 7 8 |
{include file="admin/_head.tpl" title="Fonctions avancées" current="config" custom_css=["styles/config.css"]}
{include file="admin/config/_menu.tpl" current="advanced" sub_current=null}
<p class="help block">
Attention, les fonctions avancées peuvent permettre de supprimer des données ou rendre votre instance inutilisable !
</p>
|
| |
1 2 3 4 5 6 7 8 |
{include file="admin/_head.tpl" title="Fonctions avancées" current="config" custom_css=["config.css"]} {include file="admin/config/_menu.tpl" current="advanced" sub_current=null} <p class="help block"> Attention, les fonctions avancées peuvent permettre de supprimer des données ou rendre votre instance inutilisable ! </p> |
Modified src/templates/admin/config/advanced/sql.tpl from [a5a49b987e] to [61d73a5581].
1 2 3 4 5 6 7 8 |
{include file="admin/_head.tpl" title="SQL" current="config" custom_css=["styles/config.css"]}
{include file="admin/config/_menu.tpl" current="advanced" sub_current="sql"}
{form_errors}
{if $query}
<h2 class="ruler">Requête SQL</h2>
|
| |
1 2 3 4 5 6 7 8 |
{include file="admin/_head.tpl" title="SQL" current="config" custom_css=["config.css"]} {include file="admin/config/_menu.tpl" current="advanced" sub_current="sql"} {form_errors} {if $query} <h2 class="ruler">Requête SQL</h2> |
Modified src/templates/admin/config/categories/index.tpl from [b1e9086f4b] to [30fbc63a13].
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 |
{include file="admin/_head.tpl" title="Catégories de membres" current="config"} {include file="admin/config/_menu.tpl" current="categories"} <table class="list"> <thead> <th>Nom</th> <td class="num">Membres</td> <td>Droits</td> <td></td> </thead> <tbody> {foreach from=$liste item="cat"} <tr> <th>{$cat.nom}</th> <td class="num">{$cat.nombre}</td> <td class="droits"> {format_droits droits=$cat} </td> <td class="actions"> {if $cat.id != $user.id_categorie} {linkbutton shape="delete" label="Supprimer" href="supprimer.php?id=%d"|args:$cat.id} {/if} {linkbutton shape="edit" label="Modifier" href="modifier.php?id=%d"|args:$cat.id} {linkbutton shape="users" label="Liste des membres" href="!membres/?cat=%d"|args:$cat.id} </td> </tr> {/foreach} </tbody> </table> <form method="post" action="{$self_url}"> <fieldset> <legend>Ajouter une catégorie</legend> <dl> <dt><label for="f_nom">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="text" name="nom" id="f_nom" value="{form_field name=nom}" required="required" /></dd> </dl> </fieldset> <p class="submit"> {csrf_field key="new_cat"} {button type="submit" name="save" label="Ajouter" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
| | | | | | | | | | | | | | | | | | | | | | | | | | | | < | < | | | | > |
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 |
{include file="admin/_head.tpl" title="Catégories de membres" current="config"} {include file="admin/config/_menu.tpl" current="categories"} <table class="list"> <thead> <th>Nom</th> <td class="num">Membres</td> <td>Droits</td> <td></td> </thead> <tbody> {foreach from=$list item="cat"} <tr> <th>{$cat.name}</th> <td class="num">{$cat.count}</td> <td class="permissions"> {display_permissions permissions=$cat} </td> <td class="actions"> {if $cat.id != $user.category_id} {linkbutton shape="delete" label="Supprimer" href="supprimer.php?id=%d"|args:$cat.id} {/if} {linkbutton shape="edit" label="Modifier" href="modifier.php?id=%d"|args:$cat.id} {linkbutton shape="users" label="Liste des membres" href="!membres/?cat=%d"|args:$cat.id} </td> </tr> {/foreach} </tbody> </table> <form method="post" action="{$self_url}"> <fieldset> <legend>Ajouter une catégorie</legend> <dl> {input type="text" name="name" label="Nom" required=true} </dl> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Ajouter" shape="right" class="main"} </p> </fieldset> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/categories/modifier.tpl from [4bc679901e] to [abe451d3d1].
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 |
{include file="admin/config/_menu.tpl" current="categories"} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Informations générales</legend> <dl> <dt><label for="f_nom">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="text" name="nom" id="f_nom" value="{form_field data=$cat name=nom}" required="required" /></dd> <dt> <input type="checkbox" name="cacher" value="1" id="f_cacher" {if $cat.cacher}checked="checked"{/if} /> <label for="f_cacher">Catégorie cachée</label> </dt> <dd class="help"> Si coché cette catégorie ne sera visible qu'aux administrateurs et ne recevra pas de messages collectifs ou de rappels. </dd> </dl> </fieldset> <fieldset> <legend>Droits</legend> <dl class="droits"> <dt><label for="f_droit_connexion_aucun">Les membres de cette catégorie peuvent-ils se connecter ?</label></dt> {if $readonly} <dd class="help"> Il n'est pas possible de désactiver ce droit pour votre propre catégorie. </dd> {/if} <dd> <input type="radio" name="droit_connexion" value="{$membres::DROIT_AUCUN}" id="f_droit_connexion_aucun" {if $cat.droit_connexion == $membres::DROIT_AUCUN}checked="checked"{/if} {$readonly} /> <label for="f_droit_connexion_aucun"><b class="aucun">C</b> Non</label> </dd> <dd> <input type="radio" name="droit_connexion" value="{$membres::DROIT_ACCES}" id="f_droit_connexion_acces" {if $cat.droit_connexion == $membres::DROIT_ACCES}checked="checked"{/if} {$readonly} /> <label for="f_droit_connexion_acces"><b class="acces">C</b> Oui</label> </dd> </dl> {* TODO pas d'inscription pour le moment <dl class="droits"> <dt><label for="f_droit_inscription_aucun">Les membres de cette catégorie peuvent-ils s'inscrire d'eux-même ?</label></dt> <dd> <input type="radio" name="droit_inscription" value="{$membres::DROIT_AUCUN}" id="f_droit_inscription_aucun" {if $cat.droit_inscription == $membres::DROIT_AUCUN}checked="checked"{/if} /> <label for="f_droit_inscription_aucun"><b class="aucun">I</b> Non</label> </dd> <dd> <input type="radio" name="droit_inscription" value="{$membres::DROIT_ACCES}" id="f_droit_inscription_acces" {if $cat.droit_inscription == $membres::DROIT_ACCES}checked="checked"{/if} /> <label for="f_droit_inscription_acces"><b class="acces">I</b> Oui</label> </dd> </dl> *} <dl class="droits"> <dt><label for="f_droit_membres_aucun">Gestion des membres :</label></dt> <dd> <input type="radio" name="droit_membres" value="{$membres::DROIT_AUCUN}" id="f_droit_membres_aucun" {if $cat.droit_membres == $membres::DROIT_AUCUN}checked="checked"{/if} /> <label for="f_droit_membres_aucun"><b class="aucun">M</b> Pas d'accès</label> </dd> <dd> <input type="radio" name="droit_membres" value="{$membres::DROIT_ACCES}" id="f_droit_membres_acces" {if $cat.droit_membres == $membres::DROIT_ACCES}checked="checked"{/if} /> <label for="f_droit_membres_acces"><b class="acces">M</b> Lecture uniquement <em>(peut voir les informations personnelles de tous les membres, y compris leurs inscriptions à des activités)</em></label> </dd> <dd> <input type="radio" name="droit_membres" value="{$membres::DROIT_ECRITURE}" id="f_droit_membres_ecriture" {if $cat.droit_membres == $membres::DROIT_ECRITURE}checked="checked"{/if} /> <label for="f_droit_membres_ecriture"><b class="ecriture">M</b> Lecture & écriture <em>(peut ajouter et modifier des membres, mais pas les supprimer ni les changer de catégorie, peut inscrire des membres à des activités)</em></label> </dd> <dd> <input type="radio" name="droit_membres" value="{$membres::DROIT_ADMIN}" id="f_droit_membres_admin" {if $cat.droit_membres == $membres::DROIT_ADMIN}checked="checked"{/if} /> <label for="f_droit_membres_admin"><b class="admin">M</b> Administration <em>(peut tout faire)</em></label> </dd> </dl> <dl class="droits"> <dt><label for="f_droit_compta_aucun">Comptabilité :</label></dt> <dd> <input type="radio" name="droit_compta" value="{$membres::DROIT_AUCUN}" id="f_droit_compta_aucun" {if $cat.droit_compta == $membres::DROIT_AUCUN}checked="checked"{/if} /> <label for="f_droit_compta_aucun"><b class="aucun">€</b> Pas d'accès</label> </dd> <dd> <input type="radio" name="droit_compta" value="{$membres::DROIT_ACCES}" id="f_droit_compta_acces" {if $cat.droit_compta == $membres::DROIT_ACCES}checked="checked"{/if} /> <label for="f_droit_compta_acces"><b class="acces">€</b> Lecture uniquement <em>(peut lire toutes les informations de tous les exercices)</em></label> </dd> <dd> <input type="radio" name="droit_compta" value="{$membres::DROIT_ECRITURE}" id="f_droit_compta_ecriture" {if $cat.droit_compta == $membres::DROIT_ECRITURE}checked="checked"{/if} /> <label for="f_droit_compta_ecriture"><b class="ecriture">€</b> Lecture & écriture <em>(peut ajouter des écritures, mais pas les modifier ni les supprimer)</em></label> </dd> <dd> <input type="radio" name="droit_compta" value="{$membres::DROIT_ADMIN}" id="f_droit_compta_admin" {if $cat.droit_compta == $membres::DROIT_ADMIN}checked="checked"{/if} /> <label for="f_droit_compta_admin"><b class="admin">€</b> Administration <em>(peut modifier et supprimer des écritures, gérer les comptes, les exercices, etc.)</em></label> </dd> </dl> <dl class="droits"> <dt><label for="f_droit_wiki_aucun">Wiki :</label></dt> <dd> <input type="radio" name="droit_wiki" value="{$membres::DROIT_AUCUN}" id="f_droit_wiki_aucun" {if $cat.droit_wiki == $membres::DROIT_AUCUN}checked="checked"{/if} /> <label for="f_droit_wiki_aucun"><b class="aucun">W</b> Pas d'accès</label> </dd> <dd> <input type="radio" name="droit_wiki" value="{$membres::DROIT_ACCES}" id="f_droit_wiki_acces" {if $cat.droit_wiki == $membres::DROIT_ACCES}checked="checked"{/if} /> <label for="f_droit_wiki_acces"><b class="acces">W</b> Lecture uniquement <em>(peut lire les pages auquel il a le droit d'accéder)</em></label> </dd> <dd> <input type="radio" name="droit_wiki" value="{$membres::DROIT_ECRITURE}" id="f_droit_wiki_ecriture" {if $cat.droit_wiki == $membres::DROIT_ECRITURE}checked="checked"{/if} /> <label for="f_droit_wiki_ecriture"><b class="ecriture">W</b> Lecture & écriture <em>(peut modifier ou ajouter des pages, mais pas les supprimer)</em></label> </dd> <dd> <input type="radio" name="droit_wiki" value="{$membres::DROIT_ADMIN}" id="f_droit_wiki_admin" {if $cat.droit_wiki == $membres::DROIT_ADMIN}checked="checked"{/if} /> <label for="f_droit_wiki_admin"><b class="admin">W</b> Administration <em>(peut tout faire)</em></label> </dd> </dl> <dl class="droits"> <dt><label for="f_droit_config_aucun">Les membres de cette catégorie peuvent-ils modifier la configuration ?</label></dt> {if $readonly} <dd class="help"> Il n'est pas possible de désactiver ce droit pour votre propre catégorie. </dd> {/if} <dd> <input type="radio" name="droit_config" value="{$membres::DROIT_AUCUN}" id="f_droit_config_aucun" {if $cat.droit_config == $membres::DROIT_AUCUN}checked="checked"{/if} {$readonly} /> <label for="f_droit_config_aucun"><b class="aucun">☑</b> Non</label> </dd> <dd> <input type="radio" name="droit_config" value="{$membres::DROIT_ADMIN}" id="f_droit_config_admin" {if $cat.droit_config == $membres::DROIT_ADMIN}checked="checked"{/if} {$readonly} /> <label for="f_droit_config_admin"><b class="admin">☑</b> Oui</label> </dd> </dl> </fieldset> <p class="submit"> {csrf_field key="edit_cat_"|cat:$cat.id} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
| | | | < < < < | < | < < | | | | | < < > > > | | | | > | < < > > | < < < < > > | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | |
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 |
{include file="admin/config/_menu.tpl" current="categories"} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Informations générales</legend> <dl> {input type="text" name="name" label="Nom" required=true source=$cat} <dt>Configuration</dt> {input type="checkbox" name="hidden" label="Catégorie cachée" source=$cat value=1 help="Si coché cette catégorie ne sera visible qu'aux administrateurs et ne recevra pas de messages collectifs ou de rappels"} </dl> </fieldset> <fieldset> <legend>Droits</legend> <dl class="permissions"> {foreach from=$permissions key="type" item="perm"} <dt><label for="f_perm_{$type}_0">{$perm.label}</label></dt> {if $perm.disabled} <dd class="help"> Il n'est pas possible de désactiver ce droit pour votre propre catégorie. </dd> {/if} {foreach from=$perm.options key="level" item="label"} <dd> <input type="radio" name="perm_{$type}" value="{$level}" id="f_perm_{$type}_{$level}" {if $cat->{'perm_' . $type} == $level}checked="checked"{/if} {if $perm.disabled}disabled="disabled"{/if} /> <label for="f_perm_{$type}_{$level}"><b class="access_{$level}">{$perm.shape}</b> {$label}</label> </dd> {/foreach} {/foreach} </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/categories/supprimer.tpl from [f56242fa31] to [b8906b9dfd].
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 |
{include file="admin/_head.tpl" title="Supprimer une catégorie de membre" current="config"} {include file="admin/config/_menu.tpl" current="categories"} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Supprimer la catégorie de membres ?</legend> <h3 class="warning"> Êtes-vous sûr de vouloir supprimer la catégorie « {$cat.nom} » ? </h3> <p class="help"> Attention, la catégorie ne doit plus contenir de membres pour pouvoir être supprimée. </p> <p class="help"> Notez que si des pages du wiki étaient restreintes à la lecture ou à l'écriture aux seuls membres de ce groupe, elles redeviendront lisibles et modifiables par tous les membres ayant accès au wiki ! </p> </fieldset> <p class="submit"> {csrf_field key="delete_cat_"|cat:$cat.id} {button type="submit" name="delete" label="Supprimer" shape="delete" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
| < < < < | < | < < | < < < < < < < < > < < < < < < < |
1 2 3 4 5 6 7 8 9 10 11 |
{include file="admin/_head.tpl" title="Supprimer une catégorie de membre" current="config"} {include file="admin/config/_menu.tpl" current="categories"} {include file="common/delete_form.tpl" legend="Supprimer cette catégorie de membres ?" warning="Êtes-vous sûr de vouloir supprimer la catégorie « %s » ?"|args:$cat.name alert="Attention, la catégorie ne doit plus contenir de membres pour pouvoir être supprimée." info="Les écritures comptables liées à l'historique des membres inscrits à cette activité ne seront pas supprimées, et la comptabilité demeurera inchangée."} {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/donnees/index.tpl from [8251f0824b] to [69b445ed57].
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
..
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
<form method="post" action="{$self_url_no_qs}"> <fieldset> <legend>Téléchargement d'une sauvegarde</legend> <p class="help"> Info : la base de données fait actuellement {$db_size|format_bytes} (dont {$files_size|format_bytes} pour les documents et images). </p> <p class="submit"> {csrf_field key="backup_download"} {button type="submit" name="download" label="Télécharger une copie de la base de données sur mon ordinateur" shape="download" class="main"} </p> </fieldset> ................................................................................ Une sauvegarde des données courantes sera effectuée avant le remplacement, en cas de besoin d'annuler cette restauration. </p> <p> {csrf_field key="backup_restore"} <input type="hidden" name="MAX_FILE_SIZE" value="{$max_file_size}" /> <input type="file" name="file" id="f_file" required="required" /> (maximum {$max_file_size|format_bytes}) </p> <p class="submit"> {button type="submit" name="restore_file" label="Restaurer depuis le fichier sélectionné" shape="upload" class="main"} </p> {if $code && ($code == Sauvegarde::INTEGRITY_FAIL && ALLOW_MODIFIED_IMPORT)} <p> {input type="checkbox" name="force_import" value="1" label="Ignorer les erreurs, je sais ce que je fait"} |
|
|
|
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
..
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
<form method="post" action="{$self_url_no_qs}"> <fieldset> <legend>Téléchargement d'une sauvegarde</legend> <p class="help"> Info : la base de données fait actuellement {$db_size|size_in_bytes} (dont {$files_size|size_in_bytes} pour les documents et images). </p> <p class="submit"> {csrf_field key="backup_download"} {button type="submit" name="download" label="Télécharger une copie de la base de données sur mon ordinateur" shape="download" class="main"} </p> </fieldset> ................................................................................ Une sauvegarde des données courantes sera effectuée avant le remplacement, en cas de besoin d'annuler cette restauration. </p> <p> {csrf_field key="backup_restore"} <input type="hidden" name="MAX_FILE_SIZE" value="{$max_file_size}" /> <input type="file" name="file" id="f_file" required="required" /> (maximum {$max_file_size|size_in_bytes}) </p> <p class="submit"> {button type="submit" name="restore_file" label="Restaurer depuis le fichier sélectionné" shape="upload" class="main"} </p> {if $code && ($code == Sauvegarde::INTEGRITY_FAIL && ALLOW_MODIFIED_IMPORT)} <p> {input type="checkbox" name="force_import" value="1" label="Ignorer les erreurs, je sais ce que je fait"} |
Modified src/templates/admin/config/donnees/local.tpl from [c86c4163a9] to [27a9d3cce8].
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
{else} <table class="list"> <tbody> {foreach from=$list item="backup"} <tr> <td class="check">{input type="radio" name="selected" value=$backup.filename}</td> <th><label for="f_selected_{$backup.filename}">{$backup.name}</label></th> <td>{$backup.size|format_bytes}</td> <td>{$backup.date|date_long}</td> <td>{if !$backup.can_restore}<span class="alert">Version {$backup.version} trop ancienne pour pouvoir être restaurée</span>{/if}</td> <td class="actions"> {linkbutton href="?download=%s"|args:$backup.filename label="Télécharger" shape="download"} </td> </tr> {/foreach} </tbody> |
| | |
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
{else} <table class="list"> <tbody> {foreach from=$list item="backup"} <tr> <td class="check">{input type="radio" name="selected" value=$backup.filename}</td> <th><label for="f_selected_{$backup.filename}">{$backup.name}</label></th> <td>{$backup.size|size_in_bytes}</td> <td>{$backup.date|date_short:true}</td> <td>{if !$backup.can_restore}<span class="alert">Version {$backup.version} trop ancienne pour pouvoir être restaurée</span>{/if}</td> <td class="actions"> {linkbutton href="?download=%s"|args:$backup.filename label="Télécharger" shape="download"} </td> </tr> {/foreach} </tbody> |
Modified src/templates/admin/config/index.tpl from [8115cb5714] to [8588b6ed2f].
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 |
{include file="admin/_head.tpl" title="Configuration" current="config"} {include file="admin/config/_menu.tpl" current="index"} {if $ok && !$form->hasErrors()} <p class="block confirm"> La configuration a bien été enregistrée. </p> {/if} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Garradin</legend> <dl> <dt>Version installée</dt> <dd class="help">{$garradin_version}</dd> {if $new_version} <dd><p class="block alert"> Une nouvelle version <strong>{$new_version}</strong> est disponible !<br /> <a href="{$garradin_website}" target="_blank">Aller télécharger la nouvelle version</a> </p></dd> {/if} {if ENABLE_TECH_DETAILS} <dt>Informations système</dt> <dd class="help"> Version PHP : {$php_version}<br /> Version SQLite : {$sqlite_version}<br /> Heure du serveur : {$server_time|date_fr}<br /> Chiffrement GnuPG : {if $has_gpg_support}disponible, module activé{else}non, module PHP gnupg non installé ?{/if}<br /> </dd> {/if} </dl> </fieldset> <fieldset> <legend>Informations sur l'association</legend> <dl> <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="text" name="nom_asso" id="f_nom_asso" required="required" value="{form_field data=$config name=nom_asso}" /></dd> <dt><label for="f_email_asso">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="email" name="email_asso" id="f_email_asso" required="required" value="{form_field data=$config name=email_asso}" /></dd> <dt><label for="f_adresse_asso">Adresse postale</label></dt> <dd><textarea cols="30" rows="5" name="adresse_asso" id="f_adresse_asso">{form_field data=$config name=adresse_asso}</textarea></dd> <dt><label for="f_site_asso">Site web</label></dt> <dd><input type="url" name="site_asso" id="f_site_asso" value="{form_field name=site_asso data=$config}" /></dd> </dl> </fieldset> <fieldset> <legend>Localisation</legend> <dl> <dt><label for="f_monnaie">Monnaie</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="text" name="monnaie" id="f_monnaie" required="required" value="{form_field name=monnaie data=$config}" size="5" /></dd> <dt><label for="f_pays">Pays</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd> <select name="pays" id="f_pays" required="required"> {foreach from=$pays key="cc" item="nom"} <option value="{$cc}"{if $cc == $config.pays} selected="selected"{/if}>{$nom}</option> {/foreach} </select> </dd> </dl> </fieldset> <fieldset> <legend>Wiki</legend> <dl> <dt><label for="f_accueil_wiki">Page d'accueil du wiki</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd>Indiquer ici l'adresse unique de la page qui sera utilisée comme page d'accueil du wiki.</dd> <dd><input type="text" name="accueil_wiki" id="f_accueil_wiki" required="required" value="{form_field data=$config name=accueil_wiki}" /></dd> <dt><label for="f_accueil_connexion">Page d'accueil à la connexion</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd>Indiquer ici l'adresse unique de la page qui sera affichée à la connexion d'un membre.</dd> <dd><input type="text" name="accueil_connexion" id="f_accueil_connexion" required="required" value="{form_field data=$config name=accueil_connexion}" /></dd> </dl> </fieldset> <fieldset> <legend>Membres</legend> <dl> <dt><label for="f_categorie_membres">Catégorie par défaut des nouveaux membres</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd> <select name="categorie_membres" required="required" id="f_categorie_membres"> {foreach from=$membres_cats key="id" item="nom"} <option value="{$id}"{if $config.categorie_membres == $id} selected="selected"{/if}>{$nom}</option> {/foreach} </select> </dd> <dt><label for="f_champ_identite">Champ utilisé pour définir l'identité des membres</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd class="help">Ce champ des fiches membres sera utilisé comme identité du membre dans les emails, les fiches, les pages, etc.</dd> <dd> <select name="champ_identite" required="required" id="f_champ_identite"> {foreach from=$champs key="c" item="champ"} <option value="{$c}" {form_field selected=$c name="champ_identite" data=$config}>{$champ.title}</option> {/foreach} </select> </dd> <dt><label for="f_champ_identifiant">Champ utilisé comme identifiant de connexion</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd class="help">Ce champ des fiches membres sera utilisé en guise d'identifiant pour se connecter à Garradin. Pour cela le champ doit être unique (pas de doublons).</dd> <dd> <select name="champ_identifiant" required="required" id="f_champ_identifiant"> {foreach from=$champs key="c" item="champ"} <option value="{$c}" {form_field selected=$c name="champ_identifiant" data=$config}>{$champ.title}</option> {/foreach} </select> </dd> </dl> </fieldset> <fieldset id="couleurs"> <legend>Personnalisation de l'interface</legend> <dl> {input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$couleur1 name="couleur1" label="Couleur primaire" placeholder=$couleurs_defaut[0]} {input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$couleur2 name="couleur2" label="Couleur secondaire" placeholder=$couleurs_defaut[1]} {input type="file" label="Image de fond" name="background" help="Il est conseillé d'utiliser une image en noir et blanc avec un fond blanc pour un meilleur rendu. Dimensions recommandées : 380x200" accept="image/*,*.jpeg,*.jpg,*.png,*.gif"} </dl> <input type="hidden" name="image_fond" id="f_image_fond" data-source="{$background_image_source}" data-default="{$background_image_default}" value="{form_field name=image_fond}" /> </fieldset> <p class="submit"> {csrf_field key="config"} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | < < < | | | | | < < < < < < < < < < > > | | | | | < < < < < < < < > > > | | | < > | < > | < < < < < > | < < < < < < < < > > | < < < < < < < < < < < < < < < < | | | | < > | | | | | |
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 |
{include file="admin/_head.tpl" title="Configuration" current="config"} {include file="admin/config/_menu.tpl" current="index"} {if isset($_GET['ok']) && !$form->hasErrors()} <p class="block confirm"> La configuration a bien été enregistrée. </p> {/if} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Garradin</legend> <dl> <dt>Version installée</dt> <dd class="help">{$garradin_version}</dd> {if $new_version} <dd><p class="block alert"> Une nouvelle version <strong>{$new_version}</strong> est disponible !<br /> <a href="{$garradin_website}" target="_blank">Aller télécharger la nouvelle version</a> </p></dd> {/if} {if ENABLE_TECH_DETAILS} <dt>Informations système</dt> <dd class="help"> Version PHP : {$php_version}<br /> Version SQLite : {$sqlite_version}<br /> Heure du serveur : {$server_time|date}<br /> Chiffrement GnuPG : {if $has_gpg_support}disponible, module activé{else}non, module PHP gnupg non installé ?{/if}<br /> </dd> {/if} </dl> </fieldset> <fieldset> <legend>Informations sur l'association</legend> <dl> {input type="text" name="nom_asso" required=true source=$config label="Nom"} {input type="email" name="email_asso" required=true source=$config label="Adresse e-mail de contact"} {input type="textarea" name="adresse_asso" source=$config label="Adresse postale"} {input type="tel" name="telephone_asso" source=$config label="Numéro de téléphone"} {input type="url" name="site_asso" source=$config label="Site web" help="Si vous n'utilisez pas la fonctionnalité site web de Garradin"} </dl> </fieldset> <fieldset> <legend>Localisation</legend> <dl> {input type="text" name="monnaie" required=true source=$config label="Monnaie" help="Inscrire ici la devise utilisée : €, CHF, XPF, etc." size="3"} {input type="select" name="pays" required=true source=$config label="Pays" options=$countries} </dl> </fieldset> <fieldset> <legend>Membres</legend> <dl> {input type="select" name="categorie_membres" source=$config options=$membres_cats required=true label="Catégorie par défaut des nouveaux membres"} {input type="select" name="champ_identite" source=$config options=$champs required=true label="Champ utilisé pour définir l'identité des membres" help="Ce champ des fiches membres sera utilisé comme identité du membre dans les emails, les fiches, les pages, etc."} {input type="select" name="champ_identifiant" source=$config options=$champs required=true label="Champ utilisé comme identifiant de connexion" help="Ce champ des fiches membres sera utilisé comme identifiant pour se connecter à Garradin. Ce champ doit être unique (il ne peut pas contenir deux membres ayant la même valeur dans ce champ)."} </dl> </fieldset> <fieldset> <legend>Personnalisation</legend> <dl> <dt>Texte de la page d'accueil</dt> <dd> {linkbutton href="!common/files/edit.php?p=%s"|args:$config.admin_homepage->path() label="Modifier" shape="edit" target="_dialog" data-dialog-height="90%"} </dd> <dd class="help"> Ce contenu sera affiché à la connexion d'un membre, ou en cliquant sur l'onglet 'Accueil' du menu de gauche. </dd> {input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$color1 source=$config name="couleur1" label="Couleur primaire" placeholder=$color1} {input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$color2 source=$config name="couleur2" label="Couleur secondaire" placeholder=$color2} {input type="file" label="Image de fond" name="background" help="Il est conseillé d'utiliser une image en noir et blanc avec un fond blanc pour un meilleur rendu. Dimensions recommandées : 380x200" accept="image/*,*.jpeg,*.jpg,*.png,*.gif"} </dl> <input type="hidden" name="image_fond" id="f_image_fond" data-current="{$background_image_current}" data-default="{$background_image_default}" value="{$_POST.image_fond}" /> </fieldset> <p class="submit"> {csrf_field key="config"} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/membres.tpl from [5a8266eee1] to [563b1fbac5].
1 2 3 4 5 6 7 8 |
{include file="admin/_head.tpl" current="config" custom_css=['styles/config.css']}
{include file="admin/config/_menu.tpl" current="fiches_membres"}
{if isset($status) && $status == 'OK'}
<p class="block confirm">
La configuration a bien été enregistrée.
</p>
|
| |
1 2 3 4 5 6 7 8 |
{include file="admin/_head.tpl" current="config" custom_css=['config.css']} {include file="admin/config/_menu.tpl" current="fiches_membres"} {if isset($status) && $status == 'OK'} <p class="block confirm"> La configuration a bien été enregistrée. </p> |
Deleted src/templates/admin/config/site.tpl version [2454b9048e].
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 |
{include file="admin/_head.tpl" title="Configuration — Site public" current="config"} {form_errors} {include file="admin/config/_menu.tpl" current="site"} {if $config.desactiver_site} <div class="block alert"> <h3>Site public désactivé</h3> <p>Le site public est désactivé, les visiteurs sont redirigés automatiquement vers la page de connexion.</p> <form method="post" action="{$self_url}"> <p class="submit"> {csrf_field key="config_site"} {button type="submit" name="activer_site" label="Réactiver le site public" shape="right" class="main"} </p> </form> </div> {elseif isset($edit)} <form method="post" action="{$self_url}"> <h3>Éditer un squelette</h3> {if $ok} <p class="block confirm"> Modifications enregistrées. </p> {/if} <fieldset class="skelEdit"> <legend>{$edit.file}</legend> <p> <textarea name="content" cols="90" rows="50" id="f_content">{form_field name=content data=$edit}</textarea> </p> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> <script type="text/javascript"> var doc_url = "{$admin_url}doc/skel/"; var skel_list = {$sources|escape:json}; var skel_current = "{$edit.file|escape:'js'}"; g.script("scripts/skel_editor.js"); </script> {else} <fieldset> <legend>Activation du site public</legend> <dl> <dt> <form method="post" action="{$self_url}"> <p class="submit"> {button type="submit" name="desactiver_site" label="Désactiver le site public" shape="right" class="main"} {csrf_field key="config_site"} </p> </form> </dt> <dd class="help"> En désactivant le site public, les visiteurs seront automatiquement redirigés vers la page de connexion.<br /> Cette option est utile si vous avez déjà un site web et ne souhaitez pas utiliser la fonctionnalité site web de Garradin. </dd> </dl> </fieldset> <fieldset> <legend>Gérer le contenu du site public</legend> <p class="help"> Le contenu affiché sur le site est celui présent dans le wiki, il suffit de sélectionner « Cette page est visible sur le site de l'association » à l'édition d'une page wiki. Il est également possible de <a href="{$admin_url}wiki/creer.php?public">créer une nouvelle page publique sur le wiki</a>. </p> </fieldset> <form method="post" action="{$self_url}"> <fieldset class="templatesList"> <legend>Squelettes du site</legend> {if $reset_ok} <p class="block confirm"> Réinitialisation effectuée. Les squelettes ont été remis à jour </p> {/if} <table class="list"> <thead> <tr> <td class="check"></td> <th>Fichier</th> <td>Dernière modification</td> <td></td> </tr> </thead> <tbody> {foreach from=$sources key="source" item="local"} <tr> <td>{if $local && $local.dist}<input type="checkbox" name="select[]" value="{$source}" id="f_source_{$iteration}" /><label for="f_source_{$iteration}"></label>{/if}</td> <th><a href="{$admin_url}config/site.php?edit={$source|escape:'url'}" title="Éditer">{$source}</a></th> <td>{if $local}{$local.mtime|date_fr:'d/m/Y à H:i:s'}{else}<em>(fichier non modifié)</em>{/if}</td> <td class="actions"> {linkbutton shape="edit" label="Éditer" href="?edit=%s"|args:$source} </td> </tr> {/foreach} </tbody> </table> <p class="actions"> Pour les squelettes sélectionnés : <input type="submit" name="reset" value="Réinitialiser" onclick="return confirm('Effacer toute modification locale et restaurer les squelettes d\'installation ?');" /> {csrf_field key="squelettes"} </p> </fieldset> </form> {/if} {include file="admin/_foot.tpl"} |
< < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/templates/admin/index.tpl from [8ad9228981] to [8fc4b88474].
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 |
{include file="admin/_head.tpl" title="Bonjour %s !"|args:$user.identite current="home"} {$banniere|raw} <nav class="tabs"> <ul> <li><a href="{$admin_url}mes_infos.php">Modifier mes informations personnelles</a></li> <li><a href="{$admin_url}my_services.php">Suivi de mes activités et cotisations</a></li> </ul> </nav> <aside class="describe"> <h3>{$config.nom_asso}</h3> {if !empty($config.adresse_asso)} <p> {$config.adresse_asso|escape|nl2br} </p> {/if} {if !empty($config.email_asso)} <p> E-Mail : <a href="mailto:{$config.email_asso}">{$config.email_asso}</a> </p> {/if} {if !empty($config.site_asso)} <p> Web : <a href="{$config.site_asso}" target="_blank">{$config.site_asso}</a> </p> {/if} </aside> <div class="wikiContent"> {$page.contenu.contenu|raw|format_wiki|liens_wiki:'wiki/?'} </div> {include file="admin/_foot.tpl"} |
| | | | | | | | | | | > > > > > | | | | | | | | | | < < < > > > > > |
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 |
{include file="admin/_head.tpl" title="Bonjour %s !"|args:$user.identite current="home"} {$banner|raw} <nav class="tabs"> <ul> <li><a href="{$admin_url}mes_infos.php">Modifier mes informations personnelles</a></li> <li><a href="{$admin_url}my_services.php">Suivi de mes activités et cotisations</a></li> </ul> </nav> <aside class="describe"> <h3>{$config.nom_asso}</h3> {if !empty($config.adresse_asso)} <p> {$config.adresse_asso|escape|nl2br} </p> {/if} {if !empty($config.telephone_asso)} <p> Tél. : <a href="tel:{$config.telephone_asso}">{$config.telephone_asso}</a> </p> {/if} {if !empty($config.email_asso)} <p> E-Mail : <a href="mailto:{$config.email_asso}">{$config.email_asso}</a> </p> {/if} {if !empty($config.site_asso)} <p> Web : <a href="{$config.site_asso}" target="_blank">{$config.site_asso}</a> </p> {/if} </aside> {if $homepage} <article class="web-content"> {$homepage|raw} </article> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/admin/install.tpl from [e0d35a0a40] to [57684a4a9c].
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 |
{include file="admin/_head.tpl" title="Garradin - Installation"} {if $disabled} <p class="block error">Garradin est déjà installé.</p> {else} <p class="help"> Bienvenue dans Garradin ! Veuillez remplir les quelques informations suivantes pour terminer l'installation. </p> {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Informations sur l'association</legend> <dl> <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="text" name="nom_asso" id="f_nom_asso" required="required" value="{form_field name=nom_asso}" /></dd> </dl> </fieldset> <fieldset> <legend>Création du compte administrateur</legend> <dl> <dt><label for="f_nom_membre">Nom et prénom</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="text" name="nom_membre" id="f_nom_membre" required="required" value="{form_field name=nom_membre}" /></dd> <dt><label for="f_email_membre">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="email" name="email_membre" id="f_email_membre" required="required" value="{form_field name=email_membre}" /></dd> <dt><label for="f_passe_membre">Mot de passe</label> (minimum {$password_length} caractères) <b title="(Champ obligatoire)">obligatoire</b></dt> <dd class="help"> Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres. </dd> <dd class="help"> Pas d'idée ? Voici une suggestion choisie au hasard : <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="pw_suggest" value="{$passphrase}" autocomplete="off" /> </dd> <dd><input type="password" name="passe" id="f_passe_membre" value="{form_field name=passe}" pattern="{$password_pattern}" required="required" autocomplete="off" /></dd> <dt><label for="f_repasse_membre">Encore le mot de passe</label> (vérification) <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="password" name="passe_confirmed" id="f_repasse_membre" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" required="required" autocomplete="off" /></dd> </dl> </fieldset> <p class="submit"> {csrf_field key="install"} {button type="submit" name="save" label="Terminer l'installation" shape="right" class="main"} </p> <script type="text/javascript" src="{$admin_url}static/scripts/loader.js"></script> <script type="text/javascript"> {literal} g.script('scripts/password.js', () => { initPasswordField('pw_suggest', 'f_passe_membre', 'f_repasse_membre'); }); var form = $('form')[0]; form.onsubmit = function () { $('#f_submit').style.opacity = 0; var loader = document.createElement('div'); loader.className = 'loader install'; loader.innerHTML = '<b>Garradin est en cours d\'installation…</b>'; $('#f_submit').parentNode.appendChild(loader); animatedLoader(loader, 5); }; {/literal} </script> </form> {/if} {include file="admin/_foot.tpl"} |
| < < < | | | | | | | | | | < < > | | | | | < < < < < < < < < < < < < < < < > > > | | | | | | | | | | | | | | | | | | | | | | | | < > |
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 |
{include file="admin/_head.tpl" title="Garradin - Installation" menu=false} <p class="help"> Bienvenue dans Garradin ! Veuillez remplir les quelques informations suivantes pour terminer l'installation. </p> {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Informations sur l'association</legend> <dl> {input type="text" label="Nom de l'association" required=true name="name"} </dl> </fieldset> <fieldset> <legend>Création du compte administrateur</legend> <dl> {input type="text" label="Nom et prénom" required=true name="user_name"} {input type="email" label="Adresse E-Mail" required=true name="user_email"} {password_change label="Mot de passe" required=true name="user_password"} </dl> </fieldset> <p class="submit"> {csrf_field key="install"} {button type="submit" name="save" label="Terminer l'installation" shape="right" class="main"} </p> <script type="text/javascript" src="{$admin_url}static/scripts/loader.js"></script> <script type="text/javascript"> {literal} g.script('scripts/password.js', () => { initPasswordField('user_password'); }); var form = $('form')[0]; form.onsubmit = function () { $('#f_submit').style.opacity = 0; var loader = document.createElement('div'); loader.className = 'loader install'; loader.innerHTML = '<b>Garradin est en cours d\'installation…</b>'; $('#f_submit').parentNode.appendChild(loader); animatedLoader(loader, 5); }; {/literal} </script> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/membres/_list_actions.tpl from [143fb71a16] to [158163abdd].
1 2 3 4 5 6 7 8 9 10 |
<tfoot>
<tr>
{if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td>{/if}
<td class="actions" colspan="{$colspan}">
<em>Pour les membres cochés :</em>
{csrf_field key="membres_action"}
<select name="action">
<option value="">— Choisir une action à effectuer —</option>
<option value="move">Changer de catégorie</option>
<option value="csv">Exporter en tableau CSV</option>
|
| |
1 2 3 4 5 6 7 8 9 10 |
<tfoot>
<tr>
{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td>{/if}
<td class="actions" colspan="{$colspan}">
<em>Pour les membres cochés :</em>
{csrf_field key="membres_action"}
<select name="action">
<option value="">— Choisir une action à effectuer —</option>
<option value="move">Changer de catégorie</option>
<option value="csv">Exporter en tableau CSV</option>
|
Modified src/templates/admin/membres/_nav.tpl from [419563ac94] to [bb7c40e577].
1 2 3 4 5 6 7 8 9 10 |
<nav class="tabs">
<ul>
<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}membres/">Liste des membres</a></li>
<li{if $current == 'recherche'} class="current"{/if}><a href="{$admin_url}membres/recherche.php">Recherche avancée</a></li>
<li{if $current == 'recherches'} class="current"{/if}><a href="{$admin_url}membres/recherches.php">Recherches enregistrées</a></li>
{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
<li{if $current == 'import'} class="current"{/if}><a href="{$admin_url}membres/import.php">Import & export</a></li>
{/if}
</ul>
</nav>
|
| |
1 2 3 4 5 6 7 8 9 10 |
<nav class="tabs">
<ul>
<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}membres/">Liste des membres</a></li>
<li{if $current == 'recherche'} class="current"{/if}><a href="{$admin_url}membres/recherche.php">Recherche avancée</a></li>
<li{if $current == 'recherches'} class="current"{/if}><a href="{$admin_url}membres/recherches.php">Recherches enregistrées</a></li>
{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}
<li{if $current == 'import'} class="current"{/if}><a href="{$admin_url}membres/import.php">Import & export</a></li>
{/if}
</ul>
</nav>
|
Modified src/templates/admin/membres/ajouter.tpl from [e4d16e8863] to [4bd767586d].
1
2
3
4
5
6
7
8
9
10
11
12
..
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
{include file="admin/_head.tpl" title="Ajouter un membre" current="membres/ajouter"} {form_errors} <form method="post" action="{$self_url}"> <!-- This is to avoid chrome autofill, Chrome developers you suck --> <input type="text" style="display: none;" name="email" /> {if $id_field_name != 'email'}<input type="text" style="display: none;" name="{$id_field_name}" />{/if} <input type="password" style="display: none;" name="password" /> <fieldset> <legend>Informations personnelles</legend> ................................................................................ </dd> <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern="{$password_pattern}" autocomplete="new-password" /></dd> <dt><label for="f_repasse">Encore le mot de passe</label> (vérification)</dt> <dd><input type="password" name="passe_confirmed" id="f_repasse" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" autocomplete="new-password" /></dd> </dl> </fieldset> {if $session->canAccess('membres', Membres::DROIT_ADMIN)} <fieldset> <legend>Général</legend> <dl> <dt><label for="f_cat">Catégorie du membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd> <select name="id_categorie" id="f_cat"> {foreach from=$membres_cats key="id" item="nom"} <option value="{$id}"{if $current_cat == $id} selected="selected"{/if}>{$nom}</option> {/foreach} </select> </dd> </dl> </fieldset> |
|
|
|
|
1
2
3
4
5
6
7
8
9
10
11
12
..
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
{include file="admin/_head.tpl" title="Ajouter un membre" current="membres/ajouter"} {form_errors} <form method="post" action="{$self_url}" enctype="multipart/form-data"> <!-- This is to avoid chrome autofill, Chrome developers you suck --> <input type="text" style="display: none;" name="email" /> {if $id_field_name != 'email'}<input type="text" style="display: none;" name="{$id_field_name}" />{/if} <input type="password" style="display: none;" name="password" /> <fieldset> <legend>Informations personnelles</legend> ................................................................................ </dd> <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern="{$password_pattern}" autocomplete="new-password" /></dd> <dt><label for="f_repasse">Encore le mot de passe</label> (vérification)</dt> <dd><input type="password" name="passe_confirmed" id="f_repasse" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" autocomplete="new-password" /></dd> </dl> </fieldset> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} <fieldset> <legend>Général</legend> <dl> <dt><label for="f_cat">Catégorie du membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd> <select name="category_id" id="f_cat"> {foreach from=$membres_cats key="id" item="nom"} <option value="{$id}"{if $current_cat == $id} selected="selected"{/if}>{$nom}</option> {/foreach} </select> </dd> </dl> </fieldset> |
Modified src/templates/admin/membres/fiche.tpl from [4cc42f5c24] to [119c83684f].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .. 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 .. 73 74 75 76 77 78 79 80 81 82 83 84 85 86 .. 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
{include file="admin/_head.tpl" title="%s (%s)"|args:$membre.identite:$categorie.nom current="membres"} <nav class="tabs"> <ul> <li class="current"><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li> {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}<li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>{/if} {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id} <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li> {/if} </ul> </nav> <dl class="cotisation"> <dt>Activités et cotisations</dt> ................................................................................ Ce membre n'est inscrit à aucune activité ou cotisation. </dd> {/foreach} <dd> {if count($services)} {linkbutton href="!services/user.php?id=%d"|args:$membre.id label="Liste des inscriptions aux activités" shape="menu"} {/if} {if $session->canAccess('membres', Membres::DROIT_ECRITURE)} {linkbutton href="!services/save.php?user=%d"|args:$membre.id label="Inscrire à une activité" shape="plus"} {/if} </dd> {if $session->canAccess('membres', Membres::DROIT_ACCES)} {if !empty($transactions_linked)} <dt>Écritures comptables liées</dt> <dd><a href="{$admin_url}acc/transactions/user.php?id={$membre.id}">{$transactions_linked} écritures comptables liées à ce membre</a></dd> {/if} {if !empty($transactions_created)} <dt>Écritures comptables créées</dt> <dd><a href="{$admin_url}acc/transactions/creator.php?id={$membre.id}">{$transactions_created} écritures comptables créées par ce membre</a></dd> ................................................................................ {/if} {/if} </dl> <aside class="describe"> <dl class="describe"> <dt>Catégorie</dt> <dd>{$categorie.nom} <span class="droits">{format_droits droits=$categorie}</span></dd> <dt>Inscription</dt> <dd>{$membre.date_inscription|date_short}</dd> <dt>Dernière connexion</dt> <dd>{if empty($membre.date_connexion)}Jamais{else}{$membre.date_connexion|date_long}{/if}</dd> <dt>Mot de passe</dt> <dd> {if empty($membre.passe)} Pas de mot de passe configuré {else} <b class="icn">☑</b> Oui {if !empty($membre.secret_otp)} ................................................................................ <dl class="describe"> {foreach from=$champs key="c" item="c_config"} <dt>{$c_config.title}</dt> <dd> {if $c_config.type == 'checkbox'} {if $membre->$c}Oui{else}Non{/if} {elseif empty($membre->$c)} <em>(Non renseigné)</em> {elseif $c == $c_config.champ_identite} <strong>{$membre->$c}</strong> {elseif $c_config.type == 'email'} <a href="mailto:{$membre->$c|escape:'url'}">{$membre->$c}</a> {if $c == 'email'} ................................................................................ {elseif $c_config.type == 'tel'} <a href="tel:{$membre->$c}">{$membre->$c|format_tel}</a> {elseif $c_config.type == 'country'} {$membre->$c|get_country_name} {elseif $c_config.type == 'date'} {$membre->$c|date_short} {elseif $c_config.type == 'datetime'} {$membre->$c|date_fr} {elseif $c_config.type == 'password'} ******* {elseif $c_config.type == 'multiple'} <ul> {foreach from=$c_config.options key="b" item="name"} {if $membre->$c & (0x01 << $b)} <li>{$name}</li> |
| | | | | | | > > > > > > | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .. 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 .. 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 .. 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
{include file="admin/_head.tpl" title="%s (%s)"|args:$membre.identite:$category.name current="membres"} <nav class="tabs"> <ul> <li class="current"><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)}<li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>{/if} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN) && $user.id != $membre.id} <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li> {/if} </ul> </nav> <dl class="cotisation"> <dt>Activités et cotisations</dt> ................................................................................ Ce membre n'est inscrit à aucune activité ou cotisation. </dd> {/foreach} <dd> {if count($services)} {linkbutton href="!services/user.php?id=%d"|args:$membre.id label="Liste des inscriptions aux activités" shape="menu"} {/if} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} {linkbutton href="!services/save.php?user=%d"|args:$membre.id label="Inscrire à une activité" shape="plus"} {/if} </dd> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)} {if !empty($transactions_linked)} <dt>Écritures comptables liées</dt> <dd><a href="{$admin_url}acc/transactions/user.php?id={$membre.id}">{$transactions_linked} écritures comptables liées à ce membre</a></dd> {/if} {if !empty($transactions_created)} <dt>Écritures comptables créées</dt> <dd><a href="{$admin_url}acc/transactions/creator.php?id={$membre.id}">{$transactions_created} écritures comptables créées par ce membre</a></dd> ................................................................................ {/if} {/if} </dl> <aside class="describe"> <dl class="describe"> <dt>Catégorie</dt> <dd>{$category.name} <span class="permissions">{display_permissions permissions=$category}</span></dd> <dt>Inscription</dt> <dd>{$membre.date_inscription|date_short}</dd> <dt>Dernière connexion</dt> <dd>{if empty($membre.date_connexion)}Jamais{else}{$membre.date_connexion|date_short:true}{/if}</dd> <dt>Mot de passe</dt> <dd> {if empty($membre.passe)} Pas de mot de passe configuré {else} <b class="icn">☑</b> Oui {if !empty($membre.secret_otp)} ................................................................................ <dl class="describe"> {foreach from=$champs key="c" item="c_config"} <dt>{$c_config.title}</dt> <dd> {if $c_config.type == 'checkbox'} {if $membre->$c}Oui{else}Non{/if} {elseif $c_config.type == 'file'} <?php $files = $membre->$c ? [$membre->$c] : []; $can_upload = !count($files) && $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE); ?> {include file="common/files/_context_list.tpl" files=$files can_upload=$can_upload parent_path="%s/%s"|args:$user_files_path,$c} {elseif empty($membre->$c)} <em>(Non renseigné)</em> {elseif $c == $c_config.champ_identite} <strong>{$membre->$c}</strong> {elseif $c_config.type == 'email'} <a href="mailto:{$membre->$c|escape:'url'}">{$membre->$c}</a> {if $c == 'email'} ................................................................................ {elseif $c_config.type == 'tel'} <a href="tel:{$membre->$c}">{$membre->$c|format_tel}</a> {elseif $c_config.type == 'country'} {$membre->$c|get_country_name} {elseif $c_config.type == 'date'} {$membre->$c|date_short} {elseif $c_config.type == 'datetime'} {$membre->$c|date} {elseif $c_config.type == 'password'} ******* {elseif $c_config.type == 'multiple'} <ul> {foreach from=$c_config.options key="b" item="name"} {if $membre->$c & (0x01 << $b)} <li>{$name}</li> |
Modified src/templates/admin/membres/import.tpl from [203c781562] to [dc72822dd3].
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
{else}
<fieldset>
<legend>Importer depuis un fichier</legend>
<dl>
<dt><label for="f_file">Fichier à importer</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
<dd class="help">La taille maximale du fichier est de {$max_upload_size|format_bytes}.</dd>
<dd><input type="file" name="upload" id="f_file" required="required" /></dd>
<dt><label for="f_type">Type de fichier</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
<dd>
<input type="radio" name="type" id="f_type" value="garradin" {form_field name=type checked="garradin" default="garradin"} />
<label for="f_type">Fichier CSV de Garradin</label>
</dd>
<dd class="help">
|
| |
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
{else}
<fieldset>
<legend>Importer depuis un fichier</legend>
<dl>
<dt><label for="f_file">Fichier à importer</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
<dd class="help">La taille maximale du fichier est de {$max_upload_size|size_in_bytes}.</dd>
<dd><input type="file" name="upload" id="f_file" required="required" /></dd>
<dt><label for="f_type">Type de fichier</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
<dd>
<input type="radio" name="type" id="f_type" value="garradin" {form_field name=type checked="garradin" default="garradin"} />
<label for="f_type">Fichier CSV de Garradin</label>
</dd>
<dd class="help">
|
Modified src/templates/admin/membres/index.tpl from [5aaf76eac9] to [ea21066fdc].
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
..
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
{if !empty($categories)} <form method="get" action="{$self_url}" class="shortFormRight"> <fieldset> <legend>Filtrer par catégorie</legend> <select name="cat" id="f_cat" onchange="this.form.submit();"> <option value="0" {if $current_cat == 0} selected="selected"{/if}>-- Toutes</option> {foreach from=$categories key="id" item="nom"} {if $session->canAccess('membres', Membres::DROIT_ECRITURE) || !array_key_exists($id, $hidden_categories)} <option value="{$id}"{if $current_cat == $id} selected="selected"{/if}>{$nom}</option> {/if} {/foreach} </select> <noscript><input type="submit" value="Filtrer →" /></noscript> </fieldset> ................................................................................ {if $key == $id_field}</a>{/if} </td> {/if} {/foreach} <td class="actions"> {linkbutton label="Fiche membre" shape="user" href="!membres/fiche.php?id=%d"|args:$row._user_id} {if $session->canAccess('membres', Membres::DROIT_ECRITURE)} {linkbutton label="Modifier" shape="edit" href="!membres/modifier.php?id=%d"|args:$row._user_id} {/if} </td> </tr> {/foreach} </tbody> {if $session->canAccess('membres', Membres::DROIT_ADMIN)} {include file="admin/membres/_list_actions.tpl" colspan=count($list->getHeaderColumns())+$can_edit+1} {/if} </table> {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} {else} |
|
|
|
|
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
..
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
{if !empty($categories)} <form method="get" action="{$self_url}" class="shortFormRight"> <fieldset> <legend>Filtrer par catégorie</legend> <select name="cat" id="f_cat" onchange="this.form.submit();"> <option value="0" {if $current_cat == 0} selected="selected"{/if}>-- Toutes</option> {foreach from=$categories key="id" item="nom"} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE) || !array_key_exists($id, $hidden_categories)} <option value="{$id}"{if $current_cat == $id} selected="selected"{/if}>{$nom}</option> {/if} {/foreach} </select> <noscript><input type="submit" value="Filtrer →" /></noscript> </fieldset> ................................................................................ {if $key == $id_field}</a>{/if} </td> {/if} {/foreach} <td class="actions"> {linkbutton label="Fiche membre" shape="user" href="!membres/fiche.php?id=%d"|args:$row._user_id} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} {linkbutton label="Modifier" shape="edit" href="!membres/modifier.php?id=%d"|args:$row._user_id} {/if} </td> </tr> {/foreach} </tbody> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} {include file="admin/membres/_list_actions.tpl" colspan=count($list->getHeaderColumns())+$can_edit+1} {/if} </table> {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} {else} |
Modified src/templates/admin/membres/modifier.tpl from [be24573e1c] to [1d6b81c0d9].
1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
{include file="admin/_head.tpl" title="Modifier un membre" current="membres"} <nav class="tabs"> <ul> <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li> <li class="current"><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li> {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id} <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li> {/if} </ul> </nav> {form_errors} ................................................................................ {if $membre.clef_pgp} {input type="checkbox" name="clear_pgp" value="1" label="Supprimer la clé PGP associée au membre"} {/if} </dl> </fieldset> {/if} {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id} <fieldset> <legend>Général</legend> <dl> <dt><label for="f_cat">Catégorie du membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd> <select name="id_categorie" id="f_cat"> {foreach from=$membres_cats key="id" item="nom"} <option value="{$id}"{if $current_cat == $id} selected="selected"{/if}>{$nom}</option> {/foreach} </select> </dd> </dl> </fieldset> |
|
|
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
{include file="admin/_head.tpl" title="Modifier un membre" current="membres"} <nav class="tabs"> <ul> <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li> <li class="current"><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN) && $user.id != $membre.id} <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li> {/if} </ul> </nav> {form_errors} ................................................................................ {if $membre.clef_pgp} {input type="checkbox" name="clear_pgp" value="1" label="Supprimer la clé PGP associée au membre"} {/if} </dl> </fieldset> {/if} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN) && $user.id != $membre.id} <fieldset> <legend>Général</legend> <dl> <dt><label for="f_cat">Catégorie du membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd> <select name="category_id" id="f_cat"> {foreach from=$membres_cats key="id" item="nom"} <option value="{$id}"{if $current_cat == $id} selected="selected"{/if}>{$nom}</option> {/foreach} </select> </dd> </dl> </fieldset> |
Modified src/templates/admin/membres/recherche.tpl from [5a5c80b31f] to [7ed26a1626].
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
..
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
|
{include file="admin/_head.tpl" title="Recherche de membre" current="membres" custom_js=['query_builder.min.js']} {include file="admin/membres/_nav.tpl" current="recherche"} {include file="common/search/advanced.tpl" action_url=$self_url} {if !empty($result)} {if $session->canAccess('membres', Membres::DROIT_ECRITURE)} <form method="post" action="{$admin_url}membres/action.php" class="memberList"> {/if} <p class="help">{$result|count} membres trouvés pour cette recherche.</p> <table class="list search"> <thead> <tr> {if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>{/if} {foreach from=$result_header item="label"} <td>{$label}</td> {/foreach} <td></td> </tr> </thead> <tbody> {foreach from=$result item="row"} <tr> {if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check">{if $row._user_id}{input type="checkbox" name="selected[]" value=$row._user_id}{/if}</td>{/if} {foreach from=$row key="key" item="value"} <?php $link = false; ?> {if isset($result_header[$key])} <td> {if !$link && $row._user_id} <a href="{$admin_url}membres/fiche.php?id={$row._user_id}"> {/if} ................................................................................ {elseif substr($key, 0, 1) != '_'} <td>{$value}</td> {/if} {/foreach} <td class="actions"> {if $row._user_id} {linkbutton shape="user" label="Fiche membre" href="!membres/fiche.php?id=%d"|args:$row._user_id} {if $session->canAccess('membres', Membres::DROIT_ECRITURE)} {linkbutton shape="edit" label="Modifier" href="!membres/modifier.php?id=%d"|args:$row._user_id} {/if} {/if} </td> </tr> {/foreach} </tbody> {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $row._user_id} {include file="admin/membres/_list_actions.tpl" colspan=count($result_header)+1} {/if} </table> {if $session->canAccess('membres', Membres::DROIT_ECRITURE)} </form> {/if} {elseif $result !== null} <p class="block alert"> Aucun membre trouvé. |
|
|
|
|
|
|
|
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
..
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
|
{include file="admin/_head.tpl" title="Recherche de membre" current="membres" custom_js=['query_builder.min.js']} {include file="admin/membres/_nav.tpl" current="recherche"} {include file="common/search/advanced.tpl" action_url=$self_url} {if !empty($result)} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} <form method="post" action="{$admin_url}membres/action.php" class="memberList"> {/if} <p class="help">{$result|count} membres trouvés pour cette recherche.</p> <table class="list search"> <thead> <tr> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>{/if} {foreach from=$result_header item="label"} <td>{$label}</td> {/foreach} <td></td> </tr> </thead> <tbody> {foreach from=$result item="row"} <tr> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}<td class="check">{if $row._user_id}{input type="checkbox" name="selected[]" value=$row._user_id}{/if}</td>{/if} {foreach from=$row key="key" item="value"} <?php $link = false; ?> {if isset($result_header[$key])} <td> {if !$link && $row._user_id} <a href="{$admin_url}membres/fiche.php?id={$row._user_id}"> {/if} ................................................................................ {elseif substr($key, 0, 1) != '_'} <td>{$value}</td> {/if} {/foreach} <td class="actions"> {if $row._user_id} {linkbutton shape="user" label="Fiche membre" href="!membres/fiche.php?id=%d"|args:$row._user_id} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} {linkbutton shape="edit" label="Modifier" href="!membres/modifier.php?id=%d"|args:$row._user_id} {/if} {/if} </td> </tr> {/foreach} </tbody> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN) && $row._user_id} {include file="admin/membres/_list_actions.tpl" colspan=count($result_header)+1} {/if} </table> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} </form> {/if} {elseif $result !== null} <p class="block alert"> Aucun membre trouvé. |
Modified src/templates/admin/membres/selector.tpl from [6df59fc8ca] to [d560c1b60a].
1 2 3 4 5 6 7 8 9 10 |
{include file="admin/_head.tpl" title="Sélectionner un compte" body_id="popup" is_popup=true} <form method="get" action="{$self_url_no_qs}"> <h2 class="ruler"> <input type="text" placeholder="Recherche rapide de membre" value="{$query}" name="q" /> <input type="submit" value="Chercher →" /> </h2> </form> <table class="list"> |
| | |
1 2 3 4 5 6 7 8 9 10 |
{include file="admin/_head.tpl" title="Sélectionner un compte"}
<form method="post" action="{$self_url}">
<h2 class="ruler">
<input type="text" placeholder="Recherche rapide de membre" value="{$query}" name="q" />
<input type="submit" value="Chercher →" />
</h2>
</form>
<table class="list">
|
Modified src/templates/admin/membres/supprimer.tpl from [82816cbfa7] to [9188bf5497].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{include file="admin/_head.tpl" title="Supprimer un membre" current="membres"}
<nav class="tabs">
<ul>
<li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li>
<li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
<li class="current"><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
{/if}
</ul>
</nav>
{form_errors}
|
| |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{include file="admin/_head.tpl" title="Supprimer un membre" current="membres"}
<nav class="tabs">
<ul>
<li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li>
<li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}
<li class="current"><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
{/if}
</ul>
</nav>
{form_errors}
|
Modified src/templates/admin/mes_infos_securite.tpl from [99becf7d6d] to [f732fceed2].
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
</form>
{else}
<form method="post" action="{$self_url_no_qs}">
<fieldset>
<legend>Changer mon mot de passe</legend>
{if $user.droit_membres < Membres::DROIT_ADMIN && (!empty($champs.passe.private) || empty($champs.passe.editable))}
<p class="help">Vous devez contacter un administrateur pour changer votre mot de passe.</p>
{else}
<dl>
<dd>Vous avez déjà un mot de passe, ne remplissez les champs suivants que si vous souhaitez en changer.</dd>
<dt><label for="f_passe">Nouveau mot de passe</label> (minimum {$password_length} caractères)</dt>
<dd class="help">
Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr
|
| |
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
</form>
{else}
<form method="post" action="{$self_url_no_qs}">
<fieldset>
<legend>Changer mon mot de passe</legend>
{if $user.droit_membres < $session::ACCESS_ADMIN && (!empty($champs.passe.private) || empty($champs.passe.editable))}
<p class="help">Vous devez contacter un administrateur pour changer votre mot de passe.</p>
{else}
<dl>
<dd>Vous avez déjà un mot de passe, ne remplissez les champs suivants que si vous souhaitez en changer.</dd>
<dt><label for="f_passe">Nouveau mot de passe</label> (minimum {$password_length} caractères)</dt>
<dd class="help">
Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr
|
Deleted src/templates/admin/wiki/_chercher_parent.tpl version [8f5769baca].
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 |
{include file="admin/_head.tpl" title="Choisir la page parent" current="wiki" body_id="popup" is_popup=true} <div class="wikiTree"> <p class="choice"> <input type="button" onclick="chooseParent();" value="Choisir la page sélectionnée" /> </p> {display_tree tree=$list} </div> {literal} <script type="text/javascript"> (function() { window.chooseParent = function() { var elm = document.getElementsByClassName("current")[0].getElementsByTagName("a")[0]; if (match = elm.href.match(/=(\d+)$/)) { var id = parseInt(match[1], 10); var titre = (id == 0 ? 'la racine du site' : elm.innerHTML); if (window.opener.changeParent(id, titre)) { self.close(); } else { alert("Impossible de choisir la page comme parent d'elle-même !"); } return false; } }; }()); </script> {/literal} {include file="admin/_foot.tpl"} |
< < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted src/templates/admin/wiki/_fichiers.tpl version [528fa66749].
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 |
{include file="admin/_head.tpl" title="Inclure un fichier" current="wiki" body_id="transparent" is_popup=true} {form_errors} <form method="post" enctype="multipart/form-data" action="{$self_url}" id="f_upload"> <fieldset> <legend>Téléverser un fichier</legend> <input type="hidden" name="MAX_FILE_SIZE" value="{$max_size}" id="f_maxsize" /> <dl> <dd class="help">Taille maximale : {$max_size|format_bytes}</dd> <dd class="fileUpload"><input type="file" name="fichier" id="f_fichier" data-hash-check /></dd> </dl> <p class="submit"> {csrf_field key=$csrf_id} {button type="submit" name="upload" label="Envoyer le fichier" shape="upload" class="main" id="f_submit"} </p> </fieldset> </form> <form method="get" action="#" style="display: none;" id="insertImage"> <fieldset> <h3>Insérer une image dans le texte</h3> <dl> <dd class="image"></dd> <dt>Légende <i>(facultatif)</i></dt> <dd class="caption"> <input type="text" name="f_caption" size="50" /> </dd> <dt>Alignement :</dt> <dd class="align"> <input type="button" name="gauche" value="À gauche" /> <input type="button" name="centre" value="Au centre" /> <input type="button" name="droite" value="À droite" /> </dd> <dd class="cancel"> <input type="reset" value="Annuler" /> </dd> </dl> </fieldset> </form> {if !empty($images)} <ul class="gallery"> {foreach from=$images item="file"} <li> <figure> <a href="{$file.url}" data-id="{$file.id}"><img src="{$file.thumb}" alt="" title="{$file.nom}" /></a> <form class="actions" method="post" action="{$self_url}"> {linkbutton shape="download" label="Télécharger" href=$file.url target="_blank"} {csrf_field key=$csrf_id} <input type="hidden" name="delete" value="{$file.id}" /> <noscript><input type="submit" value="Supprimer" /></noscript> </form> </figure> </li> {/foreach} </ul> {/if} {if !empty($fichiers)} <table class="list"> <tbody> {foreach from=$fichiers item="file"} <tr> <th>{$file.nom}</th> <td>{if $file.type}{$file.type}{/if}</td> <td class="actions"> <form class="actions" method="post" action="{$self_url}"> {linkbutton shape="download" label="Télécharger" href=$file.url target="_blank"} {csrf_field key=$csrf_id} <input type="hidden" name="delete" value="{$file.id}" /> <noscript><input type="submit" value="Supprimer" /></noscript> </form> </td> </tr> {/foreach} </tbody> </table> {/if} {include file="admin/_foot.tpl"} |
< < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted src/templates/admin/wiki/_preview.tpl version [1c9551975d].
1 2 3 4 5 6 7 |
{include file="admin/_head.tpl" title="Wiki" current="wiki" is_popup=1 body_id="transparent"} <div class="wikiContent"> {$contenu|raw|format_wiki|liens_wiki:'#'} </div> {includ |