Comment: | Merge stable changes in dev |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | dev |
Files: | files | file ages | folders |
SHA3-256: |
d0ca9be742dc998fd584ac6bc8ca4a1e |
User & Date: | bohwaz on 2021-10-05 00:52:50 |
Other Links: | branch diff | manifest | tags |
2021-10-05
| ||
01:53 | Remove unused variable check-in: 20b010c98b user: bohwaz tags: dev | |
00:52 | Merge stable changes in dev check-in: d0ca9be742 user: bohwaz tags: dev | |
00:47 | Implement generic signals for entities check-in: 84133f87d4 user: bohwaz tags: trunk, stable | |
2021-06-07
| ||
19:23 | Merge trunk/stable changes check-in: b7a5f89a8c user: bohwaz tags: dev | |
Modified doc/index.md from [e4968f9379] to [1d3794b203].
︙ | ︙ | |||
142 143 144 145 146 147 148 | * la gestion des __adhérent⋅e⋅s__ : ajout, modification, suppression, possibilité de choisir les informations présentes sur les fiches adhérent, envoi de mails collectifs aux adhérent⋅e⋅s * la tenue de la __comptabilité__ : avoir une gestion comptable complète à même de satisfaire un expert-comptable tout en restant à la portée de celles et ceux qui ne savent pas ce qu'est la comptabilité à double entrée, permettre la production des rapports et bilans annuels et de suivre au jour le jour le budget de l'association * la gestion des __cotisations__ et __activités__ : suivi des cotisations à jour, inscriptions et paiement des activités, rappels automatiques par e-mail, etc. * le travail __collaboratif__ et __collectif__ : gestion fine des droits d'accès aux fonctions, échange de mails entre membres… * la __simplification administrative__ : prise de notes en réunion, archivage et partage de fichiers (afin d'éliminer le besoin d'archiver les documents papier), etc. * la publication d'un __site web__ pour l'association, simple mais suffisamment flexible pour pouvoir adapter le fonctionnement à la plupart des besoins * l'__autonomisation des adhérents__ : possibilité de mettre à jour leurs informations par eux-même, ou de s'inscrire seul depuis un ordinateur ou un smartphone | | | | < | 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 | * la gestion des __adhérent⋅e⋅s__ : ajout, modification, suppression, possibilité de choisir les informations présentes sur les fiches adhérent, envoi de mails collectifs aux adhérent⋅e⋅s * la tenue de la __comptabilité__ : avoir une gestion comptable complète à même de satisfaire un expert-comptable tout en restant à la portée de celles et ceux qui ne savent pas ce qu'est la comptabilité à double entrée, permettre la production des rapports et bilans annuels et de suivre au jour le jour le budget de l'association * la gestion des __cotisations__ et __activités__ : suivi des cotisations à jour, inscriptions et paiement des activités, rappels automatiques par e-mail, etc. * le travail __collaboratif__ et __collectif__ : gestion fine des droits d'accès aux fonctions, échange de mails entre membres… * la __simplification administrative__ : prise de notes en réunion, archivage et partage de fichiers (afin d'éliminer le besoin d'archiver les documents papier), etc. * la publication d'un __site web__ pour l'association, simple mais suffisamment flexible pour pouvoir adapter le fonctionnement à la plupart des besoins * l'__autonomisation des adhérents__ : possibilité de mettre à jour leurs informations par eux-même, ou de s'inscrire seul depuis un ordinateur ou un smartphone * la possibilité d'adapter aux besoins spécifiques de chaque association via des [__extensions__](/wiki/?name=Extensions). Tous ces objectifs ne sont pas encore réalisés, voir : * [la liste des fonctionnalités disponibles](/wiki/?name=Fonctionnalités) pour ce qui est actuellement disponible ; * [la feuille de route](/wiki/?name=Roadmap) pour la liste des fonctionnalités qu'il reste à implémenter. Garradin est un logiciel libre disponible sous licence [AGPL v3](https://www.gnu.org/licenses/why-affero-gpl.fr.html). Garradin signifie *argent* en *Wagiman*, un dialecte aborigène du nord de l'Australie. ## Documentation et entraide * D'abord lire la [documentation](/wiki/?name=Documentation) et notamment la [foire aux questions](/wiki/?name=FAQ) * Voir la page [Entraide](/wiki/?name=Entraide) pour accéder aux listes de discussion et au salon de discussion IRC ## Participer Tout coup de main est le bienvenu, pas besoin d'avoir des connaissances techniques ! Nous avons un [guide de contribution](/wiki/?name=Contribuer) pour vous aider à voir comment vous pouvez participer à Garradin :) ### Développement Garradin est un logiciel libre, développé en PHP, utilisant la base de données SQLite, et avec une interface utilisant HTML, CSS et un peu de Javascript. Nous acceptons les contributions (plugins, patch, code, tickets, etc.) avec plaisir, consultez la [documentation développeur⋅euse](/wiki/?name=Documentation développeur) pour découvrir comment vous pouvez contribuer. |
Modified src/VERSION from [07c940882d] to [23f241f8f4].
|
| | | 1 | 1.1.11 |
Modified src/config.dist.php from [d4d9a98eb8] to [731c136e13].
︙ | ︙ | |||
427 428 429 430 431 432 433 | * 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, donc généralement l'espace dispo sur le disque dur !) */ //const FILE_STORAGE_QUOTA = 10000; // Forcer le quota alloué à 10 Mo, quel que soit le backend de stockage | > > > > > > > > > > > > > > > | 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | * 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, donc généralement l'espace dispo sur le disque dur !) */ //const FILE_STORAGE_QUOTA = 10000; // Forcer le quota alloué à 10 Mo, quel que soit le backend de stockage /** * Commande de création de PDF * * Commande qui sera exécutée pour créer un fichier PDF à partir d'un HTML. * Si laissé non spécifié (ou NULL), Garradin essaiera de détecter une solution entre * PrinceXML, Chromium, wkhtmltopdf ou weasyprint. * * %1$s sera remplacé par le chemin du fichier HTML, et %2$s par le chemin du fichier PDF. * * Exemple : chromium --headless --print-to-pdf=%2$s %1$s * * Défaut : null */ //const PDF_COMMAND = 'wkhtmltopdf %2$s %1$s'; |
Added src/include/data/1.0.0_schema.sql version [292ae06778].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | 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_wiki INTEGER NOT NULL DEFAULT 1, droit_membres INTEGER NOT NULL DEFAULT 1, droit_compta INTEGER NOT NULL DEFAULT 1, droit_inscription INTEGER NOT NULL DEFAULT 0, droit_connexion INTEGER NOT NULL DEFAULT 1, droit_config INTEGER NOT NULL DEFAULT 0, 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); -- -- WIKI -- CREATE TABLE IF NOT EXISTS wiki_pages -- Pages du wiki ( id INTEGER PRIMARY KEY NOT NULL, uri TEXT NOT NULL, -- URI unique (équivalent NomPageWiki) titre TEXT NOT NULL, date_creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_creation) IS NOT NULL AND datetime(date_creation) = date_creation), date_modification TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_modification) IS NOT NULL AND datetime(date_modification) = date_modification), parent INTEGER NOT NULL DEFAULT 0, -- ID de la page parent revision INTEGER NOT NULL DEFAULT 0, -- Numéro de révision (commence à 0 si pas de texte, +1 à chaque changement du texte) droit_lecture INTEGER NOT NULL DEFAULT 0, -- Accès en lecture (-1 = public [site web], 0 = tous ceux qui ont accès en lecture au wiki, 1+ = ID de groupe) droit_ecriture INTEGER NOT NULL DEFAULT 0 -- Accès en écriture (0 = tous ceux qui ont droit d'écriture sur le wiki, 1+ = ID de groupe) ); CREATE UNIQUE INDEX IF NOT EXISTS wiki_uri ON wiki_pages (uri); CREATE VIRTUAL TABLE IF NOT EXISTS wiki_recherche USING fts4 -- Table dupliquée pour chercher une page ( id INT PRIMARY KEY NOT NULL, -- Clé externe obligatoire titre TEXT NOT NULL, contenu TEXT NULL, -- Contenu de la dernière révision FOREIGN KEY (id) REFERENCES wiki_pages(id) ); CREATE TABLE IF NOT EXISTS wiki_revisions -- Révisions du contenu des pages ( id_page INTEGER NOT NULL REFERENCES wiki_pages (id) ON DELETE CASCADE, revision INTEGER NULL, id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL, contenu TEXT NOT NULL, modification TEXT NULL, -- Description des modifications effectuées chiffrement INTEGER NOT NULL DEFAULT 0, -- 1 si le contenu est chiffré, 0 sinon date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date), PRIMARY KEY(id_page, revision) ); CREATE INDEX IF NOT EXISTS wiki_revisions_id_page ON wiki_revisions (id_page); CREATE INDEX IF NOT EXISTS wiki_revisions_id_auteur ON wiki_revisions (id_auteur); -- Triggers pour synchro avec table wiki_pages CREATE TRIGGER IF NOT EXISTS wiki_recherche_delete AFTER DELETE ON wiki_pages BEGIN DELETE FROM wiki_recherche WHERE id = old.id; END; CREATE TRIGGER IF NOT EXISTS wiki_recherche_update AFTER UPDATE OF id, titre ON wiki_pages BEGIN UPDATE wiki_recherche SET id = new.id, titre = new.titre WHERE id = old.id; END; -- Trigger pour mettre à jour le contenu de la table de recherche lors d'une nouvelle révision CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_insert AFTER INSERT ON wiki_revisions WHEN new.chiffrement != 1 BEGIN UPDATE wiki_recherche SET contenu = new.contenu WHERE id = new.id_page; END; -- Si le contenu est chiffré, la recherche n'affiche pas de contenu CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_chiffre AFTER INSERT ON wiki_revisions WHEN new.chiffrement = 1 BEGIN UPDATE wiki_recherche SET contenu = '' WHERE id = new.id_page; END; -- -- 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 TRIGGER IF NOT EXISTS acc_years_delete BEFORE DELETE ON acc_years BEGIN UPDATE services_fees SET id_account = NULL, id_year = NULL WHERE id_year = OLD.id; END; 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, id_year); 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_transaction ON acc_transactions_lines (id_transaction); 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) ); CREATE TABLE IF NOT EXISTS fichiers -- Données sur les fichiers ( id INTEGER NOT NULL PRIMARY KEY, nom TEXT NOT NULL, -- nom de fichier (par exemple image1234.jpeg) type TEXT NULL, -- Type MIME image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue datetime TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(datetime) IS NOT NULL AND datetime(datetime) = datetime), -- Date d'ajout ou mise à jour du fichier id_contenu INTEGER NOT NULL REFERENCES fichiers_contenu (id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS fichiers_date ON fichiers (datetime); CREATE TABLE IF NOT EXISTS fichiers_contenu -- Contenu des fichiers ( id INTEGER NOT NULL PRIMARY KEY, hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier taille INTEGER NOT NULL, -- Taille en octets contenu BLOB NULL ); CREATE UNIQUE INDEX IF NOT EXISTS fichiers_hash ON fichiers_contenu (hash); CREATE TABLE IF NOT EXISTS fichiers_membres -- Associations entre fichiers et membres (photo de profil par exemple) ( fichier INTEGER NOT NULL REFERENCES fichiers (id) ON DELETE CASCADE, id INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE, PRIMARY KEY(fichier, id) ); CREATE TABLE IF NOT EXISTS fichiers_wiki_pages -- Associations entre fichiers et pages du wiki ( fichier INTEGER NOT NULL REFERENCES fichiers (id) ON DELETE CASCADE, id INTEGER NOT NULL REFERENCES wiki_pages (id) ON DELETE CASCADE, PRIMARY KEY(fichier, id) ); CREATE TABLE IF NOT EXISTS fichiers_acc_transactions -- Associations entre fichiers et journal de compta (pièce comptable par exemple) ( fichier INTEGER NOT NULL REFERENCES fichiers (id) ON DELETE CASCADE, id INTEGER NOT NULL REFERENCES acc_transactions (id) ON DELETE CASCADE, PRIMARY KEY(fichier, id) ); CREATE TABLE IF NOT EXISTS recherches -- Recherches enregistrées ( id INTEGER NOT NULL PRIMARY KEY, id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, -- Si non NULL, alors la recherche ne sera visible que par le membre associé intitule TEXT NOT NULL, creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(creation) IS NOT NULL AND datetime(creation) = creation), cible TEXT NOT NULL, -- "membres" ou "compta" 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/1.0.7_migration.sql from [607cda27c7] to [f53124e2db].
1 | -- Add indexes | | | 1 2 3 4 5 | -- Add indexes DROP INDEX IF EXISTS acc_transactions_type; CREATE INDEX IF NOT EXISTS acc_transactions_type ON acc_transactions (type, id_year); CREATE INDEX IF NOT EXISTS acc_transactions_lines_transaction ON acc_transactions_lines (id_transaction); |
Modified src/include/data/1.1.7_migration.sql from [40a52d8865] to [1dab4145d3].
1 2 | ALTER TABLE services_reminders_sent RENAME TO srs_old; | < | > > | > | > > > > > > > > > > > > > > > > > > > | 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 | ALTER TABLE services_reminders_sent RENAME TO srs_old; -- Add new column in services_reminders_sent 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, sent_date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(sent_date) IS NOT NULL AND date(sent_date) = sent_date), due_date TEXT NOT NULL CHECK (date(due_date) IS NOT NULL AND date(due_date) = due_date) ); CREATE UNIQUE INDEX IF NOT EXISTS srs_index ON services_reminders_sent (id_user, id_service, id_reminder, due_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); INSERT INTO services_reminders_sent SELECT id, id_user, id_service, id_reminder, date, date FROM srs_old; DROP TABLE srs_old; -- Missing acc_years_delete trigger, again, because of missing symlink in previous release -- Make sure id_account is reset when a year is deleted CREATE TRIGGER IF NOT EXISTS acc_years_delete BEFORE DELETE ON acc_years BEGIN UPDATE services_fees SET id_account = NULL, id_year = NULL WHERE id_year = OLD.id; END; |
Modified src/include/init.php from [aae3e95898] to [22b419591e].
︙ | ︙ | |||
172 173 174 175 176 177 178 179 180 181 182 183 184 185 | 'ADMIN_COLOR1' => '#9c4f15', 'ADMIN_COLOR2' => '#d98628', 'FILE_STORAGE_BACKEND' => 'SQLite', 'FILE_STORAGE_CONFIG' => null, 'FILE_STORAGE_QUOTA' => null, 'API_USER' => null, 'API_PASSWORD' => null, ]; foreach ($default_config as $const => $value) { $const = sprintf('Garradin\\%s', $const); if (!defined($const)) | > | 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | 'ADMIN_COLOR1' => '#9c4f15', 'ADMIN_COLOR2' => '#d98628', 'FILE_STORAGE_BACKEND' => 'SQLite', 'FILE_STORAGE_CONFIG' => null, 'FILE_STORAGE_QUOTA' => null, 'API_USER' => null, 'API_PASSWORD' => null, 'PDF_COMMAND' => null, ]; foreach ($default_config as $const => $value) { $const = sprintf('Garradin\\%s', $const); if (!defined($const)) |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Reports.php from [0f2d41f8c0] to [d37a6b0099].
︙ | ︙ | |||
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | if (!empty($criterias['service_user'])) { $where[] = sprintf('t.id IN (SELECT tu.id_transaction FROM acc_transactions_users tu WHERE id_service_user = %d)', $criterias['service_user']); } if (!empty($criterias['analytical'])) { $where[] = sprintf('l.id_analytical = %d', $criterias['analytical']); } if (!count($where)) { throw new \LogicException('Unknown criteria'); } return implode(' AND ', $where); } /** * Return account sums per year or per account * @param bool $by_year If true will return accounts grouped by year, if false it will return years grouped by account */ static public function getAnalyticalSums(bool $by_year = false): \Generator { $sql = 'SELECT a.label AS account_label, a.description AS account_description, a.id AS id_account, y.id AS id_year, y.label AS year_label, y.start_date, y.end_date, | > > > > | | 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 | if (!empty($criterias['service_user'])) { $where[] = sprintf('t.id IN (SELECT tu.id_transaction FROM acc_transactions_users tu WHERE id_service_user = %d)', $criterias['service_user']); } if (!empty($criterias['analytical'])) { $where[] = sprintf('l.id_analytical = %d', $criterias['analytical']); } if (!empty($criterias['analytical_only'])) { $where[] = 'l.id_analytical IS NOT NULL'; } if (!count($where)) { throw new \LogicException('Unknown criteria'); } return implode(' AND ', $where); } /** * Return account sums per year or per account * @param bool $by_year If true will return accounts grouped by year, if false it will return years grouped by account */ static public function getAnalyticalSums(bool $by_year = false): \Generator { $sql = 'SELECT a.label AS account_label, a.description AS account_description, a.id AS id_account, y.id AS id_year, y.label AS year_label, y.start_date, y.end_date, SUM(l.credit - l.debit) AS sum, SUM(l.credit) AS credit, SUM(l.debit) AS debit, 0 AS total, (SELECT SUM(l2.credit - l2.debit) FROM acc_transactions_lines l2 INNER JOIN acc_transactions t2 ON t2.id = l2.id_transaction INNER JOIN acc_accounts a2 ON a2.id = l2.id_account WHERE a2.position = %d AND l2.id_analytical = l.id_analytical AND t2.id_year = t.id_year) * -1 AS sum_expense, (SELECT SUM(l2.credit - l2.debit) FROM acc_transactions_lines l2 INNER JOIN acc_transactions t2 ON t2.id = l2.id_transaction INNER JOIN acc_accounts a2 ON a2.id = l2.id_account |
︙ | ︙ | |||
103 104 105 106 107 108 109 110 111 112 113 114 115 116 | $total = function (\stdClass $current, bool $by_year) use ($sums) { $out = (object) [ 'label' => 'Total', 'id_account' => $by_year ? null : $current->id, 'id_year' => $by_year ? $current->id : null, ]; foreach ($sums as $s) { $out->{$s} = $current->{$s}; } return $out; | > | 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | $total = function (\stdClass $current, bool $by_year) use ($sums) { $out = (object) [ 'label' => 'Total', 'id_account' => $by_year ? null : $current->id, 'id_year' => $by_year ? $current->id : null, 'total' => 1, ]; foreach ($sums as $s) { $out->{$s} = $current->{$s}; } return $out; |
︙ | ︙ | |||
361 362 363 364 365 366 367 368 369 | * Grand livre */ static public function getGeneralLedger(array $criterias): \Generator { $where = self::getWhereClause($criterias); $db = DB::getInstance(); $sql = sprintf('SELECT | > > > > > > > | | | | 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 | * Grand livre */ static public function getGeneralLedger(array $criterias): \Generator { $where = self::getWhereClause($criterias); $db = DB::getInstance(); if (!empty($criterias['analytical_only'])) { $join = 'acc_accounts a ON a.id = l.id_analytical'; } else { $join = 'acc_accounts a ON a.id = l.id_account'; } $sql = sprintf('SELECT t.id_year, a.id AS id_account, t.id, t.date, t.reference, l.debit, l.credit, l.reference AS line_reference, t.label, l.label AS line_label, a.label AS account_label, a.code AS account_code FROM acc_transactions t INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id INNER JOIN %s WHERE %s ORDER BY a.code COLLATE NOCASE, t.date, t.id;', $join, $where); $account = null; $debit = $credit = 0; foreach ($db->iterate($sql) as $row) { if (null !== $account && $account->id != $row->id_account) { yield $account; |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Transactions.php from [b8d932894e] to [f117213851].
︙ | ︙ | |||
31 32 33 34 35 36 37 | 'reference' => 'Numéro pièce comptable', 'p_reference' => 'Référence paiement', 'debit_account' => 'Compte de débit', 'credit_account' => 'Compte de crédit', 'amount' => 'Montant', ]; | | | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 'reference' => 'Numéro pièce comptable', 'p_reference' => 'Référence paiement', 'debit_account' => 'Compte de débit', 'credit_account' => 'Compte de crédit', 'amount' => 'Montant', ]; const MANDATORY_CSV_COLUMNS = ['label', 'date', 'credit_account', 'debit_account', 'amount']; static public function get(int $id) { return EntityManager::findOneById(Transaction::class, $id); } static public function saveReconciled(\Generator $journal, ?array $checked) |
︙ | ︙ |
Modified src/include/lib/Garradin/DB.php from [7ce0e3f7d3] to [ba3c284bd4].
︙ | ︙ | |||
208 209 210 211 212 213 214 | * @see https://www.sqlite.org/c3ref/strlike.html * @see https://sqlite.org/src/file?name=ext/icu/icu.c&ci=trunk */ static public function unicodeLike($pattern, $value, $escape = null) { $id = md5($pattern . $escape); if (!array_key_exists($id, self::$unicode_patterns_cache)) { | < > | | > > > > > > > > | > > | > > > > | 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 | * @see https://www.sqlite.org/c3ref/strlike.html * @see https://sqlite.org/src/file?name=ext/icu/icu.c&ci=trunk */ static public function unicodeLike($pattern, $value, $escape = null) { $id = md5($pattern . $escape); if (!array_key_exists($id, self::$unicode_patterns_cache)) { $escape = $escape ? '(?!' . preg_quote($escape, '/') . ')' : ''; preg_match_all('/('.$escape.'[%_])|(\w+)|(.+?)/iu', $pattern, $parts, PREG_SET_ORDER); $pattern = ''; foreach ($parts as $part) { if (isset($part[3])) { $pattern .= preg_quote($part[0], '/'); } elseif (isset($part[2])) { $pattern .= Utils::unicodeCaseFold($part[2]); } elseif ($part[1] == '%') { $pattern .= '.*'; } elseif ($part[1] == '_') { $pattern .= '.'; } } $pattern = '/^' . $pattern . '$/im'; self::$unicode_patterns_cache[$id] = $pattern; } $value = Utils::unicodeCaseFold($value); return (bool) preg_match(self::$unicode_patterns_cache[$id], $value); } } |
Modified src/include/lib/Garradin/Entities/Files/File.php from [f33e63dac6] to [2f3a044bb4].
︙ | ︙ | |||
129 130 131 132 133 134 135 | public function selfCheck(): void { $this->assert($this->type === self::TYPE_DIRECTORY || $this->type === self::TYPE_FILE, 'Unknown file type'); $this->assert($this->type === self::TYPE_DIRECTORY || $this->size !== null, 'File size must be set'); $this->assert($this->image === 0 || $this->image === 1, 'Unknown image value'); $this->assert(trim($this->name) !== '', 'Le nom de fichier ne peut rester vide'); $this->assert(strlen($this->path), 'Le chemin ne peut rester vide'); | | | 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | public function selfCheck(): void { $this->assert($this->type === self::TYPE_DIRECTORY || $this->type === self::TYPE_FILE, 'Unknown file type'); $this->assert($this->type === self::TYPE_DIRECTORY || $this->size !== null, 'File size must be set'); $this->assert($this->image === 0 || $this->image === 1, 'Unknown image value'); $this->assert(trim($this->name) !== '', 'Le nom de fichier ne peut rester vide'); $this->assert(strlen($this->path), 'Le chemin ne peut rester vide'); $this->assert(strlen($this->parent) || '' === $this->parent, 'Le chemin ne peut rester vide'); } public function context(): string { return strtok($this->path, '/'); } |
︙ | ︙ | |||
167 168 169 170 171 172 173 174 175 176 177 178 179 180 | Plugin::fireSignal('files.delete', ['file' => $this]); // clean up thumbnails foreach (self::ALLOWED_THUMB_SIZES as $size) { Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $size)); } if ($this->exists()) { return parent::delete(); } return true; } | > > | 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | Plugin::fireSignal('files.delete', ['file' => $this]); // clean up thumbnails foreach (self::ALLOWED_THUMB_SIZES as $size) { Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $size)); } DB::getInstance()->delete('files_search', 'path = ? OR path LIKE ?', $this->path, $this->path . '/%'); if ($this->exists()) { return parent::delete(); } return true; } |
︙ | ︙ | |||
685 686 687 688 689 690 691 | } public function fetch() { return Files::callStorage('fetch', $this); } | | | | 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 | } public function fetch() { return Files::callStorage('fetch', $this); } public function render(?string $user_prefix = null) { $editor_type = $this->renderFormat(); if ($editor_type == 'text') { return sprintf('<pre>%s</pre>', htmlspecialchars($this->fetch())); } elseif (!$editor_type) { throw new \LogicException('Cannot render file of this type'); } else { return Render::render($editor_type, $this, $this->fetch(), $user_prefix); } } public function checkReadAccess(?Session $session): bool { // Web pages and config files are always public if ($this->isPublic()) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Services/Fee.php from [0f3df37a82] to [7491493b33].
︙ | ︙ | |||
166 167 168 169 170 171 172 173 174 175 176 177 178 179 | $conditions = sprintf('su.id_fee = %d AND su.paid = 1 AND (su.expiry_date >= date() OR su.expiry_date IS NULL) AND m.id_category 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.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); | > > > > > | 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | $conditions = sprintf('su.id_fee = %d AND su.paid = 1 AND (su.expiry_date >= date() OR su.expiry_date IS NULL) AND m.id_category 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)'); $list->setExportCallback(function (&$row) { $row->paid_amount = $row->paid_amount ? Utils::money_format($row->paid_amount, '.', '', false) : null; }); return $list; } public function unpaidUsersList(): DynamicList { $list = $this->paidUsersList(); $conditions = sprintf('su.id_fee = %d AND su.paid = 0 AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Services/Service_User.php from [9cb86069a3] to [10de17a459].
︙ | ︙ | |||
111 112 113 114 115 116 117 | if ($this->fee()->label != $label) { $label .= ' - ' . $this->fee()->label; } $label .= sprintf(' (%s)', (new Membres)->getNom($this->id_user)); $source['label'] = $label; | < | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | if ($this->fee()->label != $label) { $label .= ' - ' . $this->fee()->label; } $label .= sprintf(' (%s)', (new Membres)->getNom($this->id_user)); $source['label'] = $label; $transaction->importFromNewForm($source); $transaction->save(); $transaction->linkToUser($this->id_user, $this->id()); return $transaction; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Web/Page.php from [7dc41152d2] to [a8ee380b83].
︙ | ︙ | |||
109 110 111 112 113 114 115 | return $this->_file; } public function load(array $data): void { parent::load($data); | | | 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | return $this->_file; } public function load(array $data): void { parent::load($data); if ($this->file() && $this->file()->modified > $this->modified) { $this->loadFromFile($this->file()); $this->save(); } } public function url(): string { |
︙ | ︙ | |||
133 134 135 136 137 138 139 | { $out = $this->asArray(); $out['url'] = $this->url(); $out['html'] = $this->render(); return $out; } | | | | | 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 | { $out = $this->asArray(); $out['url'] = $this->url(); $out['html'] = $this->render(); return $out; } public function render(?string $user_prefix = null): string { if (!$this->file()) { throw new \LogicException('File does not exist: ' . $this->file_path); } return Render::render($this->format, $this->file(), $this->content, $user_prefix); } public function preview(string $content): string { return Render::render($this->format, $this->file(), $content, '#'); } public function filepath(bool $stored = true): string { return $stored && isset($this->file_path) ? $this->file_path : File::CONTEXT_WEB . '/' . $this->path . '/' . $this->_name; } |
︙ | ︙ | |||
181 182 183 184 185 186 187 188 189 190 191 192 193 194 | $this->_file = null; } $file = $this->file(); // Or update file if ($file->fetch() !== $export) { $file->store(null, $export); } } $this->syncSearch(); } | > | 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | $this->_file = null; } $file = $this->file(); // Or update file if ($file->fetch() !== $export) { $file->set('modified', $this->modified); $file->store(null, $export); } } $this->syncSearch(); } |
︙ | ︙ | |||
202 203 204 205 206 207 208 209 210 211 212 213 214 215 | { $change_parent = null; if (isset($this->_modified['uri']) || isset($this->_modified['path'])) { $this->set('file_path', $this->filepath(false)); $change_parent = $this->_modified['path']; } $current_path = $this->_modified['file_path'] ?? $this->file_path; parent::save(); $this->syncFile($current_path); // Rename/move children if ($change_parent) { | > > > > > | 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | { $change_parent = null; if (isset($this->_modified['uri']) || isset($this->_modified['path'])) { $this->set('file_path', $this->filepath(false)); $change_parent = $this->_modified['path']; } // Update modified date if required if (count($this->_modified) && !isset($this->_modified['modified'])) { $this->set('modified', new \DateTime); } $current_path = $this->_modified['file_path'] ?? $this->file_path; parent::save(); $this->syncFile($current_path); // Rename/move children if ($change_parent) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Entity.php from [722d6cf126] to [46fdcfd8d1].
︙ | ︙ | |||
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 | // Add plugin signals to save/delete public function save(): bool { $name = get_class($this); $name = str_replace('Garradin\Entities', '', $name); $name = 'entity.' . $name . '.save'; if (Plugin::fireSignal($name . '.before', ['entity' => $this])) { return true; } $return = parent::save(); Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]); return $return; } public function delete(): bool { $name = get_class($this); $name = str_replace('Garradin\Entities', '', $name); $name = 'entity.' . $name . '.delete'; if (Plugin::fireSignal($name . '.before', ['entity' => $this])) { return true; } $return = parent::delete(); Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]); return $return; } } | > > > > > > > > > > > > > > | 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 | // Add plugin signals to save/delete public function save(): bool { $name = get_class($this); $name = str_replace('Garradin\Entities', '', $name); $name = 'entity.' . $name . '.save'; // Specific entity signal if (Plugin::fireSignal($name . '.before', ['entity' => $this])) { return true; } // Generic entity signal if (Plugin::fireSignal('entity.save.before', ['entity' => $this])) { return true; } $return = parent::save(); Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]); Plugin::fireSignal('entity.save.after', ['entity' => $this, 'success' => $return]); return $return; } public function delete(): bool { $name = get_class($this); $name = str_replace('Garradin\Entities', '', $name); $name = 'entity.' . $name . '.delete'; if (Plugin::fireSignal($name . '.before', ['entity' => $this])) { return true; } // Generic entity signal if (Plugin::fireSignal('entity.delete.before', ['entity' => $this])) { return true; } $return = parent::delete(); Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]); Plugin::fireSignal('entity.delete.after', ['entity' => $this, 'success' => $return]); return $return; } } |
Modified src/include/lib/Garradin/Files/Files.php from [893a79714f] to [9196c1dfe3].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?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 KD2\ZipWriter; | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin\Files; use Garradin\Static_Cache; use Garradin\DB; use Garradin\Utils; use Garradin\UserException; use Garradin\ValidationException; use Garradin\Membres\Session; use Garradin\Entities\Files\File; use Garradin\Entities\Web\Page; use KD2\DB\EntityManager as EM; use KD2\ZipWriter; |
︙ | ︙ | |||
151 152 153 154 155 156 157 | call_user_func([$to, 'lock']); $db = DB::getInstance(); $db->begin(); $i = 0; self::migrateDirectory($from, $to, '', $i, $callback); | | | > > > > > > > | 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 | call_user_func([$to, 'lock']); $db = DB::getInstance(); $db->begin(); $i = 0; self::migrateDirectory($from, $to, '', $i, $callback); } catch (UserException $e) { throw new \RuntimeException('Migration failed', 0, $e); } finally { $db->commit(); call_user_func([$from, 'unlock']); call_user_func([$to, 'unlock']); } } static protected function migrateDirectory(string $from, string $to, string $path, int &$i, ?callable $callback) { $db = DB::getInstance(); foreach (call_user_func([$from, 'list'], $path) as $file) { if (!$file->parent && $file->name == '.lock') { // Ignore lock file continue; } if (++$i >= 100) { $db->commit(); $db->begin(); $i = 0; } if ($file->type == File::TYPE_DIRECTORY) { |
︙ | ︙ | |||
282 283 284 285 286 287 288 | return $quota; } static public function getRemainingQuota(bool $force_refresh = false): float { if (FILE_STORAGE_QUOTA !== null) { | | > > | | > | 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | return $quota; } static public function getRemainingQuota(bool $force_refresh = false): float { if (FILE_STORAGE_QUOTA !== null) { $quota = FILE_STORAGE_QUOTA - self::getUsedQuota($force_refresh); } else { $quota = self::callStorage('getRemainingQuota'); } return max(0, $quota); } static public function checkQuota(int $size = 0): void { if (!self::$quota) { return; } |
︙ | ︙ | |||
320 321 322 323 324 325 326 | if (FILE_STORAGE_BACKEND == 'SQLite') { return 'files'; } return 'tmp_files'; } | | > > > > > > > > > > | 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 | if (FILE_STORAGE_BACKEND == 'SQLite') { return 'files'; } return 'tmp_files'; } static public function syncVirtualTable(string $parent = '', bool $recursive = false) { if (FILE_STORAGE_BACKEND == 'SQLite') { // No need to create a virtual table, use the real one return; } $db = DB::getInstance(); $db->begin(); $db->exec('CREATE TEMP TABLE IF NOT EXISTS tmp_files AS SELECT * FROM files WHERE 0;'); foreach (Files::list($parent) as $file) { // Ignore additional directories if ($parent == '' && !array_key_exists($file->name, File::CONTEXTS_NAMES)) { continue; } $db->insert('tmp_files', $file->asArray(true)); if ($recursive && $file->type === $file::TYPE_DIRECTORY) { self::syncVirtualTable($file->path, $recursive); } } $db->commit(); } } |
Modified src/include/lib/Garradin/Files/Storage/FileSystem.php from [33ec3a8fb7] to [286067b967].
︙ | ︙ | |||
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 | $data['modified']->setTimeZone(new \DateTimeZone(date_default_timezone_get())); $data['image'] = (int) in_array($data['mime'], File::IMAGE_TYPES); $file = new File; $file->load($data); return $file; } static public function list(string $path): array { $fullpath = self::_getRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path); $fullpath = rtrim($fullpath, DIRECTORY_SEPARATOR); if (!file_exists($fullpath)) { return []; } $files = []; foreach (new \FilesystemIterator($fullpath, \FilesystemIterator::SKIP_DOTS) as $file) { // Used to make sorting easier // directory_blabla // file_image.jpeg $files[$file->getType() . '_' .$file->getFilename()] = self::_SplToFile($file); } return Utils::knatcasesort($files); | > > > > > > | 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 | $data['modified']->setTimeZone(new \DateTimeZone(date_default_timezone_get())); $data['image'] = (int) in_array($data['mime'], File::IMAGE_TYPES); $file = new File; $file->load($data); $file->parent = $parent; // Force empty parent to be empty, not null return $file; } static public function list(string $path): array { $fullpath = self::_getRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path); $fullpath = rtrim($fullpath, DIRECTORY_SEPARATOR); if (!file_exists($fullpath)) { return []; } $files = []; foreach (new \FilesystemIterator($fullpath, \FilesystemIterator::SKIP_DOTS) as $file) { // Seems that SKIP_DOTS does not work all the time? if ($file->getFilename()[0] == '.') { continue; } // Used to make sorting easier // directory_blabla // file_image.jpeg $files[$file->getType() . '_' .$file->getFilename()] = self::_SplToFile($file); } return Utils::knatcasesort($files); |
︙ | ︙ | |||
301 302 303 304 305 306 307 | } static public function checkLock(): void { $lock = file_exists(self::_getRoot() . DIRECTORY_SEPARATOR . '.lock'); if ($lock) { | | | 307 308 309 310 311 312 313 314 315 316 317 | } static public function checkLock(): void { $lock = file_exists(self::_getRoot() . DIRECTORY_SEPARATOR . '.lock'); if ($lock) { throw new \RuntimeException('FileSystem storage is locked'); } } } |
Modified src/include/lib/Garradin/Files/Storage/SQLite.php from [ce000d8bf2] to [69084d5de4].
︙ | ︙ | |||
127 128 129 130 131 132 133 | static public function listDirectoriesRecursively(string $path): array { $files = []; $it = DB::getInstance()->iterate('SELECT path FROM files WHERE parent LIKE ? ORDER BY path;', $path . '/%'); foreach ($it as $file) { | | | 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | static public function listDirectoriesRecursively(string $path): array { $files = []; $it = DB::getInstance()->iterate('SELECT path FROM files WHERE parent LIKE ? ORDER BY path;', $path . '/%'); foreach ($it as $file) { $files[] = substr($file->path, strlen($path) + 1); } return $files; } static public function exists(string $path): bool { |
︙ | ︙ |
Modified src/include/lib/Garradin/Plugin.php from [2c3e05e869] to [d9b7c0cb75].
︙ | ︙ | |||
725 726 727 728 729 730 731 | { throw new \RuntimeException('Le fichier garradin_plugin.ini ne contient pas d\'entrée "'.$key.'".'); } } if (!empty($infos->min_version) && !version_compare(garradin_version(), $infos->min_version, '>=')) { | | | | 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 | { throw new \RuntimeException('Le fichier garradin_plugin.ini ne contient pas d\'entrée "'.$key.'".'); } } if (!empty($infos->min_version) && !version_compare(garradin_version(), $infos->min_version, '>=')) { throw new UserException('Le plugin '.$id.' nécessite Garradin version '.$infos->min_version.' ou supérieure.'); } if (!empty($infos->max_version) && !version_compare(garradin_version(), $infos->max_version, '>')) { throw new UserException('Le plugin '.$id.' nécessite Garradin version '.$infos->max_version.' ou inférieure.'); } if (!empty($infos->menu) && !file_exists($path . '/www/admin/index.php')) { throw new \RuntimeException('Le plugin '.$id.' ne comporte pas de fichier www/admin/index.php alors qu\'il demande à figurer au menu.'); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Recherche.php from [ee18756339] to [5e796b3eb4].
︙ | ︙ | |||
444 445 446 447 448 449 450 | } if (!array_key_exists($condition['column'], $target_columns)) { // Ignorer une condition qui se rapporte à une colonne // qui n'existe pas, cas possible si on reprend une recherche // après avoir modifié les fiches de membres | | | 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 | } if (!array_key_exists($condition['column'], $target_columns)) { // Ignorer une condition qui se rapporte à une colonne // qui n'existe pas, cas possible si on reprend une recherche // après avoir modifié les fiches de membres continue; } $query_columns[] = $condition['column']; $column = $target_columns[$condition['column']]; $query = sprintf('%s %s', $db->quoteIdentifier($condition['column']), $condition['operator']); |
︙ | ︙ | |||
518 519 520 521 522 523 524 | { $query_groups[] = implode(' ' . $group['operator'] . ' ', $query_group_conditions); } } if (!count($query_groups)) { | | | 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 | { $query_groups[] = implode(' ' . $group['operator'] . ' ', $query_group_conditions); } } if (!count($query_groups)) { throw new UserException('Aucune clause trouvée dans la recherche : elle contenait peut-être des clauses qui correspondent à des champs qui ont été supprimés ?'); } // Ajout du champ identité si pas présent if ($target == 'membres') { $query_columns = array_merge([$config->get('champ_identite')], $query_columns); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Template.php from [7374ce76d8] to [426892fe1f].
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 28 29 | { static protected $_instance = null; static public function getInstance() { return self::$_instance ?: self::$_instance = new Template; } private function __clone() { } public function __construct() { | > > > > > > > > > > > > > > > > > > > > | 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 | { static protected $_instance = null; static public function getInstance() { return self::$_instance ?: self::$_instance = new Template; } public function display($template = null) { if (isset($_GET['_pdf'])) { $out = $this->fetch($template); $filename = 'Print.pdf'; if (preg_match('!<title>(.*)</title>!U', $out, $match)) { $filename = trim($match[1]) . '.pdf'; } header('Content-type: application/pdf'); header(sprintf('Content-Disposition: attachment; filename="%s"', Utils::safeFileName($filename))); Utils::streamPDF($out); return $this; } return parent::display($template); } private function __clone() { } public function __construct() { |
︙ | ︙ | |||
94 95 96 97 98 99 100 | $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) { | | | 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | $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) { $skriv = new Skriv; return $skriv->render((string) $str); }); foreach (CommonModifiers::MODIFIERS_LIST as $key => $name) { $this->register_modifier(is_int($key) ? $name : $key, is_int($key) ? [CommonModifiers::class, $name] : $name); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Upgrade.php from [0b2351ebf8] to [a3d7e4ed57].
︙ | ︙ | |||
55 56 57 58 59 60 61 | try { if (version_compare($v, '1.0.0-rc1', '<')) { $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0_migration.sql'); $db->commitSchemaUpdate(); | < < < < < < < < | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | try { if (version_compare($v, '1.0.0-rc1', '<')) { $db->beginSchemaUpdate(); $db->import(ROOT . '/include/data/1.0.0_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'); |
︙ | ︙ | |||
318 319 320 321 322 323 324 325 326 327 328 329 330 331 | \Garradin\Web\Web::sync(); // Add UNIQUE index $db->import(ROOT . '/include/data/1.1.8_migration.sql'); $db->commit(); } // Vérification de la cohérence des clés étrangères $db->foreignKeyCheck(); // Delete local cached files Utils::resetCache(USER_TEMPLATES_CACHE_ROOT); Utils::resetCache(STATIC_CACHE_ROOT); | > > > > > > > > > > > > > > | 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 | \Garradin\Web\Web::sync(); // Add UNIQUE index $db->import(ROOT . '/include/data/1.1.8_migration.sql'); $db->commit(); } if (version_compare($v, '1.1.8', '==')) { // Force sync to add missing pages if you had the buggy 1.1.8 version \Garradin\Web\Web::sync(true); } if (version_compare($v, '1.1.10', '<')) { \Garradin\Web\Web::sync(true); // Force sync of web pages Files::syncVirtualTable('', true); $db->begin(); $db->exec(sprintf('DELETE FROM files_search WHERE path NOT IN (SELECT path FROM %s);', Files::getVirtualTableName())); $db->commit(); } // Vérification de la cohérence des clés étrangères $db->foreignKeyCheck(); // Delete local cached files Utils::resetCache(USER_TEMPLATES_CACHE_ROOT); Utils::resetCache(STATIC_CACHE_ROOT); |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Modifiers.php from [e9e89801c5] to [e398fea153].
︙ | ︙ | |||
112 113 114 115 116 117 118 | else { return '<a href="'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'">'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'</a>'; } } static public function atom_date($date) { | | | 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | 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, DATE_ATOM); } static public function xml_escape($str) { return htmlspecialchars($str, ENT_XML1); } } |
Modified src/include/lib/Garradin/UserTemplate/Sections.php from [9a0667f343] to [b168f12ea3].
︙ | ︙ | |||
291 292 293 294 295 296 297 298 299 300 301 302 303 304 | } // Allow for count=true, count=1 and also count="DISTINCT user_id" count="id" if (!empty($params['count'])) { $params['select'] = sprintf('COUNT(%s) AS count', $params['count'] == 1 ? '*' : $params['count']); $params['order'] = '1'; } $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'], | > > > > | 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | } // Allow for count=true, count=1 and also count="DISTINCT user_id" count="id" if (!empty($params['count'])) { $params['select'] = sprintf('COUNT(%s) AS count', $params['count'] == 1 ? '*' : $params['count']); $params['order'] = '1'; } if (!empty($params['where']) && !preg_match('/^\s*AND\s+/i', $params['where'])) { $params['where'] = ' AND ' . $params['where']; } $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'], |
︙ | ︙ |
Modified src/include/lib/Garradin/Utils.php from [87963c7e33] to [2a526310a9].
︙ | ︙ | |||
14 15 16 17 18 19 20 | const EMAIL_CONTEXT_PRIVATE = 'private'; const EMAIL_CONTEXT_SYSTEM = 'system'; static protected $collator; static protected $transliterator; const FRENCH_DATE_NAMES = [ | > | > > > > > > | > > > > > | | > > > > | < > > > > > > > > > > > > > > > > > | 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 | const EMAIL_CONTEXT_PRIVATE = 'private'; const EMAIL_CONTEXT_SYSTEM = 'system'; static protected $collator; static protected $transliterator; 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', 'Jan' => 'jan', 'Feb' => 'fév', 'Mar' => 'mar', 'Apr' => 'avr', 'Jun' => 'juin', 'Jul' => 'juil', 'Aug' => 'août', 'Sep' => 'sep', 'Oct' => 'oct', 'Nov' => 'nov', 'Dec' => 'déc', 'Mon' => 'lun', 'Tue' => 'mar', 'Wed' => 'mer', 'Thu' => 'jeu', 'Fri' => 'ven', 'Sat' => 'sam', 'Sun' => 'dim', ]; static public function get_datetime($ts) { if (is_object($ts) && $ts instanceof \DateTimeInterface) { return $ts; } elseif (is_numeric($ts)) { |
︙ | ︙ | |||
53 54 55 56 57 58 59 | if (null === $ts) { return $ts; } $date = strftime($format, $ts->getTimestamp()); $date = strtr($date, self::FRENCH_DATE_NAMES); | < < | 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 | if (null === $ts) { return $ts; } $date = strftime($format, $ts->getTimestamp()); $date = strtr($date, self::FRENCH_DATE_NAMES); return $date; } static public function date_fr($ts, $format = null) { $ts = self::get_datetime($ts); if (null === $ts) { return $ts; } if (is_null($format)) { $format = 'd/m/Y à H:i'; } $date = $ts->format($format); $date = strtr($date, self::FRENCH_DATE_NAMES); return $date; } /** * @deprecated */ static public function checkDate($str) |
︙ | ︙ | |||
862 863 864 865 866 867 868 869 870 871 872 873 874 875 | $str = preg_replace('![^\w\d_-]!i', '-', $str); $str = preg_replace('!-{2,}!', '-', $str); $str = trim($str, '-'); return $str; } /** * dirname may have undefined behaviour depending on the locale! */ static public function dirname(string $str): string { $str = str_replace(DIRECTORY_SEPARATOR, '/', $str); | > > > > > > > > > | 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 | $str = preg_replace('![^\w\d_-]!i', '-', $str); $str = preg_replace('!-{2,}!', '-', $str); $str = trim($str, '-'); return $str; } static public function safeFileName(string $str): string { $str = Utils::transliterateToAscii($str); $str = preg_replace('![^\w\d_ -]!i', '.', $str); $str = preg_replace('!\.{2,}!', '.', $str); $str = trim($str, '.'); return $str; } /** * dirname may have undefined behaviour depending on the locale! */ static public function dirname(string $str): string { $str = str_replace(DIRECTORY_SEPARATOR, '/', $str); |
︙ | ︙ | |||
920 921 922 923 924 925 926 | static public function unicodeCaseFold(?string $str): string { if (null === $str || trim($str) === '') { return ''; } if (!isset(self::$transliterator) && function_exists('transliterator_create')) { | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 | static public function unicodeCaseFold(?string $str): string { if (null === $str || trim($str) === '') { return ''; } if (!isset(self::$transliterator) && function_exists('transliterator_create')) { self::$transliterator = \Transliterator::create('Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; Lower();'); } if (isset(self::$transliterator)) { return self::$transliterator->transliterate($str); } return strtoupper(self::transliterateToAscii($str)); } static public function knatcasesort(array $array) { uksort($array, [self::class, 'unicodeCaseComparison']); return $array; } /** * Displays a PDF from a string, only works when PDF_COMMAND constant is set to "prince" * @param string $str HTML string * @return void */ static public function streamPDF(string $str): void { if (!PDF_COMMAND) { // Try to see if there's a plugin $in = ['string' => $str]; if (Plugin::fireSignal('pdf.stream', $in)) { return; } unset($in); } // Only Prince handles using STDIN and STDOUT if (PDF_COMMAND != 'prince') { $file = self::filePDF($str); readfile($file); unlink($file); return; } $descriptorspec = [ 0 => ["pipe", "r"], // stdin is a pipe that the child will read from 1 => ["pipe", "w"], // stdout is a pipe that the child will write to 2 => ['pipe', 'w'], // stderr ]; $cmd = 'prince -o - -'; $process = proc_open($cmd, $descriptorspec, $pipes); if (!is_resource($process)) { throw new \RuntimeException('Cannot execute Prince XML'); } // $pipes now looks like this: // 0 => writeable handle connected to child stdin // 1 => readable handle connected to child stdout fwrite($pipes[0], $str); fclose($pipes[0]); echo stream_get_contents($pipes[1]); fclose($pipes[1]); // It is important that you close any pipes before calling // proc_close in order to avoid a deadlock proc_close($process); } /** * Creates a PDF file from a HTML string * @param string $str HTML string * @return string File path of the PDF file (temporary), you must delete or move it */ static public function filePDF(string $str): ?string { $source = sprintf('%s/print-%s.html', CACHE_ROOT, md5(random_bytes(16))); $target = str_replace('.html', '.pdf', $source); file_put_contents($source, $str); $cmd = PDF_COMMAND; if (!$cmd) { // Try to see if there's a plugin $in = ['source' => $source, 'target' => $target]; if (Plugin::fireSignal('pdf.create', $in)) { return $target; } unset($in); // Try to find a local executable $list = ['prince', 'chromium', 'wkhtmltopdf', 'weasyprint']; foreach ($list as $program) { if (shell_exec('which ' . $program)) { $cmd = $program; break; } } // We still haven't found anything if (!$cmd) { throw new \LogicException('No PDF creation executable found. Please install or configure one.'); } } switch ($cmd) { case 'prince': $cmd = 'prince -o %2$s %1$s'; break; case 'chromium': $cmd = 'chromium --headless --disable-gpu --run-all-compositor-stages-before-draw --print-to-pdf-no-header --print-to-pdf=%s %s'; break; case 'wkhtmltopdf': $cmd = 'wkhtmltopdf %1$s %2$s'; break; case 'weasyprint': $cmd = 'weasyprint %1$s %2$s'; break; default: break; } exec(sprintf($cmd, escapeshellarg($source), escapeshellarg($target))); if (!file_exists($target)) { throw new \RuntimeException('PDF command failed'); } unlink($source); return $target; } } |
Modified src/include/lib/Garradin/Web/Render/AbstractRender.php from [c4980b1d31] to [5c2f7cfe96].
︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 18 | abstract class AbstractRender { protected $current_path; protected $context; protected $link_prefix; protected $link_suffix; protected $file; | > | | > | > | | | | < < < | < | | < | | | | | 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 | abstract class AbstractRender { protected $current_path; protected $context; protected $link_prefix; protected $link_suffix; protected $user_prefix; protected $file; public function __construct(?File $file = null, ?string $user_prefix = null) { $this->file = $file; if ($file) { $this->isRelativeTo($file); } $this->user_prefix = $user_prefix; } abstract public function render(?string $content = null): string; protected function resolveAttachment(string $uri) { $prefix = $this->current_path; $pos = strpos($uri, '/'); // Absolute URL: treat it as absolute! if ($pos === 0) { return WWW_URL . ltrim($uri, '/'); } // Handle relative URIs return WWW_URL . $prefix . '/' . $uri; } protected function resolveLink(string $uri) { $first = substr($uri, 0, 1); if ($first == '/' || $first == '!') { return Utils::getLocalURL($uri); } if (strpos(Utils::basename($uri), '.') === false) { $uri .= $this->link_suffix; } return $this->link_prefix . $uri; } public function isRelativeTo(File $file) { $this->current_path = $file->parent; $this->context = $file->context(); $this->link_suffix = ''; if ($this->context === File::CONTEXT_WEB) { $this->link_prefix = $this->user_prefix ?? WWW_URL; $this->current_path = Utils::basename(Utils::dirname($file->path)); } else { $this->link_prefix = $this->user_prefix ?? sprintf(ADMIN_URL . 'common/files/preview.php?p=%s/', $this->context); $this->link_suffix = '.skriv'; } } } |
Modified src/include/lib/Garradin/Web/Render/Markdown.php from [7e10d16363] to [1e59334a43].
1 2 3 4 5 6 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; | < < < < | < | < < < | | | | | < | | | | 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 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; use Garradin\Plugin; use Garradin\Utils; use Garradin\Files\Files; use Garradin\UserTemplate\CommonModifiers; use const Garradin\{ADMIN_URL, WWW_URL}; class Markdown extends AbstractRender { public function render(?string $content = null): string { $parsedown = new Parsedown($this->file, $this->user_prefix); $parsedown->setBreaksEnabled(true); $parsedown->setUrlsLinked(true); $parsedown->setSafeMode(true); $str = $content ?? $this->file->fetch(); $str = $parsedown->text($str); $str = CommonModifiers::typo($str); $str = preg_replace_callback(';<a href="((?!https?://|\w+:|#).+)">;i', function ($matches) { return sprintf('<a href="%s" target="_parent">', $this->resolveLink($matches[1])); }, $str); return sprintf('<div class="web-content">%s</div>', $str); } } |
Modified src/include/lib/Garradin/Web/Render/Parsedown.php from [af58530681] to [8582efeb8b].
︙ | ︙ | |||
16 17 18 19 20 21 22 | * @see https://github.com/erusev/parsedown/wiki/Tutorial:-Create-Extensions */ class Parsedown extends Parent_Parsedown { protected $skriv; protected $toc = []; | | | | 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 | * @see https://github.com/erusev/parsedown/wiki/Tutorial:-Create-Extensions */ class Parsedown extends Parent_Parsedown { protected $skriv; protected $toc = []; function __construct(?File $file, ?string $user_prefix) { $this->BlockTypes['<'][] = 'SkrivExtension'; $this->BlockTypes['['][]= 'TOC'; # identify footnote definitions before reference definitions array_unshift($this->BlockTypes['['], 'Footnote'); # identify footnote markers before before links array_unshift($this->InlineTypes['['], 'FootnoteMarker'); $this->skriv = new Skriv($file, $user_prefix); } protected function blockSkrivExtension(array $line): ?array { $line = $line['text']; if (strpos($line, '<<') === 0 && preg_match('/^<<<?([a-z_]+)((?:(?!>>>?).)*?)(>>>?$|$)/i', trim($line), $match)) { |
︙ | ︙ | |||
148 149 150 151 152 153 154 | $block['footnotes'][$matches[1]] = $matches[2]; return $block; } end($block['footnotes']); $last = key($block['footnotes']); | | < < | | < | 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | $block['footnotes'][$matches[1]] = $matches[2]; return $block; } end($block['footnotes']); $last = key($block['footnotes']); if (isset($block['interrupted']) && $line['indent'] >= 4) { $block['footnotes'][$last] .= "\n\n" . $line['text']; return $block; } else { $block['footnotes'][$last] .= "\n" . $line['text']; return $block; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Render/Render.php from [a31683eb85] to [baee19544e].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; class Render { const FORMAT_SKRIV = 'skriv'; const FORMAT_ENCRYPTED = 'skriv/encrypted'; const FORMAT_MARKDOWN = 'markdown'; | | | | | | | 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\Web\Render; use Garradin\Entities\Files\File; class Render { const FORMAT_SKRIV = 'skriv'; const FORMAT_ENCRYPTED = 'skriv/encrypted'; const FORMAT_MARKDOWN = 'markdown'; static public function render(string $format, File $file, string $content = null, string $link_prefix = null) { if ($format == self::FORMAT_SKRIV) { $r = new Skriv($file, $link_prefix); } else if ($format == self::FORMAT_ENCRYPTED) { $r = new EncryptedSkriv($file, $link_prefix); } else if ($format == self::FORMAT_MARKDOWN) { $r = new Markdown($file, $link_prefix); } else { throw new \LogicException('Invalid format: ' . $format); } return $r->render($content); } } |
Modified src/templates/acc/accounts/reconcile_assist.tpl from [80420a98b7] to [d7fc6d0db8].
︙ | ︙ | |||
13 14 15 16 17 18 19 | <form method="post" action="{$self_url}" enctype="multipart/form-data"> {if !$csv->loaded()} <fieldset> <legend>Relevé de compte</legend> <p class="help block"> Le rapprochement assisté permet de s'aider d'un relevé de compte au format CSV pour trouver les écritures manquantes ou erronées.<br /> | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <form method="post" action="{$self_url}" enctype="multipart/form-data"> {if !$csv->loaded()} <fieldset> <legend>Relevé de compte</legend> <p class="help block"> Le rapprochement assisté permet de s'aider d'un relevé de compte au format CSV pour trouver les écritures manquantes ou erronées.<br /> <a href="https://garradin.eu/Rapprochement_assiste" target="_blank">Aide détaillée</a> </p> <dl> {include file="common/_csv_help.tpl"} {input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1} </dl> <p class="submit"> {csrf_field key=$csrf_key} |
︙ | ︙ |
Modified src/templates/acc/reports/_header.tpl from [8e4c7a5fd3] to [b23bfd50d9].
1 2 3 4 | <div class="year-header"> <nav class="tabs noprint"> <ul> | | | > > > > | | > | 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 | <div class="year-header"> <nav class="tabs noprint"> <ul> {if isset($analytical) || $current == 'analytical_ledger'} <li><strong><a href="{$admin_url}acc/reports/projects.php">Projets</a></strong></li> {/if} {if $current == 'analytical_ledger'} <li class="current"><a href="{$admin_url}acc/reports/ledger.php?{$criterias_query}">Grand livre analytique</a></li> {else} <li{if $current == "graphs"} class="current"{/if}><a href="{$admin_url}acc/reports/graphs.php?{$criterias_query}">Graphiques</a></li> <li{if $current == "trial_balance"} class="current"{/if}><a href="{$admin_url}acc/reports/trial_balance.php?{$criterias_query}">Balance générale</a></li> <li{if $current == "journal"} class="current"{/if}><a href="{$admin_url}acc/reports/journal.php?{$criterias_query}">Journal général</a></li> <li{if $current == "ledger"} class="current"{/if}><a href="{$admin_url}acc/reports/ledger.php?{$criterias_query}">Grand livre</a></li> <li{if $current == "statement"} class="current"{/if}><a href="{$admin_url}acc/reports/statement.php?{$criterias_query}">Compte de résultat</a></li> <li{if $current == "balance_sheet"} class="current"{/if}><a href="{$admin_url}acc/reports/balance_sheet.php?{$criterias_query}">Bilan</a></li> {/if} </ul> </nav> <h2>{$config.nom_asso} — {$title}</h2> {if isset($analytical)} <h3>Projet : {$analytical.label}</h3> {/if} {if isset($year)} <p>Exercice : {$year.label} ({if $year.closed}clôturé{else}en cours{/if}, du {$year.start_date|date_short} au {$year.end_date|date_short}, généré le {$close_date|date_short})</p> {/if} <p class="noprint print-btn"> <button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button> {linkbutton shape="download" href="%s&_pdf"|args:$self_url label="Télécharger en PDF"} </p> </div> |
Modified src/templates/acc/reports/ledger.tpl from [2309772ddd] to [5c2ec98ec8].
|
| > | | > > | > | > > > > > > > | 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 | {if !empty($criterias.analytical_only)} {include file="admin/_head.tpl" title="Grand livre analytique" current="acc/years"} {include file="acc/reports/_header.tpl" current="analytical_ledger" title="Grand livre analytique"} {else} {include file="admin/_head.tpl" title="Grand livre" current="acc/years"} {include file="acc/reports/_header.tpl" current="ledger" title="Grand livre"} {/if} <div class="year-header noprint"> <button type="button" data-icon="↓" class="icn-btn" id="open_details">Déplier tous les comptes</button> <button type="button" data-icon="↑" class="icn-btn" id="close_details">Replier tous les comptes</button> </div> {foreach from=$ledger item="account"} <details open="open"> <summary><h2 class="ruler"> {if !empty($criterias.analytical_only)} <?php $link = sprintf('%sacc/reports/trial_balance.php?analytical=%d&year=%d', $admin_url, $account->id, $account->id_year); ?> {else} <?php $link = sprintf('%sacc/reports/journal.php?id=%d&year=%d', $admin_url, $account->id, $account->id_year); ?> {/if} <a href="{$link}">{$account.code} — {$account.label}</a> </h2></summary> <table class="list"> <thead> <tr> <td></td> <td>N° pièce</td> <td>Réf. ligne</td> |
︙ | ︙ |
Modified src/templates/acc/reports/projects.tpl from [4800025ba6] to [c7e14dfc07].
︙ | ︙ | |||
53 54 55 56 57 58 59 | </th> </tr> {foreach from=$parent.items item="item"} <tr> <th>{$item.label}</th> <td> <span class="noprint"> | | | | | | | > > > | 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | </th> </tr> {foreach from=$parent.items item="item"} <tr> <th>{$item.label}</th> <td> <span class="noprint"> <a href="{$admin_url}acc/reports/graphs.php?analytical={$item.id_account}&year={$item.id_year}">Graphiques</a> | <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> {if $item.total && $by_year} | <a href="{$admin_url}acc/reports/ledger.php?analytical_only=1&year={$item.id_year}">Grand livre analytique</a> {/if} </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> |
︙ | ︙ |
Modified src/templates/acc/transactions/details.tpl from [0098f837e0] to [b3d4a382f1].
1 2 | {include file="admin/_head.tpl" title="Écriture n°%d"|args:$transaction.id current="acc"} | < > > > > < > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {include file="admin/_head.tpl" title="Écriture n°%d"|args:$transaction.id current="acc"} <nav class="tabs"> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} <aside>{linkbutton href="new.php?copy=%d"|args:$transaction.id shape="plus" label="Dupliquer cette écriture"}</aside> {/if} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$transaction->validated && !$tr_year->closed} <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> {/if} </nav> {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} |
︙ | ︙ |
Modified src/templates/acc/transactions/new.tpl from [49e96a9c33] to [91b454b44d].
︙ | ︙ | |||
64 65 66 67 68 69 70 | <legend>{$type.label}</legend> {if $type.id == $transaction::TYPE_ADVANCED} {* Saisie avancée *} {include file="acc/transactions/_lines_form.tpl" chart_id=$current_year.id_chart} {else} <dl> {foreach from=$type.accounts key="key" item="account"} | > | | 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | <legend>{$type.label}</legend> {if $type.id == $transaction::TYPE_ADVANCED} {* Saisie avancée *} {include file="acc/transactions/_lines_form.tpl" chart_id=$current_year.id_chart} {else} <dl> {foreach from=$type.accounts key="key" item="account"} <?php $selected = $types_accounts[$key] ?? null; ?> {input type="list" target="acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$account.targets_string,$chart_id name="account_%d_%d"|args:$type.id,$key label=$account.label required=1 default=$selected} {/foreach} </dl> {/if} </fieldset> {/foreach} {/if} |
︙ | ︙ |
Modified src/templates/acc/transactions/service_user.tpl from [31416dfe77] to [51d9da9e41].
1 2 | {include file="admin/_head.tpl" title="Écritures liées à une inscription" current="acc/accounts"} | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | {include file="admin/_head.tpl" title="Écritures liées à une inscription" current="acc/accounts"} <nav class="tabs"> {linkbutton href="!membres/fiche.php?id=%d"|args:$user_id label="Retour à la fiche membre" shape="user"} {linkbutton href="!services/payment.php?id=%d"|args:$service_user_id label="Nouveau règlement" shape="plus"} </nav> {include file="acc/reports/_journal.tpl"} <h2 class="ruler">Solde des comptes</h2> <table class="list"> <thead> |
︙ | ︙ |
Modified src/templates/acc/years/export.tpl from [f38c0c6bff] to [640274763b].
|
| | | 1 2 3 4 5 6 7 8 | {include file="admin/_head.tpl" title="Export d'exercice" current="acc/years"} <nav class="acc-year"> <h4>Exercice sélectionné :</h4> <h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3> </nav> <nav class="tabs"> |
︙ | ︙ |
Modified src/templates/admin/login.tpl from [011baee06b] to [f3e8752670].
︙ | ︙ | |||
40 41 42 43 44 45 46 | <span class="error">Connexion non-sécurisée !</span> <a href="{$own_https_url}">Se connecter en HTTPS (sécurisé)</a> {else} <span class="alert">Connexion non-sécurisée</span> {/if} {/if} </dd> | | | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | <span class="error">Connexion non-sécurisée !</span> <a href="{$own_https_url}">Se connecter en HTTPS (sécurisé)</a> {else} <span class="alert">Connexion non-sécurisée</span> {/if} {/if} </dd> {input type="checkbox" name="permanent" value="1" label="Rester connecté⋅e" help="recommandé seulement sur ordinateur personnel"} </dl> </fieldset> <p class="submit"> {csrf_field key="login"} {button type="submit" name="login" label="Se connecter" shape="right" class="main"} </p> |
︙ | ︙ |
Modified src/templates/common/files/edit_web.tpl from [5e930c6a6c] to [c721ff089b].
1 2 3 4 | {include file="admin/_head.tpl" title="Édition de fichier" custom_js=['wiki_editor.js']} <form method="post" action="{$self_url}"> <p class="textEditor"> | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | {include file="admin/_head.tpl" title="Édition de fichier" custom_js=['wiki_editor.js']} <form method="post" action="{$self_url}"> <p class="textEditor"> {input type="textarea" name="content" cols="70" rows="30" default=$content data-preview-url="!common/files/_preview.php?f=%s"|local_url|args:$file.path data-fullscreen="1" data-attachments="0" data-savebtn="1" data-format=$file->renderFormat()} </p> <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/docs/index.tpl from [586034f95e] to [ce2d8a6b1b].
1 2 3 4 5 6 7 8 9 10 11 | <?php use Garradin\Entities\Files\File; ?> {include file="admin/_head.tpl" title="Documents" current="docs"} <nav class="tabs"> <aside> {if $context == File::CONTEXT_DOCUMENTS} {linkbutton shape="search" label="Rechercher" href="search.php" target="_dialog"} {/if} {if $can_mkdir} | | | | | | | | | 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 use Garradin\Entities\Files\File; ?> {include file="admin/_head.tpl" title="Documents" current="docs"} <nav class="tabs"> <aside> {if $context == File::CONTEXT_DOCUMENTS} {linkbutton shape="search" label="Rechercher" href="search.php" target="_dialog"} {/if} {if $can_mkdir} {linkbutton shape="plus" label="Nouveau répertoire" target="_dialog" href="!docs/new_dir.php?path=%s"|args:$path} {/if} {if $can_upload} {linkbutton shape="plus" label="Nouveau fichier texte" target="_dialog" href="!docs/new_file.php?path=%s"|args:$path} {linkbutton shape="upload" label="Ajouter un fichier" target="_dialog" href="!common/files/upload.php?p=%s"|args:$path} {/if} </aside> <ul> <li{if $context == File::CONTEXT_DOCUMENTS} class="current"{/if}><a href="./">Documents</a></li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)} <li{if $context == File::CONTEXT_TRANSACTION} class="current"{/if}><a href="./?path=<?=File::CONTEXT_TRANSACTION?>">Fichiers des écritures</a></li> {/if} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)} <li{if $context == File::CONTEXT_USER} class="current"{/if}><a href="./?path=<?=File::CONTEXT_USER?>">Fichiers des membres</a></li> {/if} {if $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)} <li{if $context == File::CONTEXT_SKELETON} class="current"{/if}><a href="./?path=<?=File::CONTEXT_SKELETON?>">Squelettes du site web</a></li> {/if} </ul> </nav> <nav class="breadcrumbs"> {if count($breadcrumbs) > 1} {linkbutton href="?path=%s"|args:$parent_path label="Retour au répertoire parent" shape="left"} {/if} {if $context == File::CONTEXT_TRANSACTION} {if $context_ref} {linkbutton href="!acc/transactions/details.php?id=%d"|args:$context_ref|local_url label="Détails de l'écriture" shape="menu"} {/if} {elseif $context == File::CONTEXT_USER} {if $context_ref} {linkbutton href="!membres/fiche.php?id=%d"|args:$context_ref|local_url label="Fiche du membre" shape="user"} {/if} {else} <ul> {foreach from=$breadcrumbs item="name" key="bc_path"} <li><a href="?path={$bc_path}">{$name}</a></li> {/foreach} </ul> {/if} <aside class="quota"> <h4><b>{$quota_left|size_in_bytes}</b> libres sur <i>{$quota_max|size_in_bytes}</i></h4> <span class="meter"><span style="width: {$quota_percent}%"></span></span> |
︙ | ︙ | |||
94 95 96 97 98 99 100 | {if $file.type == $file::TYPE_DIRECTORY} <tr> {if $can_delete} <td class="check"> {input type="checkbox" name="check[]" value=$file.path} </td> {/if} | | | 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | {if $file.type == $file::TYPE_DIRECTORY} <tr> {if $can_delete} <td class="check"> {input type="checkbox" name="check[]" value=$file.path} </td> {/if} <th><a href="?path={$file.path}">{$file.name}</a></th> <td></td> <td>Répertoire</td> <td></td> <td class="actions"> {if $can_write && ($context == File::CONTEXT_SKELETON || $context == File::CONTEXT_DOCUMENTS)} {linkbutton href="!common/files/rename.php?p=%s"|args:$file.path label="Renommer" shape="minus" target="_dialog"} {/if} |
︙ | ︙ | |||
176 177 178 179 180 181 182 | {include file="common/dynamic_list_head.tpl" check=false} {foreach from=$list->iterate() item="item"} <tr> {if $context == File::CONTEXT_TRANSACTION} <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$item.id}">#{$item.id}</a></td> | | | | | > > | | 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 | {include file="common/dynamic_list_head.tpl" check=false} {foreach from=$list->iterate() item="item"} <tr> {if $context == File::CONTEXT_TRANSACTION} <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$item.id}">#{$item.id}</a></td> <th><a href="?path={$item.path}">{$item.label}</a></th> <td>{$item.date|date_short}</td> <td>{$item.reference}</td> <td>{$item.year}</td> <td class="actions"> {linkbutton href="!docs/?path=%s"|args:$item.path label="Fichiers" shape="menu"} {linkbutton href="!acc/transactions/details.php?id=%d"|args:$item.id label="Écriture" shape="search"} </td> {else} <td class="num"><a href="{$admin_url}membres/fiche.php?id={$item.id}">#{$item.number}</a></td> <th><a href="?path={$item.path}">{$item.identity}</a></th> <td class="actions"> {linkbutton href="!docs/?path=%s"|args:$item.path label="Fichiers" shape="menu"} {linkbutton href="!membres/fiche.php?id=%d"|args:$item.id label="Fiche membre" shape="user"} </td> {/if} </tr> {/foreach} </tbody> </table> {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} {/if} <p class="actions"> {linkbutton href="!docs/zip.php?path=%s"|args:$path label="Télécharger ce répertoire (ZIP)" shape="download"} </p> </form> {else} <p class="alert block">Il n'y a aucun fichier dans ce répertoire.</p> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/services/fees/_fee_form.tpl from [60289da098] to [9d5c15a2e4].
︙ | ︙ | |||
24 25 26 27 28 29 30 | </dl> </dd> {input name="amount_type" type="radio" value="2" label="Montant variable" default=$amount_type} <dd class="amount_type_2"> <dl> {input name="formula" type="textarea" label="Formule de calcul" source=$fee fake_required=1} <dd class="help"> | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | </dl> </dd> {input name="amount_type" type="radio" value="2" label="Montant variable" default=$amount_type} <dd class="amount_type_2"> <dl> {input name="formula" type="textarea" label="Formule de calcul" source=$fee fake_required=1} <dd class="help"> <a href="https://garradin.eu/Formule-calcul-activite">Aide sur les formules de calcul</a> </dd> </dl> </dd> <dt><strong>Comptabilité</strong></dt> {input name="accounting" type="checkbox" value="1" label="Enregistrer en comptabilité" default=$accounting_enabled} <dd class="help">Laissez cette case décochée si vous n'utilisez pas Garradin pour la comptabilité. Il ne sera pas possible de suivre le montant des règlements effectués pour ce tarif.</dd> </dl> |
︙ | ︙ |
Modified src/templates/services/payment.tpl from [10d2082879] to [c4cd0c545e].
︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <legend>Enregistrer un règlement</legend> <dl> <dt>Membre sélectionné</dt> <dd><h3>{$user_name}</h3></dd> <dt><strong>Inscription</strong></dt> {input type="checkbox" name="paid" value="1" default=$su.paid label="Marquer cette inscription comme payée"} {input type="money" name="amount" label="Montant réglé par le membre" required=1} {input type="list" target="acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account" label="Compte de règlement" required=1} {input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de note de frais, etc."} {input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."} </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"} | > > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <legend>Enregistrer un règlement</legend> <dl> <dt>Membre sélectionné</dt> <dd><h3>{$user_name}</h3></dd> <dt><strong>Inscription</strong></dt> {input type="checkbox" name="paid" value="1" default=$su.paid label="Marquer cette inscription comme payée"} {input type="date" name="date" label="Date" required=1 source=$su} {input type="money" name="amount" label="Montant réglé par le membre" required=1} {input type="list" target="acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account" label="Compte de règlement" required=1} {input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de note de frais, etc."} {input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."} </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} {button type="submit" name="save_and_add_payment" label="Enregistrer et ajouter un autre règlement" shape="plus"} </p> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/services/save.tpl from [c8f34251fe] to [b1114e7276].
︙ | ︙ | |||
111 112 113 114 115 116 117 118 119 120 121 122 123 124 | {/if} </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {if $user_id} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} {else} {button type="submit" name="next" label="Continuer" shape="right" class="main"} {/if} </p> </form> | > | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | {/if} </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {if $user_id} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} {button type="submit" name="save_and_add_payment" class="accounting" label="Enregistrer et ajouter un autre règlement" shape="plus"} {else} {button type="submit" name="next" label="Continuer" shape="right" class="main"} {/if} </p> </form> |
︙ | ︙ |
Modified src/templates/services/user.tpl from [d3b73b33b5] to [6397212801].
︙ | ︙ | |||
24 25 26 27 28 29 30 | Aucune inscription. </dd> {/foreach} <dt>Nombre d'inscriptions pour ce membre</dt> <dd> {$list->count()} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} | | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | Aucune inscription. </dd> {/foreach} <dt>Nombre d'inscriptions pour ce membre</dt> <dd> {$list->count()} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} {linkbutton href="?id=%d&export=csv"|args:$user.id shape="export" label="Export CSV"} {linkbutton href="?id=%d&export=ods"|args:$user.id shape="export" label="Export tableur"} {/if} </dd> </dl> {include file="common/dynamic_list_head.tpl"} {foreach from=$list->iterate() item="row"} |
︙ | ︙ |
Modified src/templates/web/config.tpl from [cbe2f68864] to [6540f9cc0f].
︙ | ︙ | |||
118 119 120 121 122 123 124 | <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> <p> | | | 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | <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> <p> {linkbutton href="!docs/?path=skel" label="Gérer les fichiers de squelettes" shape="folder"} </p> </fieldset> </form> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/web/index.tpl from [387d7766db] to [38a56e5fed].
︙ | ︙ | |||
71 72 73 74 75 76 77 | </table> {/if} {if count($pages)} <h2 class="ruler">Pages</h2> <p> {if !$order_date} | | | | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | </table> {/if} {if count($pages)} <h2 class="ruler">Pages</h2> <p> {if !$order_date} {linkbutton shape="down" label="Trier par date" href="?p=%s&order_date"|args:$current_path} {else} {linkbutton shape="up" label="Trier par titre" href="?p=%s"|args:$current_path} {/if} </p> <table class="list"> <tbody> {foreach from=$pages item="p"} <tr> <th>{$p.title}</th> |
︙ | ︙ |
Modified src/www/admin/acc/reports/_inc.php from [923715a8c5] to [c7c07f3287].
︙ | ︙ | |||
31 32 33 34 35 36 37 38 39 40 41 42 43 44 | throw new UserException('Exercice inconnu.'); } $criterias['year'] = $year->id(); $tpl->assign('year', $year); $tpl->assign('close_date', $year->closed ? $year->end_date : time()); } if (!count($criterias)) { throw new UserException('Critère de rapport inconnu.'); } $tpl->assign('criterias_query', http_build_query($criterias)); | > > > > > | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | throw new UserException('Exercice inconnu.'); } $criterias['year'] = $year->id(); $tpl->assign('year', $year); $tpl->assign('close_date', $year->closed ? $year->end_date : time()); } if (qg('analytical_only')) { $criterias['analytical_only'] = true; } if (!count($criterias)) { throw new UserException('Critère de rapport inconnu.'); } $tpl->assign('criterias', $criterias); $tpl->assign('criterias_query', http_build_query($criterias)); |
Modified src/www/admin/acc/transactions/new.php from [d8191985b4] to [c93d48adea].
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 | <?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Transaction; use Garradin\Entities\Files\File; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE); if (!CURRENT_YEAR_ID) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN'); } $chart = $current_year->chart(); $accounts = $chart->accounts(); $transaction = new Transaction; $lines = [[], []]; $amount = 0; $payoff_for = qg('payoff_for') ?: f('payoff_for'); $date = new \DateTime; if ($session->get('acc_last_date')) { $date = \DateTime::createFromFormat('!d/m/Y', $session->get('acc_last_date')); } | > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Transaction; use Garradin\Entities\Files\File; use Garradin\Accounting\Transactions; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE); if (!CURRENT_YEAR_ID) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN'); } $chart = $current_year->chart(); $accounts = $chart->accounts(); $transaction = new Transaction; $lines = [[], []]; $amount = 0; $payoff_for = qg('payoff_for') ?: f('payoff_for'); $types_accounts = null; // Duplicate transaction if (qg('copy')) { $old = Transactions::get((int)qg('copy')); $transaction = $old->duplicate($current_year); $lines = $transaction->getLinesWithAccounts(); $payoff_for = null; $amount = $transaction->getLinesCreditSum(); $types_accounts = $transaction->getTypesAccounts(); $transaction->resetLines(); foreach ($lines as $k => &$line) { $line->account = [$line->id_account => sprintf('%s — %s', $line->account_code, $line->account_name)]; } unset($line); } $date = new \DateTime; if ($session->get('acc_last_date')) { $date = \DateTime::createFromFormat('!d/m/Y', $session->get('acc_last_date')); } |
︙ | ︙ | |||
86 87 88 89 90 91 92 | Utils::redirect(Utils::getSelfURI(false) . '?ok=' . $transaction->id()); } catch (UserException $e) { $form->addError($e->getMessage()); } } | | | 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | Utils::redirect(Utils::getSelfURI(false) . '?ok=' . $transaction->id()); } catch (UserException $e) { $form->addError($e->getMessage()); } } $tpl->assign(compact('transaction', 'payoff_for', 'amount', 'lines', 'types_accounts')); $tpl->assign('payoff_targets', implode(':', [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING])); $tpl->assign('ok', (int) qg('ok')); $tpl->assign('types_details', Transaction::getTypesDetails()); $tpl->assign('chart_id', $chart->id()); $tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical()); $tpl->display('acc/transactions/new.tpl'); |
Modified src/www/admin/acc/transactions/service_user.php from [fb13cfe6c3] to [6aaa90de39].
︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 | $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); $criterias = ['service_user' => (int)qg('id')]; $tpl->assign('balance', Reports::getClosingSumsWithAccounts($criterias)); $tpl->assign('journal', Reports::getJournal($criterias)); $tpl->assign('user_id', qg('user')); $tpl->display('acc/transactions/service_user.tpl'); | > | 9 10 11 12 13 14 15 16 17 18 | $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); $criterias = ['service_user' => (int)qg('id')]; $tpl->assign('balance', Reports::getClosingSumsWithAccounts($criterias)); $tpl->assign('journal', Reports::getJournal($criterias)); $tpl->assign('user_id', qg('user')); $tpl->assign('service_user_id', qg('id')); $tpl->display('acc/transactions/service_user.tpl'); |
Modified src/www/admin/common/files/_preview.php from [35c5f4da82] to [a08e5e24c0].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Web\Render\Render; use Garradin\Web\Web; require_once __DIR__ . '/../../_inc.php'; $page = null; if ($path = qg('f')) { $file = Files::get($path); if (!$file || !$file->checkReadAccess($session)) { throw new UserException('Vous n\'avez pas le droit de lire ce fichier.'); } | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Web\Render\Render; use Garradin\Web\Web; require_once __DIR__ . '/../../_inc.php'; $page = null; $content = f('content'); if (null == $content) { throw new UserException('Aucun contenu à prévisualiser'); } if ($path = qg('f')) { $file = Files::get($path); if (!$file || !$file->checkReadAccess($session)) { throw new UserException('Vous n\'avez pas le droit de lire ce fichier.'); } |
︙ | ︙ | |||
27 28 29 30 31 32 33 | $file = $page->file(); } else { throw new UserException('Fichier inconnu'); } | | | | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | $file = $page->file(); } else { throw new UserException('Fichier inconnu'); } $prefix = $page ? 'web/page.php?uri=' : 'common/files/_preview.php?p='; $content = Render::render(f('format'), $file, f('content'), ADMIN_URL . $prefix); $tpl->assign(compact('file', 'content')); $tpl->assign('custom_css', ['!web/css.php']); $tpl->display('common/files/_preview.tpl'); |
Modified src/www/admin/common/files/preview.php from [a192851b45] to [5a776333ed].
︙ | ︙ | |||
13 14 15 16 17 18 19 | } if (!$file->checkReadAccess($session)) { throw new UserException('Vous n\'avez pas le droit de lire ce fichier.'); } try { | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | } if (!$file->checkReadAccess($session)) { throw new UserException('Vous n\'avez pas le droit de lire ce fichier.'); } try { $tpl->assign('content', $file->render('common/files/_preview.php?p=')); $tpl->assign('file', $file); $tpl->display('common/files/_preview.tpl'); } catch (\LogicException $e) { $file->serve($session); } |
Modified src/www/admin/docs/action.php from [a63ecf9df6] to [1987800963].
︙ | ︙ | |||
29 30 31 32 33 34 35 | } unset($file); foreach ($check as $file) { $file->delete(); } | | | | 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 | } unset($file); foreach ($check as $file) { $file->delete(); } }, $csrf_key, '!docs/?path=' . $parent); $form->runIf(f('move') && f('select'), function () use ($check, $session) { foreach ($check as &$file) { $file = Files::get($file); if (!$file || !$file->checkWriteAccess($session) || $file->context() != File::CONTEXT_DOCUMENTS) { throw new UserException('Impossible de déplacer un fichier car vous n\'avez pas le droit de le modifier'); } } $target = f('select'); unset($file); foreach ($check as $file) { $file->move($target); } }, $csrf_key, '!docs/?path=' . $parent); $count = count($check); $extra = compact('parent', 'action', 'check'); $tpl->assign(compact('csrf_key', 'extra', 'action', 'count')); if ($action == 'delete') { |
︙ | ︙ |
Modified src/www/admin/docs/index.php from [8becda408f] to [7fe6a0182d].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Files\Transactions; use Garradin\Files\Users; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Files\Transactions; use Garradin\Files\Users; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $path = trim(qg('path')) ?: File::CONTEXT_DOCUMENTS; $context = Files::getContext($path); $context_ref = Files::getContextRef($path); $list = null; // Specific lists for some contexts if ($context == File::CONTEXT_TRANSACTION) { |
︙ | ︙ |
Modified src/www/admin/docs/new_dir.php from [e00667d877] to [d2c27619fc].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.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 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $parent = trim(qg('path')); if (!File::checkCreateAccess(File::CONTEXT_DOCUMENTS, $session)) { throw new UserException('Vous n\'avez pas le droit de créer de répertoire ici.'); } $csrf_key = 'create_dir'; $form->runIf('create', function () use ($parent) { $name = trim(f('name')); File::validatePath($parent . '/' . $name); File::createDirectory($parent, $name); }, $csrf_key, '!docs/?path=' . $parent); $tpl->assign(compact('csrf_key')); $tpl->display('docs/new_dir.tpl'); |
Modified src/www/admin/docs/new_file.php from [ad63719729] to [b548528de7].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.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; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $parent = trim(qg('path')); if (!File::checkCreateAccess(File::CONTEXT_DOCUMENTS, $session)) { throw new UserException('Vous n\'avez pas le droit de créer de répertoire ici.'); } $csrf_key = 'create_file'; $form->runIf('create', function () use ($parent) { $name = trim(f('name')); if (!strpos($name, '.')) { $name .= '.skriv'; } File::validatePath($parent . '/' . $name); $name = File::filterName($name); $file = File::createAndStore($parent, $name, null, ''); }, $csrf_key, '!docs/?path=' . $parent); $tpl->assign(compact('csrf_key')); $tpl->display('docs/new_file.tpl'); |
Modified src/www/admin/docs/zip.php from [a9aa6c6682] to [21577262c1].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Files\Transactions; use Garradin\Files\Users; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Files\Transactions; use Garradin\Files\Users; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $path = trim(qg('path')) ?: File::CONTEXT_DOCUMENTS; $name = preg_replace('/[^\p{L}_-]+/i', '_', $path); $name = sprintf('%s - Fichiers - %s.zip', Config::getInstance()->get('nom_asso'), $name); header('Content-type: application/zip'); header(sprintf('Content-Disposition: attachment; filename="%s"', $name)); Files::zip($path, $session); |
Modified src/www/admin/index.php from [bf52e33fdb] to [025ac1c529].
︙ | ︙ | |||
10 11 12 13 14 15 16 | $homepage = Config::getInstance()->get('admin_homepage'); $banner = null; Plugin::fireSignal('accueil.banniere', ['user' => $user, 'session' => $session], $banner); if ($homepage && ($file = Files::get($homepage))) { | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | $homepage = Config::getInstance()->get('admin_homepage'); $banner = null; Plugin::fireSignal('accueil.banniere', ['user' => $user, 'session' => $session], $banner); if ($homepage && ($file = Files::get($homepage))) { $homepage = $file->render(ADMIN_URL . 'common/files/preview.php?p=' . File::CONTEXT_DOCUMENTS . '/'); } else { $homepage = null; } $tpl->assign(compact('homepage', 'banner')); |
︙ | ︙ |
Modified src/www/admin/membres/modifier.php from [171580f332] to [cbd131a783].
︙ | ︙ | |||
16 17 18 19 20 21 22 | if (!$membre) { throw new UserException("Ce membre n'existe pas."); } // Ne pas modifier le membre courant, on risque de se tirer une balle dans le pied if ($membre->id == $user->id) { | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | if (!$membre) { throw new UserException("Ce membre n'existe pas."); } // Ne pas modifier le membre courant, on risque de se tirer une balle dans le pied if ($membre->id == $user->id) { throw new UserException("Vous ne pouvez pas modifier votre propre profil, la modification doit être faite par un autre membre, pour éviter de vous empêcher de vous reconnecter.\nUtilisez la page 'Mes infos personnelles' pour modifier vos informations."); } $champs = $config->get('champs_membres'); // Protection contre la modification des admins par des membres moins puissants $membre_cat = Categories::get($membre->id_category); |
︙ | ︙ |
Modified src/www/admin/services/payment.php from [abbfbc0faf] to [6351db0e7d].
︙ | ︙ | |||
15 16 17 18 19 20 21 | throw new UserException("Cette inscription n'existe pas"); } $user_name = (new Membres)->getNom($su->id_user); $csrf_key = 'service_pay'; | | > > > > | > > > | 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 | throw new UserException("Cette inscription n'existe pas"); } $user_name = (new Membres)->getNom($su->id_user); $csrf_key = 'service_pay'; $form->runIf(f('save') || f('save_and_add_payment'), function () use ($su, $session) { $su->addPayment($session->getUser()->id); if ($su->paid != (bool) f('paid')) { $su->paid = (bool) f('paid'); $su->save(); } if (f('save_and_add_payment')) { $url = ADMIN_URL . 'services/payment.php?id=' . $su->id; } else { $url = ADMIN_URL . 'services/user.php?id=' . $su->id_user; } Utils::redirect($url); }, $csrf_key); $types_details = Transaction::getTypesDetails(); $account_targets = $types_details[Transaction::TYPE_REVENUE]->accounts[1]->targets_string; $tpl->assign(compact('csrf_key', 'account_targets', 'user_name', 'su')); $tpl->display('services/payment.tpl'); |
Modified src/www/admin/services/save.php from [0acb3f484c] to [ebe0bb9e7c].
︙ | ︙ | |||
47 48 49 50 51 52 53 | Utils::redirect(Utils::getSelfURI(['user' => $user_id, 'past_services' => $current_only])); } $has_past_services = count($grouped_services) != $count_all; $csrf_key = 'service_save'; | | > > > > | > > > | 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 | Utils::redirect(Utils::getSelfURI(['user' => $user_id, 'past_services' => $current_only])); } $has_past_services = count($grouped_services) != $count_all; $csrf_key = 'service_save'; $form->runIf(f('save') || f('save_and_add_payment'), function () use ($session) { $su = Service_User::saveFromForm($session->getUser()->id); if (f('save_and_add_payment')) { $url = ADMIN_URL . 'services/payment.php?id=' . $su->id; } else { $url = ADMIN_URL . 'services/user.php?id=' . $su->id_user; } Utils::redirect($url); }, $csrf_key); $selected_user = $user_name ? [$user_id => $user_name] : null; $types_details = Transaction::getTypesDetails(); $account_targets = $types_details[Transaction::TYPE_REVENUE]->accounts[1]->targets_string; $today = new \DateTime; $tpl->assign(compact('today', 'grouped_services', 'csrf_key', 'selected_user', 'account_targets', 'user_name', 'user_id', 'current_only', 'has_past_services')); $tpl->display('services/save.tpl'); |
Modified src/www/admin/static/print.css from [5d6f8ad8c8] to [6937e4b226].
1 2 3 4 5 6 7 8 9 10 11 | @page { size: A4 landscape; margin: 0; } body { background: #fff; padding: 0; margin: 1cm; font-size: 10pt; } | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @page { size: A4 landscape; margin: 0; } html { height: auto; } body { background: #fff; padding: 0; margin: 1cm; font-size: 10pt; } |
︙ | ︙ | |||
97 98 99 100 101 102 103 | text-decoration: none; } /* Don't repeat the table footer on every printed page */ table tfoot{ display:table-row-group; } | > > > > > > > > | 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | text-decoration: none; } /* Don't repeat the table footer on every printed page */ table tfoot{ display:table-row-group; } details summary::after { display: none; } .ruler::after, .ruler::before { display: none; } |
Modified src/www/admin/static/scripts/global.js from [b78bac285f] to [0da4b43a86].
︙ | ︙ | |||
423 424 425 426 427 428 429 430 431 432 433 434 435 436 | var tableActions = document.querySelectorAll('form table tfoot .actions select'); for (var i = 0; i < tableActions.length; i++) { tableActions[i].onchange = function () { if (!this.form.querySelector('table tbody input[type=checkbox]:checked')) { return !window.alert("Aucune ligne sélectionnée !"); } this.form.dispatchEvent(new Event('submit')); this.form.submit(); }; } | > | 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 | var tableActions = document.querySelectorAll('form table tfoot .actions select'); for (var i = 0; i < tableActions.length; i++) { tableActions[i].onchange = function () { if (!this.form.querySelector('table tbody input[type=checkbox]:checked')) { this.selectedIndex = 0; return !window.alert("Aucune ligne sélectionnée !"); } this.form.dispatchEvent(new Event('submit')); this.form.submit(); }; } |
︙ | ︙ |
Modified src/www/admin/web/index.php from [030354055c] to [fbf5316a30].
︙ | ︙ | |||
19 20 21 22 23 24 25 | } else { foreach (Web::sync() as $error) { $form->addError($error); } } | | | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | } else { foreach (Web::sync() as $error) { $form->addError($error); } } $order_date = qg('order_date') !== null; $categories = Web::listCategories($cat ? $cat->path : ''); $pages = Web::listPages($cat ? $cat->path : '', $order_date); $title = $cat ? sprintf('Gestion du site web : %s', $cat->title) : 'Gestion du site web'; $type_page = Page::TYPE_PAGE; $type_category = Page::TYPE_CATEGORY; $breadcrumbs = $cat ? $cat->getBreadcrumbs() : []; $parent = $cat ? $cat->parent : null; $tpl->assign(compact('categories', 'pages', 'title', 'current_path', 'parent', 'type_page', 'type_category', 'order_date', 'breadcrumbs', 'cat')); $tpl->display('web/index.tpl'); |
Modified src/www/admin/web/page.php from [da153abd6b] to [7684223213].
︙ | ︙ | |||
22 23 24 25 26 27 28 | $membres = new Membres; $tpl->assign('breadcrumbs', $page->getBreadcrumbs()); $images = $page->getImageGallery(true); $files = $page->getAttachmentsGallery(true); | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | $membres = new Membres; $tpl->assign('breadcrumbs', $page->getBreadcrumbs()); $images = $page->getImageGallery(true); $files = $page->getAttachmentsGallery(true); $content = $page->render(ADMIN_URL . 'web/page.php?p='); $type_page = Page::TYPE_PAGE; $type_category = Page::TYPE_CATEGORY; $tpl->assign(compact('page', 'images', 'files', 'content', 'type_page', 'type_category')); $tpl->assign('custom_js', ['wiki_gallery.js']); $tpl->assign('custom_css', ['wiki.css', '!web/css.php']); $tpl->display('web/page.tpl'); |
Added src/www/skel-dist/robots.txt version [9d03a7f081].
> > > > | 1 2 3 4 | User-agent: * Disallow: /admin/ Sitemap: {{$root_url}}sitemap.xml |
Added src/www/skel-dist/sitemap.xml version [923be64ae6].
> > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> {{#pages limit=1 order="modified DESC"}} <url> <loc>{{$root_url}}</loc> <lastmod>{{$modified|atom_date}}</lastmod> </url> {{/pages}} {{#pages limit=10000}} <url> <loc>{{$url}}</loc> <lastmod>{{$modified|atom_date}}</lastmod> </url> {{/pages}} </urlset> |