From 1ab3343a95ed3ef4958d91dfbf49372dce8a092e Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Fri, 19 Sep 2014 00:04:19 +0200 Subject: [PATCH] init --- .htaccess | 7 + COPYING | 661 +++++++ README | 33 + VERSION | 1 + config.dist.php | 63 + cron.php | 25 + include/class.champs_membres.php | 470 +++++ include/class.compta_categories.php | 126 ++ include/class.compta_comptes.php | 325 ++++ include/class.compta_comptes_bancaires.php | 164 ++ include/class.compta_exercices.php | 569 ++++++ include/class.compta_import.php | 387 ++++ include/class.compta_journal.php | 363 ++++ include/class.compta_stats.php | 122 ++ include/class.config.php | 326 ++++ include/class.cotisations.php | 170 ++ include/class.cotisations_membres.php | 336 ++++ include/class.db.php | 411 ++++ include/class.membres.php | 792 ++++++++ include/class.membres_categories.php | 150 ++ include/class.membres_import.php | 272 +++ include/class.plugin.php | 523 +++++ include/class.rappels.php | 208 ++ include/class.rappels_envoyes.php | 231 +++ include/class.sauvegarde.php | 263 +++ include/class.squelette.php | 753 ++++++++ include/class.wiki.php | 528 +++++ include/data/0.4.0.sql | 103 + include/data/0.4.3.sql | 79 + include/data/0.6.0.sql | 110 ++ include/data/categories_comptables.sql | 22 + include/data/champs_membres.ini | 129 ++ include/data/plan_comptable.json | 1718 +++++++++++++++++ include/data/schema.sql | 316 +++ include/index.html | 1 + include/init.php | 353 ++++ include/lib.squelette_filtres.php | 350 ++++ include/lib.static_cache.php | 87 + include/lib.template.php | 604 ++++++ include/lib.utils.php | 677 +++++++ include/libs/countries/countries_en.php | 261 +++ include/libs/countries/countries_fr.php | 260 +++ include/libs/diff/class.simplediff.php | 414 ++++ .../libs/garbage2xhtml/lib.garbage2xhtml.php | 868 +++++++++ include/libs/miniskel/class.miniskel.php | 754 ++++++++ .../libs/passphrase/lib.passphrase.french.php | 52 + include/libs/svgplot/lib.svgpie.php | 144 ++ include/libs/svgplot/lib.svgplot.php | 229 +++ include/libs/template_lite/class.compiler.php | 1013 ++++++++++ include/libs/template_lite/class.config.php | 165 ++ include/libs/template_lite/class.parser.php | 344 ++++ include/libs/template_lite/class.template.php | 969 ++++++++++ .../libs/template_lite/class.tokenparser.php | 291 +++ .../internal/compile.compile_config.php | 74 + .../internal/compile.compile_custom_block.php | 65 + .../compile.compile_custom_function.php | 60 + .../internal/compile.compile_if.php | 159 ++ ...compile.generate_compiler_debug_output.php | 35 + .../internal/compile.include.php | 56 + .../internal/compile.parse_is_expr.php | 77 + .../internal/compile.section_start.php | 129 ++ include/libs/template_lite/internal/debug.tpl | 77 + .../internal/template.build_dir.php | 29 + .../internal/template.config_loader.php | 76 + .../internal/template.destroy_dir.php | 72 + .../template.fetch_compile_include.php | 42 + .../template.generate_debug_output.php | 37 + .../template_lite/plugins/block.capture.php | 29 + .../template_lite/plugins/block.strip.php | 23 + .../plugins/block.textformat.php | 78 + .../template_lite/plugins/compiler.debug.php | 33 + .../plugins/compiler.tplheader.php | 16 + .../plugins/function.counter.php | 97 + .../template_lite/plugins/function.cycle.php | 101 + .../plugins/function.db_function_call.php | 67 + .../plugins/function.db_result_call.php | 75 + .../plugins/function.html_checkboxes.php | 70 + .../plugins/function.html_hidden.php | 49 + .../plugins/function.html_image.php | 203 ++ .../plugins/function.html_input.php | 58 + .../plugins/function.html_options.php | 104 + .../plugins/function.html_radios.php | 55 + .../plugins/function.html_select_date.php | 269 +++ .../plugins/function.html_select_time.php | 177 ++ .../plugins/function.html_table.php | 88 + .../plugins/function.html_textbox.php | 51 + .../plugins/function.in_array.php | 21 + .../template_lite/plugins/function.mailto.php | 148 ++ .../template_lite/plugins/function.math.php | 90 + .../template_lite/plugins/function.popup.php | 81 + .../plugins/function.popup_init.php | 32 + .../plugins/function.resize_image.php | 239 +++ .../plugins/modifier.bbcode2html.php | 44 + .../plugins/modifier.capitalize.php | 13 + .../template_lite/plugins/modifier.cat.php | 31 + .../plugins/modifier.count_characters.php | 32 + .../plugins/modifier.count_paragraphs.php | 27 + .../plugins/modifier.count_sentences.php | 27 + .../plugins/modifier.count_words.php | 31 + .../template_lite/plugins/modifier.date.php | 63 + .../plugins/modifier.date_format.php | 64 + .../plugins/modifier.debug_print_var.php | 54 + .../plugins/modifier.default.php | 22 + .../template_lite/plugins/modifier.escape.php | 102 + .../template_lite/plugins/modifier.indent.php | 28 + .../template_lite/plugins/modifier.lower.php | 13 + .../plugins/modifier.regex_replace.php | 33 + .../plugins/modifier.replace.php | 15 + .../plugins/modifier.spacify.php | 27 + .../plugins/modifier.string_format.php | 15 + .../template_lite/plugins/modifier.strip.php | 16 + .../plugins/modifier.truncate.php | 34 + .../template_lite/plugins/modifier.upper.php | 13 + .../plugins/outputfilter.gzip.php | 61 + .../plugins/outputfilter.trimwhitespace.php | 81 + .../plugins/postfilter.showtemplatevars.php | 16 + .../plugins/prefilter.jstrip.php | 130 ++ .../plugins/prefilter.showinfoheader.php | 23 + .../plugins/shared.escape_chars.php | 18 + .../plugins/shared.make_timestamp.php | 40 + include/libs/template_lite/tests/parser.php | 51 + .../libs/template_lite/tests/tokenparser.php | 46 + index.php | 13 + plugins/index.html | 1 + templates/admin/_foot.tpl | 19 + templates/admin/_head.tpl | 112 ++ templates/admin/compta/banques/ajouter.tpl | 32 + templates/admin/compta/banques/index.tpl | 34 + templates/admin/compta/banques/modifier.tpl | 32 + templates/admin/compta/banques/supprimer.tpl | 29 + templates/admin/compta/categories/ajouter.tpl | 39 + templates/admin/compta/categories/index.tpl | 31 + .../admin/compta/categories/modifier.tpl | 28 + .../admin/compta/categories/supprimer.tpl | 29 + templates/admin/compta/comptes/ajouter.tpl | 39 + templates/admin/compta/comptes/index.tpl | 51 + templates/admin/compta/comptes/journal.tpl | 56 + templates/admin/compta/comptes/modifier.tpl | 33 + templates/admin/compta/comptes/supprimer.tpl | 53 + templates/admin/compta/exercices/ajouter.tpl | 30 + templates/admin/compta/exercices/bilan.tpl | 84 + templates/admin/compta/exercices/cloturer.tpl | 41 + .../compta/exercices/compte_resultat.tpl | 110 ++ .../admin/compta/exercices/grand_livre.tpl | 89 + templates/admin/compta/exercices/index.tpl | 43 + templates/admin/compta/exercices/journal.tpl | 39 + templates/admin/compta/exercices/modifier.tpl | 30 + .../admin/compta/exercices/supprimer.tpl | 30 + templates/admin/compta/import.tpl | 61 + templates/admin/compta/index.tpl | 21 + templates/admin/compta/operations/index.tpl | 77 + templates/admin/compta/operations/membre.tpl | 76 + .../admin/compta/operations/modifier.tpl | 100 + .../admin/compta/operations/recherche_sql.tpl | 61 + templates/admin/compta/operations/saisir.tpl | 143 ++ .../admin/compta/operations/supprimer.tpl | 26 + templates/admin/compta/operations/voir.tpl | 90 + templates/admin/config/_menu.tpl | 8 + templates/admin/config/donnees.tpl | 133 ++ templates/admin/config/import.tpl | 16 + templates/admin/config/index.tpl | 121 ++ templates/admin/config/membres.tpl | 353 ++++ templates/admin/config/plugins.tpl | 107 + templates/admin/config/site.tpl | 53 + templates/admin/index.tpl | 43 + templates/admin/install.tpl | 81 + templates/admin/login.tpl | 36 + templates/admin/membres/action.tpl | 62 + templates/admin/membres/ajouter.tpl | 67 + templates/admin/membres/cat_modifier.tpl | 154 ++ templates/admin/membres/cat_supprimer.tpl | 34 + templates/admin/membres/categories.tpl | 51 + templates/admin/membres/cotisations.tpl | 99 + templates/admin/membres/cotisations/ajout.tpl | 126 ++ .../membres/cotisations/gestion/modifier.tpl | 114 ++ .../cotisations/gestion/rappel_modifier.tpl | 65 + .../cotisations/gestion/rappel_supprimer.tpl | 46 + .../membres/cotisations/gestion/rappels.tpl | 126 ++ .../membres/cotisations/gestion/supprimer.tpl | 36 + templates/admin/membres/cotisations/index.tpl | 160 ++ .../admin/membres/cotisations/rappels.tpl | 102 + .../admin/membres/cotisations/supprimer.tpl | 35 + templates/admin/membres/cotisations/voir.tpl | 59 + templates/admin/membres/fiche.tpl | 106 + templates/admin/membres/import.tpl | 88 + templates/admin/membres/index.tpl | 156 ++ templates/admin/membres/message.tpl | 38 + templates/admin/membres/message_collectif.tpl | 43 + templates/admin/membres/modifier.tpl | 84 + templates/admin/membres/recherche.tpl | 209 ++ templates/admin/membres/recherche_sql.tpl | 111 ++ templates/admin/membres/supprimer.tpl | 43 + templates/admin/mes_cotisations.tpl | 81 + templates/admin/mes_infos.tpl | 58 + templates/admin/password.tpl | 54 + templates/admin/wiki/_chercher_parent.tpl | 40 + templates/admin/wiki/chercher.tpl | 31 + templates/admin/wiki/creer.tpl | 27 + templates/admin/wiki/editer.tpl | 164 ++ templates/admin/wiki/historique.tpl | 81 + templates/admin/wiki/page.tpl | 102 + templates/admin/wiki/recent.tpl | 20 + templates/admin/wiki/supprimer.tpl | 36 + templates/error.tpl | 45 + templates/index.html | 1 + templates/index.tpl | 26 + www/.htaccess | 11 + www/_inc.php | 6 + www/_route.php | 30 + www/admin/.htaccess | 1 + www/admin/_inc.php | 33 + www/admin/compta/_inc.php | 14 + www/admin/compta/banques/ajouter.php | 45 + www/admin/compta/banques/index.php | 41 + www/admin/compta/banques/modifier.php | 54 + www/admin/compta/banques/supprimer.php | 48 + www/admin/compta/categories/ajouter.php | 55 + www/admin/compta/categories/index.php | 23 + www/admin/compta/categories/modifier.php | 59 + www/admin/compta/categories/supprimer.php | 48 + www/admin/compta/comptes/ajouter.php | 56 + www/admin/compta/comptes/index.php | 37 + www/admin/compta/comptes/journal.php | 34 + www/admin/compta/comptes/modifier.php | 53 + www/admin/compta/comptes/supprimer.php | 69 + www/admin/compta/exercices/ajouter.php | 44 + www/admin/compta/exercices/bilan.php | 32 + www/admin/compta/exercices/cloturer.php | 53 + .../compta/exercices/compte_resultat.php | 31 + www/admin/compta/exercices/grand_livre.php | 31 + www/admin/compta/exercices/index.php | 13 + www/admin/compta/exercices/journal.php | 34 + www/admin/compta/exercices/modifier.php | 57 + www/admin/compta/exercices/supprimer.php | 53 + www/admin/compta/graph.php | 83 + www/admin/compta/import.php | 65 + www/admin/compta/index.php | 10 + www/admin/compta/operations/index.php | 54 + www/admin/compta/operations/membre.php | 51 + www/admin/compta/operations/modifier.php | 152 ++ www/admin/compta/operations/recherche_sql.php | 36 + www/admin/compta/operations/saisir.php | 194 ++ www/admin/compta/operations/supprimer.php | 48 + www/admin/compta/operations/voir.php | 53 + www/admin/compta/pie.php | 62 + www/admin/config/_inc.php | 12 + www/admin/config/donnees.php | 115 ++ www/admin/config/import.php | 8 + www/admin/config/index.php | 69 + www/admin/config/membres.php | 142 ++ www/admin/config/plugins.php | 66 + www/admin/config/site.php | 78 + www/admin/index.php | 40 + www/admin/install.php | 246 +++ www/admin/login.php | 56 + www/admin/logout.php | 10 + www/admin/membres/action.php | 79 + www/admin/membres/ajouter.php | 66 + www/admin/membres/cat_modifier.php | 73 + www/admin/membres/cat_supprimer.php | 53 + www/admin/membres/categories.php | 43 + www/admin/membres/cotisations.php | 49 + www/admin/membres/cotisations/ajout.php | 112 ++ .../membres/cotisations/gestion/modifier.php | 71 + .../cotisations/gestion/rappel_modifier.php | 71 + .../cotisations/gestion/rappel_supprimer.php | 52 + .../membres/cotisations/gestion/rappels.php | 60 + .../membres/cotisations/gestion/supprimer.php | 52 + www/admin/membres/cotisations/index.php | 61 + www/admin/membres/cotisations/rappels.php | 64 + www/admin/membres/cotisations/supprimer.php | 64 + www/admin/membres/cotisations/voir.php | 43 + www/admin/membres/fiche.php | 56 + www/admin/membres/import.php | 71 + www/admin/membres/index.php | 77 + www/admin/membres/message.php | 69 + www/admin/membres/message_collectif.php | 48 + www/admin/membres/modifier.php | 88 + www/admin/membres/recherche.php | 81 + www/admin/membres/recherche_sql.php | 34 + www/admin/membres/supprimer.php | 44 + www/admin/mes_cotisations.php | 39 + www/admin/mes_infos.php | 58 + www/admin/password.php | 56 + www/admin/plugin.php | 18 + www/admin/static/admin.css | 1149 +++++++++++ www/admin/static/bg00.png | Bin 0 -> 130 bytes www/admin/static/bg01.png | Bin 0 -> 47923 bytes www/admin/static/code_editor.min.js | 1 + www/admin/static/datepickr.css | 97 + www/admin/static/datepickr.js | 463 +++++ www/admin/static/font/garradin.css | 63 + www/admin/static/font/garradin.eot | Bin 0 -> 5656 bytes www/admin/static/font/garradin.svg | 33 + www/admin/static/font/garradin.ttf | Bin 0 -> 5488 bytes www/admin/static/font/garradin.woff | Bin 0 -> 3716 bytes www/admin/static/garradin.png | Bin 0 -> 32562 bytes www/admin/static/gibberish-aes.min.js | 19 + www/admin/static/global.js | 87 + www/admin/static/handheld.css | 138 ++ www/admin/static/loader.js | 48 + www/admin/static/password.js | 170 ++ www/admin/static/print.css | 58 + www/admin/static/skel_editor.css | 137 ++ www/admin/static/skel_editor.js | 203 ++ www/admin/static/wiki-encryption.js | 201 ++ www/admin/static/wikitoolbar.js | 123 ++ www/admin/upgrade.php | 224 +++ www/admin/wiki/_chercher_parent.php | 46 + www/admin/wiki/_inc.php | 14 + www/admin/wiki/chercher.php | 26 + www/admin/wiki/creer.php | 36 + www/admin/wiki/editer.php | 92 + www/admin/wiki/historique.php | 59 + www/admin/wiki/index.php | 34 + www/admin/wiki/recent.php | 16 + www/admin/wiki/supprimer.php | 50 + www/index.php | 9 + www/squelettes-dist/article.html | 29 + www/squelettes-dist/atom.xml | 32 + www/squelettes-dist/default.css | 250 +++ www/squelettes-dist/entete.html | 42 + www/squelettes-dist/pied.html | 8 + www/squelettes-dist/rubrique.html | 31 + www/squelettes-dist/sommaire.html | 25 + 325 files changed, 37197 insertions(+) create mode 100644 .htaccess create mode 100644 COPYING create mode 100644 README create mode 100644 VERSION create mode 100644 config.dist.php create mode 100644 cron.php create mode 100644 include/class.champs_membres.php create mode 100644 include/class.compta_categories.php create mode 100644 include/class.compta_comptes.php create mode 100644 include/class.compta_comptes_bancaires.php create mode 100644 include/class.compta_exercices.php create mode 100644 include/class.compta_import.php create mode 100644 include/class.compta_journal.php create mode 100644 include/class.compta_stats.php create mode 100644 include/class.config.php create mode 100644 include/class.cotisations.php create mode 100644 include/class.cotisations_membres.php create mode 100644 include/class.db.php create mode 100644 include/class.membres.php create mode 100644 include/class.membres_categories.php create mode 100644 include/class.membres_import.php create mode 100644 include/class.plugin.php create mode 100644 include/class.rappels.php create mode 100644 include/class.rappels_envoyes.php create mode 100644 include/class.sauvegarde.php create mode 100644 include/class.squelette.php create mode 100644 include/class.wiki.php create mode 100644 include/data/0.4.0.sql create mode 100644 include/data/0.4.3.sql create mode 100644 include/data/0.6.0.sql create mode 100644 include/data/categories_comptables.sql create mode 100644 include/data/champs_membres.ini create mode 100644 include/data/plan_comptable.json create mode 100644 include/data/schema.sql create mode 100644 include/index.html create mode 100644 include/init.php create mode 100644 include/lib.squelette_filtres.php create mode 100644 include/lib.static_cache.php create mode 100644 include/lib.template.php create mode 100644 include/lib.utils.php create mode 100644 include/libs/countries/countries_en.php create mode 100644 include/libs/countries/countries_fr.php create mode 100644 include/libs/diff/class.simplediff.php create mode 100644 include/libs/garbage2xhtml/lib.garbage2xhtml.php create mode 100644 include/libs/miniskel/class.miniskel.php create mode 100644 include/libs/passphrase/lib.passphrase.french.php create mode 100644 include/libs/svgplot/lib.svgpie.php create mode 100644 include/libs/svgplot/lib.svgplot.php create mode 100644 include/libs/template_lite/class.compiler.php create mode 100644 include/libs/template_lite/class.config.php create mode 100644 include/libs/template_lite/class.parser.php create mode 100644 include/libs/template_lite/class.template.php create mode 100644 include/libs/template_lite/class.tokenparser.php create mode 100644 include/libs/template_lite/internal/compile.compile_config.php create mode 100644 include/libs/template_lite/internal/compile.compile_custom_block.php create mode 100644 include/libs/template_lite/internal/compile.compile_custom_function.php create mode 100644 include/libs/template_lite/internal/compile.compile_if.php create mode 100644 include/libs/template_lite/internal/compile.generate_compiler_debug_output.php create mode 100644 include/libs/template_lite/internal/compile.include.php create mode 100644 include/libs/template_lite/internal/compile.parse_is_expr.php create mode 100644 include/libs/template_lite/internal/compile.section_start.php create mode 100644 include/libs/template_lite/internal/debug.tpl create mode 100644 include/libs/template_lite/internal/template.build_dir.php create mode 100644 include/libs/template_lite/internal/template.config_loader.php create mode 100644 include/libs/template_lite/internal/template.destroy_dir.php create mode 100644 include/libs/template_lite/internal/template.fetch_compile_include.php create mode 100644 include/libs/template_lite/internal/template.generate_debug_output.php create mode 100644 include/libs/template_lite/plugins/block.capture.php create mode 100644 include/libs/template_lite/plugins/block.strip.php create mode 100644 include/libs/template_lite/plugins/block.textformat.php create mode 100644 include/libs/template_lite/plugins/compiler.debug.php create mode 100644 include/libs/template_lite/plugins/compiler.tplheader.php create mode 100644 include/libs/template_lite/plugins/function.counter.php create mode 100644 include/libs/template_lite/plugins/function.cycle.php create mode 100644 include/libs/template_lite/plugins/function.db_function_call.php create mode 100644 include/libs/template_lite/plugins/function.db_result_call.php create mode 100644 include/libs/template_lite/plugins/function.html_checkboxes.php create mode 100644 include/libs/template_lite/plugins/function.html_hidden.php create mode 100644 include/libs/template_lite/plugins/function.html_image.php create mode 100644 include/libs/template_lite/plugins/function.html_input.php create mode 100644 include/libs/template_lite/plugins/function.html_options.php create mode 100644 include/libs/template_lite/plugins/function.html_radios.php create mode 100644 include/libs/template_lite/plugins/function.html_select_date.php create mode 100644 include/libs/template_lite/plugins/function.html_select_time.php create mode 100644 include/libs/template_lite/plugins/function.html_table.php create mode 100644 include/libs/template_lite/plugins/function.html_textbox.php create mode 100644 include/libs/template_lite/plugins/function.in_array.php create mode 100644 include/libs/template_lite/plugins/function.mailto.php create mode 100644 include/libs/template_lite/plugins/function.math.php create mode 100644 include/libs/template_lite/plugins/function.popup.php create mode 100644 include/libs/template_lite/plugins/function.popup_init.php create mode 100644 include/libs/template_lite/plugins/function.resize_image.php create mode 100644 include/libs/template_lite/plugins/modifier.bbcode2html.php create mode 100644 include/libs/template_lite/plugins/modifier.capitalize.php create mode 100644 include/libs/template_lite/plugins/modifier.cat.php create mode 100644 include/libs/template_lite/plugins/modifier.count_characters.php create mode 100644 include/libs/template_lite/plugins/modifier.count_paragraphs.php create mode 100644 include/libs/template_lite/plugins/modifier.count_sentences.php create mode 100644 include/libs/template_lite/plugins/modifier.count_words.php create mode 100644 include/libs/template_lite/plugins/modifier.date.php create mode 100644 include/libs/template_lite/plugins/modifier.date_format.php create mode 100644 include/libs/template_lite/plugins/modifier.debug_print_var.php create mode 100644 include/libs/template_lite/plugins/modifier.default.php create mode 100644 include/libs/template_lite/plugins/modifier.escape.php create mode 100644 include/libs/template_lite/plugins/modifier.indent.php create mode 100644 include/libs/template_lite/plugins/modifier.lower.php create mode 100644 include/libs/template_lite/plugins/modifier.regex_replace.php create mode 100644 include/libs/template_lite/plugins/modifier.replace.php create mode 100644 include/libs/template_lite/plugins/modifier.spacify.php create mode 100644 include/libs/template_lite/plugins/modifier.string_format.php create mode 100644 include/libs/template_lite/plugins/modifier.strip.php create mode 100644 include/libs/template_lite/plugins/modifier.truncate.php create mode 100644 include/libs/template_lite/plugins/modifier.upper.php create mode 100644 include/libs/template_lite/plugins/outputfilter.gzip.php create mode 100644 include/libs/template_lite/plugins/outputfilter.trimwhitespace.php create mode 100644 include/libs/template_lite/plugins/postfilter.showtemplatevars.php create mode 100644 include/libs/template_lite/plugins/prefilter.jstrip.php create mode 100644 include/libs/template_lite/plugins/prefilter.showinfoheader.php create mode 100644 include/libs/template_lite/plugins/shared.escape_chars.php create mode 100644 include/libs/template_lite/plugins/shared.make_timestamp.php create mode 100644 include/libs/template_lite/tests/parser.php create mode 100644 include/libs/template_lite/tests/tokenparser.php create mode 100644 index.php create mode 100644 plugins/index.html create mode 100644 templates/admin/_foot.tpl create mode 100644 templates/admin/_head.tpl create mode 100644 templates/admin/compta/banques/ajouter.tpl create mode 100644 templates/admin/compta/banques/index.tpl create mode 100644 templates/admin/compta/banques/modifier.tpl create mode 100644 templates/admin/compta/banques/supprimer.tpl create mode 100644 templates/admin/compta/categories/ajouter.tpl create mode 100644 templates/admin/compta/categories/index.tpl create mode 100644 templates/admin/compta/categories/modifier.tpl create mode 100644 templates/admin/compta/categories/supprimer.tpl create mode 100644 templates/admin/compta/comptes/ajouter.tpl create mode 100644 templates/admin/compta/comptes/index.tpl create mode 100644 templates/admin/compta/comptes/journal.tpl create mode 100644 templates/admin/compta/comptes/modifier.tpl create mode 100644 templates/admin/compta/comptes/supprimer.tpl create mode 100644 templates/admin/compta/exercices/ajouter.tpl create mode 100644 templates/admin/compta/exercices/bilan.tpl create mode 100644 templates/admin/compta/exercices/cloturer.tpl create mode 100644 templates/admin/compta/exercices/compte_resultat.tpl create mode 100644 templates/admin/compta/exercices/grand_livre.tpl create mode 100644 templates/admin/compta/exercices/index.tpl create mode 100644 templates/admin/compta/exercices/journal.tpl create mode 100644 templates/admin/compta/exercices/modifier.tpl create mode 100644 templates/admin/compta/exercices/supprimer.tpl create mode 100644 templates/admin/compta/import.tpl create mode 100644 templates/admin/compta/index.tpl create mode 100644 templates/admin/compta/operations/index.tpl create mode 100644 templates/admin/compta/operations/membre.tpl create mode 100644 templates/admin/compta/operations/modifier.tpl create mode 100644 templates/admin/compta/operations/recherche_sql.tpl create mode 100644 templates/admin/compta/operations/saisir.tpl create mode 100644 templates/admin/compta/operations/supprimer.tpl create mode 100644 templates/admin/compta/operations/voir.tpl create mode 100644 templates/admin/config/_menu.tpl create mode 100644 templates/admin/config/donnees.tpl create mode 100644 templates/admin/config/import.tpl create mode 100644 templates/admin/config/index.tpl create mode 100644 templates/admin/config/membres.tpl create mode 100644 templates/admin/config/plugins.tpl create mode 100644 templates/admin/config/site.tpl create mode 100644 templates/admin/index.tpl create mode 100644 templates/admin/install.tpl create mode 100644 templates/admin/login.tpl create mode 100644 templates/admin/membres/action.tpl create mode 100644 templates/admin/membres/ajouter.tpl create mode 100644 templates/admin/membres/cat_modifier.tpl create mode 100644 templates/admin/membres/cat_supprimer.tpl create mode 100644 templates/admin/membres/categories.tpl create mode 100644 templates/admin/membres/cotisations.tpl create mode 100644 templates/admin/membres/cotisations/ajout.tpl create mode 100644 templates/admin/membres/cotisations/gestion/modifier.tpl create mode 100644 templates/admin/membres/cotisations/gestion/rappel_modifier.tpl create mode 100644 templates/admin/membres/cotisations/gestion/rappel_supprimer.tpl create mode 100644 templates/admin/membres/cotisations/gestion/rappels.tpl create mode 100644 templates/admin/membres/cotisations/gestion/supprimer.tpl create mode 100644 templates/admin/membres/cotisations/index.tpl create mode 100644 templates/admin/membres/cotisations/rappels.tpl create mode 100644 templates/admin/membres/cotisations/supprimer.tpl create mode 100644 templates/admin/membres/cotisations/voir.tpl create mode 100644 templates/admin/membres/fiche.tpl create mode 100644 templates/admin/membres/import.tpl create mode 100644 templates/admin/membres/index.tpl create mode 100644 templates/admin/membres/message.tpl create mode 100644 templates/admin/membres/message_collectif.tpl create mode 100644 templates/admin/membres/modifier.tpl create mode 100644 templates/admin/membres/recherche.tpl create mode 100644 templates/admin/membres/recherche_sql.tpl create mode 100644 templates/admin/membres/supprimer.tpl create mode 100644 templates/admin/mes_cotisations.tpl create mode 100644 templates/admin/mes_infos.tpl create mode 100644 templates/admin/password.tpl create mode 100644 templates/admin/wiki/_chercher_parent.tpl create mode 100644 templates/admin/wiki/chercher.tpl create mode 100644 templates/admin/wiki/creer.tpl create mode 100644 templates/admin/wiki/editer.tpl create mode 100644 templates/admin/wiki/historique.tpl create mode 100644 templates/admin/wiki/page.tpl create mode 100644 templates/admin/wiki/recent.tpl create mode 100644 templates/admin/wiki/supprimer.tpl create mode 100644 templates/error.tpl create mode 100644 templates/index.html create mode 100644 templates/index.tpl create mode 100644 www/.htaccess create mode 100644 www/_inc.php create mode 100644 www/_route.php create mode 100644 www/admin/.htaccess create mode 100644 www/admin/_inc.php create mode 100644 www/admin/compta/_inc.php create mode 100644 www/admin/compta/banques/ajouter.php create mode 100644 www/admin/compta/banques/index.php create mode 100644 www/admin/compta/banques/modifier.php create mode 100644 www/admin/compta/banques/supprimer.php create mode 100644 www/admin/compta/categories/ajouter.php create mode 100644 www/admin/compta/categories/index.php create mode 100644 www/admin/compta/categories/modifier.php create mode 100644 www/admin/compta/categories/supprimer.php create mode 100644 www/admin/compta/comptes/ajouter.php create mode 100644 www/admin/compta/comptes/index.php create mode 100644 www/admin/compta/comptes/journal.php create mode 100644 www/admin/compta/comptes/modifier.php create mode 100644 www/admin/compta/comptes/supprimer.php create mode 100644 www/admin/compta/exercices/ajouter.php create mode 100644 www/admin/compta/exercices/bilan.php create mode 100644 www/admin/compta/exercices/cloturer.php create mode 100644 www/admin/compta/exercices/compte_resultat.php create mode 100644 www/admin/compta/exercices/grand_livre.php create mode 100644 www/admin/compta/exercices/index.php create mode 100644 www/admin/compta/exercices/journal.php create mode 100644 www/admin/compta/exercices/modifier.php create mode 100644 www/admin/compta/exercices/supprimer.php create mode 100644 www/admin/compta/graph.php create mode 100644 www/admin/compta/import.php create mode 100644 www/admin/compta/index.php create mode 100644 www/admin/compta/operations/index.php create mode 100644 www/admin/compta/operations/membre.php create mode 100644 www/admin/compta/operations/modifier.php create mode 100644 www/admin/compta/operations/recherche_sql.php create mode 100644 www/admin/compta/operations/saisir.php create mode 100644 www/admin/compta/operations/supprimer.php create mode 100644 www/admin/compta/operations/voir.php create mode 100644 www/admin/compta/pie.php create mode 100644 www/admin/config/_inc.php create mode 100644 www/admin/config/donnees.php create mode 100644 www/admin/config/import.php create mode 100644 www/admin/config/index.php create mode 100644 www/admin/config/membres.php create mode 100644 www/admin/config/plugins.php create mode 100644 www/admin/config/site.php create mode 100644 www/admin/index.php create mode 100644 www/admin/install.php create mode 100644 www/admin/login.php create mode 100644 www/admin/logout.php create mode 100644 www/admin/membres/action.php create mode 100644 www/admin/membres/ajouter.php create mode 100644 www/admin/membres/cat_modifier.php create mode 100644 www/admin/membres/cat_supprimer.php create mode 100644 www/admin/membres/categories.php create mode 100644 www/admin/membres/cotisations.php create mode 100644 www/admin/membres/cotisations/ajout.php create mode 100644 www/admin/membres/cotisations/gestion/modifier.php create mode 100644 www/admin/membres/cotisations/gestion/rappel_modifier.php create mode 100644 www/admin/membres/cotisations/gestion/rappel_supprimer.php create mode 100644 www/admin/membres/cotisations/gestion/rappels.php create mode 100644 www/admin/membres/cotisations/gestion/supprimer.php create mode 100644 www/admin/membres/cotisations/index.php create mode 100644 www/admin/membres/cotisations/rappels.php create mode 100644 www/admin/membres/cotisations/supprimer.php create mode 100644 www/admin/membres/cotisations/voir.php create mode 100644 www/admin/membres/fiche.php create mode 100644 www/admin/membres/import.php create mode 100644 www/admin/membres/index.php create mode 100644 www/admin/membres/message.php create mode 100644 www/admin/membres/message_collectif.php create mode 100644 www/admin/membres/modifier.php create mode 100644 www/admin/membres/recherche.php create mode 100644 www/admin/membres/recherche_sql.php create mode 100644 www/admin/membres/supprimer.php create mode 100644 www/admin/mes_cotisations.php create mode 100644 www/admin/mes_infos.php create mode 100644 www/admin/password.php create mode 100644 www/admin/plugin.php create mode 100644 www/admin/static/admin.css create mode 100644 www/admin/static/bg00.png create mode 100644 www/admin/static/bg01.png create mode 100644 www/admin/static/code_editor.min.js create mode 100644 www/admin/static/datepickr.css create mode 100644 www/admin/static/datepickr.js create mode 100644 www/admin/static/font/garradin.css create mode 100644 www/admin/static/font/garradin.eot create mode 100644 www/admin/static/font/garradin.svg create mode 100644 www/admin/static/font/garradin.ttf create mode 100644 www/admin/static/font/garradin.woff create mode 100644 www/admin/static/garradin.png create mode 100644 www/admin/static/gibberish-aes.min.js create mode 100644 www/admin/static/global.js create mode 100644 www/admin/static/handheld.css create mode 100644 www/admin/static/loader.js create mode 100644 www/admin/static/password.js create mode 100644 www/admin/static/print.css create mode 100644 www/admin/static/skel_editor.css create mode 100644 www/admin/static/skel_editor.js create mode 100644 www/admin/static/wiki-encryption.js create mode 100644 www/admin/static/wikitoolbar.js create mode 100644 www/admin/upgrade.php create mode 100644 www/admin/wiki/_chercher_parent.php create mode 100644 www/admin/wiki/_inc.php create mode 100644 www/admin/wiki/chercher.php create mode 100644 www/admin/wiki/creer.php create mode 100644 www/admin/wiki/editer.php create mode 100644 www/admin/wiki/historique.php create mode 100644 www/admin/wiki/index.php create mode 100644 www/admin/wiki/recent.php create mode 100644 www/admin/wiki/supprimer.php create mode 100644 www/index.php create mode 100644 www/squelettes-dist/article.html create mode 100644 www/squelettes-dist/atom.xml create mode 100644 www/squelettes-dist/default.css create mode 100644 www/squelettes-dist/entete.html create mode 100644 www/squelettes-dist/pied.html create mode 100644 www/squelettes-dist/rubrique.html create mode 100644 www/squelettes-dist/sommaire.html diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..395cdf0 --- /dev/null +++ b/.htaccess @@ -0,0 +1,7 @@ + + RedirectMatch 403 /include/ + RedirectMatch 403 /cache/ + RedirectMatch 403 /plugins/ + RedirectMatch 403 /templates/ + RedirectMatch 403 /*.sqlite + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README b/README new file mode 100644 index 0000000..c11e982 --- /dev/null +++ b/README @@ -0,0 +1,33 @@ +Garradin - Gestionnaire d'association libre +=========================================== + +Inclus les bibliothèques suivantes : + +- Gibberish AES + https://github.com/mdp/gibberish-aes + Copyright : Mark Percival 2008 - http://markpercival.us + Licence : MIT + +- Countries - Liste des pays ISO 3166-1 + Copyright : BohwaZ + Licence : Domaine public + +- Simple Diff PHP library + Copyright : BohwaZ 2009 + Licence : GNU GPL v3 + +- Garbage2xhtml - HTML cleaner + Copyright : BohwaZ 2006-2011 + Licence : GNU AGPL v3 + +- miniSkel - SPIP-like templates + Copyright : BohwaZ 2007-2012 + Licence : GNU GPL v3 + +- Passphrase - a PHP library to generate passphrases + Copyright : BohwaZ 2011-2012 + Licence : WTFPL + +- Template_Lite + Copyright : 2003,2004,2005 by Paul Lockaby, 2005,2006 Mark Dickenson, 2005-2012 BohwaZ + Licence : GNU GPL v2.1 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..b616048 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.6.2 diff --git a/config.dist.php b/config.dist.php new file mode 100644 index 0000000..15ef961 --- /dev/null +++ b/config.dist.php @@ -0,0 +1,63 @@ +get('frequence_sauvegardes') && $config->get('nombre_sauvegardes')) +{ + $s = new Sauvegarde; + $s->auto(); +} + + +// Exécution des rappels automatiques +$rappels = new Rappels; + +if ($rappels->countAll()) +{ + $rappels->sendPending(); +} + +// Nettoyage du cache statique +Static_Cache::clean(); \ No newline at end of file diff --git a/include/class.champs_membres.php b/include/class.champs_membres.php new file mode 100644 index 0000000..9c07ae5 --- /dev/null +++ b/include/class.champs_membres.php @@ -0,0 +1,470 @@ + 'Adresse E-Mail', + 'url' => 'Adresse URL', + 'checkbox' => 'Case à cocher', + 'date' => 'Date', + 'datetime' => 'Date et heure', + //'file' => 'Fichier', + 'password' => 'Mot de passe', + 'number' => 'Numéro', + 'tel' => 'Numéro de téléphone', + 'select' => 'Sélecteur à choix unique', + 'multiple' => 'Sélecteur à choix multiple', + 'country' => 'Sélecteur de pays', + 'text' => 'Texte', + 'textarea' => 'Texte multi-lignes', + ]; + + protected $text_types = [ + 'email', + 'text', + 'select', + 'textarea', + 'url', + 'password', + 'country' + ]; + + protected $config_fields = [ + 'type', + 'title', + 'help', + 'editable', + 'list_row', + 'mandatory', + 'private', + 'options' + ]; + + static protected $presets = null; + + public function __toString() + { + return utils::write_ini_string($this->champs); + } + + public function toString() + { + return utils::write_ini_string($this->champs); + } + + static public function importInstall() + { + $champs = parse_ini_file(ROOT . '/include/data/champs_membres.ini', true); + $champs = array_filter($champs, function ($row) { return !empty($row['install']); }); + return new Champs_Membres($champs); + } + + static public function importPresets() + { + if (is_null(self::$presets)) + { + self::$presets = parse_ini_file(ROOT . '/include/data/champs_membres.ini', true); + } + + return self::$presets; + } + + static public function listUnusedPresets(Champs_Membres $champs) + { + return array_diff_key(self::importPresets(), $champs->getAll()); + } + + public function __construct($champs) + { + if ($champs instanceOf Champs_Membres) + { + $this->champs = $champs->getAll(); + } + elseif (is_array($champs)) + { + foreach ($champs as $key=>&$config) + { + $this->_checkField($key, $config); + } + + $this->champs = $champs; + } + else + { + $champs = parse_ini_string((string)$champs, true); + + foreach ($champs as $key=>&$config) + { + $this->_checkField($key, $config); + } + + $this->champs = $champs; + } + } + + public function getTypes() + { + return $this->types; + } + + public function get($champ, $key = null) + { + if ($champ == 'id') + { + return ['title' => 'Numéro unique', 'type' => 'number']; + } + + if (!array_key_exists($champ, $this->champs)) + return null; + + if ($key !== null) + { + if (array_key_exists($key, $this->champs[$champ])) + return $this->champs[$champ][$key]; + else + return null; + } + + return $this->champs[$champ]; + } + + public function isText($champ) + { + if (!array_key_exists($champ, $this->champs)) + return null; + + if (in_array($this->champs[$champ]['type'], $this->text_types)) + return true; + else + return false; + } + + public function getAll() + { + $this->champs['passe']['title'] = 'Mot de passe'; + return $this->champs; + } + + public function getList() + { + $champs = $this->champs; + unset($champs['passe']); + return $champs; + } + + public function getFirst() + { + reset($this->champs); + return key($this->champs); + } + + public function getListedFields() + { + $champs = $this->champs; + + $champs = array_filter($champs, function ($a) { + return empty($a['list_row']) ? false : true; + }); + + uasort($champs, function ($a, $b) { + if ($a['list_row'] == $b['list_row']) + return 0; + + return ($a['list_row'] > $b['list_row']) ? 1 : -1; + }); + + return $champs; + } + + /** + * Vérifie la cohérence et la présence des bons éléments pour un champ + * @param string $name Nom du champ + * @param array $config Configuration du champ + * @return boolean true + */ + protected function _checkField($name, &$config) + { + if (!preg_match('!^\w+(_\w+)*$!', $name)) + { + throw new UserException('Le nom du champ est invalide.'); + } + + foreach ($config as $key=>&$value) + { + // Champ install non pris en compte + if ($key == 'install') + { + unset($config[$key]); + continue; + } + + if (!in_array($key, $this->config_fields)) + { + throw new \BadMethodCallException('Champ '.$key.' non valide.'); + } + + if ($key == 'editable' || $key == 'private' || $key == 'mandatory') + { + $value = (bool) (int) $value; + } + elseif ($key == 'list_row') + { + $value = (int) $value; + } + elseif ($key == 'help' || $key == 'title') + { + $value = trim((string) $value); + } + elseif ($key == 'options') + { + $value = (array) $value; + + foreach ($value as $option_key=>$option_value) + { + if (trim($option_value) == '') + { + unset($value[$option_key]); + } + } + } + } + + if (empty($config['title']) && $name != 'passe') + { + throw new UserException('Champ "'.$name.'" : Le titre est obligatoire.'); + } + + if (empty($config['type']) || !array_key_exists($config['type'], $this->types)) + { + throw new UserException('Champ "'.$name.'" : Le type est vide ou non valide.'); + } + + if ($name == 'email' && $config['type'] != 'email') + { + throw new UserException('Le champ email ne peut être d\'un type différent de email.'); + } + + if ($name == 'passe' && $config['type'] != 'password') + { + throw new UserException('Le champ mot de passe ne peut être d\'un type différent de mot de passe.'); + } + + if (($config['type'] == 'multiple' || $config['type'] == 'select') && empty($config['options'])) + { + throw new UserException('Le champ "'.$name.'" nécessite de comporter au moins une option possible.'); + } + + if (!array_key_exists('editable', $config)) + { + $config['editable'] = false; + } + + if (!array_key_exists('mandatory', $config)) + { + $config['mandatory'] = false; + } + + if (!array_key_exists('private', $config)) + { + $config['private'] = false; + } + + return true; + } + + /** + * Ajouter un nouveau champ + * @param string $name Nom du champ + * @param array $config Configuration du champ + * @return boolean true + */ + public function add($name, $config) + { + if (!preg_match('!^[a-z0-9]+(_[a-z0-9]+)*$!', $name)) + { + throw new UserException('Le nom du champ est invalide : ne sont acceptés que des lettres minuscules et chiffres.'); + } + + $this->_checkField($name, $config); + + $this->champs[$name] = $config; + + return true; + } + + /** + * Modifie un champ particulier + * @param string $champ Nom du champ + * @param string $key Nom de la clé à modifier + * @param mixed $value Valeur à affecter + * @return boolean true + */ + public function set($champ, $key, $value) + { + if (!isset($this->champs[$champ])) + { + throw new \LogicException('Champ "'.$champ.'" inconnu.'); + } + + // Vérification + $config = $this->champs[$champ]; + $config[$key] = $value; + $this->_checkField($champ, $config); + + $this->champs[$champ] = $config; + return true; + } + + /** + * Modifie les champs en interne en vérifiant que tout va bien + * @param array $champs Liste des champs + * @return boolean true + */ + public function setAll($champs) + { + if (!array_key_exists('email', $champs)) + { + throw new UserException('Le champ E-Mail ne peut être supprimé des fiches membres.'); + } + + if (!array_key_exists('passe', $champs)) + { + throw new UserException('Le champ Mot de passe ne peut être supprimé des fiches membres.'); + } + + $config = Config::getInstance(); + + if (!array_key_exists($config->get('champ_identite'), $champs)) + { + throw new UserException('Le champ '.$config->get('champ_identite') + .' est défini comme identité des membres et ne peut donc être supprimé des fiches membres.'); + } + + if (!array_key_exists($config->get('champ_identifiant'), $champs)) + { + throw new UserException('Le champ '.$config->get('champ_identifiant') + .' est défini comme identifiant à la connexion et ne peut donc être supprimé des fiches membres.'); + } + + foreach ($champs as $name=>&$config) + { + $this->_checkField($name, $config); + } + + $this->champs = $champs; + + return true; + } + + /** + * Enregistre les changements de champs en base de données + * @param boolean $enable_copy Recopier les anciennes champs dans les nouveaux ? + * @return boolean true + */ + public function save($enable_copy = true) + { + $db = DB::getInstance(); + $config = Config::getInstance(); + + // Champs à créer + $create = [ + 'id INTEGER PRIMARY KEY, -- Numéro attribué automatiquement', + 'id_categorie INTEGER NOT NULL, -- Numéro de catégorie', + 'date_connexion TEXT NULL, -- Date de dernière connexion', + 'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE, -- Date d\'inscription', + ]; + + $create_keys = [ + 'FOREIGN KEY (id_categorie) REFERENCES membres_categories (id)' + ]; + + // Champs à recopier + $copy = [ + 'id', + 'id_categorie', + 'date_connexion', + 'date_inscription', + ]; + + $anciens_champs = $config->get('champs_membres'); + $anciens_champs = is_null($anciens_champs) ? $this->champs : $anciens_champs->getAll(); + + foreach ($this->champs as $key=>$cfg) + { + if ($cfg['type'] == 'number') + $type = 'FLOAT'; + elseif ($cfg['type'] == 'multiple' || $cfg['type'] == 'checkbox') + $type = 'INTEGER'; + elseif ($cfg['type'] == 'file') + $type = 'BLOB'; + else + $type = 'TEXT'; + + $line = $key . ' ' . $type . ','; + + if (!empty($cfg['title'])) + { + $line .= ' -- ' . str_replace(["\n", "\r"], '', $cfg['title']); + } + + $create[] = $line; + + if (array_key_exists($key, $anciens_champs)) + { + $copy[] = $key; + } + } + + $create = array_merge($create, $create_keys); + + $create = 'CREATE TABLE membres_tmp (' . "\n\t" . implode("\n\t", $create) . "\n);"; + $copy = 'INSERT INTO membres_tmp (' . implode(', ', $copy) . ') SELECT ' . implode(', ', $copy) . ' FROM membres;'; + + $db->exec('PRAGMA foreign_keys = OFF;'); + $db->exec('BEGIN;'); + $db->exec($create); + + if ($enable_copy) { + $db->exec($copy); + } + + $db->exec('DROP TABLE IF EXISTS membres;'); + $db->exec('ALTER TABLE membres_tmp RENAME TO membres;'); + $db->exec('CREATE INDEX membres_id_categorie ON membres (id_categorie);'); // Index + + if ($config->get('champ_identifiant')) + { + // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique + $db->exec('UPDATE membres SET '.$config->get('champ_identifiant').' = NULL + WHERE '.$config->get('champ_identifiant').' = "";'); + + // Création de l'index unique + $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$config->get('champ_identifiant').');'); + } + + // Création des index pour les champs affichés dans la liste des membres + $listed_fields = array_keys($this->getListedFields()); + foreach ($listed_fields as $field) + { + if ($field === $config->get('champ_identifiant')) + { + // Il y a déjà un index + continue; + } + + $db->exec('CREATE INDEX membres_liste_' . $field . ' ON membres (' . $field . ');'); + } + + $db->exec('END;'); + $db->exec('PRAGMA foreign_keys = ON;'); + + $config->set('champs_membres', $this); + $config->save(); + + return true; + } +} \ No newline at end of file diff --git a/include/class.compta_categories.php b/include/class.compta_categories.php new file mode 100644 index 0000000..0798be2 --- /dev/null +++ b/include/class.compta_categories.php @@ -0,0 +1,126 @@ +exec(file_get_contents(ROOT . '/include/data/categories_comptables.sql')); + } + + public function add($data) + { + $this->_checkFields($data); + + $db = DB::getInstance(); + + if (empty($data['compte']) || !trim($data['compte'])) + { + throw new UserException('Le compte associé ne peut rester vide.'); + } + + $data['compte'] = trim($data['compte']); + + if (!$db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $data['compte'])) + { + throw new UserException('Le compte associé n\'existe pas.'); + } + + if (!isset($data['type']) || + ($data['type'] != self::DEPENSES && $data['type'] != self::RECETTES)) + { + // Catégories "autres" pas possibles pour le moment + throw new UserException('Type de catégorie inconnu.'); + } + + $db->simpleInsert('compta_categories', [ + 'intitule' => $data['intitule'], + 'description'=> $data['description'], + 'compte' => $data['compte'], + 'type' => (int)$data['type'], + ]); + + return $db->lastInsertRowId(); + } + + public function edit($id, $data) + { + $this->_checkFields($data); + + $db = DB::getInstance(); + + $db->simpleUpdate('compta_categories', + [ + 'intitule' => $data['intitule'], + 'description'=> $data['description'], + ], + 'id = \''.$db->escapeString(trim($id)).'\''); + + return true; + } + + public function delete($id) + { + $db = DB::getInstance(); + + // Ne pas supprimer une catégorie qui est utilisée ! + if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_categorie = ? LIMIT 1;', false, $id)) + { + throw new UserException('Cette catégorie ne peut être supprimée car des opérations comptables y sont liées.'); + } + + $db->simpleExec('DELETE FROM compta_categories WHERE id = ?;', $id); + + return true; + } + + public function get($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT * FROM compta_categories WHERE id = ?;', true, (int)$id); + } + + public function getList($type = null) + { + $db = DB::getInstance(); + $type = is_null($type) ? '' : 'cat.type = '.(int)$type; + return $db->simpleStatementFetchAssocKey(' + SELECT cat.id, cat.*, cc.libelle AS compte_libelle + FROM compta_categories AS cat INNER JOIN compta_comptes AS cc + ON cc.id = cat.compte + WHERE '.$type.' ORDER BY cat.intitule;', SQLITE3_ASSOC); + } + + public function listMoyensPaiement() + { + $db = DB::getInstance(); + return $db->simpleStatementFetchAssocKey('SELECT code, nom FROM compta_moyens_paiement ORDER BY nom COLLATE NOCASE;'); + } + + public function getMoyenPaiement($code) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT nom FROM compta_moyens_paiement WHERE code = ?;', false, $code); + } + + protected function _checkFields(&$data) + { + if (empty($data['intitule']) || !trim($data['intitule'])) + { + throw new UserException('L\'intitulé ne peut rester vide.'); + } + + $data['intitule'] = trim($data['intitule']); + $data['description'] = isset($data['description']) ? trim($data['description']) : ''; + + return true; + } +} + +?> \ No newline at end of file diff --git a/include/class.compta_comptes.php b/include/class.compta_comptes.php new file mode 100644 index 0000000..02021a2 --- /dev/null +++ b/include/class.compta_comptes.php @@ -0,0 +1,325 @@ +exec('BEGIN;'); + $ids = []; + + foreach ($plan as $id=>$compte) + { + $ids[] = $id; + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $id)) + { + $db->simpleUpdate('compta_comptes', [ + 'parent' => $compte['parent'], + 'libelle' => $compte['nom'], + 'position' => $compte['position'], + 'plan_comptable' => 1, + ], 'id = \''.$db->escapeString($id).'\''); + } + else + { + $db->simpleInsert('compta_comptes', [ + 'id' => $id, + 'parent' => $compte['parent'], + 'libelle' => $compte['nom'], + 'position' => $compte['position'], + 'plan_comptable' => 1, + ]); + } + } + + $db->exec('DELETE FROM compta_comptes WHERE id NOT IN(\''.implode('\', \'', $ids).'\') AND plan_comptable = 1;'); + + $db->exec('END;'); + + return true; + } + + public function add($data) + { + $this->_checkFields($data, true); + + $db = DB::getInstance(); + + if (empty($data['id'])) + { + $new_id = $data['parent']; + $nb_sous_comptes = $db->simpleQuerySingle('SELECT COUNT(*) FROM compta_comptes WHERE parent = ?;', false, $new_id); + + // Pas plus de 26 sous-comptes par compte, parce que l'alphabet s'arrête à 26 lettres + if ($nb_sous_comptes >= 26) + { + throw new UserException('Nombre de sous-comptes maximal atteint pour ce compte parent-ci.'); + } + + $new_id .= chr(65+(int)$nb_sous_comptes); + } + else + { + $new_id = $data['id']; + } + + if (isset($data['position'])) + { + $position = (int) $data['position']; + } + else + { + $position = $db->simpleQuerySingle('SELECT position FROM compta_comptes WHERE id = ?;', false, $data['parent']); + } + + $db->simpleInsert('compta_comptes', [ + 'id' => $new_id, + 'libelle' => trim($data['libelle']), + 'parent' => $data['parent'], + 'plan_comptable' => 0, + 'position' => (int)$position, + ]); + + return $new_id; + } + + public function edit($id, $data) + { + $db = DB::getInstance(); + + // Vérification que l'on peut éditer ce compte + if ($db->simpleQuerySingle('SELECT plan_comptable FROM compta_comptes WHERE id = ?;', false, $id)) + { + throw new UserException('Ce compte fait partie du plan comptable et n\'est pas modifiable.'); + } + + if (isset($data['position']) && empty($data['position'])) + { + throw new UserException('Aucune position du compte n\'a été indiquée.'); + } + + $this->_checkFields($data); + + $update = [ + 'libelle' => trim($data['libelle']), + ]; + + if (isset($data['position'])) + { + $update['position'] = (int) trim($data['position']); + } + + $db->simpleUpdate('compta_comptes', $update, 'id = \''.$db->escapeString(trim($id)).'\''); + + return true; + } + + public function delete($id) + { + $db = DB::getInstance(); + + // Ne pas supprimer un compte qui est utilisé ! + if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE compte_debit = ? OR compte_debit = ? LIMIT 1;', false, $id, $id)) + { + throw new UserException('Ce compte ne peut être supprimé car des opérations comptables y sont liées.'); + } + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_comptes_bancaires WHERE id = ? LIMIT 1;', false, $id)) + { + throw new UserException('Ce compte ne peut être supprimé car il est lié à un compte bancaire.'); + } + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id)) + { + throw new UserException('Ce compte ne peut être supprimé car des catégories y sont liées.'); + } + + $db->simpleExec('DELETE FROM compta_comptes WHERE id = ?;', trim($id)); + + return true; + } + + /** + * Peut-on supprimer ce compte ? (OUI s'il n'a pas d'écriture liée) + * @param string $id Numéro du compte + * @return boolean TRUE si le compte n'a pas d'écriture liée + */ + public function canDelete($id) + { + $db = DB::getInstance(); + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal + WHERE compte_debit = ? OR compte_debit = ? LIMIT 1;', false, $id, $id)) + { + return false; + } + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id)) + { + return false; + } + + return true; + } + + /** + * Peut-on désactiver ce compte ? (OUI s'il n'a pas d'écriture liée dans l'exercice courant) + * @param string $id Numéro du compte + * @return boolean TRUE si le compte n'a pas d'écriture liée dans l'exercice courant + */ + public function canDisable($id) + { + $db = DB::getInstance(); + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal + WHERE id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1) + AND (compte_debit = ? OR compte_debit = ?) LIMIT 1;', false, $id, $id)) + { + return false; + } + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id)) + { + return false; + } + + return true; + } + + /** + * Désactiver un compte + * Le compte ne sera plus utilisable pour les écritures ou les catégories mais restera en base de données + * @param string $id Numéro du compte + * @return boolean TRUE si la désactivation a fonctionné, une exception utilisateur si + * la désactivation n'est pas possible. + */ + public function disable($id) + { + $db = DB::getInstance(); + + // Ne pas désactiver un compte utilisé dans l'exercice courant + if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal + WHERE id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1) + AND (compte_debit = ? OR compte_debit = ?) LIMIT 1;', false, $id, $id)) + { + throw new UserException('Ce compte ne peut être désactivé car des écritures y sont liées sur l\'exercice courant. ' + . 'Il faut supprimer ou ré-attribuer ces écritures avant de pouvoir supprimer le compte.'); + } + + // Ne pas désactiver un compte utilisé pour une catégorie + if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id)) + { + throw new UserException('Ce compte ne peut être désactivé car des catégories y sont liées.'); + } + + return $db->simpleUpdate('compta_comptes', ['desactive' => 1], 'id = \''.$db->escapeString(trim($id)).'\''); + } + + public function get($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT * FROM compta_comptes WHERE id = ?;', true, trim($id)); + } + + public function getList($parent = 0) + { + $db = DB::getInstance(); + return $db->simpleStatementFetchAssocKey('SELECT id, * FROM compta_comptes WHERE parent = ? ORDER BY id;', SQLITE3_ASSOC, $parent); + } + + public function getListAll($parent = 0) + { + $db = DB::getInstance(); + return $db->queryFetchAssoc('SELECT id, libelle FROM compta_comptes ORDER BY id;'); + } + + public function listTree($parent = 0, $include_children = true) + { + $db = DB::getInstance(); + + if ($include_children) + { + $parent = $parent ? 'WHERE parent LIKE \''.$db->escapeString($parent).'%\' ' : ''; + } + else + { + $parent = $parent ? 'WHERE parent = \''.$db->escapeString($parent).'\' ' : 'WHERE parent = 0'; + } + + return $db->simpleStatementFetch('SELECT * FROM compta_comptes '.$parent.' ORDER BY id;'); + } + + protected function _checkFields(&$data, $force_parent_check = false) + { + $db = DB::getInstance(); + + if (empty($data['libelle']) || !trim($data['libelle'])) + { + throw new UserException('Le libellé ne peut rester vide.'); + } + + $data['libelle'] = trim($data['libelle']); + + if (isset($data['id'])) + { + $force_parent_check = true; + $data['id'] = trim($data['id']); + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $data['id'])) + { + throw new UserException('Le compte numéro '.$data['id'].' existe déjà.'); + } + } + + if (isset($data['parent']) || $force_parent_check) + { + if (empty($data['parent']) && !trim($data['parent'])) + { + throw new UserException('Le compte ne peut pas ne pas avoir de compte parent.'); + } + + if (!($id = $db->simpleQuerySingle('SELECT id FROM compta_comptes WHERE id = ?;', false, $data['parent']))) + { + throw new UserException('Le compte parent indiqué n\'existe pas.'); + } + + $data['parent'] = trim($id); + } + + if (isset($data['id'])) + { + if (strncmp($data['id'], $data['parent'], strlen($data['parent'])) !== 0) + { + throw new UserException('Le compte '.$data['id'].' n\'est pas un sous-compte de '.$data['parent'].'.'); + } + } + + return true; + } + + public function getPositions() + { + return [ + self::ACTIF => 'Actif', + self::PASSIF => 'Passif', + self::ACTIF | self::PASSIF => 'Actif ou passif (déterminé automatiquement au bilan selon le solde du compte)', + self::CHARGE => 'Charge', + self::PRODUIT => 'Produit', + self::CHARGE | self::PRODUIT => 'Charge et produit', + ]; + } +} + +?> \ No newline at end of file diff --git a/include/class.compta_comptes_bancaires.php b/include/class.compta_comptes_bancaires.php new file mode 100644 index 0000000..1ac1213 --- /dev/null +++ b/include/class.compta_comptes_bancaires.php @@ -0,0 +1,164 @@ +_checkBankFields($data); + + $new_id = parent::add($data); + + $db->simpleInsert('compta_comptes_bancaires', [ + 'id' => $new_id, + 'banque' => $data['banque'], + 'iban' => $data['iban'], + 'bic' => $data['bic'], + ]); + + return $new_id; + } + + public function edit($id, $data) + { + $db = DB::getInstance(); + + if (!$db->simpleQuerySingle('SELECT 1 FROM compta_comptes_bancaires WHERE id = ?;', false, $id)) + { + throw new UserException('Ce compte n\'est pas un compte bancaire.'); + } + + $this->_checkBankFields($data); + $result = parent::edit($id, $data); + + if (!$result) + { + return $result; + } + + $db->simpleUpdate('compta_comptes_bancaires', [ + 'banque' => $data['banque'], + 'iban' => $data['iban'], + 'bic' => $data['bic'], + ], 'id = \''.$db->escapeString(trim($id)).'\''); + + return true; + } + + /** + * Supprime un compte bancaire + * La suppression sera refusée si le compte est utilisé dans l'exercice en cours + * ou dans une catégorie. + * Le compte bancaire sera supprimé et le compte au plan comptable seulement désactivé + * si le compte est utilisé dans un exercice précédent. + * + * La désactivation d'un compte fait qu'il n'est plus utilisable dans l'exercice courant + * ou les exercices suivants, mais il est possible de le réactiver. + * @param string $id Numéro du compte + * @return boolean TRUE si la suppression ou désactivation a été effectuée, une exception ou FALSE sinon + */ + public function delete($id) + { + $db = DB::getInstance(); + if (!$db->simpleQuerySingle('SELECT 1 FROM compta_comptes_bancaires WHERE id = ?;', false, trim($id))) + { + throw new UserException('Ce compte n\'est pas un compte bancaire.'); + } + + // Ne pas supprimer/désactiver un compte qui est utilisé dans l'exercice courant + if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal + WHERE id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1) + AND (compte_debit = ? OR compte_debit = ?) LIMIT 1;', false, $id, $id)) + { + throw new UserException('Ce compte ne peut être supprimé car des écritures y sont liées sur l\'exercice courant. ' + . 'Il faut supprimer ou ré-attribuer ces écritures avant de pouvoir supprimer le compte.'); + } + + // Il n'est pas possible de supprimer ou désactiver un compte qui est lié à des catégories + if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id)) + { + throw new UserException('Ce compte ne peut être supprimé car des catégories y sont liées. ' + . 'Merci de supprimer ou modifier les catégories liées avant de le supprimer.'); + } + + $db->simpleExec('DELETE FROM compta_comptes_bancaires WHERE id = ?;', trim($id)); + + try { + $return = parent::delete($id); + } + catch (UserException $e) { + // Impossible de supprimer car des opérations y sont encore liées + // sur les exercices précédents, alors on le désactive + $return = parent::disable($id); + } + + return $return; + } + + public function get($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT * FROM compta_comptes AS c + INNER JOIN compta_comptes_bancaires AS cc + ON c.id = cc.id + WHERE c.id = ?;', true, $id); + } + + public function getList($parent = false) + { + $db = DB::getInstance(); + return $db->simpleStatementFetchAssocKey('SELECT c.id AS id, * FROM compta_comptes AS c + INNER JOIN compta_comptes_bancaires AS cc ON c.id = cc.id + WHERE c.parent = '.self::NUMERO_PARENT_COMPTES.' ORDER BY c.id;'); + } + + protected function _checkBankFields(&$data) + { + if (empty($data['banque']) || !trim($data['banque'])) + { + throw new UserException('Le nom de la banque ne peut rester vide.'); + } + + if (empty($data['bic'])) + { + $data['bic'] = ''; + } + else + { + $data['bic'] = trim(strtoupper($data['bic'])); + $data['bic'] = preg_replace('![^\dA-Z]!', '', $data['bic']); + + if (!utils::checkBIC($data['bic'])) + { + throw new UserException('Code BIC/SWIFT invalide.'); + } + } + + if (empty($data['iban'])) + { + $data['iban'] = ''; + } + else + { + $data['iban'] = trim(strtoupper($data['iban'])); + $data['iban'] = preg_replace('![^\dA-Z]!', '', $data['iban']); + + if (!utils::checkIBAN($data['iban'])) + { + throw new UserException('Code IBAN invalide.'); + } + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/include/class.compta_exercices.php b/include/class.compta_exercices.php new file mode 100644 index 0000000..02edb14 --- /dev/null +++ b/include/class.compta_exercices.php @@ -0,0 +1,569 @@ +_checkFields($data); + + $db = DB::getInstance(); + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices WHERE + (debut <= :debut AND fin >= :debut) OR (debut <= :fin AND fin >= :fin);', false, + ['debut' => $data['debut'], 'fin' => $data['fin']])) + { + throw new UserException('La date de début ou de fin se recoupe avec un autre exercice.'); + } + + if ($db->querySingle('SELECT 1 FROM compta_exercices WHERE cloture = 0;')) + { + throw new UserException('Il n\'est pas possible de créer un nouvel exercice tant qu\'il existe un exercice non-clôturé.'); + } + + $db->simpleInsert('compta_exercices', [ + 'libelle' => trim($data['libelle']), + 'debut' => $data['debut'], + 'fin' => $data['fin'], + ]); + + return $db->lastInsertRowId(); + } + + public function edit($id, $data) + { + $db = DB::getInstance(); + + $this->_checkFields($data); + + // Evitons que les exercices se croisent + if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices WHERE id != :id AND + ((debut <= :debut AND fin >= :debut) OR (debut <= :fin AND fin >= :fin));', false, + ['debut' => $data['debut'], 'fin' => $data['fin'], 'id' => (int) $id])) + { + throw new UserException('La date de début ou de fin se recoupe avec un autre exercice.'); + } + + // On vérifie qu'on ne va pas mettre des opérations en dehors de tout exercice + if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_exercice = ? + AND date < ? LIMIT 1;', false, (int)$id, $data['debut'])) + { + throw new UserException('Des opérations de cet exercice ont une date antérieure à la date de début de l\'exercice.'); + } + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_exercice = ? + AND date > ? LIMIT 1;', false, (int)$id, $data['fin'])) + { + throw new UserException('Des opérations de cet exercice ont une date postérieure à la date de fin de l\'exercice.'); + } + + $db->simpleUpdate('compta_exercices', [ + 'libelle' => trim($data['libelle']), + 'debut' => $data['debut'], + 'fin' => $data['fin'], + ], 'id = \''.(int)$id.'\''); + + return true; + } + + /** + * Clôturer un exercice et en ouvrir un nouveau + * Le report à nouveau n'est pas effectué automatiquement par cette fonction, voir doReports pour ça. + * @param integer $id ID de l'exercice à clôturer + * @param string $end Date de clôture de l'exercice au format Y-m-d + * @return integer L'ID du nouvel exercice créé + */ + public function close($id, $end) + { + $db = DB::getInstance(); + + if (!utils::checkDate($end)) + { + throw new UserException('Date de fin vide ou invalide.'); + } + + $db->exec('BEGIN;'); + + // Clôture de l'exercice + $db->simpleUpdate('compta_exercices', [ + 'cloture' => 1, + 'fin' => $end, + ], 'id = \''.(int)$id.'\''); + + // Date de début du nouvel exercice : lendemain de la clôture du précédent exercice + $new_begin = utils::modifyDate($end, '+1 day'); + + // Date de fin du nouvel exercice : un an moins un jour après l'ouverture + $new_end = utils::modifyDate($new_begin, '+1 year -1 day'); + + // Enfin sauf s'il existe déjà des opérations après cette date, auquel cas la date de fin + // est fixée à la date de la dernière opération, ceci pour ne pas avoir d'opération + // orpheline d'exercice + $last = $db->simpleQuerySingle('SELECT date FROM compta_journal WHERE id_exercice = ? AND date >= ? ORDER BY date DESC LIMIT 1;', false, $id, $new_end); + $new_end = $last ?: $new_end; + + // Création du nouvel exercice + $new_id = $this->add([ + 'debut' => $new_begin, + 'fin' => $new_end, + 'libelle' => 'Nouvel exercice' + ]); + + // Ré-attribution des opérations de l'exercice à clôturer qui ne sont pas dans son + // intervale au nouvel exercice + $db->simpleExec('UPDATE compta_journal SET id_exercice = ? WHERE id_exercice = ? AND date >= ?;', + $new_id, $id, $new_begin); + + $db->exec('END;'); + + return $new_id; + } + + /** + * Créer les reports à nouveau issus de l'exercice $old_id dans le nouvel exercice courant + * @param integer $old_id ID de l'ancien exercice + * @param integer $new_id ID du nouvel exercice + * @param string $date Date Y-m-d donnée aux opérations créées + * @return boolean true si succès + */ + public function doReports($old_id, $date) + { + $db = DB::getInstance(); + + $db->exec('BEGIN;'); + + $this->solderResultat($old_id, $date); + + $report_crediteur = 110; + $report_debiteur = 119; + + // Récupérer chacun des comptes de bilan et leurs soldes (uniquement les classes 1 à 5) + $statement = $db->simpleStatement('SELECT compta_comptes.id AS compte, compta_comptes.position AS position, + COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_debit = compta_comptes.id AND id_exercice = :id), 0) + - COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_credit = compta_comptes.id AND id_exercice = :id), 0) AS solde + FROM compta_comptes + INNER JOIN compta_journal ON compta_comptes.id = compta_journal.compte_debit + OR compta_comptes.id = compta_journal.compte_credit + WHERE id_exercice = :id AND solde != 0 AND CAST(substr(compta_comptes.id, 1, 1) AS INTEGER) <= 5 + GROUP BY compta_comptes.id;', ['id' => $old_id]); + + $diff = 0; + $journal = new Compta_Journal; + + while ($row = $statement->fetchArray(SQLITE3_ASSOC)) + { + $solde = ($row['position'] & Compta_Comptes::ACTIF) ? abs($row['solde']) : -abs($row['solde']); + $solde = round($solde, 2); + + $diff += $solde; + + if (empty($solde)) + { + continue; + } + + // Chaque solde de compte est reporté dans le nouvel exercice + $journal->add([ + 'libelle' => 'Report à nouveau', + 'date' => $date, + 'montant' => abs($solde), + 'compte_debit' => ($solde < 0 ? NULL : $row['compte']), + 'compte_credit' => ($solde > 0 ? NULL : $row['compte']), + 'remarques' => 'Report de solde créé automatiquement à la clôture de l\'exercice précédent', + ]); + } + + // FIXME utiliser $diff pour équilibrer + + $db->exec('END;'); + + return true; + } + + /** + * Solder les comptes de charge et de produits de l'exercice N + * et les inscrire au résultat de l'exercice N+1 + * @param integer $exercice ID de l'exercice à solder + * @param string $date Date de début de l'exercice Y-m-d + * @return boolean true en cas de succès + */ + public function solderResultat($exercice, $date) + { + $db = DB::getInstance(); + + $resultat_excedent = 120; + $resultat_debiteur = 129; + + $resultat = $this->getCompteResultat($exercice); + $resultat = $resultat['resultat']; + + if ($resultat != 0) + { + $journal = new Compta_Journal; + $journal->add([ + 'libelle' => 'Résultat de l\'exercice précédent', + 'date' => $date, + 'montant' => abs($resultat), + 'compte_debit' => $resultat < 0 ? 129 : NULL, + 'compte_credit' => $resultat > 0 ? 120 : NULL, + ]); + } + + return true; + } + + public function delete($id) + { + $db = DB::getInstance(); + + // Ne pas supprimer un compte qui est utilisé ! + if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_exercice = ? LIMIT 1;', false, $id)) + { + throw new UserException('Cet exercice ne peut être supprimé car des opérations comptables y sont liées.'); + } + + $db->simpleExec('DELETE FROM compta_exercices WHERE id = ?;', (int)$id); + + return true; + } + + public function get($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT *, strftime(\'%s\', debut) AS debut, + strftime(\'%s\', fin) AS fin FROM compta_exercices WHERE id = ?;', true, (int)$id); + } + + public function getCurrent() + { + $db = DB::getInstance(); + return $db->querySingle('SELECT *, strftime(\'%s\', debut) AS debut, strftime(\'%s\', fin) FROM compta_exercices + WHERE cloture = 0 LIMIT 1;', true); + } + + public function getCurrentId() + { + $db = DB::getInstance(); + return $db->querySingle('SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1;'); + } + + public function getList() + { + $db = DB::getInstance(); + return $db->simpleStatementFetchAssocKey('SELECT id, *, strftime(\'%s\', debut) AS debut, + strftime(\'%s\', fin) AS fin, + (SELECT COUNT(*) FROM compta_journal WHERE id_exercice = compta_exercices.id) AS nb_operations + FROM compta_exercices ORDER BY fin DESC;', SQLITE3_ASSOC); + } + + protected function _checkFields(&$data) + { + if (empty($data['libelle']) || !trim($data['libelle'])) + { + throw new UserException('Le libellé ne peut rester vide.'); + } + + $data['libelle'] = trim($data['libelle']); + + if (empty($data['debut']) || !checkdate(substr($data['debut'], 5, 2), substr($data['debut'], 8, 2), substr($data['debut'], 0, 4))) + { + throw new UserException('Date de début vide ou invalide.'); + } + + if (empty($data['fin']) || !checkdate(substr($data['fin'], 5, 2), substr($data['fin'], 8, 2), substr($data['fin'], 0, 4))) + { + throw new UserException('Date de fin vide ou invalide.'); + } + + return true; + } + + + public function getJournal($exercice) + { + $db = DB::getInstance(); + $query = 'SELECT *, strftime(\'%s\', date) AS date FROM compta_journal + WHERE id_exercice = '.(int)$exercice.' ORDER BY date, id;'; + return $db->simpleStatementFetch($query); + } + + public function getGrandLivre($exercice) + { + $db = DB::getInstance(); + $livre = ['classes' => [], 'debit' => 0.0, 'credit' => 0.0]; + + $res = $db->prepare('SELECT compte FROM + (SELECT compte_debit AS compte FROM compta_journal + WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_debit + UNION + SELECT compte_credit AS compte FROM compta_journal + WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_credit) + ORDER BY base64(compte) COLLATE BINARY ASC;' + )->execute(); + + while ($row = $res->fetchArray(SQLITE3_NUM)) + { + $compte = $row[0]; + + if (is_null($compte)) + continue; + + $classe = substr($compte, 0, 1); + $parent = substr($compte, 0, 2); + + if (!array_key_exists($classe, $livre['classes'])) + { + $livre['classes'][$classe] = []; + } + + if (!array_key_exists($parent, $livre['classes'][$classe])) + { + $livre['classes'][$classe][$parent] = [ + 'total' => 0.0, + 'comptes' => [], + ]; + } + + $livre['classes'][$classe][$parent]['comptes'][$compte] = ['debit' => 0.0, 'credit' => 0.0, 'journal' => []]; + + $livre['classes'][$classe][$parent]['comptes'][$compte]['journal'] = $db->simpleStatementFetch( + 'SELECT *, strftime(\'%s\', date) AS date FROM ( + SELECT * FROM compta_journal WHERE compte_debit = :compte AND id_exercice = '.(int)$exercice.' + UNION + SELECT * FROM compta_journal WHERE compte_credit = :compte AND id_exercice = '.(int)$exercice.' + ) + ORDER BY date, numero_piece, id;', SQLITE3_ASSOC, ['compte' => $compte]); + + $debit = (float) $db->simpleQuerySingle( + 'SELECT SUM(montant) FROM compta_journal WHERE compte_debit = ? AND id_exercice = '.(int)$exercice.';', + false, $compte); + + $credit = (float) $db->simpleQuerySingle( + 'SELECT SUM(montant) FROM compta_journal WHERE compte_credit = ? AND id_exercice = '.(int)$exercice.';', + false, $compte); + + $livre['classes'][$classe][$parent]['comptes'][$compte]['debit'] = $debit; + $livre['classes'][$classe][$parent]['comptes'][$compte]['credit'] = $credit; + + $livre['classes'][$classe][$parent]['total'] += $debit; + $livre['classes'][$classe][$parent]['total'] -= $credit; + + $livre['debit'] += $debit; + $livre['credit'] += $credit; + } + + $res->finalize(); + + return $livre; + } + + public function getCompteResultat($exercice) + { + $db = DB::getInstance(); + + $charges = ['comptes' => [], 'total' => 0.0]; + $produits = ['comptes' => [], 'total' => 0.0]; + $resultat = 0.0; + + $res = $db->prepare('SELECT compte, SUM(debit), SUM(credit) + FROM + (SELECT compte_debit AS compte, SUM(montant) AS debit, 0 AS credit + FROM compta_journal WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_debit + UNION + SELECT compte_credit AS compte, 0 AS debit, SUM(montant) AS credit + FROM compta_journal WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_credit) + WHERE compte LIKE \'6%\' OR compte LIKE \'7%\' + GROUP BY compte + ORDER BY base64(compte) COLLATE BINARY ASC;' + )->execute(); + + while ($row = $res->fetchArray(SQLITE3_NUM)) + { + list($compte, $debit, $credit) = $row; + $classe = substr($compte, 0, 1); + $parent = substr($compte, 0, 2); + + if ($classe == 6) + { + if (!isset($charges['comptes'][$parent])) + { + $charges['comptes'][$parent] = ['comptes' => [], 'solde' => 0.0]; + } + + $solde = round($debit - $credit, 2); + + if (empty($solde)) + continue; + + $charges['comptes'][$parent]['comptes'][$compte] = $solde; + $charges['total'] += $solde; + $charges['comptes'][$parent]['solde'] += $solde; + } + elseif ($classe == 7) + { + if (!isset($produits['comptes'][$parent])) + { + $produits['comptes'][$parent] = ['comptes' => [], 'solde' => 0.0]; + } + + $solde = round($credit - $debit, 2); + + if (empty($solde)) + continue; + + $produits['comptes'][$parent]['comptes'][$compte] = $solde; + $produits['total'] += $solde; + $produits['comptes'][$parent]['solde'] += $solde; + } + } + + $res->finalize(); + + $resultat = $produits['total'] - $charges['total']; + + return ['charges' => $charges, 'produits' => $produits, 'resultat' => $resultat]; + } + + /** + * Calculer le bilan comptable pour l'exercice $exercice + * @param integer $exercice ID de l'exercice dont il faut produire le bilan + * @param boolean $resultat true s'il faut calculer le résultat de l'exercice (utile pour un exercice en cours) + * @return array Un tableau multi-dimensionnel avec deux clés : actif et passif + */ + public function getBilan($exercice) + { + $db = DB::getInstance(); + + $include = [Compta_Comptes::ACTIF, Compta_Comptes::PASSIF, + Compta_Comptes::PASSIF | Compta_Comptes::ACTIF]; + + $actif = ['comptes' => [], 'total' => 0.0]; + $passif = ['comptes' => [], 'total' => 0.0]; + + $resultat = $this->getCompteResultat($exercice); + + if ($resultat['resultat'] >= 0) + { + $passif['comptes']['12'] = [ + 'comptes' => ['120' => $resultat['resultat']], + 'solde' => $resultat['resultat'] + ]; + + $passif['total'] = $resultat['resultat']; + } + else + { + $passif['comptes']['12'] = [ + 'comptes' => ['129' => $resultat['resultat']], + 'solde' => $resultat['resultat'] + ]; + + $passif['total'] = $resultat['resultat']; + } + + // Y'a sûrement moyen d'améliorer tout ça pour que le maximum de travail + // soit fait au niveau du SQL, mais pour le moment ça marche + $res = $db->prepare('SELECT compte, debit, credit, (SELECT position FROM compta_comptes WHERE id = compte) AS position + FROM + (SELECT compte_debit AS compte, SUM(montant) AS debit, NULL AS credit + FROM compta_journal WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_debit + UNION + SELECT compte_credit AS compte, NULL AS debit, SUM(montant) AS credit + FROM compta_journal WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_credit) + WHERE compte IN (SELECT id FROM compta_comptes WHERE position IN ('.implode(', ', $include).')) + ORDER BY base64(compte) COLLATE BINARY ASC;' + )->execute(); + + while ($row = $res->fetchArray(SQLITE3_NUM)) + { + list($compte, $debit, $credit, $position) = $row; + $parent = substr($compte, 0, 2); + $classe = $compte[0]; + + if (($position & Compta_Comptes::ACTIF) && ($position & Compta_Comptes::PASSIF)) + { + $solde = $debit - $credit; + + if ($solde > 0) + $position = 'actif'; + elseif ($solde < 0) + $position = 'passif'; + else + continue; + + $solde = abs($solde); + } + else if ($position & Compta_Comptes::ACTIF) + { + $position = 'actif'; + $solde = $debit - $credit; + } + else if ($position & Compta_Comptes::PASSIF) + { + $position = 'passif'; + $solde = $credit - $debit; + } + else + { + continue; + } + + if (!isset(${$position}['comptes'][$parent])) + { + ${$position}['comptes'][$parent] = ['comptes' => [], 'solde' => 0]; + } + + if (!isset(${$position}['comptes'][$parent]['comptes'][$compte])) + { + ${$position}['comptes'][$parent]['comptes'][$compte] = 0; + } + + $solde = round($solde, 2); + ${$position}['comptes'][$parent]['comptes'][$compte] += $solde; + ${$position}['total'] += $solde; + ${$position}['comptes'][$parent]['solde'] += $solde; + } + + $res->finalize(); + + // Suppression des soldes nuls + foreach ($passif['comptes'] as $parent=>$p) + { + if ($p['solde'] == 0) + { + unset($passif['comptes'][$parent]); + continue; + } + + foreach ($p['comptes'] as $id=>$solde) + { + if ($solde == 0) + { + unset($passif['comptes'][$parent]['comptes'][$id]); + } + } + } + + foreach ($actif['comptes'] as $parent=>$p) + { + if (empty($p['solde'])) + { + unset($actif['comptes'][$parent]); + continue; + } + + foreach ($p['comptes'] as $id=>$solde) + { + if (empty($solde)) + { + unset($actif['comptes'][$parent]['comptes'][$id]); + } + } + } + + return ['actif' => $actif, 'passif' => $passif]; + } +} + +?> \ No newline at end of file diff --git a/include/class.compta_import.php b/include/class.compta_import.php new file mode 100644 index 0000000..73fd99b --- /dev/null +++ b/include/class.compta_import.php @@ -0,0 +1,387 @@ +prepare('SELECT + journal.id, + strftime(\'%d/%m/%Y\', date) AS date, + (CASE cat.type WHEN 1 THEN \'Recette\' WHEN -1 THEN \'Dépense\' ELSE \'Autre\' END) AS type, + (CASE cat.intitule WHEN NULL THEN \'\' ELSE cat.intitule END) AS cat, + journal.libelle, + montant, + compte_debit, + debit.libelle AS libelle_debit, + compte_credit, + credit.libelle AS libelle_credit, + (CASE moyen_paiement WHEN NULL THEN \'\' ELSE moyen.nom END) AS moyen, + numero_cheque, + numero_piece, + remarques + FROM compta_journal AS journal + LEFT JOIN compta_categories AS cat ON cat.id = journal.id_categorie + LEFT JOIN compta_comptes AS debit ON debit.id = journal.compte_debit + LEFT JOIN compta_comptes AS credit ON credit.id = journal.compte_credit + LEFT JOIN compta_moyens_paiement AS moyen ON moyen.code = journal.moyen_paiement + WHERE id_exercice = '.(int)$exercice.' + ORDER BY journal.date; + ')->execute(); + + $fp = fopen('php://output', 'w'); + + fputcsv($fp, $this->csv_header); + + while ($row = $res->fetchArray(SQLITE3_ASSOC)) + { + fputcsv($fp, $row); + } + + fclose($fp); + + return true; + } + + public function fromCSV($path) + { + if (!file_exists($path) || !is_readable($path)) + { + throw new \RuntimeException('Fichier inconnu : '.$path); + } + + $fp = fopen($path, 'r'); + + if (!$fp) + { + return false; + } + + $db = DB::getInstance(); + $db->exec('BEGIN;'); + $comptes = new Compta_Comptes; + $banques = new Compta_Comptes_Bancaires; + $cats = new Compta_Categories; + $journal = new Compta_Journal; + + $columns = array_flip($this->csv_header); + $liste_comptes = $db->simpleStatementFetchAssoc('SELECT id, id FROM compta_comptes;'); + $liste_cats = $db->simpleStatementFetchAssoc('SELECT intitule, id FROM compta_categories;'); + $liste_moyens = $cats->listMoyensPaiement(); + + $col = function($column) use (&$row, &$columns) + { + if (!isset($columns[$column])) + return null; + + if (!isset($row[$columns[$column]])) + return null; + + return $row[$columns[$column]]; + }; + + $line = 0; + $delim = utils::find_csv_delim($fp); + + while (!feof($fp)) + { + $row = fgetcsv($fp, 4096, $delim); + $line++; + + if (empty($row)) + { + continue; + } + + if ($line === 1) + { + if (trim($row[0]) != 'Numéro mouvement') + { + throw new UserException('Erreur sur la ligne ' . $line . ' : l\'entête des colonnes est absent ou incorrect.'); + } + + continue; + } + + if (count($row) != count($columns)) + { + $db->exec('ROLLBACK;'); + throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.'); + } + + if (trim($row[0]) !== '' && !is_numeric($row[0])) + { + $db->exec('ROLLBACK;'); + throw new UserException('Erreur sur la ligne ' . $line . ' : la première colonne doit être vide ou contenir le numéro unique d\'opération.'); + } + + $id = $col('Numéro mouvement'); + $date = $col('Date'); + + if (!preg_match('!^\d{2}/\d{2}/\d{4}$!', $date)) + { + $db->exec('ROLLBACK;'); + throw new UserException('Erreur sur la ligne ' . $line . ' : la date n\'est pas au format jj/mm/aaaa.'); + } + + $date = explode('/', $date); + $date = $date[2] . '-' . $date[1] . '-' . $date[0]; + + // En dehors de l'exercice courant + if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices + WHERE (? < debut OR ? > fin) AND cloture = 0;', false, $date, $date)) + { + continue; + } + + $debit = $col('Compte de débit - numéro'); + $credit = $col('Compte de crédit - numéro'); + + if (trim($debit) == '' && trim($credit) != '') + { + $debit = null; + } + elseif (trim($debit) != '' && trim($credit) == '') + { + $credit = null; + } + + $cat = $col('Catégorie'); + $moyen = strtoupper(substr($col('Moyen de paiement'), 0, 2)); + + if (!$moyen || !array_key_exists($moyen, $liste_moyens)) + { + $moyen = false; + $cat = false; + } + + if ($cat && !array_key_exists($cat, $liste_cats)) + { + $cat = $moyen = false; + } + + $data = [ + 'libelle' => $col('Libellé'), + 'montant' => (float) $col('Montant'), + 'date' => $date, + 'compte_credit' => $credit, + 'compte_debit' => $debit, + 'numero_piece' => $col('Numéro de pièce'), + 'remarques' => $col('Remarques'), + ]; + + if ($cat) + { + $data['moyen_paiement'] = $moyen; + $data['numero_cheque'] = $col('Numéro de chèque'); + $data['id_categorie'] = $liste_cats[$cat]; + } + + if (empty($id)) + { + $journal->add($data); + } + else + { + $journal->edit($id, $data); + } + } + + $db->exec('END;'); + + fclose($fp); + return true; + } + + public function fromCitizen($path) + { + if (!file_exists($path) || !is_readable($path)) + { + throw new \RuntimeException('Fichier inconnu : '.$path); + } + + $fp = fopen($path, 'r'); + + if (!$fp) + { + return false; + } + + $db = DB::getInstance(); + $db->exec('BEGIN;'); + $comptes = new Compta_Comptes; + $banques = new Compta_Comptes_Bancaires; + $cats = new Compta_Categories; + $journal = new Compta_Journal; + + $columns = []; + $liste_comptes = $db->simpleStatementFetchAssoc('SELECT id, id FROM compta_comptes;'); + $liste_cats = $db->simpleStatementFetchAssoc('SELECT intitule, id FROM compta_categories;'); + $liste_moyens = $cats->listMoyensPaiement(); + + $get_compte = function ($compte, $intitule) use (&$liste_comptes, &$comptes, &$banques) + { + if (substr($compte, 0, 2) == '51') + { + $compte = '512' . substr($compte, -1); + } + + // Création comptes + if (!array_key_exists($compte, $liste_comptes)) + { + if (substr($compte, 0, 3) == '512') + { + $liste_comptes[$compte] = $banques->add([ + 'libelle' => $intitule, + 'banque' => 'Inconnue', + ]); + } + else + { + $liste_comptes[$compte] = $comptes->add([ + 'id' => $compte, + 'libelle' => $intitule, + 'parent' => substr($compte, 0, -1) + ]); + } + } + + return $compte; + }; + + $col = function($column) use (&$row, &$columns) + { + if (!isset($columns[$column])) + return null; + + if (!isset($row[$columns[$column]])) + return null; + + return $row[$columns[$column]]; + }; + + $line = 0; + $delim = utils::find_csv_delim($fp); + + while (!feof($fp)) + { + $row = fgetcsv($fp, 4096, $delim); + $line++; + + if (empty($row)) + { + continue; + } + + if (empty($columns)) + { + $columns = $row; + $columns = array_flip($columns); + continue; + } + + $date = $col('Date'); + + if (!preg_match('!^\d{2}/\d{2}/\d{4}$!', $date)) + { + $db->exec('ROLLBACK;'); + throw new UserException('Erreur sur la ligne ' . $line . ' : la date n\'est pas au format jj/mm/aaaa.'); + } + + $date = explode('/', $date); + $date = $date[2] . '-' . $date[1] . '-' . $date[0]; + + if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices + WHERE (? < debut OR ? > fin) AND cloture = 0;', false, $date, $date)) + { + continue; + } + + $debit = $get_compte($col('Compte débité - Numéro'), $col('Compte débité - Intitulé')); + $credit = $get_compte($col('Compte crédité - Numéro'), $col('Compte crédité - Intitulé')); + + $cat = $col('Rubrique'); + $moyen = strtoupper(substr($col('Moyen de paiement'), 0, 2)); + + if (!$moyen || !array_key_exists($moyen, $liste_moyens)) + { + $moyen = false; + $cat = false; + } + + if ($cat && !array_key_exists($cat, $liste_cats)) + { + if ($col('Nature') == 'Recette') + { + $type = $cats::RECETTES; + $compte = $credit; + } + elseif ($col('Nature') == 'Dépense') + { + $type = $cats::DEPENSES; + $compte = $debit; + } + else + { + $type = $cats::AUTRES; + $cat = false; + } + + if ($type != $cats::AUTRES) + { + $liste_cats[$cat] = $cats->add([ + 'intitule' => $cat, + 'type' => $type, + 'compte' => $compte + ]); + } + } + + $data = [ + 'libelle' => $col('Libellé'), + 'montant' => $col('Montant'), + 'date' => $date, + 'compte_credit' => $credit, + 'compte_debit' => $debit, + 'numero_piece' => $col('Numéro de pièce'), + 'remarques' => $col('Remarques'), + ]; + + if ($cat) + { + $data['moyen_paiement'] = $moyen; + $data['numero_cheque'] = $col('Numéro de chèque'); + $data['id_categorie'] = $liste_cats[$cat]; + } + + $journal->add($data); + } + + $db->exec('END;'); + + fclose($fp); + return true; + } +} + +?> \ No newline at end of file diff --git a/include/class.compta_journal.php b/include/class.compta_journal.php new file mode 100644 index 0000000..9ff8c95 --- /dev/null +++ b/include/class.compta_journal.php @@ -0,0 +1,363 @@ +querySingle('SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1;'); + + if (!$id) + { + throw new UserException('Aucun exercice en cours.'); + } + + return $id; + } + + public function checkExercice() + { + return $this->_getCurrentExercice(); + } + + protected function _checkOpenExercice($id) + { + if (is_null($id)) + return true; + + $db = DB::getInstance(); + $id = $db->simpleQuerySingle('SELECT id FROM compta_exercices + WHERE cloture = 0 AND id = ? LIMIT 1;', false, (int)$id); + + if ($id) + return true; + + return false; + } + + public function getSolde($id_compte, $inclure_sous_comptes = false) + { + $db = DB::getInstance(); + $exercice = $this->_getCurrentExercice(); + $compte = $inclure_sous_comptes + ? 'LIKE \'' . $db->escapeString(trim($id_compte)) . '%\'' + : '= \'' . $db->escapeString(trim($id_compte)) . '\''; + + $debit = 'COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_debit '.$compte.' AND id_exercice = '.(int)$exercice.'), 0)'; + $credit = 'COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_credit '.$compte.' AND id_exercice = '.(int)$exercice.'), 0)'; + + // L'actif augmente au débit, le passif au crédit + $position = $db->simpleQuerySingle('SELECT position FROM compta_comptes WHERE id = ?;', false, $id_compte); + + if (($position & Compta_Comptes::ACTIF) || ($position & Compta_Comptes::CHARGE)) + { + $query = $debit . ' - ' . $credit; + } + else + { + $query = $credit . ' - ' . $debit; + } + + return $db->querySingle('SELECT ' . $query . ';'); + } + + public function getJournalCompte($compte, $inclure_sous_comptes = false) + { + $db = DB::getInstance(); + + $position = $db->simpleQuerySingle('SELECT position FROM compta_comptes WHERE id = ?;', false, $compte); + + $exercice = $this->_getCurrentExercice(); + $compte = $inclure_sous_comptes + ? 'LIKE \'' . $db->escapeString(trim($compte)) . '%\'' + : '= \'' . $db->escapeString(trim($compte)) . '\''; + + // L'actif et les charges augmentent au débit, le passif et les produits au crédit + if (($position & Compta_Comptes::ACTIF) || ($position & Compta_Comptes::CHARGE)) + { + $d = ''; + $c = '-'; + } + else + { + $d = '-'; + $c = ''; + } + + $query = 'SELECT *, strftime(\'%s\', date) AS date, + running_sum(CASE WHEN compte_debit '.$compte.' THEN '.$d.'montant ELSE '.$c.'montant END) AS solde + FROM compta_journal WHERE (compte_debit '.$compte.' OR compte_credit '.$compte.') AND id_exercice = '.(int)$exercice.' + ORDER BY date ASC;'; + + // Obligatoire pour bien taper dans l'index de la date + // sinon running_sum est appelé 2 fois et ça marche pas du coup + // FIXME mettre ça ailleurs pour que ça soit appelé moins souvent + $db->exec('ANALYZE compta_journal;'); + + $db->resetRunningSum(); + return $db->simpleStatementFetch($query); + } + + public function add($data) + { + $this->_checkFields($data); + + $db = DB::getInstance(); + + $data['id_exercice'] = $this->_getCurrentExercice(); + + $db->simpleInsert('compta_journal', $data); + $id = $db->lastInsertRowId(); + + return $id; + } + + public function edit($id, $data) + { + $db = DB::getInstance(); + + // Vérification que l'on peut éditer cette opération + if (!$this->_checkOpenExercice($db->simpleQuerySingle('SELECT id_exercice FROM compta_journal WHERE id = ?;', false, $id))) + { + throw new UserException('Cette opération fait partie d\'un exercice qui a été clôturé.'); + } + + $this->_checkFields($data); + + $db->simpleUpdate('compta_journal', $data, + 'id = \''.trim($id).'\''); + + return true; + } + + public function delete($id) + { + $db = DB::getInstance(); + + // Vérification que l'on peut éditer cette opération + if (!$this->_checkOpenExercice($db->simpleQuerySingle('SELECT id_exercice FROM compta_journal WHERE id = ?;', false, $id))) + { + throw new UserException('Cette opération fait partie d\'un exercice qui a été clôturé.'); + } + + $db->simpleExec('DELETE FROM membres_operations WHERE id_operation = ?;', (int)$id); + $db->simpleExec('DELETE FROM compta_journal WHERE id = ?;', (int)$id); + + return true; + } + + public function get($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT *, strftime(\'%s\', date) AS date FROM compta_journal WHERE id = ?;', true, $id); + } + + public function countForMember($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT COUNT(*) + FROM compta_journal WHERE id_auteur = ?;', false, (int)$id); + } + + public function listForMember($id, $exercice) + { + $db = DB::getInstance(); + return $db->simpleStatementFetch('SELECT * FROM compta_journal + WHERE id_auteur = ? AND id_exercice = ?;', \SQLITE3_ASSOC, (int)$id, (int)$exercice); + } + + protected function _checkFields(&$data) + { + $db = DB::getInstance(); + + if (empty($data['libelle']) || !trim($data['libelle'])) + { + throw new UserException('Le libellé ne peut rester vide.'); + } + + $data['libelle'] = trim($data['libelle']); + + if (!empty($data['moyen_paiement']) + && !$db->simpleQuerySingle('SELECT 1 FROM compta_moyens_paiement WHERE code = ?;', false, $data['moyen_paiement'])) + { + throw new UserException('Moyen de paiement invalide.'); + } + + if (empty($data['date']) || !utils::checkDate($data['date'])) + { + throw new UserException('Date vide ou invalide.'); + } + + if (!$db->simpleQuerySingle('SELECT 1 FROM compta_exercices WHERE cloture = 0 + AND debut <= :date AND fin >= :date;', false, ['date' => $data['date']])) + { + throw new UserException('La date ne correspond pas à l\'exercice en cours.'); + } + + if (empty($data['moyen_paiement'])) + { + $data['moyen_paiement'] = null; + $data['numero_cheque'] = null; + } + else + { + $data['moyen_paiement'] = strtoupper($data['moyen_paiement']); + + if ($data['moyen_paiement'] != 'CH') + { + $data['numero_cheque'] = null; + } + + if (!$db->simpleQuerySingle('SELECT 1 FROM compta_moyens_paiement WHERE code = ? LIMIT 1;', + false, $data['moyen_paiement'])) + { + throw new UserException('Moyen de paiement invalide.'); + } + } + + $data['montant'] = str_replace(',', '.', $data['montant']); + $data['montant'] = (float)$data['montant']; + + if ($data['montant'] <= 0) + { + throw new UserException('Le montant ne peut être égal ou inférieur à zéro.'); + } + + foreach (['remarques', 'numero_piece', 'numero_cheque'] as $champ) + { + if (empty($data[$champ]) || !trim($data[$champ])) + { + $data[$champ] = ''; + } + else + { + $data[$champ] = trim($data[$champ]); + } + } + + if (!array_key_exists('compte_debit', $data) || + (!is_null($data['compte_debit']) && + !$db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $data['compte_debit']))) + { + throw new UserException('Compte débité inconnu.'); + } + + if (!array_key_exists('compte_credit', $data) || + (!is_null($data['compte_credit']) && + !$db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $data['compte_credit']))) + { + throw new UserException('Compte crédité inconnu.'); + } + + $data['compte_credit'] = is_null($data['compte_credit']) ? null : strtoupper(trim($data['compte_credit'])); + $data['compte_debit'] = is_null($data['compte_debit']) ? null : strtoupper(trim($data['compte_debit'])); + + if ($data['compte_credit'] == $data['compte_debit']) + { + throw new UserException('Compte crédité identique au compte débité.'); + } + + if (isset($data['id_categorie'])) + { + if (!$db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE id = ?;', false, (int)$data['id_categorie'])) + { + throw new UserException('Catégorie inconnue.'); + } + + $data['id_categorie'] = (int)$data['id_categorie']; + } + else + { + $data['id_categorie'] = NULL; + } + + if (isset($data['id_auteur'])) + { + $data['id_auteur'] = (int)$data['id_auteur']; + } + + return true; + } + + public function getListForCategory($type = null, $cat = null) + { + $db = DB::getInstance(); + $exercice = $this->_getCurrentExercice(); + + $query = 'SELECT compta_journal.*, strftime(\'%s\', compta_journal.date) AS date '; + + if (is_null($cat) && !is_null($type)) + { + $query.= ', compta_categories.intitule AS categorie + FROM compta_journal LEFT JOIN compta_categories + ON compta_journal.id_categorie = compta_categories.id '; + } + else + { + $query.= ' FROM compta_journal '; + } + + $query .= ' WHERE '; + + if (!is_null($cat)) + { + $query .= 'id_categorie = ' . (int)$cat; + } + elseif (is_null($type) && is_null($cat)) + { + $query .= 'id_categorie IS NULL'; + } + else + { + $query.= 'id_categorie IN (SELECT id FROM compta_categories WHERE type = '.(int)$type.')'; + } + + $query .= ' AND id_exercice = ' . (int)$exercice; + $query .= ' ORDER BY date;'; + + return $db->simpleStatementFetch($query); + } + + public function searchSQL($query) + { + $db = DB::getInstance(); + + if (!preg_match('/LIMIT\s+/', $query)) + { + $query = preg_replace('/;?\s*$/', '', $query); + $query .= ' LIMIT 100'; + } + + $st = $db->prepare($query); + + if (!$st->readOnly()) + { + throw new UserException('Seules les requêtes en lecture sont autorisées.'); + } + + $res = $st->execute(); + $out = []; + + while ($row = $res->fetchArray(SQLITE3_ASSOC)) + { + $out[] = $row; + } + + return $out; + } + + public function schemaSQL() + { + $db = DB::getInstance(); + + $tables = [ + 'journal' => $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'compta_journal\';'), + ]; + + return $tables; + } +} + +?> \ No newline at end of file diff --git a/include/class.compta_stats.php b/include/class.compta_stats.php new file mode 100644 index 0000000..335fbe0 --- /dev/null +++ b/include/class.compta_stats.php @@ -0,0 +1,122 @@ +simpleStatementFetch('SELECT COUNT(*) AS nb, id_categorie + FROM compta_journal + WHERE id_categorie IN (SELECT id FROM compta_categories WHERE type = ?) + AND id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0) + GROUP BY id_categorie ORDER BY nb DESC;', SQLITE3_ASSOC, $type); + } + + public function repartitionRecettes() + { + return $this->_parRepartitionCategorie(Compta_Categories::RECETTES); + } + + public function repartitionDepenses() + { + return $this->_parRepartitionCategorie(Compta_Categories::DEPENSES); + } + + protected function _parType($type) + { + return $this->getStats('SELECT strftime(\'%Y%m\', date) AS date, + SUM(montant) FROM compta_journal + WHERE id_categorie IN (SELECT id FROM compta_categories WHERE type = '.$type.') + AND id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0) + GROUP BY strftime(\'%Y-%m\', date) ORDER BY date;'); + } + + public function recettes() + { + return $this->_parType(Compta_Categories::RECETTES); + } + + public function depenses() + { + return $this->_parType(Compta_Categories::DEPENSES); + } + + public function soldeCompte($compte, $augmente = 'debit', $diminue = 'credit') + { + $db = DB::getInstance(); + + if (strpos($compte, '%') !== false) + { + $compte = 'LIKE \''. $db->escapeString($compte) . '\''; + } + else + { + $compte = '= \''. $db->escapeString($compte) . '\''; + } + + $stats = $this->getStats('SELECT strftime(\'%Y%m\', date) AS date, + (COALESCE((SELECT SUM(montant) FROM compta_journal + WHERE compte_'.$augmente.' '.$compte.' AND id_exercice = cj.id_exercice + AND date >= strftime(\'%Y-%m-01\', cj.date) + AND date <= strftime(\'%Y-%m-31\', cj.date)), 0) + - COALESCE((SELECT SUM(montant) FROM compta_journal + WHERE compte_'.$diminue.' '.$compte.' AND id_exercice = cj.id_exercice + AND date >= strftime(\'%Y-%m-01\', cj.date) + AND date <= strftime(\'%Y-%m-31\', cj.date)), 0) + ) AS solde + FROM compta_journal AS cj + WHERE (compte_debit '.$compte.' OR compte_credit '.$compte.') + AND id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0) + GROUP BY strftime(\'%Y-%m\', date) ORDER BY date;'); + + $c = 0; + foreach ($stats as $k=>$v) + { + $c += $v; + $stats[$k] = $c; + } + + return $stats; + } + + public function getStats($query) + { + $db = DB::getInstance(); + + $data = $db->simpleStatementFetchAssoc($query); + + $e = $db->querySingle('SELECT *, strftime(\'%s\', debut) AS debut, + strftime(\'%s\', fin) AS fin FROM compta_exercices WHERE cloture = 0;', true); + + $y = date('Y', $e['debut']); + $m = date('m', $e['debut']); + $max = date('Ym', $e['fin']); + + while ($y . $m <= $max) + { + if (!isset($data[$y . $m])) + { + $data[$y . $m] = 0; + } + + if ($m == 12) + { + $m = '01'; + $y++; + } + else + { + $m++; + $m = str_pad((int)$m, 2, '0', STR_PAD_LEFT); + } + } + + ksort($data); + + return $data; + } +} + +?> \ No newline at end of file diff --git a/include/class.config.php b/include/class.config.php new file mode 100644 index 0000000..6ee1bd3 --- /dev/null +++ b/include/class.config.php @@ -0,0 +1,326 @@ +fields_types = [ + 'nom_asso' => $string, + 'adresse_asso' => $string, + 'email_asso' => $string, + 'site_asso' => $string, + + 'monnaie' => $string, + 'pays' => $string, + + 'champs_membres' => $object, + + 'email_envoi_automatique'=> $string, + + 'categorie_membres' => $int, + + 'categorie_dons' => $int, + 'categorie_cotisations' => $int, + + 'accueil_wiki' => $string, + 'accueil_connexion' => $string, + + 'frequence_sauvegardes' => $int, + 'nombre_sauvegardes' => $int, + + 'champ_identifiant' => $string, + 'champ_identite' => $string, + + 'version' => $string, + ]; + + $db = DB::getInstance(); + + $this->config = $db->simpleStatementFetchAssoc('SELECT cle, valeur FROM config ORDER BY cle;'); + + foreach ($this->config as $key=>&$value) + { + if (!array_key_exists($key, $this->fields_types)) + { + // Ancienne clé de config qui n'est plus utilisée + continue; + } + + if (is_array($this->fields_types[$key])) + { + $value = explode(',', $value); + } + elseif ($key == 'champs_membres') + { + $value = new Champs_Membres((string)$value); + } + else + { + settype($value, gettype($this->fields_types[$key])); + } + } + } + + public function __destruct() + { + if (!empty($this->modified)) + { + //echo '
Il y a des champs modifiés non sauvés dans '.__CLASS__.' !
'; + } + } + + public function save() + { + if (empty($this->modified)) + return true; + + $values = []; + + $db = DB::getInstance(); + $db->exec('BEGIN;'); + + foreach ($this->modified as $key=>$modified) + { + $value = $this->config[$key]; + + if (is_array($value)) + { + $value = implode(',', $value); + } + elseif (is_object($value)) + { + $value = (string) $value; + } + + $db->simpleExec('INSERT OR REPLACE INTO config (cle, valeur) VALUES (?, ?);', + $key, $value); + } + + if (!empty($this->modified['champ_identifiant'])) + { + // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique + $db->exec('UPDATE membres SET '.$this->get('champ_identifiant').' = NULL + WHERE '.$this->get('champ_identifiant').' = "";'); + + // Création de l'index unique + $db->exec('DROP INDEX IF EXISTS membres_identifiant;'); + $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$this->get('champ_identifiant').');'); + } + + $db->exec('END;'); + + $this->modified = []; + + return true; + } + + public function get($key) + { + if (!array_key_exists($key, $this->fields_types)) + { + throw new \OutOfBoundsException('Ce champ est inconnu.'); + } + + if (!array_key_exists($key, $this->config)) + { + return null; + } + + return $this->config[$key]; + } + + public function getVersion() + { + if (!array_key_exists('version', $this->config)) + { + return '0'; + } + + return $this->config['version']; + } + + public function setVersion($version) + { + $this->config['version'] = $version; + + $db = DB::getInstance(); + $db->simpleExec('INSERT OR REPLACE INTO config (cle, valeur) VALUES (?, ?);', + 'version', $version); + + return true; + } + + public function set($key, $value) + { + if (!array_key_exists($key, $this->fields_types)) + { + throw new \OutOfBoundsException('Ce champ est inconnu.'); + } + + if (is_array($this->fields_types[$key])) + { + $value = !empty($value) ? (array) $value : []; + } + elseif (is_int($this->fields_types[$key])) + { + $value = (int) $value; + } + elseif (is_float($this->fields_types[$key])) + { + $value = (float) $value; + } + elseif (is_bool($this->fields_types[$key])) + { + $value = (bool) $value; + } + elseif (is_string($this->fields_types[$key])) + { + $value = (string) $value; + } + + switch ($key) + { + case 'nom_asso': + { + if (!trim($value)) + { + throw new UserException('Le nom de l\'association ne peut rester vide.'); + } + break; + } + case 'accueil_wiki': + case 'accueil_connexion': + { + if (!trim($value)) + { + $key = str_replace('accueil_', '', $key); + throw new UserException('Le nom de la page d\'accueil ' . $key . ' ne peut rester vide.'); + } + break; + } + case 'email_asso': + case 'email_envoi_automatique': + { + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) + { + throw new UserException('Adresse e-mail invalide.'); + } + break; + } + case 'champs_membres': + { + if (!($value instanceOf Champs_Membres)) + { + throw new \UnexpectedValueException('$value doit être de type Champs_Membres'); + } + break; + } + case 'champ_identite': + case 'champ_identifiant': + { + $champs = $this->get('champs_membres'); + $db = DB::getInstance(); + + // Vérification que le champ existe bien + if (!$champs->get($value)) + { + throw new UserException('Le champ '.$value.' n\'existe pas pour la configuration de '.$key); + } + + // Vérification que le champ est unique pour l'identifiant + if ($key == 'champ_identifiant' + && !$db->simpleQuerySingle('SELECT (COUNT(DISTINCT '.$value.') = COUNT(*)) + FROM membres WHERE '.$value.' IS NOT NULL AND '.$value.' != \'\';')) + { + throw new UserException('Le champ '.$value.' comporte des doublons et ne peut donc pas servir comme identifiant pour la connexion.'); + } + break; + } + case 'categorie_cotisations': + case 'categorie_dons': + { + return false; + $db = DB::getInstance(); + if (!$db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE id = ?;', false, $value)) + { + throw new UserException('Champ '.$key.' : La catégorie comptable numéro \''.$value.'\' ne semble pas exister.'); + } + break; + } + case 'categorie_membres': + { + $db = DB::getInstance(); + if (!$db->simpleQuerySingle('SELECT 1 FROM membres_categories WHERE id = ?;', false, $value)) + { + throw new UserException('La catégorie de membres par défaut numéro \''.$value.'\' ne semble pas exister.'); + } + break; + } + case 'monnaie': + { + if (!trim($value)) + { + throw new UserException('La monnaie doit être renseignée.'); + } + + break; + } + case 'pays': + { + if (!trim($value) || !utils::getCountryName($value)) + { + throw new UserException('Le pays renseigné est invalide.'); + } + + break; + } + default: + break; + } + + if (!isset($this->config[$key]) || $value !== $this->config[$key]) + { + $this->config[$key] = $value; + $this->modified[$key] = true; + } + + return true; + } + + public function getFieldsTypes() + { + return $this->fields_types; + } + + public function getConfig() + { + return $this->config; + } +} + +?> \ No newline at end of file diff --git a/include/class.cotisations.php b/include/class.cotisations.php new file mode 100644 index 0000000..9490784 --- /dev/null +++ b/include/class.cotisations.php @@ -0,0 +1,170 @@ +simpleQuerySingle('SELECT 1 FROM compta_categories WHERE id = ?;', false, (int) $data['id_categorie_compta'])) + { + throw new UserException('Catégorie comptable inconnue'); + } + + $data['id_categorie_compta'] = (int) $data['id_categorie_compta']; + } + } + + /** + * Ajouter une cotisation + * @param array $data Tableau des champs à insérer + * @return integer ID de la cotisation créée + */ + public function add($data) + { + $db = DB::getInstance(); + + $this->_checkFields($data); + + $db->simpleInsert('cotisations', $data); + $id = $db->lastInsertRowId(); + + return $id; + } + + /** + * Modifier une cotisation + * @param integer $id ID de la cotisation à modifier + * @param array $data Tableau des champs à modifier + * @return bool true si succès + */ + public function edit($id, $data) + { + $db = DB::getInstance(); + + $this->_checkFields($data); + + return $db->simpleUpdate('cotisations', $data, 'id = \''.(int) $id.'\''); + } + + /** + * Supprimer une cotisation + * @param integer $id ID de la cotisation à supprimer + * @return integer true en cas de succès + */ + public function delete($id) + { + $db = DB::getInstance(); + + $db->exec('BEGIN;'); + $db->simpleExec('DELETE FROM cotisations_membres WHERE id_cotisation = ?;', (int) $id); + $db->simpleExec('DELETE FROM cotisations WHERE id = ?;', (int) $id); + $db->exec('END;'); + + return true; + } + + /** + * Renvoie les infos sur une cotisation + * @param integer $id Numéro de la cotisation + * @return array Infos de la cotisation + */ + public function get($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT co.*, + (SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres WHERE id_cotisation = co.id) AS nb_membres, + (SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres AS cm WHERE id_cotisation = co.id + AND ((co.duree IS NOT NULL AND date(cm.date, \'+\'||co.duree||\' days\') >= date()) + OR (co.fin IS NOT NULL AND co.debut <= cm.date AND co.fin >= cm.date))) AS nb_a_jour + FROM cotisations AS co WHERE id = :id;', true, ['id' => (int) $id]); + } + + public function listByName() + { + $db = DB::getInstance(); + return $db->simpleStatementFetch('SELECT * FROM cotisations ORDER BY intitule;'); + } + + public function listCurrent() + { + $db = DB::getInstance(); + return $db->simpleStatementFetch('SELECT * FROM cotisations WHERE fin >= date(\'now\') OR fin IS NULL + ORDER BY transliterate_to_ascii(intitule) COLLATE NOCASE;'); + } + + public function listCurrentWithStats() + { + $db = DB::getInstance(); + return $db->simpleStatementFetch('SELECT co.*, + (SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres WHERE id_cotisation = co.id) AS nb_membres, + (SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres AS cm WHERE id_cotisation = co.id + AND ((co.duree IS NOT NULL AND date(cm.date, \'+\'||co.duree||\' days\') >= date()) + OR (co.fin IS NOT NULL AND co.debut <= cm.date AND co.fin >= cm.date))) AS nb_a_jour + FROM cotisations AS co WHERE fin >= date(\'now\') OR fin IS NULL + ORDER BY transliterate_to_ascii(intitule) COLLATE NOCASE;'); + } +} + +?> \ No newline at end of file diff --git a/include/class.cotisations_membres.php b/include/class.cotisations_membres.php new file mode 100644 index 0000000..a895ee2 --- /dev/null +++ b/include/class.cotisations_membres.php @@ -0,0 +1,336 @@ +simpleQuerySingle('SELECT 1 FROM cotisations WHERE id = ?;', false, (int) $data['id_cotisation'])) + { + throw new UserException('Cotisation inconnue.'); + } + + $data['id_cotisation'] = (int) $data['id_cotisation']; + + if (empty($data['id_membre']) + || !$db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ?;', false, (int) $data['id_membre'])) + { + throw new UserException('Membre inconnu ou invalide.'); + } + + $data['id_membre'] = (int) $data['id_membre']; + + if ($compta) + { + if (!isset($data['moyen_paiement']) || trim($data['moyen_paiement']) === '') + { + throw new UserException('Moyen de paiement inconnu ou invalide.'); + } + + if ($data['moyen_paiement'] != 'ES') + { + if (trim($data['banque']) == '') + { + throw new UserException('Le compte bancaire choisi est invalide.'); + } + + if (!$db->simpleQuerySingle('SELECT 1 FROM compta_comptes_bancaires WHERE id = ?;', + false, $data['banque'])) + { + throw new UserException('Le compte bancaire choisi n\'existe pas.'); + } + } + + if (empty($data['montant']) || !is_numeric($data['montant'])) + { + throw new UserException('Le montant indiqué n\'est pas un nombre valide.'); + } + } + } + + /** + * Enregistrer un événement de cotisation + * @param array $data Tableau des champs à insérer + * @return integer ID de l'événement créé + */ + public function add($data) + { + $db = DB::getInstance(); + + $co = $db->simpleQuerySingle('SELECT * FROM cotisations WHERE id = ?;', + true, (int)$data['id_cotisation']); + + $this->_checkFields($data, !empty($co['id_categorie_compta'])); + + $check = $db->simpleQuerySingle('SELECT 1 FROM cotisations_membres + WHERE id_cotisation = ? AND id_membre = ? AND date = ?;', + false, (int)$data['id_cotisation'], (int)$data['id_membre'], $data['date']); + + if ($check) + { + throw new UserException('Cette cotisation a déjà été enregistrée pour ce jour-ci et ce membre-ci.'); + } + + $db->begin(); + + $db->simpleInsert('cotisations_membres', [ + 'date' => $data['date'], + 'id_cotisation' => $data['id_cotisation'], + 'id_membre' => $data['id_membre'], + ]); + + $id = $db->lastInsertRowId(); + + if ($co['id_categorie_compta'] && $co['montant'] > 0) + { + try { + $id_operation = $this->addOperationCompta($id, [ + 'id_categorie' => $co['id_categorie_compta'], + 'libelle' => 'Cotisation (automatique)', + 'montant' => $data['montant'], + 'date' => $data['date'], + 'moyen_paiement'=> $data['moyen_paiement'], + 'numero_cheque' => isset($data['numero_cheque']) ? $data['numero_cheque'] : null, + 'id_auteur' => $data['id_auteur'], + 'banque' => isset($data['banque']) ? $data['banque'] : null, + 'id_membre' => $data['id_membre'], + ]); + } + catch (\Exception $e) + { + $db->rollback(); + throw $e; + } + } + + $db->commit(); + + return $id; + } + + /** + * Supprimer un événement de cotisation + * @param integer $id ID de l'événement à supprimer + * @return integer true en cas de succès + */ + public function delete($id) + { + $db = DB::getInstance(); + $db->simpleExec('DELETE FROM membres_operations WHERE id_cotisation = ?;', (int)$id); + return $db->simpleExec('DELETE FROM cotisations_membres WHERE id = ?;', (int) $id); + } + + public function get($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT * FROM cotisations_membres WHERE id = ?;', true, (int)$id); + } + + /** + * Renvoie une liste des écritures comptables liées à une cotisation + * @param int $id Numéro de la cotisation membre + * @return array Liste des écritures + */ + public function listOperationsCompta($id) + { + $db = DB::getInstance(); + return $db->simpleStatementFetch('SELECT * FROM compta_journal + WHERE id IN (SELECT id_operation FROM membres_operations + WHERE id_cotisation = ?);', \SQLITE3_ASSOC, (int)$id); + } + + /** + * Ajouter une écriture comptable pour un paiemement membre + * @param int $id Numéro de la cotisation membre + * @param array $data Données + */ + public function addOperationCompta($id, $data) + { + $journal = new Compta_Journal; + $db = DB::getInstance(); + + if (!isset($data['libelle']) || trim($data['libelle']) == '') + { + throw new UserException('Le libellé ne peut rester vide.'); + } + + $data['libelle'] = trim($data['libelle']); + + if (!isset($data['montant']) || !is_numeric($data['montant']) || (float)$data['montant'] < 0) + { + throw new UserException('Le montant doit être un nombre positif et valide.'); + } + + $data['montant'] = (float) $data['montant']; + + if ($data['moyen_paiement'] != 'ES') + { + $debit = $data['banque']; + } + else + { + $debit = Compta_Comptes::CAISSE; + } + + $credit = $db->simpleQuerySingle('SELECT compte FROM compta_categories WHERE id = ?;', + false, $data['id_categorie']); + + $id_operation = $journal->add([ + 'libelle' => $data['libelle'], + 'montant' => $data['montant'], + 'date' => $data['date'], + 'moyen_paiement'=> $data['moyen_paiement'], + 'numero_cheque' => isset($data['numero_cheque']) ? $data['numero_cheque'] : null, + 'compte_debit' => $debit, + 'compte_credit' => $credit, + 'id_categorie' => (int)$data['id_categorie'], + 'id_auteur' => (int)$data['id_auteur'], + ]); + + $db->simpleInsert('membres_operations', [ + 'id_operation' => $id_operation, + 'id_membre' => $data['id_membre'], + 'id_cotisation' => (int)$id, + ]); + + return $id_operation; + } + + /** + * Nombre de membres pour une cotisation + * @param integer $id Numéro de la cotisation + * @return integer Nombre d'événements pour cette cotisation + */ + public function countMembersForCotisation($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres + WHERE id_cotisation = ?;', + false, (int)$id); + } + + /** + * Liste des membres qui sont inscrits à une cotisation + * @param integer $id Numéro de la cotisation + * @return array Liste des membres + */ + public function listMembersForCotisation($id, $page = 1, $order = null, $desc = true) + { + $begin = ($page - 1) * self::ITEMS_PER_PAGE; + + $db = DB::getInstance(); + $champ_id = Config::getInstance()->get('champ_identite'); + + if (empty($order)) + $order = 'date'; + + switch ($order) + { + case 'date': + case 'a_jour': + break; + case 'identite': + $order = 'transliterate_to_ascii('.$champ_id.') COLLATE NOCASE'; + break; + default: + $order = 'cm.id_membre'; + break; + } + + $desc = $desc ? 'DESC' : 'ASC'; + + return $db->simpleStatementFetch('SELECT cm.id_membre, cm.date, cm.id, + (SELECT '.$champ_id.' FROM membres WHERE id = cm.id_membre) AS nom, c.montant, + CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date() + WHEN c.fin IS NOT NULL THEN c.fin >= date() ELSE 1 END AS a_jour + FROM cotisations_membres AS cm + INNER JOIN cotisations AS c ON c.id = cm.id_cotisation + WHERE + cm.id_cotisation = ? + GROUP BY cm.id_membre ORDER BY '.$order.' '.$desc.' LIMIT ?,?;', + \SQLITE3_ASSOC, (int)$id, $begin, self::ITEMS_PER_PAGE); + } + + /** + * Liste des événements d'un membre + * @param integer $id Numéro de membre + * @return array Liste des événements de cotisation fait par ce membre + */ + public function listForMember($id) + { + $db = DB::getInstance(); + return $db->simpleStatementFetch('SELECT cm.*, c.intitule, c.duree, c.debut, c.fin, c.montant, + (SELECT COUNT(*) FROM membres_operations WHERE id_cotisation = cm.id) AS nb_operations + FROM cotisations_membres AS cm + LEFT JOIN cotisations AS c ON c.id = cm.id_cotisation + WHERE cm.id_membre = ? ORDER BY cm.date DESC;', \SQLITE3_ASSOC, (int)$id); + } + + /** + * Liste des cotisations / activités en cours pour ce membre + * @param integer $id Numéro de membre + * @return array Liste des cotisations en cours de validité + */ + public function listSubscriptionsForMember($id) + { + $db = DB::getInstance(); + return $db->simpleStatementFetch('SELECT c.*, + CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date() + WHEN c.fin IS NOT NULL THEN c.fin >= date() + WHEN cm.id IS NOT NULL THEN 1 ELSE 0 END AS a_jour, + CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') + WHEN c.fin IS NOT NULL THEN c.fin ELSE 1 END AS expiration, + (julianday(date()) - julianday(CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') + WHEN c.fin IS NOT NULL THEN c.fin END)) AS nb_jours + FROM cotisations_membres AS cm + INNER JOIN cotisations AS c ON c.id = cm.id_cotisation + WHERE cm.id_membre = ? + GROUP BY cm.id_cotisation + ORDER BY cm.date DESC;', \SQLITE3_ASSOC, (int)$id); + } + + /** + * Ce membre est-il à jour sur cette cotisation ? + * @param integer $id Numéro de membre + * @param integer $id_cotisation Numéro de cotisation + * @return array Infos sur la cotisation, et champ expiration + * (si NULL = cotisation jamais enregistrée, si 1 = cotisation ponctuelle enregistrée, sinon date d'expiration) + */ + public function isMemberUpToDate($id, $id_cotisation) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT c.*, + CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date() + WHEN c.fin IS NOT NULL THEN c.fin >= date() + WHEN cm.id IS NOT NULL THEN 1 ELSE 0 END AS a_jour, + CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') + WHEN c.fin IS NOT NULL THEN c.fin ELSE 1 END AS expiration + FROM cotisations AS c + LEFT JOIN cotisations_membres AS cm ON cm.id_cotisation = c.id AND cm.id_membre = ? + WHERE c.id = ? ORDER BY cm.date DESC;', + true, (int)$id, (int)$id_cotisation); + } + + public function countForMember($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT COUNT(DISTINCT id_cotisation) FROM cotisations_membres + WHERE id_membre = ?;', false, (int)$id); + } +} \ No newline at end of file diff --git a/include/class.db.php b/include/class.db.php new file mode 100644 index 0000000..c560c59 --- /dev/null +++ b/include/class.db.php @@ -0,0 +1,411 @@ +enableExceptions(true); + + // Activer les contraintes des foreign keys + $this->exec('PRAGMA foreign_keys = ON;'); + + $this->createFunction('transliterate_to_ascii', ['Garradin\utils', 'transliterateToAscii']); + $this->createFunction('base64', 'base64_encode'); + $this->createFunction('rank', [$this, 'sql_rank']); + $this->createFunction('running_sum', [$this, 'sql_running_sum']); + } + + public function sql_running_sum($data) + { + // Why is this function called two times for the first row?! + // Dunno but here is a workaround + if (is_null($this->_running_sum)) + { + $this->_running_sum = 0.0; + return $this->_running_sum; + } + + $this->_running_sum += $data; + return $this->_running_sum; + } + + public function resetRunningSum() + { + $this->_running_sum = null; + } + + public function sql_rank($aMatchInfo) + { + $iSize = 4; // byte size + $iPhrase = (int) 0; // Current phrase // + $score = (double)0.0; // Value to return // + + /* Check that the number of arguments passed to this function is correct. + ** If not, jump to wrong_number_args. Set aMatchinfo to point to the array + ** of unsigned integer values returned by FTS function matchinfo. Set + ** nPhrase to contain the number of reportable phrases in the users full-text + ** query, and nCol to the number of columns in the table. + */ + $aMatchInfo = (string) func_get_arg(0); + $nPhrase = ord(substr($aMatchInfo, 0, $iSize)); + $nCol = ord(substr($aMatchInfo, $iSize, $iSize)); + + if (func_num_args() > (1 + $nCol)) + { + throw new \Exception("Invalid number of arguments : ".$nCol); + } + + // Iterate through each phrase in the users query. // + for ($iPhrase = 0; $iPhrase < $nPhrase; $iPhrase++) + { + $iCol = (int) 0; // Current column // + + /* Now iterate through each column in the users query. For each column, + ** increment the relevancy score by: + ** + ** ( / ) * + ** + ** aPhraseinfo[] points to the start of the data for phrase iPhrase. So + ** the hit count and global hit counts for each column are found in + ** aPhraseinfo[iCol*3] and aPhraseinfo[iCol*3+1], respectively. + */ + $aPhraseinfo = substr($aMatchInfo, (2 + $iPhrase * $nCol * 3) * $iSize); + + for ($iCol = 0; $iCol < $nCol; $iCol++) + { + $nHitCount = ord(substr($aPhraseinfo, 3 * $iCol * $iSize, $iSize)); + $nGlobalHitCount = ord(substr($aPhraseinfo, (3 * $iCol + 1) * $iSize, $iSize)); + $weight = ($iCol < func_num_args() - 1) ? (double) func_get_arg($iCol + 1) : 0; + + if ($nHitCount > 0) + { + $score += ((double)$nHitCount / (double)$nGlobalHitCount) * $weight; + } + } + } + + return $score; + } + + public function escape($str) + { + return $this->escapeString($str); + } + + public function e($str) + { + return $this->escapeString($str); + } + + public function begin() + { + if (!$this->_transaction) + { + $this->exec('BEGIN;'); + } + + $this->_transaction++; + + return $this->_transaction == 1 ? true : false; + } + + public function commit() + { + if ($this->_transaction == 1) + { + $this->exec('END;'); + } + + if ($this->_transaction > 0) + { + $this->_transaction--; + } + + return $this->_transaction ? false : true; + } + + public function rollback() + { + $this->exec('ROLLBACK;'); + $this->_transaction = 0; + return true; + } + + protected function _getArgType($arg, $name = '') + { + if (is_float($arg)) + return SQLITE3_FLOAT; + elseif (is_int($arg)) + return SQLITE3_INTEGER; + elseif (is_bool($arg)) + return SQLITE3_INTEGER; + elseif (is_null($arg)) + return SQLITE3_NULL; + elseif (is_string($arg)) + return SQLITE3_TEXT; + else + throw new \InvalidArgumentException('Argument '.$name.' is of invalid type '.gettype($arg)); + } + + public function simpleStatement($query, $args = []) + { + $statement = $this->prepare($query); + $nb = $statement->paramCount(); + + if (!empty($args)) + { + if (is_array($args) && count($args) == 1 && is_array(current($args))) + { + $args = current($args); + } + + if (count($args) != $nb) + { + throw new \LengthException('Arguments error: '.count($args).' supplied, but '.$nb.' are required by query.'); + } + + reset($args); + + if (is_int(key($args))) + { + foreach ($args as $i=>$arg) + { + $statement->bindValue((int)$i+1, $arg, $this->_getArgType($arg, $i+1)); + } + } + else + { + foreach ($args as $key=>$value) + { + if (is_int($key)) + { + throw new \InvalidArgumentException(__FUNCTION__ . ' requires argument to be a named-associative array, but key '.$key.' is an integer.'); + } + + $statement->bindValue(':'.$key, $value, $this->_getArgType($value, $key)); + } + } + } + + try { + return $statement->execute(); + } + catch (\Exception $e) + { + throw new \Exception($e->getMessage() . "\n" . $query . "\n" . json_encode($args, true)); + } + } + + public function simpleStatementFetch($query, $mode = SQLITE3_BOTH) + { + if ($mode != SQLITE3_BOTH && $mode != SQLITE3_ASSOC && $mode != SQLITE3_NUM) + { + throw new \InvalidArgumentException('Mode argument should be either SQLITE3_BOTH, SQLITE3_ASSOC or SQLITE3_NUM.'); + } + + $args = array_slice(func_get_args(), 2); + return $this->fetchResult($this->simpleStatement($query, $args), $mode); + } + + public function simpleStatementFetchAssoc($query) + { + $args = array_slice(func_get_args(), 1); + return $this->fetchResultAssoc($this->simpleStatement($query, $args)); + } + + public function simpleStatementFetchAssocKey($query, $mode = SQLITE3_BOTH) + { + if ($mode != SQLITE3_BOTH && $mode != SQLITE3_ASSOC && $mode != SQLITE3_NUM) + { + throw new \InvalidArgumentException('Mode argument should be either SQLITE3_BOTH, SQLITE3_ASSOC or SQLITE3_NUM.'); + } + + $args = array_slice(func_get_args(), 2); + return $this->fetchResultAssocKey($this->simpleStatement($query, $args), $mode); + } + + public function escapeAuto($value, $name = '') + { + $type = $this->_getArgType($value, $name); + + switch ($type) + { + case SQLITE3_FLOAT: + return floatval($value); + case SQLITE3_INTEGER: + return intval($value); + case SQLITE3_NULL: + return 'NULL'; + case SQLITE3_TEXT: + return '\'' . $this->escapeString($value) . '\''; + } + } + + /** + * Simple INSERT query + */ + public function simpleInsert($table, $fields) + { + $fields_names = array_keys($fields); + return $this->simpleStatement('INSERT INTO '.$table.' ('.implode(', ', $fields_names).') + VALUES (:'.implode(', :', $fields_names).');', $fields); + } + + public function simpleUpdate($table, $fields, $where) + { + if (empty($fields)) + return false; + + $query = 'UPDATE '.$table.' SET '; + + foreach ($fields as $key=>$value) + { + $query .= $key . ' = :'.$key.', '; + } + + $query = substr($query, 0, -2); + $query .= ' WHERE '.$where.';'; + return $this->simpleStatement($query, $fields); + } + + /** + * Formats and escapes a statement and then returns the result of exec() + */ + public function simpleExec($query) + { + return $this->simpleStatement($query, array_slice(func_get_args(), 1)); + } + + public function simpleQuerySingle($query, $all_columns = false) + { + $res = $this->simpleStatement($query, array_slice(func_get_args(), 2)); + + $row = $res->fetchArray($all_columns ? SQLITE3_ASSOC : SQLITE3_NUM); + + if (!$all_columns) + { + if (isset($row[0])) + return $row[0]; + return false; + } + else + { + return $row; + } + } + + public function queryFetch($query, $mode = SQLITE3_BOTH) + { + return $this->fetchResult($this->query($query), $mode); + } + + public function queryFetchAssoc($query) + { + return $this->fetchResultAssoc($this->query($query)); + } + + public function queryFetchAssocKey($query, $mode = SQLITE3_BOTH) + { + return $this->fetchResultAssocKey($this->query($query), $mode); + } + + public function fetchResult($result, $mode = \SQLITE3_BOTH) + { + $out = []; + + while ($row = $result->fetchArray($mode)) + { + $out[] = $row; + } + + $result->finalize(); + unset($result, $row); + + return $out; + } + + protected function fetchResultAssoc($result) + { + $out = []; + + while ($row = $result->fetchArray(SQLITE3_NUM)) + { + $out[$row[0]] = $row[1]; + } + + $result->finalize(); + unset($result, $row); + + return $out; + } + + protected function fetchResultAssocKey($result, $mode = \SQLITE3_BOTH) + { + $out = []; + + while ($row = $result->fetchArray($mode)) + { + $key = current($row); + $out[$key] = $row; + } + + $result->finalize(); + unset($result, $row); + + return $out; + } + + public function countRows($result) + { + $i = 0; + + while ($result->fetchArray(SQLITE3_NUM)) + $i++; + + return $i; + } +} + +?> \ No newline at end of file diff --git a/include/class.membres.php b/include/class.membres.php new file mode 100644 index 0000000..0136d6d --- /dev/null +++ b/include/class.membres.php @@ -0,0 +1,792 @@ +_getSalt(22); + return crypt($password, $salt); + } + + protected function _checkPassword($password, $stored_hash) + { + return crypt($password, $stored_hash) == $stored_hash; + } + + protected function _sessionStart($force = false) + { + if (!isset($_SESSION) && ($force || isset($_COOKIE[session_name()]))) + { + session_start(); + } + + return true; + } + + public function keepSessionAlive() + { + $this->_sessionStart(true); + } + + public function login($id, $passe) + { + $db = DB::getInstance(); + $champ_id = Config::getInstance()->get('champ_identifiant'); + + $r = $db->simpleQuerySingle('SELECT id, passe, id_categorie FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id)); + + if (empty($r)) + return false; + + if (!$this->_checkPassword(trim($passe), $r['passe'])) + return false; + + $droits = $this->getDroits($r['id_categorie']); + + if ($droits['connexion'] == self::DROIT_AUCUN) + return false; + + $this->_sessionStart(true); + $db->simpleExec('UPDATE membres SET date_connexion = datetime(\'now\') WHERE id = ?;', $r['id']); + + return $this->updateSessionData($r['id'], $droits); + } + + public function recoverPasswordCheck($id) + { + $db = DB::getInstance(); + $config = Config::getInstance(); + + $champ_id = $config->get('champ_identifiant'); + + $membre = $db->simpleQuerySingle('SELECT id, email FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id)); + + if (!$membre || trim($membre['email']) == '') + { + return false; + } + + $this->_sessionStart(true); + $hash = sha1($membre['email'] . $membre['id'] . 'recover' . ROOT . time()); + $_SESSION['recover_password'] = [ + 'id' => (int) $membre['id'], + 'email' => $membre['email'], + 'hash' => $hash + ]; + + $message = "Bonjour,\n\nVous avez oublié votre mot de passe ? Pas de panique !\n\n"; + $message.= "Il vous suffit de cliquer sur le lien ci-dessous pour recevoir un nouveau mot de passe.\n\n"; + $message.= WWW_URL . 'admin/password.php?c=' . substr($hash, -10); + $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé."; + + return utils::mail($membre['email'], '['.$config->get('nom_asso').'] Mot de passe perdu ?', $message); + } + + public function recoverPasswordConfirm($hash) + { + $this->_sessionStart(); + + if (empty($_SESSION['recover_password']['hash'])) + return false; + + if (substr($_SESSION['recover_password']['hash'], -10) != $hash) + return false; + + $config = Config::getInstance(); + $db = DB::getInstance(); + + $password = utils::suggestPassword(); + + $dest = $_SESSION['recover_password']['email']; + $id = (int)$_SESSION['recover_password']['id']; + + $message = "Bonjour,\n\nVous avez demandé un nouveau mot de passe pour votre compte.\n\n"; + $message.= "Votre adresse email : ".$dest."\n"; + $message.= "Votre nouveau mot de passe : ".$password."\n\n"; + $message.= "Si vous n'avez pas demandé à recevoir ce message, merci de nous le signaler."; + + $password = $this->_hashPassword($password); + + $db->simpleUpdate('membres', ['passe' => $password], 'id = '.(int)$id); + + return utils::mail($dest, '['.$config->get('nom_asso').'] Nouveau mot de passe', $message); + } + + public function updateSessionData($membre = null, $droits = null) + { + if (is_null($membre)) + { + $membre = $this->get($_SESSION['logged_user']['id']); + } + elseif (is_int($membre)) + { + $membre = $this->get($membre); + } + + if (is_null($droits)) + { + $droits = $this->getDroits($membre['id_categorie']); + } + + $membre['droits'] = $droits; + $_SESSION['logged_user'] = $membre; + return true; + } + + public function localLogin() + { + if (!defined('Garradin\LOCAL_LOGIN')) + return false; + + if (trim(LOCAL_LOGIN) == '') + return false; + + $db = DB::getInstance(); + $config = Config::getInstance(); + $champ_id = $config->get('champ_identifiant'); + + if (is_int(LOCAL_LOGIN) && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ? LIMIT 1;', true, LOCAL_LOGIN)) + { + $this->_sessionStart(true); + return $this->updateSessionData(LOCAL_LOGIN); + } + elseif ($id = $db->simpleQuerySingle('SELECT id FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, LOCAL_LOGIN)) + { + $this->_sessionStart(true); + return $this->updateSessionData($membre); + } + + throw new UserException('Le membre ' . LOCAL_LOGIN . ' n\'existe pas, merci de modifier la directive Garradin\LOCAL_LOGIN.'); + } + + public function isLogged() + { + $this->_sessionStart(); + + if (empty($_SESSION['logged_user'])) + { + if (defined('Garradin\LOCAL_LOGIN')) + { + return $this->localLogin(); + } + + return false; + } + + return true; + } + + public function getLoggedUser() + { + if (!$this->isLogged()) + return false; + + return $_SESSION['logged_user']; + } + + public function logout() + { + $_SESSION = []; + setcookie(session_name(), '', 0, '/'); + return true; + } + + public function sessionStore($key, $value) + { + if (!isset($_SESSION['storage'])) + { + $_SESSION['storage'] = []; + } + + if ($value === null) + { + unset($_SESSION['storage'][$key]); + } + else + { + $_SESSION['storage'][$key] = $value; + } + + return true; + } + + public function sessionGet($key) + { + if (!isset($_SESSION['storage'][$key])) + { + return null; + } + + return $_SESSION['storage'][$key]; + } + + public function sendMessage($dest, $sujet, $message, $copie = false) + { + if (!$this->isLogged()) + { + throw new \LogicException('Cette fonction ne peut être appelée que par un utilisateur connecté.'); + } + + $from = $this->getLoggedUser(); + $from = $from['email']; + // Uniquement adresse email pour le moment car faudrait trouver comment + // indiquer le nom mais qu'il soit correctement échappé FIXME + + $config = Config::getInstance(); + + $message .= "\n\n--\nCe message a été envoyé par un membre de ".$config->get('nom_asso'); + $message .= ", merci de contacter ".$config->get('email_asso')." en cas d'abus."; + + if ($copie) + { + utils::mail($from, $sujet, $message); + } + + return utils::mail($dest, $sujet, $message, ['From' => $from]); + } + + // Gestion des données /////////////////////////////////////////////////////// + + public function _checkFields(&$data, $check_editable = true, $check_password = true) + { + $champs = Config::getInstance()->get('champs_membres'); + + foreach ($champs->getAll() as $key=>$config) + { + if (!$check_editable && (!empty($config['private']) || empty($config['editable']))) + { + unset($data[$key]); + continue; + } + + if (!isset($data[$key]) || (!is_array($data[$key]) && trim($data[$key]) === '') + || (is_array($data[$key]) && empty($data[$key]))) + { + if (!empty($config['mandatory']) && ($check_password || $key != 'passe')) + { + throw new UserException('Le champ "' . $config['title'] . '" doit obligatoirement être renseigné.'); + } + elseif (!empty($config['mandatory'])) + { + continue; + } + } + + if (isset($data[$key])) + { + if ($config['type'] == 'email' && trim($data[$key]) !== '' && !filter_var($data[$key], FILTER_VALIDATE_EMAIL)) + { + throw new UserException('Adresse e-mail invalide dans le champ "' . $config['title'] . '".'); + } + elseif ($config['type'] == 'url' && trim($data[$key]) !== '' && !filter_var($data[$key], FILTER_VALIDATE_URL)) + { + throw new UserException('Adresse URL invalide dans le champ "' . $config['title'] . '".'); + } + elseif ($config['type'] == 'date' && trim($data[$key]) !== '' && !utils::checkDate($data[$key])) + { + throw new UserException('Date invalide "' . $config['title'] . '", format attendu : AAAA-MM-JJ.'); + } + elseif ($config['type'] == 'datetime' && trim($data[$key]) !== '') + { + if (!utils::checkDateTime($data[$key]) || !($dt = new DateTime($data[$key]))) + { + throw new UserException('Date invalide "' . $config['title'] . '", format attendu : AAAA-MM-JJ HH:mm.'); + } + + $data[$key] = $dt->format('Y-m-d H:i'); + } + elseif ($config['type'] == 'tel') + { + $data[$key] = utils::normalizePhoneNumber($data[$key]); + } + elseif ($config['type'] == 'country') + { + $data[$key] = strtoupper(substr($data[$key], 0, 2)); + } + elseif ($config['type'] == 'checkbox') + { + $data[$key] = empty($data[$key]) ? 0 : 1; + } + elseif ($config['type'] == 'number' && trim($data[$key]) !== '') + { + if (empty($data[$key])) + { + $data[$key] = 0; + } + + if (!is_numeric($data[$key])) + throw new UserException('Le champ "' . $config['title'] . '" doit contenir un chiffre.'); + } + elseif ($config['type'] == 'select' && !in_array($data[$key], $config['options'])) + { + throw new UserException('Le champ "' . $config['title'] . '" ne correspond pas à un des choix proposés.'); + } + elseif ($config['type'] == 'multiple') + { + if (empty($data[$key]) || !is_array($data[$key])) + { + $data[$key] = 0; + continue; + } + + $binary = 0; + + foreach ($data[$key] as $k => $v) + { + if (array_key_exists($k, $config['options']) && !empty($v)) + { + $binary |= 0x01 << $k; + } + } + + $data[$key] = $binary; + } + + // Un champ texte vide c'est un champ NULL + if (is_string($data[$key]) && trim($data[$key]) === '') + { + $data[$key] = null; + } + } + } + + if (isset($data['code_postal']) && trim($data['code_postal']) != '') + { + if (!empty($data['pays']) && $data['pays'] == 'FR' && !preg_match('!^\d{5}$!', $data['code_postal'])) + { + throw new UserException('Code postal invalide.'); + } + } + + if (!empty($data['passe']) && strlen($data['passe']) < 5) + { + throw new UserException('Le mot de passe doit faire au moins 5 caractères.'); + } + + return true; + } + + public function add($data = []) + { + $this->_checkFields($data); + $db = DB::getInstance(); + $config = Config::getInstance(); + $id = $config->get('champ_identifiant'); + + if (!empty($data[$id]) + && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE '.$id.' = ? LIMIT 1;', false, $data[$id])) + { + throw new UserException('La valeur du champ '.$id.' est déjà utilisée par un autre membre, hors ce champ doit être unique à chaque membre.'); + } + + if (isset($data['passe']) && trim($data['passe']) != '') + { + $data['passe'] = $this->_hashPassword($data['passe']); + } + else + { + unset($data['passe']); + } + + if (empty($data['id_categorie'])) + { + $data['id_categorie'] = Config::getInstance()->get('categorie_membres'); + } + + $db->simpleInsert('membres', $data); + return $db->lastInsertRowId(); + } + + public function edit($id, $data = [], $check_editable = true) + { + $db = DB::getInstance(); + $config = Config::getInstance(); + + if (isset($data['id']) && ($data['id'] == $id || empty($data['id']))) + { + unset($data['id']); + } + + $this->_checkFields($data, $check_editable, false); + $champ_id = $config->get('champ_identifiant'); + + if (!empty($data[$champ_id]) + && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE '.$champ_id.' = ? AND id != ? LIMIT 1;', false, $data[$champ_id], (int)$id)) + { + throw new UserException('La valeur du champ '.$champ_id.' est déjà utilisée par un autre membre, hors ce champ doit être unique à chaque membre.'); + } + + if (!empty($data['id'])) + { + if ($db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ?;', false, (int)$data['id'])) + { + throw new UserException('Ce numéro est déjà attribué à un autre membre.'); + } + + // Si on ne vérifie pas toutes les tables qui sont liées ici à un ID de membre + // la requête de modification provoquera une erreur de contrainte de foreign key + // ce qui est normal. Donc : il n'est pas possible de changer l'ID d'un membre qui + // a participé au wiki, à la compta, etc. + if ($db->simpleQuerySingle('SELECT 1 FROM wiki_revisions WHERE id_auteur = ?;', false, (int)$id) + || $db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_auteur = ?;', false, (int)$id)) + # FIXME || $db->simpleQuerySingle('SELECT 1 FROM wiki_suivi WHERE id_membre = ?;', false, (int)$id)) + { + throw new UserException('Le numéro n\'est pas modifiable pour ce membre car des contenus sont liés à ce numéro de membre (wiki, compta, etc.).'); + } + } + + if (!empty($data['passe']) && trim($data['passe'])) + { + $data['passe'] = $this->_hashPassword($data['passe']); + } + else + { + unset($data['passe']); + } + + if (isset($data['id_categorie']) && empty($data['id_categorie'])) + { + $data['id_categorie'] = Config::getInstance()->get('categorie_membres'); + } + + if (empty($data)) + { + return true; + } + + return $db->simpleUpdate('membres', $data, 'id = '.(int)$id); + } + + public function get($id) + { + $db = DB::getInstance(); + $config = Config::getInstance(); + + return $db->simpleQuerySingle('SELECT *, + '.$config->get('champ_identite').' AS identite, + strftime(\'%s\', date_inscription) AS date_inscription, + strftime(\'%s\', date_connexion) AS date_connexion + FROM membres WHERE id = ? LIMIT 1;', true, (int)$id); + } + + public function delete($ids) + { + if (!is_array($ids)) + { + $ids = [(int)$ids]; + } + + if ($this->isLogged()) + { + $user = $this->getLoggedUser(); + + foreach ($ids as $id) + { + if ($user['id'] == $id) + { + throw new UserException('Il n\'est pas possible de supprimer son propre compte.'); + } + } + } + + return self::_deleteMembres($ids); + } + + public function getNom($id) + { + $db = DB::getInstance(); + $config = Config::getInstance(); + + return $db->simpleQuerySingle('SELECT '.$config->get('champ_identite').' FROM membres WHERE id = ? LIMIT 1;', false, (int)$id); + } + + public function getDroits($id) + { + $db = DB::getInstance(); + $droits = $db->simpleQuerySingle('SELECT * FROM membres_categories WHERE id = ?;', true, (int)$id); + + foreach ($droits as $key=>$value) + { + unset($droits[$key]); + $key = str_replace('droit_', '', $key, $found); + + if ($found) + { + $droits[$key] = (int) $value; + } + } + + return $droits; + } + + public function search($field, $query) + { + $db = DB::getInstance(); + $config = Config::getInstance(); + + $champs = $config->get('champs_membres'); + + if ($field != 'id' && !$champs->get($field)) + { + throw new \UnexpectedValueException($field . ' is not a valid field'); + } + + $champ = $champs->get($field); + + if ($champ['type'] == 'multiple') + { + $where = 'WHERE '.$field.' & (1 << '.(int)$query.')'; + $order = false; + } + elseif ($champ['type'] == 'tel') + { + $query = utils::normalizePhoneNumber($query); + $query = preg_replace('!^0+!', '', $query); + + if ($query == '') + { + return false; + } + + $where = 'WHERE '.$field.' LIKE \'%'.$db->escapeString($query).'\''; + $order = $field; + } + elseif (!$champs->isText($field)) + { + $where = 'WHERE '.$field.' = \''.$db->escapeString($query).'\''; + $order = $field; + } + else + { + $where = 'WHERE transliterate_to_ascii('.$field.') LIKE transliterate_to_ascii(\'%'.$db->escapeString($query).'%\')'; + $order = 'transliterate_to_ascii('.$field.') COLLATE NOCASE'; + } + + $fields = array_keys($champs->getListedFields()); + + if (!in_array($field, $fields)) + { + $fields[] = $field; + } + + if (!in_array('email', $fields)) + { + $fields[] = 'email'; + } + + return $db->simpleStatementFetch( + 'SELECT id, id_categorie, ' . implode(', ', $fields) . ', + '.$config->get('champ_identite').' AS identite, + strftime(\'%s\', date_inscription) AS date_inscription + FROM membres ' . $where . ($order ? ' ORDER BY ' . $order : '') . ' + LIMIT 1000;', + SQLITE3_ASSOC + ); + } + + public function listByCategory($cat, $fields, $page = 1, $order = null, $desc = false) + { + $begin = ($page - 1) * self::ITEMS_PER_PAGE; + + $db = DB::getInstance(); + $config = Config::getInstance(); + + $champs = $config->get('champs_membres'); + + if (is_int($cat) && $cat) + $where = 'WHERE id_categorie = '.(int)$cat; + elseif (is_array($cat)) + $where = 'WHERE id_categorie IN ('.implode(',', $cat).')'; + else + $where = ''; + + if (is_null($order) || !$champs->get($order)) + $order = 'id'; + + if (!empty($fields) && $order != 'id' && $champs->isText($order)) + { + $order = 'transliterate_to_ascii('.$order.') COLLATE NOCASE'; + } + + if ($desc) + { + $order .= ' DESC'; + } + + if (!in_array('email', $fields)) + { + $fields []= 'email'; + } + + $fields = implode(', ', $fields); + + $query = 'SELECT id, id_categorie, '.$fields.', '.$config->get('champ_identite').' AS identite, + strftime(\'%s\', date_inscription) AS date_inscription + FROM membres '.$where.' + ORDER BY '.$order.' LIMIT ?, ?;'; + + return $db->simpleStatementFetch($query, SQLITE3_ASSOC, $begin, self::ITEMS_PER_PAGE); + } + + public function countByCategory($cat = 0) + { + $db = DB::getInstance(); + + if (is_int($cat) && $cat) + $where = 'WHERE id_categorie = '.(int)$cat; + elseif (is_array($cat)) + $where = 'WHERE id_categorie IN ('.implode(',', $cat).')'; + else + $where = ''; + + return $db->simpleQuerySingle('SELECT COUNT(*) FROM membres '.$where.';'); + } + + public function countAllButHidden() + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT COUNT(*) FROM membres WHERE id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1);'); + } + + static public function changeCategorie($id_cat, $membres) + { + foreach ($membres as &$id) + { + $id = (int) $id; + } + + $db = DB::getInstance(); + return $db->simpleUpdate('membres', + ['id_categorie' => (int)$id_cat], + 'id IN ('.implode(',', $membres).')' + ); + } + + static protected function _deleteMembres($membres) + { + foreach ($membres as &$id) + { + $id = (int) $id; + } + + $membres = implode(',', $membres); + + $db = DB::getInstance(); + $db->exec('UPDATE wiki_revisions SET id_auteur = NULL WHERE id_auteur IN ('.$membres.');'); + $db->exec('UPDATE compta_journal SET id_auteur = NULL WHERE id_auteur IN ('.$membres.');'); + //$db->exec('DELETE FROM wiki_suivi WHERE id_membre IN ('.$membres.');'); + return $db->exec('DELETE FROM membres WHERE id IN ('.$membres.');'); + } + + public function sendMessageToCategory($dest, $sujet, $message, $subscribed_only = false) + { + $config = Config::getInstance(); + + $headers = [ + 'From' => '"'.$config->get('nom_asso').'" <'.$config->get('email_asso').'>', + ]; + $message .= "\n\n--\n".$config->get('nom_asso')."\n".$config->get('site_asso'); + + if ($dest == 0) + $where = 'id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)'; + else + $where = 'id_categorie = '.(int)$dest; + + if ($subscribed_only) + { + $where .= ' AND lettre_infos = 1'; + } + + $db = DB::getInstance(); + $res = $db->query('SELECT email FROM membres WHERE LENGTH(email) > 0 AND '.$where.' ORDER BY id;'); + + $sujet = '['.$config->get('nom_asso').'] '.$sujet; + + while ($row = $res->fetchArray(SQLITE3_ASSOC)) + { + utils::mail($row['email'], $sujet, $message, $headers); + } + + return true; + } + + public function searchSQL($query) + { + $db = DB::getInstance(); + + $st = $db->prepare($query); + + if (!$st->readOnly()) + { + throw new UserException('Seules les requêtes en lecture sont autorisées.'); + } + + if (!preg_match('/LIMIT\s+/', $query)) + { + $query = preg_replace('/;?\s*$/', '', $query); + $query .= ' LIMIT 100'; + } + + if (!preg_match('/FROM\s+membres(?:\s+|$|;)/i', $query)) + { + throw new UserException('Seules les requêtes sur la table membres sont autorisées.'); + } + + if (preg_match('/;\s*(.+?)$/', $query)) + { + throw new UserException('Une seule requête peut être envoyée en même temps.'); + } + + $st = $db->prepare($query); + + $res = $st->execute(); + $out = []; + + while ($row = $res->fetchArray(SQLITE3_ASSOC)) + { + if (array_key_exists('passe', $row)) + { + unset($row['passe']); + } + + $out[] = $row; + } + + return $out; + } + + public function schemaSQL() + { + $db = DB::getInstance(); + + $tables = [ + 'membres' => $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'), + 'categories'=> $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'), + ]; + + return $tables; + } +} + +?> \ No newline at end of file diff --git a/include/class.membres_categories.php b/include/class.membres_categories.php new file mode 100644 index 0000000..4a78e13 --- /dev/null +++ b/include/class.membres_categories.php @@ -0,0 +1,150 @@ + Membres::DROIT_AUCUN, + 'connexion' => Membres::DROIT_ACCES, + 'membres' => Membres::DROIT_ACCES, + 'compta' => Membres::DROIT_ACCES, + 'wiki' => Membres::DROIT_ACCES, + 'config' => Membres::DROIT_AUCUN, + ]; + + static public function getDroitsDefaut() + { + return $this->droits; + } + + protected function _checkData(&$data) + { + $db = DB::getInstance(); + + if (!isset($data['nom']) || !trim($data['nom'])) + { + throw new UserException('Le nom de catégorie ne peut rester vide.'); + } + + if (!empty($data['id_cotisation_obligatoire']) + && !$db->simpleQuerySingle('SELECT 1 FROM cotisations WHERE id = ?;', + false, (int)$data['id_cotisation_obligatoire'])) + { + throw new UserException('Numéro de cotisation inconnu.'); + } + + if (isset($data['id_cotisation_obligatoire']) && empty($data['id_cotisation_obligatoire'])) + { + $data['id_cotisation_obligatoire'] = null; + } + } + + public function add($data) + { + $this->_checkData($data); + + if (!isset($data['description'])) + { + $data['description'] = ''; + } + + foreach ($this->droits as $key=>$value) + { + if (!isset($data['droit_'.$key])) + $data['droit_'.$key] = $value; + else + $data['droit_'.$key] = (int)$data['droit_'.$key]; + } + + $db = DB::getInstance(); + $db->simpleInsert('membres_categories', $data); + + return $db->lastInsertRowID(); + } + + public function edit($id, $data) + { + $this->_checkData($data); + + foreach ($this->droits as $key=>$value) + { + if (isset($data['droit_'.$key])) + $data['droit_'.$key] = (int)$data['droit_'.$key]; + } + + if (!isset($data['cacher']) || $data['cacher'] != 1) + $data['cacher'] = 0; + + $db = DB::getInstance(); + return $db->simpleUpdate('membres_categories', $data, 'id = '.(int)$id); + } + + public function get($id) + { + $db = DB::getInstance(); + + return $db->simpleQuerySingle('SELECT * FROM membres_categories WHERE id = ?;', + true, (int) $id); + } + + public function remove($id) + { + $db = DB::getInstance(); + $config = Config::getInstance(); + + if ($id == $config->get('categorie_membres')) + { + throw new UserException('Il est interdit de supprimer la catégorie définie par défaut dans la configuration.'); + } + + if ($db->simpleQuerySingle('SELECT 1 FROM membres WHERE id_categorie = ?;', false, (int)$id)) + { + throw new UserException('La catégorie contient encore des membres, il n\'est pas possible de la supprimer.'); + } + + $db->simpleUpdate( + 'wiki_pages', + [ + 'droit_lecture' => Wiki::LECTURE_NORMAL, + 'droit_ecriture' => Wiki::ECRITURE_NORMAL, + ], + 'droit_lecture = '.(int)$id.' OR droit_ecriture = '.(int)$id + ); + + return $db->simpleExec('DELETE FROM membres_categories WHERE id = ?;', (int) $id); + } + + public function listSimple() + { + $db = DB::getInstance(); + return $db->queryFetchAssoc('SELECT id, nom FROM membres_categories ORDER BY nom;'); + } + + public function listComplete() + { + $db = DB::getInstance(); + return $db->queryFetch('SELECT * FROM membres_categories ORDER BY nom;'); + } + + public function listCompleteWithStats() + { + $db = DB::getInstance(); + return $db->queryFetch('SELECT *, (SELECT COUNT(*) FROM membres WHERE id_categorie = membres_categories.id) AS nombre FROM membres_categories ORDER BY nom;'); + } + + + public function listHidden() + { + $db = DB::getInstance(); + return $db->queryFetchAssoc('SELECT id, nom FROM membres_categories WHERE cacher = 1;'); + } + + public function listNotHidden() + { + $db = DB::getInstance(); + return $db->queryFetchAssoc('SELECT id, nom FROM membres_categories WHERE cacher = 0;'); + } +} + +?> \ No newline at end of file diff --git a/include/class.membres_import.php b/include/class.membres_import.php new file mode 100644 index 0000000..eb3342a --- /dev/null +++ b/include/class.membres_import.php @@ -0,0 +1,272 @@ + 'date_inscription'] + * @return boolean TRUE en cas de succès + */ + public function fromGalette($path, $translation_table) + { + if (!file_exists($path) || !is_readable($path)) + { + throw new \RuntimeException('Fichier inconnu : '.$path); + } + + $fp = fopen($path, 'r'); + + if (!$fp) + { + return false; + } + + $db = DB::getInstance(); + $db->exec('BEGIN;'); + $membres = new Membres; + + $columns = array_flip($this->galette_fields); + + $col = function($column) use (&$row, &$columns) + { + if (!isset($columns[$column])) + return null; + + if (!isset($row[$columns[$column]])) + return null; + + return $row[$columns[$column]]; + }; + + $line = 0; + $delim = utils::find_csv_delim($fp); + + while (!feof($fp)) + { + $row = fgetcsv($fp, 4096, $delim); + $line++; + + if (empty($row)) + { + continue; + } + + if (count($row) != count($columns)) + { + $db->exec('ROLLBACK;'); + throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.'); + } + + $data = []; + + foreach ($translation_table as $galette=>$garradin) + { + // Champs qu'on ne veut pas importer + if (empty($garradin)) + continue; + + // Concaténer plusieurs champs + if (isset($data[$garradin])) + $data[$garradin] .= "\n" . $col($galette); + else + $data[$garradin] = $col($galette); + } + + try { + $membres->add($data); + } + catch (UserException $e) + { + $db->exec('ROLLBACK;'); + throw new UserException('Erreur sur la ligne ' . $line . ' : ' . $e->getMessage()); + } + } + + $db->exec('END;'); + + fclose($fp); + return true; + } + + /** + * Importer un CSV de la liste des membres depuis un export Garradin + * @param string $path Chemin vers le CSV + * @return boolean TRUE en cas de succès + */ + public function fromCSV($path) + { + if (!file_exists($path) || !is_readable($path)) + { + throw new \RuntimeException('Fichier inconnu : '.$path); + } + + $fp = fopen($path, 'r'); + + if (!$fp) + { + return false; + } + + $db = DB::getInstance(); + $db->exec('BEGIN;'); + $membres = new Membres; + + // On récupère les champs qu'on peut importer + $champs = Config::getInstance()->get('champs_membres')->getAll(); + $champs = array_keys($champs); + $champs[] = 'date_inscription'; + $champs[] = 'date_connexion'; + $champs[] = 'id'; + $champs[] = 'id_categorie'; + + $line = 0; + $delim = utils::find_csv_delim($fp); + + while (!feof($fp)) + { + $row = fgetcsv($fp, 4096, $delim); + + $line++; + + if (empty($row)) + { + continue; + } + + if ($line == 1) + { + if (is_numeric($row[0])) + { + throw new UserException('Erreur sur la ligne 1 : devrait contenir l\'en-tête des colonnes.'); + } + + $columns = array_flip($row); + continue; + } + + if (count($row) != count($columns)) + { + $db->exec('ROLLBACK;'); + throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.'); + } + + $data = []; + + foreach ($columns as $name=>$id) + { + $name = trim($name); + + // Champs qui n'existent pas dans le schéma actuel + if (!in_array($name, $champs)) + continue; + + if (trim($row[$id]) !== '') + $data[$name] = $row[$id]; + } + + if (!empty($data['id'])) + { + $id = (int)$data['id']; + unset($data['id']); + } + else + { + $id = false; + } + + try { + if ($id) + $membres->edit($id, $data); + else + $membres->add($data); + } + catch (UserException $e) + { + $db->exec('ROLLBACK;'); + throw new UserException('Erreur sur la ligne ' . $line . ' : ' . $e->getMessage()); + } + } + + $db->exec('END;'); + + fclose($fp); + return true; + } + + public function toCSV() + { + $db = DB::getInstance(); + + $res = $db->prepare('SELECT m.id, c.nom AS categorie, m.* FROM membres AS m + LEFT JOIN membres_categories AS c ON m.id_categorie = c.id ORDER BY c.id;')->execute(); + + $fp = fopen('php://output', 'w'); + $header = false; + + while ($row = $res->fetchArray(SQLITE3_ASSOC)) + { + unset($row['passe']); + + if (!$header) + { + fputcsv($fp, array_keys($row)); + $header = true; + } + + fputcsv($fp, $row); + } + + fclose($fp); + + return true; + } +} \ No newline at end of file diff --git a/include/class.plugin.php b/include/class.plugin.php new file mode 100644 index 0000000..611e59d --- /dev/null +++ b/include/class.plugin.php @@ -0,0 +1,523 @@ + 'text/css', + 'gif' => 'image/gif', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ico' => 'image/x-ico', + 'jpe' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'js' => 'application/x-javascript', + 'pdf' => 'application/pdf', + 'png' => 'image/png', + 'swf' => 'application/shockwave-flash', + 'xml' => 'text/xml', + 'svg' => 'image/svg+xml', + ]; + + /** + * Construire un objet Plugin pour un plugin + * @param string $id Identifiant du plugin + * @throws UserException Si le plugin n'est pas installé (n'existe pas en DB) + */ + public function __construct($id) + { + $db = DB::getInstance(); + $this->plugin = $db->simpleQuerySingle('SELECT * FROM plugins WHERE id = ?;', true, $id); + + if (!$this->plugin) + { + throw new UserException('Ce plugin n\'existe pas ou n\'est pas installé correctement.'); + } + + $this->plugin['config'] = json_decode($this->plugin['config'], true); + + if (!is_array($this->plugin['config'])) + { + $this->plugin['config'] = []; + } + + $this->id = $id; + } + + /** + * Renvoie le chemin absolu vers l'archive du plugin + * @return string Chemin PHAR vers l'archive + */ + public function path() + { + return 'phar://' . PLUGINS_ROOT . '/' . $this->id . '.tar.gz'; + } + + /** + * Renvoie une entrée de la configuration ou la configuration complète + * @param string $key Clé à rechercher, ou NULL si on désire toutes les entrées de la + * @return mixed L'entrée demandée (mixed), ou l'intégralité de la config (array), + * ou NULL si l'entrée demandée n'existe pas. + */ + public function getConfig($key = null) + { + if (is_null($key)) + { + return $this->plugin['config']; + } + + if (array_key_exists($key, $this->plugin['config'])) + { + return $this->plugin['config'][$key]; + } + + return null; + } + + /** + * Enregistre une entrée dans la configuration du plugin + * @param string $key Clé à modifier + * @param mixed $value Valeur à enregistrer, choisir NULL pour effacer cette clé de la configuration + * @return boolean TRUE si tout se passe bien + */ + public function setConfig($key, $value = null) + { + if (is_null($value)) + { + unset($this->plugin['config'][$key]); + } + else + { + $this->plugin['config'][$key] = $value; + } + + $db = DB::getInstance(); + $db->simpleUpdate('plugins', + ['config' => json_encode($this->plugin['config'])], + 'id = \'' . $this->id . '\''); + + return true; + } + + /** + * Renvoie une information ou toutes les informations sur le plugin + * @param string $key Clé de l'info à retourner, ou NULL pour recevoir toutes les infos + * @return mixed Info demandée ou tableau des infos. + */ + public function getInfos($key = null) + { + if (is_null($key)) + { + return $this->plugin; + } + + if (array_key_exists($key, $this->plugin)) + { + return $this->plugin[$key]; + } + + return null; + } + + /** + * Renvoie l'identifiant du plugin + * @return string Identifiant du plugin + */ + public function id() + { + return $this->id; + } + + /** + * Inclure un fichier depuis le plugin (dynamique ou statique) + * @param string $file Chemin du fichier à aller chercher : si c'est un .php il sera inclus, + * sinon il sera juste affiché + * @return void + * @throws UserException Si le fichier n'existe pas ou fait partie des fichiers qui ne peuvent + * être appelés que par des méthodes de Plugin. + * @throws RuntimeException Si le chemin indiqué tente de sortir du contexte du PHAR + */ + public function call($file) + { + $file = preg_replace('!^[./]*!', '', $file); + + if (preg_match('!(?:\.\.|[/\\\\]\.|\.[/\\\\])!', $file)) + { + throw new \RuntimeException('Chemin de fichier incorrect.'); + } + + $forbidden = ['install.php', 'garradin_plugin.ini', 'upgrade.php', 'uninstall.php', 'signals.php']; + + if (in_array($file, $forbidden)) + { + throw new UserException('Le fichier ' . $file . ' ne peut être appelé par cette méthode.'); + } + + if (!file_exists($this->path() . '/www/' . $file)) + { + throw new UserException('Le fichier ' . $file . ' n\'existe pas dans le plugin ' . $this->id); + } + + $plugin = $this; + global $tpl, $config, $user, $membres; + + if (substr($file, -4) === '.php') + { + include $this->path() . '/www/' . $file; + } + else + { + // Récupération du type MIME à partir de l'extension + $ext = substr($file, strrpos($file, '.')+1); + + if (isset($this->mimes[$ext])) + { + $mime = $this->mimes[$ext]; + } + else + { + $mime = 'text/plain'; + } + + header('Content-Type: ' .$this->mimes[$ext]); + header('Content-Length: ' . filesize($this->path() . '/www/' . $file)); + + readfile($this->path() . '/www/' . $file); + } + } + + /** + * Désinstaller le plugin + * @return boolean TRUE si la suppression a fonctionné + */ + public function uninstall() + { + if (file_exists($this->path() . '/uninstall.php')) + { + include $this->path() . '/uninstall.php'; + } + + unlink(PLUGINS_ROOT . '/' . $this->id . '.tar.gz'); + + $db = DB::getInstance(); + return $db->simpleExec('DELETE FROM plugins WHERE id = ?;', $this->id); + } + + /** + * Renvoie TRUE si le plugin a besoin d'être mis à jour + * (si la version notée dans la DB est différente de la version notée dans garradin_plugin.ini) + * @return boolean TRUE si le plugin doit être mis à jour, FALSE sinon + */ + public function needUpgrade() + { + $infos = parse_ini_file($this->path() . '/garradin_plugin.ini', false); + + if (version_compare($this->plugin['version'], $infos['version'], '!=')) + return true; + + return false; + } + + /** + * Mettre à jour le plugin + * Appelle le fichier upgrade.php dans l'archive si celui-ci existe. + * @return boolean TRUE si tout a fonctionné + */ + public function upgrade() + { + if (file_exists($this->path() . '/upgrade.php')) + { + include $this->path() . '/upgrade.php'; + } + + $db = DB::getInstance(); + return $db->simpleUpdate('plugins', + 'id = \''.$db->escapeString($this->id).'\'', + ['version' => $infos['version']]); + } + + /** + * Liste des plugins installés (en DB) + * @return array Liste des plugins triés par nom + */ + static public function listInstalled() + { + $db = DB::getInstance(); + $plugins = $db->simpleStatementFetchAssocKey('SELECT id, * FROM plugins ORDER BY nom;'); + $system = explode(',', PLUGINS_SYSTEM); + + foreach ($plugins as &$row) + { + $row['system'] = in_array($row['id'], $system); + } + + return $plugins; + } + + /** + * Liste les plugins qui doivent être affichés dans le menu + * @return array Tableau associatif id => nom (ou un tableau vide si aucun plugin ne doit être affiché) + */ + static public function listMenu() + { + $db = DB::getInstance(); + return $db->simpleStatementFetchAssoc('SELECT id, nom FROM plugins WHERE menu = 1 ORDER BY nom;'); + } + + /** + * Liste les plugins téléchargés mais non installés + * @return array Liste des plugins téléchargés + */ + static public function listDownloaded() + { + $installed = self::listInstalled(); + + $list = []; + $dir = dir(PLUGINS_ROOT); + + while ($file = $dir->read()) + { + if (substr($file, 0, 1) == '.') + continue; + + if (!preg_match('!^([a-z0-9_.-]+)\.tar\.gz$!', $file, $match)) + continue; + + if (array_key_exists($match[1], $installed)) + continue; + + $list[$match[1]] = parse_ini_file('phar://' . PLUGINS_ROOT . '/' . $match[1] . '.tar.gz/garradin_plugin.ini', false); + } + + $dir->close(); + + return $list; + } + + /** + * Liste des plugins officiels depuis le repository signé + * @return array Liste des plugins + */ + static public function listOfficial() + { + // La liste est stockée en cache une heure pour ne pas tuer le serveur distant + if (Static_Cache::expired('plugins_list', 3600 * 24)) + { + $url = parse_url(PLUGINS_URL); + + $context_options = [ + 'ssl' => [ + 'verify_peer' => TRUE, + // On vérifie en utilisant le certificat maître de CACert + 'cafile' => ROOT . '/include/data/cacert.pem', + 'verify_depth' => 5, + 'CN_match' => $url['host'], + 'SNI_enabled' => true, + 'SNI_server_name' => $url['host'], + 'disable_compression' => true, + ] + ]; + + $context = stream_context_create($context_options); + + try { + $result = file_get_contents(PLUGINS_URL, NULL, $context); + } + catch (\Exception $e) + { + throw new UserException('Le téléchargement de la liste des plugins a échoué : ' . $e->getMessage()); + } + + Static_Cache::store('plugins_list', $result); + } + else + { + $result = Static_Cache::get('plugins_list'); + } + + $list = json_decode($result, true); + return $list; + } + + /** + * Vérifier le hash du plugin $id pour voir s'il correspond au hash du fichier téléchargés + * @param string $id Identifiant du plugin + * @return boolean TRUE si le hash correspond (intégrité OK), sinon FALSE + */ + static public function checkHash($id) + { + $list = self::fetchOfficialList(); + + if (!array_key_exists($id, $list)) + return null; + + $hash = sha1_file(PLUGINS_ROOT . '/' . $id . '.tar.gz'); + + return ($hash === $list[$id]['hash']); + } + + /** + * Est-ce que le plugin est officiel ? + * @param string $id Identifiant du plugin + * @return boolean TRUE si le plugin est officiel, FALSE sinon + */ + static public function isOfficial($id) + { + $list = self::fetchOfficialList(); + return array_key_exists($id, $list); + } + + /** + * Télécharge un plugin depuis le repository officiel, et l'installe + * @param string $id Identifiant du plugin + * @return boolean TRUE si ça marche + * @throws LogicException Si le plugin n'est pas dans la liste des plugins officiels + * @throws UserException Si le plugin est déjà installé ou que le téléchargement a échoué + * @throws RuntimeException Si l'archive téléchargée est corrompue (intégrité du hash ne correspond pas) + */ + static public function download($id) + { + $list = self::fetchOfficialList(); + + if (!array_key_exists($id, $list)) + { + throw new \LogicException($id . ' n\'est pas un plugin officiel (absent de la liste)'); + } + + if (file_exists(PLUGINS_ROOT . '/' . $id . '.tar.gz')) + { + throw new UserException('Le plugin '.$id.' existe déjà.'); + } + + $url = parse_url(PLUGINS_URL); + + $context_options = [ + 'ssl' => [ + 'verify_peer' => TRUE, + 'cafile' => ROOT . '/include/data/cacert.pem', + 'verify_depth' => 5, + 'CN_match' => $url['host'], + 'SNI_enabled' => true, + 'SNI_server_name' => $url['host'], + 'disable_compression' => true, + ] + ]; + + $context = stream_context_create($context_options); + + try { + copy($list[$id]['phar'], PLUGINS_ROOT . '/' . $id . '.tar.gz', $context); + } + catch (\Exception $e) + { + throw new UserException('Le téléchargement du plugin '.$id.' a échoué : ' . $e->getMessage()); + } + + if (!self::checkHash($id)) + { + unlink(PLUGINS_ROOT . '/' . $id . '.tar.gz'); + throw new \RuntimeException('L\'archive du plugin '.$id.' est corrompue (le hash SHA1 ne correspond pas).'); + } + + self::install($id, true); + + return true; + } + + /** + * Installer un plugin + * @param string $id Identifiant du plugin + * @param boolean $official TRUE si le plugin est officiel + * @return boolean TRUE si tout a fonctionné + */ + static public function install($id, $official = false) + { + if (!file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz')) + { + throw new \RuntimeException('Le plugin ' . $id . ' ne semble pas exister et ne peut donc être installé.'); + } + + if (!file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/garradin_plugin.ini')) + { + throw new UserException('L\'archive '.$id.'.tar.gz n\'est pas une extension Garradin : fichier garradin_plugin.ini manquant.'); + } + + $infos = parse_ini_file('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/garradin_plugin.ini', false); + + $required = ['nom', 'description', 'auteur', 'url', 'version', 'menu', 'config']; + + foreach ($required as $key) + { + if (!array_key_exists($key, $infos)) + { + throw new \RuntimeException('Le fichier garradin_plugin.ini ne contient pas d\'entrée "'.$key.'".'); + } + } + + if (!empty($infos['menu']) && !file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/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.'); + } + + $config = ''; + + if ((bool)$infos['config']) + { + if (!file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/config.json')) + { + throw new \RuntimeException('L\'archive '.$id.'.tar.gz ne comporte pas de fichier config.json + alors que le plugin nécessite le stockage d\'une configuration.'); + } + + if (!file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/www/admin/config.php')) + { + throw new \RuntimeException('L\'archive '.$id.'.tar.gz ne comporte pas de fichier www/admin/config.php + alors que le plugin nécessite le stockage d\'une configuration.'); + } + + $config = json_decode(file_get_contents('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/config.json'), true); + + if (is_null($config)) + { + throw new \RuntimeException('config.json invalide. Code erreur JSON: ' . json_last_error()); + } + + $config = json_encode($config); + } + + $db = DB::getInstance(); + $db->simpleInsert('plugins', [ + 'id' => $id, + 'officiel' => (int)(bool)$official, + 'nom' => $infos['nom'], + 'description'=> $infos['description'], + 'auteur' => $infos['auteur'], + 'url' => $infos['url'], + 'version' => $infos['version'], + 'menu' => (int)(bool)$infos['menu'], + 'config' => $config, + ]); + + if (file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/install.php')) + { + include 'phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/install.php'; + } + + return true; + } + + /** + * Renvoie la version installée d'un plugin ou FALSE s'il n'est pas installé + * @param string $id Identifiant du plugin + * @return mixed Numéro de version du plugin ou FALSE + */ + static public function getInstalledVersion($id) + { + return DB::getInstance()->simpleQuerySingle('SELECT version FROM plugins WHERE id = ?;'); + } +} \ No newline at end of file diff --git a/include/class.rappels.php b/include/class.rappels.php new file mode 100644 index 0000000..d970cf6 --- /dev/null +++ b/include/class.rappels.php @@ -0,0 +1,208 @@ +simpleQuerySingle('SELECT 1 FROM cotisations WHERE id = ?;', false, (int) $data['id_cotisation'])) + { + throw new UserException('Cotisation inconnue.'); + } + + $data['id_cotisation'] = (int) $data['id_cotisation']; + + if ((trim($data['delai']) === '') || !is_numeric($data['delai'])) + { + throw new UserException('Délai avant rappel invalide : doit être indiqué en nombre de jours.'); + } + + $data['delai'] = (int) $data['delai']; + + if (!isset($data['sujet']) || trim($data['sujet']) === '') + { + throw new UserException('Le sujet du rappel ne peut être vide.'); + } + + $data['sujet'] = trim($data['sujet']); + + if (!isset($data['texte']) || trim($data['texte']) === '') + { + throw new UserException('Le contenu du rappel ne peut être vide.'); + } + + $data['texte'] = trim($data['texte']); + } + + /** + * Ajouter un rappel + * @param array $data Données du rappel + * @return integer Numéro ID du rappel créé + */ + public function add($data) + { + $db = DB::getInstance(); + + $this->_checkFields($data); + + $db->simpleInsert('rappels', $data); + + return $db->lastInsertRowId(); + } + + /** + * Modifier un rappel automatique + * @param integer $id Numéro du rappel + * @param array $data Données du rappel + * @return boolean TRUE si tout s'est bien passé + * @throws UserException En cas d'erreur dans une donnée à modifier + */ + public function edit($id, $data) + { + $db = DB::getInstance(); + + $this->_checkFields($data); + + return $db->simpleUpdate('rappels', $data, 'id = ' . (int)$id); + } + + /** + * Supprimer un rappel automatique + * @param integer $id Numéro du rappel + * @param boolean $delete_history Effacer aussi l'historique des rappels envoyés + * @return boolean TRUE en cas de succès + */ + public function delete($id, $delete_history = false) + { + $db = DB::getInstance(); + + $db->exec('BEGIN;'); + + if ($delete_history) + { + $db->simpleExec('DELETE FROM rappels_envoyes WHERE id_rappel = ?;', (int) $id); + } + else + { + $db->simpleExec('UPDATE rappels_envoyes SET id_rappel = NULL WHERE id_rappel = ?;', (int) $id); + } + + $db->simpleExec('DELETE FROM rappels WHERE id = ?;', (int) $id); + $db->exec('END;'); + + return true; + } + + /** + * Renvoie les données sur un rappel + * @param integer $id Numéro du rappel + * @return array Données du rappel + */ + public function get($id) + { + return DB::getInstance()->simpleQuerySingle('SELECT * FROM rappels WHERE id = ?;', true, (int)$id); + } + + /** + * Renvoie le nombre de rappels automatiques enregistrés + * @return integer Nombre de rappels + */ + public function countAll() + { + return DB::getInstance()->simpleQuerySingle('SELECT COUNT(*) FROM rappels;'); + } + + /** + * Liste des rappels triés par cotisation + * @return array Liste des rappels + */ + public function listByCotisation() + { + return DB::getInstance()->simpleStatementFetch('SELECT r.*, + c.intitule, c.montant, c.duree, c.debut, c.fin + FROM rappels AS r + INNER JOIN cotisations AS c ON c.id = r.id_cotisation + ORDER BY r.id_cotisation, r.delai, r.sujet;'); + } + + /** + * Liste des rappels pour une cotisation donnée + * @param integer $id Numéro du rappel + * @return array Liste des rappels + */ + public function listForCotisation($id) + { + return DB::getInstance()->simpleStatementFetch('SELECT * FROM rappels + WHERE id_cotisation = ? ORDER BY delai, sujet;', \SQLITE3_ASSOC, (int)$id); + } + + /** + * Envoi des rappels automatiques par e-mail + * @return boolean TRUE en cas de succès + */ + public function sendPending() + { + $db = DB::getInstance(); + $config = Config::getInstance(); + + // Requête compliquée qui fait tout le boulot + // la logique est un JOIN des tables rappels, cotisations, cotisations_membres et membres + // pour récupérer la liste des membres qui doivent recevoir une cotisation + $query = ' + SELECT + *, + /* Nombre de jours avant ou après expiration */ + (julianday(date()) - julianday(expiration)) AS nb_jours, + /* Date de mise en œuvre du rappel */ + date(expiration, delai || \' days\') AS date_rappel + FROM ( + SELECT m.*, r.delai, r.sujet, r.texte, r.id_cotisation, + m.'.$config->get('champ_identite').' AS identite, + CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') + WHEN c.fin IS NOT NULL THEN c.fin ELSE 0 END AS expiration + FROM rappels AS r + INNER JOIN cotisations AS c ON c.id = r.id_cotisation + INNER JOIN cotisations_membres AS cm ON cm.id_cotisation = c.id + INNER JOIN membres AS m ON m.id = cm.id_membre + WHERE + /* Inutile de sélectionner les membres sans email */ + m.email IS NOT NULL AND m.email != \'\' + /* Les cotisations ponctuelles ne comptent pas */ + AND (c.fin IS NOT NULL OR c.duree IS NOT NULL) + /* Rien nest envoyé aux membres des catégories cachées, logique */ + AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1) + ORDER BY r.delai ASC + ) + WHERE nb_jours >= delai + /* Pour ne pas spammer on n\'envoie pas de rappel antérieur au dernier rappel déjà effectué */ + AND id NOT IN (SELECT id_membre FROM rappels_envoyes AS re + WHERE id_cotisation = re.id_cotisation AND id = re.id_membre + AND re.date >= date(expiration, delai || \' days\') + ) + /* Grouper par membre, pour n\'envoyer qu\'un seul rappel par membre/cotise */ + GROUP BY id, id_cotisation + ORDER BY nb_jours DESC;'; + + $db->exec('BEGIN'); + $st = $db->prepare($query); + $res = $st->execute(); + $re = new Rappels_Envoyes; + + while ($row = $res->fetchArray(DB::ASSOC)) + { + $re->sendAuto($row); + } + + $db->exec('END;'); + return true; + } +} \ No newline at end of file diff --git a/include/class.rappels_envoyes.php b/include/class.rappels_envoyes.php new file mode 100644 index 0000000..4bb67cd --- /dev/null +++ b/include/class.rappels_envoyes.php @@ -0,0 +1,231 @@ +simpleQuerySingle('SELECT 1 FROM cotisations WHERE id = ?;', false, (int) $data['id_cotisation'])) + { + throw new UserException('Cotisation inconnue.'); + } + + $data['id_cotisation'] = (int) $data['id_cotisation']; + } + + if (empty($data['id_membre']) + || !$db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ?;', false, (int) $data['id_membre'])) + { + throw new UserException('Membre inconnu.'); + } + + $data['id_membre'] = (int) $data['id_membre']; + + if (empty($data['media']) || !is_numeric($data['media']) + || !in_array((int)$data['media'], [self::MEDIA_EMAIL, self::MEDIA_COURRIER, self::MEDIA_TELEPHONE, self::MEDIA_AUTRE])) + { + throw new UserException('Média invalide.'); + } + + $data['media'] = (int) $data['media']; + + if (empty($data['date']) || !utils::checkDate($data['date'])) + { + throw new UserException('La date indiquée n\'est pas valide.'); + } + } + + /** + * Enregistrer un rappel + * @param array $data Données du rappel + * @return integer Numéro ID du rappel créé + */ + public function add($data) + { + $db = DB::getInstance(); + + $this->_checkFields($data); + + $db->simpleInsert('rappels_envoyes', $data); + + return $db->lastInsertRowId(); + } + + /** + * Supprimer un rappel enregistré + * @param integer $id Numéro du rappel + * @return boolean TRUE en cas de succès + */ + public function delete($id) + { + $db = DB::getInstance(); + $db->simpleExec('DELETE FROM rappels_envoyes WHERE id = ?;', (int) $id); + return true; + } + + /** + * Renvoie les données sur un rappel + * @param integer $id Numéro du rappel + * @return array Données du rappel + */ + public function get($id) + { + return DB::getInstance()->simpleQuerySingle('SELECT * FROM rappels_envoyes WHERE id = ?;', true, (int)$id); + } + + /** + * Remplacer les tags dans le contenu/sujet du mail + * @param string $content Chaîne à traiter + * @param array $data Données supplémentaires à utiliser comme tags (tableau associatif) + * @return string $content dont les tags ont été remplacés par le contenu correct + */ + public function replaceTagsInContent($content, $data = null) + { + $config = Config::getInstance(); + $tags = [ + '#NOM_ASSO' => $config->get('nom_asso'), + '#ADRESSE_ASSO' => $config->get('adresse_asso'), + '#EMAIL_ASSO' => $config->get('email_asso'), + '#SITE_ASSO' => $config->get('site_asso'), + '#URL_RACINE' => WWW_URL, + '#URL_SITE' => WWW_URL, + '#URL_ADMIN' => WWW_URL . 'admin/', + ]; + + if (!empty($data) && is_array($data)) + { + foreach ($data as $key=>$value) + { + $key = '#' . strtoupper($key); + $tags[$key] = $value; + } + } + + return strtr($content, $tags); + } + + /** + * Envoi de mail pour rappel automatisé + * @param array $data Données du rappel automatisé + * @return boolean TRUE + */ + public function sendAuto($data) + { + $replace = $data; + $replace['date_rappel'] = utils::sqliteDateToFrench($replace['date_rappel']); + $replace['date_expiration'] = utils::sqliteDateToFrench($replace['expiration']); + $replace['nb_jours'] = abs($replace['nb_jours']); + $replace['delai'] = abs($replace['delai']); + + $subject = $this->replaceTagsInContent($data['sujet'], $replace); + $text = $this->replaceTagsInContent($data['texte'], $replace); + + // Envoi du mail + utils::mail($data['email'], $subject, $text); + + // Enregistrement en DB + $this->add([ + 'id_cotisation' => $data['id_cotisation'], + 'id_membre' => $data['id'], + 'media' => Rappels_Envoyes::MEDIA_EMAIL, + // On enregistre la date de mise en œuvre du rappel + // et non pas la date d'envoi effective du rappel + // car l'envoi du rappel peut ne pas être effectué + // le jour où il aurait dû être envoyé (la magie des cron) + 'date' => $data['date_rappel'], + ]); + + return true; + } + + /** + * Liste des rappels envoyés à un membre + * @param integer $id Numéro du membre + * @return array Liste des rappels + */ + public function listForMember($id) + { + return DB::getInstance()->simpleStatementFetch('SELECT + re.*, c.intitule, c.montant + FROM rappels_envoyes AS re + INNER JOIN cotisations AS c ON c.id = re.id_cotisation + WHERE re.id_membre = ? + ORDER BY re.date DESC;', \SQLITE3_ASSOC, (int)$id); + } + + /** + * Liste des rappels pour une cotisation donnée + * @param integer $id Numéro de la cotisation + * @param integer $page Numéro de page de liste + * @return array Liste des rappels + */ + public function listForCotisation($id, $page = 1) + { + $begin = ($page - 1) * self::ITEMS_PER_PAGE; + + return DB::getInstance()->simpleStatementFetch('SELECT * FROM rappels_envoyes + WHERE id_rappel IN (SELECT id FROM rappels WHERE id_cotisation = ?) + ORDER BY date DESC;', \SQLITE3_ASSOC, (int)$id); + } + + /** + * Nombre de rappels pour une cotisation donnée + * @param integer $id Numéro de la cotisation + * @return integer Nombre de rappels envoyés + */ + public function countForCotisation($id) + { + return DB::getInstance()->simpleQuerySingle('SELECT COUNT(*) FROM rappels_envoyes + WHERE id_rappel IN (SELECT id FROM rappels WHERE id_cotisation = ?);', + false, (int)$id); + } + + /** + * Liste des rappels envoyés pour un rappel automatique + * @param integer $id Numéro du rappel + * @param integer $page Numéro de page de liste + * @return array Liste des rappels envoyés + */ + public function listForRappel($id, $page = 1) + { + $begin = ($page - 1) * self::ITEMS_PER_PAGE; + + return DB::getInstance()->simpleStatementFetch('SELECT * FROM rappels_envoyes + WHERE id_rappel = ? ORDER BY date DESC LIMIT ?,?;', + \SQLITE3_ASSOC, (int)$id, (int)$begin, self::ITEMS_PER_PAGE); + } + + /** + * Nombre de rappels envoyés pour un rappel automatique + * @param integer $id Numéro du rappel + * @return integer Nombre de rappels envoyés pour ce rappel + */ + public function countForRappel($id) + { + return DB::getInstance()->simpleQuerySingle('SELECT COUNT(*) FROM rappels_envoyes + WHERE id_rappel = ?;', false, (int)$id); + } +} \ No newline at end of file diff --git a/include/class.sauvegarde.php b/include/class.sauvegarde.php new file mode 100644 index 0000000..4fae1cb --- /dev/null +++ b/include/class.sauvegarde.php @@ -0,0 +1,263 @@ +read()) + { + if ($file[0] != '.' && is_file(DATA_ROOT . '/' . $file) + && preg_match('![\w\d._-]+\.' . $ext . '$!i', $file) && $file != basename(DB_FILE)) + { + $out[$file] = filemtime(DATA_ROOT . '/' . $file); + } + } + + $dir->close(); + + ksort($out); + + return $out; + } + + /** + * Crée une nouvelle sauvegarde + * @param boolean $auto Si true le nom de fichier sera celui de la sauvegarde automatique courante, + * sinon le nom sera basé sur la date (sauvegarde manuelle) + * @return string Le nom de fichier de la sauvegarde ainsi créée + */ + public function create($auto = false) + { + $backup = str_replace('.sqlite', ($auto ? '.auto.1' : date('.Y-m-d-His')) . '.sqlite', DB_FILE); + copy(DB_FILE, $backup); + return basename($backup); + } + + /** + * Effectue une rotation des sauvegardes automatiques + * association.auto.1.sqlite deviendra association.auto.2.sqlite par exemple + * @return boolean true + */ + public function rotate() + { + $config = Config::getInstance(); + $nb = $config->get('nombre_sauvegardes'); + + $list = $this->getList(true); + krsort($list); + + if (count($list) >= $nb) + { + $this->remove(key($list)); + array_shift($list); + } + + foreach ($list as $f=>$d) + { + $new = preg_replace_callback('!\.auto\.(\d+)\.sqlite$!', function ($m) { + return '.auto.' . ((int) $m[1] + 1) . '.sqlite'; + }, $f); + + rename(DATA_ROOT . '/' . $f, DATA_ROOT . '/' . $new); + } + + return true; + } + + /** + * Crée une sauvegarde automatique si besoin est + * @return boolean true + */ + public function auto() + { + $config = Config::getInstance(); + + // Pas besoin d'aller plus loin si on ne fait pas de sauvegarde auto + if ($config->get('frequence_sauvegardes') == 0 || $config->get('nombre_sauvegardes') == 0) + return true; + + $list = $this->getList(true); + + if (count($list) > 0) + { + $last = current($list); + } + else + { + $last = false; + } + + // Test de la date de création de la dernière sauvegarde + if ($last >= (time() - ($config->get('frequence_sauvegardes') * 3600 * 24))) + { + return true; + } + + // Si pas de modif depuis la dernière sauvegarde, ça sert à rien d'en faire + if ($last >= filemtime(DB_FILE)) + { + return true; + } + + $this->rotate(); + $this->create(true); + + return true; + } + + /** + * Efface une sauvegarde locale + * @param string $file Nom du fichier à supprimer + * @return boolean true si le fichier a bien été supprimé, false sinon + */ + public function remove($file) + { + if (preg_match('!\.\.+!', $file) || !preg_match('!^[\w\d._-]+\.sqlite$!i', $file) + || $file == basename(DB_FILE)) + { + throw new UserException('Nom de fichier non valide.'); + } + + return unlink(DATA_ROOT . '/' . $file); + } + + /** + * Renvoie sur la sortie courante le contenu du fichier de base de données courant + * @return boolean true + */ + public function dump() + { + $in = fopen(DB_FILE, 'r'); + $out = fopen('php://output', 'w'); + + while (!feof($in)) + { + fwrite($out, fread($in, 8192)); + } + + fclose($in); + fclose($out); + return true; + } + + /** + * Restaure une sauvegarde locale + * @param string $file Le nom de fichier à utiliser comme point de restauration + * @return boolean true si la restauration a fonctionné, false sinon + */ + public function restoreFromLocal($file) + { + if (preg_match('!\.\.+!', $file) || !preg_match('!^[\w\d._-]+$!i', $file)) + { + throw new UserException('Nom de fichier non valide.'); + } + + if (!file_exists(DATA_ROOT . '/' . $file)) + { + throw new UserException('Le fichier fourni n\'existe pas.'); + } + + return $this->restoreDB(DATA_ROOT . '/' . $file); + } + + /** + * Restaure une copie distante (fichier envoyé) + * @param array $file Tableau provenant de $_FILES + * @return boolean true + */ + public function restoreFromUpload($file) + { + if (empty($file['size']) || empty($file['tmp_name']) || !empty($file['error'])) + { + throw new UserException('Le fichier n\'a pas été correctement envoyé. Essayer de le renvoyer à nouveau.'); + } + + $r = $this->restoreDB($file['tmp_name']); + + if ($r) + { + unlink($file['tmp_name']); + } + + return $r; + } + + /** + * Restauration de base de données, la fonction qui le fait vraiment + * @param string $file Chemin absolu vers la base de données à utiliser + * @return mixed true si rien ne va plus, ou self::NEED_UPGRADE si la version de la DB + * ne correspond pas à la version de Garradin (mise à jour nécessaire). + */ + protected function restoreDB($file) + { + // Essayons déjà d'ouvrir la base de données à restaurer en lecture + try { + $db = new \SQLite3($file, SQLITE3_OPEN_READONLY); + } + catch (\Exception $e) + { + throw new UserException('Le fichier fourni n\'est pas une base de données valide. ' . + 'Message d\'erreur de SQLite : ' . $e->getMessage()); + } + + // Regardons ensuite si la base de données n'est pas corrompue + $check = $db->querySingle('PRAGMA integrity_check;'); + + if (strtolower(trim($check)) != 'ok') + { + throw new UserException('Le fichier fourni est corrompu. SQLite a trouvé ' . $check . ' erreurs.'); + } + + // On ne peut pas faire de vérifications très poussées sur la structure de la base de données, + // celle-ci pouvant changer d'une version à l'autre et on peut vouloir importer une base + // un peu vieille, mais on vérifie quand même que ça ressemble un minimum à une base garradin + $table = $db->querySingle('SELECT 1 FROM sqlite_master WHERE type=\'table\' AND tbl_name=\'config\';'); + + if (!$table) + { + throw new UserException('Le fichier fourni ne semble pas contenir de données liées à Garradin.'); + } + + // On récupère la version pour plus tard + $version = $db->querySingle('SELECT valeur FROM config WHERE cle=\'version\';'); + + $db->close(); + + $backup = str_replace('.sqlite', date('.Y-m-d-His') . '.avant_restauration.sqlite', DB_FILE); + + if (!rename(DB_FILE, $backup)) + { + throw new \RuntimeException('Unable to backup current DB file.'); + } + + if (!copy($file, DB_FILE)) + { + rename($backup, DB_FILE); + throw new \RuntimeException('Unable to copy backup DB to main location.'); + } + + if ($version != garradin_version()) + { + return self::NEED_UPGRADE; + } + + return true; + } + +} + +?> \ No newline at end of file diff --git a/include/class.squelette.php b/include/class.squelette.php new file mode 100644 index 0000000..d659981 --- /dev/null +++ b/include/class.squelette.php @@ -0,0 +1,753 @@ +_getType($type, $value); + + if ($type == self::OBJ) + { + $this->_content = $value->get(); + } + else + { + $this->_content[] = (string) (int) $type . $value; + } + + unset($value); + } + + public function prepend($type = self::TEXT, $value, $pos = false) + { + $type = $this->_getType($type, $value); + + if ($type == self::OBJ) + { + if ($pos) + { + array_splice($this->_content, $pos, 0, $value->get()); + } + else + { + $this->_content = array_merge($value->get(), $this->_content); + } + } + else + { + $value = (string) (int) $type . $value; + + if ($pos) + { + array_splice($this->_content, $pos, 0, $value); + } + else + { + array_unshift($this->_content, $value); + } + } + + unset($value); + } + + public function append($type = self::TEXT, $value, $pos = false) + { + $type = $this->_getType($type, $value); + + if ($type == self::OBJ) + { + if ($pos) + { + array_splice($this->_content, $pos + 1, 0, $value->get()); + } + else + { + $this->_content = array_merge($this->_content, $value->get()); + } + } + else + { + $value = (string) (int) $type . $value; + + if ($pos) + { + array_splice($this->_content, $pos + 1, 0, $value); + } + else + { + array_push($this->_content, $value); + } + } + + unset($value); + } + + public function output($in_php = false) + { + $out = ''; + $php = $in_php ?: false; + + foreach ($this->_content as $line) + { + if ($line[0] == self::PHP && !$php) + { + $php = true; + $out .= ''; + } + + $out .= substr($line, 1); + + if ($line[0] == self::PHP) + { + $out .= "\n"; + } + } + + if ($php && !$in_php) + { + $out .= ' ?>'; + } + + $this->_content = []; + + return $out; + } + + public function __toString() + { + return $this->output(false); + } + + public function get() + { + return $this->_content; + } + + public function replace($key, $type = self::TEXT, $value) + { + $type = $this->_getType($type, $value); + + if ($type == self::OBJ) + { + array_splice($this->_content, $key, 1, $value->get()); + } + else + { + $this->_content[$key] = (string) (int) $type . $value; + } + + unset($value); + } +} + +class Squelette extends \miniSkel +{ + private $parent = null; + private $current = null; + private $_vars = []; + + private function _registerDefaultModifiers() + { + foreach (Squelette_Filtres::$filtres_php as $func=>$name) + { + if (is_string($func)) + $this->register_modifier($name, $func); + else + $this->register_modifier($name, $name); + } + + foreach (get_class_methods('Garradin\Squelette_Filtres') as $name) + { + $this->register_modifier($name, ['Garradin\Squelette_Filtres', $name]); + } + + foreach (Squelette_Filtres::$filtres_alias as $name=>$func) + { + $this->register_modifier($name, ['Garradin\Squelette_Filtres', $func]); + } + } + + public function __construct() + { + $this->_registerDefaultModifiers(); + + $config = Config::getInstance(); + + $this->assign('nom_asso', $config->get('nom_asso')); + $this->assign('adresse_asso', $config->get('adresse_asso')); + $this->assign('email_asso', $config->get('email_asso')); + $this->assign('site_asso', $config->get('site_asso')); + + $this->assign('url_racine', WWW_URL); + $this->assign('url_site', WWW_URL); + $this->assign('url_atom', WWW_URL . 'feed/atom/'); + $this->assign('url_elements', WWW_URL . 'squelettes/'); + $this->assign('url_admin', WWW_URL . 'admin/'); + } + + protected function processInclude($args) + { + if (empty($args)) + throw new \miniSkelMarkupException("Le tag INCLURE demande à préciser le fichier à inclure."); + + $file = key($args); + + if (empty($file) || !preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $file)) + throw new \miniSkelMarkupException("INCLURE: le nom de fichier ne peut contenir que des caractères alphanumériques."); + + return new Squelette_Snippet(1, '$this->fetch("'.$file.'", false);'); + } + + protected function processVariable($name, $value, $applyDefault, $modifiers, $pre, $post, $context) + { + if ($context == self::CONTEXT_IN_ARG) + { + $out = new Squelette_Snippet(1, '$this->getVariable(\''.$name.'\')'); + + if ($pre) + { + $out->prepend(2, $pre); + } + + if ($post) + { + $out->append(2, $post); + } + + return $out; + } + + $out = new Squelette_Snippet(1, '$value = $this->getVariable(\''.$name.'\');'); + + // We process modifiers + foreach ($modifiers as &$modifier) + { + if (!isset($this->modifiers[$modifier['name']])) + { + throw new \miniSkelMarkupException('Filtre '.$modifier['name'].' inconnu !'); + } + + $out->append(1, '$value = call_user_func_array('.var_export($this->modifiers[$modifier['name']], true).', [$value, '); + + foreach ($modifier['arguments'] as $arg) + { + if ($arg == 'debut_liste') + { + $out->append(1, '$this->getVariable(\'debut_liste\')'); + } + elseif ($arg instanceOf Squelette_Snippet) + { + $out->append(3, $arg); + } + else + { + //if (preg_match('!getVariable!', $arg)) throw new Exception("lol"); + $out->append(1, '"'.str_replace('"', '\\"', $arg).'"'); + } + + $out->append(1, ', '); + } + + $out->append(1, ']);'); + + if (in_array($modifier['name'], Squelette_Filtres::$desactiver_defaut)) + { + $applyDefault = false; + } + } + + if ($applyDefault) + { + $out->append(1, 'if (is_string($value) && trim($value)) $value = htmlspecialchars($value, ENT_QUOTES, \'UTF-8\', false);'); + } + + $out->append(1, 'if ($value === true || trim($value) !== \'\'):'); + + // Getting pre-content + if ($pre) + { + $out->append(2, $pre); + } + + $out->append(1, 'echo is_bool($value) ? "" : $value;'); + + // Getting post-content + if ($post) + { + $out->append(2, $post); + } + + $out->append(1, 'endif;'); + + return $out; + } + + protected function processLoop($loopName, $loopType, $loopCriterias, $loopContent, $preContent, $postContent, $altContent) + { + if ($loopType != 'articles' && $loopType != 'rubriques' && $loopType != 'pages') + { + throw new \miniSkelMarkupException("Le type de boucle '".$loopType."' est inconnu."); + } + + $loopStart = ''; + $query = $where = $order = ''; + $limit = $begin = 0; + + $query = 'SELECT w.*, strftime(\\\'%s\\\', w.date_creation) AS date_creation, strftime(\\\'%s\\\', w.date_modification) AS date_modification'; + + if (trim($loopContent)) + { + $query .= ', r.contenu AS texte FROM wiki_pages AS w LEFT JOIN wiki_revisions AS r ON (w.id = r.id_page AND w.revision = r.revision) '; + } + else + { + $query .= '\'\' AS texte '; + } + + $where = 'WHERE w.droit_lecture = -1 '; + + if ($loopType == 'articles') + { + $where .= 'AND (SELECT COUNT(id) FROM wiki_pages WHERE parent = w.id) = 0 '; + } + elseif ($loopType == 'rubriques') + { + $where .= 'AND (SELECT COUNT(id) FROM wiki_pages WHERE parent = w.id) > 0 '; + } + + $allowed_fields = ['id', 'uri', 'titre', 'date', 'date_creation', 'date_modification', + 'parent', 'rubrique', 'revision', 'points', 'recherche', 'texte']; + $search = $search_rank = false; + + foreach ($loopCriterias as $criteria) + { + if (isset($criteria['field'])) + { + if (!in_array($criteria['field'], $allowed_fields)) + { + throw new \miniSkelMarkupException("Critère '".$criteria['field']."' invalide pour la boucle '$loopName' de type '$loopType'."); + } + elseif ($criteria['field'] == 'rubrique') + { + $criteria['field'] = 'parent'; + } + elseif ($criteria['field'] == 'date') + { + $criteria['field'] = 'date_creation'; + } + elseif ($criteria['field'] == 'points') + { + if ($criteria['action'] != \miniSkel::ACTION_ORDER_BY) + { + throw new \miniSkelMarkupException("Le critère 'points' n\'est pas valide dans ce contexte."); + } + + $search_rank = true; + } + } + + switch ($criteria['action']) + { + case \miniSkel::ACTION_ORDER_BY: + if (!$order) + $order = 'ORDER BY '.$criteria['field'].''; + else + $order .= ', '.$criteria['field'].''; + break; + case \miniSkel::ACTION_ORDER_DESC: + if ($order) + $order .= ' DESC'; + break; + case \miniSkel::ACTION_LIMIT: + $begin = $criteria['begin']; + $limit = $criteria['number']; + break; + case \miniSkel::ACTION_MATCH_FIELD_BY_VALUE: + $where .= ' AND '.$criteria['field'].' '.$criteria['comparison'].' \\\'\'.$db->escapeString(\''.$criteria['value'].'\').\'\\\''; + break; + case \miniSkel::ACTION_MATCH_FIELD: + { + if ($criteria['field'] == 'recherche') + { + $query = 'SELECT w.*, r.contenu AS texte, rank(matchinfo(wiki_recherche), 0, 1.0, 1.0) AS points FROM wiki_pages AS w INNER JOIN wiki_recherche AS r ON (w.id = r.id) '; + $where .= ' AND wiki_recherche MATCH \\\'\'.$db->escapeString($this->getVariable(\''.$criteria['field'].'\')).\'\\\''; + $search = true; + } + else + { + if ($criteria['field'] == 'parent') + $field = 'id'; + else + $field = $criteria['field']; + + $where .= ' AND '.$criteria['field'].' = \\\'\'.$db->escapeString($this->getVariable(\''.$field.'\')).\'\\\''; + } + break; + } + default: + break; + } + } + + if ($search_rank && !$search) + { + throw new \miniSkelMarkupException("Le critère par points n'est possible que dans les boucles de recherche."); + } + + if (trim($loopContent)) + { + $loopStart .= '$row[\'url\'] = WWW_URL . $row[\'uri\']; '; + } + + $query .= $where . ' ' . $order; + + if (!$limit || $limit > 100) + $limit = 100; + + if ($limit) + { + $query .= ' LIMIT '.(is_numeric($begin) ? (int) $begin : '\'.$this->variables[\'debut_liste\'].\'').','.(int)$limit; + } + + $hash = sha1(uniqid(mt_rand(), true)); + $out = new Squelette_Snippet(); + $out->append(1, '$parent_hash = $this->current[\'_self_hash\'];'); + $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$parent_hash] : null;'); + + if ($search) + { + $out->append(1, 'if (trim($this->getVariable(\'recherche\'))) { '); + } + + $out->append(1, '$statement = $db->prepare(\''.$query.'\'); '); + // Sécurité anti injection + $out->append(1, 'if (!$statement->readOnly()) { throw new \\miniSkelMarkupException("Requête en écriture illégale: '.$query.'"); } '); + $out->append(1, '$result_'.$hash.' = $statement->execute(); '); + $out->append(1, '$nb_rows = $db->countRows($result_'.$hash.'); '); + + if ($search) + { + $out->append(1, '} else { $result_'.$hash.' = false; $nb_rows = 0; }'); + } + + $out->append(1, '$this->_vars[\''.$hash.'\'] = [\'_self_hash\' => \''.$hash.'\', \'_parent_hash\' => $parent_hash, \'total_boucle\' => $nb_rows, \'compteur_boucle\' => 0];'); + $out->append(1, '$this->current =& $this->_vars[\''.$hash.'\']; '); + $out->append(1, 'if ($nb_rows > 0):'); + + if ($preContent) + { + $out->append(2, $this->parse($preContent, $loopName, self::PRE_CONTENT)); + } + + $out->append(1, 'while ($row = $result_'.$hash.'->fetchArray(SQLITE3_ASSOC)): '); + $out->append(1, '$this->_vars[\''.$hash.'\'][\'compteur_boucle\'] += 1; '); + $out->append(1, $loopStart); + $out->append(1, '$this->_vars[\''.$hash.'\'] = array_merge($this->_vars[\''.$hash.'\'], $row); '); + + $out->append(2, $this->parseVariables($loopContent)); + + $out->append(1, 'endwhile;'); + + // we put the post-content after the loop content + if ($postContent) + { + $out->append(2, $this->parse($postContent, $loopName, self::POST_CONTENT)); + } + + if ($altContent) + { + $out->append(1, 'else:'); + $out->append(2, $this->parse($altContent, $loopName, self::ALT_CONTENT)); + } + + $out->append(1, 'endif; '); + $out->append(1, '$parent_hash = $this->_vars[\''.$hash.'\'][\'_parent_hash\']; '); + $out->append(1, 'unset($result_'.$hash.', $nb_rows, $this->_vars[\''.$hash.'\']); '); + $out->append(1, 'if ($parent_hash) { $this->current =& $this->_vars[$parent_hash]; $parent_hash = $this->current[\'_parent_hash\']; } '); + $out->append(1, 'else { $this->current = null; }'); + $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$_parent_hash] : null;'); + + return $out; + } + + public function fetch($template, $no_display = false) + { + $this->currentTemplate = $template; + + $path = file_exists(DATA_ROOT . '/www/squelettes/' . $template) + ? DATA_ROOT . '/www/squelettes/' . $template + : ROOT . '/www/squelettes-dist/' . $template; + + $tpl_id = basename(dirname($path)) . '/' . $template; + + if (!self::compile_check($tpl_id, $path)) + { + if (!file_exists($path)) + { + throw new \miniSkelMarkupException('Le squelette "'.$tpl_id.'" n\'existe pas.'); + } + + $content = file_get_contents($path); + $content = strtr($content, [' '<?php', ' '']); + + $out = new Squelette_Snippet(2, $this->parse($content)); + $out->prepend(1, '/* '.$tpl_id.' */ '. + 'namespace Garradin; $db = DB::getInstance(); '. + 'if ($this->parent) $parent_hash = $this->parent[\'_self_hash\']; '. // For included files + 'else $parent_hash = false;'); + + if (!$no_display) + { + self::compile_store($tpl_id, $out); + } + } + + if (!$no_display) + { + require self::compile_get_path($tpl_id); + } + else + { + eval($tpl_id); + } + + return null; + } + + public function dispatchURI() + { + $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; + + header('HTTP/1.1 200 OK', 200, true); + + if ($pos = strpos($uri, '?')) + { + $uri = substr($uri, 0, $pos); + } + else + { + // WWW_URI inclus toujours le slash final, mais on veut le conserver ici + $uri = substr($uri, strlen(WWW_URI) - 1); + } + + if ($uri == '/') + { + $skel = 'sommaire.html'; + } + elseif ($uri == '/feed/atom/') + { + header('Content-Type: application/atom+xml'); + $skel = 'atom.xml'; + } + elseif (substr($uri, -1) == '/') + { + $skel = 'rubrique.html'; + $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1, -1); + } + elseif (preg_match('!^/admin/!', $uri)) + { + throw new UserException('Cette page n\'existe pas.'); + } + else + { + $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1); + + if (preg_match('!^[\w\d_-]+$!i', $_GET['uri']) + && file_exists(DATA_ROOT . '/www/squelettes/' . strtolower($_GET['uri']) . '.html')) + { + $skel = strtolower($_GET['uri']) . '.html'; + } + else + { + $skel = 'article.html'; + } + } + + $this->display($skel); + } + + static private function compile_get_path($path) + { + $hash = sha1($path); + return DATA_ROOT . '/cache/compiled/s_' . $hash . '.php'; + } + + static private function compile_check($tpl, $check) + { + if (!file_exists(self::compile_get_path($tpl))) + return false; + + $time = filemtime(self::compile_get_path($tpl)); + + if (empty($time)) + { + return false; + } + + if ($time < filemtime($check)) + return false; + return $time; + } + + static private function compile_store($tpl, $content) + { + $path = self::compile_get_path($tpl); + + if (!file_exists(dirname($path))) + { + mkdir(dirname($path)); + } + + file_put_contents($path, $content); + return true; + } + + static public function compile_clear($tpl) + { + $path = self::compile_get_path($tpl); + + if (file_exists($path)) + unlink($path); + + return true; + } + + protected function getVariable($var) + { + if (isset($this->current[$var])) + { + return $this->current[$var]; + } + elseif (isset($this->parent[$var])) + { + return $this->parent[$var]; + } + elseif (isset($this->variables[$var])) + { + return $this->variables[$var]; + } + elseif (isset($_REQUEST[$var])) + { + return $_REQUEST[$var]; + } + else + { + return null; + } + } + + static public function getSource($template) + { + if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $template)) + return false; + + $path = file_exists(DATA_ROOT . '/www/squelettes/' . $template) + ? DATA_ROOT . '/www/squelettes/' . $template + : ROOT . '/www/squelettes-dist/' . $template; + + if (!file_exists($path)) + return false; + + return file_get_contents($path); + } + + static public function editSource($template, $content) + { + if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $template)) + return false; + + $path = DATA_ROOT . '/www/squelettes/' . $template; + + return file_put_contents($path, $content); + } + + static public function resetSource($template) + { + if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $template)) + return false; + + if (file_exists(DATA_ROOT . '/www/squelettes/' . $template)) + { + unlink(DATA_ROOT . '/www/squelettes/' . $template); + } + + return true; + } + + static public function listSources() + { + if (!file_exists(DATA_ROOT . '/www/squelettes')) + { + mkdir(DATA_ROOT . '/www/squelettes'); + } + + $sources = []; + + $dir = dir(ROOT . '/www/squelettes-dist'); + + while ($file = $dir->read()) + { + if ($file[0] == '.') + continue; + + if (!preg_match('/\.(?:css|x?html?|atom|rss|xml|svg|txt)$/i', $file)) + continue; + + $sources[] = $file; + } + + $dir->close(); + + $dir = dir(DATA_ROOT . '/www/squelettes'); + + while ($file = $dir->read()) + { + if ($file[0] == '.') + continue; + + if (!preg_match('/\.(?:css|x?html?|atom|rss|xml|svg|txt)$/i', $file)) + continue; + + $sources[] = $file; + } + + $dir->close(); + + $sources = array_unique($sources); + sort($sources); + + return $sources; + } + +} + +?> \ No newline at end of file diff --git a/include/class.wiki.php b/include/class.wiki.php new file mode 100644 index 0000000..3b66544 --- /dev/null +++ b/include/class.wiki.php @@ -0,0 +1,528 @@ +simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE id = ?;', false, $data['parent'])) + { + $data['parent'] = 0; + } + } + + return true; + } + + public function create($data = []) + { + $this->_checkFields($data); + $db = DB::getInstance(); + + if (!empty($data['uri'])) + { + $data['uri'] = self::transformTitleToURI($data['uri']); + + if ($db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE uri = ? LIMIT 1;', false, $data['uri'])) + { + throw new UserException('Cette adresse de page est déjà utilisée pour une autre page, il faut en choisir une autre.'); + } + } + else + { + $data['uri'] = self::transformTitleToURI($data['titre']); + + if (!trim($data['uri']) || $db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE uri = ? LIMIT 1;', false, $data['uri'])) + { + $data['uri'] .= '_' . date('d-m-Y_H-i-s'); + } + } + + $db->simpleInsert('wiki_pages', $data); + $id = $db->lastInsertRowId(); + + // On ne peut utiliser un trigger pour insérer dans la recherche + // car les tables virtuelles font des opérations qui modifient + // last_insert_rowid() et donc résultat incohérent + $db->simpleInsert('wiki_recherche', ['id' => $id, 'titre' => $data['titre']]); + + return $id; + } + + public function edit($id, $data = []) + { + $db = DB::getInstance(); + $this->_checkFields($data); + + if (isset($data['uri'])) + { + $data['uri'] = self::transformTitleToURI($data['uri']); + + if ($db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE uri = ? AND id != ? LIMIT 1;', false, $data['uri'], (int)$id)) + { + throw new UserException('Cette adresse de page est déjà utilisée pour une autre page, il faut en choisir une autre.'); + } + } + + if (isset($data['droit_lecture']) && $data['droit_lecture'] >= self::LECTURE_CATEGORIE) + { + $data['droit_ecriture'] = $data['droit_lecture']; + } + + if (isset($data['parent']) && (int)$data['parent'] == (int)$id) + { + $data['parent'] = 0; + } + + $data['date_modification'] = gmdate('Y-m-d H:i:s'); + + // Modification de la date de création + if (isset($data['date_creation'])) + { + // Si la date n'est pas valide tant pis + if (!(strtotime($data['date_creation']) > 0)) + { + unset($data['date_creation']); + } + else + { + $data['date_creation'] = gmdate('Y-m-d H:i:s', $data['date_creation']); + } + } + + $db->simpleUpdate('wiki_pages', $data, 'id = '.(int)$id); + return true; + } + + public function delete($id) + { + $db = DB::getInstance(); + + if ($db->simpleQuerySingle('SELECT COUNT(*) FROM wiki_pages WHERE parent = ?;', false, (int)$id)) + { + return false; + } + + $db->simpleExec('DELETE FROM wiki_revisions WHERE id_page = ?;', (int)$id); + //$db->simpleExec('DELETE FROM wiki_suivi WHERE id_page = ?;', (int)$id); FIXME + $db->simpleExec('DELETE FROM wiki_recherche WHERE id = ?;', (int)$id); + $db->simpleExec('DELETE FROM wiki_pages WHERE id = ?;', (int)$id); + return true; + } + + public function get($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT *, + strftime(\'%s\', date_creation) AS date_creation, + strftime(\'%s\', date_modification) AS date_modification + FROM wiki_pages WHERE id = ? LIMIT 1;', true, (int)$id); + } + + public function getTitle($id) + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT titre FROM wiki_pages WHERE id = ? LIMIT 1;', false, (int)$id); + } + + public function getRevision($id, $rev) + { + $db = DB::getInstance(); + $champ_id = Config::getInstance()->get('champ_identite'); + + // FIXME pagination au lieu de bloquer à 1000 + return $db->simpleQuerySingle('SELECT r.revision, r.modification, r.id_auteur, r.contenu, + strftime(\'%s\', r.date) AS date, LENGTH(r.contenu) AS taille, m.'.$champ_id.' AS nom_auteur, + r.chiffrement + FROM wiki_revisions AS r LEFT JOIN membres AS m ON m.id = r.id_auteur + WHERE r.id_page = ? AND revision = ? LIMIT 1;', true, (int) $id, (int) $rev); + } + + public function listRevisions($id) + { + $db = DB::getInstance(); + $champ_id = Config::getInstance()->get('champ_identite'); + + // FIXME pagination au lieu de bloquer à 1000 + return $db->simpleStatementFetch('SELECT r.revision, r.modification, r.id_auteur, + strftime(\'%s\', r.date) AS date, LENGTH(r.contenu) AS taille, m.'.$champ_id.' AS nom_auteur, + LENGTH(r.contenu) - (SELECT LENGTH(contenu) FROM wiki_revisions WHERE id_page = r.id_page AND revision < r.revision ORDER BY revision DESC LIMIT 1) + AS diff_taille, r.chiffrement + FROM wiki_revisions AS r LEFT JOIN membres AS m ON m.id = r.id_auteur + WHERE r.id_page = ? ORDER BY r.revision DESC LIMIT 1000;', SQLITE3_ASSOC, (int) $id); + } + + public function editRevision($id, $revision_edition = 0, $data) + { + $db = DB::getInstance(); + + $revision = $db->simpleQuerySingle('SELECT revision FROM wiki_pages WHERE id = ?;', false, (int)$id); + + // ?! L'ID fournit ne correspond à rien ? + if ($revision === false) + { + throw new \RuntimeException('La page demandée n\'existe pas.'); + } + + // Pas de révision + if ($revision == 0 && !trim($data['contenu'])) + { + return true; + } + + // Il faut obligatoirement fournir un ID d'auteur + if (empty($data['id_auteur']) && $data['id_auteur'] !== null) + { + throw new \BadMethodCallException('Aucun ID auteur de fourni.'); + } + + $contenu = $db->simpleQuerySingle('SELECT contenu FROM wiki_revisions WHERE revision = ? AND id_page = ?;', false, (int)$revision, (int)$id); + + // Pas de changement au contenu, pas la peine d'enregistrer une nouvelle révision + if (trim($contenu) == trim($data['contenu'])) + { + return true; + } + + // Révision sur laquelle est basée la nouvelle révision + // utilisé pour vérifier que le contenu n'a pas été modifié depuis qu'on + // a chargé la page d'édition + if ($revision > $revision_edition) + { + throw new UserException('La page a été modifiée depuis le début de votre modification.'); + } + + if (empty($data['chiffrement'])) + $data['chiffrement'] = 0; + + if (!isset($data['modification']) || !trim($data['modification'])) + $data['modification'] = null; + + // Incrémentons le numéro de révision + $revision++; + + $data['id_page'] = $id; + $data['revision'] = $revision; + + $db->simpleInsert('wiki_revisions', $data); + $db->simpleUpdate('wiki_pages', [ + 'revision' => $revision, + 'date_modification' => gmdate('Y-m-d H:i:s'), + ], 'id = '.(int)$id); + + return true; + } + + public function search($query) + { + $db = DB::getInstance(); + return $db->simpleStatementFetch('SELECT + p.uri, r.*, snippet(wiki_recherche, \'\', \'\', \'...\', -1, -50) AS snippet, + rank(matchinfo(wiki_recherche), 0, 1.0, 1.0) AS points + FROM wiki_recherche AS r INNER JOIN wiki_pages AS p ON p.id = r.id + WHERE '.$this->_getLectureClause('p.').' AND wiki_recherche MATCH \''.$db->escapeString($query).'\' + ORDER BY points DESC LIMIT 0,50;'); + } + + public function setRestrictionCategorie($id, $droit_wiki) + { + $this->restriction_categorie = $id; + $this->restriction_droit = $droit_wiki; + return true; + } + + protected function _getLectureClause($prefix = '') + { + if (is_null($this->restriction_categorie)) + { + throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.'); + } + + if ($this->restriction_droit == Membres::DROIT_AUCUN) + { + throw new UserException('Vous n\'avez pas accès au wiki.'); + } + + if ($this->restriction_droit == Membres::DROIT_ADMIN) + return '1'; + + return '('.$prefix.'droit_lecture = '.self::LECTURE_NORMAL.' OR '.$prefix.'droit_lecture = '.self::LECTURE_PUBLIC.' + OR '.$prefix.'droit_lecture = '.(int)$this->restriction_categorie.')'; + } + + public function canReadPage($lecture) + { + if (is_null($this->restriction_categorie)) + { + throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.'); + } + + if ($this->restriction_droit < Membres::DROIT_ACCES) + { + return false; + } + + if ($this->restriction_droit == Membres::DROIT_ADMIN + || $lecture == self::LECTURE_NORMAL || $lecture == self::LECTURE_PUBLIC + || $lecture == $this->restriction_categorie) + return true; + + return false; + } + + public function canWritePage($ecriture) + { + if (is_null($this->restriction_categorie)) + { + throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.'); + } + + if ($this->restriction_droit < Membres::DROIT_ECRITURE) + { + return false; + } + + if ($this->restriction_droit == Membres::DROIT_ADMIN + || $ecriture == self::ECRITURE_NORMAL + || $ecriture == $this->restriction_categorie) + return true; + + return false; + } + + public function getList($parent = 0) + { + $db = DB::getInstance(); + + return $db->simpleStatementFetch( + 'SELECT id, revision, uri, titre, + strftime(\'%s\', date_creation) AS date_creation, + strftime(\'%s\', date_modification) AS date_modification + FROM wiki_pages + WHERE parent = ? AND '.$this->_getLectureClause().' + ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE LIMIT 500;', + SQLITE3_ASSOC, + (int) $parent + ); + } + + public function getById($id) + { + $db = DB::getInstance(); + $page = $db->simpleQuerySingle('SELECT *, + strftime(\'%s\', date_creation) AS date_creation, + strftime(\'%s\', date_modification) AS date_modification + FROM wiki_pages + WHERE id = ?;', true, (int)$id); + + if (!$page) + { + return false; + } + + if ($page['revision'] > 0) + { + $page['contenu'] = $db->simpleQuerySingle('SELECT * FROM wiki_revisions + WHERE id_page = ? AND revision = ?;', true, (int)$page['id'], (int)$page['revision']); + } + else + { + $page['contenu'] = false; + } + + return $page; + } + + public function getByURI($uri) + { + $db = DB::getInstance(); + $page = $db->simpleQuerySingle('SELECT *, + strftime(\'%s\', date_creation) AS date_creation, + strftime(\'%s\', date_modification) AS date_modification + FROM wiki_pages + WHERE uri = ?;', true, trim($uri)); + + if (!$page) + { + return false; + } + + if ($page['revision'] > 0) + { + $page['contenu'] = $db->simpleQuerySingle('SELECT * FROM wiki_revisions + WHERE id_page = ? AND revision = ?;', true, (int)$page['id'], (int)$page['revision']); + } + else + { + $page['contenu'] = false; + } + + return $page; + } + + public function listRecentModifications($page = 1) + { + $begin = ($page - 1) * self::ITEMS_PER_PAGE; + + $db = DB::getInstance(); + + return $db->simpleStatementFetch('SELECT *, + strftime(\'%s\', date_creation) AS date_creation, + strftime(\'%s\', date_modification) AS date_modification + FROM wiki_pages + WHERE '.$this->_getLectureClause().' + ORDER BY date_modification DESC;', SQLITE3_ASSOC); + } + + public function countRecentModifications() + { + $db = DB::getInstance(); + return $db->simpleQuerySingle('SELECT COUNT(*) FROM wiki_pages WHERE '.$this->_getLectureClause().';'); + } + + public function listBackBreadCrumbs($id) + { + if ($id == 0) + return []; + + $db = DB::getInstance(); + $flat = []; + + while ($id > 0) + { + $res = $db->simpleQuerySingle('SELECT parent, titre, uri + FROM wiki_pages WHERE id = ? LIMIT 1;', true, (int)$id); + + $flat[] = [ + 'id' => $id, + 'titre' => $res['titre'], + 'uri' => $res['uri'], + ]; + + $id = (int)$res['parent']; + } + + return array_reverse($flat); + } + + public function listBackParentTree($id) + { + $db = DB::getInstance(); + $flat = [ + [ + 'id' => 0, + 'parent' => null, + 'titre' => 'Racine', + 'children' => $db->simpleStatementFetchAssocKey('SELECT id, parent, titre FROM wiki_pages + WHERE parent = ? ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE;', + SQLITE3_ASSOC, 0) + ] + ]; + + do + { + $parent = $db->simpleQuerySingle('SELECT parent FROM wiki_pages WHERE id = ? LIMIT 1;', false, (int)$id); + + $flat[$id] = [ + 'id' => $id, + 'parent' => $id ? (int)$parent : null, + 'titre' => $id ? (string)$db->simpleQuerySingle('SELECT titre FROM wiki_pages WHERE id = ? LIMIT 1;', false, (int)$id) : 'Racine', + 'children' => $db->simpleStatementFetchAssocKey('SELECT id, parent, titre FROM wiki_pages + WHERE parent = ? ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE;', + SQLITE3_ASSOC, (int)$id) + ]; + + $id = (int)$parent; + } + while ($id != 0); + + $tree = []; + foreach ($flat as $id=>&$node) + { + if (is_null($node['parent'])) + { + $tree[$id] = &$node; + } + else + { + if (!isset($flat[$node['parent']]['children'])) + { + $flat[$node['parent']]['children'] = []; + } + + $flat[$node['parent']]['children'][$id] = &$node; + } + } + + return $tree; + } +} + +?> \ No newline at end of file diff --git a/include/data/0.4.0.sql b/include/data/0.4.0.sql new file mode 100644 index 0000000..33b7e53 --- /dev/null +++ b/include/data/0.4.0.sql @@ -0,0 +1,103 @@ +CREATE TABLE compta_exercices +-- Exercices +( + id INTEGER PRIMARY KEY, + + libelle TEXT NOT NULL, + + debut TEXT NOT NULL DEFAULT CURRENT_DATE, + fin TEXT NULL DEFAULT NULL, + + clos INTEGER NOT NULL DEFAULT 0 +); + + +CREATE TABLE compta_comptes +-- Plan comptable +( + id TEXT PRIMARY KEY, + parent TEXT NOT NULL DEFAULT 0, + + libelle TEXT NOT NULL, + + position INTEGER NOT NULL, -- position actif/passif/charge/produit + plan_comptable INTEGER NOT NULL DEFAULT 1 -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur +); + +CREATE INDEX compta_comptes_parent ON compta_comptes (parent); + +CREATE TABLE compta_comptes_bancaires +-- Comptes bancaires +( + id TEXT PRIMARY KEY, + + banque TEXT NOT NULL, + + iban TEXT, + bic TEXT, + + FOREIGN KEY(id) REFERENCES compta_comptes(id) +); + +CREATE TABLE compta_journal +-- Journal des opérations comptables +( + id INTEGER PRIMARY KEY, + + libelle TEXT NOT NULL, + remarques TEXT, + numero_piece TEXT, -- N° de pièce comptable + + montant REAL, + + date TEXT DEFAULT CURRENT_DATE, + moyen_paiement TEXT DEFAULT NULL, + numero_cheque TEXT DEFAULT NULL, + + compte_debit INTEGER, -- N° du compte dans le plan + compte_credit INTEGER, -- N° du compte dans le plan + + id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL) + id_auteur INTEGER NOT NULL, + id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple) + + FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code), + FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id), + FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id), + FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id), + FOREIGN KEY(id_auteur) REFERENCES membres(id), + FOREIGN KEY(id_categorie) REFERENCES compta_categories(id) +); + +CREATE INDEX compta_operations_exercice ON compta_journal (id_exercice); +CREATE INDEX compta_operations_date ON compta_journal (date); +CREATE INDEX compta_operations_comptes ON compta_journal (compte_debit, compte_credit); +CREATE INDEX compta_operations_auteur ON compta_journal (id_auteur); + +CREATE TABLE compta_moyens_paiement +-- Moyens de paiement +( + code TEXT PRIMARY KEY, + nom TEXT +); + +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement'); + +CREATE TABLE compta_categories +-- Catégories pour simplifier le plan comptable +( + id INTEGER PRIMARY KEY, + type INTEGER DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface) + + intitule TEXT NOT NULL, + description TEXT, + + compte TEXT NOT NULL, -- Compte affecté par cette catégorie + + FOREIGN KEY(compte) REFERENCES compta_comptes(id) +); \ No newline at end of file diff --git a/include/data/0.4.3.sql b/include/data/0.4.3.sql new file mode 100644 index 0000000..2e73272 --- /dev/null +++ b/include/data/0.4.3.sql @@ -0,0 +1,79 @@ +DROP TABLE compta_exercices; + +CREATE TABLE compta_exercices +-- Exercices +( + id INTEGER PRIMARY KEY, + + libelle TEXT NOT NULL, + + debut TEXT NOT NULL DEFAULT CURRENT_DATE, + fin TEXT NULL DEFAULT NULL, + + cloture INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO compta_exercices (libelle, debut, fin, cloture) + VALUES ( + 'Premier exercice', + (CASE WHEN + (SELECT strftime('%Y-01-01', date) FROM compta_journal ORDER BY date ASC LIMIT 1) + IS NOT NULL THEN (SELECT strftime('%Y-01-01', date) FROM compta_journal ORDER BY date ASC LIMIT 1) + ELSE strftime('%Y-01-01', 'now') END + ), + (CASE WHEN + (SELECT strftime('%Y-12-31', date) FROM compta_journal ORDER BY date DESC LIMIT 1) + IS NOT NULL THEN (SELECT strftime('%Y-12-31', date) FROM compta_journal ORDER BY date DESC LIMIT 1) + ELSE strftime('%Y-12-31', 'now') END + ), + 0 + ); + +BEGIN; +ALTER TABLE compta_journal RENAME TO old_compta_journal; +DROP INDEX compta_operations_exercice; +DROP INDEX compta_operations_date; +DROP INDEX compta_operations_comptes; +DROP INDEX compta_operations_auteur; + +CREATE TABLE compta_journal +-- Journal des opérations comptables +( + id INTEGER PRIMARY KEY, + + libelle TEXT NOT NULL, + remarques TEXT, + numero_piece TEXT, -- N° de pièce comptable + + montant REAL, + + date TEXT DEFAULT CURRENT_DATE, + moyen_paiement TEXT DEFAULT NULL, + numero_cheque TEXT DEFAULT NULL, + + compte_debit INTEGER, -- N° du compte dans le plan + compte_credit INTEGER, -- N° du compte dans le plan + + id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL) + id_auteur INTEGER NULL, + id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple) + + FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code), + FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id), + FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id), + FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id), + FOREIGN KEY(id_auteur) REFERENCES membres(id), + FOREIGN KEY(id_categorie) REFERENCES compta_categories(id) +); + +CREATE INDEX compta_operations_exercice ON compta_journal (id_exercice); +CREATE INDEX compta_operations_date ON compta_journal (date); +CREATE INDEX compta_operations_comptes ON compta_journal (compte_debit, compte_credit); +CREATE INDEX compta_operations_auteur ON compta_journal (id_auteur); + +INSERT INTO compta_journal SELECT * FROM old_compta_journal; + +UPDATE compta_journal SET id_exercice = 1; + +DROP TABLE old_compta_journal; +END; \ No newline at end of file diff --git a/include/data/0.6.0.sql b/include/data/0.6.0.sql new file mode 100644 index 0000000..543e50e --- /dev/null +++ b/include/data/0.6.0.sql @@ -0,0 +1,110 @@ +CREATE TABLE cotisations +-- Types de cotisations et activités +( + id INTEGER PRIMARY KEY, + id_categorie_compta INTEGER NULL, -- NULL si le type n'est pas associé automatiquement à la compta + + intitule TEXT NOT NULL, + description TEXT NULL, + montant REAL NOT NULL, + + duree INTEGER NULL, -- En jours + debut TEXT NULL, -- timestamp + fin TEXT NULL, + + FOREIGN KEY (id_categorie_compta) REFERENCES compta_categories (id) +); + +CREATE TABLE cotisations_membres +-- Enregistrement des cotisations et activités +( + id INTEGER NOT NULL PRIMARY KEY, + id_membre INTEGER NOT NULL REFERENCES membres (id), + id_cotisation INTEGER NOT NULL REFERENCES cotisations (id), + + date TEXT NOT NULL DEFAULT CURRENT_DATE +); + +CREATE UNIQUE INDEX cm_unique ON cotisations_membres (id_membre, id_cotisation, date); + +CREATE TABLE membres_operations +-- Liaision des enregistrement des paiements en compta +( + id_membre INTEGER NOT NULL REFERENCES membres (id), + id_operation INTEGER NOT NULL REFERENCES compta_journal (id), + id_cotisation INTEGER NULL REFERENCES cotisations_membres (id), + + PRIMARY KEY (id_membre, id_operation) +); + +CREATE TABLE rappels +-- Rappels de devoir renouveller une cotisation +( + id INTEGER PRIMARY KEY, + id_cotisation INTEGER NOT NULL REFERENCES cotisations (id), + + delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel + + sujet TEXT NOT NULL, + texte TEXT NOT NULL +); + +CREATE TABLE rappels_envoyes +-- Enregistrement des rappels envoyés à qui et quand +( + id INTEGER PRIMARY KEY, + + id_membre INTEGER NOT NULL REFERENCES membres (id), + id_cotisation INTEGER NOT NULL REFERENCES cotisations (id), + + date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + + media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre +); + +CREATE TABLE plugins +-- Plugins / extensions +( + id TEXT PRIMARY KEY, + officiel INTEGER NOT NULL DEFAULT 0, + nom TEXT NOT NULL, + description TEXT, + auteur TEXT, + url TEXT, + version TEXT NOT NULL, + menu INTEGER NOT NULL DEFAULT 0, + config TEXT +); + +-- Mise à jour des catégories + +CREATE TABLE membres_categories_tmp +-- Catégories de membres +( + id INTEGER PRIMARY KEY, + nom TEXT, + description TEXT, + + droit_wiki INT DEFAULT 1, + droit_membres INT DEFAULT 1, + droit_compta INT DEFAULT 1, + droit_inscription INT DEFAULT 0, + droit_connexion INT DEFAULT 1, + droit_config INT DEFAULT 0, + cacher INT DEFAULT 0, + + id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id) +); + +-- Remise des anciennes infos +INSERT INTO membres_categories_tmp SELECT id, nom, description, droit_wiki, droit_membres, + droit_compta, droit_inscription, droit_connexion, droit_config, cacher, NULL FROM membres_categories; + +-- Suppression de l'ancienne table et renommage de la nouvelle +DROP TABLE membres_categories; +ALTER TABLE membres_categories_tmp RENAME TO membres_categories; + +-- Ajout désactivation compte +ALTER TABLE compta_comptes ADD COLUMN desactive INTEGER NOT NULL DEFAULT 0; + +PRAGMA foreign_keys = ON; \ No newline at end of file diff --git a/include/data/categories_comptables.sql b/include/data/categories_comptables.sql new file mode 100644 index 0000000..80d0d48 --- /dev/null +++ b/include/data/categories_comptables.sql @@ -0,0 +1,22 @@ +INSERT INTO "compta_categories" VALUES(NULL,-1,'Prestations de service','','604'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Achat de marchandises à vendre','Marchandises destinées à être revendues en l''état.','607'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Achat de fournitures consommables','','6068'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Publicité et relations publiques','','623'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Frais de déplacement des membres','Billet SNCF, remboursement de frais kilométrique, etc.','625'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Locations','Locations versées pour un local ou du matériel.','613'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Fournitures non stockables : eau, électricité...','Facture d''eau, d''opérateur électrique, etc.','6061'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Fournitures administratives','Cartouches d''encre, papier, matériel bureautique, etc.','6064'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Frais d''actes et de contentieux','Insertion au Journal Officiel, frais de justice, etc.','6227'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Frais postaux et télécommunications','Facture d''accès à Internet, timbres, etc.','626'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Prime d''assurance','','616'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Services bancaires','','627'); +INSERT INTO "compta_categories" VALUES(NULL,-1,'Divers','','658'); + +INSERT INTO "compta_categories" VALUES(NULL,1,'Vente de produits finis','Vente de produits fabriqués par l''association.','701'); +INSERT INTO "compta_categories" VALUES(NULL,1,'Prestation de service','','706'); +INSERT INTO "compta_categories" VALUES(NULL,1,'Revente de marchandises','','707'); +INSERT INTO "compta_categories" VALUES(NULL,1,'Manifestations diverses','Revenus provenant de manifestations au profit de l''association : droit d''entrée, location d''emplacement en vide grenier, ventes, etc.','7780'); +INSERT INTO "compta_categories" VALUES(NULL,1,'Cotisations','','756'); +INSERT INTO "compta_categories" VALUES(NULL,1,'Dons et collectes','','754'); +INSERT INTO "compta_categories" VALUES(NULL,1,'Subventions','','740'); +INSERT INTO "compta_categories" VALUES(NULL,1,'Divers','','758'); diff --git a/include/data/champs_membres.ini b/include/data/champs_membres.ini new file mode 100644 index 0000000..d68e352 --- /dev/null +++ b/include/data/champs_membres.ini @@ -0,0 +1,129 @@ +; Ce fichier contient la configuration par défaut des champs des fiches membres. +; La configuration est ensuite enregistrée au format INI dans la table +; config de la base de données. +; +; Syntaxe : +; +; [nom_du_champ] ; Nom unique du champ, ne peut contenir que des lettres et des tirets bas +; type = text +; title = "Super champ trop cool" +; mandatory = true +; editable = false +; +; Description des options possibles pour chaque champ : +; +; type: (défaut: text) OBLIGATOIRE +; certains types gérés par de HTML5 : +; text, number, date, datetime, url, email, checkbox, file, password, tel +; champs spécifiques : +; - country = sélecteur de pays +; - textarea = texte multi lignes +; - multiple = multiples cases à cocher (jusqu'à 32, binaire) +; - select = un choix parmis plusieurs +; title: OBLIGATOIRE +; Titre du champ +; help: +; Texte d'aide sur les fiches membres +; options[]: +; pour définir les options d'un champ de type select ou multiple +; editable: +; true = modifiable par le membre +; false = modifiable uniquement par un admin (défaut) +; mandatory: +; true = obligatoire, la fiche membre ne pourra être enregistrée si ce champ est vide +; false = facultatif (défaut) +; private: +; true = non visible par le membre lui-même +; false = visible par le membre (défaut) +; list_row: +; Si absent ou zéro ('0') ou false, ce champ n'apparaîtra pas dans la liste des membres +; Si présent et un chiffre supérieur à 0, alors le champ apparaîtra dans la liste des membres +; dans l'ordre défini par le chiffre (si nom est à 2 et email à 1, alors email sera +; la première colonne et nom la seconde) +; install: +; true = sera ajouté aux fiches membres à l'installation +; false = sera seulement présent dans les champs supplémentaires possibles (défaut) + +[nom] +type = text +title = "Nom & prénom" +mandatory = true +install = true +editable = true +list_row = 1 + +[email] +; ce champ est obligatoirement présent et de type 'email' +type = email +title = "Adresse E-Mail" +mandatory = true +install = true +editable = true + +[passe] +; ce champ est obligatoirement présent et de type 'password' +; le titre ne peut être modifié +type = password +mandatory = true +install = true +editable = true + +[adresse] +type = textarea +title = "Adresse postale" +help = "Indiquer ici le numéro, le type de voie, etc." +install = true +editable = true + +[code_postal] +type = text +title = "Code postal" +install = true +editable = true +list_row = 2 + +[ville] +type = text +title = "Ville" +install = true +editable = true +list_row = 3 + +[pays] +type = country +title = "Pays" +install = true +editable = true + +[telephone] +type = tel +title = "Numéro de téléphone" +install = true +editable = true + +[lettre_infos] +type = checkbox +title = "Inscription à la lettre d'information" +install = true +editable = true + +[groupe_travail] +type = multiple +title = "Groupes de travail" +editable = false +options[] = "Télécoms" +options[] = "Trésorerie" +options[] = "Relations publiques" +options[] = "Communication presse" +options[] = "Organisation d'événements" + +[date_naissance] +type = date +title = "Date de naissance" +editable = true + +[notes] +type = textarea +title = "Notes" +editable = false +private = true diff --git a/include/data/plan_comptable.json b/include/data/plan_comptable.json new file mode 100644 index 0000000..b249b85 --- /dev/null +++ b/include/data/plan_comptable.json @@ -0,0 +1,1718 @@ +{ + "1": { + "code": 1, + "nom": "Classe 1 \u2014 Comptes de capitaux (Fonds propres, emprunts et dettes assimil\u00e9s)", + "parent": 0, + "position": 1 + }, + "10": { + "code": 10, + "nom": "FONDS ASSOCIATIFS ET R\u00c9SERVES", + "parent": 1, + "position": 1 + }, + "102": { + "code": 102, + "nom": "Fonds associatif sans droit de reprise", + "parent": 10, + "position": 1 + }, + "1021": { + "code": 1021, + "nom": "Valeur du patrimoine int\u00e9gr\u00e9", + "parent": 102, + "position": 1 + }, + "1022": { + "code": 1022, + "nom": "Fonds statutaire", + "parent": 102, + "position": 1 + }, + "1024": { + "code": 1024, + "nom": "Apports sans droit de reprise", + "parent": 102, + "position": 1 + }, + "103": { + "code": 103, + "nom": "Fonds associatif avec droit de reprise", + "parent": 10, + "position": 1 + }, + "1034": { + "code": 1034, + "nom": "Apports avec droit de reprise", + "parent": 103, + "position": 1 + }, + "105": { + "code": 105, + "nom": "\u00c9carts de r\u00e9\u00e9valuation", + "parent": 10, + "position": 1 + }, + "106": { + "code": 106, + "nom": "R\u00e9serves", + "parent": 10, + "position": 1 + }, + "1063": { + "code": 1063, + "nom": "R\u00e9serves statutaires ou contractuelles", + "parent": 106, + "position": 1 + }, + "1064": { + "code": 1064, + "nom": "R\u00e9serves r\u00e9glement\u00e9es", + "parent": 106, + "position": 1 + }, + "1068": { + "code": 1068, + "nom": "Autres r\u00e9serves (dont r\u00e9serves pour projet associatif)", + "parent": 106, + "position": 1 + }, + "11": { + "code": 11, + "nom": "REPORT \u00c0 NOUVEAU", + "parent": 1, + "position": 1 + }, + "110": { + "code": 110, + "nom": "Report \u00e0 nouveau (Solde cr\u00e9diteur)", + "parent": 11, + "position": 1 + }, + "119": { + "code": 119, + "nom": "Report \u00e0 nouveau (Solde d\u00e9biteur)", + "parent": 11, + "position": 1 + }, + "12": { + "code": 12, + "nom": "R\u00c9SULTAT NET DE L'EXERCICE", + "parent": 1, + "position": 1 + }, + "120": { + "code": 120, + "nom": "R\u00e9sultat de l'exercice (exc\u00e9dent)", + "parent": 12, + "position": 1 + }, + "129": { + "code": 129, + "nom": "R\u00e9sultat de l'exercice (d\u00e9ficit)", + "parent": 12, + "position": 1 + }, + "13": { + "code": 13, + "nom": "SUBVENTIONS D'INVESTISSEMENT AFFECT\u00c9ES A DES BIENS NON RENOUVELABLES", + "parent": 1, + "position": 1 + }, + "131": { + "code": 131, + "nom": "Subventions d'investissement (renouvelables)", + "parent": 13, + "position": 1 + }, + "139": { + "code": 139, + "nom": "Subventions d'investissement inscrites au compte de r\u00e9sultat", + "parent": 13, + "position": 1 + }, + "14": { + "code": 14, + "nom": "PROVISIONS REGLEMENT\u00c9ES", + "parent": 1, + "position": 1 + }, + "15": { + "code": 15, + "nom": "PROVISIONS", + "parent": 1, + "position": 1 + }, + "151": { + "code": 151, + "nom": "Provisions pour risques", + "parent": 15, + "position": 1 + }, + "157": { + "code": 157, + "nom": "Provisions pour charges \u00e0 r\u00e9partir sur plusieurs exercices", + "parent": 15, + "position": 1 + }, + "158": { + "code": 158, + "nom": "Autres provisions pour charges", + "parent": 15, + "position": 1 + }, + "16": { + "code": 16, + "nom": "EMPRUNTS ET DETTES ASSIMIL\u00c9ES", + "parent": 1, + "position": 1 + }, + "164": { + "code": 164, + "nom": "Emprunts aupr\u00e8s des \u00e9tablissements de cr\u00e9dits", + "parent": 16, + "position": 1 + }, + "165": { + "code": 165, + "nom": "D\u00e9p\u00f4ts et cautionnements re\u00e7us", + "parent": 16, + "position": 1 + }, + "167": { + "code": 167, + "nom": "Emprunts et dettes assorties de conditions particuli\u00e8res", + "parent": 16, + "position": 1 + }, + "168": { + "code": 168, + "nom": "Autres emprunts et dettes assimil\u00e9s", + "parent": 16, + "position": 1 + }, + "17": { + "code": 17, + "nom": "DETTES RATTACH\u00c9ES \u00c0 DES PARTICIPATIONS", + "parent": 1, + "position": 1 + }, + "18": { + "code": 18, + "nom": "COMPTES DE LIAISON DES \u00c9TABLISSEMENTS", + "parent": 1, + "position": 1 + }, + "181": { + "code": 181, + "nom": "Apports permanents entre si\u00e8ge social et \u00e9tablissements", + "parent": 18, + "position": 1 + }, + "185": { + "code": 185, + "nom": "Biens et prestations de services \u00e9chang\u00e9s entre \u00e9tablissements et si\u00e8ge social", + "parent": 18, + "position": 1 + }, + "186": { + "code": 186, + "nom": "Biens et prestations de services \u00e9chang\u00e9s entre \u00e9tablissements (charges)", + "parent": 18, + "position": 1 + }, + "187": { + "code": 187, + "nom": "Biens et prestations de services \u00e9chang\u00e9s entre \u00e9tablissements (produits)", + "parent": 18, + "position": 1 + }, + "19": { + "code": 19, + "nom": "FONDS D\u00c9DI\u00c9S", + "parent": 1, + "position": 1 + }, + "194": { + "code": 194, + "nom": "Fonds d\u00e9di\u00e9s sur subventions de fonctionnement", + "parent": 19, + "position": 1 + }, + "195": { + "code": 195, + "nom": "Fonds d\u00e9di\u00e9s sur dons manuels affect\u00e9s", + "parent": 19, + "position": 1 + }, + "197": { + "code": 197, + "nom": "Fonds d\u00e9di\u00e9s sur legs et donations affect\u00e9s", + "parent": 19, + "position": 1 + }, + "198": { + "code": 198, + "nom": "Exc\u00e9dent disponible apr\u00e8s affectation au projet associatif", + "parent": 19, + "position": 1 + }, + "199": { + "code": 199, + "nom": "Reprise des fonds affect\u00e9s au projet associatif", + "parent": 19, + "position": 1 + }, + "2": { + "code": 2, + "nom": "Classe 2 \u2014 Comptes d'immobilisations", + "parent": 0, + "position": 2 + }, + "20": { + "code": 20, + "nom": "IMMOBILISATIONS INCORPORELLES", + "parent": 2, + "position": 2 + }, + "200": { + "code": 200, + "nom": "Immobilisations incorporelles", + "parent": 20, + "position": 2 + }, + "21": { + "code": 21, + "nom": "IMMOBILISATIONS CORPORELLES", + "parent": 2, + "position": 2 + }, + "210": { + "code": 210, + "nom": "Investissements", + "parent": 21, + "position": 2 + }, + "22": { + "code": 22, + "nom": "IMMOBILISATIONS GREV\u00c9ES DE DROITS", + "parent": 2, + "position": 2 + }, + "228": { + "code": 228, + "nom": "Immobilisations grev\u00e9es de droits", + "parent": 22, + "position": 2 + }, + "229": { + "code": 229, + "nom": "Droits des propri\u00e9taires", + "parent": 22, + "position": 2 + }, + "23": { + "code": 23, + "nom": "IMMOBILISATIONS EN COURS", + "parent": 2, + "position": 2 + }, + "231": { + "code": 231, + "nom": "Immobilisations corporelles en cours", + "parent": 23, + "position": 2 + }, + "238": { + "code": 238, + "nom": "Avances et acomptes vers\u00e9s sur commande d'immobilisations corporelles", + "parent": 23, + "position": 2 + }, + "26": { + "code": 26, + "nom": "PARTICIPATIONS ET CR\u00c9ANCES RATTACH\u00c9ES A DES PARTICIPATIONS", + "parent": 2, + "position": 2 + }, + "261": { + "code": 261, + "nom": "Titres de participation", + "parent": 26, + "position": 2 + }, + "27": { + "code": 27, + "nom": "AUTRES IMMOBILISATIONS FINANCI\u00c8RES", + "parent": 2, + "position": 2 + }, + "270": { + "code": 270, + "nom": "Participations financi\u00e8res", + "parent": 27, + "position": 2 + }, + "275": { + "code": 275, + "nom": "D\u00e9p\u00f4ts et cautionnements vers\u00e9s", + "parent": 27, + "position": 2 + }, + "28": { + "code": 28, + "nom": "AMORTISSEMENTS DES IMMOBILISATIONS", + "parent": 2, + "position": 2 + }, + "280": { + "code": 280, + "nom": "Amortissements des immobilisations incorporelles", + "parent": 28, + "position": 2 + }, + "281": { + "code": 281, + "nom": "Amortissements des immobilisations corporelles", + "parent": 28, + "position": 2 + }, + "29": { + "code": 29, + "nom": "D\u00c9PR\u00c9CIATION DES IMMOBILISATIONS", + "parent": 2, + "position": 2 + }, + "290": { + "code": 290, + "nom": "D\u00e9pr\u00e9ciation des immobilisations incorporelles", + "parent": 29, + "position": 2 + }, + "291": { + "code": 291, + "nom": "D\u00e9pr\u00e9ciation des immobilisations corporelles", + "parent": 29, + "position": 2 + }, + "3": { + "code": 3, + "nom": "Classe 3 \u2014 Comptes de stocks", + "parent": 0, + "position": 2 + }, + "31": { + "code": 31, + "nom": "MATIERES PREMIERES ET FOURNITURES", + "parent": 3, + "position": 2 + }, + "311": { + "code": 311, + "nom": "Mati\u00e8res", + "parent": 31, + "position": 2 + }, + "317": { + "code": 317, + "nom": "Fournitures", + "parent": 31, + "position": 2 + }, + "32": { + "code": 32, + "nom": "AUTRES APPROVISIONNEMENTS", + "parent": 3, + "position": 2 + }, + "321": { + "code": 321, + "nom": "Mati\u00e8res consommables", + "parent": 32, + "position": 2 + }, + "322": { + "code": 322, + "nom": "Fournitures consommables", + "parent": 32, + "position": 2 + }, + "33": { + "code": 33, + "nom": "EN-COURS DE PRODUCTION DE BIENS", + "parent": 3, + "position": 2 + }, + "331": { + "code": 331, + "nom": "Produits en cours", + "parent": 33, + "position": 2 + }, + "335": { + "code": 335, + "nom": "Travaux en cours", + "parent": 33, + "position": 2 + }, + "34": { + "code": 34, + "nom": "EN-COURS DE PRODUCTION DE SERVICES", + "parent": 3, + "position": 2 + }, + "35": { + "code": 35, + "nom": "STOCKS DE PRODUITS", + "parent": 3, + "position": 2 + }, + "351": { + "code": 351, + "nom": "Produits interm\u00e9diaires", + "parent": 35, + "position": 2 + }, + "355": { + "code": 355, + "nom": "Produits finis", + "parent": 35, + "position": 2 + }, + "358": { + "code": 358, + "nom": "Produits r\u00e9siduels", + "parent": 35, + "position": 2 + }, + "3581": { + "code": 3581, + "nom": "D\u00e9chets", + "parent": 358, + "position": 2 + }, + "3585": { + "code": 3585, + "nom": "Rebuts", + "parent": 358, + "position": 2 + }, + "3586": { + "code": 3586, + "nom": "Mati\u00e8re de r\u00e9cup\u00e9ration", + "parent": 358, + "position": 2 + }, + "37": { + "code": 37, + "nom": "STOCKS DE MARCHANDISES", + "parent": 3, + "position": 2 + }, + "370": { + "code": 370, + "nom": "Autres stocks de marchandises", + "parent": 37, + "position": 2 + }, + "39": { + "code": 39, + "nom": "PROVISIONS POUR DEPRECIATION DES STOCKS ET EN-COURS", + "parent": 3, + "position": 2 + }, + "391": { + "code": 391, + "nom": "Provisions pour d\u00e9pr\u00e9ciation des mati\u00e8res premi\u00e8res et fournitures", + "parent": 39, + "position": 2 + }, + "4": { + "code": 4, + "nom": "Classe 4 \u2014 Comptes de tiers", + "parent": 0, + "position": 3 + }, + "40": { + "code": 40, + "nom": "FOURNISSEURS ET COMPTES RATTACH\u00c9S", + "parent": 4, + "position": 1 + }, + "401": { + "code": 401, + "nom": "Fournisseurs", + "parent": 40, + "position": 1 + }, + "4010": { + "code": 4010, + "nom": "Autres fournisseurs", + "parent": 401, + "position": 1 + }, + "408": { + "code": 408, + "nom": "Fournisseurs - Factures non parvenues", + "parent": 40, + "position": 1 + }, + "409": { + "code": 409, + "nom": "Avances aux fournisseurs", + "parent": 40, + "position": 2 + }, + "41": { + "code": 41, + "nom": "USAGERS ET COMPTES RATTACH\u00c9S", + "parent": 4, + "position": 2 + }, + "411": { + "code": 411, + "nom": "Usagers", + "parent": 41, + "position": 2 + }, + "4110": { + "code": 4110, + "nom": "Autres usagers", + "parent": 411, + "position": 2 + }, + "419": { + "code": 419, + "nom": "Avances aux usagers", + "parent": 41, + "position": 1 + }, + "42": { + "code": 42, + "nom": "PERSONNEL ET COMPTES RATTACH\u00c9S", + "parent": 4, + "position": 1 + }, + "421": { + "code": 421, + "nom": "Personnel - R\u00e9mun\u00e9rations dues", + "parent": 42, + "position": 1 + }, + "4210": { + "code": 4210, + "nom": "Autres membres du personnel", + "parent": 421, + "position": 1 + }, + "425": { + "code": 425, + "nom": "Personnel - Avances et acomptes", + "parent": 42, + "position": 2 + }, + "428": { + "code": 428, + "nom": "Personnel - Charges \u00e0 payer et produits \u00e0 recevoir", + "parent": 42, + "position": 1 + }, + "43": { + "code": 43, + "nom": "S\u00c9CURIT\u00c9 SOCIALE ET AUTRES ORGANISMES SOCIAUX", + "parent": 4, + "position": 1 + }, + "430": { + "code": 430, + "nom": "Dettes et cr\u00e9dits envers les organismes sociaux", + "parent": 43, + "position": 1 + }, + "431": { + "code": 431, + "nom": "S\u00e9curit\u00e9 sociale", + "parent": 43, + "position": 1 + }, + "437": { + "code": 437, + "nom": "Autres organismes sociaux", + "parent": 43, + "position": 1 + }, + "4372": { + "code": 4372, + "nom": "Mutuelles", + "parent": 437, + "position": 1 + }, + "4373": { + "code": 4373, + "nom": "Caisse de retraite et de pr\u00e9voyance", + "parent": 437, + "position": 1 + }, + "4374": { + "code": 4374, + "nom": "Caisse d'allocations de ch\u00f4mage - P\u00f4le emploi", + "parent": 437, + "position": 1 + }, + "4375": { + "code": 4375, + "nom": "AGESSA", + "parent": 437, + "position": 1 + }, + "4378": { + "code": 4378, + "nom": "Autres organismes sociaux - Divers", + "parent": 437, + "position": 1 + }, + "438": { + "code": 438, + "nom": "Organismes sociaux - Charges \u00e0 payer et produits \u00e0 recevoir", + "parent": 43, + "position": 1 + }, + "4382": { + "code": 4382, + "nom": "Charges sociales sur cong\u00e9s \u00e0 payer", + "parent": 438, + "position": 1 + }, + "4386": { + "code": 4386, + "nom": "Autres charges \u00e0 payer", + "parent": 438, + "position": 1 + }, + "4387": { + "code": 4387, + "nom": "Produits \u00e0 recevoir", + "parent": 438, + "position": 2 + }, + "439": { + "code": 439, + "nom": "Avances aupr\u00e8s des organismes sociaux", + "parent": 43, + "position": 1 + }, + "44": { + "code": 44, + "nom": "\u00c9TAT ET AUTRES COLLECTIVIT\u00c9S PUBLIQUES", + "parent": 4, + "position": 2 + }, + "441": { + "code": 441, + "nom": "\u00c9tat - Subventions \u00e0 recevoir", + "parent": 44, + "position": 2 + }, + "4411": { + "code": 4411, + "nom": "Subventions d'investissement", + "parent": 441, + "position": 2 + }, + "4417": { + "code": 4417, + "nom": "Subventions d'exploitation", + "parent": 441, + "position": 2 + }, + "4418": { + "code": 4418, + "nom": "Subventions d'\u00e9quilibre", + "parent": 441, + "position": 2 + }, + "4419": { + "code": 4419, + "nom": "Avances sur subventions", + "parent": 441, + "position": 2 + }, + "442": { + "code": 442, + "nom": "\u00c9tat - Imp\u00f4ts et taxes recouvrables sur des tiers", + "parent": 44, + "position": 1 + }, + "444": { + "code": 444, + "nom": "\u00c9tat - Imp\u00f4ts sur les b\u00e9n\u00e9fices", + "parent": 44, + "position": 2 + }, + "445": { + "code": 445, + "nom": "\u00c9tat - Taxes sur le chiffre d'affaires", + "parent": 44, + "position": 2 + }, + "4455": { + "code": 4455, + "nom": "Taxes sur le chiffre d'affaires \u00e0 d\u00e9caisser", + "parent": 445, + "position": 2 + }, + "44551": { + "code": 44551, + "nom": "TVA \u00e0 d\u00e9caisser", + "parent": 4455, + "position": 2 + }, + "44558": { + "code": 44558, + "nom": "Taxes assimil\u00e9es \u00e0 la TVA", + "parent": 4455, + "position": 2 + }, + "4456": { + "code": 4456, + "nom": "Taxes sur le chiffre d'affaires d\u00e9ductibles", + "parent": 445, + "position": 2 + }, + "44562": { + "code": 44562, + "nom": "TVA sur immobilisations", + "parent": 4456, + "position": 2 + }, + "44566": { + "code": 44566, + "nom": "TVA sur autres biens et services", + "parent": 4456, + "position": 2 + }, + "4457": { + "code": 4457, + "nom": "Taxes sur le chiffre d'affaires collect\u00e9es par l'association", + "parent": 445, + "position": 2 + }, + "4458": { + "code": 4458, + "nom": "Taxes sur le chiffre d'affaires \u00e0 r\u00e9gulariser ou en attente", + "parent": 445, + "position": 2 + }, + "44581": { + "code": 44581, + "nom": "Acomptes - R\u00e9gime simplifi\u00e9 d'imposition", + "parent": 4458, + "position": 2 + }, + "44582": { + "code": 44582, + "nom": "Acomptes - R\u00e9gime du forfait", + "parent": 4458, + "position": 2 + }, + "44583": { + "code": 44583, + "nom": "Remboursement de taxes sur le chiffre d'affaires demand\u00e9", + "parent": 4458, + "position": 2 + }, + "44584": { + "code": 44584, + "nom": "TVA r\u00e9cup\u00e9r\u00e9e d'avance", + "parent": 4458, + "position": 2 + }, + "44586": { + "code": 44586, + "nom": "Taxes sur le chiffre d'affaires sur factures non parvenues", + "parent": 4458, + "position": 2 + }, + "44587": { + "code": 44587, + "nom": "Taxes sur le chiffre d'affaires sur factures \u00e0 \u00e9tablir", + "parent": 4458, + "position": 2 + }, + "447": { + "code": 447, + "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s", + "parent": 44, + "position": 1 + }, + "4471": { + "code": 4471, + "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s sur r\u00e9mun\u00e9rations (Administration des imp\u00f4ts)", + "parent": 447, + "position": 1 + }, + "44711": { + "code": 44711, + "nom": "Taxe sur les salaires", + "parent": 4471, + "position": 1 + }, + "44713": { + "code": 44713, + "nom": "Participation des employeurs \u00e0 la formation professionnelle continue", + "parent": 4471, + "position": 1 + }, + "44714": { + "code": 44714, + "nom": "Cotisation par d\u00e9faut d'investissement obligatoire dans la construction", + "parent": 4471, + "position": 1 + }, + "44718": { + "code": 44718, + "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s", + "parent": 4471, + "position": 1 + }, + "4473": { + "code": 4473, + "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s sur r\u00e9mun\u00e9rations (Autres organismes)", + "parent": 447, + "position": 1 + }, + "44733": { + "code": 44733, + "nom": "Participation des employeurs \u00e0 la formation professionnelle continue", + "parent": 4473, + "position": 1 + }, + "44734": { + "code": 44734, + "nom": "Participation des employeurs \u00e0 l'effort de construction (versements \u00e0 fonds perdus)", + "parent": 4473, + "position": 1 + }, + "4475": { + "code": 4475, + "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s (Administration des imp\u00f4ts)", + "parent": 447, + "position": 1 + }, + "4477": { + "code": 4477, + "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s (Autres organismes)", + "parent": 447, + "position": 1 + }, + "448": { + "code": 448, + "nom": "\u00c9tat - Charges \u00e0 payer et produits \u00e0 recevoir", + "parent": 44, + "position": 1 + }, + "4482": { + "code": 4482, + "nom": "Charges fiscales sur cong\u00e9s \u00e0 payer", + "parent": 448, + "position": 1 + }, + "4486": { + "code": 4486, + "nom": "Autres charges \u00e0 payer", + "parent": 448, + "position": 1 + }, + "4487": { + "code": 4487, + "nom": "Produits \u00e0 recevoir", + "parent": 448, + "position": 2 + }, + "449": { + "code": 449, + "nom": "Avances aupr\u00e8s de l'\u00e9tat et des collectivit\u00e9s publiques", + "parent": 44, + "position": 1 + }, + "45": { + "code": 45, + "nom": "CONF\u00c9D\u00c9RATION, F\u00c9D\u00c9RATION, UNIONS ET ASSOCIATIONS AFFILI\u00c9ES", + "parent": 4, + "position": 3 + }, + "451": { + "code": 451, + "nom": "Conf\u00e9d\u00e9ration, f\u00e9d\u00e9ration et associations affili\u00e9es - Compte courant", + "parent": 45, + "position": 3 + }, + "455": { + "code": 455, + "nom": "Soci\u00e9taires - Comptes courants", + "parent": 45, + "position": 3 + }, + "46": { + "code": 46, + "nom": "D\u00c9BITEURS DIVERS ET CR\u00c9DITEURS DIVERS", + "parent": 4, + "position": 3 + }, + "467": { + "code": 467, + "nom": "Autres comptes d\u00e9biteurs et cr\u00e9diteurs", + "parent": 46, + "position": 3 + }, + "468": { + "code": 468, + "nom": "Divers - Charges \u00e0 payer et produits \u00e0 recevoir", + "parent": 46, + "position": 3 + }, + "4686": { + "code": 4686, + "nom": "Charges \u00e0 payer", + "parent": 468, + "position": 1 + }, + "4687": { + "code": 4687, + "nom": "Produits \u00e0 recevoir", + "parent": 468, + "position": 2 + }, + "47": { + "code": 47, + "nom": "COMPTES TRANSITOIRES OU D'ATTENTE", + "parent": 4, + "position": 3 + }, + "471": { + "code": 471, + "nom": "Recettes \u00e0 classer", + "parent": 47, + "position": 1 + }, + "472": { + "code": 472, + "nom": "D\u00e9penses \u00e0 classer et \u00e0 r\u00e9gulariser", + "parent": 47, + "position": 2 + }, + "48": { + "code": 48, + "nom": "COMPTES DE R\u00c9GULARISATION", + "parent": 4, + "position": 3 + }, + "481": { + "code": 481, + "nom": "Charges \u00e0 r\u00e9partir sur plusieurs exercices", + "parent": 48, + "position": 2 + }, + "486": { + "code": 486, + "nom": "Charges constat\u00e9es d'avance", + "parent": 48, + "position": 2 + }, + "487": { + "code": 487, + "nom": "Produits constat\u00e9s d'avance", + "parent": 48, + "position": 1 + }, + "49": { + "code": 49, + "nom": "DEPRECIATION DES COMPTES DE TIERS", + "parent": 4, + "position": 2 + }, + "491": { + "code": 491, + "nom": "D\u00e9pr\u00e9ciation des comptes clients", + "parent": 49, + "position": 2 + }, + "496": { + "code": 496, + "nom": "D\u00e9pr\u00e9ciation des comptes d\u00e9biteurs divers", + "parent": 49, + "position": 2 + }, + "5": { + "code": 5, + "nom": "Classe 5 \u2014 Comptes financiers", + "parent": 0, + "position": 2 + }, + "50": { + "code": 50, + "nom": "VALEURS MOBILI\u00c8RES DE PLACEMENT", + "parent": 5, + "position": 2 + }, + "51": { + "code": 51, + "nom": "BANQUES, \u00c9TABLISSEMENTS FINANCIERS ET ASSIMIL\u00c9S", + "parent": 5, + "position": 2 + }, + "512": { + "code": 512, + "nom": "Banques", + "parent": 51, + "position": 2 + }, + "53": { + "code": 53, + "nom": "CAISSE", + "parent": 5, + "position": 2 + }, + "530": { + "code": 530, + "nom": "Caisse", + "parent": 53, + "position": 2 + }, + "54": { + "code": 54, + "nom": "R\u00c9GIES D'AVANCES ET ACCR\u00c9DITIFS", + "parent": 5, + "position": 2 + }, + "58": { + "code": 58, + "nom": "VIREMENTS INTERNES", + "parent": 5, + "position": 2 + }, + "59": { + "code": 59, + "nom": "PROVISIONS POUR D\u00c9PR\u00c9CIATION DES COMPTES FINANCIERS", + "parent": 5, + "position": 2 + }, + "6": { + "code": 6, + "nom": "Classe 6 \u2014 Comptes de charges", + "parent": 0, + "position": 8 + }, + "60": { + "code": 60, + "nom": "ACHATS", + "parent": 6, + "position": 8 + }, + "601": { + "code": 601, + "nom": "Achats stock\u00e9s - Mati\u00e8res premi\u00e8res et fournitures", + "parent": 60, + "position": 8 + }, + "602": { + "code": 602, + "nom": "Achats stock\u00e9s - Autres approvisionnements", + "parent": 60, + "position": 8 + }, + "604": { + "code": 604, + "nom": "Achat d'\u00e9tudes et prestations de services", + "parent": 60, + "position": 8 + }, + "606": { + "code": 606, + "nom": "Achats non stock\u00e9s de mati\u00e8res et fournitures", + "parent": 60, + "position": 8 + }, + "6061": { + "code": 6061, + "nom": "Fournitures non stockables (eau, \u00e9nergie...)", + "parent": 606, + "position": 8 + }, + "6063": { + "code": 6063, + "nom": "Fournitures d'entretien et de petit \u00e9quipement", + "parent": 606, + "position": 8 + }, + "6064": { + "code": 6064, + "nom": "Fournitures administratives", + "parent": 606, + "position": 8 + }, + "6068": { + "code": 6068, + "nom": "Autres mati\u00e8res et fournitures", + "parent": 606, + "position": 8 + }, + "607": { + "code": 607, + "nom": "Achats de marchandises", + "parent": 60, + "position": 8 + }, + "61": { + "code": 61, + "nom": "SERVICES EXT\u00c9RIEURS", + "parent": 6, + "position": 8 + }, + "611": { + "code": 611, + "nom": "Sous-traitance g\u00e9n\u00e9rale", + "parent": 61, + "position": 8 + }, + "612": { + "code": 612, + "nom": "Redevances de cr\u00e9dit-bail", + "parent": 61, + "position": 8 + }, + "613": { + "code": 613, + "nom": "Locations", + "parent": 61, + "position": 8 + }, + "614": { + "code": 614, + "nom": "Charges locatives et de co-propri\u00e9t\u00e9", + "parent": 61, + "position": 8 + }, + "615": { + "code": 615, + "nom": "Entretiens et r\u00e9parations", + "parent": 61, + "position": 8 + }, + "616": { + "code": 616, + "nom": "Primes d'assurance", + "parent": 61, + "position": 8 + }, + "618": { + "code": 618, + "nom": "Divers", + "parent": 61, + "position": 8 + }, + "62": { + "code": 62, + "nom": "AUTRES SERVICES EXT\u00c9RIEURS", + "parent": 6, + "position": 8 + }, + "621": { + "code": 621, + "nom": "Personnel ext\u00e9rieur \u00e0 l'association", + "parent": 62, + "position": 8 + }, + "622": { + "code": 622, + "nom": "R\u00e9mun\u00e9rations d'interm\u00e9diaires et honoraires", + "parent": 62, + "position": 8 + }, + "6226": { + "code": 6226, + "nom": "Honoraires", + "parent": 622, + "position": 8 + }, + "6227": { + "code": 6227, + "nom": "Frais d'actes et de contentieux", + "parent": 622, + "position": 8 + }, + "6228": { + "code": 6228, + "nom": "Divers", + "parent": 622, + "position": 8 + }, + "623": { + "code": 623, + "nom": "Publicit\u00e9, publications, relations publiques", + "parent": 62, + "position": 8 + }, + "624": { + "code": 624, + "nom": "Transports de biens et transports collectifs du personnel", + "parent": 62, + "position": 8 + }, + "625": { + "code": 625, + "nom": "D\u00e9placements, missions et r\u00e9ceptions", + "parent": 62, + "position": 8 + }, + "626": { + "code": 626, + "nom": "Frais postaux et de t\u00e9l\u00e9communications", + "parent": 62, + "position": 8 + }, + "627": { + "code": 627, + "nom": "Services bancaires et assimil\u00e9s", + "parent": 62, + "position": 8 + }, + "628": { + "code": 628, + "nom": "Divers", + "parent": 62, + "position": 8 + }, + "63": { + "code": 63, + "nom": "IMP\u00d4TS, TAXES ET VERSEMENTS ASSIMIL\u00c9S", + "parent": 6, + "position": 8 + }, + "631": { + "code": 631, + "nom": "Imp\u00f4ts, taxes et versements assimil\u00e9s sur r\u00e9mun\u00e9rations (Administration des imp\u00f4ts)", + "parent": 63, + "position": 8 + }, + "6311": { + "code": 6311, + "nom": "Taxes sur les salaires", + "parent": 631, + "position": 8 + }, + "6313": { + "code": 6313, + "nom": "Participations des employeurs \u00e0 la formation professionnelle continue", + "parent": 631, + "position": 8 + }, + "635": { + "code": 635, + "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s (Administration des imp\u00f4ts)", + "parent": 63, + "position": 8 + }, + "6351": { + "code": 6351, + "nom": "Imp\u00f4ts directs (sauf imp\u00f4ts sur les b\u00e9n\u00e9fices)", + "parent": 635, + "position": 8 + }, + "6353": { + "code": 6353, + "nom": "Imp\u00f4ts indirects", + "parent": 635, + "position": 8 + }, + "637": { + "code": 637, + "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s (Autres organismes)", + "parent": 63, + "position": 8 + }, + "64": { + "code": 64, + "nom": "CHARGES DE PERSONNEL", + "parent": 6, + "position": 8 + }, + "641": { + "code": 641, + "nom": "R\u00e9mun\u00e9rations du personnel", + "parent": 64, + "position": 8 + }, + "643": { + "code": 643, + "nom": "R\u00e9mun\u00e9rations du personnel artistique et assimil\u00e9s", + "parent": 64, + "position": 8 + }, + "645": { + "code": 645, + "nom": "Charges de s\u00e9curit\u00e9 sociale et de pr\u00e9voyance", + "parent": 64, + "position": 8 + }, + "647": { + "code": 647, + "nom": "Autres charges sociales", + "parent": 64, + "position": 8 + }, + "648": { + "code": 648, + "nom": "Autres charges de personnel", + "parent": 64, + "position": 8 + }, + "65": { + "code": 65, + "nom": "AUTRES CHARGES DE GESTION COURANTE", + "parent": 6, + "position": 8 + }, + "658": { + "code": 658, + "nom": "Charges diverses de gestion courante", + "parent": 65, + "position": 8 + }, + "66": { + "code": 66, + "nom": "CHARGES FINANCI\u00c8RES", + "parent": 6, + "position": 8 + }, + "661": { + "code": 661, + "nom": "Charges d'int\u00e9r\u00eats", + "parent": 66, + "position": 8 + }, + "67": { + "code": 67, + "nom": "CHARGES EXCEPTIONNELLES", + "parent": 6, + "position": 8 + }, + "671": { + "code": 671, + "nom": "Charges exceptionnelles sur op\u00e9rations de gestion", + "parent": 67, + "position": 8 + }, + "6713": { + "code": 6713, + "nom": "Dons, lib\u00e9ralit\u00e9s", + "parent": 671, + "position": 8 + }, + "678": { + "code": 678, + "nom": "Autres charges exceptionnelles", + "parent": 67, + "position": 8 + }, + "6788": { + "code": 6788, + "nom": "Charges exceptionnelles diverses", + "parent": 678, + "position": 8 + }, + "68": { + "code": 68, + "nom": "DOTATIONS AUX AMORTISSEMENTS, D\u00c9PR\u00c9CIATIONS, PROVISIONS ET ENGAGEMENTS", + "parent": 6, + "position": 8 + }, + "681": { + "code": 681, + "nom": "Dotations aux amortissements, d\u00e9pr\u00e9ciations et provisions - Charges d'exploitation", + "parent": 68, + "position": 8 + }, + "6811": { + "code": 6811, + "nom": "Dotations aux amortissements des immobilisations incorporelles et corporelles", + "parent": 681, + "position": 8 + }, + "68111": { + "code": 68111, + "nom": "Immobilisations incorporelles", + "parent": 6811, + "position": 8 + }, + "68112": { + "code": 68112, + "nom": "Immobilisations corporelles", + "parent": 6811, + "position": 8 + }, + "686": { + "code": 686, + "nom": "Dotations aux amortissements, d\u00e9pr\u00e9ciations et provisions - Charges financi\u00e8res", + "parent": 68, + "position": 8 + }, + "69": { + "code": 69, + "nom": "PARTICIPATION DES SALARI\u00c9S - IMP\u00d4TS SUR LES B\u00c9N\u00c9FICES ET ASSIMIL\u00c9S", + "parent": 6, + "position": 8 + }, + "695": { + "code": 695, + "nom": "Imp\u00f4ts sur les soci\u00e9t\u00e9s (y compris imp\u00f4ts sur les soci\u00e9t\u00e9s des personnes morales non lucratives)", + "parent": 69, + "position": 8 + }, + "7": { + "code": 7, + "nom": "Classe 7 \u2014 Comptes de produits", + "parent": 0, + "position": 4 + }, + "70": { + "code": 70, + "nom": "VENTES DE PRODUITS FINIS, PRESTATIONS DE SERVICES, MARCHANDISES", + "parent": 7, + "position": 4 + }, + "701": { + "code": 701, + "nom": "Ventes de produits finis", + "parent": 70, + "position": 4 + }, + "706": { + "code": 706, + "nom": "Prestations de services", + "parent": 70, + "position": 4 + }, + "707": { + "code": 707, + "nom": "Ventes de marchandises", + "parent": 70, + "position": 4 + }, + "708": { + "code": 708, + "nom": "Produits des activit\u00e9s annexes", + "parent": 70, + "position": 4 + }, + "71": { + "code": 71, + "nom": "PRODUCTION STOCK\u00c9E (OU D\u00c9STOCKAGE)", + "parent": 7, + "position": 4 + }, + "72": { + "code": 72, + "nom": "PRODUCTION IMMOBILIS\u00c9E", + "parent": 7, + "position": 4 + }, + "74": { + "code": 74, + "nom": "SUBVENTIONS D'EXPLOITATION", + "parent": 7, + "position": 4 + }, + "740": { + "code": 740, + "nom": "Subventions re\u00e7ues", + "parent": 74, + "position": 4 + }, + "75": { + "code": 75, + "nom": "AUTRES PRODUITS DE GESTION COURANTE", + "parent": 7, + "position": 4 + }, + "754": { + "code": 754, + "nom": "Collectes", + "parent": 75, + "position": 4 + }, + "756": { + "code": 756, + "nom": "Cotisations", + "parent": 75, + "position": 4 + }, + "758": { + "code": 758, + "nom": "Produits divers de gestion courante", + "parent": 75, + "position": 4 + }, + "7587": { + "code": 7587, + "nom": "Ventes de dons en nature", + "parent": 758, + "position": 4 + }, + "7588": { + "code": 7588, + "nom": "Autres produits de la g\u00e9n\u00e9rosit\u00e9 du public", + "parent": 758, + "position": 4 + }, + "76": { + "code": 76, + "nom": "PRODUITS FINANCIERS", + "parent": 7, + "position": 4 + }, + "760": { + "code": 760, + "nom": "Produits financiers", + "parent": 76, + "position": 4 + }, + "77": { + "code": 77, + "nom": "PRODUITS EXCEPTIONNELS", + "parent": 7, + "position": 4 + }, + "771": { + "code": 771, + "nom": "Produits exceptionnels sur op\u00e9rations de gestion", + "parent": 77, + "position": 4 + }, + "7713": { + "code": 7713, + "nom": "Lib\u00e9ralit\u00e9s re\u00e7ues", + "parent": 771, + "position": 4 + }, + "7715": { + "code": 7715, + "nom": "Subventions d'\u00e9quilibre", + "parent": 771, + "position": 4 + }, + "775": { + "code": 775, + "nom": "Produits des cessions d'\u00e9l\u00e9ments d'actifs", + "parent": 77, + "position": 4 + }, + "778": { + "code": 778, + "nom": "Autres produits exceptionnels", + "parent": 77, + "position": 4 + }, + "7780": { + "code": 7780, + "nom": "Manifestations diverses", + "parent": 778, + "position": 4 + }, + "7788": { + "code": 7788, + "nom": "Produits exceptionnels divers", + "parent": 778, + "position": 4 + }, + "78": { + "code": 78, + "nom": "REPRISES SUR AMORTISSEMENTS ET PROVISIONS", + "parent": 7, + "position": 4 + }, + "79": { + "code": 79, + "nom": "TRANSFERT DE CHARGES", + "parent": 7, + "position": 4 + }, + "791": { + "code": 791, + "nom": "Transferts de charges d'exploitation", + "parent": 79, + "position": 4 + }, + "796": { + "code": 796, + "nom": "Transferts de charges financi\u00e8res", + "parent": 79, + "position": 4 + }, + "797": { + "code": 797, + "nom": "Transferts de charges exceptionnels", + "parent": 79, + "position": 4 + }, + "8": { + "code": 8, + "nom": "Classe 8 \u00ad\u2014 Contributions b\u00e9n\u00e9voles en nature", + "parent": 0, + "position": 12 + }, + "86": { + "code": 86, + "nom": "R\u00c9PARTITION PAR NATURE DE CHARGES", + "parent": 8, + "position": 8 + }, + "861": { + "code": 861, + "nom": "Mise \u00e0 dispositions gratuites de biens", + "parent": 86, + "position": 8 + }, + "862": { + "code": 862, + "nom": "Prestations", + "parent": 86, + "position": 8 + }, + "864": { + "code": 864, + "nom": "Personnel b\u00e9n\u00e9vole", + "parent": 86, + "position": 8 + }, + "87": { + "code": 87, + "nom": "R\u00c9PARTITION PAR NATURE DE RESSOURCES", + "parent": 8, + "position": 4 + }, + "870": { + "code": 870, + "nom": "B\u00e9n\u00e9volat", + "parent": 87, + "position": 4 + }, + "871": { + "code": 871, + "nom": "Prestations en nature", + "parent": 87, + "position": 4 + }, + "875": { + "code": 875, + "nom": "Dons en nature", + "parent": 87, + "position": 4 + }, + "9": { + "code": 9, + "nom": "Classe 9 \u2014 Comptes analytiques", + "parent": 0, + "position": 12 + } +} \ No newline at end of file diff --git a/include/data/schema.sql b/include/data/schema.sql new file mode 100644 index 0000000..2cc846f --- /dev/null +++ b/include/data/schema.sql @@ -0,0 +1,316 @@ +CREATE TABLE config ( +-- Configuration de Garradin + cle TEXT PRIMARY KEY, + valeur TEXT +); + +-- On stocke ici les ID de catégorie de compta correspondant aux types spéciaux +-- compta_categorie_cotisations => id_categorie +-- compta_categorie_dons => id_categorie + +CREATE TABLE membres_categories +-- Catégories de membres +( + id INTEGER PRIMARY KEY, + nom TEXT, + description TEXT, + + droit_wiki INT DEFAULT 1, + droit_membres INT DEFAULT 1, + droit_compta INT DEFAULT 1, + droit_inscription INT DEFAULT 0, + droit_connexion INT DEFAULT 1, + droit_config INT DEFAULT 0, + cacher INT DEFAULT 0, + + id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id) +); + +-- Membres de l'asso +-- Table dynamique générée par l'application +-- voir class.champs_membres.php + +CREATE TABLE cotisations +-- Types de cotisations et activités +( + id INTEGER PRIMARY KEY, + id_categorie_compta INTEGER NULL, -- NULL si le type n'est pas associé automatiquement à la compta + + intitule TEXT NOT NULL, + description TEXT NULL, + montant REAL NOT NULL, + + duree INTEGER NULL, -- En jours + debut TEXT NULL, -- timestamp + fin TEXT NULL, + + FOREIGN KEY (id_categorie_compta) REFERENCES compta_categories (id) +); + +CREATE TABLE cotisations_membres +-- Enregistrement des cotisations et activités +( + id INTEGER NOT NULL PRIMARY KEY, + id_membre INTEGER NOT NULL REFERENCES membres (id), + id_cotisation INTEGER NOT NULL REFERENCES cotisations (id), + + date TEXT NOT NULL DEFAULT CURRENT_DATE +); + +CREATE UNIQUE INDEX cm_unique ON cotisations_membres (id_membre, id_cotisation, date); + +CREATE TABLE membres_operations +-- Liaision des enregistrement des paiements en compta +( + id_membre INTEGER NOT NULL REFERENCES membres (id), + id_operation INTEGER NOT NULL REFERENCES compta_journal (id), + id_cotisation INTEGER NULL REFERENCES cotisations_membres (id), + + PRIMARY KEY (id_membre, id_operation) +); + +CREATE TABLE rappels +-- Rappels de devoir renouveller une cotisation +( + id INTEGER PRIMARY KEY, + id_cotisation INTEGER NOT NULL REFERENCES cotisations (id), + + delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel + + sujet TEXT NOT NULL, + texte TEXT NOT NULL +); + +CREATE TABLE rappels_envoyes +-- Enregistrement des rappels envoyés à qui et quand +( + id INTEGER PRIMARY KEY, + + id_membre INTEGER NOT NULL REFERENCES membres (id), + id_cotisation INTEGER NOT NULL REFERENCES cotisations (id), + + date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + + media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre +); + +-- +-- WIKI +-- + +CREATE TABLE wiki_pages +-- Pages du wiki +( + id INTEGER PRIMARY KEY, + uri TEXT, -- URI unique (équivalent NomPageWiki) + titre TEXT, + date_creation TEXT DEFAULT CURRENT_TIMESTAMP, + date_modification TEXT DEFAULT CURRENT_TIMESTAMP, + parent INTEGER DEFAULT 0, -- ID de la page parent + revision INTEGER DEFAULT 0, -- Numéro de révision (commence à 0 si pas de texte, +1 à chaque changement du texte) + droit_lecture INTEGER 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 DEFAULT 0 -- Accès en écriture (0 = tous ceux qui ont droit d'écriture sur le wiki, 1+ = ID de groupe) +); + +CREATE UNIQUE INDEX wiki_uri ON wiki_pages (uri); + +CREATE VIRTUAL TABLE wiki_recherche USING fts4 +-- Table dupliquée pour chercher une page +( + id INT PRIMARY KEY NOT NULL, -- Clé externe obligatoire + titre TEXT, + contenu TEXT, -- Contenu de la dernière révision + FOREIGN KEY (id) REFERENCES wiki_pages(id) +); + +CREATE TABLE wiki_revisions +-- Révisions du contenu des pages +( + id_page INTEGER NOT NULL, + revision INTEGER, + + id_auteur INTEGER, + + contenu TEXT, + modification TEXT, -- Description des modifications effectuées + chiffrement INTEGER DEFAULT 0, -- 1 si le contenu est chiffré, 0 sinon + date TEXT DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY(id_page, revision), + FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire + FOREIGN KEY (id_auteur) REFERENCES membres (id) -- Clé externe non-obligatoire (peut être supprimée après en cas de suppression de membre) +); + +CREATE INDEX wiki_revisions_id_page ON wiki_revisions (id_page); +CREATE INDEX wiki_revisions_id_auteur ON wiki_revisions (id_auteur); + +-- Triggers pour synchro avec table wiki_pages +CREATE TRIGGER wiki_recherche_delete AFTER DELETE ON wiki_pages + BEGIN + DELETE FROM wiki_recherche WHERE id = old.id; + END; + +CREATE TRIGGER 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 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 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; + +/* +CREATE TABLE wiki_fichiers ( + id INTEGER PRIMARY KEY, + id_page INTEGER NOT NULL, + nom TEXT, + hash TEXT, + + FOREIGN KEY (id_page) REFERENCES wiki_pages (id) -- Clé externe obligatoire +); + +CREATE INDEX wiki_fichiers_id_page ON wiki_fichiers (id_page); + +CREATE TABLE wiki_suivi +-- Suivi des pages +( + id_membre INTEGER NOT NULL, + id_page INTEGER NOT NULL, + + PRIMARY KEY (id_membre, id_page), + + FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire + FOREIGN KEY (id_membre) REFERENCES membres (id) -- Clé externe obligatoire +); +*/ + +-- +-- COMPTA +-- + +CREATE TABLE compta_exercices +-- Exercices +( + id INTEGER PRIMARY KEY, + + libelle TEXT NOT NULL, + + debut TEXT NOT NULL DEFAULT CURRENT_DATE, + fin TEXT NULL DEFAULT NULL, + + cloture INTEGER NOT NULL DEFAULT 0 +); + + +CREATE TABLE compta_comptes +-- Plan comptable +( + id TEXT PRIMARY KEY, -- peut contenir des lettres, eg. 53A, 53B, etc. + parent TEXT NOT NULL DEFAULT 0, + + libelle TEXT NOT NULL, + + position INTEGER NOT NULL, -- position actif/passif/charge/produit + plan_comptable INTEGER NOT NULL DEFAULT 1, -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur + desactive INTEGER NOT NULL DEFAULT 0 -- 1 = compte historique désactivé +); + +CREATE INDEX compta_comptes_parent ON compta_comptes (parent); + +CREATE TABLE compta_comptes_bancaires +-- Comptes bancaires +( + id TEXT PRIMARY KEY, + + banque TEXT NOT NULL, + + iban TEXT, + bic TEXT, + + FOREIGN KEY(id) REFERENCES compta_comptes(id) +); + +CREATE TABLE compta_journal +-- Journal des opérations comptables +( + id INTEGER PRIMARY KEY, + + libelle TEXT NOT NULL, + remarques TEXT, + numero_piece TEXT, -- N° de pièce comptable + + montant REAL, + + date TEXT DEFAULT CURRENT_DATE, + moyen_paiement TEXT DEFAULT NULL, + numero_cheque TEXT DEFAULT NULL, + + compte_debit TEXT, -- N° du compte dans le plan + compte_credit TEXT, -- N° du compte dans le plan + + id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL) + id_auteur INTEGER NULL, + id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple) + + FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code), + FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id), + FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id), + FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id), + FOREIGN KEY(id_auteur) REFERENCES membres(id), + FOREIGN KEY(id_categorie) REFERENCES compta_categories(id) +); + +CREATE INDEX compta_operations_exercice ON compta_journal (id_exercice); +CREATE INDEX compta_operations_date ON compta_journal (date); +CREATE INDEX compta_operations_comptes ON compta_journal (compte_debit, compte_credit); +CREATE INDEX compta_operations_auteur ON compta_journal (id_auteur); + +CREATE TABLE compta_moyens_paiement +-- Moyens de paiement +( + code TEXT PRIMARY KEY, + nom TEXT +); + +--INSERT INTO compta_moyens_paiement (code, nom) VALUES ('AU', 'Autre'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP'); +INSERT INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement'); + +CREATE TABLE compta_categories +-- Catégories pour simplifier le plan comptable +( + id INTEGER PRIMARY KEY, + type INTEGER DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface) + + intitule TEXT NOT NULL, + description TEXT, + + compte TEXT NOT NULL, -- Compte affecté par cette catégorie + + FOREIGN KEY(compte) REFERENCES compta_comptes(id) +); + +CREATE TABLE plugins +( + id TEXT PRIMARY KEY, + officiel INTEGER NOT NULL DEFAULT 0, + nom TEXT NOT NULL, + description TEXT, + auteur TEXT, + url TEXT, + version TEXT NOT NULL, + menu INTEGER NOT NULL DEFAULT 0, + config TEXT +); \ No newline at end of file diff --git a/include/index.html b/include/index.html new file mode 100644 index 0000000..9a31a28 --- /dev/null +++ b/include/index.html @@ -0,0 +1 @@ +404 Not Found

Not Found

The requested URL was not found on this server.

\ No newline at end of file diff --git a/include/init.php b/include/init.php new file mode 100644 index 0000000..fe4e820 --- /dev/null +++ b/include/init.php @@ -0,0 +1,353 @@ + +
 \__/
(xx)
//||\\\\
+

Erreur fatale

+

Une erreur fatale s\'est produite à l\'exécution de Garradin. Pour rapporter ce bug + merci d\'inclure le message ci-dessous :

+

'); + ini_set('error_append_string', '


+

Comment rapporter un bug

'); + } +} + +/* + * Gestion des erreurs et exceptions + */ + +class UserException extends \LogicException +{ +} + +function exception_error_handler($errno, $errstr, $errfile, $errline ) +{ + // For @ ignored errors + if (error_reporting() === 0) return; + throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); +} + +function exception_handler($e) +{ + if ($e instanceOf UserException || $e instanceOf miniSkelMarkupException) + { + try { + if (PHP_SAPI == 'cli') + { + echo $e->getMessage(); + } + else + { + $tpl = Template::getInstance(); + + $tpl->assign('error', $e->getMessage()); + $tpl->display('error.tpl'); + } + + exit; + } + catch (Exception $e) + { + } + } + + $file = str_replace(ROOT, '', $e->getFile()); + + $error = "Exception of type ".get_class($e)." happened !\n\n". + $e->getCode()." - ".$e->getMessage()."\n\nIn: ". + $file . ":" . $e->getLine()."\n\n"; + + if (!empty($_SERVER['HTTP_HOST']) && !empty($_SERVER['REQUEST_URI'])) + $error .= 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']."\n\n"; + + $error .= $e->getTraceAsString(); + $error .= "\n-------------\n"; + $error .= 'Garradin version: ' . garradin_version() . "\n"; + $error .= 'Garradin manifest: ' . garradin_manifest() . "\n"; + $error .= 'PHP version: ' . phpversion() . "\n"; + + foreach ($_SERVER as $key=>$value) + { + if (is_array($value)) + $value = json_encode($value); + + $error .= $key . ': ' . $value . "\n"; + } + + $error = str_replace("\r", '', $error); + error_log($error); + + if (MAIL_ERRORS) + { + mail(MAIL_ERRORS, '[Garradin] Erreur d\'exécution', $error, 'From: "' . WWW_URL . '" '); + } + + if (PHP_SAPI == 'cli') + { + echo $error; + } + else + { + echo ' +
 \__/
(xx)
//||\\\\
+

Erreur d\'exécution

'; + + if (SHOW_ERRORS) + { + echo '

Une erreur s\'est produite à l\'exécution de Garradin. Pour rapporter ce bug + merci d\'inclure le message suivant :

+ +
+

Comment rapporter un bug

'; + } + else + { + echo '

Une erreur s\'est produite à l\'exécution de Garradin.

+

Le webmaster a été prévenu.

'; + } + } + + exit; +} + +set_error_handler('Garradin\exception_error_handler'); +set_exception_handler('Garradin\exception_handler'); + +/** + * Auto-load classes and libs + */ +class Loader +{ + /** + * Already loaded filenames + * @var array + */ + static protected $loaded = []; + + static protected $libs = [ + 'utils', + 'squelette_filtres', + 'static_cache', + 'template' + ]; + + /** + * Loads a class from the $name + * @param stringg $classname + * @return bool true + */ + static public function load($classname) + { + $classname = ltrim($classname, '\\'); + $filename = ''; + $namespace = ''; + + if ($lastnspos = strripos($classname, '\\')) + { + $namespace = substr($classname, 0, $lastnspos); + $classname = substr($classname, $lastnspos + 1); + + if ($namespace != 'Garradin') + { + $filename = str_replace('\\', '/', $namespace) . '/'; + } + } + + $classname = strtolower($classname); + + if (in_array($classname, self::$libs)) { + $filename = 'lib.' . $classname . '.php'; + } else { + $filename .= 'class.' . $classname . '.php'; + } + + $filename = ROOT . '/include/' . $filename; + + if (array_key_exists($filename, self::$loaded)) + { + return true; + } + + if (!file_exists($filename)) { + throw new \Exception('File '.$filename.' doesn\'t exists'); + } + + self::$loaded[$filename] = true; + + require $filename; + } +} + +\spl_autoload_register(['Garradin\Loader', 'load'], true); + +$n = new Membres; + +/* + * Inclusion des fichiers de base + */ + +if (!defined('Garradin\INSTALL_PROCESS') && !defined('Garradin\UPGRADE_PROCESS')) +{ + if (!file_exists(DB_FILE)) + { + utils::redirect('/admin/install.php'); + } + + $config = Config::getInstance(); + + if (version_compare($config->getVersion(), garradin_version(), '<')) + { + utils::redirect('/admin/upgrade.php'); + } +} + +?> \ No newline at end of file diff --git a/include/lib.squelette_filtres.php b/include/lib.squelette_filtres.php new file mode 100644 index 0000000..aab766d --- /dev/null +++ b/include/lib.squelette_filtres.php @@ -0,0 +1,350 @@ + 'supprimer_tags', + 'var_dump', + ]; + + static public $filtres_alias = [ + '!=' => 'different_de', + '==' => 'egal_a', + '?' => 'choixsivide', + '>' => 'superieur_a', + '>=' => 'superieur_ou_egal_a', + '<' => 'inferieur_a', + '<=' => 'inferieur_ou_egal_a', + 'yes' => 'oui', + 'no' => 'non', + 'and' => 'et', + 'or' => 'ou', + 'xor' => 'xou', + ]; + + static public $desactiver_defaut = [ + 'formatter_texte', + 'entites_html', + 'proteger_contact', + 'echapper_xml', + ]; + + static public function date_en_francais($date) + { + return ucfirst(strtolower(utils::strftime_fr('%A %e %B %Y', $date))); + } + + static public function heure_en_francais($date) + { + return utils::strftime_fr('%Hh%I', $date); + } + + static public function mois_en_francais($date) + { + return utils::strftime_fr('%B %Y', $date); + } + + static public function date_perso($date, $format) + { + return utils::strftime_fr($format, $date); + } + + static public function date_intelligente($date) + { + if (date('Ymd', $date) == date('Ymd')) + return 'Aujourd\'hui, '.date('H\hi', $date); + elseif (date('Ymd', $date) == date('Ymd', strtotime('yesterday'))) + return 'Hier, '.date('H\hi', $date); + elseif (date('Y', $date) == date('Y')) + return strtolower(utils::strftime_fr('%e %B, %Hh%M', $date)); + else + return strtolower(utils::strftime_fr('%e %B %Y', $date)); + } + + static public function date_atom($date) + { + return date(DATE_ATOM, $date); + } + + static public function alterner($v, $name, $valeur1, $valeur2) + { + if (!array_key_exists($name, self::$alt)) + { + self::$alt[$name] = 0; + } + + if (self::$alt[$name]++ % 2 == 0) + return $valeur1; + else + return $valeur2; + } + + static public function proteger_contact($contact) + { + if (!trim($contact)) + return ''; + + if (strpos($contact, '@')) + return ''.htmlspecialchars(strrev($contact), ENT_QUOTES, 'UTF-8').''; + else + return ''.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').''; + } + + static public function entites_html($texte) + { + return htmlspecialchars($texte, ENT_QUOTES, 'UTF-8'); + } + + static public function echapper_xml($texte) + { + return str_replace(''', ''', htmlspecialchars($texte, ENT_QUOTES, 'UTF-8')); + } + + static public function formatter_texte($texte) + { + $texte = utils::htmlLinksOnUrls($texte); + $texte = utils::htmlSpip($texte); + $texte = utils::htmlGarbage2xhtml($texte); + + $texte = self::typo_fr($texte); + + return $texte; + } + + static public function typo_fr($str, $html = true) + { + $space = $html ? ' ' : ' '; + $str = preg_replace('/(?:[\h]| )*([?!:»])(\s+|$)/u', $space.'\\1\\2', $str); + $str = preg_replace('/(^|\s+)([«])(?:[\h]| )*/u', '\\1\\2'.$space, $str); + return $str; + } + + static public function pagination($total, $debut, $par_page) + { + $max_page = ceil($total / $par_page); + $current = ($debut > 0) ? ceil($debut / $par_page) + 1 : 1; + $out = ''; + + if ($current > 1) + { + $out .= '« Page précédente - '; + } + + for ($i = 1; $i <= $max_page; $i++) + { + $link = ($i == 1) ? './' : './+' . (($i - 1) * $par_page); + + if ($i == $current) + $out .= ''.$i.' - '; + else + $out .= ''.$i.' - '; + } + + if ($current < $max_page) + { + $out .= 'Page suivante »'; + } + else + { + $out = substr($out, 0, -3); + } + + return $out; + } + + // Compatibilité SPIP + + static public function egal_a($value, $test) + { + if ($value == $test) + return true; + else + return false; + } + + static public function different_de($value, $test) + { + if ($value != $test) + return true; + else + return false; + } + + // disponible aussi avec : | ?{sioui, sinon} + static public function choixsivide($value, $un, $deux = '') + { + if (empty($value) || !trim($value)) + return $deux; + else + return $un; + } + + static public function sinon($value, $sinon = '') + { + if ($value) + return $value; + else + return $sinon; + } + + static public function choixsiegal($value, $test, $un, $deux) + { + return ($value == $test) ? $un : $deux; + } + + static public function supprimer_tags($value, $replace = '') + { + return preg_replace('!<[^>]*>!', $replace, $value); + } + + static public function supprimer_spip($value) + { + $value = preg_replace('!\[([^\]]+)(?:->[^\]]*)?\]!U', '$1', $value); + $value = preg_replace('!\{+([^\}]*)\}+!', '$1', $value); + return $value; + } + + static public function couper($texte, $taille, $etc = ' (...)') + { + if (strlen($texte) > $taille) + { + $texte = substr($texte, 0, $taille); + $taille -= ($taille * 0.1); + + $texte = preg_replace('!([\s.,;:\!?])[^\s.,;:\!?]*?$!', '\\1', $texte); + $texte.= $etc; + } + + return $texte; + } + + static public function replace($texte, $expression, $replace, $modif='UsimsS') + { + return preg_replace('/'.$expression.'/'.$modif, $replace, $texte); + } + + static public function plus($a, $b) + { + return $a + $b; + } + + static public function moins($a, $b) + { + return $a - $b; + } + + static public function mult($a, $b) + { + return $a * $b; + } + + static public function div($a, $b) + { + return $b ? $a / $b : 0; + } + + static public function modulo($a, $mod, $add) + { + return ($mod ? $nb % $mod : 0) + $add; + } + + static public function vide($value) + { + return ''; + } + + static public function concat() + { + return implode('', func_get_args()); + } + + static public function singulier_ou_pluriel($nb, $singulier, $pluriel, $var = null) + { + if (!$nb) + return ''; + + if ($nb == 1) + return str_replace('@'.$var.'@', $nb, $singulier); + else + return str_replace('@'.$var.'@', $nb, $pluriel); + } + + static public function date_w3c($date) + { + return date(DATE_W3C, $date); + } + + static public function et($value, $test) + { + return ($value && $test); + } + + static public function ou($value, $test) + { + return ($value || $test); + } + + static public function xou($value, $test) + { + return ($value XOR $test); + } + + static public function oui($value) + { + return $value ? true : false; + } + + static public function non($value) + { + return !$value ? true : false; + } + + static public function superieur_a($value, $test) + { + return ($value > $test) ? true : false; + } + + static public function superieur_ou_egal_a($value, $test) + { + return ($value >= $test) ? true : false; + } + + static public function inferieur_a($value, $test) + { + return ($value < $test) ? true : false; + } + + static public function inferieur_ou_egal_a($value, $test) + { + return ($value <= $test) ? true : false; + } +} + +?> \ No newline at end of file diff --git a/include/lib.static_cache.php b/include/lib.static_cache.php new file mode 100644 index 0000000..d307bd6 --- /dev/null +++ b/include/lib.static_cache.php @@ -0,0 +1,87 @@ + (time() - (int)$expire)) ? false : true; + } + + static public function get($id) + { + $path = self::_getCachePath($id); + return file_get_contents($path); + } + + static public function display($id) + { + $path = self::_getCachePath($id); + return readfile($path); + } + + static public function getPath($id) + { + return self::_getCachePath($id); + } + + static public function remove($id) + { + $path = self::_getCachePath($id); + return unlink($path); + } + + static public function clean($expire = self::CLEAN_EXPIRE) + { + $dir = self::_getCacheDir(); + $d = dir($dir); + + $expire = time() - $expire; + + while ($file = $d->read()) + { + if ($file[0] == '.') + { + continue; + } + + if (filemtime($dir . '/' . $file) > $expire) + { + unlink($dir . '/' . $file); + } + } + + $d->close(); + + return true; + } +} diff --git a/include/lib.template.php b/include/lib.template.php new file mode 100644 index 0000000..6f9f8e7 --- /dev/null +++ b/include/lib.template.php @@ -0,0 +1,604 @@ +cache = false; + + $this->compile_dir = DATA_ROOT . '/cache/compiled'; + $this->template_dir = ROOT . '/templates'; + + $this->compile_check = true; + + $this->reserved_template_varname = 'tpl'; + + $this->assign('www_url', WWW_URL); + $this->assign('self_url', utils::getSelfUrl()); + + $this->assign('is_logged', false); + } +} + +$tpl = Template::getInstance(); + +function tpl_csrf_field($params) +{ + $name = utils::CSRF_field_name($params['key']); + $value = utils::CSRF_create($params['key']); + + return ''; +} + +function tpl_form_field($params) +{ + if (!isset($params['name'])) + throw new \BadFunctionCallException('name argument is mandatory'); + + $name = $params['name']; + + if (isset($_POST[$name])) + $value = $_POST[$name]; + elseif (isset($params['data']) && isset($params['data'][$name])) + $value = $params['data'][$name]; + elseif (isset($params['default'])) + $value = $params['default']; + else + $value = ''; + + if (is_array($value)) + { + return $value; + } + + if (isset($params['checked'])) + { + if ($value == $params['checked']) + return ' checked="checked" '; + + return ''; + } + elseif (isset($params['selected'])) + { + if ($value == $params['selected']) + return ' selected="selected" '; + + return ''; + } + + return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); +} + +function tpl_format_tel($n) +{ + $n = preg_replace('![^\d\+]!', '', $n); + + if (substr($n, 0, 1) == '+') + { + $n = preg_replace('!^\+(?:1|2[07]|2\d{2}|3[0-469]|3\d{2}|4[013-9]|' + . '4\d{2}|5[1-8]|5\d{2}|6[0-6]|6\d{2}|7\d|8[1-469]|8\d{2}|' + . '9[0-58]|9\d{2})!', '\\0 ', $n); + } + elseif (preg_match('/^\d{10}$/', $n)) + { + $n = preg_replace('!(\d{2})!', '\\1 ', $n); + } + + return $n; +} + +function tpl_strftime_fr($ts, $format) +{ + return utils::strftime_fr($format, $ts); +} + +function tpl_date_fr($ts, $format) +{ + return utils::date_fr($format, $ts); +} + +function tpl_format_droits($params) +{ + $droits = $params['droits']; + + $out = ['connexion' => '', 'inscription' => '', 'membres' => '', 'compta' => '', + 'wiki' => '', 'config' => '']; + $classes = [ + Membres::DROIT_AUCUN => 'aucun', + Membres::DROIT_ACCES => 'acces', + Membres::DROIT_ECRITURE=> 'ecriture', + Membres::DROIT_ADMIN => 'admin', + ]; + + foreach ($droits as $cle=>$droit) + { + $cle = str_replace('droit_', '', $cle); + + if (array_key_exists($cle, $out)) + { + + $class = $classes[$droit]; + $desc = false; + $s = false; + + if ($cle == 'connexion') + { + if ($droit == Membres::DROIT_AUCUN) + $desc = 'N\'a pas le droit de se connecter'; + else + $desc = 'A le droit de se connecter'; + } + elseif ($cle == 'inscription') + { + if ($droit == Membres::DROIT_AUCUN) + $desc = 'N\'a pas le droit de s\'inscrire seul'; + else + $desc = 'A le droit de s\'inscrire seul'; + } + elseif ($cle == 'config') + { + $s = '☑'; + + if ($droit == Membres::DROIT_AUCUN) + $desc = 'Ne peut modifier la configuration'; + else + $desc = 'Peut modifier la configuration'; + } + elseif ($cle == 'compta') + { + $s = '€'; + } + + if (!$s) + $s = strtoupper($cle[0]); + + if (!$desc) + { + $desc = ucfirst($cle). ' : '; + + if ($droit == Membres::DROIT_AUCUN) + $desc .= 'Pas accès'; + elseif ($droit == Membres::DROIT_ACCES) + $desc .= 'Lecture uniquement'; + elseif ($droit == Membres::DROIT_ECRITURE) + $desc .= 'Lecture & écriture'; + else + $desc .= 'Administration'; + } + + $out[$cle] = ''.$s.''; + } + } + + return implode(' ', $out); +} + +function tpl_format_wiki($str) +{ + $str = utils::htmlLinksOnUrls($str); + $str = utils::htmlSpip($str); + $str = utils::htmlGarbage2xhtml($str); + return $str; +} + +function tpl_liens_wiki($str, $prefix) +{ + return preg_replace_callback('!!i', function ($matches) use ($prefix) { + return ''; + }, $str); +} + +function tpl_pagination($params) +{ + if (!isset($params['url']) || !isset($params['page']) || !isset($params['bypage']) || !isset($params['total'])) + throw new \BadFunctionCallException("Paramètre manquant pour pagination"); + + if ($params['total'] == -1) + return ''; + + $pagination = utils::getGenericPagination($params['page'], $params['total'], $params['bypage']); + + if (empty($pagination)) + return ''; + + $out = ''; + + return $out; +} + +function tpl_diff($params) +{ + if (!isset($params['old']) || !isset($params['new'])) + { + throw new Template_Exception('Paramètres old et new requis.'); + } + + $old = $params['old']; + $new = $params['new']; + + require_once ROOT . '/include/libs/diff/class.simplediff.php'; + $diff = \simpleDiff::diff_to_array(false, $old, $new, 3); + + $out = ''; + $prev = key($diff); + + foreach ($diff as $i=>$line) + { + if ($i > $prev + 1) + { + $out .= ''; + } + + list($type, $old, $new) = $line; + + $class1 = $class2 = ''; + $t1 = $t2 = ''; + + if ($type == \simpleDiff::INS) + { + $class2 = 'ins'; + $t2 = '➕'; + $old = htmlspecialchars($old, ENT_QUOTES, 'UTF-8'); + $new = htmlspecialchars($new, ENT_QUOTES, 'UTF-8'); + } + elseif ($type == \simpleDiff::DEL) + { + $class1 = 'del'; + $t1 = '➖'; + $old = htmlspecialchars($old, ENT_QUOTES, 'UTF-8'); + $new = htmlspecialchars($new, ENT_QUOTES, 'UTF-8'); + } + elseif ($type == \simpleDiff::CHANGED) + { + $class1 = 'del'; + $class2 = 'ins'; + $t1 = '➖'; + $t2 = '➕'; + + $lineDiff = \simpleDiff::wdiff($old, $new); + $lineDiff = htmlspecialchars($lineDiff, ENT_QUOTES, 'UTF-8'); + + // Don't show new things in deleted line + $old = preg_replace('!\{\+(?:.*)\+\}!U', '', $lineDiff); + $old = str_replace(' ', ' ', $old); + $old = str_replace('-] [-', ' ', $old); + $old = preg_replace('!\[-(.*)-\]!U', '\\1', $old); + + // Don't show old things in added line + $new = preg_replace('!\[-(?:.*)-\]!U', '', $lineDiff); + $new = str_replace(' ', ' ', $new); + $new = str_replace('+} {+', ' ', $new); + $new = preg_replace('!\{\+(.*)\+\}!U', '\\1', $new); + } + else + { + $old = htmlspecialchars($old, ENT_QUOTES, 'UTF-8'); + $new = htmlspecialchars($new, ENT_QUOTES, 'UTF-8'); + } + + $out .= ''; + $out .= ''; + $out .= ''; + $out .= ''; + $out .= ''; + $out .= ''; + $out .= ''; + + $prev = $i; + } + + $out .= '

'.($i+1).''.$t1.''.$old.''.$t2.''.$new.'
'; + return $out; +} + +function tpl_select_compte($params) +{ + $name = $params['name']; + $comptes = $params['comptes']; + $selected = isset($params['data'][$params['name']]) ? $params['data'][$params['name']] : utils::post($name); + + $out = ''; + foreach ($config['options'] as $k=>$v) + { + if (is_int($k)) + $k = $v; + + $field .= '' . "\n"; + foreach ($values as $key => $value) + { + $optgroup_html .= tpl_function_html_options_optoutput($tpl, $key, $value, $selected); + } + $optgroup_html .= "\n"; + return $optgroup_html; +} + +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/function.html_radios.php b/include/libs/template_lite/plugins/function.html_radios.php new file mode 100644 index 0000000..7b2f236 --- /dev/null +++ b/include/libs/template_lite/plugins/function.html_radios.php @@ -0,0 +1,55 @@ + + */ +function tpl_function_html_radios($params, &$tpl) +{ + require_once("shared.escape_chars.php"); + $name = null; + $value = ''; + $extra = ''; + + foreach($params as $_key => $_value) + { + switch($_key) + { + case 'name': + case 'value': + $$_key = $_value; + break; + default: + if(!is_array($_key)) + { + $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"'; + } + else + { + throw new Template_Exception("html_radio: attribute '$_key' cannot be an array", $tpl); + } + } + } + + if (!isset($name) || empty($name)) + { + throw new Template_Exception("html_radio: missing 'name' parameter", $tpl); + return; + } + + $toReturn = ''; + return $toReturn; +} +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/function.html_select_date.php b/include/libs/template_lite/plugins/function.html_select_date.php new file mode 100644 index 0000000..c93eb2b --- /dev/null +++ b/include/libs/template_lite/plugins/function.html_select_date.php @@ -0,0 +1,269 @@ +'s of the different / tags. + An example might be in the template: all_extra ='class ="foo"'. */ + $all_extra = null; + /* Separate attributes for the tags. */ + $day_extra = null; + $month_extra = null; + $year_extra = null; + /* Order in which to display the fields. + "D" -> day, "M" -> month, "Y" -> year. */ + $field_order = 'MDY'; + /* String printed between the different fields. */ + $field_separator = "\n"; + $time = time(); + + extract($params); + + // If $time is not in format yyyy-mm-dd + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $time)) + { + // then $time is empty or unix timestamp or mysql timestamp + // using smarty_make_timestamp to get an unix timestamp and + // strftime to make yyyy-mm-dd + $time = strftime('%Y-%m-%d', tpl_make_timestamp($time)); + } + // Now split this in pieces, which later can be used to set the select + $time = explode("-", $time); + + // make syntax "+N" or "-N" work with start_year and end_year + if (preg_match('!^(\+|\-)\s*(\d+)$!', $end_year, $match)) + { + if ($match[1] == '+') + { + $end_year = strftime('%Y') + $match[2]; + } + else + { + $end_year = strftime('%Y') - $match[2]; + } + } + if (preg_match('!^(\+|\-)\s*(\d+)$!', $start_year, $match)) + { + if ($match[1] == '+') + { + $start_year = strftime('%Y') + $match[2]; + } + else + { + $start_year = strftime('%Y') - $match[2]; + } + } + + $field_order = strtoupper($field_order); + $html_result = $month_result = $day_result = $year_result = ""; + + if ($display_months) + { + $month_names = array(); + $month_values = array(); + + for ($i = 1; $i <= 12; $i++) + { + $month_names[] = strftime($month_format, mktime(0, 0, 0, $i, 1, 2000)); + $month_values[] = strftime($month_value_format, mktime(0, 0, 0, $i, 1, 2000)); + } + + $month_result .= ''; + } + + if ($display_days) + { + $days = array(); + for ($i = 1; $i <= 31; $i++) + { + $days[] = sprintf($day_format, $i); + $day_values[] = sprintf($day_value_format, $i); + } + + $day_result .= ''; + } + + if ($display_years) + { + if (null !== $field_array) + { + $year_name = $field_array . '[' . $prefix . 'Year]'; + } + else + { + $year_name = $prefix . 'Year'; + } + if ($year_as_text) + { + $year_result .= ' $years, + 'values' => $years, + 'selected' => $time[0], + 'print_result' => false), + $template_object); + $year_result .= ''; + } + } + + // Loop thru the field_order field + for ($i = 0; $i <= 2; $i++) + { + $c = substr($field_order, $i, 1); + switch ($c) + { + case 'D': + $html_result .= $day_result; + break; + + case 'M': + $html_result .= $month_result; + break; + + case 'Y': + $html_result .= $year_result; + break; + } + // Add the field seperator + if($i != 2) + { + $html_result .= $field_separator; + } + } + return $html_result; +} + +?> diff --git a/include/libs/template_lite/plugins/function.html_select_time.php b/include/libs/template_lite/plugins/function.html_select_time.php new file mode 100644 index 0000000..48c6b8f --- /dev/null +++ b/include/libs/template_lite/plugins/function.html_select_time.php @@ -0,0 +1,177 @@ +'."\n"; + $html_result .= tpl_function_html_options(array('output' => $hours, + 'values' => $hours, + 'selected' => strftime($hour_fmt, $time), + 'print_result' => false), + $template_object); + $html_result .= "\n"; + } + + if ($display_minutes) + { + $all_minutes = range(0, 59); + for ($i = 0, $for_max = count($all_minutes); $i < $for_max; $i+= $minute_interval) + { + $minutes[] = sprintf('%02d', $all_minutes[$i]); + } + $selected = intval(floor(strftime('%M', $time) / $minute_interval) * $minute_interval); + $html_result .= '\n"; + } + + if ($display_seconds) + { + $all_seconds = range(0, 59); + for ($i = 0, $for_max = count($all_seconds); $i < $for_max; $i+= $second_interval) + { + $seconds[] = sprintf('%02d', $all_seconds[$i]); + } + $selected = intval(floor(strftime('%S', $time) / $second_interval) * $second_interval); + $html_result .= '\n"; + } + + if ($display_meridian && !$use_24_hours) + { + $html_result .= '\n"; + } + return $html_result; +} + +?> diff --git a/include/libs/template_lite/plugins/function.html_table.php b/include/libs/template_lite/plugins/function.html_table.php new file mode 100644 index 0000000..1b04dd7 --- /dev/null +++ b/include/libs/template_lite/plugins/function.html_table.php @@ -0,0 +1,88 @@ + + * Purpose: make an html table from an array of data + * Input: loop = array to loop through + * cols = number of columns + * table_attr = table attributes + * tr_attr = table row attributes (arrays are cycled) + * td_attr = table cell attributes (arrays are cycled) + * trailpad = value to pad trailing cells with + * + * Examples: {table loop=$data} + * {$table loop=$data cols=4 tr_attr='"bgcolor=red"'} + * {$table loop=$data cols=4 tr_attr=$colors} + * Taken from the original Smarty + * http://smarty.php.net + * ------------------------------------------------------------- + */ +function tpl_function_html_table($params, &$template_object) +{ + $table_attr = 'border="1"'; + $tr_attr = ''; + $td_attr = ''; + $cols = 3; + $trailpad = ' '; + + extract($params); + + if (!isset($loop)) + { + throw new Template_Exception("html_table: missing 'loop' parameter", $template_object); + return; + } + + $output = "\n"; + $output .= "\n"; + + for($x = 0, $y = count($loop); $x < $y; $x++) + { + $output .= "\n"; + if((!(($x+1) % $cols)) && $x < $y-1) + { + // go to next row + $output .= "\n\n"; + } + if($x == $y-1) + { + // last row, pad remaining cells + $cells = $cols - $y % $cols; + if($cells != $cols) { + for($padloop = 0; $padloop < $cells; $padloop++) { + $output .= "\n"; + } + } + $output .= "\n"; + } + } + $output .= "
" . $loop[$x] . "
$trailpad
\n"; + return $output; +} + +function tpl_function_html_table_cycle($name, $var) +{ + static $names = array(); + + if(!is_array($var)) + { + return $var; + } + + if(!isset($names[$name]) || $names[$name] == count($var)-1) + { + $names[$name] = 0; + return $var[0]; + } + + $names[$name]++; + return $var[$names[$name]]; +} + +?> diff --git a/include/libs/template_lite/plugins/function.html_textbox.php b/include/libs/template_lite/plugins/function.html_textbox.php new file mode 100644 index 0000000..da6dbd4 --- /dev/null +++ b/include/libs/template_lite/plugins/function.html_textbox.php @@ -0,0 +1,51 @@ + + */ +function tpl_function_html_textbox($params, &$tpl) +{ + require_once("shared.escape_chars.php"); + $name = null; + $value = ''; + $extra = ''; + + foreach($params as $_key => $_value) + { + switch($_key) + { + case 'name': + case 'value': + $$_key = $_value; + break; + default: + if(!is_array($_key)) + { + $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"'; + } + else + { + throw new Template_Exception("html_textbox: attribute '$_key' cannot be an array", $tpl); + } + } + } + + if (!isset($name) || empty($name)) + { + throw new Template_Exception("html_textbox: missing 'name' parameter", $tpl); + return; + } + + $toReturn = ''; + return $toReturn; +} +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/function.in_array.php b/include/libs/template_lite/plugins/function.in_array.php new file mode 100644 index 0000000..a37aa9e --- /dev/null +++ b/include/libs/template_lite/plugins/function.in_array.php @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/include/libs/template_lite/plugins/function.mailto.php b/include/libs/template_lite/plugins/function.mailto.php new file mode 100644 index 0000000..af9273f --- /dev/null +++ b/include/libs/template_lite/plugins/function.mailto.php @@ -0,0 +1,148 @@ + + * Credits: Jason Sweat (added cc, bcc and subject functionality) + * Purpose: automate mailto address link creation, and optionally + * encode them. + * Input: address = e-mail address + * text = (optional) text to display, default is address + * encode = (optional) can be one of: + * none : no encoding (default) + * javascript : encode with javascript + * hex : encode with hexidecimal (no javascript) + * cc = (optional) address(es) to carbon copy + * bcc = (optional) address(es) to blind carbon copy + * subject = (optional) e-mail subject + * newsgroups = (optional) newsgroup(s) to post to + * followupto = (optional) address(es) to follow up to + * extra = (optional) extra tags for the href link + * + * Examples: {mailto address="me@domain.com"} + * {mailto address="me@domain.com" encode="javascript"} + * {mailto address="me@domain.com" encode="hex"} + * {mailto address="me@domain.com" subject="Hello to you!"} + * {mailto address="me@domain.com" cc="you@domain.com,they@domain.com"} + * {mailto address="me@domain.com" extra='class="mailto"'} + * Taken from the original Smarty + * http://smarty.php.net + * ------------------------------------------------------------- + */ +function tpl_function_mailto($params, &$template_object) +{ + extract($params); + + if (empty($address)) + { + throw new Template_Exception("mailto: missing 'address' parameter", $template_object); + return; + } + + if (empty($text)) + { + $text = $address; + } + + if (empty($extra)) + { + $extra = ""; + } + + // netscape and mozilla do not decode %40 (@) in BCC field (bug?) + // so, don't encode it. + + $mail_parms = array(); + if (!empty($cc)) + { + $mail_parms[] = 'cc='.str_replace('%40','@',rawurlencode($cc)); + } + + if (!empty($bcc)) + { + $mail_parms[] = 'bcc='.str_replace('%40','@',rawurlencode($bcc)); + } + + if (!empty($subject)) + { + $mail_parms[] = 'subject='.rawurlencode($subject); + } + + if (!empty($newsgroups)) + { + $mail_parms[] = 'newsgroups='.rawurlencode($newsgroups); + } + + if (!empty($followupto)) + { + $mail_parms[] = 'followupto='.str_replace('%40','@',rawurlencode($followupto)); + } + + $mail_parm_vals = ""; + for ($i=0; $i'.$text.'\');'; + $js_encode = ''; + for ($x=0; $x < strlen($string); $x++) + { + $js_encode .= '%' . bin2hex($string[$x]); + } + return ''; + } + elseif ($encode == 'hex') + { + preg_match('!^(.*)(\?.*)$!',$address,$match); + if(!empty($match[2])) + { + throw new Template_Exception("mailto: hex encoding does not work with extra attributes. Try javascript.", $template_object); + return; + } + $address_encode = ""; + for ($x=0; $x < strlen($address); $x++) + { + if(preg_match('!\w!',$address[$x])) + { + $address_encode .= '%' . bin2hex($address[$x]); + } + else + { + $address_encode .= $address[$x]; + } + } + $text_encode = ""; + for ($x=0; $x < strlen($text); $x++) + { + $text_encode .= '&#x' . bin2hex($text[$x]).';'; + } + return ''.$text_encode.''; + } + else + { + // no encoding + return ''.$text.''; + } +} + +?> diff --git a/include/libs/template_lite/plugins/function.math.php b/include/libs/template_lite/plugins/function.math.php new file mode 100644 index 0000000..7369ef7 --- /dev/null +++ b/include/libs/template_lite/plugins/function.math.php @@ -0,0 +1,90 @@ + $val) + { + if ($key != "equation" && $key != "format" && $key != "assign") + { + // make sure value is not empty + if (strlen($val)==0) + { + throw new Template_Exception("math: parameter $key is empty", $template_object); + return; + } + if (!is_numeric($val)) + { + throw new Template_Exception("math: parameter $key: is not numeric", $template_object); + return; + } + $equation = preg_replace("/\b$key\b/",$val, $equation); + } + } + + eval("\$template_object_math_result = ".$equation.";"); + + if (empty($params['format'])) + { + if (empty($params['assign'])) + { + return $template_object_math_result; + } + else + { + $template_object->assign($params['assign'],$template_object_math_result); + } + } + else + { + if (empty($params['assign'])) + { + printf($params['format'],$template_object_math_result); + } + else + { + $template_object->assign($params['assign'],sprintf($params['format'],$template_object_math_result)); + } + } +} + +?> diff --git a/include/libs/template_lite/plugins/function.popup.php b/include/libs/template_lite/plugins/function.popup.php new file mode 100644 index 0000000..3f68fcc --- /dev/null +++ b/include/libs/template_lite/plugins/function.popup.php @@ -0,0 +1,81 @@ + diff --git a/include/libs/template_lite/plugins/function.popup_init.php b/include/libs/template_lite/plugins/function.popup_init.php new file mode 100644 index 0000000..bbcd2b7 --- /dev/null +++ b/include/libs/template_lite/plugins/function.popup_init.php @@ -0,0 +1,32 @@ +' . "\n" + . '' . "\n"; + } + else + { + throw new Template_Exception("popup_init: missing src parameter", $template_object); + } +} + +?> diff --git a/include/libs/template_lite/plugins/function.resize_image.php b/include/libs/template_lite/plugins/function.resize_image.php new file mode 100644 index 0000000..206a0df --- /dev/null +++ b/include/libs/template_lite/plugins/function.resize_image.php @@ -0,0 +1,239 @@ + + *
+ * {resize_image img_src="/thumbnails/" directory="/html/mysite/ad_images/" thumbdir="/html/mysite/thumbnails/" filename="Myfile.jpg" xscale="150" yscale="200" thumbname="thumb_"}
+ * 
+ * + * Output image + * + * Author: Rick Thomson rick@oznet.com + * Author: Mark Dickenson akapanamajack@sourceforge.net +*/ + +// Calculate percentage between width and height +function tpl_function_resize_percent($maximum, $current) +{ + return (real)(100 * ($maximum / $current)); +} + +function tpl_function_resize_unpercent($percent, $whole) +{ + return (real)(($percent * $whole) / 100); +} + +function tpl_function_resize_image($params, &$tpl) +{ + extract($params); + + if (empty($directory)) + { + throw new Template_Exception("resize_image: missing 'directory' parameter", $tpl); + } + + if (empty($thumbdir)) + { + $thumbdir = $directory; + } + + if (empty($filename)) + { + throw new Template_Exception("resize_image: missing 'filename' parameter", $tpl); + } + + if (empty($xscale)) + { + $xscale = 2000; + } + $maximagewidth=$xscale; + + if (empty($xscale)) + { + $yscale = 2000; + } + $maximageheight=$yscale; + + if (empty($alt)) + { + $alt = "image"; + } + + if (empty($border)) + { + $border = 0; + } + + if (empty($daystokeep)) + { + $daystokeep = 5; + } + + if (!function_exists('gd_info')) + { + throw new Template_Exception("resize_image: the GD library is not installed", $tpl); + } + + if(!file_exists($directory . $filename) && !empty($url) && function_exists('curl_init')) + { + $ch = curl_init ($url . $filename); + $fp = fopen ($directory . $filename, "w"); + curl_setopt ($ch, CURLOPT_FILE, $fp); + curl_setopt ($ch, CURLOPT_HEADER, 0); + curl_exec ($ch); + curl_close ($ch); + fclose ($fp); + } + + if(file_exists($directory . $filename)) + { + $imageinfo = @getimagesize($directory . $filename); + if(empty($imageinfo)) + { + return; + } + + if ($returntype == 1) + { + $imagewidth = $imageinfo[0]; + $imageheight = $imageinfo[1]; + + if($maximagewidth < $imagewidth) { + $imageheight = tpl_function_resize_unpercent(tpl_function_resize_percent($maximagewidth, $imagewidth), $imageheight); + $imagewidth = $maximagewidth; + } + + if($maximageheight < $imageheight) { + $imagewidth = tpl_function_resize_unpercent(tpl_function_resize_percent($maximageheight, $imageheight), $imagewidth); + $imageheight = $maximageheight; + } + return "\"""; + } + + if (empty($thumbname)) + { + throw new Template_Exception("resize_image: missing 'thumbname' parameter", $tpl); + } + + $now=urlencode(date("F j, Y, g:i a")); + + $newimagepath = $thumbdir . $thumbname . $filename; + + $newthumbnail = 0; + if(!file_exists($newimagepath)) + { + copy($directory . $filename, $newimagepath); + $newthumbnail = 1; + } + + $datechanged = date("j", time()) - date("j", filemtime($newimagepath)); + if(($datechanged > -$daystokeep && $datechanged < $daystokeep) && $newthumbnail = 0) + { + // Do not rebuild + $imagewidth = $imageinfo[0]; + $imageheight = $imageinfo[1]; + + if($maximagewidth < $imagewidth) + { + $imageheight = tpl_function_resize_unpercent(tpl_function_resize_percent($maximagewidth, $imagewidth), $imageheight); + $imagewidth = $maximagewidth; + } + + if($maximageheight < $imageheight) + { + $imagewidth = tpl_function_resize_unpercent(tpl_function_resize_percent($maximageheight, $imageheight), $imagewidth); + $imageheight = $maximageheight; + } + } + else + { + // rebuild + copy($directory . $filename, $newimagepath); + + $imagewidth = $imageinfo[0]; + $imageheight = $imageinfo[1]; + + if($maximagewidth < $imagewidth) + { + $imageheight = tpl_function_resize_unpercent(tpl_function_resize_percent($maximagewidth, $imagewidth), $imageheight); + $imagewidth = $maximagewidth; + } + + if($maximageheight < $imageheight) + { + $imagewidth = tpl_function_resize_unpercent(tpl_function_resize_percent($maximageheight, $imageheight), $imagewidth); + $imageheight = $maximageheight; + } + $imagewidth = round($imagewidth); + $imageheight = round($imageheight); + $scale = $imagewidth . "x" . $imageheight . "!"; + + if (empty($binpath)) + { + if($imageinfo[2] == 1) + { + $sourceimage = imagecreatefromgif($directory . $filename); + } + elseif($imageinfo[2] == 2) + { + $sourceimage = imagecreatefromjpeg($directory . $filename); + } + elseif($imageinfo[2] == 3) + { + $sourceimage = imagecreatefrompng($directory . $filename); + } + + $destinationimage = imagecreatetruecolor($imagewidth, $imageheight); + imagecopyresized($destinationimage, $sourceimage, 0, 0, 0, 0, $imagewidth, $imageheight, $imageinfo[0], $imageinfo[1]); + if($imageinfo[2] == 1) + { + imagegif($destinationimage, $newimagepath); + } + elseif($imageinfo[2] == 2) + { + imageJPEG($destinationimage, $newimagepath, 75); + } + elseif($imageinfo[2] == 3) + { + imagepng($destinationimage, $newimagepath); + } + imagedestroy($sourceimage); + imagedestroy($destinationimage); + } + else + { + if ($imageinfo[2] == 2) + { + system( $binpath . "mogrify -quality 75 -geometry $scale $newimagepath"); + } + else + { + system( $binpath . "mogrify -geometry $scale $newimagepath"); + } + } + } + + return "\"""; + } +} +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.bbcode2html.php b/include/libs/template_lite/plugins/modifier.bbcode2html.php new file mode 100644 index 0000000..b1c6f76 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.bbcode2html.php @@ -0,0 +1,44 @@ + + * - string: data to convert + */ +function tpl_modifier_bbcode2html($data) +{ + $data = nl2br(stripslashes(addslashes($data))); + + $search = array("\n", "\r", "[b]", "[/b]", "[i]", "[/i]", "[u]", "[/u]"); + $replace = array("", "", "", "", "", "", "", ""); + $data = str_replace($search, $replace, $data); + + $search = array( + "/\[email\](.*?)\[\/email\]/si", + "/\[email=(.*?)\](.*?)\[\/email\]/si", + "/\[url\](.*?)\[\/url\]/si", + "/\[url=(.*?)\](.*?)\[\/url\]/si", + "/\[img\](.*?)\[\/img\]/si", + "/\[code\](.*?)\[\/code\]/si", + "/\[pre\](.*?)\[\/pre\]/si", + "/\[list\](.*?)\[\/list\]/si", + "/\[\*\](.*?)/si" + ); + $replace = array( + "\\1", + "\\2", + "\\1", + "\\2", + "", + "

code:
\\1


", + "
\\1
", + "
    \\1
", + "
  • \\1
  • " + ); + $data = preg_replace($search, $replace, $data); + return $data; +} +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.capitalize.php b/include/libs/template_lite/plugins/modifier.capitalize.php new file mode 100644 index 0000000..8797d8c --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.capitalize.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.cat.php b/include/libs/template_lite/plugins/modifier.cat.php new file mode 100644 index 0000000..ee655c5 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.cat.php @@ -0,0 +1,31 @@ + + * Name: cat
    + * Date: Feb 24, 2003 + * Purpose: catenate a value to a variable + * Input: string to catenate + * Example: {$var|cat:"foo"} + * @link http://smarty.php.net/manual/en/language.modifier.cat.php cat + * (Smarty online manual) + * @author Monte Ohrt + * @version 1.0 + * @param string + * @param string + * @return string + */ +function tpl_modifier_cat($string, $cat) +{ + return $string . $cat; +} + +?> diff --git a/include/libs/template_lite/plugins/modifier.count_characters.php b/include/libs/template_lite/plugins/modifier.count_characters.php new file mode 100644 index 0000000..56e98d3 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.count_characters.php @@ -0,0 +1,32 @@ + + * Name: count_characteres
    + * Purpose: count the number of characters in a text + * @link http://smarty.php.net/manual/en/language.modifier.count.characters.php + * count_characters (Smarty online manual) + * @author Monte Ohrt + * @param string + * @param boolean include whitespace in the character count + * @return integer + */ +function tpl_modifier_count_characters($string, $include_spaces = false) +{ + if ($include_spaces) + { + return(strlen($string)); + } + + return preg_match_all("/[^\s]/",$string, $match); +} + +?> diff --git a/include/libs/template_lite/plugins/modifier.count_paragraphs.php b/include/libs/template_lite/plugins/modifier.count_paragraphs.php new file mode 100644 index 0000000..bf7cef9 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.count_paragraphs.php @@ -0,0 +1,27 @@ + + * Name: count_paragraphs
    + * Purpose: count the number of paragraphs in a text + * @link http://smarty.php.net/manual/en/language.modifier.count.paragraphs.php + * count_paragraphs (Smarty online manual) + * @author Monte Ohrt + * @param string + * @return integer + */ +function tpl_modifier_count_paragraphs($string) +{ + // count \r or \n characters + return count(preg_split('/[\r\n]+/', $string)); +} + +?> diff --git a/include/libs/template_lite/plugins/modifier.count_sentences.php b/include/libs/template_lite/plugins/modifier.count_sentences.php new file mode 100644 index 0000000..80fb6a0 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.count_sentences.php @@ -0,0 +1,27 @@ + + * Name: count_sentences + * Purpose: count the number of sentences in a text + * @link http://smarty.php.net/manual/en/language.modifier.count.paragraphs.php + * count_sentences (Smarty online manual) + * @author Monte Ohrt + * @param string + * @return integer + */ +function tpl_modifier_count_sentences($string) +{ + // find periods with a word before but not after. + return preg_match_all('/[^\s]\.(?!\w)/', $string, $match); +} + +?> diff --git a/include/libs/template_lite/plugins/modifier.count_words.php b/include/libs/template_lite/plugins/modifier.count_words.php new file mode 100644 index 0000000..0e49474 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.count_words.php @@ -0,0 +1,31 @@ + + * Name: count_words
    + * Purpose: count the number of words in a text + * @link http://smarty.php.net/manual/en/language.modifier.count.words.php + * count_words (Smarty online manual) + * @author Monte Ohrt + * @param string + * @return integer + */ +function tpl_modifier_count_words($string) +{ + // split text by ' ',\r,\n,\f,\t + $split_array = preg_split('/\s+/',$string); + // count matches that contain alphanumerics + $word_count = preg_grep('/[a-zA-Z0-9\\x80-\\xff]/', $split_array); + + return count($word_count); +} + +?> diff --git a/include/libs/template_lite/plugins/modifier.date.php b/include/libs/template_lite/plugins/modifier.date.php new file mode 100644 index 0000000..9d2d628 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.date.php @@ -0,0 +1,63 @@ + 0) + { + return $time; + } + else + { + return time(); + } + } +} +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.date_format.php b/include/libs/template_lite/plugins/modifier.date_format.php new file mode 100644 index 0000000..762562e --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.date_format.php @@ -0,0 +1,64 @@ + 0) + { + return $time; + } + else + { + return time(); + } + } +} +?> diff --git a/include/libs/template_lite/plugins/modifier.debug_print_var.php b/include/libs/template_lite/plugins/modifier.debug_print_var.php new file mode 100644 index 0000000..6ad7a52 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.debug_print_var.php @@ -0,0 +1,54 @@ +Array (".count($var).")"; + foreach ($var as $curr_key => $curr_val) + { + $return = tpl_modifier_debug_print_var($curr_val, $depth+1, $length); + $results .= '
    \r'.str_repeat(' ', $depth*2)."$curr_key => $return"; + } + return $results; + } + else if (is_object($var)) + { + $object_vars = get_object_vars($var); + $results = "".get_class($var)." Object (".count($object_vars).")"; + foreach ($object_vars as $curr_key => $curr_val) + { + $return = tpl_modifier_debug_print_var($curr_val, $depth+1, $length); + $results .= '
    \r'.str_repeat(' ', $depth*2)."$curr_key => $return"; + } + return $results; + } + else + { + if (empty($var) && $var != "0") + { + return 'empty'; + } + if (strlen($var) > $length ) + { + $results = substr($var, 0, $length-3).'...'; + } + else + { + $results = $var; + } + $results = preg_replace("![\r\t\n]!", " ", $results); + $results = htmlspecialchars($results); + return $results; + } +} + +?> diff --git a/include/libs/template_lite/plugins/modifier.default.php b/include/libs/template_lite/plugins/modifier.default.php new file mode 100644 index 0000000..0dcaf6d --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.default.php @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.escape.php b/include/libs/template_lite/plugins/modifier.escape.php new file mode 100644 index 0000000..d04c408 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.escape.php @@ -0,0 +1,102 @@ + + * Name: escape
    + * Purpose: Escape the string according to escapement type + * @link http://smarty.php.net/manual/en/language.modifier.escape.php + * escape (Smarty online manual) + * @author Monte Ohrt + * @param string + * @param html|htmlall|url|quotes|hex|hexentity|javascript + * @return string + */ +function tpl_modifier_escape($string, $esc_type = 'html', $char_set = 'ISO-8859-1', $double_encode = true) +{ + switch ($esc_type) + { + case 'html': + if (version_compare(PHP_VERSION, '5.2.3') === 1) + return htmlspecialchars($string, ENT_QUOTES, $char_set, $double_encode); + else + return htmlspecialchars($string, ENT_QUOTES, $char_set); + + case 'htmlall': + if (version_compare(PHP_VERSION, '5.2.3') === 1) + return htmlentities($string, ENT_QUOTES, $char_set, $double_encode); + else + return htmlentities($string, ENT_QUOTES, $char_set); + + case 'url': + return rawurlencode($string); + + case 'urlpathinfo': + return str_replace('%2F','/',rawurlencode($string)); + + case 'quotes': + // escape unescaped single quotes + return preg_replace("%(?'\\\\',"'"=>"\\'",'"'=>'\\"',"\r"=>'\\r',"\n"=>'\\n',''<\/')); + + case 'mail': + // safe way to display e-mail address on a web page + return str_replace(array('@', '.'),array(' [AT] ', ' [DOT] '), $string); + + case 'nonstd': + // escape non-standard chars, such as ms document quotes + $_res = ''; + for($_i = 0, $_len = strlen($string); $_i < $_len; $_i++) + { + $_ord = ord(substr($string, $_i, 1)); + // non-standard char, escape it + if($_ord >= 126) + { + $_res .= '&#' . $_ord . ';'; + } + else + { + $_res .= substr($string, $_i, 1); + } + } + return $_res; + + default: + return $string; + } +} + +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.indent.php b/include/libs/template_lite/plugins/modifier.indent.php new file mode 100644 index 0000000..46661c4 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.indent.php @@ -0,0 +1,28 @@ + + * Name: indent
    + * Purpose: indent lines of text + * @link http://smarty.php.net/manual/en/language.modifier.indent.php + * indent (Smarty online manual) + * @author Monte Ohrt + * @param string + * @param integer + * @param string + * @return string + */ +function tpl_modifier_indent($string,$chars=4,$char=" ") +{ + return preg_replace('!^!m',str_repeat($char,$chars),$string); +} + +?> diff --git a/include/libs/template_lite/plugins/modifier.lower.php b/include/libs/template_lite/plugins/modifier.lower.php new file mode 100644 index 0000000..b59e74f --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.lower.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.regex_replace.php b/include/libs/template_lite/plugins/modifier.regex_replace.php new file mode 100644 index 0000000..d988429 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.regex_replace.php @@ -0,0 +1,33 @@ + + * Name: regex_replace
    + * Purpose: regular expression search/replace + * @link http://smarty.php.net/manual/en/language.modifier.regex.replace.php + * regex_replace (Smarty online manual) + * @author Monte Ohrt + * @param string + * @param string|array + * @param string|array + * @return string + */ +function tpl_modifier_regex_replace($string, $search, $replace) +{ + if (preg_match('!([a-zA-Z\s]+)$!s', $search, $match) && (strpos($match[1], 'e') !== false)) + { + /* remove eval-modifier from $search */ + $search = substr($search, 0, -strlen($match[1])) . preg_replace('![e\s]+!', '', $match[1]); + } + return preg_replace($search, $replace, $string); +} + +?> diff --git a/include/libs/template_lite/plugins/modifier.replace.php b/include/libs/template_lite/plugins/modifier.replace.php new file mode 100644 index 0000000..059fa70 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.replace.php @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.spacify.php b/include/libs/template_lite/plugins/modifier.spacify.php new file mode 100644 index 0000000..3b72a17 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.spacify.php @@ -0,0 +1,27 @@ + + * Name: spacify
    + * Purpose: add spaces between characters in a string + * @link http://smarty.php.net/manual/en/language.modifier.spacify.php + * spacify (Smarty online manual) + * @author Monte Ohrt + * @param string + * @param string + * @return string + */ +function tpl_modifier_spacify($string, $spacify_char = ' ') +{ + return implode($spacify_char, preg_split('//', $string, -1, PREG_SPLIT_NO_EMPTY)); +} + +?> diff --git a/include/libs/template_lite/plugins/modifier.string_format.php b/include/libs/template_lite/plugins/modifier.string_format.php new file mode 100644 index 0000000..3d777b1 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.string_format.php @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.strip.php b/include/libs/template_lite/plugins/modifier.strip.php new file mode 100644 index 0000000..8dcc118 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.strip.php @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.truncate.php b/include/libs/template_lite/plugins/modifier.truncate.php new file mode 100644 index 0000000..24b8025 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.truncate.php @@ -0,0 +1,34 @@ + $length) + { + $length -= strlen($etc); + if (!$break_words) + { + $string = preg_replace('/\s+?(\S+)?$/', '', substr($string, 0, $length+1)); + } + return substr($string, 0, $length).$etc; + } + else + { + return $string; + } +} +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/modifier.upper.php b/include/libs/template_lite/plugins/modifier.upper.php new file mode 100644 index 0000000..d077830 --- /dev/null +++ b/include/libs/template_lite/plugins/modifier.upper.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/include/libs/template_lite/plugins/outputfilter.gzip.php b/include/libs/template_lite/plugins/outputfilter.gzip.php new file mode 100644 index 0000000..7ef473b --- /dev/null +++ b/include/libs/template_lite/plugins/outputfilter.gzip.php @@ -0,0 +1,61 @@ +enable_gzip = 0 output is not compressed $template_object->enable_gzip = 1 output is compressed + */ + +function template_outputfilter_gzip($tpl_source, &$template_object) +{ + static $_tpl_saved = ''; + + $gzipped = 0; + if($template_object->enable_gzip) + { + if(extension_loaded("zlib") && !get_cfg_var('zlib.output_compression') && !$template_object->cache && (strstr($_SERVER["HTTP_ACCEPT_ENCODING"],"gzip") || $template_object->force_compression)) + { + $_tpl_saved .= $tpl_source . "\n\n\n"; + $tpl_source = ""; + + if($template_object->send_now == 1) + { + $gzipped = 1; + $tpl_source = gzencode($_tpl_saved, $template_object->compression_level); + $_tpl_saved = ""; + } + } + } + else + { + if(!$template_object->caching && !get_cfg_var('zlib.output_compression')) + { + $_tpl_saved .= $tpl_source."\n\n\n"; + $tpl_source = ""; + + if($template_object->send_now == 1) + { + $tpl_source = $_tpl_saved; + $_tpl_saved = ""; + } + } + } + + if($template_object->send_now == 1 && $template_object->enable_gzip == 1) + { + if($gzipped == 1) + { + header("Content-Encoding: gzip"); + header("Content-Length: " . strlen($tpl_source)); + } + } + + return $tpl_source; +} +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/outputfilter.trimwhitespace.php b/include/libs/template_lite/plugins/outputfilter.trimwhitespace.php new file mode 100644 index 0000000..38a9bff --- /dev/null +++ b/include/libs/template_lite/plugins/outputfilter.trimwhitespace.php @@ -0,0 +1,81 @@ + + * Type: outputfilter
    + * Name: trimwhitespace
    + * Date: Jan 25, 2003
    + * Purpose: trim leading white space and blank lines from + * template source after it gets interpreted, cleaning + * up code and saving bandwidth. Does not affect + * <
    >
    and blocks.
    + * Install: Drop into the plugin directory, call + * $template_object->load_filter('output','trimwhitespace'); + * from application. + * @author Monte Ohrt + * @author Contributions from Lars Noschinski + * @version 1.3 + * @param string + * @param Smarty + */ + +function template_outputfilter_trimwhitespace($tpl_source, &$template_object) +{ + // Pull out the script blocks + preg_match_all("!]+>.*?!is", $tpl_source, $match); + $_script_blocks = $match[0]; + $tpl_source = preg_replace("!]+>.*?!is", + '@@@TEMPLATELITE:TRIM:SCRIPT@@@', $tpl_source); + + // Pull out the pre blocks + preg_match_all("!
    .*?
    !is", $tpl_source, $match); + $_pre_blocks = $match[0]; + $tpl_source = preg_replace("!
    .*?
    !is", + '@@@TEMPLATELITE:TRIM:PRE@@@', $tpl_source); + + // Pull out the textarea blocks + preg_match_all("!]+>.*?!is", $tpl_source, $match); + $_textarea_blocks = $match[0]; + $tpl_source = preg_replace("!]+>.*?!is", + '@@@TEMPLATELITE:TRIM:TEXTAREA@@@', $tpl_source); + + // remove all leading spaces, tabs and carriage returns NOT + // preceeded by a php close tag. + $tpl_source = trim(preg_replace('/((?)\n)[\s]+/m', '\1', $tpl_source)); + + // replace script blocks + template_outputfilter_trimwhitespace_replace("@@@TEMPLATELITE:TRIM:SCRIPT@@@",$_script_blocks, $tpl_source); + + // replace pre blocks + template_outputfilter_trimwhitespace_replace("@@@TEMPLATELITE:TRIM:PRE@@@",$_pre_blocks, $tpl_source); + + // replace textarea blocks + template_outputfilter_trimwhitespace_replace("@@@TEMPLATELITE:TRIM:TEXTAREA@@@",$_textarea_blocks, $tpl_source); + + return $tpl_source; +} + +function template_outputfilter_trimwhitespace_replace($search_str, $replace, &$subject) { + $_len = strlen($search_str); + $_pos = 0; + for ($_i=0, $_count=count($replace); $_i<$_count; $_i++) + { + if (($_pos=strpos($subject, $search_str, $_pos))!==false) + { + $subject = substr_replace($subject, $replace[$_i], $_pos, $_len); + } + else + { + break; + } + } +} + +?> diff --git a/include/libs/template_lite/plugins/postfilter.showtemplatevars.php b/include/libs/template_lite/plugins/postfilter.showtemplatevars.php new file mode 100644 index 0000000..108a067 --- /dev/null +++ b/include/libs/template_lite/plugins/postfilter.showtemplatevars.php @@ -0,0 +1,16 @@ +\n_vars); ?>\n" . $compiled; + return $compiled; + } +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/prefilter.jstrip.php b/include/libs/template_lite/plugins/prefilter.jstrip.php new file mode 100644 index 0000000..9b24cb2 --- /dev/null +++ b/include/libs/template_lite/plugins/prefilter.jstrip.php @@ -0,0 +1,130 @@ +"; + } + + //line break + if ($c[$i]=="\n" OR $c[$i]=="\r") + { + //is the current line finished ? + // ")" and "}" is not OK ! (var x=function a() {}.......var ) + $finishers=array(";","{","(",",","\n",":"); + if (in_array($last,$finishers)) + { + $s=false; + } + } + + //a space ! can we cut it ? + if ($c[$i]==" " OR $c[$i]=="\t") + { + $cutme=array(" ","\t","}","{",")","(","[","]","<",">","=",";","+","-","/","*","\n",":","&"); + if (in_array($c[$i-1],$cutme) OR in_array($c[$i+1],$cutme)) + { + $s=false; + } + } + //todo : rename vars/functions !! + } + } + //save the character + if ($s AND $comment==0) + { + $o.=$c[$i]; + $last=$c[$i]; + } + } + + if ($literal) + { + return "{literal}".$o."{/literal}"; + } + else + { + return $o; + } +} + +?>?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/prefilter.showinfoheader.php b/include/libs/template_lite/plugins/prefilter.showinfoheader.php new file mode 100644 index 0000000..11f96a3 --- /dev/null +++ b/include/libs/template_lite/plugins/prefilter.showinfoheader.php @@ -0,0 +1,23 @@ + + * ------------------------------------------------------------- + */ + + function template_prefilter_showinfoheader($tpl_source, &$template_object) + { + return ''."\n\n".$tpl_source; + } +?> \ No newline at end of file diff --git a/include/libs/template_lite/plugins/shared.escape_chars.php b/include/libs/template_lite/plugins/shared.escape_chars.php new file mode 100644 index 0000000..777884e --- /dev/null +++ b/include/libs/template_lite/plugins/shared.escape_chars.php @@ -0,0 +1,18 @@ + diff --git a/include/libs/template_lite/plugins/shared.make_timestamp.php b/include/libs/template_lite/plugins/shared.make_timestamp.php new file mode 100644 index 0000000..7bc7f1e --- /dev/null +++ b/include/libs/template_lite/plugins/shared.make_timestamp.php @@ -0,0 +1,40 @@ + 0) + { + return $time; + } + else + { + return time(); + } +} + +?> diff --git a/include/libs/template_lite/tests/parser.php b/include/libs/template_lite/tests/parser.php new file mode 100644 index 0000000..7023313 --- /dev/null +++ b/include/libs/template_lite/tests/parser.php @@ -0,0 +1,51 @@ +debug[] = array('Processing string', $content); + return parent::processString($content); + } + + public function processModifier($name, $content, $arguments, $map_array) + { + $this->debug[] = array('Processing modifier', $name, $content, $arguments); + return parent::processModifier($name, $content, $arguments, $map_array); + } + + public function processVariable($name) + { + $this->debug[] = array('Processing variable', $name); + return parent::processVariable($name); + } + + public function testArgs($args) + { + return $this->parseArguments($args); + } +} + +$test = new Template_Tester; + +$args = 'truc="miam $blu\' oh" miam="ah `$bla|blu`" bla=$bla|blu autre=$a|bb|cat:$miam|escape uh=bla::blou()'; + +print_r($test->testArgs($args)); + +foreach (token_get_all('') as $line) +{ + echo "\n"; + if (is_array($line)) + { + echo token_name($line[0]) . ": "; + echo $line[1]; + echo "\t"; + } + echo str_replace("\n", "", print_r($line, true)); +} + +?> \ No newline at end of file diff --git a/include/libs/template_lite/tests/tokenparser.php b/include/libs/template_lite/tests/tokenparser.php new file mode 100644 index 0000000..0f6f469 --- /dev/null +++ b/include/libs/template_lite/tests/tokenparser.php @@ -0,0 +1,46 @@ +test($foo)|miam:"bla blu blou $t"|escape:Truc::getInstance()->miam( $foo )'; +$t = '\'miam coucou c"est marrant `$blu`s oh\''; +//$t = 'foo123($foo,$foo->bar(),"foo")'; +//$t = '$foo|bar'; + +$result = $parser->parseArgumentContent($t); +var_dump($result); + +exit; + +$args = 'first="Bla::`$blou`" truc="miam coucou c\'est marrant $blu\' oh" miam="ah `$bla|blu`" bla=$bla|blu autre=$a|bb|cat:$miam|escape uh=bla::blou()'; + +echo '
    ';
    +
    +print_r($parser->parseArguments($args));
    +$parser->parseTokens($args);
    +
    +/*
    +$content = '
    +
    +{literal}
    +
    +Miam
    +
    +function ()
    +{
    +}
    +
    +{/literal}
    +
    +
    +
    +';
    +
    +$tp = new Template_Parser;
    +echo $tp->Parse($content);*/
    +
    +?>
    \ No newline at end of file
    diff --git a/index.php b/index.php
    new file mode 100644
    index 0000000..3bbf58a
    --- /dev/null
    +++ b/index.php
    @@ -0,0 +1,13 @@
    +='))
    +{
    +	die('PHP 5.4.0 ou supérieur est nécessaire au fonctionnement de Garradin.');
    +}
    +
    +header('Location: www/');
    diff --git a/plugins/index.html b/plugins/index.html
    new file mode 100644
    index 0000000..9a31a28
    --- /dev/null
    +++ b/plugins/index.html
    @@ -0,0 +1 @@
    +404 Not Found

    Not Found

    The requested URL was not found on this server.

    \ No newline at end of file diff --git a/templates/admin/_foot.tpl b/templates/admin/_foot.tpl new file mode 100644 index 0000000..6c65910 --- /dev/null +++ b/templates/admin/_foot.tpl @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/templates/admin/_head.tpl b/templates/admin/_head.tpl new file mode 100644 index 0000000..6654c4b --- /dev/null +++ b/templates/admin/_head.tpl @@ -0,0 +1,112 @@ + + + + + {$title|escape} + + + + + {if isset($js)} + + {/if} + {if isset($custom_js)} + {foreach from=$custom_js item="js"} + + {/foreach} + {/if} + {if isset($plugin_css)} + {foreach from=$plugin_css item="css"} + + {/foreach} + {/if} + {if isset($plugin_js)} + {foreach from=$plugin_js item="hs"} + + {/foreach} + {/if} + + + + +{if empty($is_popup)} +
    + + +

    {$title|escape}

    +
    +{/if} + +
    \ No newline at end of file diff --git a/templates/admin/compta/banques/ajouter.tpl b/templates/admin/compta/banques/ajouter.tpl new file mode 100644 index 0000000..8e3e429 --- /dev/null +++ b/templates/admin/compta/banques/ajouter.tpl @@ -0,0 +1,32 @@ +{include file="admin/_head.tpl" title="Ajouter un compte" current="compta/banques"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Ajouter un compte bancaire +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    +
    +
    +
    +
    +
    + +

    + {csrf_field key="compta_ajout_banque"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/banques/index.tpl b/templates/admin/compta/banques/index.tpl new file mode 100644 index 0000000..a2c87f3 --- /dev/null +++ b/templates/admin/compta/banques/index.tpl @@ -0,0 +1,34 @@ +{include file="admin/_head.tpl" title="Comptes bancaires" current="compta/banques"} + + + + {if !empty($liste)} +
    + {foreach from=$liste item="compte"} +
    {$compte.libelle|escape} {if !empty($compte.banque)}({$compte.banque|escape}){/if}
    +
    + IBAN : {$compte.iban|escape|format_iban}
    + BIC : {$compte.bic|escape}
    + {$compte.iban|escape|format_rib} +
    +
    Solde : {$compte.solde|html_money} {$config.monnaie|escape}
    +
    + Journal + {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN} + | Modifier + | Supprimer + {/if} +
    + {/foreach} +
    + {else} +

    + Aucun compte bancaire trouvé. +

    + {/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/banques/modifier.tpl b/templates/admin/compta/banques/modifier.tpl new file mode 100644 index 0000000..d73d93f --- /dev/null +++ b/templates/admin/compta/banques/modifier.tpl @@ -0,0 +1,32 @@ +{include file="admin/_head.tpl" title="Modifier un compte" current="compta/banques"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Modifier un compte bancaire +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    +
    +
    +
    +
    +
    + +

    + {csrf_field key="compta_edit_banque_`$compte.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/banques/supprimer.tpl b/templates/admin/compta/banques/supprimer.tpl new file mode 100644 index 0000000..b0c7512 --- /dev/null +++ b/templates/admin/compta/banques/supprimer.tpl @@ -0,0 +1,29 @@ +{include file="admin/_head.tpl" title="Supprimer un compte" current="compta/banques"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Supprimer le compte ? +

    + Êtes-vous sûr de vouloir supprimer le compte « {$compte.id|escape} - {$compte.libelle|escape} Â» ? +

    +

    + Attention, le compte ne pourra pas être supprimé si des opérations y sont + affectées. +

    +
    + +

    + {csrf_field key="compta_delete_banque_`$compte.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/categories/ajouter.tpl b/templates/admin/compta/categories/ajouter.tpl new file mode 100644 index 0000000..be0590c --- /dev/null +++ b/templates/admin/compta/categories/ajouter.tpl @@ -0,0 +1,39 @@ +{include file="admin/_head.tpl" title="Ajouter une catégorie" current="compta/categories"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Ajouter une catégorie +
    +
    obligatoire
    +
    + +
    +
    obligatoire
    +
    +
    +
    +
    obligatoire
    +
    + {select_compte comptes=$comptes name="compte"} +
    +
    +
    + +

    + {csrf_field key="compta_ajout_cat"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/categories/index.tpl b/templates/admin/compta/categories/index.tpl new file mode 100644 index 0000000..a70f3a6 --- /dev/null +++ b/templates/admin/compta/categories/index.tpl @@ -0,0 +1,31 @@ +{include file="admin/_head.tpl" title="Catégories" current="compta/categories"} + + + + {if !empty($liste)} +
    + {foreach from=$liste item="cat"} +
    {$cat.intitule|escape}
    + {if !empty($cat.description)} +
    {$cat.description|escape}
    + {/if} +
    {$cat.compte|escape} - {$cat.compte_libelle|escape}
    +
    + Voir + | Modifier + | Supprimer +
    + {/foreach} +
    + {else} +

    + Aucune catégorie trouvée. +

    + {/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/categories/modifier.tpl b/templates/admin/compta/categories/modifier.tpl new file mode 100644 index 0000000..5c43c9a --- /dev/null +++ b/templates/admin/compta/categories/modifier.tpl @@ -0,0 +1,28 @@ +{include file="admin/_head.tpl" title="Modifier une catégorie" current="compta/categories"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Modifier une catégorie +
    +
    obligatoire
    +
    +
    +
    +
    +
    + +

    + {csrf_field key="compta_edit_cat_`$cat.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/categories/supprimer.tpl b/templates/admin/compta/categories/supprimer.tpl new file mode 100644 index 0000000..4d90ccc --- /dev/null +++ b/templates/admin/compta/categories/supprimer.tpl @@ -0,0 +1,29 @@ +{include file="admin/_head.tpl" title="Supprimer une catégorie" current="compta/categories"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Supprimer la catégorie comptable ? +

    + Êtes-vous sûr de vouloir supprimer la catégorie « {$cat.intitule|escape} Â» ? +

    +

    + Attention, la catégorie ne pourra pas être supprimée si des opérations y sont + affectées. +

    +
    + +

    + {csrf_field key="delete_compta_cat_"|cat:$cat.id} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/comptes/ajouter.tpl b/templates/admin/compta/comptes/ajouter.tpl new file mode 100644 index 0000000..609c812 --- /dev/null +++ b/templates/admin/compta/comptes/ajouter.tpl @@ -0,0 +1,39 @@ +{include file="admin/_head.tpl" title="Ajouter un compte" current="compta/categories"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Ajouter un compte +
    +
    obligatoire
    +
    + {select_compte comptes=$comptes name="parent" create=true} +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    obligatoire
    + {foreach from=$positions item="pos" key="id"} +
    + + +
    + {/foreach} +
    +
    + +

    + {csrf_field key="compta_ajout_compte"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/comptes/index.tpl b/templates/admin/compta/comptes/index.tpl new file mode 100644 index 0000000..ce3011c --- /dev/null +++ b/templates/admin/compta/comptes/index.tpl @@ -0,0 +1,51 @@ +{if empty($classe)} + {include file="admin/_head.tpl" title="Comptes" current="compta/categories"} + +{else} + {include file="admin/_head.tpl" title=$classe_compte.libelle current="compta/categories"} + + + +

    + Les comptes avec la mention * font partie du plan comptable standard + et ne peuvent être modifiés ou supprimés. +

    + + {if !empty($liste)} + + {foreach from=$liste item="compte"} + + + + + + {/foreach} +
    {$compte.id|escape}{$compte.libelle|escape} + {if !empty($compte.desactive)} + Désactivé + {else} + {$compte.position|get_position} + {if !$compte.plan_comptable} + | Modifier + | Supprimer + {else} + * + {/if} + {/if} +
    + + {else} +

    + Aucun compte trouvé. +

    + {/if} +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/comptes/journal.tpl b/templates/admin/compta/comptes/journal.tpl new file mode 100644 index 0000000..444b7f1 --- /dev/null +++ b/templates/admin/compta/comptes/journal.tpl @@ -0,0 +1,56 @@ +{include file="admin/_head.tpl" title="Journal : `$compte.id` - `$compte.libelle`" current="compta/gestion" body_id="rapport"} + +{if isset($tpl.get.suivi)} + +{/if} + + + + + + + + + + + + + + + + + + + + + + + {foreach from=$journal item="ligne"} + + + + + + + + + {/foreach} + + + + + + + + +
    DateMontantSolde cumuléLibellé
    {$ligne.id|escape} + {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN} + ✎ + {/if} + {$ligne.date|date_fr:'d/m/Y'|escape}{if $ligne.compte_credit == $compte.id}{$credit}{else}{$debit}{/if}{$ligne.montant|html_money}{$ligne.solde|html_money}{$ligne.libelle|escape}
    Solde{$solde|html_money} {$config.monnaie|escape}
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/comptes/modifier.tpl b/templates/admin/compta/comptes/modifier.tpl new file mode 100644 index 0000000..3c0168b --- /dev/null +++ b/templates/admin/compta/comptes/modifier.tpl @@ -0,0 +1,33 @@ +{include file="admin/_head.tpl" title="Modifier un compte" current="compta/categories"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Modifier un compte +
    +
    obligatoire
    +
    +
    obligatoire
    + {foreach from=$positions item="pos" key="id"} +
    + + +
    + {/foreach} +
    +
    + +

    + {csrf_field key="compta_edit_compte_`$compte.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/comptes/supprimer.tpl b/templates/admin/compta/comptes/supprimer.tpl new file mode 100644 index 0000000..2fc25bb --- /dev/null +++ b/templates/admin/compta/comptes/supprimer.tpl @@ -0,0 +1,53 @@ +{include file="admin/_head.tpl" title="Supprimer un compte" current="compta/categories"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +{if !$can_delete && !$can_disable} +

    + Ce compte ne peut être supprimé ou désactivé. + Pour pouvoir supprimer ou désactiver un compte aucune catégorie ou écriture comptable ne doit y faire référence. + Pour pouvoir désactiver un compte aucune écriture comptable ne doit y faire référence dans l'exercice en cours. +

    +{elseif $can_disable && !$can_delete} + +
    + +
    + Désactiver le compte ? +

    + Êtes-vous sûr de vouloir désactiver le compte « {$compte.id|escape} - {$compte.libelle|escape} Â» ? +

    +

    + Une fois désactivé il ne sera plus possible de l'utiliser, mais il pourra par contre être réactivé. +

    +
    + +

    + {csrf_field key="compta_disable_compte_`$compte.id`"} + +

    + +
    +{else} +
    + +
    + Supprimer le compte ? +

    + Êtes-vous sûr de vouloir supprimer le compte « {$compte.id|escape} - {$compte.libelle|escape} Â» ? +

    +
    + +

    + {csrf_field key="compta_delete_compte_`$compte.id`"} + +

    + +
    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/exercices/ajouter.tpl b/templates/admin/compta/exercices/ajouter.tpl new file mode 100644 index 0000000..3661f69 --- /dev/null +++ b/templates/admin/compta/exercices/ajouter.tpl @@ -0,0 +1,30 @@ +{include file="admin/_head.tpl" title="Commencer un exercice" current="compta/exercices" js=1} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Commencer un nouvel exercice +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    +
    + +

    + {csrf_field key="compta_ajout_exercice"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/exercices/bilan.tpl b/templates/admin/compta/exercices/bilan.tpl new file mode 100644 index 0000000..4704b04 --- /dev/null +++ b/templates/admin/compta/exercices/bilan.tpl @@ -0,0 +1,84 @@ +{include file="admin/_head.tpl" title="Bilan" current="compta/exercices" body_id="rapport"} + +
    +

    {$config.nom_asso|escape}

    +

    Exercice comptable {if $exercice.cloture}clôturé{else}en cours{/if} du + {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'}, généré le {$cloture|date_fr:'d/m/Y'}

    +
    + + + + + + + + + + + + + + + + + + +
    + + + + {foreach from=$bilan.actif.comptes key="parent_code" item="parent"} + + + + + {foreach from=$parent.comptes item="solde" key="compte"} + + + + + {/foreach} + {/foreach} + +

    Actif

    {$parent_code|get_nom_compte|escape}{$parent.solde|html_money}
    {$compte|get_nom_compte|escape}{$solde|html_money}
    +
    + + + + {foreach from=$bilan.passif.comptes key="parent_code" item="parent"} + + + + + {foreach from=$parent.comptes item="solde" key="compte"} + + + + + {/foreach} + {/foreach} + +

    Passif

    {$parent_code|get_nom_compte|escape}{$parent.solde|html_money}
    {$compte|get_nom_compte|escape}{$solde|html_money}
    +
    + + + + + + + +
    Total actif{$bilan.actif.total|html_money}
    +
    + + + + + + + +
    Total passif{$bilan.passif.total|html_money}
    +
    + +

    Toutes les opérations sont libellées en {$config.monnaie|escape}.

    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/exercices/cloturer.tpl b/templates/admin/compta/exercices/cloturer.tpl new file mode 100644 index 0000000..675bae3 --- /dev/null +++ b/templates/admin/compta/exercices/cloturer.tpl @@ -0,0 +1,41 @@ +{include file="admin/_head.tpl" title="Clôturer un exercice" current="compta/exercices" js=1} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Clôturer un exercice +

    + Êtes-vous sûr de vouloir clôturer l'exercice « {$exercice.libelle|escape} Â» ? +

    +

    + Attention, une fois clôturé, les opérations de cet exercice ne pourront plus être supprimées ou modifiées. +

    +
    +
    Début de l'exercice
    +
    {$exercice.debut|date_fr:'d/m/Y'}
    +
    +
    Si des opérations existent après cette date, elles seront automatiquement + attribuées à un nouvel exercice.
    +
    +
    + +
    +
    Les soldes créditeurs et débiteurs de chaque compte seront reportés + automatiquement dans le nouvel exercice. Si vous ne cochez pas la case, vous devrez faire les reports à nouveau vous-même.
    + +
    + +

    + {csrf_field key="compta_cloturer_exercice_`$exercice.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/exercices/compte_resultat.tpl b/templates/admin/compta/exercices/compte_resultat.tpl new file mode 100644 index 0000000..7ca80df --- /dev/null +++ b/templates/admin/compta/exercices/compte_resultat.tpl @@ -0,0 +1,110 @@ +{include file="admin/_head.tpl" title="Compte de résultat" current="compta/exercices" body_id="rapport"} + +
    +

    {$config.nom_asso|escape}

    +

    Exercice comptable {if $exercice.cloture}clôturé{else}en cours{/if} du + {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'}, généré le {$cloture|date_fr:'d/m/Y'}

    +
    + + + + + + + + + + + + + + + + + + + + + + +
    + + + + {foreach from=$compte_resultat.charges.comptes key="parent_code" item="parent"} + + + + + {foreach from=$parent.comptes item="solde" key="compte"} + + + + + {/foreach} + {/foreach} + +

    Charges

    {$parent_code|get_nom_compte|escape}{$parent.solde|html_money}
    {$compte|get_nom_compte|escape}{$solde|html_money}
    +
    + + + + {foreach from=$compte_resultat.produits.comptes key="parent_code" item="parent"} + + + + + {foreach from=$parent.comptes item="solde" key="compte"} + + + + + {/foreach} + {/foreach} + +

    Produits

    {$parent_code|get_nom_compte|escape}{$parent.solde|html_money}
    {$compte|get_nom_compte|escape}{$solde|html_money}
    +
    + + + + + + + +
    Total charges{$compte_resultat.charges.total|html_money}
    +
    + + + + + + + +
    Total produits{$compte_resultat.produits.total|html_money}
    +
    + {if ($compte_resultat.resultat >= 0)} + + + + + + + +
    Résultat (excédent){$compte_resultat.resultat|html_money}
    + {/if} +
    + {if ($compte_resultat.resultat < 0)} + + + + + + + +
    Résultat (déficit){$compte_resultat.resultat|html_money}
    + {/if} +
    + +

    Toutes les opérations sont libellées en {$config.monnaie|escape}.

    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/exercices/grand_livre.tpl b/templates/admin/compta/exercices/grand_livre.tpl new file mode 100644 index 0000000..4aac7a5 --- /dev/null +++ b/templates/admin/compta/exercices/grand_livre.tpl @@ -0,0 +1,89 @@ +{include file="admin/_head.tpl" title="Grand livre" current="compta/exercices" body_id="rapport"} + +
    +

    {$config.nom_asso|escape}

    +

    Exercice comptable {if $exercice.cloture}clôturé{else}en cours{/if} du + {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'}, généré le {$cloture|date_fr:'d/m/Y'}

    +
    + +{foreach from=$livre.classes key="classe" item="comptes"} +

    {$classe|get_nom_compte|escape}

    + +{foreach from=$comptes item="compte" key="code"} + {foreach from=$compte.comptes item="souscompte" key="souscode"} + + + + + + + + + + + + + + + + + + {foreach from=$souscompte.journal item="ligne"} + + + + + + + {/foreach} + + + + + + + + + +

    {$souscode|escape} — {$souscode|get_nom_compte|escape}

    DateIntituléDébitCrédit
    {$ligne.date|date_fr:'d/m/Y'|escape}{$ligne.libelle|escape}{if $ligne.compte_debit == $souscode}{$ligne.montant|html_money}{/if}{if $ligne.compte_credit == $souscode}{$ligne.montant|html_money}{/if}
    Solde final{if $souscompte.debit > 0}{$souscompte.debit|html_money}{/if}{if $souscompte.credit > 0}{$souscompte.credit|html_money}{/if}
    + {/foreach} + + + + + + + + + + + + + + + + +
    Total{$code|get_nom_compte|escape}{if $compte.total > 0}{$compte.total|abs|html_money}{/if}{if $compte.total < 0}{$compte.total|abs|html_money}{/if}
    + {/foreach} +{/foreach} + + + + + + + + + + + + + + + + +
    Total{$livre.debit|html_money}{$livre.credit|html_money}
    + +

    Toutes les opérations sont libellées en {$config.monnaie|escape}.

    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/exercices/index.tpl b/templates/admin/compta/exercices/index.tpl new file mode 100644 index 0000000..888fdfd --- /dev/null +++ b/templates/admin/compta/exercices/index.tpl @@ -0,0 +1,43 @@ +{include file="admin/_head.tpl" title="Exercices" current="compta/exercices"} + +{if !$current} + +{/if} + +{if !empty($liste)} +
    + {foreach from=$liste item="exercice"} +
    {$exercice.libelle|escape}
    +
    + {if $exercice.cloture}Clôturé{else}En cours{/if} + | Du {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'} +
    +
    + {$exercice.nb_operations|escape} opérations enregistrées. +
    +
    + Journal général + | Grand livre + | Compte de résultat + | Bilan +
    + {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN} +
    + {if !$exercice.cloture} + Modifier + | Clôturer + | Supprimer + {/if} +
    + {/if} + {/foreach} +
    +{else} +

    + Il n'y a pas d'exercice en cours. +

    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/exercices/journal.tpl b/templates/admin/compta/exercices/journal.tpl new file mode 100644 index 0000000..5e0e7e6 --- /dev/null +++ b/templates/admin/compta/exercices/journal.tpl @@ -0,0 +1,39 @@ +{include file="admin/_head.tpl" title="Journal général" current="compta/exercices" body_id="rapport"} + +
    +

    {$config.nom_asso|escape}

    +

    Exercice comptable {if $exercice.cloture}clôturé{else}en cours{/if} du + {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'}, généré le {$cloture|date_fr:'d/m/Y'}

    +
    + + + + + + + + + + + + + {foreach from=$journal item="ligne"} + + + + + + + + + + + + + {/foreach} + +
    DateIntituléComptesDébitCrédit
    {$ligne.date|date_fr:'d/m/Y'|escape}{$ligne.libelle|escape}{$ligne.compte_debit|escape} - {$ligne.compte_debit|get_nom_compte|escape}{$ligne.montant|html_money}
    {$ligne.compte_credit|escape} - {$ligne.compte_credit|get_nom_compte|escape}{$ligne.montant|html_money}
    + +

    Toutes les opérations sont libellées en {$config.monnaie|escape}.

    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/exercices/modifier.tpl b/templates/admin/compta/exercices/modifier.tpl new file mode 100644 index 0000000..88c271b --- /dev/null +++ b/templates/admin/compta/exercices/modifier.tpl @@ -0,0 +1,30 @@ +{include file="admin/_head.tpl" title="Modifier un exercice" current="compta/exercices" js=1} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Modifier un exercice +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    +
    + +

    + {csrf_field key="compta_modif_exercice_`$exercice.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/exercices/supprimer.tpl b/templates/admin/compta/exercices/supprimer.tpl new file mode 100644 index 0000000..603faf7 --- /dev/null +++ b/templates/admin/compta/exercices/supprimer.tpl @@ -0,0 +1,30 @@ +{include file="admin/_head.tpl" title="Supprimer un exercice" current="compta/exercices"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Supprimer un exercice +

    + Êtes-vous sûr de vouloir supprimer l'exercice « {$exercice.libelle|escape} Â» + du {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'} ? +

    +

    + Attention, l'exercice ne pourra pas être supprimé si des opérations y sont + toujours affectées. +

    +
    + +

    + {csrf_field key="compta_supprimer_exercice_`$exercice.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/import.tpl b/templates/admin/compta/import.tpl new file mode 100644 index 0000000..c00a844 --- /dev/null +++ b/templates/admin/compta/import.tpl @@ -0,0 +1,61 @@ +{include file="admin/_head.tpl" title="Import / Export" current="compta"} + +{if $error} +

    + {$error|escape} +

    +{elseif $ok} +

    + L'import s'est bien déroulé. +

    +{/if} + + + +
    + +
    + Importer depuis un fichier +
    +
    obligatoire
    +
    +
    obligatoire
    +
    + + +
    +
    + Export du journal comptable au format CSV provenant de Garradin. + Les lignes comportant un numéro d'opération mettront à jour les opérations existantes, + les lignes sans numéro créeront de nouvelles opérations. +
    +
    + + +
    +
    + Export des données au format CSV provenant du logiciel de comptabilité de + Citizen Place. +
    +
    + Toutes les opérations du fichier seront créées dans l'exercice en cours. Les catégories et comptes associés aux opérations seront automatiquement créés s'ils n'existent pas déjà. +
    +
    +
    + +

    + Si le fichier comporte des opérations dont la date est en dehors de l'exercice courant, + elles seront ignorées. +

    + +

    + {csrf_field key="compta_import"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/index.tpl b/templates/admin/compta/index.tpl new file mode 100644 index 0000000..fff906f --- /dev/null +++ b/templates/admin/compta/index.tpl @@ -0,0 +1,21 @@ +{include file="admin/_head.tpl" title="Comptabilité" current="compta"} + +{if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN} + +{/if} + +
    +

    + + +

    +

    + + +

    +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/operations/index.tpl b/templates/admin/compta/operations/index.tpl new file mode 100644 index 0000000..015be37 --- /dev/null +++ b/templates/admin/compta/operations/index.tpl @@ -0,0 +1,77 @@ +{include file="admin/_head.tpl" title="Suivi des opérations" current="compta/gestion"} + + + +{if $type != Garradin\Compta_Categories::AUTRES} +
    +
    + Filtrer par catégorie + + +
    +
    +{/if} + + + + + + + + + {if !$categorie && $type} + + {/if} + + + {foreach from=$journal item="ligne"} + + + + + + + {if !$categorie && $type} + + {/if} + + {foreachelse} + + + + {if !$categorie && $type}{/if} + + {/foreach} + + + + + + + + + {if !$categorie && $type}{/if} + + +
    {$ligne.id|escape} + {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN} + ✎ + {/if} + {$ligne.date|date_fr:'d/m/Y'|escape}{$ligne.montant|html_money} {$config.monnaie|escape}{$ligne.libelle|escape}{$ligne.categorie|escape}
    + Aucune opération. +
    Total{$total|html_money} {$config.monnaie|escape}
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/operations/membre.tpl b/templates/admin/compta/operations/membre.tpl new file mode 100644 index 0000000..260e8bf --- /dev/null +++ b/templates/admin/compta/operations/membre.tpl @@ -0,0 +1,76 @@ +{include file="admin/_head.tpl" title="Écritures réalisées par le membre" current="compta/gestion"} + + + +
    +
    + Exercice à visualiser +

    + + +

    + +
    +
    + +{if empty($journal)} +

    Aucune écriture comptable n'est associée à ce membre pour l'exercice demandé.

    +{else} + + + + + + + + + + + + + + + + + + + + + {foreach from=$journal item="ligne"} + + + + + + + + + + {/foreach} + +
    DateMontantLibelléCompte débitéCompte crédité
    {$ligne.id|escape} + {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN} + ✎ + {/if} + {$ligne.date|format_sqlite_date_to_french|escape}{$ligne.montant|html_money}{$ligne.libelle|escape}{$ligne.compte_debit|escape} — {$ligne.compte_debit|get_nom_compte}{$ligne.compte_credit|escape} — {$ligne.compte_credit|get_nom_compte}
    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/operations/modifier.tpl b/templates/admin/compta/operations/modifier.tpl new file mode 100644 index 0000000..9d6e2f2 --- /dev/null +++ b/templates/admin/compta/operations/modifier.tpl @@ -0,0 +1,100 @@ +{include file="admin/_head.tpl" title="Modification de l'opération n°`$operation.id`" current="compta/saisie" js=1} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Informations sur l'opération +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    obligatoire
    +
    {$config.monnaie|escape}
    + +{if is_null($type)} +
    obligatoire
    +
    + {select_compte comptes=$comptes name="compte_debit" data=$operation} +
    +
    obligatoire
    +
    + {select_compte comptes=$comptes name="compte_credit" data=$operation} +
    +{else} +
    obligatoire
    +
    + +
    +
    +
    +
    +
    + +
    +{/if} + +
    +
    +
    +
    +
    +
    + +{if !is_null($type)} +
    + Catégorie +
    + {foreach from=$categories item="cat"} +
    + + +
    + {if !empty($cat.description)} +
    {$cat.description|escape}
    + {/if} + {/foreach} +
    +
    + + +{/if} + +

    + {csrf_field key="compta_modifier_`$operation.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/operations/recherche_sql.tpl b/templates/admin/compta/operations/recherche_sql.tpl new file mode 100644 index 0000000..7e1296e --- /dev/null +++ b/templates/admin/compta/operations/recherche_sql.tpl @@ -0,0 +1,61 @@ +{include file="admin/_head.tpl" title="Recherche par requête SQL" current="compta"} + +
    +
    + Schéma des tables SQL +
    {$schema.journal|escape}
    +
    +
    +
    Si aucune limite n'est précisée, une limite de 100 résultats sera appliquée.
    +
    +
    +

    + +

    +
    +
    + +{if !empty($error)} +

    + Erreur dans la requête SQL :
    + {$error|escape} +

    +{/if} + +{if !empty($result)} +

    {$result|@count} résultats renvoyés.

    + + + {foreach from=$result[0] key="col" item="ignore"} + + {/foreach} + {if array_key_exists('id', $result[0])} + + {/if} + + + {foreach from=$result item="row"} + + {foreach from=$row item="col"} + + {/foreach} + {if array_key_exists('id', $result[0])} + + {/if} + + {/foreach} + + + +{else} +

    + Aucun résultat trouvé. +

    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/operations/saisir.tpl b/templates/admin/compta/operations/saisir.tpl new file mode 100644 index 0000000..30c103e --- /dev/null +++ b/templates/admin/compta/operations/saisir.tpl @@ -0,0 +1,143 @@ +{include file="admin/_head.tpl" title="Saisie d'une opération" current="compta/saisie" js=1} + +{if $error} +

    + {$error|escape} +

    +{/if} + +{if $ok} +

    + L'opération numéro {$ok|escape} a été ajoutée. + (Voir l'opération) +

    +{/if} + + + +
    + +
    + Informations sur l'opération +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    obligatoire
    +
    {$config.monnaie|escape}
    + +{if is_null($type)} +
    obligatoire
    +
    + {select_compte comptes=$comptes name="compte_debit"} +
    +
    obligatoire
    +
    + {select_compte comptes=$comptes name="compte_credit"} +
    +{elseif $type === 'virement'} +
    +
    + +
    +
    +
    + +
    +{elseif $type === 'dette'} +
    +
    + + +
    +
    + + +
    +{else} +
    obligatoire
    +
    + +
    +
    +
    +
    +
    + +
    +{/if} +
    +
    +
    +
    +
    +
    + +{if $type == Garradin\Compta_Categories::DEPENSES || $type == Garradin\Compta_Categories::RECETTES || $type == 'dette'} +
    + Catégorie +
    + {foreach from=$categories item="cat"} +
    + + +
    + {if !empty($cat.description)} +
    {$cat.description|escape}
    + {/if} + {/foreach} +
    +
    + + +{/if} + +

    + {csrf_field key="compta_saisie"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/operations/supprimer.tpl b/templates/admin/compta/operations/supprimer.tpl new file mode 100644 index 0000000..a9b7006 --- /dev/null +++ b/templates/admin/compta/operations/supprimer.tpl @@ -0,0 +1,26 @@ +{include file="admin/_head.tpl" title="Supprimer une opération" current="compta/gestion"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Supprimer cette opération ? +

    + Êtes-vous sûr de vouloir supprimer l'opération n°{$operation.id|escape} + « {$operation.libelle|escape} Â» du {$operation.date|date_fr:'d/m/Y'} ? +

    +
    + +

    + {csrf_field key="compta_supprimer_`$operation.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/compta/operations/voir.tpl b/templates/admin/compta/operations/voir.tpl new file mode 100644 index 0000000..2d9e37a --- /dev/null +++ b/templates/admin/compta/operations/voir.tpl @@ -0,0 +1,90 @@ +{include file="admin/_head.tpl" title="Opération n°`$operation.id`" current="compta/gestion"} + +{if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN} + +{/if} + +
    +
    Date
    +
    {$operation.date|date_fr:'l j F Y (d/m/Y)'}
    +
    Libellé
    +
    {$operation.libelle|escape}
    +
    Montant
    +
    {$operation.montant|html_money} {$config.monnaie|escape}
    +
    Numéro pièce comptable
    +
    {if trim($operation.numero_piece)}{$operation.numero_piece|escape}{else}Non renseigné{/if}
    + + {if $operation.id_categorie} + +
    Moyen de paiement
    +
    {if trim($operation.moyen_paiement)}{$moyen_paiement|escape}{else}Non renseigné{/if}
    + + {if $operation.moyen_paiement == 'CH'} +
    Numéro de chèque
    +
    {if trim($operation.numero_cheque)}{$operation.numero_cheque|escape}{else}Non renseigné{/if}
    + {/if} + + {if $operation.moyen_paiement && $operation.moyen_paiement != 'ES'} +
    Compte bancaire
    +
    {$compte|escape}
    + {/if} + +
    Catégorie
    +
    + {if $categorie.type == Garradin\Compta_Categories::DEPENSES}Dépense{else}Recette{/if} : + {$categorie.intitule|escape} +
    + {/if} + +
    Exercice
    +
    + {$exercice.libelle|escape} + | Du {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'} + | {if $exercice.cloture}Clôturé{else}En cours{/if} +
    + +
    Opération créée par
    +
    + {if $operation.id_auteur} + {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES} + {$nom_auteur|escape} + {else} + {$nom_auteur|escape} + {/if} + {else} + membre supprimé + {/if} +
    + +
    Remarques
    +
    {if trim($operation.remarques)}{$operation.remarques|escape}{else}Non renseigné{/if}
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    ComptesDébitCrédit
    {$operation.compte_debit|escape}{$nom_compte_debit}{$operation.montant|html_money} {$config.monnaie|escape}
    {$operation.compte_credit|escape}{$nom_compte_credit}{$operation.montant|html_money} {$config.monnaie|escape}
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/config/_menu.tpl b/templates/admin/config/_menu.tpl new file mode 100644 index 0000000..4881e2b --- /dev/null +++ b/templates/admin/config/_menu.tpl @@ -0,0 +1,8 @@ + diff --git a/templates/admin/config/donnees.tpl b/templates/admin/config/donnees.tpl new file mode 100644 index 0000000..cf4fe88 --- /dev/null +++ b/templates/admin/config/donnees.tpl @@ -0,0 +1,133 @@ +{include file="admin/_head.tpl" title="Données — Sauvegarde et restauration" current="config"} + +{include file="admin/config/_menu.tpl" current="donnees"} + +{if $error} +

    {$error|escape}

    +{elseif $ok} +

    + {if $ok == 'config'}La configuration a bien été enregistrée. + {elseif $ok == 'create'}Une nouvelle sauvegarde a été créée. + {elseif $ok == 'restore'}La restauration a bien été effectuée. Si vous désirez revenir en arrière, vous pouvez utiliser la sauvegarde automatique nommée date-du-jour.avant_restauration.sqlite, sinon vous pouvez l'effacer. + {elseif $ok == 'remove'}La sauvegarde a été supprimée. + {/if} +

    +{/if} + +
    + +
    + Sauvegarde automatique +

    + En activant cette option une sauvegarde sera automatiquement créée à chaque intervalle donné. + Par exemple en activant une sauvegarde hebdomadaire, une copie des données sera réalisée + une fois par semaine, sauf si aucune modification n'a été effectuée sur les données + ou que personne ne s'est connecté. +

    +
    +
    obligatoire
    +
    + +
    +
    obligatoire
    +
    + Par exemple avec l'intervalle mensuel, en indiquant de conserver 12 sauvegardes, + vous pourrez garder un an d'historique de sauvegardes. +
    +
    + Attention : si vous choisissez un nombre important et un intervalle réduit, + l'espace disque occupé par vos sauvegardes va rapidement augmenter. +
    +
    +
    +

    + {csrf_field key="backup_config"} + +

    +
    + +
    +
    + +
    + Sauvegarde manuelle +

    + {csrf_field key="backup_create"} + +

    +
    + +
    +
    + +
    + Copies de sauvegarde disponibles + {if empty($liste)} +

    Aucune copie de sauvegarde disponible.

    + {else} +
    + {foreach from=$liste key="f" item="d"} +
    + +
    + {/foreach} +
    +

    + Attention, en cas de restauration, l'intégralité des données courantes seront effacées et remplacées par celles contenues dans la sauvegarde sélectionnée. Cependant, afin de prévenir toute erreur + une sauvegarde des données sera réalisée avant la restauration. +

    +

    + {csrf_field key="backup_manage"} + + +

    + {/if} +
    + +
    +
    + +
    + Téléchargement +

    + {csrf_field key="backup_download"} + +

    +
    + +
    +
    + +
    + +

    + Attention, l'intégralité des données courantes seront effacées et remplacées par celles + contenues dans le fichier fourni. +

    +

    + Une sauvegarde des données courantes sera effectuée avant le remplacement, + en cas de besoin d'annuler cette restauration. +

    +

    + {csrf_field key="backup_restore"} + + + (maximum {$max_file_size|format_bytes}) + +

    +
    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/config/import.tpl b/templates/admin/config/import.tpl new file mode 100644 index 0000000..4de8e90 --- /dev/null +++ b/templates/admin/config/import.tpl @@ -0,0 +1,16 @@ +{include file="admin/_head.tpl" title="Import & export" current="config"} + +{include file="admin/config/_menu.tpl" current="import"} + +
    +
    +
    Membres
    +
    Import de la liste des membres
    +
    Export de la liste des membres en CSV (pour tableurs)
    +
    Comptabilité
    +
    Import des données comptables
    +
    Export des données comptables en CSV
    +
    +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/config/index.tpl b/templates/admin/config/index.tpl new file mode 100644 index 0000000..2ed36b3 --- /dev/null +++ b/templates/admin/config/index.tpl @@ -0,0 +1,121 @@ +{include file="admin/_head.tpl" title="Configuration" current="config"} + +{if $error} + {if $error == 'OK'} +

    + La configuration a bien été enregistrée. +

    + {else} +

    + {$error|escape} +

    + {/if} +{/if} + +{include file="admin/config/_menu.tpl" current="index"} + +
    + +
    + Garradin +
    +
    Version installée
    +
    {$garradin_version|escape} [Vérifier la disponibilité d'une nouvelle version]
    +
    Informations système
    +
    PHP version {$php_version|escape} — SQLite version {$sqlite_version|escape}
    +
    +
    + +
    + Informations sur l'association +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    +
    +
    +
    +
    +
    + +
    + Localisation +
    +
    obligatoire
    +
    +
    obligatoire
    +
    + +
    +
    +
    + +
    + Envois par E-Mail +
    +
    +
    +
    +
    + +
    + Wiki +
    +
    + obligatoire
    +
    Indiquer ici l'adresse unique de la page qui sera utilisée comme page d'accueil du wiki.
    +
    +
    + obligatoire
    +
    Indiquer ici l'adresse unique de la page qui sera affichée à la connexion d'un membre.
    +
    +
    +
    + +
    + Membres +
    +
    obligatoire
    +
    + +
    +
    obligatoire
    +
    Ce champ des fiches membres sera utilisé comme identité du membre dans les emails, les fiches, les pages, etc.
    +
    + +
    +
    obligatoire
    +
    Ce champ des fiches membres sera utilisé en guise d'identifiant pour se connecter à Garradin. Pour cela le champ doit être unique (pas de doublons).
    +
    + +
    + +
    +
    + +

    + {csrf_field key="config"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/config/membres.tpl b/templates/admin/config/membres.tpl new file mode 100644 index 0000000..8a6649b --- /dev/null +++ b/templates/admin/config/membres.tpl @@ -0,0 +1,353 @@ +{include file="admin/_head.tpl" title="Configuration — Fiche membres" current="config"} + +{include file="admin/config/_menu.tpl" current="membres"} + +{if $error} + {if $error == 'OK'} +

    + La configuration a bien été enregistrée. +

    + {elseif $error == 'ADD_OK'} +

    + Le champ a été ajouté à la fin de la liste. +

    + {else} +

    + {$error|escape} +

    + {/if} +{/if} + +{if $review} +
    + Fiche membre exemple +
    + {foreach from=$champs item="champ" key="nom"} + {if $nom == 'passe'}{continue}{/if} + {html_champ_membre config=$champ name=$nom disabled=true} + {if empty($champ.editable) || !empty($champ.private)} +
    + {if !empty($champ.private)} + (Champ privé) + {elseif empty($champ.editable)} + (Non-modifiable par les membres) + {/if} +
    + {/if} + {/foreach} +
    +
    + +
    + Connexion +
    +
    {if !empty($champs.passe.mandatory)} obligatoire{/if}
    +
    + {if empty($champs.passe.editable) || !empty($champs.passe.private)} +
    + {if !empty($champs.passe.private)} + (Champ privé) + {elseif empty($champs.passe.editable)} + (Non-modifiable par les membres) + {/if} +
    + {/if} +
    +
    + +
    +

    + {csrf_field key="config_membres"} + + + +

    +
    +{else} +

    + Cette page vous permet de personnaliser les fiches d'information des membres de l'association.
    + Attention : Les champs supprimés de la fiche seront effacés de toutes les fiches de tous les membres, et les données qu'ils contenaient seront perdues. +

    + +
    + Champs non-personnalisables +
    +
    Numéro unique
    +
    Ce numéro identifie de manière unique chacun des membres. + Il est incrémenté à chaque nouveau membre ajouté.
    +
    Catégorie
    +
    Identifie la catégorie du membre.
    +
    Date de dernière connexion
    +
    Mémorise la date de dernière connexion à l'administration de Garradin.
    +
    Date d'inscription
    +
    Enregistre la date de création de la fiche du membre.
    +
    +
    + + {if !empty($presets)} +
    +
    + Ajouter un champ pré-défini +

    + + + +

    +
    +
    + {/if} + +
    +
    + Ajouter un champ personnalisé +
    +
    obligatoire
    +
    Ne peut comporter que des lettres minuscules et des tirets bas.
    +
    +
    obligatoire
    +
    +
    obligatoire
    +
    + +
    +
    +

    + + +

    +
    +
    + +
    +
    + {foreach from=$champs item="champ" key="nom"} + {if $nom == 'passe'}{continue}{/if} +
    + {$nom|escape} +
    +
    +
    {$champ.type|get_type}
    +
    obligatoire
    +
    +
    +
    +
    +
    Si coché, les membres pourront changer cette information depuis leur espace personnel.
    +
    +
    Si coché, ce champ ne pourra rester vide.
    +
    +
    Si coché, ce champ ne sera visible et modifiable que par les personnes pouvant gérer les membres, mais pas les membres eux-même.
    + {if $champ.type == 'select' || $champ.type == 'multiple'} +
    + {if $champ.type == 'multiple'} +
    Attention changer l'ordre des options peut avoir des effets indésirables.
    + {else} +
    Attention renommer ou supprimer une option n'affecte pas ce qui a déjà été enregistré dans les fiches des membres.
    + {/if} +
    + <{if $champ.type == 'multiple'}ol{else}ul{/if} class="options"> + {if !empty($champ.options)} + {foreach from=$champ.options key="key" item="opt"} +
  • + {/foreach} + {/if} + {if $champ.type == 'select' || empty($champ.options) || count($champ.options) < 32} +
  • + {/if} +
    + {/if} +
    +
    Laisser vide ou indiquer le chiffre zéro pour que ce champ n'apparaisse pas dans la liste des membres. Inscrire un chiffre entre 1 et 10 pour indiquer l'ordre d'affichage du champ dans le tableau de la liste des membres.
    +
    +
    +
    + {/foreach} +
    + +
    + Mot de passe +
    +
    +
    Si coché, les membres pourront changer cette information depuis leur espace personnel.
    +
    +
    Si coché, ce champ ne pourra rester vide.
    +
    +
    Si coché, ce champ ne sera visible et modifiable que par les personnes pouvant gérer les membres, mais pas les membres eux-même.
    +
    +
    + +

    + + + + (un récapitulatif sera présenté et une confirmation sera demandée) +

    +
    + + +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/config/plugins.tpl b/templates/admin/config/plugins.tpl new file mode 100644 index 0000000..24b50d0 --- /dev/null +++ b/templates/admin/config/plugins.tpl @@ -0,0 +1,107 @@ +{include file="admin/_head.tpl" title="Extensions" current="config"} + +{include file="admin/config/_menu.tpl" current="plugins"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +{if !empty($delete)} +
    + +
    + Désinstaller une extension +

    + Êtes-vous sûr de vouloir supprimer l'extension « {$plugin.nom|escape} Â» ? +

    +

    + Attention : cette action est irréversible et effacera toutes les + données associées à l'extension. +

    +
    + +

    + {csrf_field key="delete_plugin_`$plugin.id`"} + +

    +
    +{else} + {if !empty($liste_installes)} + + + + + + + + + + + {foreach from=$liste_installes item="plugin"} + + + + + + + {/foreach} + +
    ExtensionAuteurVersion installée
    +

    {$plugin.nom|escape}

    + {$plugin.description|escape} +
    + {$plugin.auteur|escape} + + {$plugin.version|escape} + + {if empty($plugin.system)} + Désinstaller + {/if} + {if !empty($plugin.config)} + {if empty($plugin.system)}|{/if} + Configurer + {/if} +
    + {else} +

    + Aucune extension n'est installée. + Vous pouvez consulter le site de Garradin pour obtenir + des extensions à télécharger. +

    + {/if} + + {if !empty($liste_telecharges)} +
    + +
    + Extensions à installer +
    + {foreach from=$liste_telecharges item="plugin" key="id"} +
    + + (version {$plugin.version|escape}) +
    +
    [{$plugin.auteur|escape}] {$plugin.description|escape}
    + {/foreach} +
    +
    + +

    + Attention : installer une extension non officielle peut présenter des risques de sécurité + et de stabilité. +

    + +

    + {csrf_field key="install_plugin"} + +

    +
    + {/if} +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/config/site.tpl b/templates/admin/config/site.tpl new file mode 100644 index 0000000..f919dd1 --- /dev/null +++ b/templates/admin/config/site.tpl @@ -0,0 +1,53 @@ +{include file="admin/_head.tpl" title="Configuration — Site public" current="config"} + +{if $error && $error != 'OK'} +

    + {$error|escape} +

    +{/if} + +{include file="admin/config/_menu.tpl" current="site"} + +{if isset($edit)} +
    +

    Éditer un squelette

    + + {if $error == 'OK'} +

    + Modifications enregistrées. +

    + {/if} + +
    + {$edit.file|escape} +

    + +

    +
    + +

    + {csrf_field key=$csrf_key} + +

    + +
    + + + + +{else} +
    +

    Squelettes du site

    + +
    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/index.tpl b/templates/admin/index.tpl new file mode 100644 index 0000000..1cb5aed --- /dev/null +++ b/templates/admin/index.tpl @@ -0,0 +1,43 @@ +{include file="admin/_head.tpl" title="Bonjour `$user.identite` !" current="home"} + +
    +

    {$config.nom_asso|escape}

    + {if !empty($config.adresse_asso)} +

    + {$config.adresse_asso|escape|nl2br} +

    + {/if} + {if !empty($config.email_asso)} +

    + E-Mail : {mailto address=$config.email_asso} +

    + {/if} + {if !empty($config.site_asso)} +

    + Web : {$config.site_asso|escape} +

    + {/if} +
    + + + +
    + {$page.contenu.contenu|format_wiki|liens_wiki:'wiki/?'} +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/install.tpl b/templates/admin/install.tpl new file mode 100644 index 0000000..c0857d4 --- /dev/null +++ b/templates/admin/install.tpl @@ -0,0 +1,81 @@ +{include file="admin/_head.tpl" title="Garradin - Installation" js=1} + +{if $disabled} +

    Garradin est déjà installé.

    +{else} +

    + Bienvenue dans Garradin ! + Veuillez remplir les quelques informations suivantes pour terminer + l'installation. +

    + + {if !empty($error)} +

    {$error|escape}

    + {/if} + +
    + +
    + Informations sur l'association +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    +
    +
    +
    + +
    + Informations sur le premier membre +
    +
    obligatoire
    +
    +
    obligatoire
    +
    Par exemple : bureau, conseil d'administration, présidente, trésorier, etc.
    +
    +
    obligatoire
    +
    +
    obligatoire
    +
    + Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr + et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres. +
    +
    + Pas d'idée ? Voici une suggestion choisie au hasard : + +
    +
    +
    (vérification) obligatoire
    +
    +
    +
    + +

    + {csrf_field key="install"} + +

    + + + + + +
    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/login.tpl b/templates/admin/login.tpl new file mode 100644 index 0000000..ce50855 --- /dev/null +++ b/templates/admin/login.tpl @@ -0,0 +1,36 @@ +{include file="admin/_head.tpl" title="Connexion"} + +{if $error} +

    + {if $error == 'OTHER'} + Une erreur est survenue, merci de réessayer. + {else} + Connexion impossible. Vérifiez l'adresse e-mail et le mot de passe. + {/if} +

    +{/if} + +
    + +
    + Connexion +
    +
    +
    +
    +
    +
    +
    + +

    + {csrf_field key="login"} + +

    + +

    + Pas de mot de passe ou mot de passe perdu ? +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/action.tpl b/templates/admin/membres/action.tpl new file mode 100644 index 0000000..002e56e --- /dev/null +++ b/templates/admin/membres/action.tpl @@ -0,0 +1,62 @@ +{include file="admin/_head.tpl" title="Action collective sur les membres" current="membres"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + {foreach from=$selected item="id"} + + {/foreach} + + + + {if $action == 'move'} +
    + Changer la catégorie des {$nb_selected|escape} membres sélectionnés +
    +
    obligatoire
    +
    + +
    +
    +
    + +

    + {csrf_field key="membres_action"} + +

    + + {elseif $action == 'delete'} +
    + Supprimer les membres sélectionnés ? +

    + Êtes-vous sûr de vouloir supprimer les {$nb_selected|escape} membres sélectionnés ? +

    +

    + Attention : cette action est irréversible et effacera toutes les + données personnelles et l'historique de ces membres. +

    +

    + Alternativement, il est aussi possible de déplacer les membres qui ne font plus + partie de l'association dans une catégorie « Anciens membres Â», plutôt + que de les effacer complètement. +

    +
    + +

    + {csrf_field key="membres_action"} + +

    + {/if} + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/ajouter.tpl b/templates/admin/membres/ajouter.tpl new file mode 100644 index 0000000..4028ebd --- /dev/null +++ b/templates/admin/membres/ajouter.tpl @@ -0,0 +1,67 @@ +{include file="admin/_head.tpl" title="Ajouter un membre" current="membres/ajouter" js=1} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Informations personnelles +
    + {foreach from=$champs item="champ" key="nom"} + {html_champ_membre config=$champ name=$nom} + {/foreach} +
    +
    + +
    + Connexion +
    +
    {if $champs.passe.mandatory} obligatoire{/if}
    +
    + Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr + et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres. +
    +
    + Pas d'idée ? Voici une suggestion choisie au hasard : + +
    +
    +
    (vérification)
    +
    +
    +
    + + {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN} +
    + Général +
    +
    obligatoire
    +
    + +
    +
    +
    + {/if} + +

    + {csrf_field key="new_member"} + +

    + +
    + + + + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cat_modifier.tpl b/templates/admin/membres/cat_modifier.tpl new file mode 100644 index 0000000..242f19a --- /dev/null +++ b/templates/admin/membres/cat_modifier.tpl @@ -0,0 +1,154 @@ +{include file="admin/_head.tpl" title="Modifier une catégorie" current="membres/categories"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Informations générales +
    +
    obligatoire
    +
    +
    +
    +
    + + +
    +
    + Si coché cette catégorie ne sera visible qu'aux administrateurs et ne recevra pas + de messages collectifs ou de rappels. +
    +
    +
    + +
    + Cotisation obligatoire +
    +
    +
    + +
    +
    +
    + +
    + Droits +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + +

    + {csrf_field key="edit_cat_"|cat:$cat.id} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cat_supprimer.tpl b/templates/admin/membres/cat_supprimer.tpl new file mode 100644 index 0000000..f1113d6 --- /dev/null +++ b/templates/admin/membres/cat_supprimer.tpl @@ -0,0 +1,34 @@ +{include file="admin/_head.tpl" title="Supprimer une catégorie" current="membres/categories"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Supprimer la catégorie de membres ? +

    + Êtes-vous sûr de vouloir supprimer la catégorie « {$cat.nom|escape} Â» ? +

    +

    + Attention, la catégorie ne doit plus contenir de membres pour pouvoir + être supprimée. +

    +

    + Notez que si des pages du wiki étaient restreintes à la lecture ou à l'écriture + aux seuls membres de ce groupe, elles redeviendront lisibles et modifiables + par tous les membres ayant accès au wiki ! +

    +
    + +

    + {csrf_field key="delete_cat_"|cat:$cat.id} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/categories.tpl b/templates/admin/membres/categories.tpl new file mode 100644 index 0000000..50904bd --- /dev/null +++ b/templates/admin/membres/categories.tpl @@ -0,0 +1,51 @@ +{include file="admin/_head.tpl" title="Catégories de membres" current="membres/categories"} + + + + + + + + + + {foreach from=$liste item="cat"} + + + + + + + {/foreach} + +
    NomMembresDroits
    {$cat.nom|escape}{$cat.nombre|escape} + {format_droits droits=$cat} + + Modifier + | Supprimer +
    + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Ajouter une catégorie +
    +
    obligatoire
    +
    +
    +
    + +

    + {csrf_field key="new_cat"} + +

    + +
    + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations.tpl b/templates/admin/membres/cotisations.tpl new file mode 100644 index 0000000..efc295c --- /dev/null +++ b/templates/admin/membres/cotisations.tpl @@ -0,0 +1,99 @@ +{include file="admin/_head.tpl" title="Cotisations du membre" current="membres/cotisations"} + + + +
    +{if $cotisation} +
    Cotisation obligatoire
    +
    {$cotisation.intitule|escape} — + {if $cotisation.duree} + {$cotisation.duree|escape} jours + {elseif $cotisation.debut} + du {$cotisation.debut|format_sqlite_date_to_french} au {$cotisation.fin|format_sqlite_date_to_french} + {else} + ponctuelle + {/if} + — {$cotisation.montant|escape_money} {$config.monnaie|escape} +
    +
    À jour de cotisation ?
    +
    + {if !$cotisation.a_jour} + Non, cotisation non payée + {else} + ✓ Oui + {if $cotisation.expiration} + (expire le {$cotisation.expiration|format_sqlite_date_to_french}) + {/if} + {/if} +
    +{/if} +
    + {if $nb_activites == 1} + {$nb_activites|escape} cotisation enregistrée + {elseif $nb_activites} + {$nb_activites|escape} cotisations enregistrées + {else} + Aucune cotisation enregistrée + {/if} +
    +{if !empty($cotisations_membre)} + {foreach from=$cotisations_membre item="co"} +
    {$co.intitule|escape} — + {if $co.a_jour} + À jour{if $co.expiration} — Expire le {$co.expiration|format_sqlite_date_to_french}{/if} + {else} + En retard + — Suivi des rappels + {/if} +
    + {/foreach} +{/if} +
    +
    + +{if !empty($cotisations)} + + + + + + + + + {foreach from=$cotisations item="c"} + + + + + + + {/foreach} + +
    DateCotisation
    {$c.date|format_sqlite_date_to_french} + {$c.intitule|escape} — + {if $c.duree} + {$c.duree|escape} jours + {elseif $c.debut} + du {$c.debut|format_sqlite_date_to_french} au {$c.fin|format_sqlite_date_to_french} + {else} + ponctuelle + {/if} + — {$c.montant|html_money} {$config.monnaie|escape} + + {if $user.droits.compta >= Garradin\Membres::DROIT_ECRITURE && !empty($c.nb_operations)} + {$c.nb_operations} écritures + {/if} + + 👪 + ✘ +
    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/ajout.tpl b/templates/admin/membres/cotisations/ajout.tpl new file mode 100644 index 0000000..6cd623b --- /dev/null +++ b/templates/admin/membres/cotisations/ajout.tpl @@ -0,0 +1,126 @@ +{if $membre} + {include file="admin/_head.tpl" title="Enregistrer une cotisation pour le membre" current="membres/cotisations" js=1} + + +{else} + {include file="admin/_head.tpl" title="Enregistrer une cotisation" current="membres/cotisations" js=1} + + +{/if} + +{if $error} +

    {$error|escape}

    +{else if $user.droits.compta >= Garradin\Membres::DROIT_ECRITURE} +

    + Cette page sert à enregistrer les cotisations des membres de l'association. + Pour enregistrer un don ou une dépense, comme le paiement d'un prestataire ou une facture, il est possible de saisir une opération comptable. +

    +{/if} + +
    +
    + Enregistrer une cotisation +
    +
    obligatoire
    +
    + +
    +
    obligatoire
    +
    +
    obligatoire
    +
    + +
    +
    +
    +
    obligatoire
    +
    + +
    +
    obligatoire
    +
    + {if !$membre} +
    obligatoire
    +
    + {/if} +
    +
    + +

    + {csrf_field key="add_cotisation"} + {if $membre}{/if} + +

    +
    + + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/gestion/modifier.tpl b/templates/admin/membres/cotisations/gestion/modifier.tpl new file mode 100644 index 0000000..7df8bf1 --- /dev/null +++ b/templates/admin/membres/cotisations/gestion/modifier.tpl @@ -0,0 +1,114 @@ +{include file="admin/_head.tpl" title="Modifier une cotisation" current="membres/cotisations" js=1} + + + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Modifier une cotisation +
    +
    obligatoire
    +
    +
    +
    +
    obligatoire
    +
    + +
    +
    + +
    +
    +
    obligatoire
    +
    +
    +
    +
    +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    +
    +
    + +
    +
    + Si coché, à chaque enregistrement de cotisation d'un membre une opération + du montant de la cotisation sera enregistrée dans la comptabilité selon + la catégorie choisie. +
    +
    +
    + +
    +
    +
    + +

    + {csrf_field key="edit_co_`$cotisation.id`"} + +

    + +
    + + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/gestion/rappel_modifier.tpl b/templates/admin/membres/cotisations/gestion/rappel_modifier.tpl new file mode 100644 index 0000000..a0d5c64 --- /dev/null +++ b/templates/admin/membres/cotisations/gestion/rappel_modifier.tpl @@ -0,0 +1,65 @@ +{include file="admin/_head.tpl" title="Modifier un rappel automatique" current="membres/cotisations" js=1} + + + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Modifier un rappel automatique +
    +
    obligatoire
    +
    + +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    + + + +
    +
    + + + +
    +
    obligatoire
    +
    +
    Astuce : pour inclure dans le contenu du mail le nom du membre, utilisez #IDENTITE, pour inclure le délai de l'envoi utilisez #NB_JOURS.
    +
    +
    + +

    + {csrf_field key="edit_rappel_`$rappel.id`"} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/gestion/rappel_supprimer.tpl b/templates/admin/membres/cotisations/gestion/rappel_supprimer.tpl new file mode 100644 index 0000000..c2c3d57 --- /dev/null +++ b/templates/admin/membres/cotisations/gestion/rappel_supprimer.tpl @@ -0,0 +1,46 @@ +{include file="admin/_head.tpl" title="Supprimer un rappel automatique" current="membres/cotisations"} + + + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Supprimer ce rappel automatique ? +

    + Êtes-vous sûr de vouloir supprimer le rappel « {$rappel.sujet|escape} Â» ? +

    +
    +
    +
    + (toutefois il ne sera plus associé à ce rappel) +
    +
    + +
    +
    +
    + +

    + {csrf_field key="delete_rappel_"|cat:$rappel.id} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/gestion/rappels.tpl b/templates/admin/membres/cotisations/gestion/rappels.tpl new file mode 100644 index 0000000..1dee49a --- /dev/null +++ b/templates/admin/membres/cotisations/gestion/rappels.tpl @@ -0,0 +1,126 @@ +{include file="admin/_head.tpl" title="Gestion des rappels automatiques" current="membres/cotisations" js=1} + + + +

    + Les rappels automatiques sont envoyés aux membres disposant d'une adresse e-mail + selon le délai défini. Il est possible de définir plusieurs rappels pour une même cotisation. +

    + +{if empty($liste)} +

    Aucun rappel automatique n'est enregistré.

    +{else} + + + + + + + + + {foreach from=$liste item="rappel"} + + + + + + + {/foreach} + +
    CotisationDélai de rappelSujet
    + {$rappel.intitule|escape} + — {$rappel.montant|html_money} {$config.monnaie|escape} + — {if $rappel.duree}pour {$rappel.duree|escape} jours + {elseif $rappel.debut} + du {$rappel.debut|format_sqlite_date_to_french} au {$rappel.fin|format_sqlite_date_to_french} + {else} + ponctuelle + {/if} + + {if $rappel.delai == 0}le jour de l'expiration + {else} + {$rappel.delai|abs|escape} + {if abs($rappel.delai) > 1}jours{else}jour{/if} + {if $rappel.delai > 0}après{else}avant{/if} + expiration + {/if} + {$rappel.sujet|escape} + ✎ + ✘ +
    +{/if} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Ajouter un rappel automatique +
    +
    obligatoire
    +
    + +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    + + + +
    +
    + + + +
    +
    obligatoire
    +
    +
    Astuce : pour inclure dans le contenu du mail le nom du membre, utilisez #IDENTITE, pour inclure le délai de l'envoi utilisez #NB_JOURS.
    +
    +
    + +

    + {csrf_field key="new_rappel"} + +

    + +
    + + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/gestion/supprimer.tpl b/templates/admin/membres/cotisations/gestion/supprimer.tpl new file mode 100644 index 0000000..44cda28 --- /dev/null +++ b/templates/admin/membres/cotisations/gestion/supprimer.tpl @@ -0,0 +1,36 @@ +{include file="admin/_head.tpl" title="Supprimer une cotisation" current="membres/cotisations"} + + + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Supprimer cette cotisation ? +

    + Êtes-vous sûr de vouloir supprimer la cotisation « {$cotisation.intitule|escape} Â» ? +

    +

    + Attention, l'historique des membres ayant cotisé à cette cotisation sera supprimé. +

    +
    + +

    + {csrf_field key="delete_co_"|cat:$cotisation.id} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/index.tpl b/templates/admin/membres/cotisations/index.tpl new file mode 100644 index 0000000..3aa6ab1 --- /dev/null +++ b/templates/admin/membres/cotisations/index.tpl @@ -0,0 +1,160 @@ +{include file="admin/_head.tpl" title="Cotisations" current="membres/cotisations" js=1} + + + + + + + + + + + + + + {foreach from=$liste item="co"} + + + + + + + + + {/foreach} + +
    CotisationPériodeMontantMembres inscritsMembres à jour
    {$co.intitule|escape} + {if $co.duree} + {$co.duree|escape} jours + {elseif $co.debut} + du {$co.debut|format_sqlite_date_to_french} au {$co.fin|format_sqlite_date_to_french} + {else} + ponctuelle + {/if} + {$co.montant|html_money} {$config.monnaie|escape}{$co.nb_membres|escape}{$co.nb_a_jour|escape} + 👪 + {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN} + ✎ + ✘ + {/if} +
    + +{if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN} + +{if $error} +

    + {$error|escape} +

    +{else} +

    + Idée : les cotisations peuvent également être utilisées pour suivre les activités auxquels + sont inscrits les membres de l'association. +

    +{/if} + +
    + +
    + Ajouter une cotisation +
    +
    obligatoire
    +
    +
    +
    +
    obligatoire
    +
    + +
    +
    + +
    +
    +
    obligatoire
    +
    +
    +
    +
    +
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    +
    +
    + +
    +
    + Si coché, à chaque enregistrement de cotisation d'un membre une opération + du montant de la cotisation sera enregistrée dans la comptabilité selon + la catégorie choisie. +
    +
    +
    + +
    +
    +
    + +

    + {csrf_field key="new_cotisation"} + +

    + +
    + + + +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/rappels.tpl b/templates/admin/membres/cotisations/rappels.tpl new file mode 100644 index 0000000..e61a3f7 --- /dev/null +++ b/templates/admin/membres/cotisations/rappels.tpl @@ -0,0 +1,102 @@ +{include file="admin/_head.tpl" title="Rappels pour cotisations du membre" current="membres/cotisations" js=1} + + + +
    +
    + Enregistrer un rappel fait à ce membre +
    +
    +
    + +
    +
    +
    +
    +
    + +
    + {* FIXME: proposer d'envoyer un email au membre *} +
    + +
    + {* FIXME: afficher les différents numéros de téléphone de la fiche membre *} +
    + +
    +
    + +
    +
    +

    + {csrf_field key="add_rappel_`$membre.id`"} + +

    +
    +
    + +{if !empty($rappels)} + + + + + + + + + {foreach from=$rappels item="r"} + + + + + + + {/foreach} + +
    Date du rappelMoyen de communicationCotisation
    {$r.date|format_sqlite_date_to_french} + {if $r.media == Garradin\Rappels_envoyes::MEDIA_AUTRE} + Autre + {elseif $r.media == Garradin\Rappels_envoyes::MEDIA_COURRIER} + Courrier + {elseif $r.media == Garradin\Rappels_envoyes::MEDIA_TELEPHONE} + Téléphone + {else} + E-Mail + {/if} + + {$r.intitule|escape} — + {$r.montant|html_money} {$config.monnaie|escape} + +
    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/supprimer.tpl b/templates/admin/membres/cotisations/supprimer.tpl new file mode 100644 index 0000000..1a480b6 --- /dev/null +++ b/templates/admin/membres/cotisations/supprimer.tpl @@ -0,0 +1,35 @@ +{include file="admin/_head.tpl" title="Supprimer une cotisation pour le membre n°`$membre.id`" current="membres/cotisations"} + + + +{if $error} +

    {$error|escape}

    +{/if} + +
    +
    + Supprimer une cotisation membre +

    + Êtes-vous sûr de vouloir supprimer la cotisation membre + du {$cotisation.date|format_sqlite_date_to_french} ? +

    +

    Attention si des écritures comptables sont liées à cette cotisation + elles ne seront pas supprimées.

    +
    + + +

    + {csrf_field key="del_cotisation_`$cotisation.id`"} + +

    +
    + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/cotisations/voir.tpl b/templates/admin/membres/cotisations/voir.tpl new file mode 100644 index 0000000..7ad9179 --- /dev/null +++ b/templates/admin/membres/cotisations/voir.tpl @@ -0,0 +1,59 @@ +{include file="admin/_head.tpl" title="Membres ayant cotisé" current="membres/cotisations"} + + + +
    +
    Cotisation
    +
    {$cotisation.intitule|escape} — + {if $cotisation.duree} + {$cotisation.duree|escape} jours + {elseif $cotisation.debut} + du {$cotisation.debut|format_sqlite_date_to_french} au {$cotisation.fin|format_sqlite_date_to_french} + {else} + ponctuelle + {/if} + — {$cotisation.montant|escape_money} {$config.monnaie|escape} +
    +
    Nombre de membres ayant cotisé
    +
    {$cotisation.nb_membres|escape}
    +
    + +{if !empty($liste)} + + + + + + + + + + + + {foreach from=$liste item="co"} + + + + + + + + {/foreach} + +
    Membre Statut Date de cotisation
    {$co.id_membre|escape}{$co.nom|escape}{if $co.a_jour}À jour{else}En retard{/if}{$co.date|format_sqlite_date_to_french} + Saisir + | Cotisations + | Rappels +
    + + {pagination url=$pagination_url page=$page bypage=$bypage total=$total} +{/if} + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/fiche.tpl b/templates/admin/membres/fiche.tpl new file mode 100644 index 0000000..f7107b2 --- /dev/null +++ b/templates/admin/membres/fiche.tpl @@ -0,0 +1,106 @@ +{include file="admin/_head.tpl" title="`$membre.identite` (`$categorie.nom`)" current="membres"} + + + +
    +{if $cotisation} +
    Cotisation obligatoire
    +
    {$cotisation.intitule|escape} — + {if $cotisation.duree} + {$cotisation.duree|escape} jours + {elseif $cotisation.debut} + du {$cotisation.debut|format_sqlite_date_to_french} au {$cotisation.fin|format_sqlite_date_to_french} + {else} + ponctuelle + {/if} + — {$cotisation.montant|escape_money} {$config.monnaie|escape} +
    +
    À jour de cotisation ?
    +
    + {if !$cotisation.a_jour} + Non, cotisation non payée + {else} + ✓ Oui + {if $cotisation.expiration} + (expire le {$cotisation.expiration|format_sqlite_date_to_french}) + {/if} + {/if} +
    +{/if} +
    + {if $nb_activites == 1} + {$nb_activites|escape} cotisation enregistrée + {elseif $nb_activites} + {$nb_activites|escape} cotisations enregistrées + {else} + Aucune cotisation enregistrée + {/if} +
    +
    + Voir l'historique +
    +
    +{if !empty($nb_operations)} +
    Écritures comptables
    +
    {$nb_operations|escape} écritures comptables + — Voir la liste des écritures ajoutées par ce membre +
    + {/if} +
    + +
    +
    Numéro d'adhérent
    +
    {$membre.id|escape}
    +
    Catégorie
    +
    {$categorie.nom|escape} {format_droits droits=$categorie}
    +
    Inscription
    +
    {$membre.date_inscription|date_fr:'d/m/Y'}
    +
    Dernière connexion
    +
    {if empty($membre.date_connexion)}Jamais{else}{$membre.date_connexion|date_fr:'d/m/Y à H:i'}{/if}
    + {foreach from=$champs key="c" item="config"} +
    {$config.title|escape}
    +
    + {if $config.type == 'checkbox'} + {if $membre[$c]}Oui{else}Non{/if} + {elseif empty($membre[$c])} + (Non renseigné) + {elseif $c == 'nom'} + {$membre[$c]|escape} + {elseif $c == 'email'} + {$membre[$c]|escape} + | ✉ Envoyer un message + {elseif $config.type == 'email'} + {$membre[$c]|escape} + {elseif $config.type == 'tel'} + {$membre[$c]|escape|format_tel} + {elseif $config.type == 'country'} + {$membre[$c]|get_country_name|escape} + {elseif $config.type == 'date' || $config.type == 'datetime'} + {$membre[$c]|format_sqlite_date_to_french} + {elseif $c == 'passe'} + Oui + {elseif $config.type == 'password'} + ******* + {elseif $config.type == 'multiple'} +
      + {foreach from=$config.options key="b" item="name"} + {if $membre[$c] & (0x01 << $b)} +
    • {$name|escape}
    • + {/if} + {/foreach} +
    + {else} + {$membre[$c]|escape|rtrim|nl2br} + {/if} +
    + {/foreach} +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/import.tpl b/templates/admin/membres/import.tpl new file mode 100644 index 0000000..882b28b --- /dev/null +++ b/templates/admin/membres/import.tpl @@ -0,0 +1,88 @@ +{include file="admin/_head.tpl" title="Import & export des membres" current="membres" js=1} + +{if $error} +

    + {$error|escape} +

    +{elseif $ok} +

    + L'import s'est bien déroulé. +

    +{/if} + + + +
    + +
    + Importer depuis un fichier +
    +
    obligatoire
    +
    +
    obligatoire
    +
    + + +
    +
    + Export de la liste des membres au format CSV provenant de Garradin. + Les lignes comportant un numéro de membre mettront à jour les fiches des membres ayant ce numéro, + les lignes sans numéro créeront de nouveaux membres. +
    +
    + + +
    +
    + Export des données au format CSV provenant du logiciel libre + Galette. +
    +
    +
    Indiquer quels champs des fiches membre de Garradin les données de Galette doivent remplir.
    +
    + + + {foreach from=$galette_champs item="galette"} + {if is_int($galette)}{continue}{/if} + + + + + {/foreach} + +
    {$galette|escape}
    +
    +
    +
    + +

    + {csrf_field key="membres_import"} + +

    + +
    + + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/index.tpl b/templates/admin/membres/index.tpl new file mode 100644 index 0000000..44ffd62 --- /dev/null +++ b/templates/admin/membres/index.tpl @@ -0,0 +1,156 @@ +{include file="admin/_head.tpl" title="Liste des membres" current="membres"} + +{if $user.droits.membres >= Garradin\Membres::DROIT_ECRITURE} + +{/if} + +{if isset($tpl.get.sent)} +

    Votre message a été envoyé.

    +{/if} + +{if !empty($membres_cats)} +
    +
    + Filtrer par catégorie + + +
    +
    +{/if} + +
    +
    + Rechercher un membre + + +
    +
    + +{if $user.droits.membres >= Garradin\Membres::DROIT_ECRITURE} + +
    + + {if !empty($liste)} + + + {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}{/if} + + {foreach from=$champs key="c" item="champ"} + + {/foreach} + + + + {foreach from=$liste item="membre"} + + {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}{/if} + + {/foreach} + + + {/foreach} + +
    {$champ.title|escape}
    {$membre.id|escape} + {foreach from=$champs key="c" item="cfg"} + {$membre[$c]|escape|display_champ_membre:$cfg} + {if !empty($membre.email)}✉ {/if} + 👤 + ✎ +
    + + {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN} +

    + +

    +

    + Pour les membres cochés : + + + {csrf_field key="membres_action"} +

    + {/if} + + {pagination url=$pagination_url page=$page bypage=$bypage total=$total} + {else} +

    + Aucun membre trouvé. +

    + {/if} + +
    + + +{else} + {if !empty($liste)} + + + + + + + {foreach from=$liste item="membre"} + + + + + {/foreach} + +
    Membre
    {$membre.identite|escape} + {if !empty($membre.email)}Envoyer un message{/if} +
    + + {if !empty($pagination_url)} + {pagination url=$pagination_url page=$page bypage=$bypage total=$total} + {/if} + + {else} +

    + Aucun membre trouvé. +

    + {/if} +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/message.tpl b/templates/admin/membres/message.tpl new file mode 100644 index 0000000..57e9457 --- /dev/null +++ b/templates/admin/membres/message.tpl @@ -0,0 +1,38 @@ +{include file="admin/_head.tpl" title="Contacter un membre" current="membres"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    +
    + Message +
    +
    Expéditeur
    +
    {$user.identite|escape} <{$user.email|escape}>
    +
    + Votre adresse E-Mail apparaîtra dans le champ "expéditeur" du message reçu par le destinataire. +
    +
    Destinataire
    +
    {$membre.identite|escape} ({$categorie.nom|escape})
    +
    obligatoire
    +
    +
    obligatoire
    +
    +
    + + +
    +
    +
    + +

    + {csrf_field key="send_message_"|cat:$membre.id} + +

    +
    + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/message_collectif.tpl b/templates/admin/membres/message_collectif.tpl new file mode 100644 index 0000000..b699bd6 --- /dev/null +++ b/templates/admin/membres/message_collectif.tpl @@ -0,0 +1,43 @@ +{include file="admin/_head.tpl" title="Envoyer un message collectif" current="membres/message_collectif"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    +
    + Message +
    +
    Expéditeur
    +
    {$config.nom_asso|escape} <{$config.email_asso|escape}>
    +
    obligatoire
    +
    + +
    +
    + + +
    +
    obligatoire
    +
    Sera automatiquement précédé de la mention [{$config.nom_asso|escape}]
    +
    +
    obligatoire
    +
    +
    +
    + +

    + {csrf_field key="send_message_collectif"} + +

    +
    + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/modifier.tpl b/templates/admin/membres/modifier.tpl new file mode 100644 index 0000000..723abe2 --- /dev/null +++ b/templates/admin/membres/modifier.tpl @@ -0,0 +1,84 @@ +{include file="admin/_head.tpl" title="Modifier un membre" current="membres" js=1} + + + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Informations personnelles +
    + {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN} +
    obligatoire
    +
    + {/if} + {foreach from=$champs item="champ" key="nom"} + {html_champ_membre config=$champ name=$nom data=$membre} + {/foreach} +
    +
    + +
    + {if $membre.passe}Changer le mot de passe{else}Choisir un mot de passe{/if} +
    + {if $membre.passe} +
    Ce membre a déjà un mot de passe, mais vous pouvez le changer si besoin.
    + {else} +
    Ce membre n'a pas encore de mot de passe et ne peut donc se connecter.
    + {/if} +
    {if $champs.passe.mandatory} obligatoire{/if}
    +
    + Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr + et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres. +
    +
    + Pas d'idée ? Voici une suggestion choisie au hasard : + +
    +
    +
    (vérification){if $champs.passe.mandatory} obligatoire{/if}
    +
    +
    +
    + + {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN} +
    + Général +
    +
    obligatoire
    +
    + +
    +
    +
    + {/if} + +

    + {csrf_field key="edit_member_"|cat:$membre.id} + +

    + +
    + + + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/recherche.tpl b/templates/admin/membres/recherche.tpl new file mode 100644 index 0000000..698f4ba --- /dev/null +++ b/templates/admin/membres/recherche.tpl @@ -0,0 +1,209 @@ +{include file="admin/_head.tpl" title="Recherche de membre" current="membres"} + +{if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN} + +{/if} + + +
    +
    + Rechercher un membre +
    +
    +
    + +
    +
    +
    + {foreach from=$champs_liste key="k" item="v"} + {if $v.type == 'select'} +
    + +
    + {elseif $v.type == 'multiple'} +
    + +
    + {elseif $v.type == 'checkbox'} +
    + +
    + {/if} + {/foreach} +
    +

    + +

    +
    +
    + +{if $user.droits.membres >= Garradin\Membres::DROIT_ECRITURE} + +
    + + {if !empty($liste)} + + + {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}{/if} + + {foreach from=$champs_entete key="c" item="cfg"} + {if $champ == $c} + + {else} + + {/if} + {/foreach} + + + + {foreach from=$liste item="membre"} + + {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}{/if} + + {else} + + {/if} + {/foreach} + + + {/foreach} + + + + {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN} +

    + +

    +

    + Pour les membres cochés : + + + {csrf_field key="membres_action"} +

    + {/if} + + {elseif $recherche != ''} +

    + Aucun membre trouvé. +

    + {/if} + +
    + + +{else} + {if !empty($liste)} + + + + + + + {foreach from=$liste item="membre"} + + + + + {/foreach} + +
    Membre
    {$membre.identite|escape} + {if !empty($membre.email)}Envoyer un message{/if} +
    + {else} +

    + Aucun membre trouvé. +

    + {/if} +{/if} + + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/recherche_sql.tpl b/templates/admin/membres/recherche_sql.tpl new file mode 100644 index 0000000..68a50b7 --- /dev/null +++ b/templates/admin/membres/recherche_sql.tpl @@ -0,0 +1,111 @@ +{include file="admin/_head.tpl" title="Recherche par requête SQL" current="membres"} + +
    +
    + Schéma des tables SQL +
    {$schema.membres|escape}
    +
    +
    +
    Si aucune limite n'est précisée, une limite de 100 résultats sera appliquée.
    +
    +
    +

    + +

    +
    +
    + +{if !empty($error)} +

    + Erreur dans la requête SQL :
    + {$error|escape} +

    +{/if} + +
    + +{if !empty($result)} +

    {$result|@count} résultats renvoyés.

    + + + {if array_key_exists('id', $result[0])} + + {/if} + {foreach from=$result[0] key="col" item="ignore"} + + {/foreach} + {if array_key_exists('id', $result[0])} + + {/if} + + + {foreach from=$result item="row"} + + {if array_key_exists('id', $result[0])} + + {/if} + {foreach from=$row item="col"} + + {/foreach} + {if array_key_exists('id', $result[0])} + + {/if} + + {/foreach} + + + +

    + +

    +

    + Pour les membres cochés : + + + {csrf_field key="membres_action"} +

    + +{else} +

    + Aucun membre trouvé. +

    +{/if} + +
    + + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/membres/supprimer.tpl b/templates/admin/membres/supprimer.tpl new file mode 100644 index 0000000..b972539 --- /dev/null +++ b/templates/admin/membres/supprimer.tpl @@ -0,0 +1,43 @@ +{include file="admin/_head.tpl" title="Supprimer un membre" current="membres"} + + + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Supprimer ce membre ? +

    + Êtes-vous sûr de vouloir supprimer le membre « {$membre.identite|escape} Â» ? +

    +

    + Attention : cette action est irréversible et effacera toutes les + données personnelles et l'historique de ces membres. +

    +

    + Alternativement, il est aussi possible de déplacer les membres qui ne font plus + partie de l'association dans une catégorie « Anciens membres Â», plutôt + que de les effacer complètement. +

    +
    + +

    + {csrf_field key="delete_membre_"|cat:$membre.id} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/mes_cotisations.tpl b/templates/admin/mes_cotisations.tpl new file mode 100644 index 0000000..a6bc74c --- /dev/null +++ b/templates/admin/mes_cotisations.tpl @@ -0,0 +1,81 @@ +{include file="admin/_head.tpl" title="Mes cotisations" current="mes_cotisations"} + +
    +
    + {if $nb_activites == 1} + Vous avez {$nb_activites|escape} cotisation enregistrée. + {elseif $nb_activites} + Vous avez {$nb_activites|escape} cotisations enregistrées. + {else} + Vous n'avez aucune cotisation enregistrée. + {/if} +
    +{if $cotisation} +
    Cotisation obligatoire
    +
    {$cotisation.intitule|escape} — + {if $cotisation.duree} + {$cotisation.duree|escape} jours + {elseif $cotisation.debut} + du {$cotisation.debut|format_sqlite_date_to_french} au {$cotisation.fin|format_sqlite_date_to_french} + {else} + ponctuelle + {/if} + — {$cotisation.montant|escape_money} {$config.monnaie|escape} +
    +
    + {if !$cotisation.a_jour} + Vous n'êtes pas à jour de cotisation + {else} + ✓ À jour de cotisation + {if $cotisation.expiration} + (expire le {$cotisation.expiration|format_sqlite_date_to_french}) + {/if} + {/if} +
    +{/if} +{if !empty($cotisations_membre)} +
    Cotisations en cours
    + {foreach from=$cotisations_membre item="co"} +
    {$co.intitule|escape} — + {if $co.a_jour} + À jour{if $co.expiration} — Expire le {$co.expiration|format_sqlite_date_to_french}{/if} + {else} + En retard + {/if} +
    + {/foreach} +{/if} +
    + +{if !empty($cotisations)} +
    +

    Historique des cotisations

    +
    + + + + + + + + {foreach from=$cotisations item="c"} + + + + + {/foreach} + +
    DateCotisation
    {$c.date|format_sqlite_date_to_french} + {$c.intitule|escape} — + {if $c.duree} + {$c.duree|escape} jours + {elseif $c.debut} + du {$c.debut|format_sqlite_date_to_french} au {$c.fin|format_sqlite_date_to_french} + {else} + ponctuelle + {/if} + — {$c.montant|html_money} {$config.monnaie|escape} +
    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/mes_infos.tpl b/templates/admin/mes_infos.tpl new file mode 100644 index 0000000..cd99ee1 --- /dev/null +++ b/templates/admin/mes_infos.tpl @@ -0,0 +1,58 @@ +{include file="admin/_head.tpl" title="Mes informations personnelles" current="mes_infos" js=1} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + + +
    + Informations personnelles +
    + {foreach from=$champs item="champ" key="nom"} + {if empty($champ.private) && $nom != 'passe'} + {html_champ_membre config=$champ name=$nom data=$membre user_mode=true} + {/if} + {/foreach} +
    +
    + +
    + Changer mon mot de passe + {if $user.droits.membres < Garradin\Membres::DROIT_ADMIN && (!empty($champs.passe.private) || empty($champs.passe.editable))} +

    Vous devez contacter un administrateur pour changer votre mot de passe.

    + {else} +
    +
    Vous avez déjà un mot de passe, ne remplissez les champs suivants que si vous souhaitez en changer.
    +
    +
    + Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr + et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres. +
    +
    + Pas d'idée ? Voici une suggestion choisie au hasard : + +
    +
    +
    (vérification)
    +
    +
    + {/if} +
    + +

    + {csrf_field key="edit_me"} + +

    + +
    + + + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/password.tpl b/templates/admin/password.tpl new file mode 100644 index 0000000..2c231fc --- /dev/null +++ b/templates/admin/password.tpl @@ -0,0 +1,54 @@ +{include file="admin/_head.tpl" title="Mot de passe oublié ou pas de mot de passe ?"} + +{if !empty($sent)} +

    + Un e-mail vous a été envoyé, cliquez sur le lien dans cet e-mail + pour recevoir un nouveau mot de passe. +

    +

    + Ne fermez pas cette fenêtre tant que vous n'avez pas cliqué sur le lien. + Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables. +

    +{elseif !empty($new_sent)} +

    + Un e-mail contenant votre nouveau mot de passe vous a été envoyé. + Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables. +

    +

    Connexion →

    +{else} + + {if $error} +

    + {if $error == 'OTHER'} + Une erreur est survenue, merci de réessayer. + {else} + Membre inconnu ou ne disposant pas d'adresse e-mail. Si vous êtes membre, contactez un responsable pour + obtenir un mot de passe. + {/if} +

    + {/if} + +
    + +
    + Recevoir un e-mail avec un nouveau mot de passe +

    + Inscrivez ici votre {$champ.title}. + Nous vous enverrons un message vous indiquant un lien permettant de recevoir un + nouveau mot de passe. +

    +
    +
    +
    +
    +
    + +

    + {csrf_field key="recoverPassword"} + +

    + +
    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/wiki/_chercher_parent.tpl b/templates/admin/wiki/_chercher_parent.tpl new file mode 100644 index 0000000..4dbfc46 --- /dev/null +++ b/templates/admin/wiki/_chercher_parent.tpl @@ -0,0 +1,40 @@ +{include file="admin/_head.tpl" title="Choisir la page parent" current="wiki" body_id="popup" is_popup=true} + +
    +

    + +

    + + {display_tree tree=$list} + +
    + +{literal} + +{/literal} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/wiki/chercher.tpl b/templates/admin/wiki/chercher.tpl new file mode 100644 index 0000000..dd472f2 --- /dev/null +++ b/templates/admin/wiki/chercher.tpl @@ -0,0 +1,31 @@ +{include file="admin/_head.tpl" title="Recherche" current="wiki/chercher"} + +
    +
    + Rechercher une page +

    + + +

    +
    +
    + + +{if !$recherche} +

    + Aucun terme recherché. +

    +{else} +

    + {$nb_resultats|escape} pages trouvées pour « {$recherche|escape} Â» +

    + +
    + {foreach from=$resultats item="page"} +

    {$page.titre|escape}

    +

    {$page.snippet|escape|clean_snippet}

    + {/foreach} +
    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/wiki/creer.tpl b/templates/admin/wiki/creer.tpl new file mode 100644 index 0000000..ad28c0e --- /dev/null +++ b/templates/admin/wiki/creer.tpl @@ -0,0 +1,27 @@ +{include file="admin/_head.tpl" title="Créer une page" current="wiki"} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Informations +
    +
    obligatoire
    +
    +
    +
    + +

    + {csrf_field key="wiki_create"} + +

    + +
    + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/wiki/editer.tpl b/templates/admin/wiki/editer.tpl new file mode 100644 index 0000000..2deb67d --- /dev/null +++ b/templates/admin/wiki/editer.tpl @@ -0,0 +1,164 @@ +{include file="admin/_head.tpl" title="Éditer une page" current="wiki" js=1} + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Informations générales +
    +
    obligatoire
    +
    +
    obligatoire
    +
    + Ne peut comporter que des lettres, des chiffres, des tirets et des tirets bas. +
    +
    +
    +
    + + {if $page.parent == 0} + la racine du site + {else} + {$parent|escape} + {/if} + +
    +
    obligatoire
    +
    + + h + +
    +
    +
    + +
    + Droits d'accès +
    +
    +
    + + + — cette page apparaîtra sur le site public de l'association, accessible à tous les visiteurs +
    +
    + + + — seuls les membres ayant accès au wiki pourront la voir +
    +
    + = Garradin\Wiki::LECTURE_CATEGORIE}checked="checked"{/if} /> + + — seuls les membres de la même catégorie que moi pourront voir cette page +
    +
    +
    + = Garradin\Wiki::LECTURE_CATEGORIE}disabled="disabled"{/if} /> + +
    +
    + = Garradin\Wiki::ECRITURE_CATEGORIE || $page.droit_lecture >= Garradin\Wiki::LECTURE_CATEGORIE}checked="checked"{/if} {if $page.droit_lecture >= Garradin\Wiki::LECTURE_CATEGORIE}disabled="disabled"{/if} /> + +
    +
    +
    + +
    +
    +
    + + (facultatif) +
    + +
    Mot de passe : désactivé
    +
    Le mot de passe n'est ni transmis ni enregistré, vous seul le connaissez, + il n'est pas possible de retrouver le contenu si vous l'oubliez.
    +
    +
    + + +
    +

    + +

    +
    + +
    +
    +
    (facultatif)
    +
    + {* FIXME +
    + + +
    + *} +
    +
    + +

    + {csrf_field key="wiki_edit_`$page.id`"} + + + +

    + +
    + + +{/literal} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/wiki/historique.tpl b/templates/admin/wiki/historique.tpl new file mode 100644 index 0000000..4e9880f --- /dev/null +++ b/templates/admin/wiki/historique.tpl @@ -0,0 +1,81 @@ +{include file="admin/_head.tpl" title="Historique : `$page.titre`" current="wiki"} + + + +{if !empty($revisions)} + + {foreach from=$revisions item="rev"} + + + + + + + + {/foreach} +
    + {if $rev.chiffrement} + chiffré + {else} + {if $rev.revision == $page.revision} + actu + {else} + actu + {/if} + | + {if $rev.revision == 1} + diff + {else} + diff + {/if} + {/if} + {$rev.date|date_fr:'d/m/Y à H:i'} + {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES} + {$rev.nom_auteur|escape} + {/if} + + {$rev.taille|escape} octets + {if $rev.revision > 1 && !$rev.chiffrement} + {if $rev.diff_taille > 0} + (+{$rev.diff_taille|escape}) + {elseif $rev.diff_taille < 0} + ({$rev.diff_taille|escape}) + {else} + ({$rev.diff_taille|escape}) + {/if} + {/if} + + {if $rev.modification} + {$rev.modification|escape} + {/if} +
    +{elseif !empty($diff)} +
    +

    Version du {$rev1.date|date_fr:'d/m/Y à H:i'}

    + {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES} +

    De {$rev1.nom_auteur|escape}

    + {/if} + {if $rev1.modification} +

    {$rev1.modification|escape}

    + {/if} +
    +
    +

    Version {if $rev2.revision == $page.revision}actuelle en date{/if} du {$rev2.date|date_fr:'d/m/Y à H:i'}

    + {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES} +

    De {$rev2.nom_auteur|escape}

    + {/if} + {if $rev2.modification} +

    {$rev2.modification|escape}

    + {/if} +
    + {diff old=$rev1.contenu new=$rev2.contenu} +{else} +

    + Cette page n'a pas d'historique. +

    +{/if} + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/wiki/page.tpl b/templates/admin/wiki/page.tpl new file mode 100644 index 0000000..877f588 --- /dev/null +++ b/templates/admin/wiki/page.tpl @@ -0,0 +1,102 @@ +{if !empty($page.titre) && $can_read} + {include file="admin/_head.tpl" title=$page.titre current="wiki"} +{else} + {include file="admin/_head.tpl" title="Wiki" current="wiki"} +{/if} + +
      + {if $user.droits.wiki >= Garradin\Membres::DROIT_ECRITURE} +
    • Créer une nouvelle page
    • + {/if} + {if $can_edit} +
    • Éditer
    • + {/if} + {if $can_read && $page && $page.contenu} +
    • Historique + {if $page.droit_lecture == Garradin\Wiki::LECTURE_PUBLIC} +
    • Voir sur le site + {/if} + {/if} + {if $user.droits.wiki >= Garradin\Membres::DROIT_ADMIN} +
    • Supprimer
    • + {/if} +
    + +{if !$can_read} +

    Vous n'avez pas le droit de lire cette page.

    +{else} + + + {if !$page} +

    + Cette page n'existe pas. +

    + + {if $can_edit} +
    +

    + {csrf_field key="wiki_create"} + + +

    +
    + {/if} + {else} + + {if !empty($children)} +
    +

    Dans cette rubrique

    + +
    + {/if} + + {if !$page.contenu} +

    Cette page est vide, cliquez sur « Éditer » pour la modifier.

    + {else} + + {if $page.contenu.chiffrement} + + +
    +

    Cette page est chiffrée. + +

    +
    + + {else} +
    + {$page.contenu.contenu|format_wiki|liens_wiki:'?'} +
    + {/if} + +

    + Dernière modification le {$page.date_modification|date_fr:'d/m/Y à H:i'} + {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES} + par {$auteur|escape} + {/if} +

    + {/if} + {/if} +{/if} + + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/wiki/recent.tpl b/templates/admin/wiki/recent.tpl new file mode 100644 index 0000000..0393290 --- /dev/null +++ b/templates/admin/wiki/recent.tpl @@ -0,0 +1,20 @@ +{include file="admin/_head.tpl" title="Pages modifiées récemment" current="wiki/recent"} + +{if !empty($list)} + + + {foreach from=$list item="page"} + + + + + {/foreach} + +
    {$page.titre|escape}{$page.date_modification|date_fr:'d/m/Y à H:i'}
    + + {pagination url="?p=[ID]" page=$page bypage=$bypage total=$total} +{else} +

    Pas de modification récente.

    +{/if} + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/admin/wiki/supprimer.tpl b/templates/admin/wiki/supprimer.tpl new file mode 100644 index 0000000..c35746f --- /dev/null +++ b/templates/admin/wiki/supprimer.tpl @@ -0,0 +1,36 @@ +{include file="admin/_head.tpl" title="Supprimer : `$page.titre`" current="wiki"} + + + +{if $error} +

    + {$error|escape} +

    +{/if} + +
    + +
    + Supprimer cette page du wiki ? +

    + Êtes-vous sûr de vouloir supprimer la page « {$page.titre|escape} Â» ? +

    +

    + La page ne pourra pas être supprimée si d'autres pages l'utilisent comme rubrique + parente. +

    +
    + +

    + {csrf_field key="delete_wiki_"|cat:$page.id} + +

    + +
    + +{include file="admin/_foot.tpl"} \ No newline at end of file diff --git a/templates/error.tpl b/templates/error.tpl new file mode 100644 index 0000000..55e93e5 --- /dev/null +++ b/templates/error.tpl @@ -0,0 +1,45 @@ + + + + + Erreur + + + + + + +

    Erreur

    + +

    + {$error|escape} +

    + +

    + ← Retour +

    + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9a31a28 --- /dev/null +++ b/templates/index.html @@ -0,0 +1 @@ +404 Not Found

    Not Found

    The requested URL was not found on this server.

    \ No newline at end of file diff --git a/templates/index.tpl b/templates/index.tpl new file mode 100644 index 0000000..5b5b86a --- /dev/null +++ b/templates/index.tpl @@ -0,0 +1,26 @@ + + + + + {$config.nom_asso|escape} + + + + + +
    +

    {$config.nom_asso|escape}

    +
    + +
    +

    + {$config.adresse_asso|escape} +

    +
    + +

    + Administration +

    + + + \ No newline at end of file diff --git a/www/.htaccess b/www/.htaccess new file mode 100644 index 0000000..b3ec8cd --- /dev/null +++ b/www/.htaccess @@ -0,0 +1,11 @@ + + RewriteEngine on + RewriteRule admin/plugin/(.*?)/(.*) /admin/plugin.php?_p=$1&_u=$2 [QSA,L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule .* /index.php [QSA,L] + + + + ErrorDocument 404 /index.php + diff --git a/www/_inc.php b/www/_inc.php new file mode 100644 index 0000000..d869c93 --- /dev/null +++ b/www/_inc.php @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/www/_route.php b/www/_route.php new file mode 100644 index 0000000..2e3a10f --- /dev/null +++ b/www/_route.php @@ -0,0 +1,30 @@ +Erreur 404

    Page non trouvée

    Retour

    " diff --git a/www/admin/_inc.php b/www/admin/_inc.php new file mode 100644 index 0000000..1805382 --- /dev/null +++ b/www/admin/_inc.php @@ -0,0 +1,33 @@ +assign('admin_url', WWW_URL . 'admin/'); + +$membres = new Membres; + +if (!defined('Garradin\LOGIN_PROCESS')) +{ + if (!$membres->isLogged()) + { + utils::redirect('/admin/login.php'); + } + + $tpl->assign('config', Config::getInstance()->getConfig()); + $tpl->assign('is_logged', true); + $tpl->assign('user', $membres->getLoggedUser()); + $user = $membres->getLoggedUser(); + + $tpl->assign('current', ''); + $tpl->assign('plugins_menu', Plugin::listMenu()); + + if ($user['droits']['membres'] >= Membres::DROIT_ACCES) + { + $tpl->assign('nb_membres', $membres->countAllButHidden()); + } +} + +?> \ No newline at end of file diff --git a/www/admin/compta/_inc.php b/www/admin/compta/_inc.php new file mode 100644 index 0000000..11754f4 --- /dev/null +++ b/www/admin/compta/_inc.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/www/admin/compta/banques/ajouter.php b/www/admin/compta/banques/ajouter.php new file mode 100644 index 0000000..5ea4e4e --- /dev/null +++ b/www/admin/compta/banques/ajouter.php @@ -0,0 +1,45 @@ +add([ + 'libelle' => utils::post('libelle'), + 'banque' => utils::post('banque'), + 'iban' => utils::post('iban'), + 'bic' => utils::post('bic'), + ]); + + utils::redirect('/admin/compta/banques/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->display('admin/compta/banques/ajouter.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/banques/index.php b/www/admin/compta/banques/index.php new file mode 100644 index 0000000..e7494be --- /dev/null +++ b/www/admin/compta/banques/index.php @@ -0,0 +1,41 @@ +getList(); + +foreach ($liste as &$banque) +{ + $banque['solde'] = $journal->getSolde($banque['id']); +} + +$tpl->assign('liste', $liste); + +function tpl_format_iban($iban) +{ + return implode(' ', str_split($iban, 4)); +} + +function tpl_format_rib($iban) +{ + if (substr($iban, 0, 2) != 'FR') + return ''; + + $rib = utils::IBAN_RIB($iban); + $rib = explode(' ', $rib); + + $out = ''; + $out.= '
    BanqueGuichetCompteClé
    '.$rib[0].''.$rib[1].''.$rib[2].''.$rib[3].'
    '; + return $out; +} + +$tpl->register_modifier('format_iban', 'Garradin\tpl_format_iban'); +$tpl->register_modifier('format_rib', 'Garradin\tpl_format_rib'); + +$tpl->display('admin/compta/banques/index.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/banques/modifier.php b/www/admin/compta/banques/modifier.php new file mode 100644 index 0000000..91af26a --- /dev/null +++ b/www/admin/compta/banques/modifier.php @@ -0,0 +1,54 @@ +get(utils::get('id')); + +if (!$compte) +{ + throw new UserException('Le compte demandé n\'existe pas.'); +} + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('compta_edit_banque_'.$compte['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $id = $banque->edit($compte['id'], [ + 'libelle' => utils::post('libelle'), + 'banque' => utils::post('banque'), + 'iban' => utils::post('iban'), + 'bic' => utils::post('bic'), + ]); + + utils::redirect('/admin/compta/banques/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('compte', $compte); + +$tpl->display('admin/compta/banques/modifier.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/banques/supprimer.php b/www/admin/compta/banques/supprimer.php new file mode 100644 index 0000000..97f5a08 --- /dev/null +++ b/www/admin/compta/banques/supprimer.php @@ -0,0 +1,48 @@ +get(utils::get('id')); + +if (!$compte) +{ + throw new UserException('Le compte demandé n\'existe pas.'); +} + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('compta_delete_banque_'.$compte['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $banque->delete($compte['id']); + utils::redirect('/admin/compta/banques/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('compte', $compte); + +$tpl->display('admin/compta/banques/supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/categories/ajouter.php b/www/admin/compta/categories/ajouter.php new file mode 100644 index 0000000..4acad7b --- /dev/null +++ b/www/admin/compta/categories/ajouter.php @@ -0,0 +1,55 @@ +add([ + 'intitule' => utils::post('intitule'), + 'description' => utils::post('description'), + 'compte' => utils::post('compte'), + 'type' => utils::post('type'), + ]); + + if (utils::post('type') == Compta_Categories::DEPENSES) + $type = 'depenses'; + elseif (utils::post('type') == Compta_Categories::AUTRES) + $type = 'autres'; + else + $type = 'recettes'; + + utils::redirect('/admin/compta/categories/?'.$type); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('type', isset($_POST['type']) ? utils::post('type') : Compta_Categories::RECETTES); +$tpl->assign('comptes', $comptes->listTree()); + +$tpl->display('admin/compta/categories/ajouter.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/categories/index.php b/www/admin/compta/categories/index.php new file mode 100644 index 0000000..3370521 --- /dev/null +++ b/www/admin/compta/categories/index.php @@ -0,0 +1,23 @@ +assign('type', $type); +$tpl->assign('liste', $cats->getList($type)); + +$tpl->display('admin/compta/categories/index.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/categories/modifier.php b/www/admin/compta/categories/modifier.php new file mode 100644 index 0000000..deb1822 --- /dev/null +++ b/www/admin/compta/categories/modifier.php @@ -0,0 +1,59 @@ +get($id); + +if (!$cat) +{ + throw new UserException('Cette catégorie n\'existe pas.'); +} + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('compta_edit_cat_'.$cat['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $id = $cats->edit($id, [ + 'intitule' => utils::post('intitule'), + 'description' => utils::post('description'), + ]); + + if ($cat['type'] == Compta_Categories::DEPENSES) + $type = 'depenses'; + elseif ($cat['type'] == Compta_Categories::AUTRES) + $type = 'autres'; + else + $type = 'recettes'; + + utils::redirect('/admin/compta/categories/?'.$type); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('cat', $cat); + +$tpl->display('admin/compta/categories/modifier.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/categories/supprimer.php b/www/admin/compta/categories/supprimer.php new file mode 100644 index 0000000..a09eb5f --- /dev/null +++ b/www/admin/compta/categories/supprimer.php @@ -0,0 +1,48 @@ +get($id); + +if (!$cat) +{ + throw new UserException('Cette catégorie n\'existe pas.'); +} + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('delete_compta_cat_'.$cat['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $cats->delete($id); + utils::redirect('/admin/compta/categories/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('cat', $cat); + +$tpl->display('admin/compta/categories/supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/comptes/ajouter.php b/www/admin/compta/comptes/ajouter.php new file mode 100644 index 0000000..13e6b70 --- /dev/null +++ b/www/admin/compta/comptes/ajouter.php @@ -0,0 +1,56 @@ + 9) +{ + throw new UserException("Cette classe de compte n'existe pas."); +} + +$error = false; + +if (!empty($_POST['add'])) +{ + if (!utils::CSRF_check('compta_ajout_compte')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $id = $comptes->add([ + 'id' => utils::post('numero'), + 'libelle' => utils::post('libelle'), + 'parent' => utils::post('parent'), + 'position' => utils::post('position'), + ]); + + utils::redirect('/admin/compta/comptes/?classe='.$classe); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$parent = $comptes->get(utils::post('parent') ?: $classe); + +$tpl->assign('positions', $comptes->getPositions()); +$tpl->assign('position', utils::post('position') ?: $parent['position']); +$tpl->assign('comptes', $comptes->listTree($classe)); + +$tpl->display('admin/compta/comptes/ajouter.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/comptes/index.php b/www/admin/compta/comptes/index.php new file mode 100644 index 0000000..aa66463 --- /dev/null +++ b/www/admin/compta/comptes/index.php @@ -0,0 +1,37 @@ +assign('classe', $classe); + +if (!$classe) +{ + $tpl->assign('classes', $comptes->listTree(0, false)); +} +else +{ + $positions = $comptes->getPositions(); + + $tpl->assign('classe_compte', $comptes->get($classe)); + $tpl->assign('liste', $comptes->listTree($classe)); +} + +function tpl_get_position($pos) +{ + global $positions; + return $positions[$pos]; +} + +$tpl->register_modifier('get_position', 'Garradin\tpl_get_position'); + +$tpl->display('admin/compta/comptes/index.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/comptes/journal.php b/www/admin/compta/comptes/journal.php new file mode 100644 index 0000000..9d96838 --- /dev/null +++ b/www/admin/compta/comptes/journal.php @@ -0,0 +1,34 @@ +get(utils::get('id')); + +if (!$compte) +{ + throw new UserException("Le compte demandé n'existe pas."); +} + +$journal = new Compta_Journal; + +$solde = $journal->getSolde($compte['id']); + +if (($compte['position'] & Compta_Comptes::ACTIF) || ($compte['position'] & Compta_Comptes::CHARGE)) +{ + $tpl->assign('credit', '-'); + $tpl->assign('debit', '+'); +} +else +{ + $tpl->assign('credit', '+'); + $tpl->assign('debit', '-'); +} + +$tpl->assign('compte', $compte); +$tpl->assign('solde', $solde); +$tpl->assign('journal', $journal->getJournalCompte($compte['id'])); + +$tpl->display('admin/compta/comptes/journal.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/comptes/modifier.php b/www/admin/compta/comptes/modifier.php new file mode 100644 index 0000000..e6c5f08 --- /dev/null +++ b/www/admin/compta/comptes/modifier.php @@ -0,0 +1,53 @@ +get($id); + +if (!$compte) +{ + throw new UserException('Le compte demandé n\'existe pas.'); +} + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('compta_edit_compte_'.$compte['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $id = $comptes->edit($compte['id'], [ + 'libelle' => utils::post('libelle'), + 'position' => utils::post('position'), + ]); + + utils::redirect('/admin/compta/comptes/?classe='.substr($compte['id'], 0, 1)); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('positions', $comptes->getPositions()); +$tpl->assign('position', utils::post('position') ?: $compte['position']); +$tpl->assign('compte', $compte); + +$tpl->display('admin/compta/comptes/modifier.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/comptes/supprimer.php b/www/admin/compta/comptes/supprimer.php new file mode 100644 index 0000000..2e70c04 --- /dev/null +++ b/www/admin/compta/comptes/supprimer.php @@ -0,0 +1,69 @@ +get($id); + +if (!$compte) +{ + throw new UserException('Le compte demandé n\'existe pas.'); +} + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('compta_delete_compte_'.$compte['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $comptes->delete($compte['id']); + utils::redirect('/admin/compta/comptes/?classe='.substr($compte['id'], 0, 1)); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} +elseif (!empty($_POST['disable'])) +{ + if (!utils::CSRF_check('compta_disable_compte_'.$compte['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $comptes->disable($compte['id']); + utils::redirect('/admin/compta/comptes/?classe='.substr($compte['id'], 0, 1)); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('can_delete', $comptes->canDelete($compte['id'])); +$tpl->assign('can_disable', $comptes->canDisable($compte['id'])); + +$tpl->assign('error', $error); + +$tpl->assign('compte', $compte); + +$tpl->display('admin/compta/comptes/supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/exercices/ajouter.php b/www/admin/compta/exercices/ajouter.php new file mode 100644 index 0000000..f3e5f26 --- /dev/null +++ b/www/admin/compta/exercices/ajouter.php @@ -0,0 +1,44 @@ +add([ + 'libelle' => utils::post('libelle'), + 'debut' => utils::post('debut'), + 'fin' => utils::post('fin'), + ]); + + utils::redirect('/admin/compta/exercices/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->display('admin/compta/exercices/ajouter.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/exercices/bilan.php b/www/admin/compta/exercices/bilan.php new file mode 100644 index 0000000..741e8f2 --- /dev/null +++ b/www/admin/compta/exercices/bilan.php @@ -0,0 +1,32 @@ +get((int)utils::get('id')); + +if (!$exercice) +{ + throw new UserException('Exercice inconnu.'); +} + +$liste_comptes = $comptes->getListAll(); + +function get_nom_compte($compte) +{ + global $liste_comptes; + return $liste_comptes[$compte]; +} + +$tpl->register_modifier('get_nom_compte', 'Garradin\get_nom_compte'); + +$tpl->assign('bilan', $exercices->getBilan($exercice['id'])); + +$tpl->assign('cloture', $exercice['cloture'] ? $exercice['fin'] : time()); +$tpl->assign('exercice', $exercice); + +$tpl->display('admin/compta/exercices/bilan.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/exercices/cloturer.php b/www/admin/compta/exercices/cloturer.php new file mode 100644 index 0000000..4ebfb8b --- /dev/null +++ b/www/admin/compta/exercices/cloturer.php @@ -0,0 +1,53 @@ +get((int)utils::get('id')); + +if (!$exercice) +{ + throw new UserException('Exercice inconnu.'); +} + +$error = false; + +if (!empty($_POST['close'])) +{ + if (!utils::CSRF_check('compta_cloturer_exercice_'.$exercice['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $id = $e->close($exercice['id'], utils::post('fin')); + + if ($id && utils::post('reports')) + { + $e->doReports($exercice['id'], utils::modifyDate(utils::post('fin'), '+1 day')); + } + + utils::redirect('/admin/compta/exercices/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('exercice', $exercice); + +$tpl->display('admin/compta/exercices/cloturer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/exercices/compte_resultat.php b/www/admin/compta/exercices/compte_resultat.php new file mode 100644 index 0000000..f3e0f12 --- /dev/null +++ b/www/admin/compta/exercices/compte_resultat.php @@ -0,0 +1,31 @@ +get((int)utils::get('id')); + +if (!$exercice) +{ + throw new UserException('Exercice inconnu.'); +} + +$liste_comptes = $comptes->getListAll(); + +function get_nom_compte($compte) +{ + global $liste_comptes; + return $liste_comptes[$compte]; +} + +$tpl->register_modifier('get_nom_compte', 'Garradin\get_nom_compte'); +$tpl->assign('compte_resultat', $exercices->getCompteResultat($exercice['id'])); + +$tpl->assign('cloture', $exercice['cloture'] ? $exercice['fin'] : time()); +$tpl->assign('exercice', $exercice); + +$tpl->display('admin/compta/exercices/compte_resultat.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/exercices/grand_livre.php b/www/admin/compta/exercices/grand_livre.php new file mode 100644 index 0000000..bf61f04 --- /dev/null +++ b/www/admin/compta/exercices/grand_livre.php @@ -0,0 +1,31 @@ +get((int)utils::get('id')); + +if (!$exercice) +{ + throw new UserException('Exercice inconnu.'); +} + +$liste_comptes = $comptes->getListAll(); + +function get_nom_compte($compte) +{ + global $liste_comptes; + return $liste_comptes[$compte]; +} + +$tpl->register_modifier('get_nom_compte', 'Garradin\get_nom_compte'); +$tpl->assign('livre', $exercices->getGrandLivre($exercice['id'])); + +$tpl->assign('cloture', $exercice['cloture'] ? $exercice['fin'] : time()); +$tpl->assign('exercice', $exercice); + +$tpl->display('admin/compta/exercices/grand_livre.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/exercices/index.php b/www/admin/compta/exercices/index.php new file mode 100644 index 0000000..98e5fff --- /dev/null +++ b/www/admin/compta/exercices/index.php @@ -0,0 +1,13 @@ +assign('liste', $e->getList()); +$tpl->assign('current', $e->getCurrent()); + +$tpl->display('admin/compta/exercices/index.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/exercices/journal.php b/www/admin/compta/exercices/journal.php new file mode 100644 index 0000000..8885012 --- /dev/null +++ b/www/admin/compta/exercices/journal.php @@ -0,0 +1,34 @@ +get((int)utils::get('id')); + +if (!$exercice) +{ + throw new UserException('Exercice inconnu.'); +} + +$liste_comptes = $comptes->getListAll(); + +function get_nom_compte($compte) +{ + if (is_null($compte)) + return ''; + + global $liste_comptes; + return $liste_comptes[$compte]; +} + +$tpl->register_modifier('get_nom_compte', 'Garradin\get_nom_compte'); +$tpl->assign('journal', $exercices->getJournal($exercice['id'])); + +$tpl->assign('cloture', $exercice['cloture'] ? $exercice['fin'] : time()); +$tpl->assign('exercice', $exercice); + +$tpl->display('admin/compta/exercices/journal.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/exercices/modifier.php b/www/admin/compta/exercices/modifier.php new file mode 100644 index 0000000..56b1e0c --- /dev/null +++ b/www/admin/compta/exercices/modifier.php @@ -0,0 +1,57 @@ +get((int)utils::get('id')); + +if (!$exercice) +{ + throw new UserException('Exercice inconnu.'); +} + +if ($exercice['cloture']) +{ + throw new UserException('Impossible de modifier un exercice clôturé.'); +} + +$error = false; + +if (!empty($_POST['edit'])) +{ + if (!utils::CSRF_check('compta_modif_exercice_'.$exercice['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $id = $e->edit($exercice['id'], [ + 'libelle' => utils::post('libelle'), + 'debut' => utils::post('debut'), + 'fin' => utils::post('fin'), + ]); + + utils::redirect('/admin/compta/exercices/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('exercice', $exercice); + +$tpl->display('admin/compta/exercices/modifier.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/exercices/supprimer.php b/www/admin/compta/exercices/supprimer.php new file mode 100644 index 0000000..89a617a --- /dev/null +++ b/www/admin/compta/exercices/supprimer.php @@ -0,0 +1,53 @@ +get((int)utils::get('id')); + +if (!$exercice) +{ + throw new UserException('Exercice inconnu.'); +} + +if ($exercice['cloture']) +{ + throw new UserException('Impossible de supprimer un exercice clôturé.'); +} + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('compta_supprimer_exercice_'.$exercice['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $id = $e->delete($exercice['id']); + + utils::redirect('/admin/compta/exercices/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('exercice', $exercice); + +$tpl->display('admin/compta/exercices/supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/graph.php b/www/admin/compta/graph.php new file mode 100644 index 0000000..95a900f --- /dev/null +++ b/www/admin/compta/graph.php @@ -0,0 +1,83 @@ +recettes()); + $r->title = 'Recettes'; + + $d = new \SVGPlot_Data($stats->depenses()); + $d->title = 'Dépenses'; + + $data = [$d, $r]; + + $plot->setTitle('Recettes et dépenses de l\'exercice courant'); + } + elseif ($graph == 'banques_caisses') + { + $banques = new Compta_Comptes_Bancaires; + + $data = []; + + $r = new \SVGPlot_Data($stats->soldeCompte(Compta_Comptes::CAISSE)); + $r->title = 'Caisse'; + + $data[] = $r; + + foreach ($banques->getList() as $banque) + { + $r = new \SVGPlot_Data($stats->soldeCompte($banque['id'])); + $r->title = $banque['libelle']; + $data[] = $r; + } + + $plot->setTitle('Solde des comptes et caisses'); + } + + if (!empty($data)) + { + $labels = []; + + foreach ($data[0]->get() as $k=>$v) + { + $labels[] = utils::date_fr('M y', strtotime(substr($k, 0, 4) . '-' . substr($k, 4, 2) .'-01')); + } + + $plot->setLabels($labels); + + $i = 0; + $colors = ['#c71', '#941', '#fa4', '#fd9', '#ffc', '#cc9']; + + foreach ($data as $line) + { + $line->color = $colors[$i++]; + $line->width = 2; + $plot->add($line); + + if ($i > count($colors)) + $i = 0; + } + } + + Static_Cache::store('graph_' . $graph, $plot->output()); +} + +header('Content-Type: image/svg+xml'); +Static_Cache::display('graph_' . $graph); diff --git a/www/admin/compta/import.php b/www/admin/compta/import.php new file mode 100644 index 0000000..22a1137 --- /dev/null +++ b/www/admin/compta/import.php @@ -0,0 +1,65 @@ +get('nom_asso') . ' - ' . date('Y-m-d') . '.csv"'); + $import->toCSV($e->getCurrentId()); + exit; +} + +$error = false; + +if (!empty($_POST['import'])) +{ + if (!utils::CSRF_check('compta_import')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + elseif (empty($_FILES['upload']['tmp_name'])) + { + $error = 'Aucun fichier fourni.'; + } + else + { + try + { + if (utils::post('type') == 'citizen') + { + $import->fromCitizen($_FILES['upload']['tmp_name']); + } + elseif (utils::post('type') == 'garradin') + { + $import->fromCSV($_FILES['upload']['tmp_name']); + } + else + { + throw new UserException('Import inconnu.'); + } + + utils::redirect('/admin/compta/import.php?ok'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('ok', isset($_GET['ok']) ? true : false); + +$tpl->display('admin/compta/import.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/index.php b/www/admin/compta/index.php new file mode 100644 index 0000000..3accfbf --- /dev/null +++ b/www/admin/compta/index.php @@ -0,0 +1,10 @@ +display('admin/compta/index.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/operations/index.php b/www/admin/compta/operations/index.php new file mode 100644 index 0000000..e257d68 --- /dev/null +++ b/www/admin/compta/operations/index.php @@ -0,0 +1,54 @@ +get(utils::get('cat')); + + if (!$cat) + { + throw new UserException("La catégorie demandée n'existe pas."); + } + + $type = $cat['type']; +} +else +{ + if (isset($_GET['autres'])) + $type = Compta_Categories::AUTRES; + elseif (isset($_GET['depenses'])) + $type = Compta_Categories::DEPENSES; + else + $type = Compta_Categories::RECETTES; +} + +$journal = new Compta_Journal; + +$list = $journal->getListForCategory($type === Compta_Categories::AUTRES ? null : $type, $cat ? $cat['id'] : null); + +$tpl->assign('categorie', $cat); +$tpl->assign('journal', $list); +$tpl->assign('type', $type); + +if ($type !== Compta_Categories::AUTRES) +{ + $tpl->assign('liste_cats', $cats->getList($type)); +} + +$total = 0.0; + +foreach ($list as $row) +{ + $total += (float) $row['montant']; +} + +$tpl->assign('total', $total); + +$tpl->display('admin/compta/operations/index.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/operations/membre.php b/www/admin/compta/operations/membre.php new file mode 100644 index 0000000..934f203 --- /dev/null +++ b/www/admin/compta/operations/membre.php @@ -0,0 +1,51 @@ +getCurrentId(); + +if (!$exercice) +{ + throw new UserException('Exercice inconnu.'); +} + +if (empty($_GET['id']) || !is_numeric($_GET['id'])) +{ + throw new UserException("Argument du numéro de membre manquant."); +} + +$id = (int) $_GET['id']; + +$membre = $membres->get($id); + +if (!$membre) +{ + throw new UserException("Le membre demandé n'existe pas."); +} + +$liste_comptes = $comptes->getListAll(); + +function get_nom_compte($compte) +{ + if (is_null($compte)) + return ''; + + global $liste_comptes; + return $liste_comptes[$compte]; +} + +$tpl->register_modifier('get_nom_compte', 'Garradin\get_nom_compte'); + +$tpl->assign('journal', $journal->listForMember($membre['id'], $exercice)); + +$tpl->assign('exercices', $exercices->getList()); +$tpl->assign('exercice', $exercice); +$tpl->assign('membre', $membre); + +$tpl->display('admin/compta/operations/membre.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/operations/modifier.php b/www/admin/compta/operations/modifier.php new file mode 100644 index 0000000..660b879 --- /dev/null +++ b/www/admin/compta/operations/modifier.php @@ -0,0 +1,152 @@ +get(utils::get('id')); + +if (!$operation) +{ + throw new UserException("L'opération demandée n'existe pas."); +} + +if ($operation['id_categorie']) +{ + $categorie = $cats->get($operation['id_categorie']); +} +else +{ + $categorie = false; +} + +if ($categorie && $categorie['type'] != Compta_Categories::AUTRES) +{ + $type = $categorie['type']; +} +else +{ + $type = null; +} + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('compta_modifier_'.$operation['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + if (is_null($type)) + { + $journal->edit($operation['id'], [ + 'libelle' => utils::post('libelle'), + 'montant' => utils::post('montant'), + 'date' => utils::post('date'), + 'compte_credit' => utils::post('compte_credit'), + 'compte_debit' => utils::post('compte_debit'), + 'numero_piece' => utils::post('numero_piece'), + 'remarques' => utils::post('remarques'), + ]); + } + else + { + $cat = $cats->get(utils::post('id_categorie')); + + if (!$cat) + { + throw new UserException('Il faut choisir une catégorie.'); + } + + if (!array_key_exists(utils::post('moyen_paiement'), $cats->listMoyensPaiement())) + { + throw new UserException('Moyen de paiement invalide.'); + } + + if (utils::post('moyen_paiement') == 'ES') + { + $a = Compta_Comptes::CAISSE; + $b = $cat['compte']; + } + else + { + if (!trim(utils::post('banque'))) + { + throw new UserException('Le compte bancaire choisi est invalide.'); + } + + if (!array_key_exists(utils::post('banque'), $banques->getList())) + { + throw new UserException('Le compte bancaire choisi n\'existe pas.'); + } + + $a = utils::post('banque'); + $b = $cat['compte']; + } + + if ($type == Compta_Categories::DEPENSES) + { + $debit = $b; + $credit = $a; + } + elseif ($type == Compta_Categories::RECETTES) + { + $debit = $a; + $credit = $b; + } + + $journal->edit($operation['id'], [ + 'libelle' => utils::post('libelle'), + 'montant' => utils::post('montant'), + 'date' => utils::post('date'), + 'moyen_paiement'=> utils::post('moyen_paiement'), + 'numero_cheque' => utils::post('numero_cheque'), + 'compte_credit' => $credit, + 'compte_debit' => $debit, + 'numero_piece' => utils::post('numero_piece'), + 'remarques' => utils::post('remarques'), + 'id_categorie' => (int)$cat['id'], + ]); + } + + utils::redirect('/admin/compta/operations/voir.php?id='.(int)$operation['id']); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('type', $type); + +if ($type === null) +{ + $tpl->assign('comptes', $comptes->listTree()); +} +else +{ + $tpl->assign('moyens_paiement', $cats->listMoyensPaiement()); + $tpl->assign('categories', $cats->getList($type)); + $tpl->assign('comptes_bancaires', $banques->getList()); +} + +$tpl->assign('operation', $operation); + +$tpl->display('admin/compta/operations/modifier.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/operations/recherche_sql.php b/www/admin/compta/operations/recherche_sql.php new file mode 100644 index 0000000..70f852a --- /dev/null +++ b/www/admin/compta/operations/recherche_sql.php @@ -0,0 +1,36 @@ +assign('schema', $journal->schemaSQL()); +$tpl->assign('query', $query); + +if ($query != '') +{ + try { + $tpl->assign('result', $journal->searchSQL($query)); + } + catch (\Exception $e) + { + $tpl->assign('result', null); + $tpl->assign('error', $e->getMessage()); + } +} +else +{ + $tpl->assign('result', null); +} + +$tpl->display('admin/compta/operations/recherche_sql.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/operations/saisir.php b/www/admin/compta/operations/saisir.php new file mode 100644 index 0000000..8847861 --- /dev/null +++ b/www/admin/compta/operations/saisir.php @@ -0,0 +1,194 @@ +checkExercice(); + +$cats = new Compta_Categories; +$banques = new Compta_Comptes_Bancaires; + +if (isset($_GET['depense'])) + $type = Compta_Categories::DEPENSES; +elseif (isset($_GET['virement'])) + $type = 'virement'; +elseif (isset($_GET['dette'])) + $type = 'dette'; +elseif (isset($_GET['avance'])) + $type = null; +else + $type = Compta_Categories::RECETTES; + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('compta_saisie')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + if (is_null($type)) + { + $id = $journal->add([ + 'libelle' => utils::post('libelle'), + 'montant' => utils::post('montant'), + 'date' => utils::post('date'), + 'compte_credit' => utils::post('compte_credit'), + 'compte_debit' => utils::post('compte_debit'), + 'numero_piece' => utils::post('numero_piece'), + 'remarques' => utils::post('remarques'), + 'id_auteur' => $user['id'], + ]); + } + elseif ($type === 'virement') + { + $id = $journal->add([ + 'libelle' => utils::post('libelle'), + 'montant' => utils::post('montant'), + 'date' => utils::post('date'), + 'compte_credit' => utils::post('compte1'), + 'compte_debit' => utils::post('compte2'), + 'numero_piece' => utils::post('numero_piece'), + 'remarques' => utils::post('remarques'), + 'id_auteur' => $user['id'], + ]); + } + else + { + $cat = $cats->get(utils::post('categorie')); + + if (!$cat) + { + throw new UserException('Il faut choisir une catégorie.'); + } + + if ($type == 'dette') + { + if (!trim(utils::post('compte')) || + (utils::post('compte') != 4010 && utils::post('compte') != 4110)) + { + throw new UserException('Type de dette invalide.'); + } + } + else + { + if (utils::post('moyen_paiement') == 'ES') + { + $a = Compta_Comptes::CAISSE; + $b = $cat['compte']; + } + else + { + if (!trim(utils::post('banque'))) + { + throw new UserException('Le compte bancaire choisi est invalide.'); + } + + if (!array_key_exists(utils::post('banque'), $banques->getList())) + { + throw new UserException('Le compte bancaire choisi n\'existe pas.'); + } + + $a = utils::post('banque'); + $b = $cat['compte']; + } + } + + if ($type === Compta_Categories::DEPENSES) + { + $debit = $b; + $credit = $a; + } + elseif ($type === Compta_Categories::RECETTES) + { + $debit = $a; + $credit = $b; + } + elseif ($type === 'dette') + { + $debit = $cat['compte']; + $credit = utils::post('compte'); + } + + $id = $journal->add([ + 'libelle' => utils::post('libelle'), + 'montant' => utils::post('montant'), + 'date' => utils::post('date'), + 'moyen_paiement'=> ($type === 'dette') ? null : utils::post('moyen_paiement'), + 'numero_cheque' => ($type === 'dette') ? null : utils::post('numero_cheque'), + 'compte_credit' => $credit, + 'compte_debit' => $debit, + 'numero_piece' => utils::post('numero_piece'), + 'remarques' => utils::post('remarques'), + 'id_categorie' => ($type === 'dette') ? null : (int)$cat['id'], + 'id_auteur' => $user['id'], + ]); + } + + $membres->sessionStore('compta_date', utils::post('date')); + + if ($type == Compta_Categories::DEPENSES) + $type = 'depense'; + elseif (is_null($type)) + $type = 'avance'; + elseif ($type == Compta_Categories::RECETTES) + $type = 'recette'; + + utils::redirect('/admin/compta/operations/saisir.php?'.$type.'&ok='.(int)$id); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('type', $type); + +if ($type === null) +{ + $tpl->assign('comptes', $comptes->listTree()); +} +else +{ + $tpl->assign('moyens_paiement', $cats->listMoyensPaiement()); + $tpl->assign('moyen_paiement', utils::post('moyen_paiement') ?: 'ES'); + $tpl->assign('categories', $cats->getList($type === 'dette' ? Compta_Categories::DEPENSES : $type)); + $tpl->assign('comptes_bancaires', $banques->getList()); + $tpl->assign('banque', utils::post('banque')); +} + +if (!$membres->sessionGet('compta_date')) +{ + $exercices = new Compta_Exercices; + $exercice = $exercices->getCurrent(); + + if ($exercice['debut'] > time() || $exercice['fin'] < time()) + { + $membres->sessionStore('compta_date', date('Y-m-d', $exercice['debut'])); + } + else + { + $membres->sessionStore('compta_date', date('Y-m-d')); + } +} + +$tpl->assign('date', $membres->sessionGet('compta_date') ?: false); +$tpl->assign('ok', (int) utils::get('ok')); + +$tpl->display('admin/compta/operations/saisir.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/operations/supprimer.php b/www/admin/compta/operations/supprimer.php new file mode 100644 index 0000000..e187f0f --- /dev/null +++ b/www/admin/compta/operations/supprimer.php @@ -0,0 +1,48 @@ +get(utils::get('id')); + +if (!$operation) +{ + throw new UserException("L'opération demandée n'existe pas."); +} + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('compta_supprimer_'.$operation['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try + { + $journal->delete($operation['id']); + utils::redirect('/admin/compta/operations/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('operation', $operation); + +$tpl->display('admin/compta/operations/supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/operations/voir.php b/www/admin/compta/operations/voir.php new file mode 100644 index 0000000..57d2ca3 --- /dev/null +++ b/www/admin/compta/operations/voir.php @@ -0,0 +1,53 @@ +get(utils::get('id')); + +if (!$operation) +{ + throw new UserException("L'opération demandée n'existe pas."); +} +$exercices = new Compta_Exercices; + +$tpl->assign('operation', $operation); + +$credit = $comptes->get($operation['compte_credit']); +$tpl->assign('nom_compte_credit', $credit['libelle']); + +$debit = $comptes->get($operation['compte_debit']); +$tpl->assign('nom_compte_debit', $debit['libelle']); + +$tpl->assign('exercice', $exercices->get($operation['id_exercice'])); + +if ($operation['id_categorie']) +{ + $cats = new Compta_Categories; + + $categorie = $cats->get($operation['id_categorie']); + $tpl->assign('categorie', $categorie); + + if ($categorie['type'] == Compta_Categories::RECETTES) + { + $tpl->assign('compte', $debit['libelle']); + } + else + { + $tpl->assign('compte', $credit['libelle']); + } + + $tpl->assign('moyen_paiement', $cats->getMoyenPaiement($operation['moyen_paiement'])); +} + +if ($operation['id_auteur']) +{ + $auteur = $membres->get($operation['id_auteur']); + $tpl->assign('nom_auteur', $auteur['identite']); +} + +$tpl->display('admin/compta/operations/voir.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/compta/pie.php b/www/admin/compta/pie.php new file mode 100644 index 0000000..7017969 --- /dev/null +++ b/www/admin/compta/pie.php @@ -0,0 +1,62 @@ +repartitionRecettes(); + $categories = $categories->getList(Compta_Categories::RECETTES); + $pie->setTitle('Répartition des recettes'); + } + else + { + $data = $stats->repartitionDepenses(); + $categories = $categories->getList(Compta_Categories::DEPENSES); + $pie->setTitle('Répartition des dépenses'); + } + + $others = 0; + $colors = ['#c71', '#941', '#fa4', '#fd9', '#ffc', '#cc9']; + $max = count($colors); + $i = 0; + + foreach ($data as $row) + { + if ($i++ >= $max) + { + $others += $row['nb']; + } + else + { + $cat = $categories[$row['id_categorie']]; + $pie->add(new \SVGPie_Data($row['nb'], substr($cat['intitule'], 0, 50), $colors[$i-1])); + } + } + + if ($others > 0) + { + $pie->add(new \SVGPie_Data($others, 'Autres', '#ccc')); + } + + Static_Cache::store('pie_' . $graph, $pie->output()); +} + +header('Content-Type: image/svg+xml'); +Static_Cache::display('pie_' . $graph); diff --git a/www/admin/config/_inc.php b/www/admin/config/_inc.php new file mode 100644 index 0000000..d2d283d --- /dev/null +++ b/www/admin/config/_inc.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/www/admin/config/donnees.php b/www/admin/config/donnees.php new file mode 100644 index 0000000..88955c0 --- /dev/null +++ b/www/admin/config/donnees.php @@ -0,0 +1,115 @@ +set('frequence_sauvegardes', utils::post('frequence_sauvegardes')); + $config->set('nombre_sauvegardes', utils::post('nombre_sauvegardes')); + $config->save(); + + utils::redirect('/admin/config/donnees.php?ok=config'); + } catch (UserException $e) { + $error = $e->getMessage(); + } + } +} +elseif (utils::post('create')) +{ + if (!utils::CSRF_check('backup_create')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $s->create(); + utils::redirect('/admin/config/donnees.php?ok=create'); + } catch (UserException $e) { + $error = $e->getMessage(); + } + } +} +elseif (utils::post('download')) +{ + if (!utils::CSRF_check('backup_download')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + header('Content-type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . $config->get('nom_asso') . ' - Sauvegarde données - ' . date('Y-m-d') . '.sqlite"'); + + $s->dump(); + exit; + } +} +elseif (utils::post('restore')) +{ + if (!utils::CSRF_check('backup_manage')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $s->restoreFromLocal(utils::post('file')); + utils::redirect('/admin/config/donnees.php?ok=restore'); + } catch (UserException $e) { + $error = $e->getMessage(); + } + } +} +elseif (utils::post('remove')) +{ + if (!utils::CSRF_check('backup_manage')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $s->remove(utils::post('file')); + utils::redirect('/admin/config/donnees.php?ok=remove'); + } catch (UserException $e) { + $error = $e->getMessage(); + } + } +} +elseif (utils::post('restore_file')) +{ + if (!utils::CSRF_check('backup_restore')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $s->restoreFromUpload($_FILES['file']); + utils::redirect('/admin/config/donnees.php?ok=restore'); + } catch (UserException $e) { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('ok', utils::get('ok')); +$tpl->assign('liste', $s->getList()); +$tpl->assign('max_file_size', utils::getMaxUploadSize()); + +$tpl->display('admin/config/donnees.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/config/import.php b/www/admin/config/import.php new file mode 100644 index 0000000..3f3cd9b --- /dev/null +++ b/www/admin/config/import.php @@ -0,0 +1,8 @@ +display('admin/config/import.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/config/index.php b/www/admin/config/index.php new file mode 100644 index 0000000..f4f291c --- /dev/null +++ b/www/admin/config/index.php @@ -0,0 +1,69 @@ +set('nom_asso', utils::post('nom_asso')); + $config->set('email_asso', utils::post('email_asso')); + $config->set('adresse_asso', utils::post('adresse_asso')); + $config->set('site_asso', utils::post('site_asso')); + $config->set('email_envoi_automatique', utils::post('email_envoi_automatique')); + $config->set('accueil_wiki', utils::post('accueil_wiki')); + $config->set('accueil_connexion', utils::post('accueil_connexion')); + $config->set('categorie_membres', utils::post('categorie_membres')); + + $config->set('champ_identite', utils::post('champ_identite')); + $config->set('champ_identifiant', utils::post('champ_identifiant')); + + $config->set('pays', utils::post('pays')); + $config->set('monnaie', utils::post('monnaie')); + + $config->save(); + + utils::redirect('/admin/config/?ok'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('garradin_version', garradin_version() . ' [' . (garradin_manifest() ?: 'release') . ']'); +$tpl->assign('php_version', phpversion()); + +$v = \SQLite3::version(); +$tpl->assign('sqlite_version', $v['versionString']); + +$tpl->assign('pays', utils::getCountryList()); + +$cats = new Membres_Categories; +$tpl->assign('membres_cats', $cats->listSimple()); + +$champs_liste = array_merge( + ['id' => ['title' => 'Numéro unique', 'type' => 'number']], + $config->get('champs_membres')->getList() +); +$tpl->assign('champs', $champs_liste); + +$tpl->display('admin/config/index.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/config/membres.php b/www/admin/config/membres.php new file mode 100644 index 0000000..b0d8653 --- /dev/null +++ b/www/admin/config/membres.php @@ -0,0 +1,142 @@ +sessionGet('champs_membres')) +{ + $champs = new Champs_Membres($champs); +} +else +{ + // Il est nécessaire de créer une nouvelle instance ici, sinon + // l'enregistrement des modifs ne marchera pas car les deux instances seront identiques. + // Càd si on utilise directement l'instance de $config, elle sera modifiée directement + // du coup quand on essaiera de comparer si ça a changé ça comparera deux fois la même chose + // donc ça n'aura pas changé forcément. + $champs = new Champs_Membres($config->get('champs_membres')); +} + +if (isset($_GET['ok'])) +{ + $error = 'OK'; +} + +if (!empty($_POST['save']) || !empty($_POST['add']) || !empty($_POST['review']) || !empty($_POST['reset'])) +{ + if (!utils::CSRF_check('config_membres')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + if (!empty($_POST['reset'])) + { + $membres->sessionStore('champs_membres', null); + utils::redirect('/admin/config/membres.php'); + } + elseif (!empty($_POST['review'])) + { + try { + $nouveau_champs = utils::post('champs'); + + foreach ($nouveau_champs as $key=>&$cfg) + { + $cfg['type'] = $champs->get($key, 'type'); + } + + $champs->setAll($nouveau_champs); + $membres->sessionStore('champs_membres', (string)$champs); + + utils::redirect('/admin/config/membres.php?review'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } + elseif (!empty($_POST['add'])) + { + try { + if (utils::post('preset')) + { + $presets = Champs_Membres::listUnusedPresets($champs); + if (!array_key_exists(utils::post('preset'), $presets)) + { + throw new UserException('Le champ pré-défini demandé ne fait pas partie des champs disponibles.'); + } + + $champs->add(utils::post('preset'), $presets[utils::post('preset')]); + } + elseif (utils::post('new')) + { + $presets = Champs_Membres::importPresets(); + $new = utils::post('new'); + + if (array_key_exists($new, $presets)) + { + throw new UserException('Le champ personnalisé ne peut avoir le même nom qu\'un champ pré-défini.'); + } + + $config = [ + 'type' => utils::post('new_type'), + 'title' => utils::post('new_title'), + 'editable' => true, + 'mandatory' => false, + ]; + + if ($config['type'] == 'select' || $config['type'] == 'multiple') + { + $config['options'] = ['Première option']; + } + + $champs->add($new, $config); + } + + $membres->sessionStore('champs_membres', (string) $champs); + + utils::redirect('/admin/config/membres.php?added'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } + elseif (!empty($_POST['save'])) + { + try { + $champs->save(); + $membres->sessionStore('champs_membres', null); + utils::redirect('/admin/config/membres.php?ok'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('review', isset($_GET['review']) ? true : false); + +$types = $champs->getTypes(); + +$tpl->assign('champs', $champs->getAll()); +$tpl->assign('types', $types); +$tpl->assign('presets', Champs_Membres::listUnusedPresets($champs)); +$tpl->assign('new', utils::post('new')); + +$tpl->register_modifier('get_type', function ($type) use ($types) { + return $types[$type]; +}); + +$tpl->assign('csrf_name', utils::CSRF_field_name('config_membres')); +$tpl->assign('csrf_value', utils::CSRF_create('config_membres')); + +$tpl->display('admin/config/membres.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/config/plugins.php b/www/admin/config/plugins.php new file mode 100644 index 0000000..2514b5a --- /dev/null +++ b/www/admin/config/plugins.php @@ -0,0 +1,66 @@ +getMessage(); + } + } +} + +if (utils::post('delete')) +{ + if (!utils::CSRF_check('delete_plugin_' . utils::get('delete'))) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $plugin = new Plugin(utils::get('delete')); + $plugin->uninstall(); + + utils::redirect('/admin/config/plugins.php'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +if (utils::get('delete')) +{ + $plugin = new Plugin(utils::get('delete')); + $tpl->assign('plugin', $plugin->getInfos()); + $tpl->assign('delete', true); +} +else +{ + $tpl->assign('liste_telecharges', Plugin::listDownloaded()); + $tpl->assign('liste_installes', Plugin::listInstalled()); +} + +$tpl->display('admin/config/plugins.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/config/site.php b/www/admin/config/site.php new file mode 100644 index 0000000..8bb2424 --- /dev/null +++ b/www/admin/config/site.php @@ -0,0 +1,78 @@ +set('champs_obligatoires', utils::post('champs_obligatoires')); + $config->set('champs_modifiables_membre', utils::post('champs_modifiables_membre')); + $config->set('categorie_membres', utils::post('categorie_membres')); + $config->save(); + + utils::redirect('/admin/config/site.php?ok'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +if (utils::get('edit')) +{ + $source = Squelette::getSource(utils::get('edit')); + + if (!$source) + { + throw new UserException("Ce squelette n'existe pas."); + } + + $csrf_key = 'edit_skel_'.md5(utils::get('edit')); + + if (utils::post('save')) + { + if (!utils::CSRF_check($csrf_key)) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + if (Squelette::editSource(utils::get('edit'), utils::post('content'))) + { + utils::redirect('/admin/config/site.php?edit='.rawurlencode(utils::get('edit')).'&ok'); + } + else + { + $error = "Impossible d'enregistrer le squelette."; + } + } + } + + $tpl->assign('edit', ['file' => trim(utils::get('edit')), 'content' => $source]); + $tpl->assign('csrf_key', $csrf_key); + $tpl->assign('sources_json', json_encode(Squelette::listSources())); +} +else +{ + $tpl->assign('sources', Squelette::listSources()); +} + +$tpl->assign('error', $error); +$tpl->display('admin/config/site.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/index.php b/www/admin/index.php new file mode 100644 index 0000000..d51ab83 --- /dev/null +++ b/www/admin/index.php @@ -0,0 +1,40 @@ +get($user['id_categorie']); + +$tpl->assign('categorie', $categorie); + +$wiki = new Wiki; +$page = $wiki->getByURI($config->get('accueil_connexion')); +$tpl->assign('page', $page); + +$cats = new Membres_Categories; + +$categorie = $cats->get($user['id_categorie']); + +$cotisations = new Cotisations_Membres; + +if (!empty($categorie['id_cotisation_obligatoire'])) +{ + $tpl->assign('cotisation', $cotisations->isMemberUpToDate($user['id'], $categorie['id_cotisation_obligatoire'])); +} +else +{ + $tpl->assign('cotisation', false); +} + +$tpl->display('admin/index.tpl'); +flush(); + +// Si pas de cron on réalise les tâches automatisées à ce moment-là +// c'est pas idéal mais mieux que rien +if (!USE_CRON) +{ + require_once ROOT . '/cron.php'; +} + +?> \ No newline at end of file diff --git a/www/admin/install.php b/www/admin/install.php new file mode 100644 index 0000000..ca1fe6c --- /dev/null +++ b/www/admin/install.php @@ -0,0 +1,246 @@ +\n\n\nErreur\n\n"; + echo ''; + echo "\n\n\n

    Erreur

    \n

    Le problème suivant empêche Garradin de fonctionner :

    \n"; + echo '

    ' . htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . '

    '; + echo '

    Pour plus d\'informations consulter '; + echo 'l\'aide sur les problèmes à l\'installation.

    '; + echo "\n\n"; + } + else + { + echo "[ERREUR] Le problème suivant empêche Garradin de fonctionner :\n"; + echo $message . "\n"; + echo "Pour plus d'informations consulter http://dev.kd2.org/garradin/Probl%C3%A8mes%20fr%C3%A9quents\n"; + } + + exit; +} + +test_requis( + version_compare(phpversion(), '5.4', '>='), + 'PHP 5.4 ou supérieur requis. PHP version ' . phpversion() . ' installée.' +); + +test_requis( + defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH, + 'L\'algorithme de hashage de mot de passe Blowfish n\'est pas présent (pas installé ou pas compilé).' +); + +test_requis( + class_exists('SQLite3'), + 'Le module de base de données SQLite3 n\'est pas disponible.' +); + +$v = \SQLite3::version(); + +test_requis( + version_compare($v['versionString'], '3.7.4', '>='), + 'SQLite3 version 3.7.4 ou supérieur requise. Version installée : ' . $v['versionString'] +); + +test_requis( + file_exists(__DIR__ . '/../../include/libs/template_lite/class.template.php'), + 'Librairie Template_Lite non disponible.' +); + +const INSTALL_PROCESS = true; + +require_once __DIR__ . '/../../include/init.php'; + +// Vérifier que les répertoires vides existent, sinon les créer +$paths = [DATA_ROOT . '/cache', DATA_ROOT . '/cache/static', DATA_ROOT . '/cache/compiled']; + +foreach ($paths as $path) +{ + if (!file_exists($path)) + mkdir($path); + + test_requis( + file_exists($path) && is_dir($path), + 'Le répertoire '.$path.' n\'existe pas ou n\'est pas un répertoire.' + ); + + // On en profite pour vérifier qu'on peut y lire et écrire + test_requis( + is_writable($path) && is_readable($path), + 'Le répertoire '.$path.' n\'est pas accessible en lecture/écriture.' + ); +} + +if (!file_exists(DB_FILE)) +{ + // Renommage du fichier sqlite à la version 0.5.0 + $old_file = str_replace('.sqlite', '.db', DB_FILE); + + if (file_exists($old_file)) + { + rename($old_file, DB_FILE); + utils::redirect('/admin/upgrade.php'); + } +} + +$tpl = Template::getInstance(); + +$tpl->assign('admin_url', WWW_URL . 'admin/'); + +if (file_exists(DB_FILE)) +{ + $tpl->assign('disabled', true); +} +else +{ + $tpl->assign('disabled', false); + $error = false; + + if (!empty($_POST['save'])) + { + if (!utils::CSRF_check('install')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + elseif (utils::post('passe_membre') != utils::post('repasse_membre')) + { + $error = 'La vérification ne correspond pas au mot de passe.'; + } + else + { + try { + $db = DB::getInstance(true); + + // Création de la base de données + $db->exec('BEGIN;'); + $db->exec(file_get_contents(DB_SCHEMA)); + $db->exec('END;'); + + // Configuration de base + $config = Config::getInstance(); + $config->set('nom_asso', utils::post('nom_asso')); + $config->set('adresse_asso', utils::post('adresse_asso')); + $config->set('email_asso', utils::post('email_asso')); + $config->set('site_asso', WWW_URL); + $config->set('monnaie', '€'); + $config->set('pays', 'FR'); + $config->set('email_envoi_automatique', utils::post('email_asso')); + $config->setVersion(garradin_version()); + + $champs = Champs_Membres::importInstall(); + $champs->save(false); // Pas de copie car pas de table membres existante + + $config->set('champ_identifiant', 'email'); + $config->set('champ_identite', 'nom'); + + // Création catégories + $cats = new Membres_Categories; + $id = $cats->add([ + 'nom' => 'Membres actifs', + ]); + $config->set('categorie_membres', $id); + + $id = $cats->add([ + 'nom' => 'Anciens membres', + 'droit_inscription' => Membres::DROIT_AUCUN, + 'droit_wiki' => Membres::DROIT_AUCUN, + 'droit_membres' => Membres::DROIT_AUCUN, + 'droit_compta' => Membres::DROIT_AUCUN, + 'droit_config' => Membres::DROIT_AUCUN, + 'droit_connexion' => Membres::DROIT_AUCUN, + 'cacher' => 1, + ]); + + $id = $cats->add([ + 'nom' => ucfirst(utils::post('cat_membre')), + 'droit_inscription' => Membres::DROIT_AUCUN, + 'droit_wiki' => Membres::DROIT_ADMIN, + 'droit_membres' => Membres::DROIT_ADMIN, + 'droit_compta' => Membres::DROIT_ADMIN, + 'droit_config' => Membres::DROIT_ADMIN, + ]); + + // Création premier membre + $membres = new Membres; + $id_membre = $membres->add([ + 'id_categorie' => $id, + 'nom' => utils::post('nom_membre'), + 'email' => utils::post('email_membre'), + 'passe' => utils::post('passe_membre'), + 'pays' => 'FR', + ]); + + // Création wiki + $page = Wiki::transformTitleToURI(utils::post('nom_asso')); + $config->set('accueil_wiki', $page); + $wiki = new Wiki; + $id_page = $wiki->create([ + 'titre' => utils::post('nom_asso'), + 'uri' => $page, + ]); + + $wiki->editRevision($id_page, 0, [ + 'id_auteur' => $id_membre, + 'contenu' => "Bienvenue dans le wiki de ".utils::post('nom_asso')." !\n\nCliquez sur le bouton « éditer » pour modifier cette page.", + ]); + + // Création page wiki connexion + $page = Wiki::transformTitleToURI('Bienvenue'); + $config->set('accueil_connexion', $page); + $id_page = $wiki->create([ + 'titre' => 'Bienvenue', + 'uri' => $page, + ]); + + $wiki->editRevision($id_page, 0, [ + 'id_auteur' => $id_membre, + 'contenu' => "Bienvenue dans l'administration de ".utils::post('nom_asso')." !\n\n" + . "Utilisez le menu à gauche pour accéder aux différentes rubriques.", + ]); + + // Mise en place compta + $comptes = new Compta_Comptes; + $comptes->importPlan(); + + $comptes = new Compta_Categories; + $comptes->importCategories(); + + $ex = new Compta_Exercices; + $ex->add([ + 'libelle' => 'Premier exercice', + 'debut' => date('Y-01-01'), + 'fin' => date('Y-12-31') + ]); + + $config->save(); + + utils::redirect('/admin/login.php'); + } + catch (UserException $e) + { + @unlink(DB_FILE); + + $error = $e->getMessage(); + } + } + } + + $tpl->assign('error', $error); +} + +$tpl->assign('passphrase', utils::suggestPassword()); +$tpl->display('admin/install.tpl'); diff --git a/www/admin/login.php b/www/admin/login.php new file mode 100644 index 0000000..7e33834 --- /dev/null +++ b/www/admin/login.php @@ -0,0 +1,56 @@ +isLogged()) +{ + utils::redirect('/admin/'); +} + +// Relance session_start et renvoie une image de 1px transparente +if (isset($_GET['keepSessionAlive'])) +{ + $membres->keepSessionAlive(); + + header('Cache-Control: no-cache, must-revalidate'); + header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); + + header('Content-Type: image/gif'); + echo base64_decode("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="); + + exit; +} + +$error = false; + +if (utils::post('login')) +{ + if (!utils::CSRF_check('login')) + { + $error = 'OTHER'; + } + else + { + if (utils::post('id') && utils::post('passe') + && $membres->login(utils::post('id'), utils::post('passe'))) + { + utils::redirect('/admin/'); + } + + $error = 'LOGIN'; + } +} + +$champs = $config->get('champs_membres'); + +$champ = $champs->get($config->get('champ_identifiant')); + +$tpl->assign('champ', $champ); +$tpl->assign('error', $error); + +$tpl->display('admin/login.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/logout.php b/www/admin/logout.php new file mode 100644 index 0000000..0a7c910 --- /dev/null +++ b/www/admin/logout.php @@ -0,0 +1,10 @@ +logout(); +utils::redirect('/'); + +?> \ No newline at end of file diff --git a/www/admin/membres/action.php b/www/admin/membres/action.php new file mode 100644 index 0000000..f32476f --- /dev/null +++ b/www/admin/membres/action.php @@ -0,0 +1,79 @@ +changeCategorie($_POST['id_categorie'], $_POST['selected']); + } + + utils::redirect('/admin/membres/'); + } +} +elseif (!empty($_POST['delete_ok'])) +{ + if (!utils::CSRF_check('membres_action')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + $membres->delete($_POST['selected']); + + utils::redirect('/admin/membres/'); + } +} + +$tpl->assign('selected', $_POST['selected']); +$tpl->assign('nb_selected', count($_POST['selected'])); + +if (!empty($_POST['move'])) +{ + $cats = new Membres_Categories; + + $tpl->assign('membres_cats', $cats->listSimple()); + $tpl->assign('action', 'move'); +} +elseif (!empty($_POST['delete'])) +{ + $tpl->assign('action', 'delete'); +} + +$tpl->assign('error', $error); + +$tpl->display('admin/membres/action.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/ajouter.php b/www/admin/membres/ajouter.php new file mode 100644 index 0000000..83cb269 --- /dev/null +++ b/www/admin/membres/ajouter.php @@ -0,0 +1,66 @@ +get('champs_membres'); + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('new_member')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + elseif (utils::post('passe') != utils::post('repasse')) + { + $error = 'La vérification ne correspond pas au mot de passe.'; + } + else + { + try + { + if ($user['droits']['membres'] == Membres::DROIT_ADMIN) + { + $id_categorie = utils::post('id_categorie'); + } + else + { + $id_categorie = $config->get('categorie_membres'); + } + + $data = ['id_categorie' => $id_categorie]; + + foreach ($champs->getAll() as $key=>$dismiss) + { + $data[$key] = utils::post($key); + } + + $id = $membres->add($data); + + utils::redirect('/admin/membres/fiche.php?id='.(int)$id); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('passphrase', utils::suggestPassword()); +$tpl->assign('champs', $champs->getAll()); + +$tpl->assign('membres_cats', $cats->listSimple()); +$tpl->assign('current_cat', utils::post('id_categorie') ?: $config->get('categorie_membres')); + +$tpl->display('admin/membres/ajouter.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cat_modifier.php b/www/admin/membres/cat_modifier.php new file mode 100644 index 0000000..090c6d4 --- /dev/null +++ b/www/admin/membres/cat_modifier.php @@ -0,0 +1,73 @@ +get($id); + +if (!$cat) +{ + throw new UserException("Cette catégorie n'existe pas."); +} + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('edit_cat_'.$id)) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $cats->edit($id, [ + 'nom' => utils::post('nom'), + 'description' => utils::post('description'), + 'droit_wiki' => (int) utils::post('droit_wiki'), + 'droit_compta' => (int) utils::post('droit_compta'), + 'droit_config' => (int) utils::post('droit_config'), + 'droit_membres' => (int) utils::post('droit_membres'), + 'droit_connexion' => (int) utils::post('droit_connexion'), + 'droit_inscription' => (int) utils::post('droit_inscription'), + 'cacher' => (int) utils::post('cacher'), + 'id_cotisation_obligatoire' => (int) utils::post('id_cotisation_obligatoire'), + ]); + + if ($id == $user['id_categorie']) + { + $membres->updateSessionData(); + } + + utils::redirect('/admin/membres/categories.php'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('cat', $cat); +$tpl->assign('error', $error); + +$cotisations = new Cotisations; +$tpl->assign('cotisations', $cotisations->listCurrent()); + +$tpl->display('admin/membres/cat_modifier.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cat_supprimer.php b/www/admin/membres/cat_supprimer.php new file mode 100644 index 0000000..36275f6 --- /dev/null +++ b/www/admin/membres/cat_supprimer.php @@ -0,0 +1,53 @@ +get($id); + +if (!$cat) +{ + throw new UserException("Cette catégorie n'existe pas."); +} + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('delete_cat_'.$id)) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $cats->remove($id); + utils::redirect('/admin/membres/categories.php'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('cat', $cat); +$tpl->assign('error', $error); + +$tpl->display('admin/membres/cat_supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/categories.php b/www/admin/membres/categories.php new file mode 100644 index 0000000..27d9edc --- /dev/null +++ b/www/admin/membres/categories.php @@ -0,0 +1,43 @@ +add([ + 'nom' => utils::post('nom'), + ]); + + utils::redirect('/admin/membres/categories.php'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('liste', $cats->listCompleteWithStats()); + +$tpl->display('admin/membres/categories.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations.php b/www/admin/membres/cotisations.php new file mode 100644 index 0000000..0766514 --- /dev/null +++ b/www/admin/membres/cotisations.php @@ -0,0 +1,49 @@ +get($id); + +if (!$membre) +{ + throw new UserException("Ce membre n'existe pas."); +} + +$cats = new Membres_Categories; + +$categorie = $cats->get($membre['id_categorie']); +$tpl->assign('categorie', $categorie); + +$cotisations = new Cotisations_Membres; + +if (!empty($categorie['id_cotisation_obligatoire'])) +{ + $tpl->assign('cotisation', $cotisations->isMemberUpToDate($membre['id'], $categorie['id_cotisation_obligatoire'])); +} +else +{ + $tpl->assign('cotisation', false); +} + +$tpl->assign('nb_activites', $cotisations->countForMember($membre['id'])); +$tpl->assign('cotisations', $cotisations->listForMember($membre['id'])); +$tpl->assign('cotisations_membre', $cotisations->listSubscriptionsForMember($membre['id'])); + +$tpl->assign('membre', $membre); + +$tpl->display('admin/membres/cotisations.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/ajout.php b/www/admin/membres/cotisations/ajout.php new file mode 100644 index 0000000..c904499 --- /dev/null +++ b/www/admin/membres/cotisations/ajout.php @@ -0,0 +1,112 @@ +get((int) $_GET['id']); + + if (!$membre) + { + throw new UserException("Ce membre n'existe pas."); + } + + $cats = new Membres_Categories; + $categorie = $cats->get($membre['id_categorie']); +} +else +{ + $categorie = ['id_cotisation_obligatoire' => false]; +} + +$cotisations = new Cotisations; +$m_cotisations = new Cotisations_Membres; + +$cats = new Compta_Categories; +$banques = new Compta_Comptes_Bancaires; + +$error = false; + +if (!empty($_POST['add'])) +{ + if (!utils::CSRF_check('add_cotisation')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $data = [ + 'date' => utils::post('date'), + 'id_cotisation' => utils::post('id_cotisation'), + 'id_membre' => utils::post('id_membre'), + 'id_auteur' => $user['id'], + 'montant' => utils::post('montant'), + 'moyen_paiement' => utils::post('moyen_paiement'), + 'numero_cheque' => utils::post('numero_cheque'), + 'banque' => utils::post('banque'), + ]; + + $m_cotisations->add($data); + + utils::redirect('/admin/membres/cotisations.php?id=' . (int)utils::post('id_membre')); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('membre', $membre); + +$tpl->assign('cotisations', $cotisations->listCurrent()); + +$tpl->assign('default_co', null); +$tpl->assign('default_amount', 0.00); +$tpl->assign('default_date', date('Y-m-d')); +$tpl->assign('default_compta', null); + +$tpl->assign('moyens_paiement', $cats->listMoyensPaiement()); +$tpl->assign('moyen_paiement', utils::post('moyen_paiement') ?: 'ES'); +$tpl->assign('comptes_bancaires', $banques->getList()); +$tpl->assign('banque', utils::post('banque')); + + +if (utils::get('cotisation')) +{ + $co = $cotisations->get(utils::get('cotisation')); + + if (!$co) + { + throw new UserException("La cotisation indiquée en paramètre n'existe pas."); + } + + $tpl->assign('default_co', $co['id']); + $tpl->assign('default_compta', $co['id_categorie_compta']); + $tpl->assign('default_amount', $co['montant']); +} +elseif ($membre) +{ + if (!empty($categorie['id_cotisation_obligatoire'])) + { + $co = $cotisations->get($categorie['id_cotisation_obligatoire']); + + $tpl->assign('default_co', $co['id']); + $tpl->assign('default_amount', $co['montant']); + } +} + + +$tpl->display('admin/membres/cotisations/ajout.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/gestion/modifier.php b/www/admin/membres/cotisations/gestion/modifier.php new file mode 100644 index 0000000..0470c31 --- /dev/null +++ b/www/admin/membres/cotisations/gestion/modifier.php @@ -0,0 +1,71 @@ +get(utils::get('id')); +$cats = new Compta_Categories; + +if (!$co) +{ + throw new UserException("Cette cotisation n'existe pas."); +} + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('edit_co_' . $co['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $duree = utils::post('periodicite') == 'jours' ? (int) utils::post('duree') : null; + $debut = utils::post('periodicite') == 'date' ? utils::post('debut') : null; + $fin = utils::post('periodicite') == 'date' ? utils::post('fin') : null; + $id_cat = utils::post('categorie') ? (int) utils::post('id_categorie_compta') : null; + + $cotisations->edit($co['id'], [ + 'intitule' => utils::post('intitule'), + 'description' => utils::post('description'), + 'montant' => (float) utils::post('montant'), + 'duree' => $duree, + 'debut' => $debut, + 'fin' => $fin, + 'id_categorie_compta'=> $id_cat, + ]); + + utils::redirect('/admin/membres/cotisations/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$co['periodicite'] = $co['duree'] ? 'jours' : ($co['debut'] ? 'date' : 'ponctuel'); +$co['categorie'] = $co['id_categorie_compta'] ? 1 : 0; + +$tpl->assign('cotisation', $co); +$tpl->assign('categories', $cats->getList(Compta_Categories::RECETTES)); + +$tpl->display('admin/membres/cotisations/gestion/modifier.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/gestion/rappel_modifier.php b/www/admin/membres/cotisations/gestion/rappel_modifier.php new file mode 100644 index 0000000..7ada0f0 --- /dev/null +++ b/www/admin/membres/cotisations/gestion/rappel_modifier.php @@ -0,0 +1,71 @@ +get(utils::get('id')); + +if (!$rappel) +{ + throw new UserException("Ce rappel n'existe pas."); +} + +$cotisations = new Cotisations; + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('edit_rappel_' . $rappel['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + if (utils::post('delai_choix') == 0) + $delai = 0; + elseif (utils::post('delai_choix') > 0) + $delai = (int) utils::post('delai_post'); + else + $delai = -(int) utils::post('delai_pre'); + + $rappels->edit($rappel['id'], [ + 'sujet' => utils::post('sujet'), + 'texte' => utils::post('texte'), + 'delai' => $delai, + 'id_cotisation' => utils::post('id_cotisation'), + ]); + + utils::redirect('/admin/membres/cotisations/gestion/rappels.php'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$rappel['delai_pre'] = $rappel['delai_post'] = abs($rappel['delai']) ?: 30; +$rappel['delai_choix'] = $rappel['delai'] == 0 ? 0 : ($rappel['delai'] > 0 ? 1 : -1); + +$tpl->assign('rappel', $rappel); +$tpl->assign('cotisations', $cotisations->listCurrent()); + +$tpl->display('admin/membres/cotisations/gestion/rappel_modifier.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/gestion/rappel_supprimer.php b/www/admin/membres/cotisations/gestion/rappel_supprimer.php new file mode 100644 index 0000000..7dcb180 --- /dev/null +++ b/www/admin/membres/cotisations/gestion/rappel_supprimer.php @@ -0,0 +1,52 @@ +get(utils::get('id')); + +if (!$rappel) +{ + throw new UserException("Ce rappel n'existe pas."); +} + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('delete_rappel_' . $rappel['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $rappels->delete($rappel['id'], (bool) utils::post('delete_history')); + utils::redirect('/admin/membres/cotisations/gestion/rappels.php'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('rappel', $rappel); + +$tpl->display('admin/membres/cotisations/gestion/rappel_supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/gestion/rappels.php b/www/admin/membres/cotisations/gestion/rappels.php new file mode 100644 index 0000000..3231011 --- /dev/null +++ b/www/admin/membres/cotisations/gestion/rappels.php @@ -0,0 +1,60 @@ + 0) + $delai = (int) utils::post('delai_post'); + else + $delai = -(int) utils::post('delai_pre'); + + $rappels->add([ + 'sujet' => utils::post('sujet'), + 'texte' => utils::post('texte'), + 'delai' => $delai, + 'id_cotisation' => utils::post('id_cotisation'), + ]); + + utils::redirect('/admin/membres/cotisations/gestion/rappels.php'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('liste', $rappels->listByCotisation()); +$tpl->assign('cotisations', $cotisations->listCurrent()); + +$tpl->assign('default_subject', '[#NOM_ASSO] Échéance de cotisation'); +$tpl->assign('default_text', "Bonjour #IDENTITE,\n\nVotre cotisation arrive à échéance dans #NB_JOURS jours.\n\n" + . "Merci de nous contacter pour renouveler votre cotisation.\n\nCordialement.\n\n" + . "--\n#NOM_ASSO\n#ADRESSE_ASSO\nE-Mail : #EMAIL_ASSO\nSite web : #SITE_ASSO"); + +$tpl->display('admin/membres/cotisations/gestion/rappels.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/gestion/supprimer.php b/www/admin/membres/cotisations/gestion/supprimer.php new file mode 100644 index 0000000..5ee7999 --- /dev/null +++ b/www/admin/membres/cotisations/gestion/supprimer.php @@ -0,0 +1,52 @@ +get(utils::get('id')); + +if (!$co) +{ + throw new UserException("Cette cotisation n'existe pas."); +} + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('delete_co_' . $co['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $cotisations->delete($co['id']); + utils::redirect('/admin/membres/cotisations/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->assign('cotisation', $co); + +$tpl->display('admin/membres/cotisations/gestion/supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/index.php b/www/admin/membres/cotisations/index.php new file mode 100644 index 0000000..3077b9b --- /dev/null +++ b/www/admin/membres/cotisations/index.php @@ -0,0 +1,61 @@ += Membres::DROIT_ADMIN) +{ + $cats = new Compta_Categories; + + $error = false; + + if (!empty($_POST['save'])) + { + if (!utils::CSRF_check('new_cotisation')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $duree = utils::post('periodicite') == 'jours' ? (int) utils::post('duree') : null; + $debut = utils::post('periodicite') == 'date' ? utils::post('debut') : null; + $fin = utils::post('periodicite') == 'date' ? utils::post('fin') : null; + $id_cat = utils::post('categorie') ? (int) utils::post('id_categorie_compta') : null; + + $cotisations->add([ + 'intitule' => utils::post('intitule'), + 'description' => utils::post('description'), + 'montant' => (float) utils::post('montant'), + 'duree' => $duree, + 'debut' => $debut, + 'fin' => $fin, + 'id_categorie_compta'=> $id_cat, + ]); + + utils::redirect('/admin/membres/cotisations/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } + } + + $tpl->assign('error', $error); + $tpl->assign('categories', $cats->getList(Compta_Categories::RECETTES)); +} + + +$tpl->assign('liste', $cotisations->listCurrentWithStats()); + +$tpl->display('admin/membres/cotisations/index.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/rappels.php b/www/admin/membres/cotisations/rappels.php new file mode 100644 index 0000000..26bd0a9 --- /dev/null +++ b/www/admin/membres/cotisations/rappels.php @@ -0,0 +1,64 @@ +get($id); + +if (!$membre) +{ + throw new UserException("Ce membre n'existe pas."); +} + +$re = new Rappels_Envoyes; +$cm = new Cotisations_Membres; + +$error = false; + +if (utils::post('save')) +{ + if (!utils::CSRF_check('add_rappel_'.$membre['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $re->add([ + 'id_cotisation' => utils::post('id_cotisation'), + 'id_membre' => $membre['id'], + 'media' => utils::post('media'), + 'date' => utils::post('date'), + ]); + + utils::redirect('/admin/membres/cotisations/rappels.php?id=' . $membre['id'] . '&ok'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('ok', isset($_GET['ok'])); +$tpl->assign('membre', $membre); +$tpl->assign('cotisations', $cm->listSubscriptionsForMember($membre['id'])); +$tpl->assign('default_date', date('Y-m-d')); +$tpl->assign('rappels', $re->listForMember($membre['id'])); + +$tpl->display('admin/membres/cotisations/rappels.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/supprimer.php b/www/admin/membres/cotisations/supprimer.php new file mode 100644 index 0000000..c3bfe5a --- /dev/null +++ b/www/admin/membres/cotisations/supprimer.php @@ -0,0 +1,64 @@ +get($id); + +if (!$co) +{ + throw new UserException("Cette cotisation membre n'existe pas."); +} + +$membre = $membres->get($co['id_membre']); + +if (!$membre) +{ + throw new UserException("Le membre lié à la cotisation n'existe pas ou plus."); +} + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('del_cotisation_' . $co['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $m_cotisations->delete($co['id']); + utils::redirect('/admin/membres/cotisations.php?id=' . $membre['id']); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('membre', $membre); +$tpl->assign('cotisation', $co); + +$tpl->display('admin/membres/cotisations/supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/cotisations/voir.php b/www/admin/membres/cotisations/voir.php new file mode 100644 index 0000000..faf8802 --- /dev/null +++ b/www/admin/membres/cotisations/voir.php @@ -0,0 +1,43 @@ +get($id); + +if (!$co) +{ + throw new UserException("Cette cotisation n'existe pas."); +} + +$page = (int) utils::get('p') ?: 1; + +$tpl->assign('page', $page); +$tpl->assign('bypage', Cotisations_Membres::ITEMS_PER_PAGE); +$tpl->assign('total', $m_cotisations->countMembersForCotisation($co['id'])); +$tpl->assign('pagination_url', utils::getSelfUrl(true) . '?id=' . $co['id'] . '&p=[ID]'); + +$tpl->assign('cotisation', $co); +$tpl->assign('order', utils::get('o') ?: 'date'); +$tpl->assign('desc', !isset($_GET['a'])); +$tpl->assign('liste', $m_cotisations->listMembersForCotisation( + $co['id'], $page, utils::get('o'), isset($_GET['a']) ? false : true)); + +$tpl->display('admin/membres/cotisations/voir.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/fiche.php b/www/admin/membres/fiche.php new file mode 100644 index 0000000..e108f86 --- /dev/null +++ b/www/admin/membres/fiche.php @@ -0,0 +1,56 @@ +get($id); + +if (!$membre) +{ + throw new UserException("Ce membre n'existe pas."); +} + +$champs = $config->get('champs_membres'); +$tpl->assign('champs', $champs->getAll()); + +$cats = new Membres_Categories; + +$categorie = $cats->get($membre['id_categorie']); +$tpl->assign('categorie', $categorie); + +$cotisations = new Cotisations_Membres; + +if (!empty($categorie['id_cotisation_obligatoire'])) +{ + $tpl->assign('cotisation', $cotisations->isMemberUpToDate($membre['id'], $categorie['id_cotisation_obligatoire'])); +} +else +{ + $tpl->assign('cotisation', false); +} + +$tpl->assign('nb_activites', $cotisations->countForMember($membre['id'])); + +if ($user['droits']['compta'] >= Membres::DROIT_ACCES) +{ + $journal = new Compta_Journal; + $tpl->assign('nb_operations', $journal->countForMember($membre['id'])); +} + +$tpl->assign('membre', $membre); + +$tpl->display('admin/membres/fiche.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/import.php b/www/admin/membres/import.php new file mode 100644 index 0000000..394d81f --- /dev/null +++ b/www/admin/membres/import.php @@ -0,0 +1,71 @@ +get('nom_asso') . ' - ' . date('Y-m-d') . '.csv"'); + $import->toCSV(); + exit; +} + +$error = false; +$champs = $config->get('champs_membres')->getAll(); +$champs['date_inscription'] = ['title' => 'Date inscription', 'type' => 'date']; + +if (utils::post('import')) +{ + // FIXME + if (false && !utils::CSRF_check('membres_import')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + elseif (empty($_FILES['upload']['tmp_name'])) + { + $error = 'Aucun fichier fourni.'; + } + else + { + try + { + if (utils::post('type') == 'galette') + { + $import->fromGalette($_FILES['upload']['tmp_name'], utils::post('galette_translate')); + } + elseif (utils::post('type') == 'garradin') + { + $import->fromCSV($_FILES['upload']['tmp_name']); + } + else + { + throw new UserException('Import inconnu.'); + } + + utils::redirect('/admin/membres/import.php?ok'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('ok', isset($_GET['ok']) ? true : false); + +$tpl->assign('garradin_champs', $champs); +$tpl->assign('galette_champs', $import->galette_fields); +$tpl->assign('translate', utils::post('galette_translate')); + +$tpl->display('admin/membres/import.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/index.php b/www/admin/membres/index.php new file mode 100644 index 0000000..450264b --- /dev/null +++ b/www/admin/membres/index.php @@ -0,0 +1,77 @@ +search($config->get('champ_identite'), $recherche); + $tpl->assign('liste', $result); + $tpl->assign('recherche', $recherche); +} +else +{ + $cats = new Membres_Categories; + $champs = $config->get('champs_membres'); + + $membres_cats = $cats->listSimple(); + $membres_cats_cachees = $cats->listHidden(); + + $cat_id = (int) utils::get('cat') ?: 0; + $page = (int) utils::get('p') ?: 1; + + if ($cat_id) + { + if ($user['droits']['membres'] < Membres::DROIT_ECRITURE && array_key_exists($cat_id, $membres_cats_cachees)) + { + $cat_id = 0; + } + } + + if (!$cat_id) + { + $cat_id = array_diff(array_keys($membres_cats), array_keys($membres_cats_cachees)); + } + + $order = $champs->getFirst(); + $desc = false; + + if (utils::get('o')) + $order = utils::get('o'); + + if (isset($_GET['d'])) + $desc = true; + + $tpl->assign('order', $order); + $tpl->assign('desc', $desc); + + $fields = $champs->getListedFields(); + + $tpl->assign('champs', $fields); + + $tpl->assign('liste', $membres->listByCategory($cat_id, array_keys($fields), $page, $order, $desc)); + $tpl->assign('total', $membres->countByCategory($cat_id)); + + $tpl->assign('pagination_url', utils::getSelfUrl(true) . '?p=[ID]&o=' . $order . ($desc ? '&d' : '')); + + $tpl->assign('membres_cats', $membres_cats); + $tpl->assign('membres_cats_cachees', $membres_cats_cachees); + $tpl->assign('current_cat', $cat_id); + + $tpl->assign('page', $page); + $tpl->assign('bypage', Membres::ITEMS_PER_PAGE); + +} + +$tpl->display('admin/membres/index.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/message.php b/www/admin/membres/message.php new file mode 100644 index 0000000..67caabd --- /dev/null +++ b/www/admin/membres/message.php @@ -0,0 +1,69 @@ +get($id); + +if (!$membre) +{ + throw new UserException("Ce membre n'existe pas."); +} + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('send_message_'.$id)) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + elseif (!utils::post('sujet')) + { + $error = 'Le sujet ne peut rester vide.'; + } + elseif (!utils::post('message')) + { + $error = 'Le message ne peut rester vide.'; + } + else + { + try { + $membres->sendMessage($membre['email'], utils::post('sujet'), + utils::post('message'), (bool) utils::post('copie')); + + utils::redirect('/admin/membres/?sent'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$cats = new Membres_Categories; + +$tpl->assign('categorie', $cats->get($membre['id_categorie'])); +$tpl->assign('membre', $membre); +$tpl->assign('error', $error); + +$tpl->display('admin/membres/message.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/message_collectif.php b/www/admin/membres/message_collectif.php new file mode 100644 index 0000000..c302a14 --- /dev/null +++ b/www/admin/membres/message_collectif.php @@ -0,0 +1,48 @@ +sendMessageToCategory(utils::post('dest'), utils::post('sujet'), utils::post('message'), (bool) utils::post('subscribed')); + utils::redirect('/admin/membres/?sent'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$cats = new Membres_Categories; + +$tpl->assign('cats_liste', $cats->listSimple()); +$tpl->assign('cats_cachees', $cats->listHidden()); +$tpl->assign('error', $error); + +$tpl->display('admin/membres/message_collectif.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/modifier.php b/www/admin/membres/modifier.php new file mode 100644 index 0000000..9bb1df6 --- /dev/null +++ b/www/admin/membres/modifier.php @@ -0,0 +1,88 @@ +get($id); + +if (!$membre) +{ + throw new UserException("Ce membre n'existe pas."); +} + +$cats = new Membres_Categories; +$champs = $config->get('champs_membres'); + +// Protection contre la modification des admins par des membres moins puissants +$membre_cat = $cats->get($membre['id_categorie']); +if (($membre_cat['droit_membres'] == Membres::DROIT_ADMIN) + && ($user['droits']['membres'] < Membres::DROIT_ADMIN)) +{ + throw new UserException("Seul un membre admin peut modifier un autre membre admin."); +} + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('edit_member_'.$id)) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + elseif (utils::post('passe') != utils::post('repasse')) + { + $error = 'La vérification ne correspond pas au mot de passe.'; + } + else + { + try { + $data = []; + + foreach ($champs->getAll() as $key=>$config) + { + $data[$key] = utils::post($key); + } + + if ($user['droits']['membres'] == Membres::DROIT_ADMIN) + { + $data['id_categorie'] = utils::post('id_categorie'); + $data['id'] = utils::post('id'); + } + + $membres->edit($id, $data); + + utils::redirect('/admin/membres/fiche.php?id='.(int)$id); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('passphrase', utils::suggestPassword()); +$tpl->assign('champs', $champs->getAll()); + +$tpl->assign('membres_cats', $cats->listSimple()); +$tpl->assign('current_cat', utils::post('id_categorie') ?: $membre['id_categorie']); + +$tpl->assign('can_change_id', $user['droits']['membres'] == Membres::DROIT_ADMIN); + +$tpl->assign('membre', $membre); + +$tpl->display('admin/membres/modifier.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/recherche.php b/www/admin/membres/recherche.php new file mode 100644 index 0000000..f40fb5e --- /dev/null +++ b/www/admin/membres/recherche.php @@ -0,0 +1,81 @@ +get('champs_membres'); + +$auto = false; + +// On détermine magiquement quel champ on recherche +if (!$champ) +{ + $auto = true; + + if (is_numeric(trim($recherche))) { + $champ = 'id'; + } + elseif (strpos($recherche, '@') !== false) { + $champ = 'email'; + } + else { + $champ = $config->get('champ_identite'); + } +} +else +{ + if ($champ != 'id' && !$champs->get($champ)) + { + throw new UserException('Le champ demandé n\'existe pas.'); + } +} + +if ($recherche != '') +{ + $result = $membres->search($champ, $recherche); + + if (count($result) == 1 && $auto) + { + utils::redirect('/admin/membres/fiche.php?id=' . (int)$result[0]['id']); + } +} + +$champs_liste = $champs->getList(); + +$champs_liste = array_merge( + ['id' => ['title' => 'Numéro unique', 'type' => 'number']], + $champs_liste +); + +$champs_entete = $champs->getListedFields(); + +if (!array_key_exists($champ, $champs_entete)) +{ + $champs_entete = array_merge( + [$champ => $champs_liste[$champ]], + $champs_entete + ); +} + +$tpl->assign('champs_entete', $champs_entete); +$tpl->assign('champs_liste', $champs_liste); +$tpl->assign('champ', $champ); + +if ($recherche != '') +{ + $tpl->assign('liste', $result); +} + +$tpl->assign('recherche', $recherche); + +$tpl->display('admin/membres/recherche.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/recherche_sql.php b/www/admin/membres/recherche_sql.php new file mode 100644 index 0000000..0afa37e --- /dev/null +++ b/www/admin/membres/recherche_sql.php @@ -0,0 +1,34 @@ +assign('schema', $membres->schemaSQL()); +$tpl->assign('query', $query); + +if ($query != '') +{ + try { + $tpl->assign('result', $membres->searchSQL($query)); + } + catch (\Exception $e) + { + $tpl->assign('result', null); + $tpl->assign('error', $e->getMessage()); + } +} +else +{ + $tpl->assign('result', null); +} + +$tpl->display('admin/membres/recherche_sql.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/membres/supprimer.php b/www/admin/membres/supprimer.php new file mode 100644 index 0000000..4c92d79 --- /dev/null +++ b/www/admin/membres/supprimer.php @@ -0,0 +1,44 @@ +get(utils::get('id')); + +if (!$membre) +{ + throw new UserException("Ce membre n'existe pas."); +} + +$error = false; + +if (utils::post('delete')) +{ + if (!utils::CSRF_check('delete_membre_'.$membre['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + try { + $membres->delete($membre['id']); + utils::redirect('/admin/membres/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('membre', $membre); +$tpl->assign('error', $error); + +$tpl->display('admin/membres/supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/mes_cotisations.php b/www/admin/mes_cotisations.php new file mode 100644 index 0000000..9bc825a --- /dev/null +++ b/www/admin/mes_cotisations.php @@ -0,0 +1,39 @@ +getLoggedUser(); + +if (!$membre) +{ + throw new UserException("Ce membre n'existe pas."); +} + +$error = false; + +$tpl->assign('membre', $membre); + +$cats = new Membres_Categories; + +$categorie = $cats->get($membre['id_categorie']); +$tpl->assign('categorie', $categorie); + +$cotisations = new Cotisations_Membres; + +if (!empty($categorie['id_cotisation_obligatoire'])) +{ + $tpl->assign('cotisation', $cotisations->isMemberUpToDate($membre['id'], $categorie['id_cotisation_obligatoire'])); +} +else +{ + $tpl->assign('cotisation', false); +} + +$tpl->assign('nb_activites', $cotisations->countForMember($membre['id'])); +$tpl->assign('cotisations', $cotisations->listForMember($membre['id'])); +$tpl->assign('cotisations_membre', $cotisations->listSubscriptionsForMember($membre['id'])); + +$tpl->display('admin/mes_cotisations.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/mes_infos.php b/www/admin/mes_infos.php new file mode 100644 index 0000000..f12394f --- /dev/null +++ b/www/admin/mes_infos.php @@ -0,0 +1,58 @@ +getLoggedUser(); + +if (!$membre) +{ + throw new UserException("Ce membre n'existe pas."); +} + +$error = false; + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('edit_me')) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + elseif (utils::post('passe') != utils::post('repasse')) + { + $error = 'La vérification ne correspond pas au mot de passe.'; + } + else + { + try { + $data = []; + + foreach ($config->get('champs_membres')->getAll() as $key=>$c) + { + if (!empty($c['editable'])) + { + $data[$key] = utils::post($key); + } + } + + $membres->edit($membre['id'], $data, false); + $membres->updateSessionData(); + + utils::redirect('/admin/'); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('passphrase', utils::suggestPassword()); +$tpl->assign('champs', $config->get('champs_membres')->getAll()); + +$tpl->assign('membre', $membre); + +$tpl->display('admin/mes_infos.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/password.php b/www/admin/password.php new file mode 100644 index 0000000..eff1c27 --- /dev/null +++ b/www/admin/password.php @@ -0,0 +1,56 @@ +recoverPasswordConfirm(utils::get('c'))) + { + utils::redirect('/admin/password.php?new_sent'); + } + + $error = 'EXPIRED'; +} +elseif (!empty($_POST['recover'])) +{ + if (!utils::CSRF_check('recoverPassword')) + { + $error = 'OTHER'; + } + else + { + if (trim(utils::post('id')) && $membres->recoverPasswordCheck(utils::post('id'))) + { + utils::redirect('/admin/password.php?sent'); + } + + $error = 'MAIL'; + } +} + +if (!$error && isset($_GET['sent'])) +{ + $tpl->assign('sent', true); +} +elseif (!$error && isset($_GET['new_sent'])) +{ + $tpl->assign('new_sent', true); +} + + +$champs = $config->get('champs_membres'); + +$champ = $champs->get($config->get('champ_identifiant')); + +$tpl->assign('champ', $champ); + +$tpl->assign('error', $error); + +$tpl->display('admin/password.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/plugin.php b/www/admin/plugin.php new file mode 100644 index 0000000..633e3a0 --- /dev/null +++ b/www/admin/plugin.php @@ -0,0 +1,18 @@ +path()); +define('Garradin\PLUGIN_URL', WWW_URL . 'admin/plugin/' . $plugin->id() . '/'); +define('Garradin\PLUGIN_QSP', '?'); + +$tpl->assign('plugin', $plugin->getInfos()); +$tpl->assign('plugin_root', PLUGIN_ROOT); + +$plugin->call('admin/' . $page); diff --git a/www/admin/static/admin.css b/www/admin/static/admin.css new file mode 100644 index 0000000..21762a9 --- /dev/null +++ b/www/admin/static/admin.css @@ -0,0 +1,1149 @@ +@charset "UTF-8"; + +@font-face { + font-family: 'gicon'; + src: url('font/garradin.eot?36341436'); + src: url('font/garradin.eot?36341436#iefix') format('embedded-opentype'), + url('font/garradin.woff?36341436') format('woff'), + url('font/garradin.ttf?36341436') format('truetype'), + url('font/garradin.svg?36341436#garradin') format('svg'); + font-weight: normal; + font-style: normal; +} + +body, form, p, div, hr, fieldset, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; +} +h1 { font-size: 2em; } +h2 { font-size: 1.5em; } +h3 { font-size: 1.2em; } +h4 { font-size: 1em; } +h5 { font-size: 0.9em; } +h6 { font-size: 0.8em; } +ul, ol { list-style-type: none; } +article, aside, figure, footer, header, hgroup, menu, nav, section { display: block; } + +/* + marron : #9c4f15 rgb(156, 79, 21) + orange : #d98628 rgb(217, 134, 40) +*/ + +html { width: 100%; height: 100%; } +body { + font-size: 100%; + color: #000; + font-family: "Trebuchet MS", Arial, Helvetica, Sans-serif; + background: #fff url("bg00.png") repeat-y left bottom; + background: url("bg01.png") no-repeat left -100px, url("bg00.png") repeat-y left bottom, #fff; + padding-bottom: 1em; +} + +body#popup { + background: url("bg01.png") no-repeat -140px -100px, url("bg00.png") repeat-y -140px bottom, #fff; +} + +.header { + color: #fff; +} + +.header h1 { + color: #9c4f15; + margin-left: 180px; + margin-bottom: 0.4em; +} + +.header .menu { + position: absolute; + width: 168px; + margin-top: 100px; +} + +.header .menu a { + color: #fff; + font-weight: bold; + padding: 0.4em 0.4em 0.4em 1em; + display: block; + text-decoration: none; +} + +.header .menu a:hover { + text-decoration: underline; + background: rgb(217, 134, 40); + background: rgba(217, 134, 40, 0.5); +} + +.header .menu li li a { + font-size: 0.8em; + padding-left: 2em; +} + +.header .menu li.current > a { + background: #fff; + color: rgb(156, 79, 21); +} + +.header .menu a b { + float: right; + text-decoration: none; + margin-top: -.2em; + font-size: 20pt; + color: rgb(70, 70, 70); + color: rgba(0, 0, 0, .5); +} + +.page { + margin: 0px 1em 1em 180px; + position: relative; +} + +.page img { + max-width: 100%; +} + +body#popup .page { + margin: 1em 1em 1em 2.5em; +} + +span.error, b.error { + color: #900; +} + +span.confirm, b.confirm { + color: #090; +} + +span.alert, b.alert { + color: #990; +} + +p.error { + border: 1px solid #c00; + background: #fcc; + padding: 0.5em; + margin-bottom: 1em; +} + +p.confirm { + border: 1px solid #0c0; + background: #cfc; + padding: 0.5em; + margin-bottom: 1em; +} + +p.alert { + border: 1px solid #cc0; + background: #ffc; + padding: 0.5em; + margin-bottom: 1em; +} + +p.help { + margin: 1em; + color: #666; +} + +p.intro { + margin: 1em; +} + +/* Formulaires */ +fieldset { + border: 1px solid #ccc; + padding: 0.8em 1em 0 1em; + margin-bottom: 1em; + padding: 0.5em; +} + +fieldset legend { + padding: 0 0.5em; + font-weight: bold; + color: #000; +} + +label:hover { + cursor: pointer; + border-bottom: 1px dotted #900; +} + +dl dt label { + font-weight: bold; +} + +fieldset dl dt b { + color: #900; + font-size: 0.7em; + font-weight: normal; + vertical-align: super; +} + +fieldset dl dd.tip { + color: #666; +} + +fieldset dl dd { + padding: 0.2em 0.5em 0.2em 1em; +} + +fieldset dl dd ol, fieldset dl dd ul { + margin-left: 1.5em; +} + +fieldset dl dl { + margin: .5em 0 .5em 1.2em; +} + +input[type=text], textarea, input[type=password], input[type=email], +input[type=url], input[type=tel], select { + padding: 0.2em 0.4em; + font-family: Sans-serif; + min-width: 20em; + max-width: 100%; +} + +input[size] { + min-width: 0; +} + +input.time { + text-align: center; + padding: .2em 0; +} + +input[type=number], input[type=date] { + padding: 0.2em 0.4em; + font-family: Sans-serif; + min-width: 2em; +} + +input[type=submit], input[type=button] { + padding: 0.3em; + cursor: pointer; + transition: opacity .5s ease; +} + +.loader { + width: 100%; + min-height: 32px; + display: block; + position: relative; +} + +.loader.install { + margin-top: -40px; +} + +.loader b { + text-shadow: 2px 2px 5px #999; + background: rgb(255, 255, 255); + background: rgba(255, 255, 255, 0.5); + border-radius: .5em; + font-size: 16px; + line-height: 16px; + height: 16px; + z-index: 9999; + position: absolute; + display: block; + left: 10px; + top: 10px; + padding: .2em; +} + +.loader img { + position: absolute; + opacity: 0; + transition: all 0.5s ease; + z-index: 2; +} + +input[type=button].icn { + font-size: 1.2em; + font-weight: bold; + padding: 0 0.3em; + font-family: "Courier New", Courier, monospace; +} + +select.large { + width: 95%; +} + +select.large optgroup.niveau_1 { + background: #333; + color: #fff; + font-style: normal; + font-size: 1.2em; +} + +select.large optgroup.niveau_2 { + background: #666; + color: #fff; + font-style: normal; + padding-left: 1em; +} + +select.large option { + background: #fff; + color: #000; +} + +select.large .niveau_2 { font-style: italic; background: #eee; } +select.large .niveau_3 { padding-left: 1em; font-weight: bold; } +select.large .niveau_4 { padding-left: 2em; } +select.large .niveau_5 { padding-left: 3em; } +select.large .niveau_6 { padding-left: 4em; } + +p.submit { + margin: 1em; +} + +.submit input[type=submit] { + font-size: 1.2em; +} + +.submit input.minor { + font-size: .9em; +} + +form .checkUncheck { + float: left; +} + +form span.password_check { + margin-left: 1em; + padding: .1em .3em; + border-radius: .5em; +} + +form span.password_check.fail { background-color: #f99; } +form span.password_check.weak { background-color: #ff9; } +form span.password_check.medium { background-color: #ccf; } +form span.password_check.ok { background-color: #cfc; } + +dd.help input[type=text] { + cursor: pointer; + padding: 0; + font-family: monospace; +} + +form p.actions { + float: right; +} + +ul.actions { + list-style-type: none; + margin: 1em 0; + border-bottom: .1em solid #9c4f15; + padding: 0 1em; +} + +ul.actions li { + display: inline-block; + margin: 0 0.2em; +} + +ul.actions li a { + display: inline-block; + background: rgb(217, 134, 40); + background: rgba(217, 134, 40, .5); + border-radius: .5em .5em 0 0; + padding: .1em .5em; + color: #000; + text-decoration: none; +} + +ul.actions li.current a { + background: #9c4f15; + color: #fff; +} + +ul.actions li a:hover { + color: #fff; + text-decoration: underline; +} + +h3.warning { + margin: 1em; + color: red; +} + +dd.help { + color: #666; +} + +table.list { + border-collapse: collapse; + margin-bottom: 1em; + width: 100%; +} + +table.list.auto { + width: auto; +} + +table.list table { + margin: 0; +} + +table.list th { + text-align: left; + font-weight: bold; +} + +table.list thead { + background: rgb(217, 134, 40); + background: rgba(217, 134, 40, 0.5); +} + +table.list tfoot tr { + background: rgb(247, 164, 70); + background: rgba(217, 134, 40, 0.1); + color: rgb(156, 79, 21); +} + +table.list th, table.list td { + padding: 0.2em 0.5em; +} + +table.list tr { + border: 1px solid rgb(217, 134, 40); + border: 1px solid rgba(217, 134, 40, 0.5); +} + +table.list tr:nth-child(even) { + background: rgb(255, 174, 80); + background: rgba(217, 134, 40, 0.2); +} + +table.list.multi tr:nth-child(even) { + background: inherit; +} + +table.list.multi tr:nth-child(4n+1), table.list.multi tr:nth-child(4n+2) { + background: rgb(255, 174, 80); + background: rgba(217, 134, 40, 0.2); +} + +table.list .error { + color: red; + font-weight: bold; +} + +table.list .alert { + color: darkred; + font-weight: bold; +} + +table.list .confirm { + color: darkgreen; +} + +table.list .num { + text-align: center; +} + +table.list .check { + width: 1%; +} + +table.search th { + background: rgb(217, 134, 40); + background: rgba(217, 134, 40, 0.5); +} + +.userOrder .cur { + background: rgb(217, 134, 40); + color: #fff; +} + +.userOrder td, .userOrder th { + position: relative; +} + +.userOrder .icn { + float: left; + clear: left; + color: #9c4f15; + text-decoration: none; + font-size: 12pt; + line-height: 6pt; + width: 12pt; + height: 8pt; + vertical-align: middle; + font-weight: normal; + text-shadow: 0px 0px 1px #fff; +} + +thead .icn:hover { + color: darkred; + text-shadow: none; +} + +thead .cur.desc .icn.dn, thead .cur.asc .icn.up { + color: #fff; + text-shadow: none; +} + +table.list .actions { + text-align: right; +} + +b.money { + font-weight: inherit; + white-space: pre; +} + +#rapport h3 { + text-align: center; + margin-bottom: .5em; +} + +#rapport table { + width: 100%; + border-collapse: collapse; +} + +#rapport tr { + vertical-align: top; +} + +#rapport table table { + border: 1px solid rgb(217, 134, 40); + border-color: rgba(217, 134, 40, 0.5); +} + +#rapport table table tr th { + width: 80%; +} + +#rapport td, #rapport th { + padding: 0.2em 0.5em; + text-align: left; +} + +#rapport .compte th { + font-weight: normal; +} + +#rapport table table td { + text-align: right; +} + +#rapport .parent { + font-weight: bold; + background: rgb(247, 164, 70); + background: rgba(217, 134, 40, 0.2); +} + +#rapport table table tfoot tr { + background: rgb(247, 164, 70); + background: rgba(217, 134, 40, 0.1); + color: rgb(156, 79, 21); +} + +#rapport .exercice { + text-align: center; + margin-bottom: .8em; + padding-bottom: .5em; + border-bottom: 1pt solid #999; +} + +#rapport h1 { + text-align: center; +} + +.icn { + font-family: "gicon", sans-serif; + font-style: normal; + font-weight: normal; + speak: none; + font-variant: normal; + text-transform: none; +} + +.actions .icn, .icn.action { + text-decoration: none; + border-radius: 1em; + display: inline-block; + text-align: center; + font-size: 1.2em; + line-height: .8em; + vertical-align: middle; + padding: .2em; + font-family: "gicon", sans-serif; + color: #9c4f15; + text-shadow: 1px 1px 1px #999; +} + +.num a { + text-decoration: none; + border-radius: .5em; + display: inline-block; + text-align: center; + padding: 0 .2em; + background: rgb(247, 164, 70); + background: rgba(217, 134, 40, 0.5); +} + +.actions .icn:hover, .num a:hover, .icn.action:hover { + color: darkred; + background: #ff9; +} + + +.droits b { + border: 2px solid #999; + border-radius: 1em; + color: #000; + background: #ccc; + width: 16px; + display: inline-block; + text-align: center; + font-size: 0.8em; + cursor: help; + vertical-align: middle; + position: relative; + z-index: 10; + font-family: "gicon", "Trebuchet MS", Arial, Helvetica, sans-serif; +} + +.droits b.aucun { + border-color: #ccc; + background: #eee; + color: #999; +} + +.droits b.acces { + border-color: #cc9; + color: #660; + background: #ffe; +} + +.droits b.ecriture { + border-color: #9c9; + color: #060; + background: #efe; +} + +.droits b.aucun:before { + content: "X"; + position: absolute; + left: 0; + right: 0; + top: -3px; + color: #ccc; + z-index: -1; + font-size: 1.5em; + overflow: hidden; +} + +.droits b.admin { + color: #900; + border-color: #c99; + background: #fee; +} + +.infos { + margin-bottom: 1em; +} + +.infos h3 { + margin-bottom: 0.5em; +} + +.infos p { + margin-bottom: 0.8em; +} + +.infos dl { + margin-bottom: 0.8em; +} + +.infos dl dd { + margin: 0.2em 1em; +} + +.filterCategory { + width: 30em; + float: right; + font-size: 80%; + text-align: center; + margin-left: 1em; +} + +.searchMember { + font-size: 80%; +} + +.searchMember .special { + display: none; +} + +.filterCategory p.submit { + margin-top: -2em; + float: right; +} + +.memberList { + clear: both; +} + +/* WIKI */ + +fieldset.wikiText { + border: none; +} + +fieldset.wikiText textarea, fieldset.skelEdit textarea { + width: 100%; +} + +fieldset.wikiMain, fieldset.wikiRights, fieldset.wikiEncrypt { + float: right; + width: 35%; + margin-left: 3%; + clear: right; +} + +fieldset.wikiMain input[type=text] { + min-width: 0; +} + +#encryptPasswordDisplay { + cursor: help; + background: #ddd; +} + +fieldset.wikiEncrypt .help { + font-size: .9em; +} + +fieldset.wikiMain #f_titre { + width: 90%; + font-size: 14pt; +} + +fieldset.wikiMain #f_uri { + width: 90%; +} + +fieldset.wikiRights dl { + font-size: 10pt; +} + +fieldset.wikiRevision { + clear: both; +} + +fieldset.wikiRevision #f_modification { + width: 90%; +} + +.wikiContent p, .wikiContent h3, .wikiContent h4, .wikiContent h5, .wikiContent h6, +.wikiContent ul, .wikiContent ol, .wikiContent table, .wikiContent blockquote { + margin-bottom: 8pt; +} + +.wikiContent ul, .wikiContent ol, .wikiContent dd { + margin-left: 2em; +} + +.wikiContent ul { + list-style-type: disc; +} + +.wikiContent ol { + list-style-type: decimal; +} + +.toolbar input { + padding: .1em .2em; + border: 1px solid #ccc; + color: #000; + background: #fff; + cursor: pointer; + margin: 0 .5em .5em .5em; + border-bottom: 2px solid #9c4f15; + border-radius: .2em; +} + +.toolbar input:hover { + color: #fff; + background: #d98628; + border-color: transparent; +} + +.toolbar .title { font-size: 1.2em; } +.toolbar .bold { font-weight: bold; } +.toolbar .italic { font-style: italic; } +.toolbar .code { font-family: Courier New, Courier, mono; } +.toolbar .strike { text-decoration: line-through; } +.toolbar .link { color: blue; text-decoration: underline; } + +.wikiFooter { + font-size: 0.8em; + color: #666; + border-top: 0.1em solid #ccc; + clear: both; +} + +.wikiMain samp { + background: #eee; + padding: 0.2em 0.3em; +} + +.wikiChildren { + margin: 1em 0 1em 1em; + border: .1em solid rgba(217, 134, 40, .5); + padding: 1em; + background: rgba(255, 255, 255, 0.5); + float: right; + clear: right; + width: 25%; +} + +.wikiChildren ul { + color: #ccc; + list-style-type: square; + margin-left: 1em; +} + +.wikiTree ul { + margin-left: 1em; + list-style-type: none; +} + +.wikiTree ul ul { + margin-left: 2em; +} + +.wikiTree a { + color: #666; +} + +.wikiTree h3:before { + content: "▸"; + display: inline; + margin-right: .5em; +} + +.wikiTree li { + margin: 0.2em 0; +} + +.wikiTree h3 { + font-size: 1em; + font-weight: normal; +} + +.wikiTree .current > h3 a { + font-weight: bold; + color: #000; +} + +.wikiTree .choice { + text-align: center; + margin-bottom: 1em; +} + +.breadCrumbs { + margin-bottom: .8em; + font-size: .9em; + color: #999; +} + +.breadCrumbs ul, .breadCrumbs li { + list-style-type: none; + display: inline; +} + +.breadCrumbs li:before { + content: "> "; +} + +.breadCrumbs li a { + color: #333; +} + +.wikiSearch { +} + +.wikiSearch fieldset { + padding: .3em; +} + +.wikiSearch input[type=text] { + padding: .3em; +} + +.wikiResults h3 { + font-weight: normal; + margin-bottom: .3em; +} + +.wikiResults p { + margin-bottom: .8em; + font-size: .9em; +} + +.wikiRevisions .length ins { + text-decoration: none; + color: green; +} + +.wikiRevisions .length del { + text-decoration: none; + color: red; +} + +.wikiRevisions .length i { + font-style: normal; + color: gray; +} + +div.wikiRevision { + width: 48%; + margin: 1em 1%; + text-align: center; + float: left; +} + +div.wikiRevision h3 { + font-size: 1em; +} + +div.wikiRevision h4 { + font-weight: normal; + font-size: .9em; +} + +.diff .ins { + background: #cfc; + width: 45%; +} + +.diff .del { + background: #fcc; + width: 45%; +} + +.diff .line { + width: 2%; + padding: 0.2em; + text-align: right; + font-family: Mono; + font-size: 90%; + color: #666; +} + +.diff .leftChange, .diff .rightChange { + text-align: center; + vertical-align: middle; +} + +.diff ins { background: #9f9; } +.diff del { background: #f99; } + +.diff hr { + background: none; + border: none; + border-top: 5px dotted #fff; + color: #fff; + margin: .2em .4em; +} + +.diff .separator { + background: #ccc; +} + +.diff { + border-collapse: collapse; + width: 100%; + font-size: 0.9em; +} + +.diff tr { + border: 1px solid #ccc; + vertical-align: top; +} + +.diff .leftChange b, .diff .rightChange b { + text-shadow: 1px 1px 1px #ccc; + color: #666; +} + +.pagination { + clear: both; + list-style-type: none; + padding: 0.4em 0; + text-align: center; +} + +.pagination li { + display: inline-block; + margin: 0 0.3em; +} + +.pagination li.current { + font-size: 1.3em; +} + +.pagination li a { + color: #000; +} + +fieldset.memberMessage { + max-width: 30em; +} + +fieldset.memberMessage #f_sujet, fieldset.memberMessage #f_message { + width: 95%; +} + +.templatesList ul { + margin: 1em 2em; +} + +.catList dt { + font-size: 1.2em; + font-weight: bold; +} + +.catList dd.desc { + color: #666; + margin: .2em 0 .2em 2em; +} + +.catList dd.compte { + color: #9c4f15; + margin: .2em 0 .2em 2em; +} + +.catList dd.actions { + margin: .2em 0 1em 2em; +} + +ul.accountList { + list-style-type: square; + margin-left: 2em; +} + +ul.accountList > li > h4 { + font-weight: normal; + font-size: 1.2em; +} + +ul.accountList > li { + margin-bottom: .8em; +} + +table.accountList .niveau_2 .libelle { + font-weight: bold; +} + +table.accountList .niveau_3 .libelle { padding-left: 1em; } +table.accountList .niveau_4 .libelle { padding-left: 2em; } +table.accountList .niveau_5 .libelle { padding-left: 3em; } +table.accountList .niveau_6 .libelle { padding-left: 4em; } + +table.rib { display: inline-table; vertical-align: middle; font-size: .9em; text-align: center; border-collapse: collapse; } +table.rib th, table.rib td { padding: .1em .3em; border: 1px solid #ccc; } + +dl.describe { + margin-bottom: 1em; + clear: both; +} + +dl.describe > dt { + font-weight: bold; + width: 15em; + float: left; + clear: left; + margin-bottom: .5em; +} + +dl.describe > dd { + margin-bottom: .5em; + float: left; +} + +dl.describe ul { + margin-left: 1em; +} + +dl.cotisation { + background: rgb(255, 174, 80); + background: rgba(217, 134, 40, 0.2); + padding: .5em; + border-radius: .5em; + margin: 1em; +} + +dl.cotisation dt { + font-weight: bold; +} + +dl.cotisation dd { + margin: .2em 0 .4em 1em; +} + +.infos_asso { + width: 20%; + float: right; + margin: .5em; + border: .1em solid #ccc; + background: #eee; + padding: .5em; +} + +#orderFields fieldset { + position: relative; + min-height: 2em; +} + +#orderFields fieldset legend { + font-size: 1.2em; + line-height: .8em; + color: #666; +} + +#orderFields fieldset .actions { + display: block; + position: absolute; + top: 1em; + right: 1em; +} + +#orderFields fieldset .actions .icn { + position: absolute; +} + +#orderFields fieldset .actions .remove { right: 0em; } +#orderFields fieldset .actions .edit { right: 1.5em; } +#orderFields fieldset .actions .down { right: 3em; } +#orderFields fieldset .actions .up { right: 4.5em; } + +#orderFields fieldset:nth-child(1) .actions .up, #orderFields fieldset:nth-last-child(1) .actions .down { + display: none; +} + +#orderFields fieldset .actions .icn { + cursor: pointer; +} + +#orderFields fieldset .interactive:hover { + cursor: pointer; + text-decoration: underline; +} + +pre.sql_schema { + float: right; + color: #666; + font-size: .9em; + width: 30%; + overflow: auto; +} + +.hidden { + display: none; +} + diff --git a/www/admin/static/bg00.png b/www/admin/static/bg00.png new file mode 100644 index 0000000000000000000000000000000000000000..2026eeff124bfa73020a071796ddee885d01abd7 GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^D}b1hg9%6;wJJIa;uHtDJ29*~C-V}>;VkfoEM{O} z-vGjlSDKoWfP(BLp1!W^w|RJ&j5)Vj$npb)L_A#_LnJOI&v~qW%7DvFVuE226T_b- V#!2Osc0NFP22WQ%mvv4FO#m=~AG-hm literal 0 HcmV?d00001 diff --git a/www/admin/static/bg01.png b/www/admin/static/bg01.png new file mode 100644 index 0000000000000000000000000000000000000000..d4325c192622e31d3364e15c56ca26ee44921d35 GIT binary patch literal 47923 zcmXtGReyLax7 zOp-Hal09?we)qecwRX6Qk_`Gg!gl}wpv!)dR09BnEcgo#84-SkV`$|B{)XV9CL<11 zju9Wgzo0mN(QyF)r0V}(2s7V>Te>*ATqp0{&-moH*J|-@T=irTv%ZHT-ojhny;IH669;KwcnQ==ep|Tz3=> zfA+w+L_WzWxl-Id|15cWpyT74KS$lX7)`z@vx&rN5!_Or4K7B2(Wui1 zl_dk{hUCUchtfuEL&2=*6joBUmBV|2nJ$mbdOUp-G1h`bDRWPytmulSbFXIH({H- z?nyiUcaKg}C>GNQKX}-H;Ahq0C*vr*y{uj@%ghpd-5p0J3(S)di|d6g!a^(aNqrq> zl9-(_VCxeta`|u9=eBS5Y^r5pxw65% zOlRNDV((9d=8sI&FZ`WNu$YC#+PQ=3kJ%*FnH$4L&-Z1GP+bSxWVN(~^pU?KXXSD- zFals*Op=5QNGv(D#dX!PXmYP-9y^DAcmbp4wP#>|XQ^l!jvVW=Xl~X&(3xEt5&6Co z#9Rdpq<;fw?ChAWH|(YoZ3jV2nu6GJV~QnrdRNlOyo04ETHWb?bbrii-_fVuzi8=f z`77e1YJaL(SG_c%G~5oC2wC`hhi&VGd$;gk;yWE_8@oAIs4~QWCASw7c{#8Rr;Ys`)ufTou|PJZ zlHxv~ib^FqF-;$I=S{`8q9=S^5x?;-Bu^ptHEay-GH=Je8&>HVQlVf>#Q~10p74)^w|{fFVV&`HVX+oNqa6>xen~Zrki-puQZX{CYE&i&yfA0?r`rpXuVCm*VG-u9Hwg97g?^zU@k*o1N zOCfLv%=$7z>(yAtS;M#EmHV!QzuRrw346VI8G>UvOj3=bpfxC|@MxpYm91$&mPH~* zS<#6BUz8(0#cSldm7TW@7&$-unQFr))3JuyRE84v&j1brE9<5 zL`Y2l9EnR10Lnx!WA!>|5=;QA4_Td5Qs~Fn-JUR&-WFPbi?B_f_tZ^~{K+b>fA#^SpOg=8J>gVuB3OS`OSscg16Ogxr#9F&x$bQ$Ci`6T)& zK;VHT4xa$(FYErl8AluVr#^DlCr@!SX+lr(iM2g7sizlpZDYTwDhkb$vU+Os)BMn; z<;bI?(v13$T{5djgYX2TNO34$!^+A4pu1`A+15hH&$U9w@T#NzL%{j3WV1uV&QA_+ zY@&~^Ma6!jA|&P;a33t`fV;Jf>}Rf888rt@s%aw4FsqeFw5+Ytaon))ZQajhw!u-G z8FSsNf(Ix7tow5GVj*&Kh1`S>Pc2=U(vT(M>+m9&m76^=&96(E;HG7`jAdUpd6F`C z8RpAAKb2>bXKTo97%6@|=b&pSRH6aB+qv03=p*_Ztu}M~mp{Yl-w@VYu<_0Gm5??4 zMHi~hCJ7;l+mJr#pE8!!<5NPK4B}qPSTK0_#L&LqQoYNuXr>~Ue~LP7 z@i=mgtl;Ht%PO;NBbz<+#ktk(!CMjqlE4@&z zvrQ$uu=2+LfzT(b*SQIOq7FtGOn8o&4`p&`bW_`pb_^599-tMn4M>J>2Fq=P%ww0V zr-S>Lk_II!2y|DwreQ8^E!&i@Gvk7Ac3BP2h$iUd>Q-Uw2crlp|89 zzusb8=P;_8rPhwWpX)GEc@-F{tGL77Lejx?x*U+|#X>K?GV<|JiL(lQw^%a+0?re=SbOIxPUA8TxKMivFkm+FTO0_O4|(M-_W_x&AS!wug+hG=_9q#6d+m(ahAskA zG#5YKY{AhSx==v_O9YB2`m5Gq5G+`*_Zkm#=~<6)$qV#JA{eU(4d**DjqOPSQ!mnQ zDC_%~sBhFs36c%EZ6DO-uqgI7(>t5>Vcp2aD#({4*=A)V)AIP=rPa_)Q;*JSIlOY| zq`oO*d^nLcC+J;UiRcbQ>%W+L7_8^d#oNtPSgHl>1VL~FY%Sa|6g9p07q0{(eGaXq zBG}k#MJl=Val>Z?^qPzkN4$xa9kM=Y*wi!i3-ca=s#8^@CGvxzn()fq2a2q4+hlPO zt)U}&`U+p`WflL#=eqVdz>8!N#Y9pXN7U+Tz3deG-4Pzfy~p+Qu{WP$bMTK-Ays!! zHeez!{f*}>_1f_q($yG2kB7aaQ+?yu>-^KxKZ;pMYcDs8JDThG zJ8~8%n7T^yBA7d^+X1Z~GSN$PP@!ycx`lQQDx#j5#m$SW5aO`gRfAt3ux?`LslzGP zw42>nt$`U^e5dGuZ|s;`>R$ZnuiYpnRsnPKP^xCWagrGRo~PV1z4vbuNv!I9|52|k z@(bzyCeTgv_Hx1`VUvz#atxq55gn>;=uoaQxIn-n=i}7lJW)Hk4hYynPcT;^$ao93 zaAy&x%wy?q7a$qp=#Nq+5|YZ+&WDy@1+vSTp;#>2k(|DztcaNTj_viG&1vhzT5Y4K z^4t2aR)uE9i*P1RW*gvF%$`R|Dt>l??|tjm;t~fGUX(CN_cE4dcip+u2Rff7I+_kO zp#J~38u38Pp@93t=Ad*<{W@2<%c(#7Ay?)YQ7^3SK>hSvQV0xHFi3 z!=Mgg*d7;OzSbI z@ge2m;BvG!*G)V%Z-%vDxKQe5q-9ho1gtnv#KM-#8TRi{n|-+}xXDR0<;}21# z_t2%Px2E)k2*8-^dfbLK0OVTb9{v4iQyrZUf#K-|0LGtL_0N2Qyc-SGzx&<@X=-VM zhn>a=T`~nZ>*|`(N2g0A%@fZSFN67|!OUe0ABV~SaP)g%U6O>f9{3Hq?889lzO`0j z0@pKKw*Po7PN^&zhX{11B0Pq{{DoS}tW zTas*VjXx(S*q?lL(Jv;MrYi`FyWM#%_)t^Eleqy(JvPYN6?ivr@>#F*z3*?r^&P2V z(f637mWrH#JnrU$E5~zDn?zdQ`AovZzX?9w!@CHvn#mTJ`ZqoL(RNXFK-<8P&3~t_ z3eqI|$zkn>9#W$zjA#aunziRW&+svj+fpZhtFAO$s5g#;^m#S%|&V)3=sGY*#xHfeJ-CCYihy(K(I2F4Ay$zyMc4^B70<5;?^qC>7R=D z+@*Ah2l_V(X>0ldFOk=v?LWljuW+tfFx z!3WtTlY|sEkYJ(RMe38P%OWGJV?kDptT`+vf4=ZWdktewFJ-yq+5Ocf({Gs4)%IO> zxiZRq6ALj5@73!B z$xwu+^;h>GY<-ZUyEjw~J{Q}?R>&fjcwx7LP~Lj-!s# zKvF*x_7Ol>nuE#5X@cSshg?C+Zq-u_OS_4Ad0b1aNifY550iJ+c)P@a5w5Uzy2CN- z=^H`x&SC_S3|Z4BQi{|g$KVP3&J#>2RhzyuSD0n`0sT1_VLIt}QSk9k^nQ~?@t;LD z)?H~ze#ui-=-~2a@wqfJIeMRCHO~hNE@C;k^99$=91$|_Kj5U9+9G>4Q#-P-Uun)w zio~6Rq98DeIS3}r$a`{DoqzZq=djNOnc}c%w5{Y7CflyTAHA0#b6RZ5k*;9hnx^Z~ zs}hpB#%2lfB(ICMKd+yz?7>c5AEV5eN9+w<7cnfYye6ZmV2Q4yZnrarON*uO2CCyQ z1Yy)A>4SIWmSYUNbc_U_6oCXYTH!4fy;Y;hZi|n#Vzs;1WZ`Q&d#xKX3;9bWm#j?F zu^&^KWxPMeK&XrJ)-D=B2bmOeUC2gUag6enr|%0DH8PYUvt0CE0r^Q~?>3t}VH7X9 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 literal 0 HcmV?d00001 diff --git a/www/admin/static/code_editor.min.js b/www/admin/static/code_editor.min.js new file mode 100644 index 0000000..2658cad --- /dev/null +++ b/www/admin/static/code_editor.min.js @@ -0,0 +1 @@ +(function(){window.textEditor=function(a){if(!document.getElementById(a))throw new Error("Invalid ID parameter: "+a);this.id=a;this.textarea=document.getElementById(a);this.shortcuts=[];this._key_map={8:'backspace',9:'tab',13:'enter',16:'shift',17:'ctrl',18:'alt',20:'capslock',27:'esc',32:'space',33:'pageup',34:'pagedown',35:'end',36:'home',37:'left',38:'up',39:'right',40:'down',45:'ins',46:'del',91:'meta',93:'meta',224:'meta',106:false,107:false,109:false,110:false,111:false,186:false,187:false,188:false,189:false,190:false,191:false,192:false,219:false,220:false,221:false,222:false};for(var b=1;b<20;++b)this._key_map[111+b]='f'+b;for(b=0;b<=9;++b)this._key_map[b+96]=b;this.preventKeyPress=false;var c=this;this.textarea.addEventListener('keydown',this.keyEvent,true);this.textarea.addEventListener('keypress',this.keyEvent,true);};textEditor.prototype.keyEvent=function(a){var a=a||window.event;if(that.preventKeyPress&&a.type=='keypress'){that.preventKeyPress=false;return that.preventDefault(a);}for(var b in that.shortcuts){var c=that.shortcuts[b];if(a.metaKey)continue;if((a.ctrlKey&&!c.ctrl)||(c.ctrl&&!a.ctrlKey))continue;if((a.shiftKey&&!c.shift)||(c.shift&&!a.shiftKey))continue;if((a.altKey&&!c.alt)||(c.alt&&!a.altKey))continue;if(!(b=that.matchKeyPress(c.key,a)))continue;if(typeof c.callback!='function'){var d=(c.ctrl?'Ctrl-':'')+(c.alt?'Alt-':'');d+=(c.shift?'Shift-':'')+c;throw new Error("Invalid callback type for shortcut "+d);}var e=c.callback.call(that,a,b);if(a.type=='keydown'&&e)that.preventKeyPress=true;return e?that.preventDefault(a):true;}return true;};textEditor.prototype.matchKeyPress=function(a,b){b.key=(typeof b.which==='number'&&b.charCode)?b.which:b.keyCode;a=a.toLowerCase();if(b.type=='keypress'&&b.which)return(a==String.fromCharCode(b.key).toUpperCase())?a:false;else if(this._key_map[b.key])return(this._key_map[b.key]==a)?a:false;else return(String.fromCharCode(b.key).toLowerCase()==a)?a:false;};textEditor.prototype.preventDefault=function(a){if(a.preventDefault)a.preventDefault();if(a.stopPropagation)a.stopPropagation();a.returnValue=false;a.cancelBubble=true;return false;};textEditor.prototype.getSelection=function(){var a=this.textarea;if('selectionStart' in a){var b=a.selectionEnd-a.selectionStart;return{start:a.selectionStart,end:a.selectionEnd,length:b,text:a.value.substr(a.selectionStart,b)};}else if(document.selection){a.focus();var c=document.selection.createRange();var d=a.createTextRange();var e=d.duplicate();e.moveToBookmark(c.getBookmark());d.setEndPoint('EndToStart',e);if(c==null||d==null)return{start:a.value.length,end:a.value.length,length:0,text:''};var f=c.text.replace(/[\r\n]/g,'.');var g=a.value.replace(/[\r\n]/g,'.');var h=g.indexOf(f,d.text.length);return{start:h,end:h+f.length,length:f.length,text:c.text};}else return{start:a.value.length,end:a.value.length,length:0,text:''};};textEditor.prototype.replaceSelection=function(a,b){var c=this.textarea;var d=a.start;var e=d+b.length;c.value=c.value.substr(0,d)+b+c.value.substr(a.end,c.value.length);this.setSelection(d,e);return{start:d,end:e,length:b.length,text:b};};textEditor.prototype.insertAtPosition=function(a,b,c){var d=a+b.length;var e=this.textarea;e.value=e.value.substr(0,a)+b+e.value.substr(a,e.value.length-a);if(!c)c=d;return this.setSelection(c,c);};textEditor.prototype.setSelection=function(a,b){var c=this.textarea;if('selectionStart' in c){c.focus();c.selectionStart=a;c.selectionEnd=b;}else if(document.selection){c.focus();var d=c.createTextRange();var e=a;for(i=0;i'+i+'';this.lineCounter.innerHTML+='---';this.parent.appendChild(this.lineCounter);var b=document.createElement('div');b.className='container';b.appendChild(this.textarea.cloneNode(true));this.parent.appendChild(b);var c=this.textarea.parentNode;c.appendChild(this.parent);c.removeChild(this.textarea);this.textarea=this.parent.getElementsByTagName('textarea')[0];this.textarea.wrap='off';if(this.params.convert_tabs){this.textarea.value=this.textarea.value.replace(/[ ]{1,7}\t/g,' '.repeat(this.params.tab_size));this.textarea.value=this.textarea.value.replace(/\t/g,' '.repeat(this.params.tab_size));}this.textarea.addEventListener('focus',function(){a.update();},false);this.textarea.addEventListener('keyup',function(){a.update();},false);this.textarea.addEventListener('click',function(){a.update();},false);this.textarea.addEventListener('scroll',function(){a.lineCounter.scrollTop=a.textarea.scrollTop;},false);};codeEditor.prototype.update=function(){var a=this.getSelection();var b=this.getLineNumberFromPosition(a);var c=this.countLines();this.search_pos=a.end;if(c!=this.nb_lines){var d=this.lineCounter.getElementsByTagName('b');for(var e=this.nb_lines;e>c;e--)this.lineCounter.removeChild(d[e-1]);var f=this.lineCounter.lastChild;for(var e=d.length;eg.end)?true:false;if((c.length==0||!h)&&c.start!=g.start){this.insertAtPosition(c.start,' '.repeat(this.params.indent_size));return true;}if(c.length==0&&c.start==g.start){var i=(f-1 in e)?e[f-1].match(/^(\s+)/):false;if(!i||g.length!=0)var j=' '.repeat(this.params.indent_size);else var j=' '.repeat(i[1].length);this.insertAtPosition(c.start,j);return true;}var k=this.textarea.value.substr(c.start,(c.end-c.start));var e=k.split("\n");if(d){var l=new RegExp('^[ ]{1,'+this.params.indent_size+'}');for(var m=0;m=this.search_pos?this.search_pos:a.start;var c=this.textarea.value.substr(b);var d=this.getSearchRegexp(this.search_str);var e=c.search(d);if(e==-1)return window.alert(this.params.lang.no_search_result);var f=c.match(d);a.start=b+e;a.end=a.start+f[0].length;a.length=f[0].length;a.text=f[0];this.setSelection(a.start,a.end);this.search_pos=a.end;this.scrollToSelection(a);return true;};codeEditor.prototype.getSearchRegexp=function(a,b){var c,d;if(a.substr(0,1)=='/'){var e=a.lastIndexOf("/");c=a.substr(1,e-1);d=a.substr(e+1).replace(/g/,'');}else{c=a.replace(/([\/$^.?()[\]{}\\])/,'\\$1');d='i';}if(b)d+='g';return new RegExp(c,d);};codeEditor.prototype.searchAndReplace=function(a){var b=this.getSelection();var c=b.length!=0?this.params.lang.search_selection:this.params.lang.search;if(!(s=window.prompt(c,this.search_str))||!(r=window.prompt(that.params.lang.replace)))return true;var d=this.getSearchRegexp(s,true);if(b.length==0){var e=this.textarea.value.match(d).length;this.textarea.value=this.textarea.value.replace(d,r);}else{var e=b.text.match(d).length;this.replaceSelection(b,b.text.replace(d,r));}window.alert(this.params.lang.replace_result.replace(/%d/g,e));return true;};codeEditor.prototype.enter=function(a){var b=this.getSelection();var c=this.getLineNumberFromPosition(b);var d='';c=this.getLine(c);if(this.textarea.value.substr(b.start-1,1)=='{')d+=' '.repeat(this.params.indent_size);if(match=c.match(/^(\s+)/))d+=match[1];if(!d)return false;this.insertAtPosition(b.start,"\n"+d);return true;};codeEditor.prototype.backspace=function(a){var b=this.getSelection();if(b.length>0)return false;var c=this.textarea.value.substr(b.start-2,2);if(c=='""'||c=="''"||c=='{}'||c=='()'||c=='[]'){b.start-=2;this.replaceSelection(b,'');return true;}var c=this.textarea.value.substr(b.start-20,20);if((pos=c.search(/^(\s+)$/m))!=-1){b.start-=this.params.indent_size;this.replaceSelection(b,'');return true;}return false;};codeEditor.prototype.insertBrackets=function(a,b){var c=this.getSelection();var d=b;var e=d;switch(d){case '(':e=')';break;case '[':e=']';break;case '{':e='}';break;}if(c.length==0)this.insertAtPosition(c.start,d+e,c.start+1);else this.wrapSelection(c,d,e);return true;};codeEditor.prototype.toggleFullscreen=function(a){var b=this.parent.className.split(' ');for(var c=0;c 3) ? config.months[date].substring(0, 3) : config.months[date])); + } + + function formatDate(milliseconds) { + var formattedDate = '', + dateObj = new Date(milliseconds), + format = { + d: function() { + var day = format.j(); + return (day < 10) ? '0' + day : day; + }, + D: function() { + return config.weekdays[format.w()].substring(0, 3); + }, + j: function() { + return dateObj.getDate(); + }, + l: function() { + return config.weekdays[format.w()] + 'day'; + }, + S: function() { + return config.suffix[format.j()] || config.defaultSuffix; + }, + w: function() { + return dateObj.getDay(); + }, + F: function() { + return monthToStr(format.n(), true); + }, + m: function() { + var month = format.n() + 1; + return (month < 10) ? '0' + month : month; + }, + M: function() { + return monthToStr(format.n(), false); + }, + n: function() { + return dateObj.getMonth(); + }, + Y: function() { + return dateObj.getFullYear(); + }, + y: function() { + return format.Y().substring(2, 4); + } + }, + formatPieces = config.dateFormat.split(''); + + for(i = 0, x = formatPieces.length; i < x; i++) { + formattedDate += format[formatPieces[i]] ? format[formatPieces[i]]() : formatPieces[i]; + } + + return formattedDate; + } + + function handleMonthClick() { + // if we go too far into the past + if(currentMonthView < 0) { + currentYearView--; + + // start our month count at 11 (11 = december) + currentMonthView = 11; + } + + // if we go too far into the future + if(currentMonthView > 11) { + currentYearView++; + + // restart our month count (0 = january) + currentMonthView = 0; + } + + month.innerHTML = get.month.string(config.fullCurrentMonth) + ' ' + currentYearView; + + // rebuild the calendar + while(body.hasChildNodes()){ + body.removeChild(body.lastChild); + } + body.appendChild(buildCalendar()); + bindDayLinks(); + + return false; + } + + function bindMonthLinks() { + prevMonth.onclick = function() { + currentMonthView--; + return handleMonthClick(); + } + + nextMonth.onclick = function() { + currentMonthView++; + return handleMonthClick(); + } + } + + // our link binding function + function bindDayLinks() { + var days = body.getElementsByTagName('a'); + + for(i = 0, x = days.length; i < x; i++) { + days[i].onclick = function() { + currentDate = new Date(currentYearView, currentMonthView, this.innerHTML); + element.value = formatDate(currentDate.getTime()); + element.onchange(element); + close(); + return false; + } + } + } + + function buildWeekdays() { + var html = document.createDocumentFragment(); + // write out the names of each week day + for(i = 0, x = config.weekdays.length; i < x; i++) { + html.appendChild(build('th', {}, config.weekdays[i].substring(0, 2))); + } + return html; + } + + function buildCalendar() { + // get the first day of the month we are currently viewing + var firstOfMonth = new Date(currentYearView, currentMonthView, config.firstDayOfWeek).getDay(), + // get the total number of days in the month we are currently viewing + numDays = get.month.numDays(), + // declare our day counter + dayCount = 0, + weekCount = 0, + html = document.createDocumentFragment(), + row = build('tr'); + + // print out previous month's "days" + for(i = 1; i <= firstOfMonth; i++) { + row.appendChild(build('td', {}, '')); + dayCount++; + } + + for(i = 1; i <= numDays; i++) { + // if we have reached the end of a week, wrap to the next line + if(dayCount == 7) { + html.appendChild(row); + row = build('tr'); + dayCount = 0; + weekCount++; + } + + // output the text that goes inside each td + // if the day is the current day, add a class of "today" + var today = (i == get.current.day() && currentMonthView == get.current.month.integer() && currentYearView == get.current.year()); + if (today) + { + currentPosition = [weekCount+1, dayCount]; + } + row.appendChild(build('td', { className: today ? 'today' : '' }, build('a', { href: 'javascript:void(0)' }, i))); + dayCount++; + } + + // if we haven't finished at the end of the week, start writing out the "days" for the next month + for(i = 1; i <= (7 - dayCount); i++) { + row.appendChild(build('td', {}, '')); + } + + html.appendChild(row); + + currentMaxRows = weekCount+1; + + return html; + } + + function open() { + document.onmousedown = function(e) { + e = e || window.event; + var target = e.target || e.srcElement; + + var parentNode = target.parentNode; + if(target != element && parentNode != container) { + while(parentNode != container) { + parentNode = parentNode.parentNode; + if(parentNode == null) { + close(); + break; + } + } + } + + if (target == element) + { + close(); + } + + e.preventDefault(); + } + + document.onkeyup = function(e) { + var k = e.keyCode || e.which; + + if (k == 27) + { + close(); + e.preventDefault(); + return false; + } + }; + + document.onkeypress = function(e) { + var k = e.keyCode || e.which; + + if (k == 33) // PgUp + { + e.preventDefault(); + currentMonthView--; + return handleMonthClick(); + } + else if (k == 34) // PgDn + { + e.preventDefault(); + currentMonthView++; + return handleMonthClick(); + } + else if (k >= 37 && k <= 40) // Arrows + { + e.preventDefault(); + var pos = currentPosition.slice(); + if (k == 37) { // left + if (pos[1] == 0) return; + pos[1]--; + } + else if (k == 38) { // up + if (pos[0] <= 1) return; + pos[0]--; + } + else if (k == 39) { // right + if (pos[1] == 6) return; + pos[1]++; + } + else { // down + if (pos[0] == currentMaxRows) return; + pos[0]++; + } + + var table = container.getElementsByTagName('table')[0]; + var row = table.getElementsByTagName('td')[pos[0]*7+pos[1]-7]; + + if (row.innerHTML == "") return; + + table.getElementsByTagName('td')[currentPosition[0]*7+currentPosition[1]-7].className = ''; + row.className = 'today'; + + currentPosition = pos; + currentDate = new Date(currentYearView, currentMonthView, row.firstChild.innerHTML); + } + else if (k == 13 || k == 32) + { + element.value = formatDate(currentDate.getTime()); + element.onchange(element); + close(); + e.preventDefault(); + return false; + } + } + + handleMonthClick(); + container.style.display = 'block'; + } + + function close() { + document.onmousedown = null; + document.onkeypress = null; + container.style.display = 'none'; + } + + function initialise(userConfig) { + if(userConfig) { + for(var key in userConfig) { + if(config.hasOwnProperty(key)) { + config[key] = userConfig[key]; + } + } + } + + if (element.value) + { + var d = element.value.split('/').reverse(); + currentDate = new Date(parseInt(d[0], 10), parseInt(d[1], 10) - 1, parseInt(d[2], 10), 0, 0, 0, 0); + currentYearView = get.current.year(); + currentMonthView = get.current.month.integer(); + } + container = build('div', { className: 'calendar' }); + container.style.cssText = 'display: none; position: absolute; z-index: 9999;'; + + var months = build('div', { className: 'months' }); + prevMonth = build('span', { className: 'prev-month' }, build('a', { href: '#' }, '<')); + nextMonth = build('span', { className: 'next-month' }, build('a', { href: '#' }, '>')); + month = build('span', { className: 'current-month' }, get.month.string(config.fullCurrentMonth) + ' ' + currentYearView); + + months.appendChild(prevMonth); + months.appendChild(nextMonth); + months.appendChild(month); + + var calendar = build('table', {}, build('thead', {}, build('tr', { className: 'weekdays' }, buildWeekdays()))); + body = build('tbody', {}, buildCalendar()); + + calendar.appendChild(body); + + container.appendChild(months); + container.appendChild(calendar); + + element.parentNode.style.position = 'relative'; + element.parentNode.appendChild(container); + + bindMonthLinks(); + + element.onfocus = open; + element.onblur = close; + } + + return (function() { + element = typeof(targetElement) == 'string' ? document.getElementById(targetElement) : targetElement; + initialise(userConfig); + })(); +} + +// Add-on for HTML5 input type="date" fallback + +(function() { + var config_fr = { + fullCurrentMonth: true, + dateFormat: 'd/m/Y', + firstDayOfWeek: 0, + weekdays: ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'], + months: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'], + suffix: { 1: 'er' }, + defaultSuffix: '' + }; + + function dateInputFallback() + { + var inputs = document.getElementsByTagName('input'); + var length = inputs.length; + var enabled = false; + + for (i = 0; i < inputs.length; i++) + { + if (inputs[i].getAttribute('type') == 'date') + { + var new_input = inputs[i].cloneNode(true); + inputs[i].type = 'hidden'; + inputs[i].removeAttribute('pattern'); + inputs[i].removeAttribute('id'); + inputs[i].removeAttribute('required'); + + new_input.removeAttribute('name'); + new_input.setAttribute('type', 'text'); + new_input.className += ' date'; + new_input.size = 10; + new_input.maxlength = 10; + new_input.value = inputs[i].value.split('-').reverse().join('/'); + new_input.setAttribute('pattern', '([012][0-9]|3[01])/(0[0-9]|1[0-2])/[12][0-9]{3}'); + + new_input.onchange = function () + { + if (this.value.match(/\d{2}\/\d{2}\/\d{4}/)) + this.nextSibling.value = this.value.split('/').reverse().join('-'); + else + this.nextSibling.value = this.value; + }; + + inputs[i].parentNode.insertBefore(new_input, inputs[i]); + new datepickr(new_input, config_fr); + } + } + } + + dateInputFallback(); +} () ); diff --git a/www/admin/static/font/garradin.css b/www/admin/static/font/garradin.css new file mode 100644 index 0000000..8a08392 --- /dev/null +++ b/www/admin/static/font/garradin.css @@ -0,0 +1,63 @@ +@charset "UTF-8"; + + @font-face { + font-family: 'garradin'; + src: url('../font/garradin.eot?36341436'); + src: url('../font/garradin.eot?36341436#iefix') format('embedded-opentype'), + url('../font/garradin.woff?36341436') format('woff'), + url('../font/garradin.ttf?36341436') format('truetype'), + url('../font/garradin.svg?36341436#garradin') format('svg'); + font-weight: normal; + font-style: normal; +} +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'garradin'; + src: url('../font/garradin.svg?36341436#garradin') format('svg'); + } +} +*/ + + [class^="icn-"]:before, [class*=" icn-"]:before { + font-family: "garradin"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; +} + +.icn-search:before { content: '🔍'; } /* '\1f50d' */ +.icn-user:before { content: '👤'; } /* '\1f464' */ +.icn-users:before { content: '👪'; } /* '\1f46a' */ +.icn-delete:before { content: '\2718'; } /* '✘' */ +.icn-plus:before { content: '\2795'; } /* '➕' */ +.icn-minus:before { content: '\2796'; } /* '➖' */ +.icn-help:before { content: '\2753'; } /* '❓' */ +.icn-home:before { content: '\2302'; } /* '⌂' */ +.icn-attach:before { content: '📎'; } /* '\1f4ce' */ +.icn-lock:before { content: '🔒'; } /* '\1f512' */ +.icn-mail:before { content: '\2709'; } /* '✉' */ +.icn-download:before { content: '\21d3'; } /* '⇓' */ +.icn-edit:before { content: '\270e'; } /* '✎' */ +.icn-print:before { content: '\2399'; } /* '⎙' */ +.icn-alert:before { content: '\26a0'; } /* '⚠' */ +.icn-menu:before { content: '𝍢'; } /* '\1d362' */ +.icn-settings:before { content: '\2638'; } /* '☸' */ +.icn-down:before { content: '\2193'; } /* '↓' */ +.icn-up:before { content: '\2191'; } /* '↑' */ +.icn-logout:before { content: '\291d'; } /* '⤝' */ +.icn-check:before { content: '\2611'; } /* '☑' */ +.icn-unlock:before { content: '🔓'; } /* '\1f513' */ \ No newline at end of file diff --git a/www/admin/static/font/garradin.eot b/www/admin/static/font/garradin.eot new file mode 100644 index 0000000000000000000000000000000000000000..e9a936b94bdee2079e104f39d68a1f32f1612552 GIT binary patch literal 5656 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 + + +Copyright (C) 2014 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/admin/static/font/garradin.ttf b/www/admin/static/font/garradin.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f8656053f9fb94a56bb8d51d0af8e3723a246a00 GIT binary patch literal 5488 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 Xi-r8c=%j@IJdY?#*m3lTEyaHUFIm1U literal 0 HcmV?d00001 diff --git a/www/admin/static/font/garradin.woff b/www/admin/static/font/garradin.woff new file mode 100644 index 0000000000000000000000000000000000000000..cbfe8e58e7d674effedec0f06a2df1d7e053eed9 GIT binary patch literal 3716 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 bI|hw);0CLBelAS0lPPZx>CMr-F2H{P_h{b) literal 0 HcmV?d00001 diff --git a/www/admin/static/garradin.png b/www/admin/static/garradin.png new file mode 100644 index 0000000000000000000000000000000000000000..c8c3de558900ae6ffa07f9f00c59bb38f5c13134 GIT binary patch literal 32562 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�D$Xos2G?RW4r&MsX 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 literal 0 HcmV?d00001 diff --git a/www/admin/static/gibberish-aes.min.js b/www/admin/static/gibberish-aes.min.js new file mode 100644 index 0000000..ccaa2e5 --- /dev/null +++ b/www/admin/static/gibberish-aes.min.js @@ -0,0 +1,19 @@ +/* + Gibberish-AES + A lightweight Javascript Libray for OpenSSL compatible AES CBC encryption. + + Author: Mark Percival + Email: mark@mpercival.com + Copyright: Mark Percival - http://mpercival.com 2008 + + With thanks to: + Josh Davis - http://www.josh-davis.org/ecmaScrypt + Chris Veness - http://www.movable-type.co.uk/scripts/aes.html + Michel I. Gallant - http://www.jensign.com/ + + License: MIT + + Usage: GibberishAES.enc("secret", "password") + Outputs: AES Encrypted text encoded in Base64 +*/ +var GibberishAES=(function(){var p=14,w=8,g=false,S=function(T){try{return unescape(encodeURIComponent(T))}catch(U){throw"Error on UTF-8 encode"}},P=function(T){try{return decodeURIComponent(escape(T))}catch(U){throw ("Bad Key")}},F=function(V){var W=[],U,T;if(V.length<16){U=16-V.length;W=[U,U,U,U,U,U,U,U,U,U,U,U,U,U,U,U]}for(T=0;T16){throw ("Decryption error: Maybe bad key")}if(W==16){return""}for(U=0;U<16-W;U++){T+=String.fromCharCode(X[U])}}else{for(U=0;U<16;U++){T+=String.fromCharCode(X[U])}}return T},s=function(V){var T="",U;for(U=0;U=12?3:2,Y=[],V=[],T=[],ab=[],U=X.concat(Z),W;T[0]=GibberishAES.Hash.MD5(U);ab=T[0];for(W=1;W=0;X--){T[X]=J(U[X],aa);T[X]=(X===0)?E(T[X],V):E(T[X],U[X-1])}for(X=0;X-1;T--){U=a(U);U=O(U);U=N(U,V,T);if(T>0){U=R(U)}}return U},O=function(W){var V=g?D:Q,T=[],U;for(U=0;U<16;U++){T[U]=V[W[U]]}return T},a=function(W){var T=[],V=g?[0,13,10,7,4,1,14,11,8,5,2,15,12,9,6,3]:[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11],U;for(U=0;U<16;U++){T[U]=W[V[U]]}return T},R=function(U){var T=[],V;if(!g){for(V=0;V<4;V++){T[V*4]=B[U[V*4]]^f[U[1+V*4]]^U[2+V*4]^U[3+V*4];T[1+V*4]=U[V*4]^B[U[1+V*4]]^f[U[2+V*4]]^U[3+V*4];T[2+V*4]=U[V*4]^U[1+V*4]^B[U[2+V*4]]^f[U[3+V*4]];T[3+V*4]=f[U[V*4]]^U[1+V*4]^U[2+V*4]^B[U[3+V*4]]}}else{for(V=0;V<4;V++){T[V*4]=n[U[V*4]]^k[U[1+V*4]]^I[U[2+V*4]]^h[U[3+V*4]];T[1+V*4]=h[U[V*4]]^n[U[1+V*4]]^k[U[2+V*4]]^I[U[3+V*4]];T[2+V*4]=I[U[V*4]]^h[U[1+V*4]]^n[U[2+V*4]]^k[U[3+V*4]];T[3+V*4]=k[U[V*4]]^I[U[1+V*4]]^h[U[2+V*4]]^n[U[3+V*4]]}}return T},N=function(W,X,U){var T=[],V;for(V=0;V<16;V++){T[V]=W[V]^X[U][V]}return T},E=function(W,V){var T=[],U;for(U=0;U<16;U++){T[U]=W[U]^V[U]}return T},K=function(Y){var T=[],U=[],X,Z,W,aa=[],V;for(X=0;X6&&X%w==4){U=y(U)}}for(W=0;W<4;W++){T[X][W]=T[X-w][W]^U[W]}}for(X=0;X<(p+1);X++){aa[X]=[];for(V=0;V<4;V++){aa[X].push(T[X*4+V][0],T[X*4+V][1],T[X*4+V][2],T[X*4+V][3])}}return aa},y=function(T){for(var U=0;U<4;U++){T[U]=Q[T[U]]}return T},x=function(T){var V=T[0],U;for(U=0;U<4;U++){T[U]=T[U+1]}T[3]=V;return T},b=function(V,U){var T=[];for(i=0;i127)?283^(U<<1):(U<<1);T>>>=1}return V},C=function(T){var V=[];for(var U=0;U<256;U++){V[U]=q(T,U)}return V},Q=b("637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b27509832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cfd0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdbe0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9ee1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16",2),D=j(Q),L=b("01020408102040801b366cd8ab4d9a2f5ebc63c697356ad4b37dfaefc591",2),B=C(2),f=C(3),h=C(9),k=C(11),I=C(13),n=C(14),t=function(X,aa,V){var W=u(8),Z=r(o(aa,V),W),ab=Z.key,U=Z.iv,T,Y=[[83,97,108,116,101,100,95,95].concat(W)];X=o(X,V);T=c(X,ab,U);T=Y.concat(T);return M.encode(T)},v=function(V,Y,aa){var U=M.decode(V),X=U.slice(8,16),Z=r(o(Y,aa),X),W=Z.key,T=Z.iv;U=U.slice(16,U.length);V=A(U,W,T,aa);return V},m=function(X){function W(at,ar){return(at<>>(32-ar))}function ac(aw,at){var ay,ar,av,ax,au;av=(aw&2147483648);ax=(at&2147483648);ay=(aw&1073741824);ar=(at&1073741824);au=(aw&1073741823)+(at&1073741823);if(ay&ar){return(au^2147483648^av^ax)}if(ay|ar){if(au&1073741824){return(au^3221225472^av^ax)}else{return(au^1073741824^av^ax)}}else{return(au^av^ax)}}function al(ar,au,at){return(ar&au)|((~ar)&at)}function ak(ar,au,at){return(ar&at)|(au&(~at))}function aj(ar,au,at){return(ar^au^at)}function Y(ar,au,at){return(au^(ar|(~at)))}function ae(au,at,ay,ax,ar,av,aw){au=ac(au,ac(ac(al(at,ay,ax),ar),aw));return ac(W(au,av),at)}function an(au,at,ay,ax,ar,av,aw){au=ac(au,ac(ac(ak(at,ay,ax),ar),aw));return ac(W(au,av),at)}function V(au,at,ay,ax,ar,av,aw){au=ac(au,ac(ac(aj(at,ay,ax),ar),aw));return ac(W(au,av),at)}function ad(au,at,ay,ax,ar,av,aw){au=ac(au,ac(ac(Y(at,ay,ax),ar),aw));return ac(W(au,av),at)}function af(ay){var az,av=ay.length,au=av+8,at=(au-(au%64))/64,ax=(at+1)*16,aA=[],ar=0,aw=0;while(aw>>29;return aA}function T(au){var av,ar,at=[];for(ar=0;ar<=3;ar++){av=(au>>>(ar*8))&255;at=at.concat(av)}return at}var ab=[],ah,ai,U,aa,ag,aq,ap,ao,am,Z=b("67452301efcdab8998badcfe10325476d76aa478e8c7b756242070dbc1bdceeef57c0faf4787c62aa8304613fd469501698098d88b44f7afffff5bb1895cd7be6b901122fd987193a679438e49b40821f61e2562c040b340265e5a51e9b6c7aad62f105d02441453d8a1e681e7d3fbc821e1cde6c33707d6f4d50d87455a14eda9e3e905fcefa3f8676f02d98d2a4c8afffa39428771f6816d9d6122fde5380ca4beea444bdecfa9f6bb4b60bebfbc70289b7ec6eaa127fad4ef308504881d05d9d4d039e6db99e51fa27cf8c4ac5665f4292244432aff97ab9423a7fc93a039655b59c38f0ccc92ffeff47d85845dd16fa87e4ffe2ce6e0a30143144e0811a1f7537e82bd3af2352ad7d2bbeb86d391",8);ab=af(X);aq=Z[0];ap=Z[1];ao=Z[2];am=Z[3];for(ah=0;ah>2];aa+=V[((ac[Z]&3)<<4)|(ac[Z+1]>>4)];if(!(ac[Z+1]===undefined)){aa+=V[((ac[Z+1]&15)<<2)|(ac[Z+2]>>6)]}else{aa+="="}if(!(ac[Z+2]===undefined)){aa+=V[ac[Z+2]&63]}else{aa+="="}}Y=aa.slice(0,64)+"\n";for(Z=1;Z<(Math.ceil(aa.length/64));Z++){Y+=aa.slice(Z*64,Z*64+64)+(Math.ceil(aa.length/64)==Z+1?"":"\n")}return Y},W=function(Y){Y=Y.replace(/\n/g,"");var aa=[],ab=[],X=[],Z;for(Z=0;Z>4);X[1]=((ab[1]&15)<<4)|(ab[2]>>2);X[2]=((ab[2]&3)<<6)|ab[3];aa.push(X[0],X[1],X[2])}aa=aa.slice(0,aa.length-(aa.length%16));return aa};if(typeof Array.indexOf==="function"){T=V}return{encode:U,decode:W}})();return{size:d,h2a:G,expandKey:K,encryptBlock:e,decryptBlock:J,Decrypt:g,s2a:o,rawEncrypt:c,dec:v,openSSLKey:r,a2h:s,enc:t,Hash:{MD5:m},Base64:M}})();if(typeof define==="function"){define(function(){return GibberishAES})}; \ No newline at end of file diff --git a/www/admin/static/global.js b/www/admin/static/global.js new file mode 100644 index 0000000..1e44d4e --- /dev/null +++ b/www/admin/static/global.js @@ -0,0 +1,87 @@ +(function () { + window.$ = function(selector) { + if (!selector.match(/^[.#]?[a-z0-9_-]+$/i)) + { + return document.querySelectorAll(selector); + } + else if (selector.substr(0, 1) == '.') + { + return document.getElementsByClassName(selector.substr(1)); + } + else if (selector.substr(0, 1) == '#') + { + return document.getElementById(selector.substr(1)); + } + else + { + return document.getElementsByTagName(selector); + } + }; + + window.toggleElementVisibility = function(selector, visibility) + { + if (!('classList' in document.documentElement)) + return false; + + if (selector instanceof Array) + { + for (var i = 0; i < selector.length; i++) + { + toggleElementVisibility(selector[i], visibility); + } + + return true; + } + + var elements = $(selector); + + for (var i = 0; i < elements.length; i++) + { + if (!visibility) + elements[i].classList.add('hidden'); + else + elements[i].classList.remove('hidden'); + } + + return true; + }; + + function dateInputFallback() + { + var input = document.createElement('input'); + input.setAttribute('type', 'date'); + input.value = ':-)'; + input.style.position = 'absolute'; + input.style.visibility = 'hidden'; + document.body.appendChild(input); + + // If input type changed or value hasn't been sanitized then + // the input type date element is not supported + if (input.type === 'text' || input.value === ':-)') + { + var www_url = document.body.getAttribute('data-url') + 'static/'; + + var script = document.createElement('script'); + script.type = "text/javascript"; + script.src = www_url + 'datepickr.js'; + document.head.appendChild(script); + + var link = document.createElement('link'); + link.type = 'text/css'; + link.rel = 'stylesheet'; + link.href = www_url + 'datepickr.css'; + document.head.appendChild(link); + } + + document.body.removeChild(input); + } + + if (document.addEventListener) + { + document.addEventListener("DOMContentLoaded", dateInputFallback, false); + } + else + { + document.attachEvent("onDOMContentLoaded", dateInputFallback); + } +})(); \ No newline at end of file diff --git a/www/admin/static/handheld.css b/www/admin/static/handheld.css new file mode 100644 index 0000000..1229829 --- /dev/null +++ b/www/admin/static/handheld.css @@ -0,0 +1,138 @@ +body { + background: #fff url("bg01.png") no-repeat -180px -50px; + font-size: 11pt; +} + +.header h1 { + margin: 0; + text-align: center; + font-size: 1.2em; + margin: .3em 0; +} + +.header .menu { + position: relative; + margin: 0; + width: 100%; + background: none; +} + +.header .menu > li { + margin: .1em 0; +} + +.header .menu a { + font-weight: normal; + padding: 0; + display: inline; + padding: .2em; + color: black; +} + +.header .menu a:hover { + background: none; +} + +.header .menu > li > a { + background: #9c4f15; + color: white; + display: inline-block; + padding: .2em .5em .2em .2em; + border-radius: 0 .5em .5em 0; +} + +.header .menu > li > a:hover { + color: #000; + background: rgb(217, 134, 40); + background: rgba(217, 134, 40, 0.5); +} + +.header .menu a b { + float: left; + font-size: 1.1em; + margin: 0 .3em 0 0; + width: 1.2em; + text-align: center; + color: inherit; +} + +.header .menu a small { + float: none; +} + +.header .menu li li, .header .menu li ul { + display: inline; +} + +.header .menu li li a { + padding: .2em; + font-size: .9em; +} + +.header .menu li.current > a { + background: rgb(217, 134, 40); + background: rgba(217, 134, 40, 0.5); + color: #000; +} + +.header .menu li li.current > a { + border-radius: .5em; + padding: .2em .4em; +} + +.page { + margin: 0; + padding: .1em; +} + +ul.actions { + padding: 0; + border: none; + font-size: .8em; + text-align: center; + margin: .3em 0; +} + +ul.actions li a { + margin: .1em; + border-radius: .5em; +} + +.filterCategory, .searchMember { + width: auto; + float: none; +} + +pre.sql_schema, .wikiChildren, fieldset.wikiMain, fieldset.wikiRights, fieldset.wikiEncrypt { + float: none; + width: auto; +} + +dl.describe dt, dl.describe dd { + float: none; + width: auto; + text-align: center; +} + +/* Petits écrans (smartphones) */ +@media screen and (max-width:600px) { + table.list td, table.list th { + display: inline-block; + border-left: 1px solid #999; + width: auto !important; + } + + colgroup { + /* Hack pour désactiver les largeurs de colonnes */ + display: none; + } + + table.list td:first-child, table.list th:first-child { + border-left: none; + } + + .infos_asso { + float: none; + width: auto; + } +} \ No newline at end of file diff --git a/www/admin/static/loader.js b/www/admin/static/loader.js new file mode 100644 index 0000000..f9b5b60 --- /dev/null +++ b/www/admin/static/loader.js @@ -0,0 +1,48 @@ +(function () { + var points = new Array; + points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAQBAMAAAA/jegKAAAAMFBMVEWcThVVIgBVIgBUIQBWHwBVIgBVIgBVIgBVIgBVIQBVIgBPJABVIgBWIwAAAABVIgCCdzN5AAAAD3RSTlMAlcl4FOT2ptfdvRhENwF6tE0BAAAAY0lEQVQI12NgYDFtdGAAAo///+eC6PX///8ECvD8B4ICBoanIPoPA8N9EP2JgUEeRP8/wKAPor4JMMSD+QsY8sF0AEM/mJ4AVbeA4RqYNmBgBFG/HjBwgySkgfbwVCbJMjAAAAYDSpeMp7/QAAAAAElFTkSuQmCC'); + points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAALBAMAAAC9q6FRAAAAMFBMVEWcThWAMwCAMwCAMwCAMwCBMwCAMwCAMwCAMwCDLACAMwCAMwCANACAMgD/AACAMwAX2X54AAAAD3RSTlMAV83hfTmx++sLk+egJgFrG5vVAAAASklEQVQI12NgmJjckTKBgYFX////74UMDNf/g4ADw3owXcrQD6b/MJwH078Z5MH0ZwYuMP2JgW85iP7IwMAKNOD/AQYgw8k4kQEAgOI2ASlUEn0AAAAASUVORK5CYII='); + points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKBAMAAAB/HNKOAAAAMFBMVEWcThXIcTfIcTfIcTfIcTfIcTfIcTfHcTfIcjbIcTfHcTeqVVXIcTfIczf/AADIcTeCBB0DAAAAD3RSTlMAsOfT2/XGYxKMcwNYKgH3X2xUAAAAPUlEQVQI12PgEPK8wMCw8f//GAaG9f///z/AIA8kvRnsgeR3Bn8g+ZlBFEh+ZbgKZvPo////i4GhPc14HQDfoiJRD3ymmQAAAABJRU5ErkJggg=='); + points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAALBAMAAAC9q6FRAAAAMFBMVEWcThX/mVX/mFX/mFX/mVX/mVT/mFX/m1X/mVX/mVX/mFX/mlT/pFr/mVX/mVX/mVVPZKOjAAAAD3RSTlMA96lr8Ie9JNfcQBgH4vnEmqxYAAAATUlEQVQI12NgYGDX8NRmYGDgCXz3/6MBA8PK/0AgzsAlD6I/Mex9D6J/MsSDqP9fGPLB9DeG+WD6MYM9mFZg2AaiRA8wMCxr9EwuYAAAYbQ00qywMe4AAAAASUVORK5CYII='); + points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALBAMAAABbgmoVAAAAMFBMVEWcThXIcTfIcTfHcTfJcjXIcTfIcTfIbzbHcTfIcTfJcDbHcTfIcTfIcTf/AADIcTd/hisqAAAAD3RSTlMAXvKHDNbtK9LgF5vHyQH5nLrmAAAAS0lEQVQI12NgcA5tfMDAwJj///9xBoZl/////+nAsB9I/Tdg6AdRBxj0QZQCw30QFcBgD6I2MHgCyU8LGNjn//+fyMDAsPhQ9AIGALkqLN+K3VggAAAAAElFTkSuQmC'); + + function getRandomInt (min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + var anim = null; + + window.animatedLoader = function(elm, estimated_time) { + var max = 500; + var nb = 0; + var prev = null; + var i = (estimated_time * 1000) / max; + + anim = window.setInterval(function () { + if (nb++ >= max) + { + window.clearInterval(anim); + } + + if (prev) + { + prev.style.opacity = getRandomInt(25, 100) / 100; + } + + var max_w = Math.min(elm.offsetWidth, elm.offsetWidth * ((nb / max)+0.1)); + var min_w = Math.max(0, max_w - (elm.offsetWidth / 10)); + + var img = document.createElement('img'); + img.src = points[getRandomInt(0, points.length-1)]; + img.alt = ''; + img.style.left = getRandomInt(Math.abs(Math.floor(min_w)), Math.abs(Math.floor(max_w))) + 'px'; + img.style.top = getRandomInt(0, elm.offsetHeight) + 'px'; + elm.appendChild(img); + prev = img; + }, i); + }; + + window.stopAnimatedLoader = function() { + window.clearInterval(anim); + }; +})(); diff --git a/www/admin/static/password.js b/www/admin/static/password.js new file mode 100644 index 0000000..3f7bbe7 --- /dev/null +++ b/www/admin/static/password.js @@ -0,0 +1,170 @@ +(function () { + var strength_elm, match_elm, pw_elm, pw2_elm, suggest_elm; + + RegExp.quote = function(str) { + return (str+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); + }; + + window.initPasswordField = function(suggest, password, password2) + { + suggest_elm = (typeof suggest == 'string') ? document.getElementById(suggest) : suggest; + pw_elm = (typeof password == 'string') ? document.getElementById(password) : password; + pw2_elm = (typeof password2 == 'string') ? document.getElementById(password2) : password2; + + suggest_elm.size = suggest_elm.value.length; + + suggest_elm.onclick = function () { + pw_elm.value = this.value; + pw2_elm.value = this.value; + this.select(); + checkPasswordStrength(); + checkPasswordMatch(); + }; + + strength_elm = document.createElement('span'); + strength_elm.className = 'password_check'; + + pw_elm.parentNode.appendChild(strength_elm); + + match_elm = document.createElement('span'); + match_elm.className = 'password_check'; + + pw2_elm.parentNode.appendChild(match_elm); + + pw_elm.onkeyup = checkPasswordStrength; + pw_elm.onchange = function () { checkPasswordStrength(); checkPasswordMatch(); }; + pw_elm.onblur = function () { checkPasswordStrength(); checkPasswordMatch(); }; + pw2_elm.onkeypress = checkPasswordMatch; + pw2_elm.onblur = checkPasswordMatch; + pw2_elm.onchange = checkPasswordMatch; + + pw_elm.form.addEventListener('submit', function (e) { + if (pw_elm.value == '') return true; + if (scorePassword(pw_elm.value) <= 30 && !window.confirm("Êtes-vous sûr de vouloir utiliser un mot de passe aussi mauvais que ça ?")) + { + e = e || window.event; + if(e.preventDefault) + e.preventDefault(); + if(e.stopPropagation) + e.stopPropagation(); + e.returnValue = false; + e.cancelBubble = true; + return false; + } + }, true); + }; + + function scorePassword(pass) { + var score = 0; + + if (!pass) + return score; + + // Date + if (/19\d\d|200\d|201\d/.test(pass)) + score -= 5; + + // Autres champs du formulaire + var inputs = document.getElementsByTagName('input'); + + for (var i = 0; i < inputs.length; i++) + { + var input = inputs[i]; + + if (input.type != 'text' && input.type != 'url' && input.type != 'email') + continue; + + if (input == suggest_elm) + continue; + + if (input.value.replace(/\s/, '') == '') + continue; + + var v = input.value.split(/[\W]/); + for (var j = 0; j < v.length; j++) + { + if (v[j].length < 4) + continue; + + var r = new RegExp(RegExp.quote(v[j]), 'ig'); + score -= pass.match(r) ? pass.match(r).length * 5 : 0; + } + } + + // award every unique letter until 5 repetitions + var letters = new Object(); + for (var i=0; i 80) + { + strength_elm.className = strength_elm.className.split(' ')[0] + ' ok'; + strength_elm.innerHTML = 'Sécurité : forte'; + } + else if (score > 60) + { + strength_elm.className = strength_elm.className.split(' ')[0] + ' medium'; + strength_elm.innerHTML = 'Sécurité : moyenne'; + } + else if (score >= 30) + { + strength_elm.className = strength_elm.className.split(' ')[0] + ' weak'; + strength_elm.innerHTML = 'Sécurité : mauvaise'; + } + else + { + strength_elm.className = strength_elm.className.split(' ')[0] + ' fail'; + strength_elm.innerHTML = 'Sécurité : aucune'; + } + + return true; + } + + function checkPasswordMatch() + { + if (pw2_elm.value == '' && pw_elm.value == '') + { + match_elm.className = strength_elm.className.split(' ')[0]; + match_elm.innerHTML = ''; + } + else if (pw_elm.value !== pw2_elm.value) + { + match_elm.className = strength_elm.className.split(' ')[0] + ' fail'; + match_elm.innerHTML = 'Ne correspond pas au mot de passe entré.'; + } + else + { + match_elm.className = strength_elm.className.split(' ')[0] + ' ok'; + match_elm.innerHTML = '✓'; + } + } +}()); \ No newline at end of file diff --git a/www/admin/static/print.css b/www/admin/static/print.css new file mode 100644 index 0000000..31b490a --- /dev/null +++ b/www/admin/static/print.css @@ -0,0 +1,58 @@ +@page { + size: A4; + margin: 1cm; +} + +body { + background: #fff; + padding: 0; +} +.header .menu { + display: none; +} +.page { + margin: 0; +} +.header h1 { + margin: 0; + text-align: center; +} + +table.list thead { + background: #000; + color: #fff; +} + +table.list tfoot tr { + background: #666; + color: #fff; +} + +table.list tr { + border: 1px solid #666; +} + +table.list tr:nth-child(even) { + background: #ddd; +} + +table.list.multi tr:nth-child(even) { + background: inherit; +} + +table.list.multi tr:nth-child(4n+1), table.list.multi tr:nth-child(4n+2) { + background: #ddd; +} + +#rapport table table { + border: 1px solid #666; +} + +#rapport .parent { + background: #ccc; +} + +#rapport table table tfoot tr { + background: #666; + color: #fff; +} \ No newline at end of file diff --git a/www/admin/static/skel_editor.css b/www/admin/static/skel_editor.css new file mode 100644 index 0000000..357102c --- /dev/null +++ b/www/admin/static/skel_editor.css @@ -0,0 +1,137 @@ +.codeEditor { + width: 100%; + height: 600px; + border: 1px solid #999; + background: #eee; + position: relative; + display: block; +} + +.codeEditor .sk_help { + background: #ccc; + border-top: 2px solid #999; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 15px; + padding: 5px 1em 0; + font-family: "Deja Vu Sans Mono", "Droid Sans Mono", "Courier New", Courier, monospace; + font-size: 12px; +} + +.codeEditor .sk_toolbar { + background: #ccc; + border-bottom: 2px solid #999; + height: 32px; +} + +.codeEditor .sk_toolbar select { + float: right; + border: none; + border-radius: .2em; + height: 24px; + padding-left: 24px; + margin: 4px .5em; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEUAAACZm4leYFyOkX1RU09MTkp9f2+ys6KwsZrn5+KanIz///+6u6vGxryDhXtVV1Otpz1oAAAAA3RSTlMA+7omiqY6AAAAgklEQVQI12NI+f9fgQEEfO7e+QBm/BGU+P8fJPrd2LgLCD4AGSCBLCDD8Pfu3bu9QCK/Z86e/esDw4/Hv+/euf0ZyEj7PXPnbCDDI+33kZDQkE8M/11D1969e9eEwRNIlpe332f421Ne0dHRcYnh75k7Z8/cudPEoOIS4uriAlQMAwDpN0taA/g97gAAAABJRU5ErkJggg==") no-repeat 5px center; + cursor: pointer; +} + +.codeEditor .sk_toolbar select:hover { + background-color: #fff; +} + +.codeEditor .sk_toolbar p { + display: inline; + padding: .3em .5em; + border-radius: .5em; + font-size: .9em; + margin-left: 2em; +} + +.codeEditor .sk_toolbar input { + margin: 4px .5em; + padding: 0; + width: 24px; + height: 24px; + border: none; + border-radius: .2em; + cursor: pointer; + text-indent: -70em; + overflow: hidden; + background: transparent no-repeat center center; +} + +.codeEditor .sk_toolbar input:hover { background-color: #fff; } + +.codeEditor .sk_toolbar .save { margin-left: 2em; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEU6XIthfJfOXAD8rz6PpsDe3t7+/v6YrsZhe5NVV1Py8vL09PTMzc6+0upyn88gSod8oq0HAAAAAXRSTlOZyTXzhgAAAGxJREFUCNdj4P+kBAT6Hxj+XjYGAtv7DH+fbQOCfSBGKBDkgRi7gQAschQoAmbsXrVq1ToQIw0IQGregQGQ8bCjo6OvDsQ4czS04jmQ8eLMzJiDUMYZkMj3ilAIg/9h6PEjJ4AMhp8zgeD/BwBY4VdD5HZlvAAAAABJRU5ErkJggg=="); } +.codeEditor .sk_toolbar .reset { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAABCklEQVR42p2TsUrDUBSGv4RM2qlLXXwBHdoncG3u5tpZx059gojgZAtC3bqqa7cQKH2FWiFZHUSIg5lChi7Xpak312Ma/eHA5Zzz/5fzHw5UMQZ0wxgDOCb5fnQ8Glxe0AR31zdcPW0mpoDOkoDPJKRI41rywdEpRRrTHeZ4drFIY15nh5w/f4jkea/D2aPafeJKTVuyI4Ut7G0NkVDmnbpxXMAHyJJgl1xPW+XT32emC0SA3z75FugO85Ic8Qf0jR33a/p0lgR6PW1pQJtbiPbNK8GzDLPhNBVAKVUphGGIJD7vdWQBi1SBuSGA/P1FFvgNb8vbH7nFaoO5Ja2Uki6Ommt8ME36t4lfRLtlZDAJ4ScAAAAASUVORK5CYII="); } +.codeEditor .sk_toolbar .search { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAAK/INwWK6QAAADBQTFRF////epWwPGiTdYKPdHqB1tbWWW6Hp6enR2yQc6b/dqncME9tmcz/i5CX5e32w9PmM3gGSwAAAAd0Uk5TAAAAQoTswgXk1s4AAABySURBVAjXYxA0v+4syAAEIqXvQh1BDPN3J88Vgxj3/r07EwtmnDw58y2YcebMHLBI6p/358NADN/3Z75eFhQUZFC5/i4WqIaRwcjtdsrWa0AGEDByr2oAM5i4V82AMBh2nGmCMIBCEAbDrhVghpKS9jYA43soYFw0gPcAAAAASUVORK5CYII="); } +.codeEditor .sk_toolbar .search_replace { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEUAAABnVEEAAACEhoVsaWX4liHmz2/8/Pzf398uNDawrKfn5+fOXADj4+P+/v6IioXFYmG9AAAAAnRSTlMAWv0tddIAAACKSURBVAjXY2Dg/w8EAgwMDP/evSuvWwhi7N69Y8UrATDjxurbC4CMn/P3rv9vwMDw3+/J31s9BxkYOl/vm3vjaCiEsSc0hoGhxe+J7dHUgwxckz0nAwUCGDotO3qPph4zYJg8q+P20Zg+AwbJ9rt3VE60f2BgML+76VgHiPH93bt3z8uBjP9g8AEArPJMCYP5JmIAAAAASUVORK5CYII="); } +.codeEditor .sk_toolbar .gotoline { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEX///9BQUEVFRWysrREREQ4ODhVVVX7+/vu7u7q6uqcnJy+vr/m5ub19fXc3Nz///89lwBDAAAAB3RSTlMAAILOakNmMhUGwgAAAHtJREFUCNdjYIABQbE0EEhkEMx793r37tdmDILZu1fv3rVqEYNgfv3/u/X/vwEZu4/Xl5eXARnbd96trS1hcPNaWdd7924Jg+///7UdHR1XGIK01lSfnDmzhUFQY8W6N2fOABl+q/a8OXfmCYOg3jugwJlJDIJKEMCAAQCfIDck8YzyWAAAAABJRU5ErkJggg=="); } +.codeEditor .sk_toolbar .fullscreen { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEUAAABMTFFOTlBOTlCQ1uMHAAAAA3RSTlMAOcKBmOr4AAAAQUlEQVQI12P4/4D7P8N/B0Yo8XsC23+GZw+4pzM4TmBzYsAGgBKODNcecM9m+D+B7T3DPwfG/Qz/G5iABlzg+g8ANzMax/3kkQoAAAAASUVORK5CYII="); } +.codeEditor.fullscreen .sk_toolbar .fullscreen { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEUAAABNTVFOTlBOTlBLB/faAAAAA3RSTlMAOsPdsomtAAAAQElEQVQI12NIEWCRZNi5gTePIe8D/08G6Q/8Txj0P/B/YVj/gf8fAzawHyQhD1ICJvI/8O9k2FnAl8eQosAiCQCgixb13aKGIwAAAABJRU5ErkJggg=="); } + +.codeEditor .lineCount, .codeEditor textarea { + font-family: "Deja Vu Sans Mono", "Droid Sans Mono", "Courier New", Courier, monospace; + font-size: 11pt; + line-height: 11pt; +} + +.codeEditor .lineCount { + position: absolute; + top: 34px; + left: 0; + bottom: 22px; + width: 46px; + text-align: right; + border-right: 2px solid #999; + overflow: hidden; +} + +.codeEditor .lineCount i { + display: block; + padding-right: 2px; + font-weight: normal; +} + +.codeEditor .lineCount b { + display: block; + padding-right: 2px; + font-weight: normal; +} + +.codeEditor .lineCount b.current { + background: #ccc; +} + +.codeEditor .container { + position: absolute; + right: 4px; + top: 34px; + bottom: 22px; + left: 50px; + margin: 0; + padding: 0; +} + +.codeEditor textarea { + height: 100%; + width: 100%; + padding: 0 0 0 2px; + margin: 0; + background: transparent; + border: none; + overflow: auto; + resize: none; +} + +.codeEditor.fullscreen { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/www/admin/static/skel_editor.js b/www/admin/static/skel_editor.js new file mode 100644 index 0000000..2ad8806 --- /dev/null +++ b/www/admin/static/skel_editor.js @@ -0,0 +1,203 @@ +(function (){ + var www_url = document.body.getAttribute('data-url'); + + var css = document.createElement('link'); + css.type = 'text/css'; + css.rel = 'stylesheet'; + css.href = www_url + 'static/skel_editor.css'; + document.head.appendChild(css); + + var save_btn = document.querySelector('input[name=save]'); + save_btn.type = 'hidden'; + + var code = new codeEditor('f_content'); + + code.params.lang = { + search: "Texte à chercher ?\n(expression régulière autorisée, pour cela commencer par un slash '/')", + replace: "Texte pour le remplacement ?\n(utiliser $1, $2... pour les captures d'expression régulière)", + search_selection: "Texte à chercher dans la sélection ?\n(expression régulière autorisée, pour cela commencer par un slash '/')", + replace_result: "%d occurences trouvées et remplacées.", + goto: "Aller à la ligne :", + no_search_result: "Aucun résultat trouvé." + }; + + code.origValue = code.textarea.value; + code.saved = true; + + code.onlinechange = function () { + if ((p = this.parent.querySelector('nav p')) && this.origValue != code.textarea.value) + { + toolbar.removeChild(p); + } + + var line = this.getLine(this.current_line); + var doc = []; + + if (match = line.match(//)) + { + doc.push({link: 'Boucles', title: 'BOUCLE'}); + doc.push({link: 'Boucle-'+match[2], title: match[2]}); + + if (match[3]) + { + if (match[3].match(/\{".*"\}/)) + doc.push({link: 'Critere-inter', title: 'Critère inter-résultat {"..."}'}); + if (match[3].match(/\{\d+(,\d+)?\}/)) + doc.push({link: 'Critere-de-nombre', title: 'Critère de nombre {X,Y}'}); + if (match[3].match(/\{par\s+.*\}/)) + doc.push({link: 'Critere-d-ordre', title: 'Critère d\'ordre {par champ}'}); + if (match[3].match(/\{inverse\}/)) + doc.push({link: 'Critere-inverse', title: 'Critère {inverse}'}); + } + } + + if (match = line.match(//)) + { + doc.push({link: 'Inclure', title: 'Inclusion du fichier ' + match[1]}); + } + + if (match = line.match(/#[A-Z0-9_]+(\*?(\|.*?)?\).*?\])?/g)) + { + for (var i = 0; i < match.length; i++) + { + var tag = match[i].match(/(#[A-Z0-9_]+)(\*?(\|(.*?))?\).*?\])?/); + doc.push({title: 'Balise ' + tag[1]}); + + if (typeof tag[4] != 'undefined') + { + var tag = tag[4].split('|'); + for (var j = 0; j < tag.length; j++) + { + var end = tag[j].indexOf('{'); + end = (end == -1) ? tag[j].length : end; + var f = tag[j].substr(0, end); + doc.push({link: 'Filtre-'+f, title: 'Filtre '+f}); + } + } + } + } + + help.innerHTML = ''; + + for (var i = 0; i < doc.length; i++) + { + help.innerHTML += ' | '; + + if (doc[i].link) + help.innerHTML += '' + doc[i].title + ''; + else if (doc[i].tag) + help.innerHTML += '<' + tag + '>' + doc[i].title + ''; + else + help.innerHTML += doc[i].title; + } return false; + + }; + + code.saveFile = function (e) + { + if (this.fullscreen) + this.textarea.form.action += '&fullscreen'; + + this.textarea.form.submit(); + }; + + code.loadFile = function (e) + { + var file = e.target.value; + + if (file == skel_current) return; + + if (code.textarea.value != code.origValue && + !window.confirm("Le fichier a été modifié, abandonner les modifications ?")) + { + for (var i = 0; i < e.target.options.length; i++) + { + e.target.options[i].selected = false; + + if (e.target.options[i].value == skel_current) + { + e.target.options[i].selected = true; + } + } + + return false; + } + + var url = www_url + 'config/site.php?edit=' + encodeURIComponent(file); + + window.location.href = url + (code.fullscreen ? '#fullscreen' : ''); + + return true; + }; + + code.resetFile = function (e) + { + if (this.textarea.value == this.origValue) return; + if (!window.confirm("Le fichier a été modifié, abandonner les modifications ?")) return; + this.textarea.form.reset(); + }; + + var help = document.createElement('div'); + help.className = 'sk_help'; + + code.parent.appendChild(help); + + var toolbar = document.createElement('nav'); + toolbar.className = 'sk_toolbar'; + + var appendButton = function (name, title, action) + { + var btn = document.createElement('input'); + btn.type = 'button'; + btn.value = btn.title = title; + btn.className = name; + btn.onclick = function () { action.call(code); return false; }; + + toolbar.appendChild(btn); + }; + + appendButton('save', 'Enregistrer les modifications', code.saveFile); + appendButton('reset', 'Recharger le fichier (effacer les modifications)', code.resetFile); + + appendButton('search', 'Chercher', code.search); + appendButton('search_replace', 'Chercher et remplacer', code.searchAndReplace); + appendButton('gotoline', 'Aller à la ligne', code.goToLine); + appendButton('fullscreen', 'Plein écran', code.toggleFullscreen); + + var sel = document.createElement('select'); + sel.title = 'Charger un autre fichier'; + sel.onchange = code.loadFile; + + for (var i in skel_list) + { + if (!skel_list.hasOwnProperty(i)) + continue; + + var skel = skel_list[i]; + var opt = document.createElement('option'); + opt.value = skel; + opt.innerHTML = skel; + opt.selected = (skel == skel_current) ? true : false; + sel.appendChild(opt); + } + + toolbar.appendChild(sel); + + code.parent.insertBefore(toolbar, code.parent.firstChild); + + if (window.location.hash.match(/fullscreen/)) + { + code.toggleFullscreen(); + + if (msg = document.querySelector('p.error, p.confirm')) + { + var m = document.createElement('p'); + m.innerHTML = msg.innerHTML; + m.className = msg.className; + toolbar.appendChild(m); + msg.parentNode.removeChild(msg); + } + + window.location.hash = ''; + } +}()); diff --git a/www/admin/static/wiki-encryption.js b/www/admin/static/wiki-encryption.js new file mode 100644 index 0000000..e0eb19e --- /dev/null +++ b/www/admin/static/wiki-encryption.js @@ -0,0 +1,201 @@ +(function () { + var aesEnabled = false; + var iteration = 0; + var encryptPassword = null; + var www_url = location.href.replace(/admin\/.*$/, 'admin/'); + + function loadAESlib() + { + if (aesEnabled) + { + return; + } + + var s = document.createElement('script'); + s.type = 'text/javascript'; + s.src = www_url + 'static/gibberish-aes.min.js'; + + document.head.appendChild(s); + aesEnabled = true; + } + + function formatContent(content) + { + // htmlspecialchars ENT_QUOTES + content = content.replace(/&/g, '&').replace(//g, '>') + .replace(/'/g, ''').replace(/"/g, '"'); + + // HTML simple + content = content.replace(/<(\/?(del|pre|ins|b|i|strong|em|h\d|code|samp|tt))>/g, '<$1>'); + console.log(content); + + // Intertitres + content = content.replace(/\{{3}([^\n]*)\}{3}/g, '

    $1

    '); + + // Gras + content = content.replace(/\{{2}([^\n]*)\}{2}/g, '$1'); + + // Italique + content = content.replace(/\{([^\n]*)\}/g, '$1'); + + // Espaces typograhiques + content = content.replace(/\h*([?!;:»])(\s+|$)/g, ' $1$2'); + content = content.replace(/(^|\s+)([«])\h*/g, '$1$2 '); + + // Liens + content = content.replace(/\[([^-]+)->([^\]]+)\]/g, '$1'); + content = content.replace(/\[([^\]]+)\]/g, '$1'); + + // nl2br + content = content.replace(/\r/g, '').replace(/\n/g, '
    '); + + return content; + } + + window.wikiDecrypt = function (edit) + { + loadAESlib(); + + encryptPassword = window.prompt('Mot de passe ?'); + + if (!encryptPassword) + { + encryptPassword = null; + + if (edit) + { + if (window.confirm("Aucun mot de passe entré.\nDésactiver le chiffrement et effacer le contenu ?")) + { + document.getElementById('f_contenu').value = ''; + document.getElementById('f_chiffrement').checked = false; + checkEncryption(document.getElementById('f_chiffrement')); + } + else + { + wikiDecrypt(true); + } + } + + return; + } + + iteration = 0; + decrypt(edit); + }; + + var decrypt = function (edit) + { + if (typeof GibberishAES == 'undefined') + { + if (iteration >= 10) + { + iteration = 0; + encryptPassword = null; + window.alert("Impossible de charger la bibliothèque AES, empêchant le déchiffrement de la page.\nAttendez quelques instants avant de recommencer ou rechargez la page."); + return; + } + + iteration++; + window.setTimeout(decrypt, 500); + return; + } + + var content = document.getElementById(edit ? 'f_contenu' : 'wikiEncryptedContent'); + var wikiContent = !edit ? (content.textContent ? content.textContent : content.innerText) : content.value; + wikiContent = wikiContent.replace(/\s+/g, ''); + + try { + wikiContent = GibberishAES.dec(wikiContent, encryptPassword); + } + catch (e) + { + encryptPassword = null; + window.alert('Impossible de déchiffrer. Mauvais mot de passe ?'); + + if (edit) + { + // Redemander le mot de passe + wikiDecrypt(true); + } + return false; + } + + if (!edit) + { + content.style.display = 'block'; + document.getElementById('wikiEncryptedMessage').style.display = 'none'; + content.innerHTML = formatContent(wikiContent); + } + else + { + content.value = wikiContent; + checkEncryption(document.getElementById('f_chiffrement')); + } + }; + + window.checkEncryption = function(elm) + { + String.prototype.repeat = function(num) + { + return new Array(num + 1).join(this); + }; + + if (elm.checked) + { + if (!encryptPassword) + { + encryptPassword = window.prompt('Mot de passe à utiliser ?'); + } + + if (!encryptPassword) + { + elm.checked = false; + encryptPassword = null; + return; + } + + loadAESlib(); + + var hidden = true; + var d = document.getElementById('encryptPasswordDisplay'); + d.innerHTML = '•'.repeat(encryptPassword.length); + d.title = 'Cliquer pour voir le mot de passe'; + d.onclick = function () { + if (hidden) + { + this.innerHTML = encryptPassword; + this.title = 'Cliquer pour cacher le mot de passe.'; + } + else + { + this.innerHTML = '•'.repeat(encryptPassword.length); + this.title = 'Cliquer pour voir le mot de passe'; + } + hidden = !hidden; + }; + + document.getElementById('f_form').onsubmit = function () + { + if (typeof GibberishAES == 'undefined') + { + alert("Le chargement de la bibliothèque AES n'est pas terminé.\nLe chiffrement est impossible pour le moment, recommencez dans quelques instants ou désactivez le chiffrement."); + return false; + } + + var content = document.getElementById('f_contenu'); + content.value = GibberishAES.enc(content.value, encryptPassword); + content.readOnly = true; + return true; + }; + } + else + { + encryptPassword = null; + var d = document.getElementById('encryptPasswordDisplay'); + d.innerHTML = 'désactivé'; + d.title = 'Chiffrement désactivé'; + d.onclick = null; + document.getElementById('f_form').onsubmit = null; + } + }; +} ()); \ No newline at end of file diff --git a/www/admin/static/wikitoolbar.js b/www/admin/static/wikitoolbar.js new file mode 100644 index 0000000..cb00331 --- /dev/null +++ b/www/admin/static/wikitoolbar.js @@ -0,0 +1,123 @@ +(function () { + // Source: http://stackoverflow.com/questions/401593/textarea-selection + var selection = + { + get: function (e) + { + //Mozilla and DOM 3.0 + if('selectionStart' in e) + { + var l = e.selectionEnd - e.selectionStart; + return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) }; + } + //IE + else if(document.selection) + { + e.focus(); + var r = document.selection.createRange(); + var tr = e.createTextRange(); + var tr2 = tr.duplicate(); + tr2.moveToBookmark(r.getBookmark()); + tr.setEndPoint('EndToStart',tr2); + if (r == null || tr == null) return { start: e.value.length, end: e.value.length, length: 0, text: '' }; + var text_part = r.text.replace(/[\r\n]/g,'.'); //for some reason IE doesn't always count the \n and \r in the length + var text_whole = e.value.replace(/[\r\n]/g,'.'); + var the_start = text_whole.indexOf(text_part,tr.text.length); + return { start: the_start, end: the_start + text_part.length, length: text_part.length, text: r.text }; + } + //Browser not supported + else return { start: e.value.length, end: e.value.length, length: 0, text: '' }; + }, + + replace: function (e, replace_str) + { + var selection = this.get(e); + var start_pos = selection.start; + var end_pos = start_pos + replace_str.length; + e.value = e.value.substr(0, start_pos) + replace_str + e.value.substr(selection.end, e.value.length); + this.set(e,start_pos,end_pos); + return {start: start_pos, end: end_pos, length: replace_str.length, text: replace_str}; + }, + + set: function (e, start_pos,end_pos) + { + //Mozilla and DOM 3.0 + if('selectionStart' in e) + { + e.focus(); + e.selectionStart = start_pos; + e.selectionEnd = end_pos; + } + //IE + else if(document.selection) + { + e.focus(); + var tr = e.createTextRange(); + + //Fix IE from counting the newline characters as two seperate characters + var stop_it = start_pos; + for (i=0; i < stop_it; i++) if( e.value[i].search(/[\r\n]/) != -1 ) start_pos = start_pos - .5; + stop_it = end_pos; + for (i=0; i < stop_it; i++) if( e.value[i].search(/[\r\n]/) != -1 ) end_pos = end_pos - .5; + + tr.moveEnd('textedit',-1); + tr.moveStart('character',start_pos); + tr.moveEnd('character',end_pos - start_pos); + tr.select(); + } + return this.get(e); + }, + + wrap: function (e, left_str, right_str, sel_offset, sel_length) + { + var scroll = e.scrollTop; + var the_sel_text = this.get(e).text; + var selection = this.replace(e, left_str + the_sel_text + right_str ); + if(sel_offset !== undefined && sel_length !== undefined) selection = this.set(e, selection.start + sel_offset, selection.start + sel_offset + sel_length); + else if(the_sel_text == '') selection = this.set(e, selection.start + left_str.length, selection.start + left_str.length); + e.scrollTop = scroll; + return selection; + } + }; + + function launchToolbar() + { + function addBtn(className, label, action) + { + var btn = document.createElement('input'); + btn.type = 'button'; + btn.className = className; + btn.value = label; + btn.onclick = action; + toolbar.appendChild(btn); + } + + var txt = document.getElementById('f_contenu'); + var parent = txt.parentNode.parentNode; + var toolbar = document.createElement('div'); + toolbar.className = "toolbar"; + + addBtn('title', 'Titre', function () { selection.wrap(txt, '{{{', "}}}\n"); } ); + addBtn('italic', 'Italique', function () { selection.wrap(txt, '{', '}'); } ); + addBtn('bold', 'Gras', function () { selection.wrap(txt, '{{', '}}'); } ); + addBtn('strike', 'Barré', function () { selection.wrap(txt, '', ''); } ); + addBtn('code', 'Chasse fixe', function () { selection.wrap(txt, '
    ', '
    '); } ); + addBtn('link', 'Lien', function () { + if (url = window.prompt('Adresse du lien ?')) + { + selection.wrap(txt, '[', '->' + url + ']'); + } + } ); + + parent.insertBefore(toolbar, txt.parentNode); + } + + if (document.addEventListener) + { + document.addEventListener("DOMContentLoaded", launchToolbar, false); + } + else + { + document.attachEvent("onDOMContentLoaded", launchToolbar); + } +} () ); \ No newline at end of file diff --git a/www/admin/upgrade.php b/www/admin/upgrade.php new file mode 100644 index 0000000..94564f6 --- /dev/null +++ b/www/admin/upgrade.php @@ -0,0 +1,224 @@ +getVersion(); + +if (version_compare($v, garradin_version(), '>=')) +{ + throw new UserException("Pas de mise à jour à faire."); +} + +$db = DB::getInstance(); +$redirect = true; + +echo ' + + + + + + + Mise à jour + + +

    Mise à jour de Garradin '.$config->getVersion().' vers la version '.garradin_version().'...

    +
    +
    +'; + +flush(); + +// versions pré-0.3.0 +if (!$v) +{ + $db->exec('ALTER TABLE membres ADD COLUMN lettre_infos INTEGER DEFAULT 0;'); + $v = '0.3.0'; +} + +if (version_compare($v, '0.4.0', '<')) +{ + $config->set('monnaie', '€'); + $config->set('pays', 'FR'); + $config->save(); + + $db->exec(file_get_contents(ROOT . '/include/data/0.4.0.sql')); + + // Mise en place compta + $comptes = new Compta_Comptes; + $comptes->importPlan(); + + $comptes = new Compta_Categories; + $comptes->importCategories(); +} + +if (version_compare($v, '0.4.3', '<')) +{ + $db->exec(file_get_contents(ROOT . '/include/data/0.4.3.sql')); +} + +if (version_compare($v, '0.4.5', '<')) +{ + // Mise à jour plan comptable + $comptes = new Compta_Comptes; + $comptes->importPlan(); + + // Création page wiki connexion + $wiki = new Wiki; + $page = Wiki::transformTitleToURI('Bienvenue'); + $config->set('accueil_connexion', $page); + + if (!$wiki->getByUri($page)) + { + $id_page = $wiki->create([ + 'titre' => 'Bienvenue', + 'uri' => $page, + ]); + + $wiki->editRevision($id_page, 0, [ + 'id_auteur' => null, + 'contenu' => "Bienvenue dans l'administration de ".$config->get('nom_asso')." !\n\n" + . "Utilisez le menu à gauche pour accéder aux différentes rubriques.", + ]); + } + + $config->set('accueil_connexion', $page); + $config->save(); +} + +if (version_compare($v, '0.5.0', '<')) +{ + // Récupération de l'ancienne config + $champs_modifiables_membre = $db->querySingle('SELECT valeur FROM config WHERE cle = "champs_modifiables_membre";'); + $champs_modifiables_membre = !empty($champs_modifiables_membre) ? explode(',', $champs_modifiables_membre) : []; + + $champs_obligatoires = $db->querySingle('SELECT valeur FROM config WHERE cle = "champs_obligatoires";'); + $champs_obligatoires = !empty($champs_obligatoires) ? explode(',', $champs_obligatoires) : []; + + // Import des champs membres par défaut + $champs = Champs_Membres::importInstall(); + + // Application de l'ancienne config aux nouveaux champs membres + foreach ($champs_obligatoires as $name) + { + if ($champs->get($name) !== null) + $champs->set($name, 'mandatory', true); + } + + foreach ($champs_modifiables_membre as $name) + { + if ($champs->get($name) !== null) + $champs->set($name, 'editable', true); + } + + $champs->save(); + + $config->set('champs_membres', $champs); + $config->save(); + + // Suppression de l'ancienne config + $db->exec('DELETE FROM config WHERE cle IN ("champs_obligatoires", "champs_modifiables_membre");'); +} + +if (version_compare($v, '0.6.0-rc1', '<')) +{ + $categories = new Membres_Categories; + $list = $categories->listComplete(); + + $db->exec('PRAGMA foreign_keys = OFF; BEGIN;'); + + // Mise à jour base de données + $db->exec(file_get_contents(ROOT . '/include/data/0.6.0.sql')); + + $id_cat_cotisation = $db->querySingle('SELECT id FROM compta_categories WHERE compte = 756 LIMIT 1;'); + + // Conversion des cotisations de catégories en cotisations indépendantes + foreach ($list as $cat) + { + $db->simpleInsert('cotisations', [ + 'id_categorie_compta' => null, + 'intitule' => $cat['nom'], + 'montant' => (float) $cat['montant_cotisation'], + // Convertir un nombre de mois en nombre de jours + 'duree' => round($cat['duree_cotisation'] * 30.44), + 'description' => 'Créé automatiquement depuis les catégories de membres (version 0.5.x)', + ]); + + $args = [ + 'id_cotisation' => (int)$db->lastInsertRowId(), + 'id_categorie' => (int)$cat['id'], + ]; + + // import des dates de cotisation existantes comme paiements + $db->simpleExec('INSERT INTO cotisations_membres + (id_membre, id_cotisation, date) + SELECT id, :id_cotisation, date(date_cotisation) FROM membres + WHERE date_cotisation IS NOT NULL AND date_cotisation != \'\' AND id_categorie = :id_categorie;', + $args); + + // Mais on ne crée pas d'écriture comptable, car elles existent probablement déjà + } + + // Déplacement des squelettes dans le répertoire public + if (!file_exists(ROOT . '/www/squelettes')) + { + mkdir(ROOT . '/www/squelettes'); + } + + if (file_exists(ROOT . '/squelettes')) + { + $dir = dir(ROOT . '/squelettes'); + + while ($file = $dir->read()) + { + if ($file == '.' || $file == '..') + continue; + + rename(ROOT . '/squelettes/' . $file, ROOT . '/www/squelettes/' . $file); + } + + $dir->close(); + + @rmdir(ROOT . '/squelettes'); + } + + $db->exec('END; PRAGMA foreign_keys = ON;'); + + // Mise à jour de la table membres, suppression du champ date_cotisation notamment + $config->get('champs_membres')->save(); + + // Possibilité de choisir l'identité et l'identifiant d'un membre + $config->set('champ_identite', 'nom'); + $config->set('champ_identifiant', 'email'); + $config->save(); +} + +utils::clearCaches(); + +$config->setVersion(garradin_version()); + +echo '

    Mise à jour terminée.

    +

    Retour

    '; + +if ($redirect) +{ + echo ' + '; +} + +echo ' +'; + +?> \ No newline at end of file diff --git a/www/admin/wiki/_chercher_parent.php b/www/admin/wiki/_chercher_parent.php new file mode 100644 index 0000000..9c260ad --- /dev/null +++ b/www/admin/wiki/_chercher_parent.php @@ -0,0 +1,46 @@ +assign('parent', $parent); +$tpl->assign('list', $wiki->listBackParentTree($parent)); + +function tpl_display_tree($params) +{ + if (isset($params['tree'])) + $tree = $params['tree']; + else + $tree = $params; + + $out = ''; + + return $out; +} + +$tpl->register_function('display_tree', 'Garradin\tpl_display_tree'); + +$tpl->display('admin/wiki/_chercher_parent.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/wiki/_inc.php b/www/admin/wiki/_inc.php new file mode 100644 index 0000000..3057083 --- /dev/null +++ b/www/admin/wiki/_inc.php @@ -0,0 +1,14 @@ +setRestrictionCategorie($user['id_categorie'], $user['droits']['wiki']); + +?> \ No newline at end of file diff --git a/www/admin/wiki/chercher.php b/www/admin/wiki/chercher.php new file mode 100644 index 0000000..3425739 --- /dev/null +++ b/www/admin/wiki/chercher.php @@ -0,0 +1,26 @@ +assign('recherche', $q); + +if (utils::get('q')) +{ + $r = $wiki->search($q); + $tpl->assign('resultats', $r); + $tpl->assign('nb_resultats', count($r)); +} + +function tpl_clean_snippet($str) +{ + return preg_replace('!<(/?b)>!', '<$1>', $str); +} + +$tpl->register_modifier('clean_snippet', 'Garradin\tpl_clean_snippet'); + +$tpl->display('admin/wiki/chercher.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/wiki/creer.php b/www/admin/wiki/creer.php new file mode 100644 index 0000000..a90603e --- /dev/null +++ b/www/admin/wiki/creer.php @@ -0,0 +1,36 @@ +create([ + 'titre' => utils::post('titre'), + 'parent' => $parent, + ]); + + utils::redirect('/admin/wiki/editer.php?id='.$id); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$tpl->assign('error', $error); + +$tpl->display('admin/wiki/creer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/wiki/editer.php b/www/admin/wiki/editer.php new file mode 100644 index 0000000..1851500 --- /dev/null +++ b/www/admin/wiki/editer.php @@ -0,0 +1,92 @@ +getById(utils::get('id')); +$error = false; + +if (!$page) +{ + throw new UserException('Page introuvable.'); +} + +if (!empty($page['contenu'])) +{ + $page['chiffrement'] = $page['contenu']['chiffrement']; + $page['contenu'] = $page['contenu']['contenu']; +} + +if (utils::post('date')) +{ + $date = strtotime(utils::post('date') . ' ' . utils::post('date_h') . ':' . utils::post('date_min')); +} +else +{ + $date = false; +} + +if (!empty($_POST['save'])) +{ + if (!utils::CSRF_check('wiki_edit_'.$page['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + elseif ($page['date_modification'] > (int) utils::post('debut_edition')) + { + $error = 'La page a été modifiée par quelqu\'un d\'autre depuis que vous avez commencé l\'édition.'; + } + else + { + try { + $wiki->edit($page['id'], [ + 'titre' => utils::post('titre'), + 'uri' => utils::post('uri'), + 'parent' => utils::post('parent'), + 'droit_lecture' => utils::post('droit_lecture'), + 'droit_ecriture'=> utils::post('droit_ecriture'), + 'date_creation' => $date, + ]); + + $wiki->editRevision($page['id'], (int) utils::post('revision_edition'), [ + 'contenu' => utils::post('contenu'), + 'modification' => utils::post('modification'), + 'id_auteur' => $user['id'], + 'chiffrement' => utils::post('chiffrement'), + ]); + + $page = $wiki->getById($page['id']); + + utils::redirect('/admin/wiki/?'.$page['uri']); + } + catch (UserException $e) + { + $error = $e->getMessage(); + } + } +} + +$parent = (int) utils::post('parent') ?: (int) $page['parent']; +$tpl->assign('parent', $parent ? $wiki->getTitle($parent) : 0); + +$tpl->assign('error', $error); +$tpl->assign('page', $page); + +$tpl->assign('time', time()); +$tpl->assign('date', $date ? $date : $page['date_creation']); + +$tpl->assign('custom_js', ['wikitoolbar.js', 'wiki-encryption.js']); + +$tpl->display('admin/wiki/editer.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/wiki/historique.php b/www/admin/wiki/historique.php new file mode 100644 index 0000000..2da386c --- /dev/null +++ b/www/admin/wiki/historique.php @@ -0,0 +1,59 @@ +getByID(utils::get('id')); + +if (!$page) +{ + throw new UserException("Cette page n'existe pas."); +} + +if (!$wiki->canReadPage($page['droit_lecture'])) +{ + throw new UserException("Vous n'avez pas le droit de voir cette page."); +} + +if (utils::get('diff')) +{ + $revs = explode('.', utils::get('diff')); + + if (count($revs) != 2) + { + throw new UserException("Erreur de paramètre."); + } + + $rev1 = $wiki->getRevision($page['id'], (int)$revs[0]); + $rev2 = $wiki->getRevision($page['id'], (int)$revs[1]); + + if ($rev1['chiffrement']) + { + $rev1['contenu'] = 'Contenu chiffré'; + } + + if ($rev2['chiffrement']) + { + $rev2['contenu'] = 'Contenu chiffré'; + } + + $tpl->assign('rev1', $rev1); + $tpl->assign('rev2', $rev2); + $tpl->assign('diff', true); +} +else +{ + $tpl->assign('revisions', $wiki->listRevisions($page['id'])); +} + +$tpl->assign('can_edit', $wiki->canWritePage($page['droit_ecriture'])); +$tpl->assign('page', $page); + +$tpl->display('admin/wiki/historique.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/wiki/index.php b/www/admin/wiki/index.php new file mode 100644 index 0000000..b0b906e --- /dev/null +++ b/www/admin/wiki/index.php @@ -0,0 +1,34 @@ +getByURI($_SERVER['QUERY_STRING']); +} +else +{ + $page = $wiki->getByURI($config->get('accueil_wiki')); +} + +if (!$page) +{ + $tpl->assign('uri', $_SERVER['QUERY_STRING']); + $tpl->assign('can_edit', $wiki->canWritePage(Wiki::ECRITURE_NORMAL)); + $tpl->assign('can_read', true); +} +else +{ + $tpl->assign('can_read', $wiki->canReadPage($page['droit_lecture'])); + $tpl->assign('can_edit', $wiki->canWritePage($page['droit_ecriture'])); + $tpl->assign('children', $wiki->getList($page['uri'] == $config->get('accueil_wiki') ? 0 : $page['id'])); + $tpl->assign('breadcrumbs', $wiki->listBackBreadCrumbs($page['id'])); + $tpl->assign('auteur', $membres->getNom($page['contenu']['id_auteur'])); +} + +$tpl->assign('page', $page); + +$tpl->display('admin/wiki/page.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/wiki/recent.php b/www/admin/wiki/recent.php new file mode 100644 index 0000000..32d3cdf --- /dev/null +++ b/www/admin/wiki/recent.php @@ -0,0 +1,16 @@ +assign('page', $page); +$tpl->assign('bypage', Wiki::ITEMS_PER_PAGE); +$tpl->assign('total', $wiki->countRecentModifications()); +$tpl->assign('list', $wiki->listRecentModifications($page)); + +$tpl->display('admin/wiki/recent.tpl'); + +?> \ No newline at end of file diff --git a/www/admin/wiki/supprimer.php b/www/admin/wiki/supprimer.php new file mode 100644 index 0000000..3bb7a48 --- /dev/null +++ b/www/admin/wiki/supprimer.php @@ -0,0 +1,50 @@ +getByID(utils::get('id')); + +if (!$page) +{ + throw new UserException("Cette page n'existe pas."); +} + + +$error = false; + +if (!empty($_POST['delete'])) +{ + if (!utils::CSRF_check('delete_wiki_'.$page['id'])) + { + $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; + } + else + { + if ($wiki->delete($page['id'])) + { + utils::redirect('/admin/wiki/'); + } + else + { + $error = "D'autres pages utilisent cette page comme rubrique parente."; + } + } +} + +$tpl->assign('error', $error); +$tpl->assign('page', $page); + +$tpl->display('admin/wiki/supprimer.tpl'); + +?> \ No newline at end of file diff --git a/www/index.php b/www/index.php new file mode 100644 index 0000000..711e401 --- /dev/null +++ b/www/index.php @@ -0,0 +1,9 @@ +dispatchURI(); + +?> diff --git a/www/squelettes-dist/article.html b/www/squelettes-dist/article.html new file mode 100644 index 0000000..7fdd688 --- /dev/null +++ b/www/squelettes-dist/article.html @@ -0,0 +1,29 @@ + + + +
    + +

    #TITRE

    +
    + [(#TEXTE*|formatter_texte)] +
    + +
    + + +
    + +
    +

    #TITRE

    +

    [(#TEXTE|supprimer_spip|supprimer_tags|couper{200})]

    +
    + +
    +
    +
    +

    + Cette page n'existe pas. +

    + + + \ No newline at end of file diff --git a/www/squelettes-dist/atom.xml b/www/squelettes-dist/atom.xml new file mode 100644 index 0000000..9dfb274 --- /dev/null +++ b/www/squelettes-dist/atom.xml @@ -0,0 +1,32 @@ + + + + [(#NOM_ASSO|echapper_xml)] + + + + + [(#DATE_CREATION|date_atom)] + + + + [(#NOM_ASSO|echapper_xml)] + + + [(#URL_RACINE|echapper_xml)] + Garradin + + + + [(#TITRE|echapper_xml)] + + [(#URL|echapper_xml)] + [(#DATE_CREATION|date_atom)] + [(#NOM_ASSO|echapper_xml)] + + [(#TEXTE*|formatter_texte|echapper_xml)] + + + + + \ No newline at end of file diff --git a/www/squelettes-dist/default.css b/www/squelettes-dist/default.css new file mode 100644 index 0000000..8e0f0ca --- /dev/null +++ b/www/squelettes-dist/default.css @@ -0,0 +1,250 @@ +body, form, p, div, hr, fieldset, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; +} +h1 { font-size: 2em; } +h2 { font-size: 1.5em; } +h3 { font-size: 1.2em; } +h4 { font-size: 1em; } +h5 { font-size: 0.9em; } +h6 { font-size: 0.8em; } +ul, ol { list-style-type: none; } +article, aside, figure, footer, header, hgroup, menu, nav, section { display: block; } + +body { + font-size: 100.01%; + color: #000; + font-family: "Trebuchet MS", Helvetica, Sans-serif; + background: #fff; +} + +header.nav { + background: #ddd; + border-bottom: 1px solid #999; + border-top: .3em solid #666; + text-align: center; + padding-top: .3em; + font-size: 1.1em; +} + +header.nav li { + display: inline-block; + padding: .3em .5em; + margin-bottom: -1px; +} + +header.nav li a { + padding: .3em .5em; + text-transform: uppercase; + color: #666; +} + +header.nav li.current a { + background: #fff; + border: .1em solid #999; + border-bottom: none; + border-top-left-radius: .5em; + border-top-right-radius: .5em; +} + +header.nav li a:hover { + color: #000; +} + +header.main, footer.main, section.page { + max-width: 950px; + margin: 0 auto; +} + +header.main h1 { + color: #9c4f15; + padding: .2em 0 .1em 0; + font-size: 4em; + font-family: Georgia, "Times New Roman", Times, serif; + font-weight: normal; +} + +header.main h1 a { + color: #9c4f15; + text-decoration: none; +} + +header.main h4 { + color: #666; + font-family: Georgia, "Times New Roman", Times, serif; + font-weight: normal; + margin-bottom: 2em; +} + +header.main { + margin-bottom: 1em; + background: url("data:image/png;base64,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") no-repeat top right; +} + +header.main nav { + font-size: 1.2em; + margin: 1em 0; + padding: 0 1em; + background: #ddd; + border-radius: .5em; +} + +header.main nav ul li { + display: inline-block; + margin: -.3em .2em; +} + +header.main nav ul li a { + display: inline-block; + border-radius: 25%; + padding: .5em .5em .4em .5em; + color: #006; + text-decoration: none; + background: #ddd; + border-bottom: .1em solid #ddd; +} + +header.main nav ul li a:hover { + color: #00f; + border-bottom: .1em solid #000; +} + +footer.main { + color: #999; + margin-top: 1em; + text-align: center; +} + +footer.main a { + text-decoration: none; + font-weight: bold; + color: #666; +} + +footer.main a:hover { + color: #006; +} + +footer.main a#garradin { + padding-left: 20px; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAGFBMVEX///+qjnmcTxWgYzWeWyiysrGzs7Ozs7MzVhJAAAAAB3RSTlMAGtv2e9p5oKuvKgAAAH1JREFUCNcdzLEOwjAMBNCrFDoXEGK3zA906RoifwBLmpVUSrMTpf59DNM9nU4HYNhjzJNlUaUPAzddN54NKU5uJsGoL4CYce9vA3mUJiLEHrURiRUoXQLRM6N2F4jrajcXm9YDJ+Xwx5AegX/Addn82eA4tah6QPwYNe75C+4yHrwP6fqUAAAAAElFTkSuQmCC") no-repeat left top; + min-height: 16px; + display: inline-block; +} + +.error { + border-bottom: .2em solid #c00; + border-radius: .5em; + background: #fcc; + padding: .5em; + margin-bottom: 1em; + font-size: 1.2em; + color: #900; +} + +section.articles article { + border-left: .2em solid #ccc; + border-radius: .5em; + padding-left: 1em; +} + +section.articles article h3, section.articles article h1 { + margin-bottom: .3em; +} + +section.articles article h1 a { + color: #000; + text-decoration: none; + font-weight: normal; +} + +section.articles article h3 a { + color: #009; + font-weight: normal; +} + +section.articles article h3 a:visited { + color: #669; +} + +section.articles article h5 { + color: #666; + font-weight: normal; + font-size: .8em; + margin-bottom: .3em; +} + +section.page article { + margin-bottom: 1em; +} + +article h1, article h2, article h3, article h4, article p { + margin-bottom: .8em; +} + +article ul, article ol, article blockquote { + margin-left: 2em; +} + +article ul { + list-style-type: disc; +} + +article ol { + list-style-type: decimal; +} + +article dl dd { + margin: .5em 0 .5em 2em; +} + +article img { + max-width: 100%; +} + +@media handheld, screen and (max-width: 980px) { + body { + padding: 0; + } + + header.nav { + font-size: .9em; + margin: 0; + } + + header.main { + padding: 0 .2em; + background-position: center top; + text-align: center; + } + + header.main h1 { + font-size: 2em; + } + + header.main h4 { + margin-bottom: 1em; + } + + header.main nav { + font-size: 1em; + padding: 0; + } + + section.page { + margin: 0 .3em; + } + + section.page h1 { font-size: 1.5em; } + section.page h2 { font-size: 1.3em; } + section.page h3 { font-size: 1.2em; } + section.page h4 { font-size: 1em; } + section.page h5 { font-size: .9em; } + section.page h6 { font-size: .8em; } + + footer.main { + background: #eee; + padding: .2em; + font-size: .8em; + } +} \ No newline at end of file diff --git a/www/squelettes-dist/entete.html b/www/squelettes-dist/entete.html new file mode 100644 index 0000000..aebe645 --- /dev/null +++ b/www/squelettes-dist/entete.html @@ -0,0 +1,42 @@ + + + + + [(#TITRE) - ]#NOM_ASSO + + + + + + + + + + +
    +

    #NOM_ASSO

    + [

    (#ADRESSE_ASSO)

    ] + + + + +
    + +
    \ No newline at end of file diff --git a/www/squelettes-dist/pied.html b/www/squelettes-dist/pied.html new file mode 100644 index 0000000..cde5e12 --- /dev/null +++ b/www/squelettes-dist/pied.html @@ -0,0 +1,8 @@ +
    + +
    + Propulsé par Garradin — logiciel libre de gestion associative +
    + + + \ No newline at end of file diff --git a/www/squelettes-dist/rubrique.html b/www/squelettes-dist/rubrique.html new file mode 100644 index 0000000..f69d8c5 --- /dev/null +++ b/www/squelettes-dist/rubrique.html @@ -0,0 +1,31 @@ + + + + +
    +

    #TITRE

    +
    + [(#TEXTE*|formatter_texte)] +
    +
    + +
    + +
    +

    #TITRE

    +

    [(#TEXTE|supprimer_spip|supprimer_tags|couper{200})]

    +
    + +
    + + + + + + +

    + Cette page n'existe pas. +

    + + + diff --git a/www/squelettes-dist/sommaire.html b/www/squelettes-dist/sommaire.html new file mode 100644 index 0000000..7facebc --- /dev/null +++ b/www/squelettes-dist/sommaire.html @@ -0,0 +1,25 @@ + + + +
    +
    +

    #TITRE

    +
    Posté : [(#DATE_CREATION|date_intelligente)]
    +

    [(#TEXTE|formatter_texte)]

    +
    +
    + + + +
    + +
    +

    #TITRE

    +
    Posté : [(#DATE_CREATION|date_intelligente)]
    +

    [(#TEXTE|supprimer_spip|supprimer_tags|couper{200})]

    +
    + +
    +
    + + \ No newline at end of file -- 2.20.1