Changes In Branch dev Through [ed012f894f] Excluding Merge-Ins
This is equivalent to a diff from 031185e587 to ed012f894f
2020-12-13
| ||
00:30 | Refactor homepage controller check-in: 4f7a33917e user: bohwaz tags: dev | |
00:28 | You should be able to get the session object again, using a singleton check-in: ed012f894f user: bohwaz tags: dev | |
2020-12-12
| ||
17:21 | More progress on migration to files and web pages check-in: ba3e9fabe2 user: bohwaz tags: dev | |
2020-12-10
| ||
19:05 | Make sure the source has a lines array check-in: cd365f64b6 user: bohwaz tags: trunk, stable | |
18:21 | Merge with trunk check-in: ab8bff586d user: bohwaz tags: dev | |
18:17 | New release check-in: 031185e587 user: bohwaz tags: trunk, stable, 1.0.0-rc12 | |
18:05 | Allow to use date picker with something else than a text input check-in: 85e3cbbe5c user: bohwaz tags: trunk, stable | |
Modified src/config.dist.php from [9561d21e0d] to [a74f60285a].
︙ | ︙ | |||
351 352 353 354 355 356 357 | * * 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'; | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | * * 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 * - FileSystem : idem mais permet de spécifier un quota maximal * * 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 (en string). * * Valeurs possibles : * - SQLite : null, aucune configuration possible * - FileSystem : chemin du répertoire où doivent être stockés les fichiers, * %s doit être ajouté à la fin pour indiquer le répertoire et nom du fichier * - FileSystemQuota : idem, mais il faut rajouter ';quota=XXX' à la fin pour * indiquer la taille maximale de stockage autorisée. * * Défaut : null */ //const FILE_STORAGE_CONFIG = null; |
Modified src/include/data/1.0.0_migration.sql from [4e16391488] to [ba81ccc806].
︙ | ︙ | |||
15 16 17 18 19 20 21 | -------- MIGRATION COMPTA --------- INSERT INTO acc_charts (id, country, code, label) VALUES (1, 'FR', 'PCGA1999', 'Plan comptable associatif 1999'); -- Migration comptes de code comme identifiant à ID unique -- Inversement valeurs actif/passif et produit/charge INSERT INTO acc_accounts (id, id_chart, code, label, position, user) SELECT NULL, 1, id, libelle, | | | | | | | < < | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | -------- MIGRATION COMPTA --------- INSERT INTO acc_charts (id, country, code, label) VALUES (1, 'FR', 'PCGA1999', 'Plan comptable associatif 1999'); -- Migration comptes de code comme identifiant à ID unique -- Inversement valeurs actif/passif et produit/charge INSERT INTO acc_accounts (id, id_chart, code, label, position, user) SELECT NULL, 1, id, libelle, CASE position WHEN 1 THEN 2 WHEN 2 THEN 1 WHEN 3 THEN 3 WHEN 4 THEN 5 WHEN 8 THEN 4 -- Suppression de la position "charge ou produit" qui n'a aucun sens WHEN 12 THEN 0 ELSE 0 END, CASE WHEN plan_comptable = 1 THEN 0 ELSE 1 END FROM compta_comptes; -- Migrations projets vers comptes analytiques INSERT INTO acc_accounts (id_chart, code, label, position, user, type) |
︙ | ︙ |
Added src/include/data/1.1.0_migration.sql version [b636c3139c].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | ALTER TABLE membres_categories RENAME TO membres_categories_old; .read 1.1.0_schema.sql INSERT INTO membres_categories SELECT id, nom, droit_wiki, -- droit_web droit_wiki, -- droit_documents droit_membres, droit_compta, droit_inscription, droit_connexion, droit_config, cacher FROM membres_categories_old; DROP TABLE membres_categories_old; -- Copy wiki pages content CREATE TEMP TABLE wiki_as_files (old_id, new_id, hash, size, content, name, title, uri, old_parent, new_parent, created, modified, author_id, encrypted, content_id, type, public); INSERT INTO wiki_as_files SELECT id, NULL, sha1(contenu), LENGTH(contenu), contenu, uri || '.skriv', titre, uri, parent, NULL, date_creation, date_modification, id_auteur, chiffrement, NULL, 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; UPDATE wiki_as_files SET name = 'index.skriv' WHERE type = 1; -- Build back path, up to ten levels --UPDATE wiki_as_files waf SET -- path = (SELECT uri FROM wiki_as_files WHERE id = waf.parent) || '/' || path, -- parent = (SELECT parent FROM wiki_as_files WHERE id = waf.parent) -- WHERE parent > 0; -- Create private folders INSERT INTO files_folders (id, parent_id, name, system) SELECT old_id, old_parent, uri, 0 FROM wiki_as_files WHERE type = 1; -- Create web folders INSERT INTO files_folders (id, parent_id, name, system) SELECT old_id + 10000, old_parent + 10000, uri, 1 FROM wiki_as_files WHERE type = 1; UPDATE files_folders SET parent_id = (SELECT CASE WHEN f.system = 0 THEN f.id ELSE f.id + 10000 END FROM files_folders f WHERE f.id = parent_id); INSERT INTO files_contents (hash, content) SELECT hash, content FROM wiki_as_files; UPDATE wiki_as_files SET content_id = (SELECT fc.id FROM files_contents fc WHERE fc.hash = wiki_as_files.hash); INSERT INTO files (hash, folder_id, name, type, created, content_id, author_id, public) SELECT hash, (SELECT CASE WHEN public = 0 THEN f.id ELSE f.id + 10000 END FROM files_folders f WHERE f.id = old_parent), name, CASE WHEN encrypted THEN 'text/vnd.skriv.encrypted' ELSE 'text/vnd.skriv' END, created, content_id, author_id, public FROM wiki_as_files; INSERT INTO files_search (id, content) SELECT new_id, content FROM wiki_as_files WHERE encrypted = 0; UPDATE wiki_as_files SET new_id = (SELECT id FROM files WHERE hash = wiki_as_files.hash); 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); INSERT INTO web_pages SELECT new_id, new_parent, type, 1, uri, title, modified FROM wiki_as_files WHERE public = 1; INSERT INTO files_links (id, web_page_id) SELECT id, id FROM web_pages WHERE status = 1; INSERT INTO files_links (id, file_id) SELECT f.id, w.fichier FROM fichiers_wiki_pages w INNER JOIN wiki_as_files waf ON waf.old_id = w.id INNER JOIN files f ON f.hash = waf.hash; INSERT INTO files_links (id, transaction_id) SELECT fichier, id FROM fichiers_acc_transactions; INSERT INTO files_links (id, config) SELECT valeur, cle FROM config WHERE cle = 'image_fond' AND valeur > 0; INSERT INTO files (hash, folder_id, name, type, created, content_id, author_id, public) SELECT hash, NULL, name, type, created, content_id, author_id, 0 FROM files WHERE id = (SELECT new_id FROM wiki_as_files WHERE uri = (SELECT valeur FROM config WHERE cle = 'accueil_connexion')); UPDATE config SET valeur = (SELECT id FROM files WHERE name = 'Accueil_connexion.skriv') WHERE cle = 'accueil_connexion'; UPDATE config SET cle = 'admin_homepage' WHERE cle = 'accueil_connexion'; DELETE FROM config WHERE cle = 'accueil_wiki'; INSERT INTO config (cle, valeur) VALUES ('telephone_asso', NULL); DROP TRIGGER wiki_recherche_delete; DROP TRIGGER wiki_recherche_update; DROP TRIGGER wiki_recherche_contenu_insert; DROP TRIGGER wiki_recherche_contenu_chiffre; 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; |
Added src/include/data/1.1.0_schema.sql version [1510ba50b8].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | CREATE TABLE IF NOT EXISTS config ( -- Configuration de Garradin cle TEXT PRIMARY KEY NOT NULL, valeur TEXT ); CREATE TABLE IF NOT EXISTS membres_categories -- Catégories de membres ( id INTEGER PRIMARY KEY NOT NULL, nom TEXT NOT NULL, droit_web INTEGER NOT NULL DEFAULT 1, droit_documents INTEGER NOT NULL DEFAULT 1, droit_membres INTEGER NOT NULL DEFAULT 1, droit_compta INTEGER NOT NULL DEFAULT 1, droit_inscription INTEGER NOT NULL DEFAULT 0, droit_connexion INTEGER NOT NULL DEFAULT 1, droit_config INTEGER NOT NULL DEFAULT 0, cacher 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, folder_id INTEGER NULL REFERENCES files_folders, name TEXT NOT NULL, -- file name (eg. image1234.jpeg) type TEXT NULL, -- MIME type image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue public INTEGER NOT NULL DEFAULT 0, size INTEGER NOT NULL DEFAULT 0, hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier storage TEXT NULL, -- Storage medium, NULL means stored in content BLOB storage_path TEXT NULL, created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(created) IS NOT NULL AND datetime(created) = created), author_id INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL, content_id INTEGER NULL REFERENCES files_contents (id) ON DELETE SET NULL, CHECK (storage IS NOT NULL OR content_id IS NOT NULL) ); CREATE INDEX IF NOT EXISTS files_date ON files (created); CREATE INDEX IF NOT EXISTS files_hash ON files (hash); CREATE TABLE IF NOT EXISTS files_contents -- Files contents (if storage backend is SQLite) ( id INTEGER NOT NULL PRIMARY KEY, hash TEXT NOT NULL, content BLOB NULL ); CREATE UNIQUE INDEX IF NOT EXISTS files_contents_hash ON files_contents (hash); CREATE TABLE IF NOT EXISTS files_folders ( id INTEGER NOT NULL PRIMARY KEY, parent_id INTEGER NULL REFERENCES files_folders(id) ON DELETE CASCADE, name TEXT NOT NULL, system INTEGER NOT NULL DEFAULT 0 ); CREATE VIRTUAL TABLE IF NOT EXISTS files_search USING fts4 -- Search inside files content ( tokenize=unicode61, -- Available from SQLITE 3.7.13 (2012) id INT PRIMARY KEY NOT NULL REFERENCES files(id), title TEXT NULL, content TEXT NOT NULL -- Text content ); CREATE TABLE IF NOT EXISTS files_links -- This references use of a file outside of the documents module -- One file can only be linked to one thing ( id INTEGER NOT NULL REFERENCES files (id) ON DELETE CASCADE, file_id INTEGER NULL REFERENCES files (id) ON DELETE CASCADE, user_id INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, transaction_id INTEGER NULL REFERENCES acc_transactions (id) ON DELETE CASCADE, config TEXT NULL REFERENCES config (cle) ON DELETE CASCADE, web_page_id INTEGER NULL REFERENCES web_pages (id) ON DELETE CASCADE, -- Make sure that only one is filled CHECK ((file_id IS NOT NULL) + (user_id IS NOT NULL) + (transaction_id IS NOT NULL) + (config IS NOT NULL) + (web_page_id IS NOT NULL) = 1) ); CREATE UNIQUE INDEX IF NOT EXISTS files_links_unique ON files_links (id, file_id, user_id, transaction_id, config, web_page_id); CREATE TABLE IF NOT EXISTS web_pages ( id INTEGER NOT NULL PRIMARY KEY REFERENCES files(id), parent_id INTEGER NULL REFERENCES web_pages(id) ON DELETE SET NULL, type INTEGER NOT NULL, -- 1 = Category, 2 = Page status INTEGER NOT NULL DEFAULT 0, -- 0 = draft, 1 = online uri TEXT NOT NULL, title TEXT NOT NULL, modified TEXT NULL CHECK (datetime(modified) IS NULL OR datetime(modified) = modified) ); CREATE UNIQUE INDEX web_pages_uri ON web_pages (uri); CREATE TRIGGER IF NOT EXISTS web_page_insert AFTER INSERT ON web_pages BEGIN UPDATE files SET public = CASE WHEN NEW.status = 1 THEN 1 ELSE 0 END WHERE id = NEW.id; END; CREATE TRIGGER IF NOT EXISTS web_page_update AFTER UPDATE OF status ON web_pages BEGIN UPDATE files SET public = CASE WHEN NEW.status = 1 THEN 1 ELSE 0 END WHERE id = NEW.id; END; -- 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/init.php from [53a38bd077] to [e6355f8bdd].
︙ | ︙ | |||
317 318 319 320 321 322 323 | if (!defined('Garradin\INSTALL_PROCESS') && !defined('Garradin\UPGRADE_PROCESS')) { if (!file_exists(DB_FILE)) { Utils::redirect(ADMIN_URL . 'install.php'); } | | | | 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | if (!defined('Garradin\INSTALL_PROCESS') && !defined('Garradin\UPGRADE_PROCESS')) { if (!file_exists(DB_FILE)) { Utils::redirect(ADMIN_URL . 'install.php'); } $v = DB::getInstance()->firstColumn('SELECT valeur FROM config WHERE cle = \'version\';'); if (version_compare($v, garradin_version(), '<')) { Utils::redirect(ADMIN_URL . 'upgrade.php'); } } |
Modified src/include/lib/Garradin/Config.php from [85c9d942d5] to [7305829509].
1 2 3 4 5 6 | <?php namespace Garradin; use KD2\SMTP; | > > > > | > > > > > > > | > > > > > > > > > > > > > > > > | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | | | | | | | < < < < | | > | | | < | < < < < < | < < < < < < | < < > > | < > | < < < | < < < | < < < | < < > | | < | < | | < < < | < < < < < < < | < < < | | | < < | | | < < < > | | | < > | | | | | | < | < | | < < > | < < | < < | < < < < | < < < < | | < | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | < < < < | < < < | < < < | | < < | | < < < | | | < < < < < < < < < < < < < | | < < < < < < > | | < | < < < < < < < < | < | | < | < | < < < < < | < < | < | | < < < | > | > | | < < | < < < < < | | < | < | < > | < < < < < < < > | < | < < < < < | < < < > | | < < < < | < < < | < < < < < < < < < < | < < < < < < < < | < | < < < | < < | < < | < | | < | | | | | | | | 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 | <?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 $version; protected $last_chart_change; protected $last_version_check; protected $couleur1; protected $couleur2; protected $image_fond; 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' => '?Garradin\Entities\Files\File', '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' => '?Garradin\Entities\Files\File', 'desactiver_site' => 'bool', ]; static protected $_instance = null; /** * Singleton simple * @return Config */ 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 cle, valeur FROM config ORDER BY cle;'); $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; } if ($type == File::class || substr($type, 1) == File::class) { $config[$key] = Files::get((int) $value); } } $this->load($config); $this->champs_membres = new Membres\Champs((string)$value); $this->accueil_connexion = $this->accueil_connexion ? Files::get($this->accueil_connexion) : null; } public function save(): bool { if (!count($this->_modified)) { return true; } $values = []; $db = DB::getInstance(); foreach ($this->_modified as $key => $modified) { $value = $this->$key; if ($this->_types[$key] == File::class && null === $value && $this->$key !== null) { $this->$key->delete(); } elseif ($this->_types[$key] == File::class && null !== $value) { $value = $value->id(); } else if ($this->_types[$key] == Champs::class) { $value = $value->toString(); } $values[$key] = $value; } unset($value, $key, $modified); $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 delete(): bool { throw new \LogicException('Cannot delete config'); } public function getVersion(): string { return $this->get('version'); } public function setVersion(string $version): void { $this->config->version = $version; $db = DB::getInstance(); $db->preparedQuery('INSERT OR REPLACE INTO config (cle, valeur) VALUES (?, ?);', ['version', $version]); } 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 File::class: 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(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('membres_categories', 'id = ?', $this->categorie_membres), 'Catégorie de membres inconnue'); } public function getConfig() { return $this->asArray(); } } |
Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [d9fc699598] to [d64763b884].
︙ | ︙ | |||
156 157 158 159 160 161 162 | return $line; } } return null; } | < < < < < < < < < < < < < < < < < < < < < < | 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 | return $line; } } return null; } public function getLinesCreditSum() { $sum = 0; foreach ($this->getLines() as $line) { $sum += $line->credit; } return $sum; } public function getTypesAccounts() { if ($this->type == self::TYPE_ADVANCED) { return []; } $debit = null; |
︙ | ︙ |
Added src/include/lib/Garradin/Entities/Files/File.php version [419025821b].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin\Entities\Files; use KD2\Image; use Garradin\DB; use Garradin\Entity; use Garradin\UserException; class File extends Entity { const TABLE = 'files'; protected $id; protected $folder_id; protected $name; protected $type; protected $image; protected $public; protected $size; protected $hash; protected $storage; protected $storage_path; protected $created; protected $author_id; protected $content_id; protected $_types = [ 'id' => 'int', 'folder_id' => '?int', 'name' => 'string', 'type' => '?string', 'public' => 'int', 'image' => 'int', 'size' => 'int', 'hash' => 'string', 'storage' => '?string', 'storage_path' => '?string', 'created' => 'DateTime', 'author_id' => '?int', 'content_id' => '?int', ]; protected $_public; /** * 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, 1200]; // Link to another file (ie. image included in a HTML file) const LINK_FILE = 'file_id'; const LINK_USER = 'user_id'; const LINK_TRANSACTION = 'transaction_id'; const LINK_CONFIG = 'config'; const LINK_WEB_PAGE = 'web_page_id'; const LINK_WEB_CATEGORY = 'web_category_id'; const THUMB_CACHE_ID = 'file.thumb.%d.%d'; public function selfCheck(): void { parent::selfCheck(); } public function delete(): bool { $return = parent::delete(); // clean up thumbs foreach (self::ALLOWED_THUMB_SIZES as $size) { Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->id(), $size)); } return $return; } public function save(): bool { $return = parent::save(); // Store content in search table if ($return && substr($this->type, 0, 5) == 'text/') { $content = Files::callStorage('fetch', $this); if ($this->type == 'text/html') { $content = strip_tags($content); } if ($this->type == 'text/vnd.skriv.encrypted') { $content = 'Contenu chiffré'; } $db->preparedQuery('INSERT OR REPLACE INTO files_search (id, content) VALUES (?, ?);', $this->id(), $content); } return $return; } static protected function store(?string $path, string $name, string $source_path = null, $source_content = null): self { assert($path || $content); $finfo = \finfo_open(\FILEINFO_MIME_TYPE); $file = new self; $file->path = $path; if ($source_path && !$source_content) { $file->hash = sha1_file($source_path); $file->size = filesize($source_path); $file->type = finfo_file($finfo, $source_path); } else { $file->hash = sha1($source_content); $file->size = strlen($source_content); $file->type = finfo_buffer($finfo, $source_content); } $file->image = preg_match('/^image\/(?:png|jpe?g|gif)$/', $file->type); // Check that it's a real image if ($file->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); $file->hash = sha1($source_content); $file->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'); } } $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 ($content_id = $db->firstColumn('SELECT id FROM files_contents WHERE hash = ?;', $hash)) { $file->content_id = $content_id; } else { $db->preparedQuery('INSERT INTO files_contents (hash, size) VALUES (?, ?);', [$file->hash, (int)$file->size]); $file->content_id = $db->lastInsertRowID(); if (!Files::callStorage('store', $file, $path, $content)) { throw new UserException('Le fichier n\'a pas pu être enregistré.'); } } $file->save(); $db->commit(); return $file; } /** * Upload de fichier à partir d'une chaîne en base64 * @param string $name * @param string $content * @return File */ static public function storeFromBase64(?string $path, string $name, string $encoded_content): self { $content = base64_decode($encoded_content); return self::store($path, $name, null, $content); } /** * Upload du fichier par POST * @param array $file Caractéristiques du fichier envoyé * @return File */ static public function upload(?string $path, array $file): self { 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::store($path, $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(?int $size = null): string { return self::getFileURL($this->id, $this->name, $this->hash, $size); } /** * Renvoie l'URL vers un fichier * @param integer $id Numéro du fichier * @param string $name 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 getFileURL(int $id, string $name, string $hash, ?int $size = null): string { $url = sprintf('%sf/%s/%s?', WWW_URL, base_convert((int)$id, 10, 36), $name); if ($size) { $url .= self::_findNearestThumbSize($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 _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); } /** * Lier un fichier à un contenu * @param string $type Type de contenu (constantes LINK_*) * @param integer $foreign_id ID du contenu lié * @return boolean TRUE en cas de succès */ public function linkTo(string $type, int $foreign_id): bool { $db = DB::getInstance(); static $types = [self::LINK_WEB, self::LINK_FILE, self::LINK_TRANSACTION, self::LINK_USER, self::LINK_CONFIG]; if (!in_array($type, $types)) { throw new \InvalidArgumentException('Unknown file link type.'); } if ($db->test('files_links', 'id = ?', $this->id())) { throw new \LogicException('This file is already linked to something else'); } $sql = sprintf('INSERT OR IGNORE INTO files_links (id, %s) VALUES (?, ?);', $type); return $db->preparedQuery($sql, [$this->id, $foreign_id]); } public function getLinkedId(string $type): ?int { static $types = [self::LINK_WEB, self::LINK_FILE, self::LINK_TRANSACTION, self::LINK_USER, self::LINK_CONFIG]; if (!in_array($type, $types)) { throw new \InvalidArgumentException('Unknown file link type.'); } return DB::getInstance()->firstColumn(sprintf('SELECT %s FROM files_links WHERE id = %d;', $type, $this->id())); } /** * Envoie le fichier au client HTTP */ public function serve(?Session $session = null): void { if (!$this->checkAccess($session)) { header('HTTP/1.1 403 Forbidden', true, 403); throw new UserException('Accès interdit'); return; } $path = Files::callStorage('getPath', $this); $content = null === $path ? Files::callStorage('fetch', $this) : null; $this->_serve($session, $path, $content); } /** * Envoie une miniature à la taille indiquée au client HTTP */ public function serveThumbnail(?Session $session = null, ?int $width = null): void { if (!$this->checkAccess($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->id(), $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('getPath', $file)) { (new Image($source))->resize($width)->save($destination); } elseif ($content = Files::callStorage('fetch', $file)) { 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($session, $path, 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): void { if ($this->isPublic()) { Utils::HTTPCache($this->hash, $this->datetime); } else { // Disable browser cache header('Pragma: private'); header('Expires: -1'); header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0'); } header(sprintf('Content-Type: %s', $this->type)); header(sprintf('Content-Disposition: inline; filename="%s"', $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', $this->size)); if (@ob_get_length()) { @ob_clean(); } flush(); if (null !== $path) { readfile($path); } else { echo $content; } } public function isPublic(): bool { if (null === $this->_public) { throw new \RuntimeException('_public is unset'); } return $this->_public; } public function checkAccess(Session $session): bool { $link = DB::getInstance()->first('SELECT * FROM files_links WHERE id = ?;', $this->id()); // If it's linked to a file, then we want to know what the parent file is linked to if ($link->{LINK_FILE}) { $link = DB::getInstance()->first('SELECT * FROM files_links WHERE id = ?;', $link->{LINK_FILE}); } $this->_public = false; // Everyone has access to web content as long it's not draft (0) if ($link->{LINK_WEB} == 1) { $this->_public = true; return true; } elseif ($link->{LINK_WEB} == 0) { return false; } // Everyone has access to config files (logo etc.) else if ($link->{LINK_CONFIG}) { $this->_public = true; return true; } else if ($link->{LINK_TRANSACTION} && $session->canAccess('compta', Membres::DROIT_ACCES)) { return true; } // The user can access his own profile files else if ($link->{LINK_USER} && $link->{LINK_USER} == $session->getUser()->id) { return true; } // Only users able to manage users can see their profile files else if ($link->{LINK_USER} && $session->canAccess('membres', Membres::DROIT_ECRITURE)) { return true; } return $session->canAccess(Session::SECTION_DOCUMENTS, Membres::DROIT_ACCES); } } |
Added src/include/lib/Garradin/Entities/Web/Page.php version [ab2d08746c].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin\Entities\Web; use Garradin\Entity; use Garradin\UserException; use KD2\DB\EntityManager; class Page extends Entity { protected $id; protected $parent_id; protected $status; protected $title; protected $draft; protected $modified; protected $_types = [ 'id' => 'int', 'parent_id' => 'int', 'status' => 'int', 'title' => 'string', 'draft' => 'int', 'modified' => 'DateTime', ]; protected $_file; const STATUS_ONLINE = 1; const STATUS_DRAFT = 0; public function file(): File { if (null === $this->_file) { $this->_file = EM::findOneById(File::class, $this->id); } return $this->_file; } public function save() { $file = $this->file(); $file->save(); $this->id($file->id()); parent::save(); } } |
Modified src/include/lib/Garradin/Fichiers.php from [76103311d2] to [b460df65f5].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use KD2\Graphics\Image; use Garradin\Membres\Session; class Fichiers { | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; use KD2\Graphics\Image; use Garradin\Membres\Session; class Fichiers { /** * 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 */ |
︙ | ︙ | |||
458 459 460 461 462 463 464 | }); $query = sprintf('SELECT hash, 1 FROM fichiers_contenu WHERE hash IN (%s);', implode(', ', $list)); return $db->getAssoc($query); } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | }); $query = sprintf('SELECT hash, 1 FROM fichiers_contenu WHERE hash IN (%s);', implode(', ', $list)); return $db->getAssoc($query); } /** * 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 |
︙ | ︙ | |||
657 658 659 660 661 662 663 | 'type' => $file->type, 'image' => (int)$file->image, ]); return new Fichiers($db->lastInsertRowID()); } | < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | 'type' => $file->type, 'image' => (int)$file->image, ]); return new Fichiers($db->lastInsertRowID()); } /** * 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 */ |
︙ | ︙ |
Added src/include/lib/Garradin/Files/Files.php version [c4da787241].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin\Files; use Garradin\Static_Cache; use Garradin\DB; use Garradin\Entities\Files\File; use KD2\DB\EntityManager as EM; use const Garradin\FILE_STORAGE_BACKEND; class Files { static public function callStorage(string $function, ...$args) { $storage = FILE_STORAGE_BACKEND ?? 'SQLite'; $class_name = get_class(__NAMESPACE__ . '\\Backend\\' . $storage); return call_user_func_array([$class_name, $function], $args); } static public function migrateStorage(string $from, string $to): void { $res = EM::getInstance(File::class)->iterate('SELECT * FROM @TABLE;'); $from = get_class(__NAMESPACE__ . '\\Backend\\' . $from); $to = get_class(__NAMESPACE__ . '\\Backend\\' . $to); foreach ($res as $file) { $from_path = call_user_func([$from, 'path'], $file); call_user_func([$to, 'store'], $file, $from_path); } } static public function deleteOrphanFiles() { $db = DB::getInstance(); $sql = 'SELECT f.* FROM files f LEFT JOIN files_links l ON f.id = l.id WHERE l.id IS NULL;'; foreach ($db->iterate($sql) as $file) { $f = new Fichiers($file->id, (array) $file); $f->remove(); } // Remove any left-overs $db->exec('DELETE FROM files_contents WHERE hash NOT IN (SELECT DISTINCT hash FROM files);'); } static public function deleteLinkedFiles(string $type, ?int $value = null) { foreach (self::iterateLinkedTo($type, $value) as $file) { $file->delete(); } self::deleteOrphanFiles(); } static public function iterateLinkedTo(string $type, ?int $value = null) { $where = $value ? sprintf('l.%s = %d', $value) : sprintf('l.%s IS NOT NULL'); $sql = sprintf('SELECT f.* FROM @TABLE f INNER JOIN files_links l ON l.id = f.id WHERE %s;', $where); return EM::getInstance(File::class)->iterate($sql); } static public function generatePathsIndex(): void { $all = DB::getInstance()->getAssoc('SELECT path, path FROM files GROUP BY path;'); $paths = []; foreach ($all as $path) { $path = explode('/', $path); foreach ($path as $part) { } } } static public function get(int $id): ?File { return EM::findOneById(File::class, $id); } } |
Added src/include/lib/Garradin/Files/Storage/FileSystem.php version [e05f9c9e78].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin\Files\Storage; use const Garradin\FILE_STORAGE_CONFIG; /** * This class provides storage in the file system * You need ton configure FILE_STORAGE_CONFIG to give a file path */ class FileSystem implements StorageInterface { static protected $_size; static protected function _getRoot() { if (!FILE_STORAGE_CONFIG) { throw new \RuntimeException('Le stockage de fichier n\'a pas été configuré (FILE_STORAGE_CONFIG est vide).'); } if (!is_writable(FILE_STORAGE_CONFIG)) { throw new \RuntimeException('Le répertoire de stockage des fichiers est protégé contre l\'écriture.'); } $target = rtrim(FILE_STORAGE_CONFIG, DIRECTORY_SEPARATOR); return realpath($target); } static protected function ensureDirectoryExists(string $path): void { if (is_dir($path)) { return; } $permissions = fileperms(self::_getRoot(null)); mkdir($path, $permissions & 0777, true); } static public function store(File $file, ?string $path, ?string $content): bool { $target = self::getPath($file); self::ensureDirectoryExists(dirname($target)); if (null !== $path) { return copy($path, $target); } else { return file_put_contents($target, $content); } } static public function list(?string $path): ?array { $path = self::_getRoot() . ($path ? DIRECTORY_SEPARATOR . $file->path : '') . DIRECTORY_SEPARATOR . '*'; $files = glob($path); $list = []; foreach ($files as $file) { } return $list; } static public function getPath(File $file): ?string { $path = ''; if ($file->path) { $path .= DIRECTORY_SEPARATOR . $file->path; } $path .= DIRECTORY_SEPARATOR . $file->name; return self::_getRoot() . $path; } static public function display(File $file): void { readfile(self::getPath($file)); } static public function fetch(File $file): string { return file_get_contents(self::getPath($file)); } static public function delete(File $file): bool { return unlink(self::getPath($file)); } static public function move(File $old_file, File $new_file): bool { $target = self::getPath($new_file); self::ensureDirectoryExists(dirname($target)); return rename(self::getPath($old_file), $target); } 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; } static public function getRemainingQuota(): int { return disk_free_space(self::_getRoot()); } static public function getQuota(): int { return disk_total_space(self::_getRoot()); } } |
Added src/include/lib/Garradin/Files/Storage/FileSystemQuota.php version [66a57977b0].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin\Files\Storage; use const Garradin\FILE_STORAGE_CONFIG; /** * This class provides storage, same as FileSystem, * but adds the ability to define a custom quota. * To that end, just append ;quota=XXX to FILE_STORAGE_CONFIG * where XXX is the maximum storage allowed for that user, in bytes */ class FileSystemQuota extends FileSystem { static protected $quota; static protected $root; static protected function _getRoot() { if (null === self::$root) { if (!FILE_STORAGE_CONFIG) { throw new \RuntimeException('Le stockage de fichier n\'a pas été configuré (FILE_STORAGE_CONFIG est vide).'); } $target = strtok(FILE_STORAGE_CONFIG, ';'); if (!is_writable($target)) { throw new \RuntimeException('Le répertoire de stockage des fichiers est protégé contre l\'écriture.'); } strtok('='); $size = (int) strtok(''); if (!$size) { throw new \RuntimeException('Aucun quota indiqué dans FILE_STORAGE_CONFIG'); } $target = rtrim($target, DIRECTORY_SEPARATOR); self::$root = realpath($target); self::$quota = $size; } return self::$root; } static public function getRemainingQuota(): int { return self::getTotalSize() - self::getQuota(); } static public function getQuota(): int { self::_getRoot(); // Make sure quota is loaded return self::$quota; } } |
Added src/include/lib/Garradin/Files/Storage/SQLite.php version [e6d2cf42a4].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin\Files\Storage; use Garradin\Static_Cache; use Garradin\DB; class SQLite implements StorageInterface { /** * 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->content_id; if (!Static_Cache::exists($cache_id)) { $blob = DB::getInstance()->openBlob('files_contents', 'content', (int)$file->content_id); Static_Cache::storeFromPointer($cache_id, $blob); fclose($blob); } return Static_Cache::getPath($cache_id); } static public function store(File $file, ?string $path, ?string $content): bool { $db = DB::getInstance(); $db->exec(sprintf('UPDATE files_contents SET blob = zeroblob(%d) WHERE id = %d;', $file->size, $file->content_id)); $blob = $db->openBlob('files_contents', 'content', $file->content_id, 'main', SQLITE3_OPEN_READWRITE); if (null !== $content) { fwrite($blob, $content); } else { fwrite($blob, file_get_contents($path)); } fclose($blob); return true; } static public function list(string $path): ?array { return null; } static public function getPath(File $file): ?string { return self::_getFilePathFromCache($file); } static public function display(File $file): void { readfile(self::getFilePathFromCache($file)); } static public function fetch(File $file): string { return file_get_contents(self::_getFilePathFromCache($file)); } static public function delete(File $file): bool { $cache_id = 'files.' . $file->content_id; Static_Cache::remove($cache_id); return DB::getInstance()->delete('files_contents', 'id = ?', (int)$file->content_id); } static public function move(File $old_file, File $new_file): bool { return true; } static public function getTotalSize(): ?int { return (int) DB::getInstance()->firstColumn('SELECT SUM(size) FROM files_contents;'); } static public function getRemainingQuota(): int { return disk_free_space(dirname(DB_FILE)); } static public function getQuota(): int { return disk_total_space(dirname(DB_FILE)); } } |
Added src/include/lib/Garradin/Files/Storage/StorageInterface.php version [1b7b1b4fca].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <?php namespace Garradin\Files\Storage; interface StorageInterface { static public function store(File $file, ?string $path, ?string $content): bool; /** * List files contained in a path, this must return an array of File instances * If this storage backend wants to leave the directory handling to Garradin, just return NULL. * @param string $path * @return array[File...] */ static public function list(string $path): ?array; /** * 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 getPath(File $file): ?string; static public function display(File $file): void; static public function fetch(File $file): string; static public function delete(File $file): bool; static public function move(File $old_file, File $new_file): bool; static public function getTotalSize(): int; static public function getRemainingQuota(): int; static public function getQuota(): int; } |
Modified src/include/lib/Garradin/Membres/Session.php from [db8cb5f1d1] to [ede47cc742].
︙ | ︙ | |||
22 23 24 25 26 27 28 29 30 31 32 33 34 35 | { // 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)); } | > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | { // 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)); } |
︙ | ︙ | |||
52 53 54 55 56 57 58 | if ($called !== null) { return $return['is_compromised']; } return parent::isPasswordCompromised($password); } | < < < < < < < < < < < < | 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | 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 |
︙ | ︙ | |||
87 88 89 90 91 92 93 | 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, | | | 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | 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_web, c.droit_documents, 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) |
︙ | ︙ |
Modified src/include/lib/Garradin/Upgrade.php from [b4ea8189bd] to [6af05bf96b].
1 2 3 4 5 6 7 8 9 10 | <?php namespace Garradin; use Garradin\Membres\Session; class Upgrade { static public function preCheck(): bool { | > > | < | | | 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 | <?php namespace Garradin; use Garradin\Membres\Session; class Upgrade { const MIN_REQUIRED_VERSION = '1.0.0-rc8'; static public function preCheck(): bool { $v = DB::getInstance()->firstColumn('SELECT valeur FROM config WHERE cle = \'version\';'); if (version_compare($v, garradin_version(), '>=')) { return false; } if (!$v || version_compare($v, self::MIN_REQUIRED_VERSION, '<')) { throw new UserException(sprintf("Votre version de Garradin est trop ancienne pour être mise à jour. Mettez à jour vers Garradin %s avant de faire la mise à jour vers cette version.", self::MIN_REQUIRED_VERSION)); } Install::checkAndCreateDirectories(); if (Static_Cache::exists('upgrade')) { $path = Static_Cache::getPath('upgrade'); |
︙ | ︙ | |||
36 37 38 39 40 41 42 | $session = new Session; $user_is_logged = $session->isLogged(true); return true; } static public function upgrade() { | | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < > | | | 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 | $session = new Session; $user_is_logged = $session->isLogged(true); return true; } static public function upgrade() { $v = DB::getInstance()->firstColumn('SELECT valeur FROM config WHERE cle = \'version\';'); $session = new Session; $user_is_logged = $session->isLogged(true); Static_Cache::store('upgrade', 'Mise à jour en cours.'); $db = DB::getInstance(); // Créer une sauvegarde automatique $backup_name = (new Sauvegarde)->create('pre-upgrade-' . garradin_version()); try { if (version_compare($v, '1.1.0', '<=')) { // Missing trigger $db->beginSchemaUpdate(); $db->createFunction('sha1', 'sha1'); $db->import(ROOT . '/include/data/1.1.0_migration.sql'); $db->commitSchemaUpdate(); } // Vérification de la cohérence des clés étrangères $db->foreignKeyCheck(); Utils::clearCaches(); DB::getInstance()->update('config', ['valeur' => garradin_version()], 'cle = \'version\';'); Static_Cache::remove('upgrade'); // Réinstaller les plugins système si nécessaire Plugin::checkAndInstallSystemPlugins(); // Mettre à jour les plugins si nécessaire |
︙ | ︙ |
Added src/include/lib/Garradin/Web.php version [eebca41282].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin; 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); } } |
Modified src/include/lib/Garradin/Wiki.php from [6ab9be5a8f] to [ebb7e50c24].
︙ | ︙ | |||
196 197 198 199 200 201 202 | public function getTitle($id) { $db = DB::getInstance(); return $db->firstColumn('SELECT titre FROM wiki_pages WHERE id = ? LIMIT 1;', (int)$id); } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | public function getTitle($id) { $db = DB::getInstance(); return $db->firstColumn('SELECT titre FROM wiki_pages WHERE id = ? LIMIT 1;', (int)$id); } public function setRestrictionCategorie($id, $droit_wiki) { $this->restriction_categorie = $id; $this->restriction_droit = $droit_wiki; return true; } |
︙ | ︙ |
Modified src/templates/admin/_head.tpl from [d27c715ba4] to [a52e4cbdcf].
︙ | ︙ | |||
83 84 85 86 87 88 89 | <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} | | | | > > > > > > | | 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 | <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('documents', Membres::DROIT_ACCES)} <li class="{if $current == 'docs'} current{elseif $current_parent == 'docs'} current_parent{/if}"><a href="{$admin_url}docs/"><b class="icn">🗀</b><i> Fichiers</i></a> <ul> <li class="{if $current == 'docs/recent'} current{/if}"><a href="{$admin_url}docs/recent.php">Récents</a></li> </ul> </li> {/if} {if $session->canAccess('web', Membres::DROIT_ACCES)} <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> <ul> <li class="{if $current == 'web/themes'} current{/if}"><a href="{$admin_url}web/themes/">Thèmes</a></li> </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}"> |
︙ | ︙ |
Deleted src/www/admin/static/scripts/wiki_editor.css version [30c3c98262].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted src/www/admin/static/scripts/wiki_editor.js version [2d8c9cc5c3].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/www/admin/upgrade.php from [0bcf1f55dd] to [f17b581907].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; const UPGRADE_PROCESS = true; require_once __DIR__ . '/../../include/test_required.php'; require_once __DIR__ . '/../../include/init.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 25 26 27 28 29 30 31 32 33 | <?php namespace Garradin; const UPGRADE_PROCESS = true; require_once __DIR__ . '/../../include/test_required.php'; require_once __DIR__ . '/../../include/init.php'; if (!Upgrade::preCheck()) { throw new UserException('Aucune mise à jour à effectuer, tout est à jour :-)'); } echo '<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" /> <link rel="stylesheet" type="text/css" href="static/admin.css" media="all" /> <script type="text/javascript" src="static/scripts/loader.js"></script> <title>Mise à jour</title> </head> <body> <header class="header"> <nav class="menu"></nav> <h1>Mise à jour de Garradin vers la version '.garradin_version().'...</h1> </header> <main> <div id="loader" class="loader" style="margin: 2em 0; height: 50px;"></div> <script> animatedLoader(document.getElementById("loader"), 5); </script>'; |
︙ | ︙ |