init
authorJulien Moutinho <julm+garradin@autogeree.net>
Thu, 18 Sep 2014 22:04:19 +0000 (00:04 +0200)
committerJulien Moutinho <julm+garradin@autogeree.net>
Thu, 18 Sep 2014 22:04:19 +0000 (00:04 +0200)
325 files changed:
.htaccess [new file with mode: 0644]
COPYING [new file with mode: 0644]
README [new file with mode: 0644]
VERSION [new file with mode: 0644]
config.dist.php [new file with mode: 0644]
cron.php [new file with mode: 0644]
include/class.champs_membres.php [new file with mode: 0644]
include/class.compta_categories.php [new file with mode: 0644]
include/class.compta_comptes.php [new file with mode: 0644]
include/class.compta_comptes_bancaires.php [new file with mode: 0644]
include/class.compta_exercices.php [new file with mode: 0644]
include/class.compta_import.php [new file with mode: 0644]
include/class.compta_journal.php [new file with mode: 0644]
include/class.compta_stats.php [new file with mode: 0644]
include/class.config.php [new file with mode: 0644]
include/class.cotisations.php [new file with mode: 0644]
include/class.cotisations_membres.php [new file with mode: 0644]
include/class.db.php [new file with mode: 0644]
include/class.membres.php [new file with mode: 0644]
include/class.membres_categories.php [new file with mode: 0644]
include/class.membres_import.php [new file with mode: 0644]
include/class.plugin.php [new file with mode: 0644]
include/class.rappels.php [new file with mode: 0644]
include/class.rappels_envoyes.php [new file with mode: 0644]
include/class.sauvegarde.php [new file with mode: 0644]
include/class.squelette.php [new file with mode: 0644]
include/class.wiki.php [new file with mode: 0644]
include/data/0.4.0.sql [new file with mode: 0644]
include/data/0.4.3.sql [new file with mode: 0644]
include/data/0.6.0.sql [new file with mode: 0644]
include/data/categories_comptables.sql [new file with mode: 0644]
include/data/champs_membres.ini [new file with mode: 0644]
include/data/plan_comptable.json [new file with mode: 0644]
include/data/schema.sql [new file with mode: 0644]
include/index.html [new file with mode: 0644]
include/init.php [new file with mode: 0644]
include/lib.squelette_filtres.php [new file with mode: 0644]
include/lib.static_cache.php [new file with mode: 0644]
include/lib.template.php [new file with mode: 0644]
include/lib.utils.php [new file with mode: 0644]
include/libs/countries/countries_en.php [new file with mode: 0644]
include/libs/countries/countries_fr.php [new file with mode: 0644]
include/libs/diff/class.simplediff.php [new file with mode: 0644]
include/libs/garbage2xhtml/lib.garbage2xhtml.php [new file with mode: 0644]
include/libs/miniskel/class.miniskel.php [new file with mode: 0644]
include/libs/passphrase/lib.passphrase.french.php [new file with mode: 0644]
include/libs/svgplot/lib.svgpie.php [new file with mode: 0644]
include/libs/svgplot/lib.svgplot.php [new file with mode: 0644]
include/libs/template_lite/class.compiler.php [new file with mode: 0644]
include/libs/template_lite/class.config.php [new file with mode: 0644]
include/libs/template_lite/class.parser.php [new file with mode: 0644]
include/libs/template_lite/class.template.php [new file with mode: 0644]
include/libs/template_lite/class.tokenparser.php [new file with mode: 0644]
include/libs/template_lite/internal/compile.compile_config.php [new file with mode: 0644]
include/libs/template_lite/internal/compile.compile_custom_block.php [new file with mode: 0644]
include/libs/template_lite/internal/compile.compile_custom_function.php [new file with mode: 0644]
include/libs/template_lite/internal/compile.compile_if.php [new file with mode: 0644]
include/libs/template_lite/internal/compile.generate_compiler_debug_output.php [new file with mode: 0644]
include/libs/template_lite/internal/compile.include.php [new file with mode: 0644]
include/libs/template_lite/internal/compile.parse_is_expr.php [new file with mode: 0644]
include/libs/template_lite/internal/compile.section_start.php [new file with mode: 0644]
include/libs/template_lite/internal/debug.tpl [new file with mode: 0644]
include/libs/template_lite/internal/template.build_dir.php [new file with mode: 0644]
include/libs/template_lite/internal/template.config_loader.php [new file with mode: 0644]
include/libs/template_lite/internal/template.destroy_dir.php [new file with mode: 0644]
include/libs/template_lite/internal/template.fetch_compile_include.php [new file with mode: 0644]
include/libs/template_lite/internal/template.generate_debug_output.php [new file with mode: 0644]
include/libs/template_lite/plugins/block.capture.php [new file with mode: 0644]
include/libs/template_lite/plugins/block.strip.php [new file with mode: 0644]
include/libs/template_lite/plugins/block.textformat.php [new file with mode: 0644]
include/libs/template_lite/plugins/compiler.debug.php [new file with mode: 0644]
include/libs/template_lite/plugins/compiler.tplheader.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.counter.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.cycle.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.db_function_call.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.db_result_call.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_checkboxes.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_hidden.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_image.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_input.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_options.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_radios.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_select_date.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_select_time.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_table.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.html_textbox.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.in_array.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.mailto.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.math.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.popup.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.popup_init.php [new file with mode: 0644]
include/libs/template_lite/plugins/function.resize_image.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.bbcode2html.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.capitalize.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.cat.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.count_characters.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.count_paragraphs.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.count_sentences.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.count_words.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.date.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.date_format.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.debug_print_var.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.default.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.escape.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.indent.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.lower.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.regex_replace.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.replace.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.spacify.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.string_format.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.strip.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.truncate.php [new file with mode: 0644]
include/libs/template_lite/plugins/modifier.upper.php [new file with mode: 0644]
include/libs/template_lite/plugins/outputfilter.gzip.php [new file with mode: 0644]
include/libs/template_lite/plugins/outputfilter.trimwhitespace.php [new file with mode: 0644]
include/libs/template_lite/plugins/postfilter.showtemplatevars.php [new file with mode: 0644]
include/libs/template_lite/plugins/prefilter.jstrip.php [new file with mode: 0644]
include/libs/template_lite/plugins/prefilter.showinfoheader.php [new file with mode: 0644]
include/libs/template_lite/plugins/shared.escape_chars.php [new file with mode: 0644]
include/libs/template_lite/plugins/shared.make_timestamp.php [new file with mode: 0644]
include/libs/template_lite/tests/parser.php [new file with mode: 0644]
include/libs/template_lite/tests/tokenparser.php [new file with mode: 0644]
index.php [new file with mode: 0644]
plugins/index.html [new file with mode: 0644]
templates/admin/_foot.tpl [new file with mode: 0644]
templates/admin/_head.tpl [new file with mode: 0644]
templates/admin/compta/banques/ajouter.tpl [new file with mode: 0644]
templates/admin/compta/banques/index.tpl [new file with mode: 0644]
templates/admin/compta/banques/modifier.tpl [new file with mode: 0644]
templates/admin/compta/banques/supprimer.tpl [new file with mode: 0644]
templates/admin/compta/categories/ajouter.tpl [new file with mode: 0644]
templates/admin/compta/categories/index.tpl [new file with mode: 0644]
templates/admin/compta/categories/modifier.tpl [new file with mode: 0644]
templates/admin/compta/categories/supprimer.tpl [new file with mode: 0644]
templates/admin/compta/comptes/ajouter.tpl [new file with mode: 0644]
templates/admin/compta/comptes/index.tpl [new file with mode: 0644]
templates/admin/compta/comptes/journal.tpl [new file with mode: 0644]
templates/admin/compta/comptes/modifier.tpl [new file with mode: 0644]
templates/admin/compta/comptes/supprimer.tpl [new file with mode: 0644]
templates/admin/compta/exercices/ajouter.tpl [new file with mode: 0644]
templates/admin/compta/exercices/bilan.tpl [new file with mode: 0644]
templates/admin/compta/exercices/cloturer.tpl [new file with mode: 0644]
templates/admin/compta/exercices/compte_resultat.tpl [new file with mode: 0644]
templates/admin/compta/exercices/grand_livre.tpl [new file with mode: 0644]
templates/admin/compta/exercices/index.tpl [new file with mode: 0644]
templates/admin/compta/exercices/journal.tpl [new file with mode: 0644]
templates/admin/compta/exercices/modifier.tpl [new file with mode: 0644]
templates/admin/compta/exercices/supprimer.tpl [new file with mode: 0644]
templates/admin/compta/import.tpl [new file with mode: 0644]
templates/admin/compta/index.tpl [new file with mode: 0644]
templates/admin/compta/operations/index.tpl [new file with mode: 0644]
templates/admin/compta/operations/membre.tpl [new file with mode: 0644]
templates/admin/compta/operations/modifier.tpl [new file with mode: 0644]
templates/admin/compta/operations/recherche_sql.tpl [new file with mode: 0644]
templates/admin/compta/operations/saisir.tpl [new file with mode: 0644]
templates/admin/compta/operations/supprimer.tpl [new file with mode: 0644]
templates/admin/compta/operations/voir.tpl [new file with mode: 0644]
templates/admin/config/_menu.tpl [new file with mode: 0644]
templates/admin/config/donnees.tpl [new file with mode: 0644]
templates/admin/config/import.tpl [new file with mode: 0644]
templates/admin/config/index.tpl [new file with mode: 0644]
templates/admin/config/membres.tpl [new file with mode: 0644]
templates/admin/config/plugins.tpl [new file with mode: 0644]
templates/admin/config/site.tpl [new file with mode: 0644]
templates/admin/index.tpl [new file with mode: 0644]
templates/admin/install.tpl [new file with mode: 0644]
templates/admin/login.tpl [new file with mode: 0644]
templates/admin/membres/action.tpl [new file with mode: 0644]
templates/admin/membres/ajouter.tpl [new file with mode: 0644]
templates/admin/membres/cat_modifier.tpl [new file with mode: 0644]
templates/admin/membres/cat_supprimer.tpl [new file with mode: 0644]
templates/admin/membres/categories.tpl [new file with mode: 0644]
templates/admin/membres/cotisations.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/ajout.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/gestion/modifier.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/gestion/rappel_modifier.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/gestion/rappel_supprimer.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/gestion/rappels.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/gestion/supprimer.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/index.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/rappels.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/supprimer.tpl [new file with mode: 0644]
templates/admin/membres/cotisations/voir.tpl [new file with mode: 0644]
templates/admin/membres/fiche.tpl [new file with mode: 0644]
templates/admin/membres/import.tpl [new file with mode: 0644]
templates/admin/membres/index.tpl [new file with mode: 0644]
templates/admin/membres/message.tpl [new file with mode: 0644]
templates/admin/membres/message_collectif.tpl [new file with mode: 0644]
templates/admin/membres/modifier.tpl [new file with mode: 0644]
templates/admin/membres/recherche.tpl [new file with mode: 0644]
templates/admin/membres/recherche_sql.tpl [new file with mode: 0644]
templates/admin/membres/supprimer.tpl [new file with mode: 0644]
templates/admin/mes_cotisations.tpl [new file with mode: 0644]
templates/admin/mes_infos.tpl [new file with mode: 0644]
templates/admin/password.tpl [new file with mode: 0644]
templates/admin/wiki/_chercher_parent.tpl [new file with mode: 0644]
templates/admin/wiki/chercher.tpl [new file with mode: 0644]
templates/admin/wiki/creer.tpl [new file with mode: 0644]
templates/admin/wiki/editer.tpl [new file with mode: 0644]
templates/admin/wiki/historique.tpl [new file with mode: 0644]
templates/admin/wiki/page.tpl [new file with mode: 0644]
templates/admin/wiki/recent.tpl [new file with mode: 0644]
templates/admin/wiki/supprimer.tpl [new file with mode: 0644]
templates/error.tpl [new file with mode: 0644]
templates/index.html [new file with mode: 0644]
templates/index.tpl [new file with mode: 0644]
www/.htaccess [new file with mode: 0644]
www/_inc.php [new file with mode: 0644]
www/_route.php [new file with mode: 0644]
www/admin/.htaccess [new file with mode: 0644]
www/admin/_inc.php [new file with mode: 0644]
www/admin/compta/_inc.php [new file with mode: 0644]
www/admin/compta/banques/ajouter.php [new file with mode: 0644]
www/admin/compta/banques/index.php [new file with mode: 0644]
www/admin/compta/banques/modifier.php [new file with mode: 0644]
www/admin/compta/banques/supprimer.php [new file with mode: 0644]
www/admin/compta/categories/ajouter.php [new file with mode: 0644]
www/admin/compta/categories/index.php [new file with mode: 0644]
www/admin/compta/categories/modifier.php [new file with mode: 0644]
www/admin/compta/categories/supprimer.php [new file with mode: 0644]
www/admin/compta/comptes/ajouter.php [new file with mode: 0644]
www/admin/compta/comptes/index.php [new file with mode: 0644]
www/admin/compta/comptes/journal.php [new file with mode: 0644]
www/admin/compta/comptes/modifier.php [new file with mode: 0644]
www/admin/compta/comptes/supprimer.php [new file with mode: 0644]
www/admin/compta/exercices/ajouter.php [new file with mode: 0644]
www/admin/compta/exercices/bilan.php [new file with mode: 0644]
www/admin/compta/exercices/cloturer.php [new file with mode: 0644]
www/admin/compta/exercices/compte_resultat.php [new file with mode: 0644]
www/admin/compta/exercices/grand_livre.php [new file with mode: 0644]
www/admin/compta/exercices/index.php [new file with mode: 0644]
www/admin/compta/exercices/journal.php [new file with mode: 0644]
www/admin/compta/exercices/modifier.php [new file with mode: 0644]
www/admin/compta/exercices/supprimer.php [new file with mode: 0644]
www/admin/compta/graph.php [new file with mode: 0644]
www/admin/compta/import.php [new file with mode: 0644]
www/admin/compta/index.php [new file with mode: 0644]
www/admin/compta/operations/index.php [new file with mode: 0644]
www/admin/compta/operations/membre.php [new file with mode: 0644]
www/admin/compta/operations/modifier.php [new file with mode: 0644]
www/admin/compta/operations/recherche_sql.php [new file with mode: 0644]
www/admin/compta/operations/saisir.php [new file with mode: 0644]
www/admin/compta/operations/supprimer.php [new file with mode: 0644]
www/admin/compta/operations/voir.php [new file with mode: 0644]
www/admin/compta/pie.php [new file with mode: 0644]
www/admin/config/_inc.php [new file with mode: 0644]
www/admin/config/donnees.php [new file with mode: 0644]
www/admin/config/import.php [new file with mode: 0644]
www/admin/config/index.php [new file with mode: 0644]
www/admin/config/membres.php [new file with mode: 0644]
www/admin/config/plugins.php [new file with mode: 0644]
www/admin/config/site.php [new file with mode: 0644]
www/admin/index.php [new file with mode: 0644]
www/admin/install.php [new file with mode: 0644]
www/admin/login.php [new file with mode: 0644]
www/admin/logout.php [new file with mode: 0644]
www/admin/membres/action.php [new file with mode: 0644]
www/admin/membres/ajouter.php [new file with mode: 0644]
www/admin/membres/cat_modifier.php [new file with mode: 0644]
www/admin/membres/cat_supprimer.php [new file with mode: 0644]
www/admin/membres/categories.php [new file with mode: 0644]
www/admin/membres/cotisations.php [new file with mode: 0644]
www/admin/membres/cotisations/ajout.php [new file with mode: 0644]
www/admin/membres/cotisations/gestion/modifier.php [new file with mode: 0644]
www/admin/membres/cotisations/gestion/rappel_modifier.php [new file with mode: 0644]
www/admin/membres/cotisations/gestion/rappel_supprimer.php [new file with mode: 0644]
www/admin/membres/cotisations/gestion/rappels.php [new file with mode: 0644]
www/admin/membres/cotisations/gestion/supprimer.php [new file with mode: 0644]
www/admin/membres/cotisations/index.php [new file with mode: 0644]
www/admin/membres/cotisations/rappels.php [new file with mode: 0644]
www/admin/membres/cotisations/supprimer.php [new file with mode: 0644]
www/admin/membres/cotisations/voir.php [new file with mode: 0644]
www/admin/membres/fiche.php [new file with mode: 0644]
www/admin/membres/import.php [new file with mode: 0644]
www/admin/membres/index.php [new file with mode: 0644]
www/admin/membres/message.php [new file with mode: 0644]
www/admin/membres/message_collectif.php [new file with mode: 0644]
www/admin/membres/modifier.php [new file with mode: 0644]
www/admin/membres/recherche.php [new file with mode: 0644]
www/admin/membres/recherche_sql.php [new file with mode: 0644]
www/admin/membres/supprimer.php [new file with mode: 0644]
www/admin/mes_cotisations.php [new file with mode: 0644]
www/admin/mes_infos.php [new file with mode: 0644]
www/admin/password.php [new file with mode: 0644]
www/admin/plugin.php [new file with mode: 0644]
www/admin/static/admin.css [new file with mode: 0644]
www/admin/static/bg00.png [new file with mode: 0644]
www/admin/static/bg01.png [new file with mode: 0644]
www/admin/static/code_editor.min.js [new file with mode: 0644]
www/admin/static/datepickr.css [new file with mode: 0644]
www/admin/static/datepickr.js [new file with mode: 0644]
www/admin/static/font/garradin.css [new file with mode: 0644]
www/admin/static/font/garradin.eot [new file with mode: 0644]
www/admin/static/font/garradin.svg [new file with mode: 0644]
www/admin/static/font/garradin.ttf [new file with mode: 0644]
www/admin/static/font/garradin.woff [new file with mode: 0644]
www/admin/static/garradin.png [new file with mode: 0644]
www/admin/static/gibberish-aes.min.js [new file with mode: 0644]
www/admin/static/global.js [new file with mode: 0644]
www/admin/static/handheld.css [new file with mode: 0644]
www/admin/static/loader.js [new file with mode: 0644]
www/admin/static/password.js [new file with mode: 0644]
www/admin/static/print.css [new file with mode: 0644]
www/admin/static/skel_editor.css [new file with mode: 0644]
www/admin/static/skel_editor.js [new file with mode: 0644]
www/admin/static/wiki-encryption.js [new file with mode: 0644]
www/admin/static/wikitoolbar.js [new file with mode: 0644]
www/admin/upgrade.php [new file with mode: 0644]
www/admin/wiki/_chercher_parent.php [new file with mode: 0644]
www/admin/wiki/_inc.php [new file with mode: 0644]
www/admin/wiki/chercher.php [new file with mode: 0644]
www/admin/wiki/creer.php [new file with mode: 0644]
www/admin/wiki/editer.php [new file with mode: 0644]
www/admin/wiki/historique.php [new file with mode: 0644]
www/admin/wiki/index.php [new file with mode: 0644]
www/admin/wiki/recent.php [new file with mode: 0644]
www/admin/wiki/supprimer.php [new file with mode: 0644]
www/index.php [new file with mode: 0644]
www/squelettes-dist/article.html [new file with mode: 0644]
www/squelettes-dist/atom.xml [new file with mode: 0644]
www/squelettes-dist/default.css [new file with mode: 0644]
www/squelettes-dist/entete.html [new file with mode: 0644]
www/squelettes-dist/pied.html [new file with mode: 0644]
www/squelettes-dist/rubrique.html [new file with mode: 0644]
www/squelettes-dist/sommaire.html [new file with mode: 0644]

diff --git a/.htaccess b/.htaccess
new file mode 100644 (file)
index 0000000..395cdf0
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,7 @@
+<IfModule mod_alias.c>
+    RedirectMatch 403 /include/
+    RedirectMatch 403 /cache/
+    RedirectMatch 403 /plugins/
+    RedirectMatch 403 /templates/
+    RedirectMatch 403 /*.sqlite
+</IfModule>
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
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. <http://fsf.org/>
+ 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.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 <http://www.gnu.org/licenses/>.
+
+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
+<http://www.gnu.org/licenses/>.
diff --git a/README b/README
new file mode 100644 (file)
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 (file)
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 (file)
index 0000000..15ef961
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * Ce fichier représente un exemple des constantes de configuration
+ * disponibles pour Garradin.
+ */
+
+// Nécessaire pour situer les constantes dans le bon namespace
+namespace Garradin;
+
+// Connexion automatique à l'administration avec l'adresse e-mail donnée
+#const LOCAL_LOGIN = 'president@association.net';
+
+// Connexion automatique avec le numéro de membre indiqué
+// Défaut : false (connexion automatique désactivée)
+const LOCAL_LOGIN = 1;
+
+// Répertoire où est le code source de Garradin
+const ROOT = '/usr/share/garradin';
+
+// Répertoire où sont situées les données de Garradin
+// (incluant la base de données SQLite, le cache et les fichiers locaux)
+// Défaut : le même répertoire que le source
+const DATA_ROOT = '/var/www/garradin';
+
+// Emplacement de la base de données
+const DB_FILE = '/var/lib/sqlite/garradin.sqlite';
+
+// Adresse URI de la racine du site Garradin
+// (doit se terminer par un slash)
+// Défaut : découverte automatique à partir de SCRIPT_NAME
+const WWW_URI = '/garradin/';
+
+// Adresse URL HTTP(S) de Garradin
+// Défaut : découverte à partir de HTTP_HOST ou SERVER_NAME + WWW_URI
+define('Garradin\WWW_URL', 'http://garradin.net' . WWW_URI);
+
+// Emplacement de stockage des plugins
+define('Garradin\PLUGINS_ROOT', DATA_ROOT . '/plugins');
+
+// Plugins fixes qui ne peuvent être désinstallés (séparés par une virgule)
+const PLUGINS_SYSTEM = 'email,web';
+
+// Affichage des erreurs
+// Si "true" alors un message expliquant l'erreur et comment rapporter le bug s'affiche
+// en cas d'erreur. Sinon rien ne sera affiché.
+// Défaut : true
+const SHOW_ERRORS = true;
+
+// Envoi des erreurs par e-mail
+// Si rempli, un email sera envoyé à l'adresse indiquée à chaque fois qu'une erreur
+// d'exécution sera rencontrée.
+// Si "false" alors aucun email ne sera envoyé
+// Note : les erreurs sont déjà toutes loguées dans error.log à la racine de DATA_ROOT
+const MAIL_ERRORS = false;
+
+// Utilisation de cron pour les tâches automatiques
+// Si "true" on s'attend à ce qu'une tâche automatisée appelle
+// le script cron.php à la racine toutes les 24 heures. Sinon Garradin
+// effectuera les actions automatiques quand quelqu'un se connecte à 
+// l'administration ou visite le site.
+// Défaut : false
+const USE_CRON = false;
diff --git a/cron.php b/cron.php
new file mode 100644 (file)
index 0000000..c57f9d3
--- /dev/null
+++ b/cron.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Garradin;
+
+require_once __DIR__ . '/include/init.php';
+
+// Exécution des tâches automatiques
+
+if ($config->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 (file)
index 0000000..9c07ae5
--- /dev/null
@@ -0,0 +1,470 @@
+<?php
+
+namespace Garradin;
+
+class Champs_Membres
+{
+       protected $champs = null;
+
+       protected $types = [
+               'email'         =>      'Adresse E-Mail',
+               'url'           =>      'Adresse URL',
+               'checkbox'      =>      'Case à cocher',
+               'date'          =>      'Date',
+               'datetime'      =>      'Date et heure',
+               //'file'                =>      'Fichier',
+        'password'  =>  'Mot de passe',
+               'number'        =>      'Numéro',
+               'tel'           =>      'Numéro de téléphone',
+               'select'        =>      'Sélecteur à choix unique',
+        'multiple'  =>  'Sélecteur à choix multiple',
+               'country'       =>      'Sélecteur de pays',
+               'text'          =>      'Texte',
+               '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 (file)
index 0000000..0798be2
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+
+namespace Garradin;
+
+class Compta_Categories
+{
+    const DEPENSES = -1;
+    const RECETTES = 1;
+    const AUTRES = 0;
+
+    public function importCategories()
+    {
+        $db = DB::getInstance();
+        $db->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 (file)
index 0000000..02021a2
--- /dev/null
@@ -0,0 +1,325 @@
+<?php
+
+namespace Garradin;
+
+class Compta_Comptes
+{
+    const CAISSE = 530;
+
+    const PASSIF = 0x01;
+    const ACTIF = 0x02;
+    const PRODUIT = 0x04;
+    const CHARGE = 0x08;
+
+    public function importPlan()
+    {
+        $plan = json_decode(file_get_contents(ROOT . '/include/data/plan_comptable.json'), true);
+
+        $db = DB::getInstance();
+        $db->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 (file)
index 0000000..1ac1213
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+
+namespace Garradin;
+
+class Compta_Comptes_Bancaires extends Compta_Comptes
+{
+    const NUMERO_PARENT_COMPTES = 512;
+
+    public function add($data)
+    {
+        $db = DB::getInstance();
+
+        $data['parent'] = self::NUMERO_PARENT_COMPTES;
+        $data['id'] = null;
+
+        $this->_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 (file)
index 0000000..02edb14
--- /dev/null
@@ -0,0 +1,569 @@
+<?php
+
+namespace Garradin;
+
+class Compta_Exercices
+{
+    public function add($data)
+    {
+        $this->_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 (file)
index 0000000..73fd99b
--- /dev/null
@@ -0,0 +1,387 @@
+<?php
+
+namespace Garradin;
+
+class Compta_Import
+{
+       protected $csv_header = [
+               'Numéro mouvement',
+               'Date',
+               'Type de mouvement',
+               'Catégorie',
+               'Libellé',
+               'Montant',
+               'Compte de débit - numéro',
+               'Compte de débit - libellé',
+               'Compte de crédit - numéro',
+               'Compte de crédit - libellé',
+               'Moyen de paiement',
+               'Numéro de chèque',
+               'Numéro de pièce',
+               'Remarques'
+       ];
+
+       public function toCSV($exercice)
+       {
+               $db = DB::getInstance();
+
+               $res = $db->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 (file)
index 0000000..9ff8c95
--- /dev/null
@@ -0,0 +1,363 @@
+<?php
+
+namespace Garradin;
+
+class Compta_Journal
+{
+    protected function _getCurrentExercice()
+    {
+        $db = DB::getInstance();
+        $id = $db->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 (file)
index 0000000..335fbe0
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+
+namespace Garradin;
+
+class Compta_Stats
+{
+       protected function _parRepartitionCategorie($type)
+       {
+               $db = DB::getInstance();
+               return $db->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 (file)
index 0000000..6ee1bd3
--- /dev/null
@@ -0,0 +1,326 @@
+<?php
+
+namespace Garradin;
+
+class Config
+{
+    protected $fields_types = null;
+    protected $config = null;
+    protected $modified = [];
+
+    static protected $_instance = null;
+
+    static public function getInstance()
+    {
+        return self::$_instance ?: self::$_instance = new Config;
+    }
+
+    private function __clone()
+    {
+    }
+
+    protected function __construct()
+    {
+        // Définition des types de données stockées
+        $string = '';
+        $int = 0;
+        $float = 0.0;
+        $array = [];
+        $bool = false;
+        $object = new \stdClass;
+
+        $this->fields_types = [
+            'nom_asso'              =>  $string,
+            'adresse_asso'          =>  $string,
+            'email_asso'            =>  $string,
+            'site_asso'             =>  $string,
+
+            'monnaie'               =>  $string,
+            'pays'                  =>  $string,
+
+            'champs_membres'        =>  $object,
+
+            '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 '<div style="color: red; background: #fff;">Il y a des champs modifiés non sauvés dans '.__CLASS__.' !</div>';
+        }
+    }
+
+    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 (file)
index 0000000..9490784
--- /dev/null
@@ -0,0 +1,170 @@
+<?php
+
+namespace Garradin;
+
+class Cotisations
+{
+       /**
+        * Vérification des champs fournis pour la modification de donnée
+        * @param  array $data Tableau contenant les champs à ajouter/modifier
+        * @return void
+        */
+       protected function _checkFields(&$data)
+       {
+               $db = DB::getInstance();
+
+               if (!isset($data['intitule']) || trim($data['intitule']) == '')
+               {
+                       throw new UserException('L\'intitulé ne peut rester vide.');
+               }
+
+               $data['intitule'] = trim($data['intitule']);
+
+               if (isset($data['description']))
+               {
+                       $data['description'] = trim($data['description']);
+               }
+
+               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 (isset($data['duree']))
+               {
+                       $data['duree'] = (int) $data['duree'];
+
+                       if ($data['duree'] < 0)
+                       {
+                               $data['duree'] = 0;
+                       }
+               }
+
+               if (isset($data['debut']) && trim($data['debut']) != '')
+               {
+                       if (!empty($data['duree']))
+                       {
+                               throw new UserException('Il n\'est pas possible de spécifier une durée ET une date fixe, merci de choisir l\'une des deux options.');
+                       }
+
+                       if (!isset($data['fin']) || trim($data['fin']) == '')
+                       {
+                               throw new UserException('Une date de fin est obligatoire avec la date de début de validité.');
+                       }
+
+                       if (!utils::checkDate($data['debut']))
+                       {
+                               throw new UserException('La date de début est invalide.');
+                       }
+
+                       if (!utils::checkDate($data['fin']))
+                       {
+                               throw new UserException('La date de fin est invalide.');
+                       }
+               }
+
+               if (isset($data['id_categorie_compta']))
+               {
+                       if ($data['id_categorie_compta'] != 0 && !$db->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 (file)
index 0000000..a895ee2
--- /dev/null
@@ -0,0 +1,336 @@
+<?php
+
+namespace Garradin;
+
+class Cotisations_Membres
+{
+       const ITEMS_PER_PAGE = 100;
+
+       /**
+        * Vérification des champs fournis pour la modification de donnée
+        * @param  array $data Tableau contenant les champs à ajouter/modifier
+        * @return void
+        */
+       protected function _checkFields(&$data, $compta = false)
+       {
+               $db = DB::getInstance();
+
+        if (empty($data['date']) || !utils::checkDate($data['date']))
+        {
+            throw new UserException('Date vide ou invalide.');
+        }
+
+               if (empty($data['id_cotisation']) 
+                       || !$db->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 (file)
index 0000000..c560c59
--- /dev/null
@@ -0,0 +1,411 @@
+<?php
+
+namespace Garradin;
+
+function str_replace_first ($search, $replace, $subject)
+{
+    $pos = strpos($subject, $search);
+
+    if ($pos !== false)
+    {
+        $subject = substr_replace($subject, $replace, $pos, strlen($search));
+    }
+
+    return $subject;
+}
+
+class DB extends \SQLite3
+{
+    static protected $_instance = null;
+
+    protected $_running_sum = 0.0;
+
+    protected $_transaction = 0;
+
+    const NUM = \SQLITE3_NUM;
+    const ASSOC = \SQLITE3_ASSOC;
+    const BOTH = \SQLITE3_BOTH;
+
+    static public function getInstance($create = false)
+    {
+        return self::$_instance ?: self::$_instance = new DB($create);
+    }
+
+    private function __clone()
+    {
+    }
+
+    public function __construct($create = false)
+    {
+        $flags = SQLITE3_OPEN_READWRITE;
+
+        if ($create)
+        {
+            $flags |= SQLITE3_OPEN_CREATE;
+        }
+
+        parent::__construct(DB_FILE, $flags);
+
+        $this->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:
+            **
+            **   (<hit count> / <global hit count>) * <column weight>
+            **
+            ** 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 (file)
index 0000000..0136d6d
--- /dev/null
@@ -0,0 +1,792 @@
+<?php
+
+namespace Garradin;
+
+class Membres
+{
+    const DROIT_AUCUN = 0;
+    const DROIT_ACCES = 1;
+    const DROIT_ECRITURE = 2;
+    const DROIT_ADMIN = 9;
+
+    const ITEMS_PER_PAGE = 50;
+
+    protected function _getSalt($length)
+    {
+        $str = str_split('./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
+        shuffle($str);
+
+        return implode('',
+            array_rand(
+                $str,
+                $length)
+        );
+    }
+
+    protected function _hashPassword($password)
+    {
+        $salt = '$2a$08$' . $this->_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 (file)
index 0000000..4a78e13
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+
+namespace Garradin;
+
+class Membres_Categories
+{
+    protected $droits = [
+        'inscription'=> 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 (file)
index 0000000..eb3342a
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+
+namespace Garradin;
+
+class Membres_Import
+{
+       /**
+        * Champs du CSV de Galette
+        * les lignes vides ('') ne seront pas proposées à l'import
+        * @var array
+        */
+       public $galette_fields = [
+               'Numéro',
+               1,
+               'Nom',
+               'Prénom',
+               'Pseudo',
+               'Société',
+               2,
+               'Date de naissance',
+               3,
+               'Adresse, ligne 1',
+               'Adresse, ligne 2',
+               'Code postal',
+               'Ville',
+               'Pays',
+               'Téléphone fixe',
+               'Téléphone mobile',
+               'E-Mail',
+               'Site web',
+               'ICQ',
+               'MSN',
+               'Jabber',
+               'Infos (réservé administrateur)',
+               'Infos (public)',
+               'Profession',
+               'Identifiant',
+               'Mot de passe',
+               'Date création fiche',
+               'Date modification fiche',
+               4, // activite_adh
+               5, // bool_admin_adh
+               6, // bool_exempt_adh
+               7, // bool_display_info
+               8, // date_echeance
+               9, // pref_lang
+               'Lieu de naissance',
+               10, // GPG id
+               11 // Fingerprint
+       ];
+
+       /**
+        * Importer un CSV de la liste des membres depuis Galette
+        * @param  string $path              Chemin vers le CSV
+        * @param  array  $translation_table Tableau indiquant la correspondance à effectuer entre les champs
+        * de Galette et ceux de Garradin. Par exemple : ['Date création fiche' => 'date_inscription']
+        * @return boolean                   TRUE en cas de succès
+        */
+       public function fromGalette($path, $translation_table)
+       {
+               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 (file)
index 0000000..611e59d
--- /dev/null
@@ -0,0 +1,523 @@
+<?php
+
+namespace Garradin;
+
+class Plugin
+{
+       protected $id = null;
+       protected $plugin = null;
+
+       protected $mimes = [
+               'css' => '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 (file)
index 0000000..d970cf6
--- /dev/null
@@ -0,0 +1,208 @@
+<?php
+
+namespace Garradin;
+
+class Rappels
+{
+       /**
+        * Vérification des champs fournis pour la modification de donnée
+        * @param  array $data Tableau contenant les champs à ajouter/modifier
+        * @return void
+        */
+       protected function _checkFields(&$data)
+       {
+               $db = DB::getInstance();
+
+        if (empty($data['id_cotisation'])
+               || !$db->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 (file)
index 0000000..4bb67cd
--- /dev/null
@@ -0,0 +1,231 @@
+<?php
+
+namespace Garradin;
+
+class Rappels_Envoyes
+{
+       /**
+        * Types de médias
+        */
+       const MEDIA_EMAIL = 1;
+       const MEDIA_COURRIER = 2;
+       const MEDIA_TELEPHONE = 3;
+       const MEDIA_AUTRE = 4;
+
+       /**
+        * Nombre d'items par page dans les listes
+        */
+       const ITEMS_PER_PAGE = 50;
+
+       /**
+        * Vérification des champs fournis pour la modification de donnée
+        * @param  array $data Tableau contenant les champs à ajouter/modifier
+        * @return void
+        */
+       protected function _checkFields(&$data)
+       {
+               $db = DB::getInstance();
+
+        if (isset($data['id_cotisation']))
+        {
+               if (!$db->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 (file)
index 0000000..4fae1cb
--- /dev/null
@@ -0,0 +1,263 @@
+<?php
+
+namespace Garradin;
+
+class Sauvegarde
+{
+       const NEED_UPGRADE = 'nu';
+
+       /**
+        * Renvoie la liste des fichiers SQLite sauvegardés
+        * @param  boolean $auto Si true ne renvoie que la liste des sauvegardes automatiques
+        * @return array                 Liste des fichiers
+        */     
+       public function getList($auto = false)
+       {
+               $ext = $auto ? 'auto\.\d+\.sqlite' : 'sqlite';
+
+               $out = [];
+               $dir = dir(DATA_ROOT);
+
+               while ($file = $dir->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 (file)
index 0000000..d659981
--- /dev/null
@@ -0,0 +1,753 @@
+<?php
+
+namespace Garradin;
+
+require_once ROOT . '/include/libs/miniskel/class.miniskel.php';
+
+class Squelette_Snippet
+{
+    const TEXT = 0;
+    const PHP = 1;
+    const GUESS = 2;
+    const OBJ = 3;
+
+    protected $_content = [];
+
+    protected function _getType($type, $value)
+    {
+        if ($type == self::GUESS)
+        {
+            if ($value instanceof Squelette_Snippet)
+                return self::OBJ;
+            else
+                return self::TEXT;
+        }
+
+        return $type;
+    }
+
+    public function __construct($type = self::TEXT, $value = '')
+    {
+        $type = $this->_getType($type, $value);
+
+        if ($type == self::OBJ)
+        {
+            $this->_content = $value->get();
+        }
+        else
+        {
+            $this->_content[] = (string) (int) $type . $value;
+        }
+
+        unset($value);
+    }
+
+    public function prepend($type = self::TEXT, $value, $pos = false)
+    {
+        $type = $this->_getType($type, $value);
+
+        if ($type == self::OBJ)
+        {
+            if ($pos)
+            {
+                array_splice($this->_content, $pos, 0, $value->get());
+            }
+            else
+            {
+                $this->_content = array_merge($value->get(), $this->_content);
+            }
+        }
+        else
+        {
+            $value = (string) (int) $type . $value;
+
+            if ($pos)
+            {
+                array_splice($this->_content, $pos, 0, $value);
+            }
+            else
+            {
+                array_unshift($this->_content, $value);
+            }
+        }
+
+        unset($value);
+    }
+
+    public function append($type = self::TEXT, $value, $pos = false)
+    {
+        $type = $this->_getType($type, $value);
+
+        if ($type == self::OBJ)
+        {
+            if ($pos)
+            {
+                array_splice($this->_content, $pos + 1, 0, $value->get());
+            }
+            else
+            {
+                $this->_content = array_merge($this->_content, $value->get());
+            }
+        }
+        else
+        {
+            $value = (string) (int) $type . $value;
+
+            if ($pos)
+            {
+                array_splice($this->_content, $pos + 1, 0, $value);
+            }
+            else
+            {
+                array_push($this->_content, $value);
+            }
+        }
+
+        unset($value);
+    }
+
+    public function output($in_php = false)
+    {
+        $out = '';
+        $php = $in_php ?: false;
+
+        foreach ($this->_content as $line)
+        {
+            if ($line[0] == self::PHP && !$php)
+            {
+                $php = true;
+                $out .= '<?php ';
+            }
+            elseif ($line[0] == self::TEXT && $php)
+            {
+                $php = false;
+                $out .= ' ?>';
+            }
+
+            $out .= substr($line, 1);
+
+            if ($line[0] == self::PHP)
+            {
+                $out .= "\n";
+            }
+        }
+
+        if ($php && !$in_php)
+        {
+            $out .= ' ?>';
+        }
+
+        $this->_content = [];
+
+        return $out;
+    }
+
+    public function __toString()
+    {
+        return $this->output(false);
+    }
+
+    public function get()
+    {
+        return $this->_content;
+    }
+
+    public function replace($key, $type = self::TEXT, $value)
+    {
+        $type = $this->_getType($type, $value);
+
+        if ($type == self::OBJ)
+        {
+            array_splice($this->_content, $key, 1, $value->get());
+        }
+        else
+        {
+            $this->_content[$key] = (string) (int) $type . $value;
+        }
+
+        unset($value);
+    }
+}
+
+class Squelette extends \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' => '&lt;?php', '<?' => '<?php echo \'<?\'; ?>']);
+
+            $out = new Squelette_Snippet(2, $this->parse($content));
+            $out->prepend(1, '/* '.$tpl_id.' */ '.
+                'namespace Garradin; $db = DB::getInstance(); '.
+                'if ($this->parent) $parent_hash = $this->parent[\'_self_hash\']; '. // For included files
+                'else $parent_hash = false;');
+
+            if (!$no_display)
+            {
+                self::compile_store($tpl_id, $out);
+            }
+        }
+
+        if (!$no_display)
+        {
+            require self::compile_get_path($tpl_id);
+        }
+        else
+        {
+            eval($tpl_id);
+        }
+
+        return null;
+    }
+
+    public function dispatchURI()
+    {
+        $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
+
+        header('HTTP/1.1 200 OK', 200, true);
+
+        if ($pos = strpos($uri, '?'))
+        {
+            $uri = substr($uri, 0, $pos);
+        }
+        else
+        {
+            // WWW_URI inclus toujours le slash final, mais on veut le conserver ici
+            $uri = substr($uri, strlen(WWW_URI) - 1);
+        }
+
+        if ($uri == '/')
+        {
+            $skel = 'sommaire.html';
+        }
+        elseif ($uri == '/feed/atom/')
+        {
+            header('Content-Type: application/atom+xml');
+            $skel = 'atom.xml';
+        }
+        elseif (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 (file)
index 0000000..3b66544
--- /dev/null
@@ -0,0 +1,528 @@
+<?php
+
+namespace Garradin;
+
+class Wiki
+{
+    const LECTURE_PUBLIC = -1;
+    const LECTURE_NORMAL = 0;
+    const LECTURE_CATEGORIE = 1;
+
+    const ECRITURE_NORMAL = 0;
+    const ECRITURE_CATEGORIE = 1;
+
+    const ITEMS_PER_PAGE = 25;
+
+    protected $restriction_categorie = null;
+    protected $restriction_droit = null;
+
+    static public function transformTitleToURI($str)
+    {
+        $str = utils::transliterateToAscii($str);
+
+        $str = preg_replace('![^\w\d_-]!i', '-', $str);
+        $str = preg_replace('!-{2,}!', '-', $str);
+        $str = trim($str, '-');
+
+        return $str;
+    }
+
+    // Gestion des données ///////////////////////////////////////////////////////
+
+    public function _checkFields(&$data)
+    {
+        $db = DB::getInstance();
+
+        if (isset($data['titre']) && !trim($data['titre']))
+        {
+            throw new UserException('Le titre ne peut rester vide.');
+        }
+
+        if (isset($data['uri']) && !trim($data['uri']))
+        {
+            throw new UserException('L\'adresse de la page ne peut rester vide.');
+        }
+
+        if (isset($data['droit_lecture']))
+        {
+            $data['droit_lecture'] = (int) $data['droit_lecture'];
+
+            if ($data['droit_lecture'] < -1)
+            {
+                $data['droit_lecture'] = 0;
+            }
+        }
+
+        if (isset($data['droit_ecriture']))
+        {
+            $data['droit_ecriture'] = (int) $data['droit_ecriture'];
+
+            if ($data['droit_ecriture'] < 0)
+            {
+                $data['droit_ecriture'] = 0;
+            }
+        }
+
+        if (isset($data['parent']))
+        {
+            $data['parent'] = (int) $data['parent'];
+
+            if ($data['parent'] < 0)
+            {
+                $data['parent'] = 0;
+            }
+
+            if (!$db->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, \'<b>\', \'</b>\', \'...\', -1, -50) AS snippet,
+            rank(matchinfo(wiki_recherche), 0, 1.0, 1.0) AS points
+            FROM wiki_recherche AS r INNER JOIN wiki_pages AS p ON p.id = r.id
+            WHERE '.$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 (file)
index 0000000..33b7e53
--- /dev/null
@@ -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 (file)
index 0000000..2e73272
--- /dev/null
@@ -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 (file)
index 0000000..543e50e
--- /dev/null
@@ -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 (file)
index 0000000..80d0d48
--- /dev/null
@@ -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 (file)
index 0000000..d68e352
--- /dev/null
@@ -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 <input type> 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 (file)
index 0000000..b249b85
--- /dev/null
@@ -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 (file)
index 0000000..2cc846f
--- /dev/null
@@ -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 (file)
index 0000000..9a31a28
--- /dev/null
@@ -0,0 +1 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>
\ No newline at end of file
diff --git a/include/init.php b/include/init.php
new file mode 100644 (file)
index 0000000..fe4e820
--- /dev/null
@@ -0,0 +1,353 @@
+<?php
+
+namespace Garradin;
+
+error_reporting(-1);
+
+/*
+ * Version de Garradin
+ */
+
+function garradin_version()
+{
+    if (defined('Garradin\VERSION'))
+    {
+        return VERSION;
+    }
+
+    $file = __DIR__ . '/../VERSION';
+
+    if (file_exists($file))
+    {
+        $version = trim(file_get_contents($file));
+    }
+    else
+    {
+        $version = 'unknown';
+    }
+
+    define('Garradin\VERSION', $version);
+    return $version;
+}
+
+function garradin_manifest()
+{
+    $file = __DIR__ . '/../../manifest.uuid';
+
+    if (file_exists($file))
+    {
+        return substr(trim(file_get_contents($file)), 0, 10);
+    }
+
+    return false;
+}
+
+/*
+ * Configuration globale
+ */
+
+// Configuration externalisée
+if (file_exists(__DIR__ . '/../config.local.php'))
+{
+    require __DIR__ . '/../config.local.php';
+}
+
+if (!defined('Garradin\ROOT'))
+{
+    define('Garradin\ROOT', dirname(__DIR__));
+}
+
+if (!defined('Garradin\DATA_ROOT'))
+{
+    define('Garradin\DATA_ROOT', ROOT);
+}
+
+if (!defined('Garradin\DB_FILE'))
+{
+    define('Garradin\DB_FILE', DATA_ROOT . '/association.sqlite');
+}
+
+if (!defined('Garradin\DB_SCHEMA'))
+{
+    define('Garradin\DB_SCHEMA', ROOT . '/include/data/schema.sql');
+}
+
+if (!defined('Garradin\WWW_URI'))
+{
+    // Automagic URL discover
+    $path = str_replace(ROOT . '/www', '', getcwd());
+    $path = str_replace($path, '', dirname($_SERVER['SCRIPT_NAME']));
+    $path = (!empty($path[0]) && $path[0] != '/') ? '/' . $path : $path;
+    $path = (substr($path, -1) != '/') ? $path . '/' : $path;
+    define('Garradin\WWW_URI', $path);
+}
+
+if (!defined('Garradin\WWW_URL'))
+{
+    $host = isset($_SERVER['HTTP_HOST']) 
+        ? $_SERVER['HTTP_HOST'] 
+        : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost');
+    define('Garradin\WWW_URL', 'http' . (!empty($_SERVER['HTTPS']) ? 's' : '') . '://' . $host . WWW_URI);
+}
+
+if (!defined('Garradin\PLUGINS_ROOT'))
+{
+    define('Garradin\PLUGINS_ROOT', DATA_ROOT . '/plugins');
+}
+
+if (!defined('Garradin\PLUGINS_SYSTEM'))
+{
+    define('Garradin\PLUGINS_SYSTEM', '');
+}
+
+// Affichage des erreurs par défaut
+if (!defined('Garradin\SHOW_ERRORS'))
+{
+    define('Garradin\SHOW_ERRORS', true);
+}
+
+if (!defined('Garradin\MAIL_ERRORS'))
+{
+    define('Garradin\MAIL_ERRORS', false);
+}
+
+// Utilisation de cron pour les tâches automatiques
+if (!defined('Garradin\USE_CRON'))
+{
+    define('Garradin\USE_CRON', false);
+}
+
+define('Garradin\WEBSITE', 'http://garradin.eu/');
+define('Garradin\PLUGINS_URL', 'https://garradin.eu/plugins/list.json');
+
+// PHP devrait être assez intelligent pour chopper la TZ système mais nan
+// il sait pas faire (sauf sur Debian qui a le bon patch pour ça), donc pour 
+// éviter le message d'erreur à la con on définit une timezone par défaut
+// Pour utiliser une autre timezone, il suffit de définir date.timezone dans
+// un .htaccess ou dans config.local.php
+if (!ini_get('date.timezone'))
+{
+    if ($tz = @date_default_timezone_get())
+    {
+        ini_set('date.timezone', $tz);
+    }
+    else
+    {
+        ini_set('date.timezone', 'Europe/Paris');
+    }
+}
+
+ini_set('error_log', DATA_ROOT . '/error.log');
+ini_set('log_errors', true);
+
+if (SHOW_ERRORS)
+{
+    // Gestion par défaut des erreurs
+    ini_set('display_errors', true);
+    ini_set('html_errors', false);
+
+    if (PHP_SAPI != 'cli')
+    {
+        ini_set('error_prepend_string', '<!DOCTYPE html><meta charset="utf-8" /><style type="text/css">body { font-family: sans-serif; } h3 { color: darkred; } 
+            pre { text-shadow: 2px 2px 5px black; color: darkgreen; font-size: 2em; float: left; margin: 0 1em 0 0; padding: 1em; background: #cfc; border-radius: 50px; }</style>
+            <pre> \__/<br /> (xx)<br />//||\\\\</pre>
+            <h1>Erreur fatale</h1>
+            <p>Une erreur fatale s\'est produite à l\'exécution de Garradin. Pour rapporter ce bug
+            merci d\'inclure le message ci-dessous :</p>
+            <h3>');
+        ini_set('error_append_string', '</h3><hr />
+            <p><a href="http://dev.kd2.org/garradin/Rapporter%20un%20bug">Comment rapporter un bug</a></p>');
+    }
+}
+
+/*
+ * 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 . '" <noreply@no.reply>');
+    }
+
+    if (PHP_SAPI == 'cli')
+    {
+        echo $error;
+    }
+    else
+    {
+        echo '<!DOCTYPE html><meta charset="utf-8" /><style type="text/css">body { font-family: sans-serif; } h3 { color: darkred; }
+        pre { text-shadow: 2px 2px 5px black; color: darkgreen; font-size: 2em; float: left; margin: 0 1em 0 0; padding: 1em; background: #cfc; border-radius: 50px; }</style>
+        <pre> \__/<br /> (xx)<br />//||\\\\</pre>
+        <h1>Erreur d\'exécution</h1>';
+
+        if (SHOW_ERRORS)
+        {
+            echo '<p>Une erreur s\'est produite à l\'exécution de Garradin. Pour rapporter ce bug
+            merci d\'inclure le message suivant :</p>
+            <textarea cols="70" rows="'.substr_count($error, "\n").'">'.htmlspecialchars($error, ENT_QUOTES, 'UTF-8').'</textarea>
+            <hr />
+            <p><a href="http://dev.kd2.org/garradin/Rapporter%20un%20bug">Comment rapporter un bug</a></p>';
+        }
+        else
+        {
+            echo '<p>Une erreur s\'est produite à l\'exécution de Garradin.</p>
+            <p>Le webmaster a été prévenu.</p>';
+        }
+    }
+
+    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 (file)
index 0000000..aab766d
--- /dev/null
@@ -0,0 +1,350 @@
+<?php
+
+namespace Garradin;
+
+class Squelette_Filtres
+{
+    static private $g2x = null;
+    static private $alt = [];
+
+    static public $filtres_php = [
+        'strtolower',
+        'strtoupper',
+        'ucfirst',
+        'ucwords',
+        'str_rot13',
+        'str_shuffle',
+        'htmlentities',
+        'htmlspecialchars',
+        'trim',
+        'ltrim',
+        'rtrim',
+        'lcfirst',
+        'md5',
+        'sha1',
+        'metaphone',
+        'nl2br',
+        'soundex',
+        'str_split',
+        'str_word_count',
+        'strrev',
+        'strlen',
+        'wordwrap',
+        'strip_tags' => 'supprimer_tags',
+        'var_dump',
+    ];
+
+    static public $filtres_alias = [
+        '!='    =>  'different_de',
+        '=='    =>  'egal_a',
+        '?'     =>  'choixsivide',
+        '>'     =>  'superieur_a',
+        '>='    =>  'superieur_ou_egal_a',
+        '<'     =>  'inferieur_a',
+        '<='    =>  'inferieur_ou_egal_a',
+        'yes'   =>  'oui',
+        'no'    =>  'non',
+        'and'   =>  'et',
+        'or'    =>  'ou',
+        'xor'   =>  'xou',
+    ];
+
+    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 '<span style="unicode-bidi:bidi-override;direction: rtl;">'.htmlspecialchars(strrev($contact), ENT_QUOTES, 'UTF-8').'</span>';
+        else
+            return '<a href="'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'">'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'</a>';
+    }
+
+    static public function entites_html($texte)
+    {
+        return htmlspecialchars($texte, ENT_QUOTES, 'UTF-8');
+    }
+
+    static public function echapper_xml($texte)
+    {
+        return str_replace('&#039;', '&apos;', 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 ? '&nbsp;' : ' ';
+        $str = preg_replace('/(?:[\h]|&nbsp;)*([?!:»])(\s+|$)/u', $space.'\\1\\2', $str);
+        $str = preg_replace('/(^|\s+)([«])(?:[\h]|&nbsp;)*/u', '\\1\\2'.$space, $str);
+        return $str;
+    }
+
+    static public function pagination($total, $debut, $par_page)
+    {
+        $max_page = ceil($total / $par_page);
+        $current = ($debut > 0) ? ceil($debut / $par_page) + 1 : 1;
+        $out = '';
+
+        if ($current > 1)
+        {
+            $out .= '<a href="./'.($current > 2 ? '+' . ($debut - $par_page) : '').'">&laquo; Page pr&eacute;c&eacute;dente</a> - ';
+        }
+
+        for ($i = 1; $i <= $max_page; $i++)
+        {
+            $link = ($i == 1) ? './' : './+' . (($i - 1) * $par_page);
+
+            if ($i == $current)
+                $out .= '<strong>'.$i.'</strong> - ';
+            else
+                $out .= '<a href="'.$link.'">'.$i.'</a> - ';
+        }
+
+        if ($current < $max_page)
+        {
+            $out .= '<a href="./+'.($debut + $par_page).'">Page suivante &raquo;</a>';
+        }
+        else
+        {
+            $out = substr($out, 0, -3);
+        }
+
+        return $out;
+    }
+
+    // Compatibilité SPIP
+
+    static public function egal_a($value, $test)
+    {
+        if ($value == $test)
+            return true;
+        else
+            return false;
+    }
+
+    static public function different_de($value, $test)
+    {
+        if ($value != $test)
+            return true;
+        else
+            return false;
+    }
+
+    // disponible aussi avec : | ?{sioui, sinon}
+    static public function choixsivide($value, $un, $deux = '')
+    {
+        if (empty($value) || !trim($value))
+            return $deux;
+        else
+            return $un;
+    }
+
+    static public function sinon($value, $sinon = '')
+    {
+        if ($value)
+            return $value;
+        else
+            return $sinon;
+    }
+
+    static public function choixsiegal($value, $test, $un, $deux)
+    {
+        return ($value == $test) ? $un : $deux;
+    }
+
+    static public function supprimer_tags($value, $replace = '')
+    {
+        return preg_replace('!<[^>]*>!', $replace, $value);
+    }
+
+    static public function supprimer_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 (file)
index 0000000..d307bd6
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+
+namespace Garradin;
+
+class Static_Cache
+{
+       const EXPIRE = 3600; // 1h
+       const CLEAN_EXPIRE = 86400; // 1 day
+
+       protected static function _getCacheDir()
+       {
+               return DATA_ROOT . '/cache/static';
+       }
+
+       protected static function _getCachePath($id)
+       {
+               $id = 'cache_' . sha1($id);
+               return self::_getCacheDir() . '/' . $id;
+       }
+
+       static public function store($id, $content)
+       {
+               $path = self::_getCachePath($id);
+               return (bool) file_put_contents($path, $content);
+       }
+
+       static public function expired($id, $expire = self::EXPIRE)
+       {
+               $path = self::_getCachePath($id);
+               $time = @filemtime($path);
+
+               if (!$time)
+               {
+                       return true;
+               }
+
+               return ($time > (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 (file)
index 0000000..6f9f8e7
--- /dev/null
@@ -0,0 +1,604 @@
+<?php
+
+namespace Garradin;
+
+require_once ROOT . '/include/libs/template_lite/class.template.php';
+
+class Template extends \Template_Lite
+{
+    static protected $_instance = null;
+
+    static public function getInstance()
+    {
+        return self::$_instance ?: self::$_instance = new Template;
+    }
+
+    private function __clone()
+    {
+    }
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->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 '<input type="hidden" name="'.$name.'" value="'.$value.'" />';
+}
+
+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 = '&#x2611;';
+
+                if ($droit == Membres::DROIT_AUCUN)
+                    $desc = 'Ne peut modifier la configuration';
+                else
+                    $desc = 'Peut modifier la configuration';
+            }
+            elseif ($cle == 'compta')
+            {
+                $s = '&euro;';
+            }
+
+            if (!$s)
+                $s = strtoupper($cle[0]);
+
+            if (!$desc)
+            {
+                $desc = ucfirst($cle). ' : ';
+
+                if ($droit == Membres::DROIT_AUCUN)
+                    $desc .= 'Pas accès';
+                elseif ($droit == Membres::DROIT_ACCES)
+                    $desc .= 'Lecture uniquement';
+                elseif ($droit == Membres::DROIT_ECRITURE)
+                    $desc .= 'Lecture & écriture';
+                else
+                    $desc .= 'Administration';
+            }
+
+            $out[$cle] = '<b class="'.$class.' '.$cle.'" title="'
+                .htmlspecialchars($desc, ENT_QUOTES, 'UTF-8').'">'.$s.'</b>';
+        }
+    }
+
+    return implode(' ', $out);
+}
+
+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('!<a href="([^/.:@]+)">!i', function ($matches) use ($prefix) {
+        return '<a href="' . $prefix . Wiki::transformTitleToURI($matches[1]) . '">';
+    }, $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 = '<ul class="pagination">';
+
+    foreach ($pagination as &$page)
+    {
+        $attributes = '';
+
+        if (!empty($page['class']))
+            $attributes .= ' class="' . htmlspecialchars($page['class']) . '" ';
+
+        $out .= '<li'.$attributes.'>';
+
+        $attributes = '';
+
+        if (!empty($page['accesskey']))
+            $attributes .= ' accesskey="' . htmlspecialchars($page['accesskey']) . '" ';
+
+        $out .= '<a' . $attributes . ' href="' . str_replace('[ID]', htmlspecialchars($page['id']), $params['url']) . '">';
+        $out .= htmlspecialchars($page['label']);
+        $out .= '</a>';
+        $out .= '</li>' . "\n";
+    }
+
+    $out .= '</ul>';
+
+    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 = '<table class="diff">';
+    $prev = key($diff);
+
+    foreach ($diff as $i=>$line)
+    {
+        if ($i > $prev + 1)
+        {
+            $out .= '<tr><td colspan="5" class="separator"><hr /></td></tr>';
+        }
+
+        list($type, $old, $new) = $line;
+
+        $class1 = $class2 = '';
+        $t1 = $t2 = '';
+
+        if ($type == \simpleDiff::INS)
+        {
+            $class2 = 'ins';
+            $t2 = '<b class="icn">➕</b>';
+            $old = htmlspecialchars($old, ENT_QUOTES, 'UTF-8');
+            $new = htmlspecialchars($new, ENT_QUOTES, 'UTF-8');
+        }
+        elseif ($type == \simpleDiff::DEL)
+        {
+            $class1 = 'del';
+            $t1 = '<b class="icn">➖</b>';
+            $old = htmlspecialchars($old, ENT_QUOTES, 'UTF-8');
+            $new = htmlspecialchars($new, ENT_QUOTES, 'UTF-8');
+        }
+        elseif ($type == \simpleDiff::CHANGED)
+        {
+            $class1 = 'del';
+            $class2 = 'ins';
+            $t1 = '<b class="icn">➖</b>';
+            $t2 = '<b class="icn">➕</b>';
+
+            $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', '<del>\\1</del>', $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', '<ins>\\1</ins>', $new);
+        }
+        else
+        {
+            $old = htmlspecialchars($old, ENT_QUOTES, 'UTF-8');
+            $new = htmlspecialchars($new, ENT_QUOTES, 'UTF-8');
+        }
+
+        $out .= '<tr>';
+        $out .= '<td class="line">'.($i+1).'</td>';
+        $out .= '<td class="leftChange">'.$t1.'</td>';
+        $out .= '<td class="leftText '.$class1.'">'.$old.'</td>';
+        $out .= '<td class="rightChange">'.$t2.'</td>';
+        $out .= '<td class="rightText '.$class2.'">'.$new.'</td>';
+        $out .= '</tr>';
+
+        $prev = $i;
+    }
+
+    $out .= '</table>';
+    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 = '<select name="'.$name.'" id="f_'.$name.'" class="large">';
+
+    foreach ($comptes as $compte)
+    {
+        // Ne pas montrer les comptes désactivés
+        if (!empty($compte['desactive']))
+            continue;
+
+        if (!isset($compte['id'][1]))
+        {
+            $out.= '<optgroup label="'.htmlspecialchars($compte['libelle'], ENT_QUOTES, 'UTF-8', false).'" class="niveau_1"></optgroup>';
+        }
+        elseif (!isset($compte['id'][2]) && empty($params['create']))
+        {
+            if ($compte['id'] > 10)
+                $out.= '</optgroup>';
+
+            $out.= '<optgroup label="'.htmlspecialchars($compte['id'] . ' - ' . $compte['libelle'], ENT_QUOTES, 'UTF-8', false).'" class="niveau_2">';
+        }
+        else
+        {
+            $out .= '<option value="'.htmlspecialchars($compte['id'], ENT_QUOTES, 'UTF-8', false).'" class="niveau_'.strlen($compte['id']).'"';
+
+            if ($selected == $compte['id'])
+            {
+                $out .= ' selected="selected"';
+            }
+
+            $out .= '>' . htmlspecialchars($compte['id'] . ' - ' . $compte['libelle'], ENT_QUOTES, 'UTF-8', false);
+            $out .= '</option>';
+        }
+    }
+
+    $out .= '</optgroup>';
+    $out .= '</select>';
+
+    return $out;
+}
+
+function escape_money($number)
+{
+    return number_format((float)$number, 2, ',', ' ');
+}
+
+function tpl_html_money($number)
+{
+    return '<b class="money">' . escape_money($number) . '</b>';
+}
+
+function tpl_html_champ_membre($params)
+{
+    if (empty($params['config']) || empty($params['name']))
+        throw new \BadFunctionCallException('Paramètres type et name obligatoires.');
+
+    $config = $params['config'];
+    $type = $config['type'];
+
+    if ($params['name'] == 'passe' || (!empty($params['user_mode']) && !empty($config['private'])))
+    {
+        return '';
+    }
+
+    if ($type == 'select')
+    {
+        if (empty($config['options']))
+            throw new \BadFunctionCallException('Paramètre options obligatoire pour champ de type select.');
+    }
+    elseif ($type == 'country')
+    {
+        $type = 'select';
+        $config['options'] = utils::getCountryList();
+        $params['default'] = Config::getInstance()->get('pays');
+    }
+    elseif ($type == 'date')
+    {
+        $params['pattern'] = '\d{4}-\d{2}-\d{2}';
+    }
+    elseif ($type == 'multiple')
+    {
+        if (empty($config['options']))
+            throw new \BadFunctionCallException('Paramètre options obligatoire pour champ de type multiple.');
+    }
+
+    $field = '';
+    $value = tpl_form_field($params);
+    $attributes = 'name="' . htmlspecialchars($params['name'], ENT_QUOTES, 'UTF-8') . '" ';
+    $attributes .= 'id="f_' . htmlspecialchars($params['name'], ENT_QUOTES, 'UTF-8') . '" ';
+
+    if (!empty($params['disabled']))
+    {
+        $attributes .= 'disabled="disabled" ';
+    }
+
+    if (!empty($config['mandatory']))
+    {
+        $attributes .= 'required="required" ';
+    }
+
+    if (!empty($params['user_mode']) && empty($config['editable']))
+    {
+        $out = '<dt>' . htmlspecialchars($config['title'], ENT_QUOTES, 'UTF-8') . '</dt>';
+        $out .= '<dd>' . htmlspecialchars((trim($value) === '' ? 'Non renseigné' : $value), ENT_QUOTES, 'UTF-8') . '</dd>';
+        return $out;
+    }
+
+    if ($type == 'select')
+    {
+        $field .= '<select '.$attributes.'>';
+        foreach ($config['options'] as $k=>$v)
+        {
+            if (is_int($k))
+                $k = $v;
+
+            $field .= '<option value="' . htmlspecialchars($k, ENT_QUOTES, 'UTF-8') . '"';
+
+            if ($value == $k || empty($value) && !empty($params['default']))
+                $field .= ' selected="selected"';
+
+            $field .= '>' . htmlspecialchars($v, ENT_QUOTES, 'UTF-8') . '</option>';
+        }
+        $field .= '</select>';
+    }
+    elseif ($type == 'multiple')
+    {
+        if (is_array($value))
+        {
+            $binary = 0;
+
+            foreach ($value as $k => $v)
+            {
+                if (array_key_exists($k, $config['options']) && !empty($v))
+                {
+                    $binary |= 0x01 << $k;
+                }
+            }
+
+            $value = $binary;
+        }
+
+        foreach ($config['options'] as $k=>$v)
+        {
+            $b = 0x01 << (int)$k;
+            $field .= '<label><input type="checkbox" name="' 
+                . htmlspecialchars($params['name'], ENT_QUOTES, 'UTF-8') . '[' . (int)$k . ']" value="1" '
+                . (($value & $b) ? 'checked="checked"' : '') . ' ' . $attributes . '/> ' 
+                . htmlspecialchars($v, ENT_QUOTES, 'UTF-8') . '</label><br />';
+        }
+    }
+    elseif ($type == 'textarea')
+    {
+        $field .= '<textarea ' . $attributes . 'cols="30" rows="5">' . $value . '</textarea>';
+    }
+    else
+    {
+        if ($type == 'checkbox')
+        {
+            if (!empty($value))
+            {
+                $attributes .= 'checked="checked" ';
+            }
+
+            $value = '1';
+        }
+
+        $field .= '<input type="' . $type . '" ' . $attributes . ' value="' . $value . '" />';
+    }
+
+    $out = '
+    <dt>';
+
+    if ($type == 'checkbox')
+    {
+        $out .= $field . ' ';
+    }
+
+    $out .= '<label for="f_' . htmlspecialchars($params['name'], ENT_QUOTES, 'UTF-8') . '">'
+        . htmlspecialchars($config['title'], ENT_QUOTES, 'UTF-8') . '</label>';
+
+    if (!empty($config['mandatory']))
+    {
+        $out .= ' <b title="(Champ obligatoire)">obligatoire</b>';
+    }
+
+    $out .= '</dt>';
+
+    if (!empty($config['help']))
+    {
+        $out .= '
+    <dd class="help">' . htmlspecialchars($config['help'], ENT_QUOTES, 'UTF-8') . '</dd>';
+    }
+
+    if ($type != 'checkbox')
+    {
+        $out .= '
+    <dd>' . $field . '</dd>';
+    }
+
+    return $out;
+}
+
+$tpl->register_compiler('continue', function() { return 'continue;'; });
+
+$tpl->register_function('csrf_field', 'Garradin\tpl_csrf_field');
+$tpl->register_function('form_field', 'Garradin\tpl_form_field');
+$tpl->register_function('select_compte', 'Garradin\tpl_select_compte');
+
+$tpl->register_function('format_droits', 'Garradin\tpl_format_droits');
+
+$tpl->register_function('pagination', 'Garradin\tpl_pagination');
+
+$tpl->register_function('diff', 'Garradin\tpl_diff');
+$tpl->register_function('html_champ_membre', 'Garradin\tpl_html_champ_membre');
+
+$tpl->register_function('plugin_url', ['Garradin\utils', 'plugin_url']);
+
+$tpl->register_modifier('get_country_name', ['Garradin\utils', 'getCountryName']);
+$tpl->register_modifier('format_tel', 'Garradin\tpl_format_tel');
+$tpl->register_modifier('format_wiki', 'Garradin\tpl_format_wiki');
+$tpl->register_modifier('liens_wiki', 'Garradin\tpl_liens_wiki');
+$tpl->register_modifier('escape_money', 'Garradin\escape_money');
+$tpl->register_modifier('html_money', 'Garradin\tpl_html_money');
+$tpl->register_modifier('abs', 'abs');
+
+$tpl->register_modifier('display_champ_membre', function ($v, $config) {
+    if ($config['type'] == 'checkbox') {
+        return $v ? 'Oui' : 'Non';
+    } elseif ($config['type'] == 'email') {
+        return '<a href="mailto:' . $v . '">' . $v . '</a>';
+    } elseif ($config['type'] == 'tel') {
+        return '<a href="tel:' . $v . '">' . $v . '</a>';
+    } elseif ($config['type'] == 'url') {
+        return '<a href="' . $v . '">' . $v . '</a>';
+    } elseif ($config['type'] == 'country') {
+        return utils::getCountryName($v);
+    } elseif ($config['type'] == 'multiple') {
+        $out = [];
+
+        foreach ($config['options'] as $b => $name)
+        {
+            if ($v & (0x01 << $b))
+                $out[] = $name;
+        }
+
+        return implode(', ', $out);
+    } else {
+        return $v;
+    }
+
+});
+
+$tpl->register_modifier('format_sqlite_date_to_french', ['Garradin\utils', 'sqliteDateToFrench']);
+
+$tpl->register_modifier('format_bytes', function ($size) {
+    if ($size > (1024 * 1024))
+        return round($size / 1024 / 1024, 2) . ' Mo';
+    elseif ($size > 1024)
+        return round($size / 1024, 2) . ' Ko';
+    else
+        return $size . ' ob_get_contents(oid)';
+});
+
+$tpl->register_modifier('strftime_fr', 'Garradin\tpl_strftime_fr');
+$tpl->register_modifier('date_fr', 'Garradin\tpl_date_fr');
+
+?>
\ No newline at end of file
diff --git a/include/lib.utils.php b/include/lib.utils.php
new file mode 100644 (file)
index 0000000..f134fed
--- /dev/null
@@ -0,0 +1,677 @@
+<?php
+
+namespace Garradin;
+
+class utils
+{
+    static protected $country_list = null;
+
+    static protected $g2x = null;
+
+    static private $french_date_names = [
+        'January'=>'Janvier', 'February'=>'Février', 'March'=>'Mars', 'April'=>'Avril', 'May'=>'Mai',
+        'June'=>'Juin', 'July'=>'Juillet', 'August'=>'Août', 'September'=>'Septembre', 'October'=>'Octobre',
+        'November'=>'Novembre', 'December'=>'Décembre', 'Monday'=>'Lundi', 'Tuesday'=>'Mardi', 'Wednesday'=>'Mercredi',
+        'Thursday'=>'Jeudi','Friday'=>'Vendredi','Saturday'=>'Samedi','Sunday'=>'Dimanche',
+        'Feb'=>'Fév','Apr'=>'Avr','May'=>'Mai','Jun'=>'Juin', 'Jul'=>'Juil','Aug'=>'Aout','Dec'=>'Déc',
+        'Mon'=>'Lun','Tue'=>'Mar','Wed'=>'Mer','Thu'=>'Jeu','Fri'=>'Ven','Sat'=>'Sam','Sun'=>'Dim'];
+
+    static public function strftime_fr($format=null, $ts=null)
+    {
+        if (is_null($format))
+        {
+            $format = '%d/%m/%Y à %H:%M';
+        }
+
+        $date = strftime($format, $ts);
+        $date = strtr($date, self::$french_date_names);
+        $date = strtolower($date);
+        return $date;
+    }
+
+    static public function date_fr($format=null, $ts=null)
+    {
+        if (is_null($format))
+        {
+            $format = 'd/m/Y à H:i';
+        }
+
+        $date = date($format, $ts);
+        $date = strtr($date, self::$french_date_names);
+        $date = strtolower($date);
+        return $date;
+    }
+
+    static public function sqliteDateToFrench($d, $short = false)
+    {
+        if (strlen($d) == 10 || $short)
+        {
+            $d = substr($d, 0, 10);
+            $f = 'Y-m-d';
+            $f2 = 'd/m/Y';
+        }
+        elseif (strlen($d) == 16)
+        {
+            $f = 'Y-m-d H:i';
+            $f2 = 'd/m/Y H:i';
+        }
+        else
+        {
+            $f = 'Y-m-d H:i:s';
+            $f2 = 'd/m/Y H:i';
+        }
+        
+        if ($dt = \DateTime::createFromFormat($f, $d))
+            return $dt->format($f2);
+        else
+            return $d;
+    }
+
+    static public function makeTimestampFromForm($d)
+    {
+        return mktime($d['h'], $d['min'], 0, $d['m'], $d['d'], $d['y']);
+    }
+
+    static public function modifyDate($str, $change)
+    {
+        $date = \DateTime::createFromFormat('Y-m-d', $str);
+        $date->modify($change);
+        return $date->format('Y-m-d');
+    }
+
+    static public function checkDate($str)
+    {
+        if (!preg_match('!^(\d{4})-(\d{2})-(\d{2})$!', $str, $match))
+            return false;
+
+        if (!checkdate($match[2], $match[3], $match[1]))
+            return false;
+
+        return true;
+    }
+
+    static public function checkDateTime($str)
+    {
+        if (!preg_match('!^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})!', $str, $match))
+            return false;
+
+        if (!self::checkDate($match[1]))
+            return false;
+
+        if ((int) $match[2] < 0 || (int) $match[2] > 23)
+            return false;
+
+        if ((int) $match[3] < 0 || (int) $match[3] > 59)
+            return false;
+        
+        if (isset($match[4]) && ((int) $match[4] < 0 || (int) $match[4] > 59))
+            return false;
+
+        return true;
+    }
+
+    static public function getRequestURI()
+    {
+        if (!empty($_SERVER['REQUEST_URI']))
+            return $_SERVER['REQUEST_URI'];
+        else
+            return false;
+    }
+
+    static public function getSelfURL($no_qs = false)
+    {
+        $uri = self::getRequestUri();
+
+        if (strpos($uri, WWW_URI) === 0)
+        {
+            $uri = substr($uri, strlen(WWW_URI));
+        }
+
+        if ($no_qs && (strpos($uri, '?') !== false))
+        {
+            $uri = substr($uri, 0, strpos($uri, '?'));
+        }
+
+        return WWW_URL . $uri;
+    }
+
+    static public function disableHttpCaching()
+    {
+        header("Cache-Control: no-cache, must-revalidate");
+        header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
+        header('Pragma: no-cache');
+    }
+
+
+    public static function redirect($destination=false, $exit=true)
+    {
+        if (empty($destination) || !preg_match('/^https?:\/\//', $destination))
+        {
+            if (empty($destination))
+                $destination = WWW_URL;
+            else
+                $destination = WWW_URL . preg_replace('/^\//', '', $destination);
+        }
+
+        if (headers_sent())
+        {
+            echo
+              '<html>'.
+              ' <head>' .
+              '  <script type="text/javascript">' .
+              '    document.location = "' . htmlspecialchars($destination, ENT_QUOTES, 'UTF-8', false) . '";' .
+              '  </script>' .
+              ' </head>'.
+              ' <body>'.
+              '   <div>'.
+              '     <a href="' . htmlspecialchars($destination, ENT_QUOTES, 'UTF-8', false) . '">Cliquez ici pour continuer...</a>'.
+              '   </div>'.
+              ' </body>'.
+              '</html>';
+
+            if ($exit)
+              exit();
+
+            return true;
+        }
+
+        header("Location: " . $destination);
+
+        if ($exit)
+          exit();
+    }
+
+
+    static protected function _sessionStart($force = false)
+    {
+        if (!isset($_SESSION) && ($force || isset($_COOKIE[session_name()])))
+        {
+            session_start();
+        }
+        return true;
+    }
+
+    static public function CSRF_create($key)
+    {
+        self::_sessionStart(true);
+
+        if (!isset($_SESSION['csrf']))
+        {
+            $_SESSION['csrf'] = [];
+        }
+
+        $_SESSION['csrf'][$key] = sha1($key . uniqid($key, true) . time());
+        return $_SESSION['csrf'][$key];
+    }
+
+    static public function CSRF_check($key, $hash=null)
+    {
+        self::_sessionStart();
+
+        if (is_null($hash))
+        {
+            $name = self::CSRF_field_name($key);
+
+            if (!isset($_POST[$name]))
+                return false;
+
+            $hash = $_POST[$name];
+        }
+
+        if (empty($_SESSION['csrf'][$key]))
+            return false;
+
+        if ($_SESSION['csrf'][$key] != $hash)
+            return false;
+
+        unset($_SESSION['csrf'][$key]);
+
+        return true;
+    }
+
+    static public function CSRF_field_name($key)
+    {
+        return 'gecko/'.base64_encode(sha1($key, true));
+    }
+
+    static public function generatePassword($length, $chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
+    {
+        $string = '';
+        for ($i = 0; $i < $length; $i++)
+        {
+            $pos = rand(0, strlen($chars)-1);
+            $string .= $chars[$pos];
+        }
+        return $string;
+    }
+
+    static public function post($key)
+    {
+        return isset($_POST[$key]) ? $_POST[$key] : '';
+    }
+
+    static public function get($key)
+    {
+        return isset($_GET[$key]) ? $_GET[$key] : '';
+    }
+
+    static public function getIP()
+    {
+        if (!empty($_SERVER['REMOTE_ADDR']))
+            return $_SERVER['REMOTE_ADDR'];
+        return '';
+    }
+
+    static public function &getCountryList()
+    {
+        if (is_null(self::$country_list))
+        {
+            require_once ROOT . '/include/libs/countries/countries_fr.php';
+            self::$country_list = $countries;
+        }
+
+        return self::$country_list;
+    }
+
+    static public function getCountryName($code)
+    {
+        $list = self::getCountryList();
+
+        if (!isset($list[$code]))
+            return false;
+
+        return $list[$code];
+    }
+
+    /**
+     * Génération pagination à partir de la page courante ($current),
+     * du nombre d'items total ($total), et du nombre d'items par page ($bypage).
+     * $listLength représente la longueur d'items de la pagination à génerer
+     *
+     * @param int $current
+     * @param int $total
+     * @param int $bypage
+     * @param int $listLength
+     * @param bool $showLast Toggle l'affichage du dernier élément de la pagination
+     * @return array
+     */
+    public static function getGenericPagination($current, $total, $bypage, $listLength=11, $showLast = true)
+    {
+        if ($total <= $bypage)
+            return false;
+
+        $total = ceil($total / $bypage);
+
+        if ($total < $current)
+            return false;
+
+        $length = ($listLength / 2);
+
+        $begin = $current - ceil($length);
+        if ($begin < 1)
+        {
+            $begin = 1;
+        }
+
+        $end = $begin + $listLength;
+        if($end > $total)
+        {
+            $begin -= ($end - $total);
+            $end = $total;
+        }
+        if ($begin < 1)
+        {
+            $begin = 1;
+        }
+        if($end==($total-1)) {
+            $end = $total;
+        }
+        if($begin == 2) {
+            $begin = 1;
+        }
+        $out = [];
+
+        if ($current > 1) {
+            $out[] = ['id' => $current - 1, 'label' =>  '« ' . 'Page précédente', 'class' => 'prev', 'accesskey' => 'a'];
+        }
+
+        if ($begin > 1) {
+            $out[] = ['id' => 1, 'label' => '1 ...', 'class' => 'first'];
+        }
+
+        for ($i = $begin; $i <= $end; $i++)
+        {
+            $out[] = ['id' => $i, 'label' => $i, 'class' => ($i == $current) ? 'current' : ''];
+        }
+
+        if ($showLast && $end < $total) {
+            $out[] = ['id' => $total, 'label' => '... ' . $total, 'class' => 'last'];
+        }
+
+        if ($current < $total) {
+            $out[] = ['id' => $current + 1, 'label' => 'Page suivante' . ' »', 'class' => 'next', 'accesskey' => 'z'];
+        }
+
+        return $out;
+    }
+
+    static public function transliterateToAscii($str, $charset='UTF-8')
+    {
+        // Don't process empty strings
+        if (!trim($str))
+            return $str;
+
+        // We only process non-ascii strings
+        if (preg_match('!^[[:ascii:]]+$!', $str))
+            return $str;
+
+        $str = htmlentities($str, ENT_NOQUOTES, $charset);
+
+        $str = preg_replace('#&([A-za-z])(?:acute|cedil|circ|grave|orn|ring|slash|th|tilde|uml);#', '\1', $str);
+        $str = preg_replace('#&([A-za-z]{2})(?:lig);#', '\1', $str); // pour les ligatures e.g. '&oelig;'
+
+        $str = preg_replace('#&[^;]+;#', '', $str); // supprime les autres caractères
+        $str = preg_replace('![^[:ascii:]]+!', '', $str);
+
+        return $str;
+    }
+
+    static public function htmlLinksOnUrls($str)
+    {
+        return preg_replace_callback('!(?<=\s|^)((?:(ftp|https?|file|ed2k|ircs?)://|(magnet|mailto|data|tel|fax|geo|sips?|xmpp):)([^\s<]+))!',
+            function ($match) {
+                $proto = $match[2] ?: $match[3];
+                $text = ($proto == 'http' || $proto == 'mailto') ? $match[4] : $match[1];
+                return '<a class="'.$proto.'" href="'.htmlspecialchars($match[1], ENT_QUOTES, 'UTF-8').'">'.htmlspecialchars($text, ENT_QUOTES, 'UTF-8').'</a>';
+            }, $str);
+    }
+
+    static public function htmlGarbage2xhtml($str)
+    {
+        if (!self::$g2x)
+        {
+            require_once ROOT . '/include/libs/garbage2xhtml/lib.garbage2xhtml.php';
+            self::$g2x = new \garbage2xhtml;
+            self::$g2x->core_attributes = ['class', 'id', 'title'];
+        }
+
+        return self::$g2x->process($str);
+    }
+
+    static public function htmlSpip($str, $prefix = '')
+    {
+        // Intertitres
+        $str = preg_replace('/(?<!\\\\)\{{3}(\V*)\}{3}/', '<h3>$1</h3>', $str);
+
+        // Gras
+        $str = preg_replace('/(?<!\\\\)\{{2}(\V*)\}{2}/', '<strong>$1</strong>', $str);
+
+        // Italique
+        $str = preg_replace('/(?<!\\\\)\{(\V*)\}/', '<em>$1</em>', $str);
+
+        // Espaces typograhiques
+        $str = preg_replace('/\h*([?!;:»])(\s+|$)/u', '&nbsp;$1$2', $str);
+        $str = preg_replace('/(^|\s+)([«])\h*/u', '$1$2&nbsp;', $str);
+
+        // Liens
+        $str = preg_replace('/(?<!\\\\)\[(.+?)->(.+?)\]/', '<a href="$2">$1</a>', $str);
+        $str = preg_replace('/(?<!\\\\)\[(.+?)\]/', '<a href="$1">$1</a>', $str);
+
+        // Adresses email
+        $str = preg_replace('/<a href="((?!http).*@.*)">/iU', '<a href="mailto:$1">', $str);
+
+        return $str;
+    }
+
+    static public function mail($to, $subject, $content, $additional_headers = [])
+    {
+        // Création du contenu du message
+        $content = wordwrap($content);
+        $content = trim($content);
+
+        $content = preg_replace("#(?<!\r)\n#si", "\r\n", $content);
+
+        // Construction des entêtes
+        $headers = '';
+
+        $config = Config::getInstance();
+
+        if (empty($additional_headers['From']))
+        {
+            $additional_headers['From'] = '"NE PAS REPONDRE" <'.$config->get('email_envoi_automatique').'>';
+        }
+
+        $additional_headers['MIME-Version'] = '1.0';
+        $additional_headers['Content-type'] = 'text/plain; charset=UTF-8';
+        $additional_headers['Return-Path'] = $config->get('email_envoi_automatique');
+
+        foreach ($additional_headers as $name=>$value)
+        {
+            $headers .= $name . ': '.$value."\r\n";
+        }
+
+        $headers = preg_replace("#(?<!\r)\n#si", "\r\n", $headers);
+
+        $subject = '=?UTF-8?B?'.base64_encode($subject).'?=';
+
+        if (is_array($to))
+        {
+            foreach ($to as $t)
+            {
+                return mail($t, $suject, $content, $headers);
+            }
+        }
+        else
+        {
+            return mail($to, $subject, $content, $headers);
+        }
+    }
+
+    static public function clearCaches()
+    {
+        $path = DATA_ROOT . '/cache/compiled';
+        $dir = dir($path);
+
+        while ($file = $dir->read())
+        {
+            if ($file[0] != '.')
+            {
+                unlink($path . '/' . $file);
+            }
+        }
+
+        $dir->close();
+        return true;
+    }
+
+    static public function suggestPassword()
+    {
+        require_once ROOT . '/include/libs/passphrase/lib.passphrase.french.php';
+        return \Passphrase::generate();
+    }
+
+    static public function checkIBAN($iban)
+    {
+        $iban = substr($iban, 4) . substr($iban, 0, 4);
+        $iban = str_replace(range('A', 'Z'), range(10, 35), $iban);
+        return (bcmod($iban, 97) == 1);
+    }
+
+    static public function IBAN_RIB($iban)
+    {
+        if (substr($iban, 0, 2) != 'FR')
+        {
+            return '';
+        }
+
+        return substr($iban, 4, 5) // Code banque
+            . ' ' . substr($iban, 4+5, 5) // Code guichet
+            . ' ' . substr($iban, 4+5+5, -2) // Numéro de compte
+            . ' ' . substr($iban, -2); // Clé RIB
+    }
+
+    static public function checkBIC($bic)
+    {
+        return preg_match('!^[A-Z]{4}[A-Z]{2}[1-9A-Z]{2}(?:[A-Z\d]{3})?$!', $bic);
+    }
+
+    static public function normalizePhoneNumber($n)
+    {
+        $n = preg_replace('!(\+\d+)\(0\)!', '\\1', $n);
+        $n = preg_replace('![^\d\+]!', '', $n);
+        return $n;
+    }
+
+    static public function write_ini_string($in)
+    {
+        $out = '';
+        $get_ini_line = function ($key, $value) use (&$get_ini_line)
+        {
+            if (is_bool($value))
+            {
+                return $key . ' = ' . ($value ? 'true' : 'false');
+            }
+            elseif (is_numeric($value))
+            {
+                return $key . ' = ' . $value;
+            }
+            elseif (is_array($value))
+            {
+                $out = '';
+                foreach ($value as $row)
+                {
+                    $out .= $get_ini_line($key . '[]', $row) . "\n";
+                }
+
+                return substr($out, 0, -1);
+            }
+            else
+            {
+                return $key . ' = "' . str_replace('"', '\\"', $value) . '"';
+            }
+        };
+
+        foreach ($in as $key=>$value)
+        {
+            if (is_array($value) && is_string($key))
+            {
+                $out .= '[' . $key . "]\n";
+
+                foreach ($value as $row_key=>$row_value)
+                {
+                    $out .= $get_ini_line($row_key, $row_value) . "\n";
+                }
+
+                $out .= "\n";
+            }
+            else
+            {
+                $out .= $get_ini_line($key, $value) . "\n";
+            }
+        }
+
+        return $out;
+    }
+
+    static public function getMaxUploadSize()
+    {
+        return min([
+            self::return_bytes(ini_get('upload_max_filesize')),
+            self::return_bytes(ini_get('post_max_size')),
+            self::return_bytes(ini_get('memory_limit'))
+        ]);
+    }
+
+
+    static public function return_bytes ($size_str)
+    {
+        switch (substr($size_str, -1))
+        {
+            case 'G': case 'g': return (int)$size_str * pow(1024, 3);
+            case 'M': case 'm': return (int)$size_str * pow(1024, 2);
+            case 'K': case 'k': return (int)$size_str * 1024;
+            default: return $size_str;
+        }
+    }
+
+    static public function deleteRecursive($path, $delete_target = false)
+    {
+        if (!file_exists($path))
+            return false;
+
+        $dir = dir($path);
+        if (!$dir) return false;
+
+        while ($file = $dir->read())
+        {
+            if ($file == '.' || $file == '..')
+                continue;
+
+            if (is_dir($path . '/' . $file))
+            {
+                if (!self::deleteRecursive($path . '/' . $file, true))
+                    return false;
+            }
+            else
+            {
+                unlink($path . '/' . $file);
+            }
+        }
+
+        $dir->close();
+        rmdir($path);
+
+        return true;
+    }
+
+    static public function plugin_url($params = [])
+    {
+        if (isset($params['id']))
+        {
+            $url = WWW_URL . 'admin/plugin/' . $params['id'] . '/';
+        }
+        else
+        {
+            $url = PLUGIN_URL;
+        }
+
+        if (!empty($params['file']))
+            $url .= $params['file'];
+
+        if (!empty($params['query']))
+        {
+            $url .= '?';
+            
+            if (!(is_numeric($params['query']) && (int)$params['query'] === 1) && $params['query'] !== true)
+                $url .= $params['query'];
+        }
+
+        return $url;
+    }
+
+    static public function find_csv_delim($fp)
+    {
+        $line = '';
+
+        while ($line === '' && !feof($fp))
+        {
+            $line = trim(fgets($fp, 4096));
+        }
+        
+        // Delete the columns content
+        $line = preg_replace('/".*?"/', '', $line);
+
+        $delims = [
+            ';' => substr_count($line, ';'),
+            ',' => substr_count($line, ','),
+            "\t"=> substr_count($line, "\t")
+        ];
+
+        arsort($delims);
+        reset($delims);
+
+        rewind($fp);
+        return key($delims);
+    }
+
+}
diff --git a/include/libs/countries/countries_en.php b/include/libs/countries/countries_en.php
new file mode 100644 (file)
index 0000000..de27ae3
--- /dev/null
@@ -0,0 +1,261 @@
+<?php
+
+// This list states the country names (official short names in English)
+// in alphabetical order as given in ISO 3166-1 and the corresponding
+// ISO 3166-1-alpha-2 code elements.
+
+// Updated on 13/12/2011 00:39
+
+$countries = array (
+  'AF' => 'Afghanistan',
+  'AX' => 'Åland Islands',
+  'AL' => 'Albania',
+  'DZ' => 'Algeria',
+  'AS' => 'American Samoa',
+  'AD' => 'Andorra',
+  'AO' => 'Angola',
+  'AI' => 'Anguilla',
+  'AQ' => 'Antarctica',
+  'AG' => 'Antigua And Barbuda',
+  'AR' => 'Argentina',
+  'AM' => 'Armenia',
+  'AW' => 'Aruba',
+  'AU' => 'Australia',
+  'AT' => 'Austria',
+  'AZ' => 'Azerbaijan',
+  'BS' => 'Bahamas',
+  'BH' => 'Bahrain',
+  'BD' => 'Bangladesh',
+  'BB' => 'Barbados',
+  'BY' => 'Belarus',
+  'BE' => 'Belgium',
+  'BZ' => 'Belize',
+  'BJ' => 'Benin',
+  'BM' => 'Bermuda',
+  'BT' => 'Bhutan',
+  'BO' => 'Bolivia, Plurinational State Of',
+  'BQ' => 'Bonaire, Sint Eustatius And Saba',
+  'BA' => 'Bosnia And Herzegovina',
+  'BW' => 'Botswana',
+  'BV' => 'Bouvet Island',
+  'BR' => 'Brazil',
+  'IO' => 'British Indian Ocean Territory',
+  'BN' => 'Brunei Darussalam',
+  'BG' => 'Bulgaria',
+  'BF' => 'Burkina Faso',
+  'BI' => 'Burundi',
+  'KH' => 'Cambodia',
+  'CM' => 'Cameroon',
+  'CA' => 'Canada',
+  'CV' => 'Cape Verde',
+  'KY' => 'Cayman Islands',
+  'CF' => 'Central African Republic',
+  'TD' => 'Chad',
+  'CL' => 'Chile',
+  'CN' => 'China',
+  'CX' => 'Christmas Island',
+  'CC' => 'Cocos (keeling) Islands',
+  'CO' => 'Colombia',
+  'KM' => 'Comoros',
+  'CG' => 'Congo',
+  'CD' => 'Congo, The Democratic Republic Of The',
+  'CK' => 'Cook Islands',
+  'CR' => 'Costa Rica',
+  'CI' => 'CÔte D\'ivoire',
+  'HR' => 'Croatia',
+  'CU' => 'Cuba',
+  'CW' => 'CuraÇao',
+  'CY' => 'Cyprus',
+  'CZ' => 'Czech Republic',
+  'DK' => 'Denmark',
+  'DJ' => 'Djibouti',
+  'DM' => 'Dominica',
+  'DO' => 'Dominican Republic',
+  'EC' => 'Ecuador',
+  'EG' => 'Egypt',
+  'SV' => 'El Salvador',
+  'GQ' => 'Equatorial Guinea',
+  'ER' => 'Eritrea',
+  'EE' => 'Estonia',
+  'ET' => 'Ethiopia',
+  'FK' => 'Falkland Islands (malvinas)',
+  'FO' => 'Faroe Islands',
+  'FJ' => 'Fiji',
+  'FI' => 'Finland',
+  'FR' => 'France',
+  'GF' => 'French Guiana',
+  'PF' => 'French Polynesia',
+  'TF' => 'French Southern Territories',
+  'GA' => 'Gabon',
+  'GM' => 'Gambia',
+  'GE' => 'Georgia',
+  'DE' => 'Germany',
+  'GH' => 'Ghana',
+  'GI' => 'Gibraltar',
+  'GR' => 'Greece',
+  'GL' => 'Greenland',
+  'GD' => 'Grenada',
+  'GP' => 'Guadeloupe',
+  'GU' => 'Guam',
+  'GT' => 'Guatemala',
+  'GG' => 'Guernsey',
+  'GN' => 'Guinea',
+  'GW' => 'Guinea-bissau',
+  'GY' => 'Guyana',
+  'HT' => 'Haiti',
+  'HM' => 'Heard Island And Mcdonald Islands',
+  'VA' => 'Holy See (vatican City State)',
+  'HN' => 'Honduras',
+  'HK' => 'Hong Kong',
+  'HU' => 'Hungary',
+  'IS' => 'Iceland',
+  'IN' => 'India',
+  'ID' => 'Indonesia',
+  'IR' => 'Iran, Islamic Republic Of',
+  'IQ' => 'Iraq',
+  'IE' => 'Ireland',
+  'IM' => 'Isle Of Man',
+  'IL' => 'Israel',
+  'IT' => 'Italy',
+  'JM' => 'Jamaica',
+  'JP' => 'Japan',
+  'JE' => 'Jersey',
+  'JO' => 'Jordan',
+  'KZ' => 'Kazakhstan',
+  'KE' => 'Kenya',
+  'KI' => 'Kiribati',
+  'KP' => 'Korea, Democratic People\'s Republic Of',
+  'KR' => 'Korea, Republic Of',
+  'KW' => 'Kuwait',
+  'KG' => 'Kyrgyzstan',
+  'LA' => 'Lao People\'s Democratic Republic',
+  'LV' => 'Latvia',
+  'LB' => 'Lebanon',
+  'LS' => 'Lesotho',
+  'LR' => 'Liberia',
+  'LY' => 'Libya',
+  'LI' => 'Liechtenstein',
+  'LT' => 'Lithuania',
+  'LU' => 'Luxembourg',
+  'MO' => 'Macao',
+  'MK' => 'Macedonia, The Former Yugoslav Republic Of',
+  'MG' => 'Madagascar',
+  'MW' => 'Malawi',
+  'MY' => 'Malaysia',
+  'MV' => 'Maldives',
+  'ML' => 'Mali',
+  'MT' => 'Malta',
+  'MH' => 'Marshall Islands',
+  'MQ' => 'Martinique',
+  'MR' => 'Mauritania',
+  'MU' => 'Mauritius',
+  'YT' => 'Mayotte',
+  'MX' => 'Mexico',
+  'FM' => 'Micronesia, Federated States Of',
+  'MD' => 'Moldova, Republic Of',
+  'MC' => 'Monaco',
+  'MN' => 'Mongolia',
+  'ME' => 'Montenegro',
+  'MS' => 'Montserrat',
+  'MA' => 'Morocco',
+  'MZ' => 'Mozambique',
+  'MM' => 'Myanmar',
+  'NA' => 'Namibia',
+  'NR' => 'Nauru',
+  'NP' => 'Nepal',
+  'NL' => 'Netherlands',
+  'NC' => 'New Caledonia',
+  'NZ' => 'New Zealand',
+  'NI' => 'Nicaragua',
+  'NE' => 'Niger',
+  'NG' => 'Nigeria',
+  'NU' => 'Niue',
+  'NF' => 'Norfolk Island',
+  'MP' => 'Northern Mariana Islands',
+  'NO' => 'Norway',
+  'OM' => 'Oman',
+  'PK' => 'Pakistan',
+  'PW' => 'Palau',
+  'PS' => 'Palestinian Territory, Occupied',
+  'PA' => 'Panama',
+  'PG' => 'Papua New Guinea',
+  'PY' => 'Paraguay',
+  'PE' => 'Peru',
+  'PH' => 'Philippines',
+  'PN' => 'Pitcairn',
+  'PL' => 'Poland',
+  'PT' => 'Portugal',
+  'PR' => 'Puerto Rico',
+  'QA' => 'Qatar',
+  'RE' => 'RÉunion',
+  'RO' => 'Romania',
+  'RU' => 'Russian Federation',
+  'RW' => 'Rwanda',
+  'BL' => 'Saint BarthÉlemy',
+  'SH' => 'Saint Helena, Ascension And Tristan Da Cunha',
+  'KN' => 'Saint Kitts And Nevis',
+  'LC' => 'Saint Lucia',
+  'MF' => 'Saint Martin (french Part)',
+  'PM' => 'Saint Pierre And Miquelon',
+  'VC' => 'Saint Vincent And The Grenadines',
+  'WS' => 'Samoa',
+  'SM' => 'San Marino',
+  'ST' => 'Sao Tome And Principe',
+  'SA' => 'Saudi Arabia',
+  'SN' => 'Senegal',
+  'RS' => 'Serbia',
+  'SC' => 'Seychelles',
+  'SL' => 'Sierra Leone',
+  'SG' => 'Singapore',
+  'SX' => 'Sint Maarten (dutch Part)',
+  'SK' => 'Slovakia',
+  'SI' => 'Slovenia',
+  'SB' => 'Solomon Islands',
+  'SO' => 'Somalia',
+  'ZA' => 'South Africa',
+  'GS' => 'South Georgia And The South Sandwich Islands',
+  'SS' => 'South Sudan',
+  'ES' => 'Spain',
+  'LK' => 'Sri Lanka',
+  'SD' => 'Sudan',
+  'SR' => 'Suriname',
+  'SJ' => 'Svalbard And Jan Mayen',
+  'SZ' => 'Swaziland',
+  'SE' => 'Sweden',
+  'CH' => 'Switzerland',
+  'SY' => 'Syrian Arab Republic',
+  'TW' => 'Taiwan, Province Of China',
+  'TJ' => 'Tajikistan',
+  'TZ' => 'Tanzania, United Republic Of',
+  'TH' => 'Thailand',
+  'TL' => 'Timor-leste',
+  'TG' => 'Togo',
+  'TK' => 'Tokelau',
+  'TO' => 'Tonga',
+  'TT' => 'Trinidad And Tobago',
+  'TN' => 'Tunisia',
+  'TR' => 'Turkey',
+  'TM' => 'Turkmenistan',
+  'TC' => 'Turks And Caicos Islands',
+  'TV' => 'Tuvalu',
+  'UG' => 'Uganda',
+  'UA' => 'Ukraine',
+  'AE' => 'United Arab Emirates',
+  'GB' => 'United Kingdom',
+  'US' => 'United States',
+  'UM' => 'United States Minor Outlying Islands',
+  'UY' => 'Uruguay',
+  'UZ' => 'Uzbekistan',
+  'VU' => 'Vanuatu',
+  'VE' => 'Venezuela, Bolivarian Republic Of',
+  'VN' => 'Viet Nam',
+  'VG' => 'Virgin Islands, British',
+  'VI' => 'Virgin Islands, U.s.',
+  'WF' => 'Wallis And Futuna',
+  'EH' => 'Western Sahara',
+  'YE' => 'Yemen',
+  'ZM' => 'Zambia',
+  'ZW' => 'Zimbabwe',
+);
+
+?>
\ No newline at end of file
diff --git a/include/libs/countries/countries_fr.php b/include/libs/countries/countries_fr.php
new file mode 100644 (file)
index 0000000..f5a131f
--- /dev/null
@@ -0,0 +1,260 @@
+<?php
+
+// Cette liste donne les noms de pays (forme courte en français) dans l'ordre
+// alphabétique comme donné dans l'ISO 3166-1 et les codes alpha-2 correspondants.
+
+// Mis à jour le 13/12/2011 00:39
+
+$countries = array (
+  'AF' => 'Afghanistan',
+  'ZA' => 'Afrique Du Sud',
+  'AX' => 'Åland, Îles',
+  'AL' => 'Albanie',
+  'DZ' => 'Algérie',
+  'DE' => 'Allemagne',
+  'AD' => 'Andorre',
+  'AO' => 'Angola',
+  'AI' => 'Anguilla',
+  'AQ' => 'Antarctique',
+  'AG' => 'Antigua-et-barbuda',
+  'SA' => 'Arabie Saoudite',
+  'AR' => 'Argentine',
+  'AM' => 'Arménie',
+  'AW' => 'Aruba',
+  'AU' => 'Australie',
+  'AT' => 'Autriche',
+  'AZ' => 'Azerbaïdjan',
+  'BS' => 'Bahamas',
+  'BH' => 'Bahreïn',
+  'BD' => 'Bangladesh',
+  'BB' => 'Barbade',
+  'BY' => 'Bélarus',
+  'BE' => 'Belgique',
+  'BZ' => 'Belize',
+  'BJ' => 'Bénin',
+  'BM' => 'Bermudes',
+  'BT' => 'Bhoutan',
+  'BO' => 'Bolivie, L\'état Plurinational De',
+  'BQ' => 'Bonaire, Saint-eustache Et Saba',
+  'BA' => 'Bosnie-herzégovine',
+  'BW' => 'Botswana',
+  'BV' => 'Bouvet, Île',
+  'BR' => 'Brésil',
+  'BN' => 'Brunei Darussalam',
+  'BG' => 'Bulgarie',
+  'BF' => 'Burkina Faso',
+  'BI' => 'Burundi',
+  'KY' => 'Caïmans, Îles',
+  'KH' => 'Cambodge',
+  'CM' => 'Cameroun',
+  'CA' => 'Canada',
+  'CV' => 'Cap-vert',
+  'CF' => 'Centrafricaine, République',
+  'CL' => 'Chili',
+  'CN' => 'Chine',
+  'CX' => 'Christmas, Île',
+  'CY' => 'Chypre',
+  'CC' => 'Cocos (keeling), Îles',
+  'CO' => 'Colombie',
+  'KM' => 'Comores',
+  'CG' => 'Congo',
+  'CD' => 'Congo, La République Démocratique Du',
+  'CK' => 'Cook, Îles',
+  'KR' => 'Corée, République De',
+  'KP' => 'Corée, République Populaire Démocratique De',
+  'CR' => 'Costa Rica',
+  'CI' => 'Côte D\'ivoire',
+  'HR' => 'Croatie',
+  'CU' => 'Cuba',
+  'CW' => 'Curaçao',
+  'DK' => 'Danemark',
+  'DJ' => 'Djibouti',
+  'DO' => 'Dominicaine, République',
+  'DM' => 'Dominique',
+  'EG' => 'Égypte',
+  'SV' => 'El Salvador',
+  'AE' => 'Émirats Arabes Unis',
+  'EC' => 'Équateur',
+  'ER' => 'Érythrée',
+  'ES' => 'Espagne',
+  'EE' => 'Estonie',
+  'US' => 'États-unis',
+  'ET' => 'Éthiopie',
+  'FK' => 'Falkland, Îles (malvinas)',
+  'FO' => 'Féroé, Îles',
+  'FJ' => 'Fidji',
+  'FI' => 'Finlande',
+  'FR' => 'France',
+  'GA' => 'Gabon',
+  'GM' => 'Gambie',
+  'GE' => 'Géorgie',
+  'GS' => 'Géorgie Du Sud-et-les Îles Sandwich Du Sud',
+  'GH' => 'Ghana',
+  'GI' => 'Gibraltar',
+  'GR' => 'Grèce',
+  'GD' => 'Grenade',
+  'GL' => 'Groenland',
+  'GP' => 'Guadeloupe',
+  'GU' => 'Guam',
+  'GT' => 'Guatemala',
+  'GG' => 'Guernesey',
+  'GN' => 'Guinée',
+  'GW' => 'Guinée-bissau',
+  'GQ' => 'Guinée Équatoriale',
+  'GY' => 'Guyana',
+  'GF' => 'Guyane Française',
+  'HT' => 'Haïti',
+  'HM' => 'Heard-et-îles Macdonald, Île',
+  'HN' => 'Honduras',
+  'HK' => 'Hong Kong',
+  'HU' => 'Hongrie',
+  'IM' => 'Île De Man',
+  'UM' => 'Îles Mineures Éloignées Des États-unis',
+  'VG' => 'Îles Vierges Britanniques',
+  'VI' => 'Îles Vierges Des États-unis',
+  'IN' => 'Inde',
+  'ID' => 'Indonésie',
+  'IR' => 'Iran, République Islamique D\'',
+  'IQ' => 'Iraq',
+  'IE' => 'Irlande',
+  'IS' => 'Islande',
+  'IL' => 'Israël',
+  'IT' => 'Italie',
+  'JM' => 'Jamaïque',
+  'JP' => 'Japon',
+  'JE' => 'Jersey',
+  'JO' => 'Jordanie',
+  'KZ' => 'Kazakhstan',
+  'KE' => 'Kenya',
+  'KG' => 'Kirghizistan',
+  'KI' => 'Kiribati',
+  'KW' => 'Koweït',
+  'LA' => 'Lao, République Démocratique Populaire',
+  'LS' => 'Lesotho',
+  'LV' => 'Lettonie',
+  'LB' => 'Liban',
+  'LR' => 'Libéria',
+  'LY' => 'Libye',
+  'LI' => 'Liechtenstein',
+  'LT' => 'Lituanie',
+  'LU' => 'Luxembourg',
+  'MO' => 'Macao',
+  'MK' => 'Macédoine, L\'ex-république Yougoslave De',
+  'MG' => 'Madagascar',
+  'MY' => 'Malaisie',
+  'MW' => 'Malawi',
+  'MV' => 'Maldives',
+  'ML' => 'Mali',
+  'MT' => 'Malte',
+  'MP' => 'Mariannes Du Nord, Îles',
+  'MA' => 'Maroc',
+  'MH' => 'Marshall, Îles',
+  'MQ' => 'Martinique',
+  'MU' => 'Maurice',
+  'MR' => 'Mauritanie',
+  'YT' => 'Mayotte',
+  'MX' => 'Mexique',
+  'FM' => 'Micronésie, États Fédérés De',
+  'MD' => 'Moldova, République De',
+  'MC' => 'Monaco',
+  'MN' => 'Mongolie',
+  'ME' => 'Monténégro',
+  'MS' => 'Montserrat',
+  'MZ' => 'Mozambique',
+  'MM' => 'Myanmar',
+  'NA' => 'Namibie',
+  'NR' => 'Nauru',
+  'NP' => 'Népal',
+  'NI' => 'Nicaragua',
+  'NE' => 'Niger',
+  'NG' => 'Nigéria',
+  'NU' => 'Niué',
+  'NF' => 'Norfolk, Île',
+  'NO' => 'Norvège',
+  'NC' => 'Nouvelle-calédonie',
+  'NZ' => 'Nouvelle-zélande',
+  'IO' => 'Océan Indien, Territoire Britannique De L\'',
+  'OM' => 'Oman',
+  'UG' => 'Ouganda',
+  'UZ' => 'Ouzbékistan',
+  'PK' => 'Pakistan',
+  'PW' => 'Palaos',
+  'PS' => 'Palestinien Occupé, Territoire',
+  'PA' => 'Panama',
+  'PG' => 'Papouasie-nouvelle-guinée',
+  'PY' => 'Paraguay',
+  'NL' => 'Pays-bas',
+  'PE' => 'Pérou',
+  'PH' => 'Philippines',
+  'PN' => 'Pitcairn',
+  'PL' => 'Pologne',
+  'PF' => 'Polynésie Française',
+  'PR' => 'Porto Rico',
+  'PT' => 'Portugal',
+  'QA' => 'Qatar',
+  'RE' => 'Réunion',
+  'RO' => 'Roumanie',
+  'GB' => 'Royaume-uni',
+  'RU' => 'Russie, Fédération De',
+  'RW' => 'Rwanda',
+  'EH' => 'Sahara Occidental',
+  'BL' => 'Saint-barthélemy',
+  'SH' => 'Sainte-hélène, Ascension Et Tristan Da Cunha',
+  'LC' => 'Sainte-lucie',
+  'KN' => 'Saint-kitts-et-nevis',
+  'SM' => 'Saint-marin',
+  'MF' => 'Saint-martin(partie Française)',
+  'SX' => 'Saint-martin (partie Néerlandaise)',
+  'PM' => 'Saint-pierre-et-miquelon',
+  'VA' => 'Saint-siège (état De La Cité Du Vatican)',
+  'VC' => 'Saint-vincent-et-les Grenadines',
+  'SB' => 'Salomon, Îles',
+  'WS' => 'Samoa',
+  'AS' => 'Samoa Américaines',
+  'ST' => 'Sao Tomé-et-principe',
+  'SN' => 'Sénégal',
+  'RS' => 'Serbie',
+  'SC' => 'Seychelles',
+  'SL' => 'Sierra Leone',
+  'SG' => 'Singapour',
+  'SK' => 'Slovaquie',
+  'SI' => 'Slovénie',
+  'SO' => 'Somalie',
+  'SD' => 'Soudan',
+  'SS' => 'Soudan Du Sud',
+  'LK' => 'Sri Lanka',
+  'SE' => 'Suède',
+  'CH' => 'Suisse',
+  'SR' => 'Suriname',
+  'SJ' => 'Svalbard Et Île Jan Mayen',
+  'SZ' => 'Swaziland',
+  'SY' => 'Syrienne, République Arabe',
+  'TJ' => 'Tadjikistan',
+  'TW' => 'Taïwan, Province De Chine',
+  'TZ' => 'Tanzanie, République-unie De',
+  'TD' => 'Tchad',
+  'CZ' => 'Tchèque, République',
+  'TF' => 'Terres Australes Françaises',
+  'TH' => 'Thaïlande',
+  'TL' => 'Timor-leste',
+  'TG' => 'Togo',
+  'TK' => 'Tokelau',
+  'TO' => 'Tonga',
+  'TT' => 'Trinité-et-tobago',
+  'TN' => 'Tunisie',
+  'TM' => 'Turkménistan',
+  'TC' => 'Turks-et-caïcos, Îles',
+  'TR' => 'Turquie',
+  'TV' => 'Tuvalu',
+  'UA' => 'Ukraine',
+  'UY' => 'Uruguay',
+  'VU' => 'Vanuatu',
+  'VE' => 'Venezuela, République Bolivarienne Du',
+  'VN' => 'Viet Nam',
+  'WF' => 'Wallis Et Futuna',
+  'YE' => 'Yémen',
+  'ZM' => 'Zambie',
+  'ZW' => 'Zimbabwe',
+);
+
+?>
\ No newline at end of file
diff --git a/include/libs/diff/class.simplediff.php b/include/libs/diff/class.simplediff.php
new file mode 100644 (file)
index 0000000..d59fe51
--- /dev/null
@@ -0,0 +1,414 @@
+<?php
+
+/*
+    Simple diff library, using diff php implementation by nils
+    Copyleft (C) 2009 BohwaZ - http://bohwaz.net/
+
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License
+    as published by the Free Software Foundation; version 3
+    of the License.
+
+    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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+
+    http://www.gnu.org/licenses/gpl.html
+*/
+
+class simpleDiff
+{
+    const SAME = 0;
+    const INS = 1;
+    const DEL = -1;
+    const CHANGED = 2;
+
+    /**
+     * Generates a normal diff (like GNU diff utility)
+     *
+     * @param string $old Old text to compare (could be an array of lines)
+     * @param string $new New text to compare (could be an array of lines)
+     * @param bool $return_as_array Returns the diff as an array
+     */
+    static public function diff($old, $new, $return_as_array = false)
+    {
+        /**
+            Diff implemented in pure php, written from scratch.
+            Copyright (C) 2003  Daniel Unterberger <diff.phpnet@holomind.de>
+            Copyright (C) 2005  Nils Knappmeier next version
+
+            This program is free software; you can redistribute it and/or
+            modify it under the terms of the GNU General Public License
+            as published by the Free Software Foundation; either version 2
+            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 General Public License for more details.
+
+            You should have received a copy of the GNU General Public License
+            along with this program; if not, write to the Free Software
+            Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+
+            http://www.gnu.org/licenses/gpl.html
+
+            About:
+            I searched a function to compare arrays and the array_diff()
+            was not specific enough. It ignores the order of the array-values.
+            So I reimplemented the diff-function which is found on unix-systems
+            but this you can use directly in your code and adopt for your needs.
+            Simply adopt the formatline-function. with the third-parameter of arr_diff()
+            you can hide matching lines. Hope someone has use for this.
+
+            Contact: d.u.diff@holomind.de <daniel unterberger>
+        **/
+
+        # split the source text into arrays of lines
+        if (is_array($old))
+            $t1 = $old;
+        else
+            $t1 = explode("\n",$old);
+
+        $x = array_pop($t1);
+        if ($x>'') $t1[]="$x\n\\ No newline at end of file";
+
+        if (is_array($new))
+            $t2 = $new;
+        else
+            $t2 = explode("\n",$new);
+
+        $x=array_pop($t2);
+        if ($x>'') $t2[]="$x\n\\ No newline at end of file";
+
+        # build a reverse-index array using the line as key and line number as value
+        # don't store blank lines, so they won't be targets of the shortest distance
+        # search
+        foreach($t1 as $i=>$x)
+        {
+            if ($x>'') $r1[$x][]=$i;
+        }
+        foreach($t2 as $i=>$x) if ($x>'') $r2[$x][]=$i;
+
+        $a1=0; $a2=0;   # start at beginning of each list
+        $actions=array();
+
+        # walk this loop until we reach the end of one of the lists
+        while ($a1<count($t1) && $a2<count($t2)) {
+            # if we have a common element, save it and go to the next
+            if ($t1[$a1]==$t2[$a2]) { $actions[]=4; $a1++; $a2++; continue; }
+
+            # otherwise, find the shortest move (Manhattan-distance) from the
+            # current location
+            $best1=count($t1); $best2=count($t2);
+            $s1=$a1; $s2=$a2;
+            while(($s1+$s2-$a1-$a2) < ($best1+$best2-$a1-$a2)) {
+                $d=-1;
+                if (isset($t2[$s2]) && isset($r1[$t2[$s2]]))
+                {
+                    foreach((array)@$r1[$t2[$s2]] as $n)
+                    {
+                        if ($n>=$s1) { $d=$n; break; }
+                    }
+                }
+                if ($d>=$s1 && ($d+$s2-$a1-$a2)<($best1+$best2-$a1-$a2))
+                { $best1=$d; $best2=$s2; }
+                $d=-1;
+                if (isset($t1[$s1]) && isset($r2[$t1[$s1]]))
+                {
+                    foreach((array)@$r2[$t1[$s1]] as $n)
+                    {
+                        if ($n>=$s2) { $d=$n; break; }
+                    }
+                }
+                if ($d>=$s2 && ($s1+$d-$a1-$a2)<($best1+$best2-$a1-$a2))
+                { $best1=$s1; $best2=$d; }
+                $s1++; $s2++;
+            }
+            while ($a1<$best1) { $actions[]=1; $a1++; }  # deleted elements
+            while ($a2<$best2) { $actions[]=2; $a2++; }  # added elements
+        }
+
+        # we've reached the end of one list, now walk to the end of the other
+        while($a1<count($t1)) { $actions[]=1; $a1++; }  # deleted elements
+        while($a2<count($t2)) { $actions[]=2; $a2++; }  # added elements
+
+        # and this marks our ending point
+        $actions[]=8;
+
+        # now, let's follow the path we just took and report the added/deleted
+        # elements into $out.
+        $op = 0;
+        $x0=$x1=0; $y0=$y1=0;
+        $out = array();
+        foreach($actions as $act) {
+            if ($act==1) { $op|=$act; $x1++; continue; }
+            if ($act==2) { $op|=$act; $y1++; continue; }
+            if ($op>0) {
+                $xstr = ($x1==($x0+1)) ? $x1 : ($x0+1).",$x1";
+                $ystr = ($y1==($y0+1)) ? $y1 : ($y0+1).",$y1";
+                if ($op==1) $out[] = "{$xstr}d{$y1}";
+                elseif ($op==3) $out[] = "{$xstr}c{$ystr}";
+                while ($x0<$x1) { $out[] = '< '.$t1[$x0]; $x0++; }   # deleted elems
+                if ($op==2) $out[] = "{$x1}a{$ystr}";
+                elseif ($op==3) $out[] = '---';
+                while ($y0<$y1) { $out[] = '> '.$t2[$y0]; $y0++; }   # added elems
+            }
+            $x1++; $x0=$x1;
+            $y1++; $y0=$y1;
+            $op=0;
+        }
+        $out[] = '';
+
+        if ($return_as_array)
+            return $out;
+        else
+            return implode("\n",$out);
+    }
+
+    /**
+     * Applies a diff to a text
+     *
+     * @param string $original Original text to patch
+     * @param string $patch Diff text
+     * @param bool $return_as_array Returns the patched text as an array
+     */
+    static public function patch($original, $patch, $return_as_array = false)
+    {
+        $new = array();
+
+        if (!is_array($patch))
+            $patch = explode("\n", $patch);
+
+        if (!is_array($original))
+            $original = explode("\n", str_replace("\r", "", $original));
+
+        $i = 0;
+        foreach ($patch as $line)
+        {
+            if (empty($line))
+                continue;
+
+            $line = str_replace("\n\\ No newline at end of file", "", $line);
+
+            if ($line[0] == '>')
+            {
+                $new[] = substr($line, 2);
+            }
+            elseif (preg_match('!^(?P<ob>[0-9]+)(?:,(?P<oe>[0-9]+))?(?P<mode>[acd])(?P<nb>[0-9]+)(?:,(?<ne>[0-9]+))?$!', trim($line), $match))
+            {
+                $sub = ($match['mode'] == 'a') ? 0 : 1;
+                for ($a = $i; $a < ($match['ob'] - $sub); $a++)
+                {
+                    $new[] = $original[$a];
+                }
+                $i = $match['oe'] ? (int) $match['oe'] : (int) $match['ob'];
+            }
+        }
+        for ($a = $i; $a < count($original); $a++)
+        {
+            $new[] = $original[$a];
+        }
+
+        return $return_as_array ? $new : implode("\n", $new);
+    }
+
+    /**
+     * Returns an array showing differences between two arrays
+     *
+     * @param string $diff Diff text, set to false and the diff will be made from $old and $new
+     * @param string $old Old text
+     * @param string $new New text, could be set to false if the diff is supplied
+     * @param bool $show_context Include context in the array? Set to false to avoid context,
+        set to true to have all the context and set to an (int) to have this number of lines of
+        context before and after each modified line
+     */
+    static public function diff_to_array($diff = false, $old, $new = false, $show_context = true)
+    {
+        if ($diff === false && $new === false)
+        {
+            throw new Exception("diff_to_array needs either the diff text or the new text file");
+        }
+
+        if ($diff === false)
+        {
+            $diff = self::diff($old, $new, true);
+        }
+
+        if (!is_array($diff))
+            $old = explode("\n", $diff);
+
+        if (!is_array($old))
+            $old = explode("\n", $old);
+
+        if ($new === false)
+            $new = self::patch($old, $diff, true);
+
+        if (!is_array($new))
+            $new = explode("\n", $new);
+
+        $left = $right = $context = array();
+        $max_lines = max(count($new), count($old));
+
+        // Creating an array of changed lines for left and right texts
+        foreach ($diff as $line)
+        {
+            if (preg_match('!^(?P<ob>[0-9]+)(?:,(?P<oe>[0-9]+))?(?P<mode>[acd])(?P<nb>[0-9]+)(?:,(?<ne>[0-9]+))?$!', trim($line), $match))
+            {
+                if (empty($match['oe']))
+                    $match['oe'] = $match['ob'];
+
+                if (empty($match['ne']))
+                    $match['ne'] = $match['nb'];
+
+                if ($match['mode'] == 'a')
+                {
+                    for ($i = $match['nb']; $i <= $match['ne']; $i++)
+                    {
+                        $right[$i - 1] = true;
+                        $max_lines++;
+                    }
+                }
+                elseif ($match['mode'] == 'd')
+                {
+                    for ($i = $match['ob']; $i <= $match['oe']; $i++)
+                    {
+                        $left[$i - 1] = true;
+                        $max_lines++;
+                    }
+                }
+                else
+                {
+                    for ($i = $match['nb']; $i <= $match['ne']; $i++)
+                    {
+                        $right[$i - 1] = true;
+                    }
+                    for ($i = $match['ob']; $i <= $match['oe']; $i++)
+                    {
+                        $left[$i - 1] = true;
+                    }
+                }
+
+                if ($show_context && $show_context !== true)
+                {
+                    $min = $match['ob'] - (int) $show_context;
+                    if ($min < 1) $min = 1;
+                    $max = $match['oe'] + (int) $show_context;
+                    if ($max > count($new)) $max = count($new);
+
+                    for ($i = $min; $i <= $max; $i++)
+                    {
+                        $context[$i - 1] = true;
+                    }
+                }
+            }
+        }
+
+        $out = array();
+
+        $left_index = 0;
+        $right_index = 0;
+        $i = 0;
+
+        // Then we can compile this to an array of changed things
+        while ($i < $max_lines)
+        {
+            $row = array();
+
+            // Line present in left but not in right ? deleted
+            if (isset($left[$left_index]) && !isset($right[$right_index]))
+            {
+                $row = array(self::DEL, $old[$left_index], '');
+                $left_index++;
+            }
+            // Line present in right but not in left ? added
+            elseif (isset($right[$right_index]) && !isset($left[$left_index]))
+            {
+                $row = array(self::INS, '', $new[$right_index]);
+                $right_index++;
+            }
+            else
+            {
+                // Changed line
+                if (isset($left[$left_index]) && isset($right[$right_index]))
+                {
+                    $row = array(self::CHANGED, $old[$left_index], $new[$right_index]);
+                }
+                // Or nothing happened
+                else
+                {
+                    // We want all the context, ok
+                    if ($show_context === true || isset($context[$left_index]))
+                    {
+                        $l = isset($old[$left_index]) ? $old[$left_index] : '';
+                        $r = isset($new[$right_index]) ? $new[$right_index] : '';
+                        $row = array(self::SAME, $l, $r);
+                    }
+                }
+
+                $right_index++;
+                $left_index++;
+            }
+
+            $i++;
+
+            if (!empty($row))
+            {
+                $out[($i - 1)] = $row;
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * Generates a word-diff, like the GNU wdiff utility (kind of)
+     *
+     * @param string $old Left right to compare
+     * @param string $new Right line to compare
+     * @param string $union Union string to assemble words (default is whitespace)
+     */
+    static public function wdiff($old, $new, $union = ' ')
+    {
+        $diff = self::diff_to_array(false, explode(' ', $old), explode(' ', $new));
+        $out = '';
+
+        foreach ($diff as $line)
+        {
+            list ($change, $old, $new) = $line;
+
+            if ($change == self::CHANGED)
+            {
+                $out .= '[-' . $old . '-]';
+                $out .= $union;
+                $out .= '{+' . $new . '+}';
+            }
+            elseif ($change == self::DEL)
+            {
+                $out .= '[-' . $old . '-]';
+            }
+            elseif ($change == self::INS)
+            {
+                $out .= '{+' . $new . '+}';
+            }
+            else
+            {
+                $out .= $old;
+            }
+
+            $out .= $union;
+        }
+
+        $out = str_replace('+}' . $union . '{+', ' ', $out);
+        $out = str_replace('-]' . $union . '[-', ' ', $out);
+
+        return $out;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/garbage2xhtml/lib.garbage2xhtml.php b/include/libs/garbage2xhtml/lib.garbage2xhtml.php
new file mode 100644 (file)
index 0000000..28e06a0
--- /dev/null
@@ -0,0 +1,868 @@
+<?php
+/*
+    Garbage2xhtml lib
+    Takes a html text and returns something semantic and maybe valid
+
+    Copyleft (C) 2006-11 BohwaZ - http://bohwaz.net/
+
+    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, version 3 of the
+    License.
+
+    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 <http://www.gnu.org/licenses/>.
+*/
+
+class Garbage_Exception extends Exception
+{
+}
+
+class garbage2xhtml
+{
+    /**
+     * Secure attributes contents?
+     * Will check for url scheme and url content in href and src
+     * It's advised to disable <script> and <style> tags and style attribute
+     * because they could be used for XSS attacks
+     */
+    public $secure = true;
+
+    /**
+     * Enclose text which is not in any element in <p> tags?
+     */
+    public $enclose_text = true;
+
+    /**
+     * Auto-add <br /> in text blocks?
+     * Will also break <p> blocks when encountering a double line break.
+     *
+     * Example:
+     *  <p>One Two
+     *
+     *      Three
+     *      Four
+     *  </p>
+     *
+     * Will render as:
+     *  <p>One Two</p>
+     *  <p>Three<br />
+     *  Four</p>
+     */
+    public $auto_br = true;
+
+    /**
+     * Text encoding (used for escaping)
+     */
+    public $encoding = 'UTF-8';
+
+    /**
+     * Remove forbidden tags from ouput?
+     * If true, "<em>" will disappear if it's not in allowed tags.
+     * If false, "<em>" will become a text node with &lt;em&gt;
+     */
+    public $remove_forbidden_tags = false;
+
+    /**
+     * Remove forbidden tags contents?
+     * If true "<b>Hello</b>" will become "" if <b> is not allowed
+     * If false "<b>Hello</b>" will become "Hello"
+     */
+    public $remove_forbidden_tags_content = false;
+
+    public $indent = true;
+
+    /**
+     * Core attributes allowed on each element
+     */
+    public $core_attributes = array('lang', 'class', 'id', 'title', 'dir');
+
+    /**
+     * Allowed block tags
+     *
+     *  'tag'   =>  true,   // Allows core attributes
+     *  'tag'   =>  false,  // Disallow core attributes
+     *  'tag'   =>  array('allowed attribute 1', 'href', 'src'),
+     *      // Allow core attributes and those specific attributes
+     */
+    public $block_tags = array(
+        'ul'    =>  true,
+        'ol'    =>  true,
+        'li'    =>  true,
+        'dl'    =>  true,
+
+        'p'     =>  true,
+        'div'   =>  true,
+
+        'h1'    =>  true,
+        'h2'    =>  true,
+        'h3'    =>  true,
+        'h4'    =>  true,
+        'h5'    =>  true,
+        'h6'    =>  true,
+
+        'pre'   =>  true,
+        'hr'    =>  true,
+        'address'   =>  true,
+        'blockquote'=>  array('cite'),
+
+        'object'=>  array('type', 'width', 'height', 'data'),
+        'iframe'=>  array('src', 'width', 'height', 'frameborder', 'scrolling'),
+
+        'table' =>  array('summary'),
+        'tbody' =>  true,
+        'thead' =>  true,
+        'tfoot' =>  true,
+        'caption'   =>  true,
+        'colgroup'  =>  array('span'),
+        'col'   =>  true,
+        'tr'    =>  true,
+        'th'    =>  array('colspan', 'rowspan', 'scope', 'headers'),
+        'td'    =>  array('colspan', 'rowspan', 'headers'),
+
+        // XHTML 5
+        'article'   =>  true,
+        'aside'     =>  true,
+        'audio'     =>  array('src', 'controls', 'loop', 'preload'),
+        'figure'    =>  true,
+        'footer'    =>  true,
+        'header'    =>  true,
+        'hgroup'    =>  true,
+        'section'   =>  true,
+        'video'     =>  array('src', 'controls', 'width', 'height', 'poster'),
+
+    );
+
+    /**
+     * Allowed inline elements
+     */
+    public $inline_tags = array(
+        // 'tag' => array of allowed attributes
+        'abbr'  =>  array('title'),
+        'dfn'   =>  true,
+        'acronym'   =>  array('title'),
+
+        'cite'  =>  true,
+        'q'     =>  array('cite'),
+
+        'code'  =>  true,
+        'kbd'   =>  true,
+        'samp'  =>  true,
+
+        'strong'=>  true,
+        'em'    =>  true,
+
+        'small' =>  true,
+
+        'del'   =>  true,
+        'ins'   =>  true,
+        'sup'   =>  true,
+        'sub'   =>  true,
+
+        'dt'    =>  true,
+        'dd'    =>  true,
+
+        'span'  =>  true,
+        'br'    =>  false,
+
+        'a'     =>  array('href', 'hreflang', 'rel'),
+        'img'   =>  array('src', 'alt', 'width', 'height'),
+
+        'param' =>  array('name', 'value', 'type'),
+
+        // XHTML 5
+        'mark'  =>  true,
+        'var'   =>  true,
+        'time'  =>  array('pubdate', 'datetime'),
+        'figcaption'=>  true,
+    );
+
+    public $allowed_url_schemes = array(
+        'http'  =>  '://',
+        'https' =>  '://',
+        'ftp'   =>  '://',
+        'mailto'=>  ':',
+        'xmpp'  =>  ':',
+        'news'  =>  ':',
+        'nntp'  =>  '://',
+        'tel'   =>  ':',
+        'callto'=>  ':',
+        'ed2k'  =>  '://',
+        'irc'   =>  '://',
+        'magnet'=>  ':',
+        'mms'   =>  '://',
+        'rtsp'  =>  '://',
+        'sip'   =>  ':',
+        );
+
+    /**
+     * Tags who need content to be enclosed
+     */
+    public $elements_need_enclose = array('blockquote', 'form', 'address', 'noscript');
+
+    /**
+     * Tags elements who accept <br /> inside
+     */
+    public $elements_allow_break = array('p', 'dd', 'dt', 'li', 'td', 'th', 'div');
+
+    /**
+     * Autoclosing tags (eg. <br />)
+     */
+    public $autoclosing_tags = array('br', 'hr', 'img', 'param');
+
+    ///////// PRIVATE PROPERTIES
+
+    private $opened = array();
+    private $matches = array();
+    private $line = 0;
+    private $check_only = false;
+
+    private $allowed_tags = array();
+
+    const SPLIT_REGEXP = '!<(/?)([^><]*)>!';
+    const ATTRIBUTE_REGEXP = '/(?:(?:"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\') | (?>[^"\'=\s]+))+|[=]/x';
+
+    public function parse($string)
+    {
+        $string = preg_replace('#<!--.*-->#Us', '', $string);
+        $string = preg_replace('#<!\[CDATA\[.*\]\]>#Us', '', $string);
+
+        $string = str_replace(array("\r\n", "\r"), "\n", $string);
+        $string = preg_replace('!<br\s*/?>!i', '<br />', $string);
+        $string = trim($string);
+
+        $this->resetInternals();
+        $this->allowed_tags = array_merge($this->inline_tags, $this->block_tags);
+
+        $this->matches = preg_split(self::SPLIT_REGEXP, $string, null, PREG_SPLIT_DELIM_CAPTURE);
+        unset($string);
+
+        $nodes = $this->buildTree();
+        $this->resetInternals();
+
+        return $nodes;
+    }
+
+    /**
+     * Checks a string validity
+     */
+    public function check($string)
+    {
+        $this->check_only = true;
+        $this->parse($string);
+        $this->check_only = false;
+        return true;
+    }
+
+    /**
+     * Processes a string
+     */
+    public function process($string)
+    {
+        $nodes = $this->parse($string);
+        unset($string);
+
+        if ($this->enclose_text)
+        {
+            $nodes = $this->encloseChildren($nodes, false);
+        }
+
+        if ($this->auto_br)
+        {
+            $nodes = $this->autoLineBreak($nodes);
+        }
+
+        return $this->outputNodes($nodes);
+    }
+
+    /**
+     * Outputs a string from a nodes array
+     */
+    public function outputNodes($nodes, $level = 0)
+    {
+        $out = '';
+
+        foreach ($nodes as $node)
+        {
+            if (is_array($node))
+            {
+                $close = '';
+                $content = '';
+                $open = '<'.$node['name'];
+
+                foreach ($node['attrs'] as $key=>$value)
+                {
+                    $open .= ' '.$key.'="'.$value.'"';
+                }
+
+                if ($this->isTagAutoclosing($node['name']))
+                {
+                    $open .= ' />';
+                }
+                else
+                {
+                    $open .= '>';
+                    $close = '</'.$node['name'].'>';
+                }
+
+                if (!empty($node['children']))
+                {
+                    $content = $this->outputNodes($node['children'], $level + 1);
+                }
+
+                if ($close && $this->indent !== false && array_key_exists($node['name'], $this->block_tags) && $node['name'] != 'pre')
+                {
+                    $tag = $this->indentTag($open, $content, $close, $level * ($this->indent === true ? 1 : (int) $this->indent));
+                }
+                else
+                {
+                    $tag = $open . $content . $close;
+
+                    if ($node['name'] == 'br')
+                        $tag.= "\n";
+                }
+            }
+            else
+            {
+                $tag = $node;
+            }
+
+            $out .= $tag;
+        }
+
+        return $out;
+    }
+
+    private function indentTag($open, $content, $close, $indent)
+    {
+        $out = "\n";
+        $out.= str_repeat(' ', $indent);
+        $out.= $open;
+        $out.= "\n";
+
+        $content = explode("\n", $content);
+
+        foreach ($content as $line)
+        {
+            if (!trim($line))
+                continue;
+
+            $out.= str_repeat(' ', $indent + ($this->indent === true ? 2 : (int) $this->indent));
+            $out.= $line . "\n";
+        }
+
+        unset($content);
+
+        $out.= str_repeat(' ', $indent);
+        $out.= $close;
+        $out.= "\n";
+
+        return $out;
+    }
+
+    private function resetInternals()
+    {
+        $this->opened = array();
+        $this->matches = array();
+        $this->line = 0;
+    }
+
+    /**
+     * Break line and paragraphs following this rule :
+     * in a paragraph : one line break = <br />,
+     *   two line breaks = closing paragraph and opening a new one
+     * in other elements : nl2br
+     */
+    private function autoLineBreak($nodes, $parent = false)
+    {
+        $n = array();
+        $nb_nodes = count($nodes);
+        $k = 0;
+
+        foreach ($nodes as $node)
+        {
+            // Text node inside an element allowing for line breaks
+            if (is_string($node) && in_array($parent, $this->elements_allow_break))
+            {
+                $matches = preg_split('!(\n+)!', $node, -1, PREG_SPLIT_DELIM_CAPTURE);
+                $i = 1;
+                $max = count($matches);
+
+                while (($line = array_shift($matches)) !== null)
+                {
+                    // Line break
+                    if ($i++ % 2 == 0)
+                    {
+                        if (!empty($n) && ($k < $nb_nodes - 1 || $i < $max))
+                        {
+                            $n[] = array('name' => 'br', 'attrs' => array(), 'children' => array());
+                        }
+                    }
+                    elseif ($line != "")
+                    {
+                        $n[] = $line;
+                    }
+                }
+            }
+            // In paragraphs we'll try to split them each two-line breaks
+            elseif (is_array($node) && $node['name'] == 'p')
+            {
+                $n[] = array('name' => 'p', 'attrs' => $node['attrs'], 'children' => array());
+                $current_node = count($n) - 1;
+
+                // Because we need to work on parent-level we will loop on children here
+                // (so we don't do recursive calls)
+                while (($child = array_shift($node['children'])) !== null)
+                {
+                    // Text node ? try to split it
+                    if (is_string($child))
+                    {
+                        $matches = preg_split('!(\n+)!', $child, -1, PREG_SPLIT_DELIM_CAPTURE);
+                        $i = 0;
+                        $max = count($matches);
+
+                        while (($line = array_shift($matches)) !== null)
+                        {
+                            if ($i++ % 2)
+                            {
+                                // More than 2 line-breaks then we create a new paragraph and we continue
+                                if (strlen($line) >= 2)
+                                {
+                                    $n[] = array('name' => 'p', 'attrs' => $node['attrs'], 'children' => array());
+                                    $current_node = count($n) - 1;
+
+                                    $nb_nodes++;
+                                    $k++;
+                                }
+                                // Simple line break
+                                // but no line break just after or before end
+                                elseif (!empty($n[$current_node]['children']) && ($i - 1 < $max || $k < $nb_nodes - 1))
+                                {
+                                    $n[$current_node]['children'][] = array('name' => 'br', 'attrs' => array(), 'children' => array());
+                                }
+                            }
+                            elseif ($line != "")
+                            {
+                                $n[$current_node]['children'][] = $line;
+                            }
+                        }
+                    }
+                    else
+                    {
+                        $n[$current_node]['children'][] = $child;
+                    }
+                }
+            }
+            else
+            {
+                if (!is_string($node) && !empty($node['children']))
+                {
+                    $node['children'] = $this->autoLineBreak($node['children'], $node['name']);
+                }
+
+                $n[] = $node;
+            }
+
+            $k++;
+        }
+
+        unset($nodes);
+        return $n;
+    }
+
+    private function getTagAttributes($value, $tag)
+    {
+        $attributes = array();
+
+        if (array_key_exists($tag, $this->allowed_tags))
+        {
+            $tag =& $this->allowed_tags[$tag];
+        }
+        elseif (preg_match('!^[a-zA-Z0-9-]+:!', $tag, $match) && array_key_exists($match[0], $this->allowed_tags))
+        {
+            $tag =& $this->allowed_tags[$match[0]];
+        }
+
+        $value = preg_replace('!^.*\s+!U', '', $value);
+
+        if (preg_match_all(self::ATTRIBUTE_REGEXP, $value, $match))
+        {
+            $state = 0;
+            $name = false;
+
+            foreach($match[0] as $value)
+            {
+                if ($state == 0)
+                {
+                    $name = strtolower((string) $value);
+                    $state = 1;
+                    $pass = false;
+
+                    // Allowed attribute ?
+                    if ($tag && in_array($name, $this->core_attributes))
+                        $pass = true;
+                    elseif (is_array($tag) && in_array($name, $tag))
+                        $pass = true;
+                    elseif (preg_match('!^(data-|[a-z0-9-]+:)!', $name, $m))
+                    {
+                        // Allow namespaces and data- (html5) attributes
+                        if ($tag && in_array($m[1], $this->core_attributes))
+                            $pass = true;
+                        elseif (is_array($tag) && in_array($m[1], $tag))
+                            $pass = true;
+                    }
+
+                    if (!$pass)
+                    {
+                        $name = false;
+                        continue;
+                    }
+                }
+                elseif ($state == 1)
+                {
+                    if ($value != '=' && $name && $this->check_only)
+                        throw new Garbage_Exception("Expecting '=' after $name on line ".$this->line);
+
+                    $state = 2;
+                }
+                elseif ($state == 2)
+                {
+                    $state = 0;
+
+                    if (!$name)
+                        continue;
+
+                    if ($value == '=' && $this->check_only)
+                        throw new Garbage_Exception("Unexpected '=' after $name on line ".$this->line);
+
+                    if ($value[0] == '"' || $value[0] == "'")
+                        $value = substr($value, 1, -1);
+
+                    $value = $this->protectAttribute($name, $value);
+
+                    $attributes[$name] = $value;
+                }
+            }
+        }
+
+        return $attributes;
+    }
+
+    private function decodeObfuscated($value)
+    {
+        // Don't try to trick me
+        $value = rawurldecode($value);
+        $value = html_entity_decode($value, ENT_QUOTES, $this->encoding);
+
+        // unicode entities don't always have a semicolon ending the entity
+        $value = preg_replace_callback('~&#x0*([0-9a-f]+);?~i', function ($match) {
+            return chr(hexdec($match[1]));
+        }, $value);
+        
+        $value = preg_replace_callback('~&#0*([0-9]+);?~', function ($match) {
+            return chr($match[1]);
+        }, $value);
+
+        return $value;
+    }
+
+    private function protectAttribute($name, $value)
+    {
+        if (!$this->secure)
+            return $str;
+
+        if ($name == 'src' || $name == 'href')
+        {
+            $value = self::decodeObfuscated($value);
+
+            // parse_url already have some tricks against XSS
+            $url = parse_url($value);
+            $value = '';
+
+            if (!empty($url['scheme']))
+            {
+                $url['scheme'] = strtolower($url['scheme']);
+
+                if (!array_key_exists($url['scheme'], $this->allowed_url_schemes))
+                    return '';
+
+                $value .= $url['scheme'] . $this->allowed_url_schemes[$url['scheme']];
+            }
+
+            if (!empty($url['host']))
+            {
+                $value .= $url['host'];
+            }
+
+            if (!empty($url['path']))
+            {
+                $value .= $url['path'];
+            }
+
+            if (!empty($url['query']))
+            {
+                // We can't use parse_str and build_http_string to sanitize url here
+                // Or else we'll get things like ?param1&param2 transformed in ?param1=&param2=
+                $query = explode('&', $url['query']);
+
+                foreach ($query as &$item)
+                {
+                    $item = explode('=', $item);
+
+                    if (isset($item[1]))
+                        $item = rawurlencode(rawurldecode($item[0])) . '=' . rawurlencode(rawurldecode($item[1]));
+                    else
+                        $item = rawurlencode(rawurldecode($item[0]));
+                }
+
+                $value .= '?' . $this->escape(implode('&', $query));
+            }
+
+            if (!empty($url['fragment']))
+            {
+                $value .= '#' . $url['fragment'];
+            }
+        }
+        else
+        {
+            $value = str_replace('&amp;', '&', $value);
+            $value = $this->cleanEntities($value);
+            $value = $this->escape($value);
+        }
+
+        return $value;
+    }
+
+    private function getTagName($value)
+    {
+        $value = trim($value);
+
+        if (preg_match('!^([a-zA-Z0-9-]+)(?:[:]([a-zA-Z0-9-]+))?!', $value, $match))
+        {
+            if (!empty($match[2]) && array_key_exists($match[1] . ':', $this->allowed_tags))
+                return $match[0];
+            elseif (array_key_exists($match[0], $this->allowed_tags))
+                return $match[0];
+        }
+
+        return false;
+    }
+
+    private function isTagAutoclosing($tag)
+    {
+        if (in_array($tag, $this->autoclosing_tags))
+            return true;
+
+        if (preg_match('!^[a-zA-Z0-9-]+:!', $tag, $match) && in_array($match[0], $this->autoclosing_tags))
+            return true;
+
+        return false;
+    }
+
+    /**
+     * Build HTML tree
+     */
+    private function buildTree()
+    {
+        $i = 0;
+        $nodes = array();
+        $closing = false;
+        $in_forbidden_tag = false;
+
+        while (($value = array_shift($this->matches)) !== null)
+        {
+            // Line count
+            $this->line += (int) substr_count($value, "\n");
+
+            switch ($i++ % 3)
+            {
+                // Text node
+                case 0:
+                {
+                    if ($value != "" && !$this->check_only
+                        && !($in_forbidden_tag && $this->remove_forbidden_tags_content))
+                    {
+                        $nodes[] = $this->escape($value);
+                    }
+                    break;
+                }
+
+                // Next iteration is closing tag (probably ?)
+                case 1:
+                {
+                    $closing = ($value == '/');
+                    break;
+                }
+
+                // Tag itself
+                case 2:
+                {
+                    $tag = $this->getTagName($value);
+
+                    // Self-closing tag
+                    if (substr($value, -1, 1) == '/' || $this->isTagAutoclosing($tag))
+                    {
+                        $value = preg_replace('!\s*/$!', '', $value);
+
+                        // Dismis un-authorized tag
+                        if (!$tag)
+                        {
+                            if ($this->check_only)
+                                throw new Garbage_Exception("Un-authorized tag <$value>");
+
+                            if (!$this->remove_forbidden_tags)
+                                $nodes[] = '&lt;'.$this->escape($value).' /&gt;';
+
+                            $in_forbidden_tag = false;
+
+                            continue;
+                        }
+
+                        if (!$this->check_only)
+                        {
+                            $nodes[] = array(
+                                'name'  =>  $tag,
+                                'attrs' =>  $this->getTagAttributes($value, $tag),
+                                'children'=> array(),
+                            );
+                        }
+                    }
+                    // Closing tag
+                    else if ($closing)
+                    {
+                        // Dismis un-authorized tag
+                        if (!$tag)
+                        {
+                            if (!$this->remove_forbidden_tags)
+                                $nodes[] = '&lt;/'.$this->escape($value).'&gt;';
+
+                            continue;
+                        }
+
+                        $open = array_pop($this->opened);
+
+                        // Uh-oh parse error !
+                        // We could try to just dismiss tag errors or repair dirty HTML but
+                        // it's too complicated. Just write valid xHTML.
+                        if ($value != $open)
+                        {
+                            if ($this->check_only)
+                                throw new Garbage_Exception("Tag <$value> closed, which is not open, on line ".$this->line);
+                        }
+
+                        return $nodes;
+                    }
+                    // Opening tag
+                    else
+                    {
+                        if (!$tag)
+                        {
+                            if ($this->check_only)
+                                throw new Garbage_Exception("Invalid tag <$value>");
+
+                            if (!$this->remove_forbidden_tags)
+                                $nodes[] = '&lt;'.$this->escape($value).'&gt;';
+
+                            $in_forbidden_tag = true;
+
+                            continue;
+                        }
+
+                        if (!$this->check_only)
+                        {
+                            $node = array(
+                                'name'  =>  $tag,
+                                'attrs' =>  $this->getTagAttributes($value, $tag),
+                                'children'=> array(),
+                            );
+                        }
+
+                        $this->opened[] = $tag;
+
+                        if ($this->check_only)
+                        {
+                            $this->buildTree();
+                        }
+                        else
+                        {
+                            // Build child tree
+                            $node['children'] = $this->buildTree();
+
+                            // You need to enclose text in paragraphs in some tags
+                            // (Yes, read the XHTML spec)
+                            $node['children'] = $this->encloseChildren($node['children'], $node['name']);
+
+                            $nodes[] = $node;
+                        }
+                    }
+                }
+            }
+        }
+
+        return $nodes;
+    }
+
+    /**
+     * Enclose sub elements which need to be enclosed
+     */
+    private function encloseChildren($children, $parent)
+    {
+        if (!empty($children) && (in_array($parent, $this->elements_need_enclose) || !$parent))
+        {
+            $n = array();
+            $open = false;
+
+            while (($child = array_shift($children)) !== NULL)
+            {
+                if (is_string($child) || !array_key_exists($child['name'], $this->block_tags))
+                {
+                    if ($open === false)
+                    {
+                        $open = count($n);
+                        $n[$open] = array('name' => 'p', 'attrs' => array(), 'children' => array());
+                    }
+
+                    $n[$open]['children'][] = $child;
+                }
+                else
+                {
+                    $open = false;
+
+                    $n[] = $child;
+                }
+            }
+
+            $children = $n;
+            unset($n, $open, $child);
+        }
+
+        return $children;
+    }
+
+    public function escape($str)
+    {
+        $out = htmlspecialchars($str, ENT_QUOTES, $this->encoding, false);
+
+        if (empty($out) && !empty($str))
+        {
+            throw new Garbage_Exception("Encoding error.");
+        }
+
+        return $out;
+    }
+
+    /**
+     * Clean entities
+     */
+    private function cleanEntities($str)
+    {
+        return preg_replace('/&amp;(#[0-9a-fx]+|[a-z]+);/i', '&\\1;', $str);
+    }
+}
+
+?>
diff --git a/include/libs/miniskel/class.miniskel.php b/include/libs/miniskel/class.miniskel.php
new file mode 100644 (file)
index 0000000..86540e8
--- /dev/null
@@ -0,0 +1,754 @@
+<?php
+/*
+    MiniSkel - Flexible content-template parser
+    Based on SPIP Skeletons, see http://www.spip.net/
+    Developed by BohwaZ - http://bohwaz.net/
+
+    * December 2007 - Initial release
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/**
+ * This class manages technical and general exceptions
+ */
+class miniSkelException extends Exception
+{
+    protected $tpl_filename = '';
+
+    public function __construct($msg, $file='')
+    {
+        $this->tpl_filename = $file;
+        parent::__construct($msg);
+    }
+
+    public function getTemplateFilename()
+    {
+        return $this->tpl_filename;
+    }
+}
+
+/**
+ * This class only manages markup exceptions
+ */
+class miniSkelMarkupException extends miniSkelException
+{
+}
+
+class miniSkel
+{
+    /**
+     * The path where templates belongs
+     */
+    public $template_path = './';
+
+    /**
+     * You can change the name of the loop tag, by default it's BOUCLE, to be compatible with SPIP syntax
+     * be warned that your old templates using <BOUCLE...> syntax will not work anymore
+     */
+    public $loopTagName = 'BOUCLE';
+
+    /**
+     * You can change the name of short loop tags, by default it's B (like B in BOUCLE)
+     */
+    public $loopShortTagName = 'B';
+
+    /**
+     * As by default the loop keywords are in french, you can change them here
+     */
+    public $loopKeywords = array(
+        'orderBy'   =>  'par',
+        'orderDesc' =>  'inverse',
+        'begin'     =>  'debut',
+        'random'    =>  'hasard',
+        'duplicates'=>  'doublons',
+        'unique'    =>  'unique',
+    );
+
+    public $includeTagName = 'INCLURE';
+
+    /**
+     * Throw exceptions for warnings ? (bad criterias, modifiers that don't exists, etc.)
+     */
+    public $strictMode = true;
+
+    /**
+     * For internal use : name of the current loop
+     */
+    protected $currentLoop = "Unknown";
+
+    /**
+     * For internal use : file name of current template
+     */
+    protected $currentTemplate = '';
+
+    /**
+     * For internal use : avoid kloops and bad templates
+     */
+    protected $parentLoopLevel = 0;
+    protected $loopCounter = 0;
+
+    /**
+     * Internal global variables, like in smarty's assign
+     */
+    protected $variables = array();
+
+    /**
+     * Here we save for each loop the variables they have
+     * (It's for pre and post optional content of conditional variables)
+     * Like here : [#NAME, (#ADDRESS)]
+     */
+    protected $loopVariables = array();
+
+    /**
+     * External modifiers, like in smarty
+     */
+    protected $modifiers = array();
+
+    /**
+     * Criteria actions
+     */
+    const ACTION_ORDER_BY = 1;
+    const ACTION_ORDER_DESC = 2;
+    const ACTION_AVOID_DUPLICATES = 3;
+    const ACTION_LIMIT = 4;
+    const ACTION_MATCH_FIELD = 5;
+    const ACTION_MATCH_FIELD_BY_VALUE = 6;
+    const ACTION_MATCH_FIELD_BY_REGEXP = 7;
+    const ACTION_MATCH_FIELD_NOT_BY_REGEXP = 8;
+    const ACTION_MATCH_FIELD_IN = 9;
+    const ACTION_DISPLAY_SEPARATOR = 10;
+
+    /**
+     * Loop content types
+     */
+    const LOOP_CONTENT = 1;
+    const PRE_CONTENT = 2;
+    const POST_CONTENT = 3;
+    const ALT_CONTENT = 4;
+
+    /**
+     * Variables context (inside or outside a loop)
+     */
+    const CONTEXT_IN_LOOP = 1;
+    const CONTEXT_GLOBAL = 2;
+    const CONTEXT_IN_ARG = 3;
+    const CONTEXT_IN_PRE = 4;
+    const CONTEXT_IN_POST = 5;
+
+    /**
+     * Replace first occurence of string
+     */
+    protected function replaceFirst($search, $replace, $subject)
+    {
+        $pos = strpos($subject, $search);
+
+        if ($pos !== false)
+        {
+            $subject = substr_replace($subject, $replace, $pos, strlen($search));
+        }
+
+        return $subject;
+    }
+
+    /**
+     * Internal parsing of common loop criterias (tries to be compatible with SPIP syntax)
+     * You can't extend this method
+     *
+     * @param string $criteria The unparsed criteria
+     */
+    private function parseCriteria($criteria)
+    {
+        $criteria = trim($criteria);
+
+        // {inverse} -> ORDER BY ... DESC
+        if (strtolower($criteria) == $this->loopKeywords['orderDesc'])
+        {
+            return array(
+                'action'    =>  self::ACTION_ORDER_DESC,
+            );
+        }
+        // {doublons} -> avoid duplicates in a page
+        elseif (preg_match('/^('.$this->loopKeywords['duplicates'].'|'.$this->loopKeywords['unique'].')\s*([a-z0-9_-]+)?$/i', $criteria))
+        {
+            return array(
+                'action'    =>  self::ACTION_AVOID_DUPLICATES,
+                'name'      =>  isset($match[2]) ? $match[2] : false,
+            );
+        }
+        // {par id_article} -> ORDER BY id_article
+        elseif (preg_match('/^'.$this->loopKeywords['orderBy'].'\s+([a-z0-9_-]+)$/i', $criteria, $match))
+        {
+            return array(
+                'action'    =>  self::ACTION_ORDER_BY,
+                'field'     =>  $match[1],
+            );
+        }
+        // {0,10} -> LIMIT 0,10
+        elseif (preg_match('/^([0-9]+),([0-9]+)$/', $criteria, $match))
+        {
+            return array(
+                'action'    =>  self::ACTION_LIMIT,
+                'begin'     =>  (int) $match[1],
+                'number'    =>  isset($match[2]) ? (int) $match[2] : false,
+            );
+        }
+        // begin_list,20 -> LIMIT {$_GET['begin_list']},20
+        elseif (preg_match('/^('.$this->loopKeywords['begin'].'_[a-z0-9_-]+)(,([0-9]+))?$/i', $criteria, $match))
+        {
+            if (isset($_REQUEST[$match[1]]))
+            {
+                $begin = (int) $_REQUEST[$match[1]];
+            }
+            else
+            {
+                $begin = $match[1];
+            }
+
+            if (isset($match[2]) && isset($match[3]))
+            {
+                $number = (int) $match[3];
+            }
+            else
+            {
+                $number = false;
+            }
+
+            return array(
+                'action'    =>  self::ACTION_LIMIT,
+                'begin'     =>  $begin,
+                'number'    =>  $number,
+            );
+        }
+        // {id_article} -> WHERE id_article = "{$id_article}" (???)
+        elseif (preg_match('/^([a-z0-9_-]+)$/i', $criteria, $match))
+        {
+            return array(
+                'action'    =>  self::ACTION_MATCH_FIELD,
+                'field'     =>  $match[1],
+            );
+        }
+        // {id_article=5} -> WHERE id_article = 5
+        elseif (preg_match('/^([a-z0-9_-]+)\s*(>=|<=|=|!=|>|<)\s*"?(.*?)"?$/i', $criteria, $match))
+        {
+            return array(
+                'action'    =>  self::ACTION_MATCH_FIELD_BY_VALUE,
+                'field'     =>  $match[1],
+                'comparison'=>  $match[2],
+                'value'     =>  $match[3],
+            );
+        }
+        // {titre==^France} -> WHERE id_article REGEXP "^France"
+        elseif (preg_match('/^([a-z0-9_-]+)\s*(==|!==)\s*"?(.+)"?$/i', $criteria, $match))
+        {
+            return array(
+                'action'    =>  ($match[2] == '==') ? self::ACTION_MATCH_FIELD_BY_REGEXP : self::ACTION_MATCH_FIELD_NOT_BY_REGEXP,
+                'field'     =>  $match[1],
+                'value'     =>  $match[3],
+            );
+        }
+        // {pays IN "Japon", "France"} -> WHERE pays IN "Japon", "France"
+        elseif (preg_match('/^([a-z0-9_-]+)\s+IN\s+(.+)$/i', $criteria, $match))
+        {
+            $content = explode(',', $match[2]);
+            $values = array();
+
+            foreach ($content as $item)
+            {
+                $item = preg_replace('/^["\']?(.*)["\']?$/', '\\1', $item);
+                $values[] = $item;
+            }
+
+            unset($content);
+
+            return array(
+                'action'    =>  self::ACTION_MATCH_FIELD_IN,
+                'field'     =>  $match[1],
+                'values'    =>  $values,
+            );
+        }
+        // {"<br />"} -> Inserts a <br /> between each loop iteration
+        elseif (preg_match('/^"(.+)"$/', $criteria, $match))
+        {
+            return array(
+                'action'    =>  self::ACTION_DISPLAY_SEPARATOR,
+                'value'     =>  $match[1],
+            );
+        }
+        else
+        {
+            throw new miniSkelMarkupException("Unknown criteria '".$criteria."' in ".$this->currentLoop." loop.", $this->currentTemplate);
+
+            return $criteria;
+        }
+    }
+
+    /**
+     * Internal parsing of loops (tries to be compatible with SPIP)
+     * You can't extend this method
+     *
+     * @param string $content
+     * @param string $parentLoop
+     */
+    private function parseLoops($content, $parentLoop=false)
+    {
+        if ($parentLoop)
+        {
+            $this->parentLoopLevel++;
+
+            // This is a security to keep your server cool
+            if ($this->parentLoopLevel > 10)
+            {
+                throw new miniSkelException("Too many imbricated loops !", $this->currentTemplate);
+            }
+        }
+
+        while (preg_match('/<'.$this->loopTagName.'([_-][.a-z0-9_-]+|[0-9]+)\s*\(([a-z0-9_-]+)\)\s*(\{.*?\})*>/Ui', $content, $match))
+        {
+            if ($this->loopCounter > 100)
+            {
+                throw new miniSkelException("Too many loops for one template !", $this->currentTemplate);
+            }
+
+            $loopCounter = 0;
+            $loopName = $match[1];
+            $loopType = strtolower($match[2]);
+            $loopTag = $match[0];
+
+            $loopContent = false;
+            $preContent = false;
+            $postContent = false;
+            $altContent = false;
+
+            $this->currentLoop = $loopName;
+
+            $loopCriterias = array();
+
+            if (!empty($match[3]))
+            {
+                preg_match_all('/\{(.*)\}/U', $match[3], $match, PREG_SET_ORDER);
+
+                foreach ($match as $item)
+                {
+                    $loopCriterias[] = $this->parseCriteria($item[1]);
+                }
+            }
+
+            if (preg_match('/<\/'.$this->loopTagName.$loopName.'>/i', $content, $match_end))
+            {
+                $loopTagEnd = $match_end[0];
+            }
+            else
+            {
+                throw new miniSkelMarkupException("Loop tag ".$loopName." is not closed properly.", $this->currentTemplate);
+            }
+
+            unset($match, $match_end);
+
+            $loopB = strpos($content, $loopTag);
+            $loopE = strpos($content, $loopTagEnd);
+
+            $tagB = $loopB;
+            $tagE = $loopE + strlen($loopTagEnd);
+
+            if ($loopB > $loopE)
+            {
+                throw new miniSkelMarkupException("Loop tag ".$loopName." was closed before it was opened ?!", $this->currentTemplate);
+            }
+
+            // Extract the loop content
+            $loopContent = substr($content, $loopB + strlen($loopTag), $loopE - $loopB - strlen($loopTag));
+
+            // The things before the loop (if any)
+            $loopShortTagName = '<'.$this->loopShortTagName.$loopName.'>';
+            $preB = strpos($content, $loopShortTagName);
+
+            if ($preB > $loopB)
+            {
+                throw new miniSkelMarkupException("Can't open ".$loopShortTagName." after ".$loopTag."...", $this->currentTemplate);
+            }
+
+            if ($preB !== false)
+            {
+                $preContent = substr($content, $preB + strlen($loopShortTagName), $tagB - $preB - strlen($loopShortTagName));
+                $tagB = $preB;
+            }
+            unset($preB, $loopShortTagName);
+
+            // After the loop (if any)
+            $loopShortEndTagName = '</'.$this->loopShortTagName.$loopName.'>';
+            $postE = strpos($content, $loopShortEndTagName);
+
+            if ($postE !== false && $postE < $loopE)
+            {
+                throw new miniSkelMarkupException("Can't close ".$loopShortEndTagName." before ".$loopTagEnd."...", $this->currentTemplate);
+            }
+
+            if ($postE !== false)
+            {
+                $postContent = substr($content, $tagE, $postE - $tagE);
+                $tagE = $postE + strlen($loopShortEndTagName);
+            }
+            unset($postE, $loopShortEndTagName);
+
+            // alternative
+            $loopAltTagName = '<//'.$this->loopShortTagName.$loopName.'>';
+            $altE = strpos($content, $loopAltTagName);
+
+            if ($altE !== false && $altE < $tagE)
+            {
+                throw new miniSkelMarkupException("Can't close ".$loopAltTagName." before ".$loopTagEnd."...", $this->currentTemplate);
+            }
+
+            if ($altE !== false)
+            {
+                $altContent = substr($content, $tagE, $altE - $tagE);
+                $tagE = $altE + strlen($loopAltTagName);
+            }
+
+            unset($loopShortEndTagName, $loopAltTagName, $loopShortTagName, $loopB, $loopE, $altE, $postE, $preB);
+
+            $tagContent = $this->processLoop($loopName, $loopType, $loopCriterias,
+                $loopContent, $preContent, $postContent, $altContent);
+
+            $content = substr($content, 0, $tagB) . $tagContent . substr($content, $tagE);
+
+            unset($altContent, $postContent, $preContent, $loopContent, $tagContent, $tagB, $tagE);
+
+            $this->loopCounter++;
+            $this->currentLoop = false;
+        }
+
+        if ($parentLoop)
+        {
+            $this->currentLoop = $parentLoop;
+            $this->parentLoopLevel--;
+        }
+
+        return $content;
+    }
+
+    /**
+     * Internal parsing of variables
+     * You can't extend this method
+     *
+     * @param string $content
+     * @param array $variables
+     * @param int $context (Constant)
+     */
+    protected function parseVariables($content, $variables=false, $context=self::CONTEXT_IN_LOOP)
+    {
+        // This is used for parsing variables in pre or post-content of variables
+        if (!$variables && $context != self::CONTEXT_IN_LOOP && !empty($this->loopVariables))
+        {
+            $variables = $this->loopVariables;
+        }
+
+        preg_match_all(
+            '!(\[([^\[\]]*)\(#([A-Z_]+)(\*)?(\|([^\)]+)*)*\)([^\[\]]*)\]|#([A-Z_-]+))!', $content, $match, PREG_SET_ORDER);
+
+        foreach ($match as $item)
+        {
+            $tagName = !empty($item[3]) ? strtolower($item[3]) : strtolower($item[8]);
+
+            if ($tagName == 'rem')
+            {
+                // discard comments
+                $content = $this->replaceFirst($item[0], '', $content);
+                continue;
+            }
+
+            if ($variables && !array_key_exists($tagName, $variables))
+            {
+                //throw new miniSkelMarkupException("Unknow tag '".$tagName."' in loop '".$this->currentLoop."'.");
+            }
+
+            $value = isset($variables[$tagName]) ? $variables[$tagName] : false;
+            $applyDefault = empty($item[4]) ? true : false;
+            $modifiers = array();
+
+            if (!empty($item[3]))
+            {
+                $pre = trim($item[2]) ? $this->parseVariables($item[2], $variables, self::CONTEXT_IN_PRE) : $item[2];
+                $post = trim($item[7]) ? $this->parseVariables($item[7], $variables, self::CONTEXT_IN_PRE) : $item[7];
+            }
+            else
+            {
+                $pre = $post = false;
+            }
+
+            if (!empty($item[6]))
+            {
+                $modifiers = explode('|', $item[6]);
+                foreach ($modifiers as &$modifier)
+                {
+                    preg_match('/^([0-9a-z_><!=?-]+)(\{(.*)\})?$/i', $modifier, $match_mod);
+
+                    if (!isset($match_mod[1]))
+                    {
+                        throw new miniSkelMarkupException("Invalid modifier syntax: ".$modifier);
+                    }
+
+                    $modifier = array('name' => $match_mod[1], 'arguments' => array());
+
+                    if (isset($match_mod[3]))
+                    {
+                        preg_match_all('/["\']?([^"\',]+)["\']?/', $match_mod[3], $match_args, PREG_SET_ORDER);
+                        foreach ($match_args as $arg)
+                        {
+                            $arg = trim($arg[1]);
+                            $modifier['arguments'][] = $arg ? $this->parseVariables($arg, $variables, self::CONTEXT_IN_ARG) : $arg;
+                        }
+                    }
+                }
+            }
+
+            $content = $this->replaceFirst($item[0], $this->processVariable($tagName, $value, $applyDefault, $modifiers, $pre, $post, $context), $content);
+
+            unset($modifiers, $item, $match_mod, $match_args, $tagName, $applyDefault, $pre, $post, $value);
+        }
+
+        return $content;
+    }
+
+    protected function parseIncludes($content)
+    {
+        preg_match_all('/<'.$this->includeTagName.'\{(.*)\}>/U', $content, $match, PREG_SET_ORDER);
+
+        if (empty($match))
+            return $content;
+
+        foreach ($match as $m)
+        {
+            $m_args = explode(',', $m[1]);
+            $args = array();
+
+            foreach ($m_args as $m_arg)
+            {
+                $m_arg = trim($m_arg);
+                $m_arg = explode('=', $m_arg);
+                $args[trim($m_arg[0])] = isset($m_arg[1]) ? trim($m_arg[1]) : true;
+            }
+
+            $content = $this->replaceFirst($m[0], $this->processInclude($args), $content);
+        }
+
+        unset($m_arg, $args, $m, $match);
+        return $content;
+    }
+
+    /**
+     * Here we call modifiers
+     * It's just a standard method doing simple things
+     * You're encouraged to rewrite this method to suit your needs
+     */
+    protected function callModifier($name, $value, $args=false)
+    {
+        $method_name = 'variableModifier_'.$name;
+
+        // We can use internal methods as modifiers
+        if (method_exists($this, $method_name))
+        {
+            $value = $this->$method_name($value, $args);
+        }
+
+        // Are external functions or objects
+        elseif (isset($this->modifiers[$name]))
+        {
+            $value = call_user_func($this->modifiers[$name], $value, $args);
+        }
+
+        // Default is just an escape, but you can change this
+        elseif ($name == 'default')
+        {
+            $value = htmlspecialchars($value, ENT_QUOTES);
+        }
+
+        // Strict mode throw an exception here if we try to use an undefined modifier
+        elseif ($this->strictMode)
+        {
+            throw new miniSkelMarkupException("Modifier '".$name."' isn't defined in loop '".$this->currentLoop."'.");
+        }
+
+        return $value;
+    }
+
+    /**
+     * Here we process the loop
+     * This is somehow basic, but a good example
+     * You're encouraged to extend this method to suit your needs
+     */
+    protected function processLoop($loopName, $loopType, $loopCriterias, $loopContent, $preContent, $postContent, $altContent)
+    {
+        $out = '';
+
+        // We can call an internal method (use extends !) to match the loop type
+        $method_name = 'processLoopType_' . $loopType;
+
+        if (!method_exists($this, $method_name))
+        {
+            throw new miniSkelException("There is no known '".$loopType."' loop type.");
+        }
+
+        $loopContent = $this->$method_name($loopCriterias, $loopContent);
+
+        // If the loop isn't empty (!=false)
+        if ($loopContent)
+        {
+            // we put the pre-content before the loop content
+            if ($preContent)
+            {
+                $out .= $this->parse($preContent, $loopName, self::PRE_CONTENT);
+            }
+
+            $out .= $loopContent;
+
+            // we put the post-content after the loop content
+            if ($postContent)
+            {
+                $out .= $this->parse($postContent, $loopName, self::POST_CONTENT);
+            }
+        }
+
+        // If the loop is empty and we have an alternate content we show it
+        else
+        {
+            if ($altContent)
+            {
+                $out .= $this->parse($altContent, $loopName, self::ALT_CONTENT);
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * Here we process a single variable
+     * You're encouraged to extend this method to suit your needs
+     *
+     * @param string $name
+     * @param string $value
+     * @param bool $applyDefault Apply the default modifier ?
+     * @param array $modifiers Modifiers to apply
+     * @param string $pre Optional pre-content
+     * @param string $post Optional $post-content
+     * @param bool $context Variable context (may be self::CONTEXT_GLOBAL or self::CONTEXT_IN_LOOP)
+     */
+    protected function processVariable($name, $value, $applyDefault, $modifiers, $pre, $post, $context)
+    {
+        // If $value == false it seems it's not set in the variables array used in the loop,
+        // so maybe it's a global variable that we want (but you can change this)
+        if ($value === false && isset($this->variables[$name]))
+        {
+            $value = $this->variables[$name];
+        }
+
+        // The applyDefault bit is used here to apply a modifier, but you can use it for some other things
+        if ($applyDefault)
+            $value = $this->callModifier('default', $value);
+
+        // We process modifiers
+        foreach ($modifiers as &$modifier)
+        {
+            $value = $this->callModifier($modifier['name'], $value, $modifier['arguments']);
+        }
+
+        // It's important to put this here, because we can have tricky things like:
+        // [(#TITLE|orIfEmpty{"Empty title"})]
+        // where the orIfEmpty modifier will replace the $value with "Empty title" if $value is empty
+        // so $value is not empty anymore after the modifier call
+        if (empty($value))
+        {
+            return '';
+        }
+
+        $out = '';
+
+        // Getting pre-content
+        if ($pre)
+            $out .= $this->parseVariables($pre, false, $context);
+
+        $out .= $value;
+
+        // Getting post-content
+        if ($post)
+            $out .= $this->parseVariables($post, false, $context);
+
+        return $out;
+    }
+
+    /**
+     * Processing an include instruction
+     */
+    protected function processInclude($args)
+    {
+        if (empty($args))
+            throw new miniSkelMarkupException($this->includeTagName . ' requires at least an argument');
+
+        $file = key($args);
+        return $this->fetch($file);
+    }
+
+    /**
+     * Parsing a text section for loops and global variables
+     * You're encouraged to rewrite this method to suit your needs
+     *
+     * @param string $content
+     * @param string $parent The parent loop, if this function is called inside a loop
+     * @param string $content_type The content type, like self::LOOP_CONTENT and others
+     */
+    protected function parse($content, $parent=false, $content_type=false)
+    {
+        $content = $this->parseIncludes($content);
+        $content = $this->parseLoops($content, $parent);
+        $content = $this->parseVariables($content, $this->variables, self::CONTEXT_GLOBAL);
+        return $content;
+    }
+
+    /**
+     * Like in smarty we can assign global variables in the template
+     */
+    public function assign($name, $value)
+    {
+        $this->variables[$name] = $value;
+    }
+
+    /**
+     * Like in smarty we can register external modifiers
+     */
+    public function register_modifier($name, $function)
+    {
+        $this->modifiers[$name] = $function;
+    }
+
+    /**
+     * Returns the parsed template file $template
+     */
+    public function fetch($template)
+    {
+        $this->currentTemplate = $template;
+        $template = file_get_contents($this->template_path . $template);
+        return $this->parse($template);
+    }
+
+    /**
+     * Displays the parsed template file $template
+     */
+    public function display($template)
+    {
+        echo $this->fetch($template);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/passphrase/lib.passphrase.french.php b/include/libs/passphrase/lib.passphrase.french.php
new file mode 100644 (file)
index 0000000..d6737cc
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+class Passphrase
+{
+    const DEFAULT_CHARACTERS = '\pL\'-';
+    const DEFAULT_WORDS_NUMBER = 4;
+
+    static private $words = null;
+
+    static public function generate($nb_words = null, $characters = null)
+    {
+        if (is_null($nb_words))
+        {
+            $nb_words = self::DEFAULT_WORDS_NUMBER;
+        }
+
+        if (is_null($characters))
+        {
+            $characters = self::DEFAULT_CHARACTERS;
+        }
+
+        self::getDictionary();
+
+        $words = array();
+        $max = count(self::$words) - 1;
+
+        while (count($words) < $nb_words)
+        {
+            $random = self::$words[mt_rand(0, $max)];
+
+            if (preg_match('!^['.$characters.']+$!u', $random))
+            {
+                $words[] = $random;
+            }
+        }
+
+        return implode(' ', $words);
+    }
+
+    static public function getDictionary()
+    {
+        if (is_null(self::$words))
+        {
+            self::$words = array ('paysage','lui-même','erreur','pénitence','brûlant','canon','prudent','débarrasser','séparer','retomber','croix','sauter','nid','grand-maman','satisfait','défense','compter','déboucher','peintre','tentation','devant','envie','pleuvoir','fièvre','organiser','persévérer','ivre','quart','pourvu que','tilleul','barrière','police','pareil','invention','fer','guerre','cordialement','campagnard','infirmier','crépuscule','manier','animal','transporter','mobile','perfection','marque','joncher','nouveau','désespoir','joindre','cordonnier','ruelle','risquer','janvier','ménage','couronne','fusil','loi','page','clou','armée','flacon','blanc','adresser','maître','caverne','rentrée','constant','montre','surface','ruban','cause','division','note','luisant','poche','poursuite','gazon','ingratitude','pré','rose','activité','joyeux','fâcher','préoccuper','obligeance','rivière','vache','dessin','but','fond','deux','ceinture','lorsque','cantique','tâcher','expirer','aviser','gibecière','lourd','espiègle','sommet','carte','vendeur','opérer','disparaître','rire','agréer','visite','précédent','simplement','dorer','sagesse','revivre','manteau','croûte','intelligence','lion','chêne','horreur','grandeur','saluer','mauvais','sentier','estomac','modeste','nullement','hâter','millier','saigner','curé','moineau','fuite','flaque','crêpe','gant','s\'emparer','gare','gentil','sacrifice','modérer','brutal','feuille','pouvoir','dossier','clef','terrain','quarante','actif','fou','réconforter','gendarme','ignorant','grotte','habiller','stationner','implorer','chapitre','soupir','chaise','rarement','application','s\'écrier','ressource','ministre','doyen','jeu','graisse','chantre','écho','barre','clouer','bourgeon','équipage','commun','obliger','berceau','outil','coquille','procureur','germer','séminaire','partout','agréablement','énorme','radieux','retrousser','sorte','billet','mademoiselle','condamner','aviateur','tourbillonner','boucher','fumer','décembre','rang','étouffer','sens','parcours','tablier','pleur','bluet','cesse','papier','bonté','boucle','avantageux','tranche','coller','jeune','drapeau','gaieté','chemin','deviner','auparavant','orée','passage','souffle','agir','s\'envoler','apparition','hors','président','deuil','berger','merveilleux','clin d\'oeil','priver','oser','mariage','forêt','règne','commode','finir','bosquet','mouche','tailleur','essuyer','téléphone','couronner','château','tourbillon','citoyen','neveu','paisible','messager','ci-joint','ignorer','sûrement','tache','dompteur','préférence','maison','revêtir','vicaire','vendredi','frapper','chef','bâiller','seigneur','appuyer','livre','complètement','motif','adjectif numéral','train','ruisselet','arrêter','danse','blottir','raisin','raconter','dahlia','épauler','réel','morceau','étudier','amical','mouchoir','libre','printanier','voiture','journal','musée','village','charge','pâquerette','mari','ombrage','attention','choc','rapprocher','prévoir','bienveillance','suprême','sirène','ceci','giroflée','gouverner','fraîcheur','circuler','mélancolie','exactement','désireux','commandement','annoncer','cueillette','duc','désobéissant','chauffeur','dessert','murmurer','exclamation','formidable','tant','acclamer','son','désaltérer','soeur','fourrer','industrie','embarquer','bienfaiteur','mine','joyeusement','fidèle','grès','dictionnaire','nuage','miroir','conscience','équilibre','tarder','verdoyant','parvenir','boue','refuser','manuel','grand','réchauffer','succéder','bloc','front','père','employé','moissonneur','là','majesté','réclamer','auquel','général','obscurcir','commerce','singulier','consolation','passager','mais','événement','administrer','coucher','rapidité','respirer','quai','empereur','farine','préserver','glisser','ver','sourd','notaire','citer','débris','habile','qualité','inerte','football','abri','miel','franchement','essai','production','errer','ah','idéal','agile','tarte','léger','raisonnable','projeter','poésie','massif','pommier','ensoleillé','couvent','sauver','invitation','métier','poussière','école','fuir','pleurer','imaginer','sautiller','édifier','multitude','commerçant','plat','autel','toujours','craindre','tuer','cuisinière','ingrat','percher','concert','doubler','trésor','cimetière','exercice','savon','éblouir','éclat','parc','peau','compléter','escalier','sous','matinée','recherche','régner','épreuve','autrefois','fauteuil','apprendre','pétale','sujet','lequel','bien-être','chauffage','nerveux','décision','participer','gronder','aussi','tombe','éternité','rejeter','enveloppe','habitude','peut-être','accabler','confrère','prêtre','parfaitement','soigneusement','compliment','d\'après','chéri','oiseau','coureur','mentir','faucheur','héros','arrêt','graver','intrigué','plaine','combler','planche','recouvrir','imprudence','brebis','poule','spectacle','semblable','anticiper','mener','hommage','déshabiller','moins','volume','poulet','main','imposant','fendre','paille','résistance','écrivain','franchise','casquette','mien','heure','an','oranger','coucou','plomb','télégramme','amende','part','combat','applaudir','rédaction','exécution','largement','unique','jardinier','belge','réserver','colère','lueur','moyen','coiffer','guérir','échec','minute','conseiller','fontaine','scier','fabrique','oisillon','amicalement','oreille','laisser','gosse','tard','autoriser','emmener','anxiété','patron','abbé','désobéissance','bazar','destinée','plume','terrasse','potager','dessus','régulièrement','distinguer','roseau','malade','éteindre','pendule','imperméable','fougère','pelage','écriture','journalier','comprendre','manoeuvre','soudain','dessous','parmi','aire','bosselé','goutte','bateau','veiller','économiser','intelligent','ferveur','avertir','repousser','ville','convenable','farce','cultiver','provenir','acharner','pic','avril','frontière','patte','merci','prince','précieux','espoir','allumette','poumon','bleu','inutile','proposer','être','attester','couvert','trou','démontrer','effroyable','localité','reverdir','trop','user','ours','locomotive','ailleurs','fournir','piste','béret','profondément','respecter','lecture','congrès','ennuyer','lendemain','indigne','eux','détail','compagne','vérité','capital','bavarder','grue','sueur','urgent','habituel','fragile','ôter','briller','border','souper','marteau','agent','moisson','mou','repentir','disperser','commande','situation','atteindre','musique','voyage','remettre','écharpe','fruit','éclaircir','particulier','ardent','foi','bercer','papa','marbre','guérison','alcoolique','apaiser','marguerite','hôpital','séance','bousculer','aube','cordial','quelque','chaque','difficilement','union','inquiétude','filleul','ami','explication','toux','limpide','jeter','réellement','inquiéter','activer','capable','résister','reconnaissance','troupe','aussitôt','niveau','jeunesse','meunier','signature','consulter','bout','périr','amateur','livrer','rapide','engloutir','usage','nombreux','sagement','tapis','grossier','bourgmestre','demoiselle','prodiguer','limite','clochette','fortune','échanger','inquiet','parapluie','quelconque','propos','enquête','réduire','chance','énergique','source','dent','voilà','féroce','tasse','envers','capitaine','contrée','obstacle','île','plier','moderne','allemand','muet','introduction','annuel','habitation','jambe','mouton','résoudre','ferrer','éclater','sein','naufrage','galerie','foule','soulier','voûte','toutefois','fourrure','marquis','punir','féliciter','discussion','défiler','dédaigner','tailler','tomber','clé','côté','broder','encore','jambon','poulailler','habiter','chauffer','normal','aventurer','professeur','approuver','peur','automobile','conduire','ardeur','vôtre','centime','emporter','fois','moquer','éducation','fourmillière','pieux','copier','étinceler','regagner','trait','esclave','géant','attrait','allonger','rude','santé','bébé','décharger','obscur','commercial','battre','partir','parure','mesure','encombrer','reflet','reproche','recueillir','cathédrale','illustrer','chiffre','timbre','courir','climat','genre','daigner','tente','gauche','imagination','chérir','guichet','bulletin','classique','poursuivre','renseigner','boisson','ranimer','sabot','chute','brillant','volaille','grandiose','orgueil','brun','plaintif','moqueur','bruit','jadis','achat','baigner','accourir','inscrire','plancher','religieux','joue','avec','accident','figurer','surtout','buis','courageusement','moine','employer','baguette','foudre','signaler','succès','parfumer','découvrir','bête','sien','corniche','renoncer','relever','mérite','résonner','type','tournée','préparation','sillon','étable','relation','chasseur','cas','description','porc','pourrir','blesser','favori','puisque','laid','calculer','veine','chameau','indifférent','dégager','crucifix','faine','renouveler','principe','crayon','revenir','frémir','élégant','secret','parce que','opinion','mélanger','colonel','utiliser','soin','cinquante','sonore','écouler','tel','colline','seulement','futur','expédier','tuile','chapeau','mouiller','fureur','grâce','consentement','fervent','paletot','vêtir','manger','étude','ménager','proie','héroïque','pli','canal','lin','effectuer','condoléances','savoir','instructif','azuré','cristal','plus','chemise','jouer','immobile','achever','quatrième','serviteur','spécial','science','rechercher','alentours','mont','soyeux','cruel','service','jugement','négligence','oie','mortel','voyager','fourneau','gâteau','début','ciseaux','carrefour','mobilier','gambader','arroser','imiter','redevenir','rapporter','gaz','inviter','fort','hardi','passé','riche','oeuvre','souterrain','créature','tendre','souvent','ralentir','agrément','entasser','faute','scintiller','peiner','froid','traiter','gorge','remarquable','veston','princesse','peinture','basse','provision','poteau','guider','plusieurs','série','mûr','affectueux','refuge','salaire','innocent','véhicule','style','consacrer','gardien','retrouver','coupable','charmer','créateur','renard','panorama','couteau','rêver','confectionner','appartement','traîner','astre','appétit','invisible','détester','raide','réponse','ceux','creuser','carré','aujourd\'hui','ficelle','trottoir','moulin','glacer','vêtement','hâte','appeler','mission','renouvellement','dernier','missionnaire','fille','gratitude','juillet','vieil','déchaîner','commettre','attraper','parsemer','quantité','gazouiller','blancheur','foie','satin','serein','salut','connaître','novembre','avenue','curiosité','porte-plume','merveille','chaland','article','geler','mansarde','alcool','monstre','chanter','océan','ressort','sifflet','renoncule','entrée','balle','monter','effort','introduire','juin','vaillant','valoir','voisin','eh','dresser','dortoir','sobre','médecin','répéter','problème','négliger','bicyclette','couche','facile','visiteur','aise','assiette','aurore','sincère','magnifique','estimer','durée','accorder','reculer','café','fameux','serviette','ainsi','voix','protéger','circulation','agréable','communier','chrysanthème','animation','végétation','justement','cochon','courant','légume','nation','table','jardin','extérieur','attentif','important','noeud','décrire','vivant','merle','vermeil','forger','comparer','redoubler','forcer','incliner','bien-aimé','préau','ériger','gras','dentelle','rayon','régime','foncer','avant','célébrer','générosité','perle','envoi','sonnette','suffire','crever','réfectoire','indication','paternel','ronronner','péril','second','grappe','courageux','lierre','cuire','cousin','souhaiter','trembler','pas','patronage','mur','mâchoire','paresse','garde','ronce','courber','furieux','dedans','brouillard','expression','promener','intention','bille','perte','déception','situer','bonne','désormais','azur','roulotte','public','gerbe','crème','isoler','séjour','repas','donc','madame','vernir','corridor','périlleux','fauvette','encourir','position','fin','vide','jusque','agrémenter','autorité','pauvre','plage','enfouir','marché','vraiment','signifier','portière','griffe','minuscule','plonger','messe','impossibilité','ouvrage','paix','ambulance','petit','vacances','corriger','engager','humble','longuement','pratiquer','jaunir','salutation','dérober','bouche','nécessaire','causer','dessein','rouler','huit','villa','façonner','malgré','sang','alors','silencieusement','nettoyer','dévouement','conseil','tunnel','parent','fauve','cabane','distraction','opération','fruitier','poupée','crier','tapage','soir','sot','sentiment','prolonger','grimper','pur','produit','satisfaction','rustique','chambre','excuse','rouge','protection','désolation','étendre','crise','pourtant','communiant','enseignement','ample','physique','négligent','lanterne','recommencer','conserver','chaux','menteur','suffisant','vivre','abondant','étagère','douter','glace','liberté','docile','pensionnat','debout','abattre','clairon','noircir','victoire','épuiser','tiroir','chevalier','considérer','cuiller','réunion','reste','depuis','cortège','heureusement','panache','poignée','brin','peser','lettre','naissance','réunir','lâcher','venir','rassurer','porte','resplendir','taper','associer','doute','promettre','sourire','surmonter','bienveillant','chaleur','nièce','flatter','régaler','moelleux','surprise','écurie','danger','distinction','offrir','travail','exposer','colis','flot','quotidien','permettre','ennemi','invoquer','voler','artiste','attrister','linge','tombeau','répondre','donner','transmettre','août','seul','planter','acquitter','lire','horizon','splendeur','amour','kilomètre','jardinage','ornement','charme','vers','mineur','tempête','circonstance','maint','caresse','oeillet','dimension','gâter','immaculé','veille','baisser','peuplier','rouleau','lilas','lumineux','laborieux','gland','écrire','flocon','parole','cuillère','favoriser','composition','gravure','nature','tantôt','dominer','boire','fouet','spécialement','coquelicot','fourniture','lampe','grange','pétrir','port','retraite','douloureux','amusement','bibelot','enlever','clément','sabre','lutter','pluie','musicien','pie','phrase','aventure','perspective','solide','vertu','refaire','apostolique','orgue','quinze','réciter','bordure','débarquer','cycliste','randonnée','renvoyer','épargner','volonté','savoureux','tous','essayer','aliment','haleine','côte','humide','obtenir','complet','onduler','ressentir','crime','pigeon','vapeur','chaume','cirque','chiffon','ferraille','ramasser','s\'éloigner','cent','manche','planer','affreux','jurer','trimestre','triomphe','pénible','grelotter','rigole','commander','épine','suivre','gourmand','confier','hurler','joujou','dîner','dévorer','plafond','parler','enfermer','avion','bienheureux','titre','lien','peine','aucun','riant','dont','admirable','triompher','conférence','tiède','mourir','diriger','terminer','prononcer','acier','splendide','image','histoire','céleste','brigand','résultat','dos','foin','rayonner','venger','ensuite','patience','ébats','allégresse','rideau','malin','flatteur','luire','robuste','ardoise','sacoche','menton','bassin','mai','doux','date','bonbon','dévouer','enflammer','goût','instituteur','habit','lier','sympathie','naître','marraine','animer','pourquoi','chausser','communication','mouvement','sève','aubépine','vif','clos','bal','hasard','trouble','ménagère','scolaire','marche','prouver','comble','couver','théâtre','affliger','écouter','contrarier','lieu','étoile','frotter','four','caprice','fixe','puissant','ménagerie','croître','culotte','tort','pelouse','maudire','confus','occuper','tournant','angoisse','fièrement','exécuter','commission','servir','poil','catastrophe','selon','bord','chose','vaisselle','rage','louer','étirer','énormément','entre','juge','poutre','blanchir','fréquent','las','mériter','inconvénient','fréquenter','bourgeonner','demi','voie','centre','chevelure','raccourcir','affiche','découper','succulent','impression','architecte','blouse','procurer','faner','utilité','vigne','bêche','vain','reprise','chair','secouer','anxieux','recommander','onze','velours','moindre','sillonner','cité','évangile','duvet','quatre','farouche','noyer','si','caillou','manoeuvrer','négociant','chou','royaume','attirer','missel','rougir','limiter','flanc','dommage','dehors','difficile','litière','intime','penser','huile','savant','souhait','piété','calmer','faiblesse','bénir','préparatif','pinson','goûter','membre','écarter','défendre','wagon','gamin','pain','insigne','valeur','recommandation','butiner','hanneton','redoutable','consoler','appartenir','soupe','mettre','hausser','chocolat','aligner','gracieux','haillon','niche','bourdonner','peindre','briser','quoi','attaquer','nuit','paraître','lisière','cesser','recevoir','sport','maire','marin','violence','usine','survenir','meule','proprement','saint','possession','cinq','dessiner','quant','falloir','politesse','trente','fier','épaule','embaumer','dès','abaisser','pécher','noir','accrocher','tas','vie','interdire','progrès','sacrement','rusé','orgueilleux','courrier','déjà','au-dessus','guetter','pâte','vagabond','plante','rôle','avoine','voleur','craquement','taquiner','affectueusement','portée','grain','porteur','buisson','toile','tressaillir','intellectuel','mordre','bière','dette','apprêter','camion','net','bravo','groupe','espace','aimable','épée','dur','menuisier','précaution','volée','orphelin','accepter','estrade','bambin','célèbre','bétail','déborder','univers','tremper','oeuf','toit','bec','coiffure','hauteur','filer','bouleverser','réserve','talus','expédition','fatiguer','annonce','acte','vendre','samedi','appliquer','conviction','ranger','pan','camarade','terrestre','quelquefois','poli','voiler','dépendre','directeur','unir','suspendre','paradis','dormir','décorer','cadet','plan','saut','national','gémir','gloire','terme','indiquer','mélodie','exactitude','tacher','douzaine','répartir','propice','distance','région','mendier','parterre','flèche','idée','bref','confondre','couler','verre','oncle','étalage','familier','fumée','désirer','boutique','boîte','industriel','corde','veuf','refléter','gaiement','arrondissement','refermer','lèvre','banque','tableau','s\'écrouler','instant','pardon','turbulent','somme','chrétien','rompre','vol','concours','enfin','renaître','loup','envelopper','commune','bondir','barbe','paître','outre','corbeille','exposition','fleurir','pension','pays','brusquement','âne','vue','soulever','recourir','coussin','avantage','balancer','cigarette','nouvelle','charité','pitié','suffisamment','secourir','cela','long','longer','trouver','doucement','passant','demander','réalité','demeure','queue','procession','fondre','aisément','bonheur','respect','changement','aiguille','vaste','centaine','transformer','prospérité','sacrifier','prochain','geste','lointain','flamand','tenter','commencement','là-bas','diamant','prier','propriété','hirondelle','nécéssité','continuellement','fatigue','rive','travailleur','kermesse','quelqu\'un','solitude','sursauter','salir','évidemment','vieillard','cadeau','office','acquérir','péniblement','environner','grille','grammaire','végétal','pipe','fête','semaine','profondeur','délicat','détacher','retour','souffrir','supporter','gouvernement','barque','lambeau','seuil','étranger','froisser','tourment','d\'abord','personnel','prudence','remède','intéresser','étudiant','manque','jacinthe','villageois','renfermer','égarer','herbe','poire','armoire','présent','prétendre','joli','signer','plaindre','offre','sucer','ressembler','maladie','tandis que','caresser','couleur','électricité','plaisir','bras','tonneau','bruyant','proclamer','couture','bienvenue','cage','calvaire','connaissance','tenir','propre','confesser','degré','maintenant','droit','lancer','gelée','reconnaissant','ancien','colonne','nord','maussade','talent','contempler','fermer','vélo','ni','garantir','résigner','brut','blond','reporter','vite','aisance','gêner','blé','forge','nourrir','barquette','abord','teinte','pardessus','ravir','emploi','étage','sauf','frêle','prêt','lièvre','créer','pâture','extrême','victime','tendresse','rue','inconnu','possible','croquer','encre','anglais','chasser','rester','charbonnage','sinistre','carnet','effrayer','myosotis','fouetter','expliquer','écorce','ravage','sublime','revue','entretien','géographie','boucler','gravement','quel','or','lis','écolier','dégât','taire','insister','onde','supplier','chariot','mécanique','baiser','vouloir','fossé','mois','porter','exercer','puis','poulain','illusion','sécurité','marier','gîte','tapisser','domestique','amer','étincelant','garnir','providence','espérer','cartable','fonds','alouette','ébranler','estime','soleil','valise','entourer','insecte','armer','sortir','jouir','éclore','mécontent','loyal','primaire','contenu','généralement','persuader','infini','anniversaire','fenêtre','action','forgeron','agiter','fortement','réveil','accomplir','disposition','ordinairement','embellir','mesurer','arme','souci','graine','soirée','robe','proverbe','manifester','pantalon','dictée','bouleau','illuminer','fêter','relatif','certes','élever','mort','natal','drôle','point','modèle','exister','voeu','beauté','admirer','redresser','par','cours','varier','envahir','content','retenir','amuser','s\'efforcer','obéir','pondre','logis','avouer','museau','parti','grandir','promeneur','enfance','autour','flamme','durer','adversaire','préférer','retirer','informer','mardi','terreur','étonner','aimer','tricot','entrer','consentir','carrière','aérer','réaliser','régiment','renverser','foire','immédiatement','chien','accompagner','traitement','inondation','combien','épargne','détruire','faible','champ','aigu','arranger','monument','baptême','punition','abandonner','rez-de-chaussée','troupeau','sale','rien','afin','famille','agitation','tabac','coupe','fillette','sud','carton','file','habituer','triste','catholique','sévèrement','permission','match','retentir','fabriquer','communal','défunt','rare','remporter','jour','période','sec','labeur','lenteur','débattre','montant','bouger','joie','sac','demeurer','muraille','sage','facilement','bas','abîme','attentivement','tuyau','munir','tricoter','raison','nègre','morne','accueil','exquis','lisse','apporter','bise','emballer','examiner','américain','réformer','admettre','servante','droite','occasion','église','éléphant','garniture','établir','récolter','hésiter','avance','compassion','égard','sensible','emplacement','montrer','docteur','retourner','comme','acheter','étincelle','pont','zèle','déterminer','continuer','vieillesse','attribuer','enfoncer','partager','course','rond','trancher','tourmenter','ravissant','migrateur','odorant','s\'empresser','malheureux','dimanche','barreau','cependant','drap','haine','importer','attachement','sacré','croire','discuter','plumier','bouton','araignée','romain','groseillier','diminuer','convertir','saisir','interroger','garder','atelier','respiration','chaudement','distribution','collège','société','tromper','roue','réfugier','patin','remuer','mignon','corbeau','statue','perdrix','croiser','cygne','exciter','peu','nager','remords','découverte','demande','paquet','perche','meuble','pis','ferme','froment','symbole','tellement','examen','sable','art','rattraper','charbon','gonfler','monde','correspondance','soumettre','entendre','cerise','entraîner','misérable','admiration','imprimer','établissement','brumeux','bureau','crèche','tirelire','infirme','fils','sinon','mille','oui','charrette','troisième','viser','arrière','empressement','péché','acheminer','grêle','coton','extraire','maintenir','chacun','placer','avancer','soutenir','preuve','réjouir','provoquer','couvercle','tulipe','étourdi','postal','dépenser','dater','produire','percer','reprocher','émouvoir','cache-cache','lait','mémoire','bonsoir','étaler','volontiers','ouate','tigre','naturellement','davantage','richesse','avaler','brèche','serrer','conformément','paisiblement','marchandise','vigueur','caisse','darder','principalement','racine','cueillir','bouquet','ruine','baptiser','épouvantable','cadran','arriver','aboyer','rôder','reconnaître','chaussure','apôtre','attendre','incident','violent','fromage','muscle','lutte','pratique','ange','propreté','studieux','malle','bossu','femme','repos','reconduire','spectateur','accord','observation','grouper','ruisseler','figure','charmant','vitesse','époux','familial','âme','honorer','enterrement','monnaie','éclabousser','sapin','désespérer','juger','opposer','disputer','menu','arbuste','lot','bourrasque','supérieur','puissance','cuivre','payer','papillon','échantillon','pièce','composer','incendie','parcourir','patrie','calcul','apprécier','timide','orage','labourer','appel','vierge','chasse','recours','embrasser','salle','cadre','voile','million','moitié','feuillage','haut','puits','augmenter','juste','derrière','blessure','midi','calendrier','clair','hiver','fleur','magique','touffu','violette','libérer','caractère','choix','plumage','remonter','étonnement','impatiemment','fixer','proposition','banc','trois','irriter','coffre','regret','brise','divin','endormir','précéder','allée','noisette','tourner','carotte','mère','boulevard','faire','règle','témoin','rame','affectionner','sans','adresse','chant','cou','hermine','tordre','chevet','panier','fleuve','argent','silence','manquer','bourgeois','siège','replier','masse','cime','dépens','excuser','actuel','mal','apercevoir','tribunal','renouveau','domicile','abeille','cher','dépêcher','malheur','condition','riz','passion','sermon','pointe','arrivée','impatient','sérieusement','brave','soif','joueur','muguet','carrousel','accueillir','continuel','contraire','dicter','vanter','régulier','sain','encrier','vallée','canne','tendrement','imprudent','décéder','précipiter','boule','portefeuille','réparer','universel','pâtisserie','démarche','neiger','souriant','branche','narcisse','étrange','eau','clown','parrain','jeudi','février','moudre','fil','affaire','salon','sauvage','rôti','ouverture','poudre','repasser','perroquet','mare','rameau','extrémité','disparition','réception','coudre','pardonner','central','observer','dépouiller','celle-ci','absolument','amitié','inférieur','tâche','profit','illustre','honorable','assembler','sergent','faciliter','pupitre','politique','changer','basse-cour','spacieux','officier','délaisser','exemple','trouer','accuser','descente','beau','chaîne','saison','vérifier','deuxième','femelle','abîmer','tristement','portrait','entrouvrir','défenseur','redouter','représenter','parages','endosser','excellent','bénédiction','combattre','récréation','coup','horrible','ramage','constater','képi','paroisse','tranquille','décider','rencontre','broyer','maigre','paire','ombre','matière','obscurité','scène','envier','chanson','signal','exprimer','monotone','s\'absenter','septembre','utile','enrichir','matin','catéchisme','aumône','marron','taille','canot','projet','parfum','congé','imposer','départ','mars','vitre','sucre','semer','coin','tartine','sommeil','infiniment','raccommoder','hypocrite','intérêt','tournoyer','prendre','adoucir','averse','monseigneur','pompier','givre','méchant','stupéfaction','aboutir','genêt','orange','malette','hangar','ordonner','crainte','fiancé','épi','clarté','meilleur','prêcher','brochure','gagner','calme','sur','hier','dangereux','pois','défaire','convenir','indispensable','vainqueur','fabrication','prière','prisonnier','aîné','gigantesque','vente','s\'évanouir','envoyer','charger','pinceau','pousser','retard','autant','distrait','arrondir','simplicité','pâle','assez','céder','difficulté','généreux','perdre','mousse','franc','redescendre','siècle','contribuer','excursion','marchander','hygiène','délivrer','esprit','dix','objet','néanmoins','besoin','diable','verser','éprouver','marcher','nul','transport','vrai','dame','exaucer','présence','quatorze','étroit','épanouir','commandant','force','adroit','rouiller','réveiller','conduite','mélancolique','communion','sursaut','borne','conquérir','clocher','étoffe','heurter','confiture','épais','gris','honteux','quartier','dépasser','ronde','distribuer','facteur','asseoir','rencontrer','méthode','absence','rappeler','constituer','sort','enfant','grossir','mât','seau','offenser','fatal','honneur','hérissé','prêter','différence','soulager','traverser','pourvoir','seconde','adorer','sévir','odeur','colorer','malheureusement','charitable','nu','mouvoir','agacer','loisir','supposer','oh','tricolore','appui','approcher','institut','bûcheron','laver','religion','respectueux','avoir','chercher','flamber','savourer','tortue','déclarer','éclair','honnête','temps','danser','brusque','même','particulièrement','humeur','durant','plupart','réussir','barrage','maman','cacher','cheval','absolu','modestie','nuisible','pencher','tour','peupler','plateau','grippe','quitter','lever','régler','coffret','celui-ci','obéissant','délice','sortie','vivement','vin','osier','enthousiasme','firmament','déplacer','prairie','enterrer','costume','cave','acide','trajet','manière','discours','dizaine','bandit','empêcher','poêle','profession','mélodieux','relativement','cheminée','soupirer','convaincre','faucher','adopter','peuple','été','méditer','pénétrer','passer','rassembler','trace','façade','façon','attaque','apparence','solitaire','voici','échelle','époque','immense','fâcheux','ici','principal','étang','bonnet','kilogramme','français','mendiant','aborder','automne','lunette','marchand','doigt','air','conséquence','acheteur','haie','bâton','probablement','digne','bleuet','sécher','enseigner','mêler','tige','interruption','coûter','noix','poirier','os','redire','âgé','vilain','contact','pourpre','douceur','lac','ruiner','aisé','contenir','lundi','langage','espérance','direction','température','brique','sûr','cinéma','raser','sel','primevère','douze','charlatan','remarquer','entreprendre','parfait','mer','regarder','bientôt','piquer','dérouler','économie','balayer','autrui','homme','évêque','sitôt','échapper','minuit','vingt','orner','défaut','colonial','couvrir','cuir','chaud','canard','rafraîchir','couper','harmonieux','poids','bourse','purifier','différent','amusant','très','marronnier','pensionnaire','bienfaisant','médaille','navire','comte','oeil','interrompre','miette','charrue','mauve','violet','guêpe','endroit','souverain','former','administration','aide','bizarre','désigner','tonnerre','hache','aspirer','bougie','rêve','franchir','bain','regard','journée','cerisier','pendre','touffe','attente','ténèbres','prodigieux','songer','forme','révéler','verdure','monsieur','intérieur','cri','plein','appétissant','soldat','sévère','écluse','local','beaucoup','genou','coq','saule','race','géranium','roi','souiller','pomme','privation','botte','large','bâtir','rejoindre','thé','question','devoir','terre','client','noble','sombre','installer','valet','habileté','presser','risque','lit','mirer','remerciement','rocher','élargir','désastre','impossible','misère','moment','coutume','loin','bon','frayeur','cloche','pâlir','cadavre','honte','carreau','jaillir','souvenir','vulgaire','hôtel','dispenser','inspecteur','prévenir','élève','mieux','treize','profond','suc','parer','pâtre','déployer','laine','condisciple','lys','influence','déchirer','après-midi','inonder','occasionner','surveiller','déranger','disposer','transformation','verdâtre','giboulée','piano','face','allure','écrit','ton','matinal','remise','vent','paresseux','flotter','grave','creux','autrement','plainte','ruche','hêtre','louange','alerte','récompense','intervenir','rossignol','visage','six','coude','visible','parquet','mètre','perpétuel','pavé','solliciter','forestier','exhaler','don','directement','équipe','choeur','témoigner','faveur','différer','friandise','ennuyeux','mince','bois','gaufre','tôt','bond','tronc','année','tenue','emplir','rosier','moustache','bonjour','pied','humanité','injure','roux','gravir','descendre','domaine','pêcher','souple','voisinage','ça','faim','reposer','premier','élancer','argenter','liste','égal','propriétaire','subitement','salade','sincèrement','serviable','avenir','concerner','arbre','déménager','correction','car','cabine','griffer','poing','aiguiser','corps','ligue','nommer','sol','destination','lutin','métal','assidu','voyageur','attacher','successivement','transparent','abondamment','cercle','bijou','sifflement','encourager','souffler','machine','atmosphère','parfois','royal','ramener','canif','nez','détourner','boeuf','soigner','chèvre','dans','auteur','décourager','aveugle','fréquemment','soigneux','douleur','langue','sentir','assurer','tracer','mystérieux','bâtiment','lent','certain','nombre','flûte','taillis','désagréable','bourdonnement','mensonge','cérémonie','larme','constamment','importance','allumer','fée','bouder','poste','chat','récompenser','ciel','ivoire','amener','vénérer','quoique','autre','cultivateur','ventre','filet','s\'enfuir','faux','halte','embarras','brume','vigoureux','escalader','dissiper','miracle','brouter','gratter','exact','légèrement','instruction','photographie','exprès','classe','cour','compte','longueur','laboureur','protecteur','fonction','sou','s\'agenouiller','favorable','dire','loger','toilette','balançoire','résolution','jonquille','dénudé','nom','satisfaire','plutôt','brûler','court','pour','corolle','hameau','subir','majestueux','chagrin','également','habitant','cirer','jaune','merveilleusement','émerveiller','campagne','réflexion','relire','affection','enchanté','distraire','boulangerie','considérable','mystère','ordinaire','soulagement','coeur','lumière','glissant','interpeller','neige','silencieux','refrain','ouvrir','récolte','lune','éclatant','jaloux','besogne','tête','surprendre','rapidement','vague','extraordinaire','personnage','foyer','bouteille','grive','tante','remarque','tout','matériel','espèce','finalement','garçon','betterave','débiter','voir','rhume','gymnastique','multicolore','serrure','bien','craquer','suivant','démolir','guère','entièrement','poitrine','conter','bruyamment','écraser','ballon','effacer','délicieux','courage','retardataire','contenter','ennui','rosée','gibier','nappe','carabine','déjeuner','centimètre','tram','chef-d\'oeuvre','photographier','sept','sembler','toucher','exiger','lasser','présenter','surgir','suave','grincer','cuisine','contre','fermier','calice','verger','arracher','quand','fleurette','approche','remplacer','souris','pendant','non','place','civil','hélas','destiner','bibliothèque','éternel','reprendre','consister','élan','imprévu','encadrer','lors','mot','liquide','désert','gazouillement','personne','attarder','boueux','fouiller','entonner','devenir','double','abuser','simple','frissonner','désordre','voltiger','communiquer','instruire','chanteur','fourmi','banquier','bonhomme','fait','rendez-vous','angle','écureuil','remercier','pin','renseignement','pointu','dernièrement','cheveu','rat','conte','électrique','dépôt','octobre','promesse','boulanger','ensemble','émotion','rendre','ordre','poète','couloir','plaie','actuellement','tramway','fraise','arbitre','aller','conclure','auprès','auberge','abriter','travailler','volet','bataille','frère','occupation','sérieux','assaut','rater','intéressant','instrument','intense','chapelet','divers','frais','couverture','neuf','lui','mélange','soie','plaire','grenier','désoler','soutien','paysan','vaincre','tambour','visiter','compliquer','prime','maîtresse','sanglot','ravin','printemps','certainement','éveiller','remplir','état','impatience','déplaire','environ','confiance','banane','seize','moyenne','vider','velouté','pensée','refroidir','brindille','morale','profiter','pot','assister','téléphoner','poser','affairé','claquer','entier','dieu','suite','quinzaine','plaque','fléau','solennel','vitrine','rigoureux','leçon','adieu','détour','collection','gai','lieue','construire','demain','sincérité','âge','horloge','avis','apparaître','trompette','lugubre','rapport','abondance','anneau','programme','viande','pêcheur','coquet','lamentable','lentement','inspirer','ronger','témoignage','lapin','épouser','poussin','longtemps','entretenir','lécher','pittoresque','terrier','chaussée','murmure','comment','procéder','répandre','tissu','cahier','houille','corne','entrevoir','jamais','véritable','menacer','éclairer','laitier','reparaître','casser','pêche','palais','marquer','tinter','humidité','compagnon','tranquillement','acclamation','commencer','représentant','exemplaire','étrennes','bande','prix','facilité','rapiécer','conversation','multiple','embarrasser','pompe','prompt','tirer','comparaison','vert','compagnie','gros','prise','yeux','vice','cerf','ligne','poisson','choisir','excellence','troubler','hôte','absent','camp','mode','reine','près','vieux','trotter','désobéir','justice','proche','glissoire','heureux','route','étendue','jouet','affaiblir','prison','poireau','veau','aile','mûrir','militaire','moral','boiteux','tristesse','chaumière','préparer','partie','éviter','rider','secours','clairière','existence','vêpres','rétablir','grenouille','bagage','construction','représentation','après','sonner','balcon','torrent','revoir','montagne','réfléchir','aider','posséder','bienfait','expérience','feu','ivresse','naturel','gentiment','épouvanter','développer','récit','souffrance','regretter','combattant','grand-père','siffler','entrain','superbe','station','moteur','nourriture','ajouter','duquel','presque','beurre','atteler','guide','cendre','rentrer','signe','pierre','numéro','effet','maternel','promenade','travers','chapelle','énergie','ouvrier','grand-mère','humain','milieu','gens','tacheter','ruisseau','leur','vase','précisément','désir','singe','diviser','soupçonner','terrible','curieux','déposer','traîneau','culture','mercredi','goal','chez','magasin');
+        }
+
+        return self::$words;
+    }
+
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/svgplot/lib.svgpie.php b/include/libs/svgplot/lib.svgpie.php
new file mode 100644 (file)
index 0000000..b049704
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+
+class SVGPie
+{
+       protected $width = null;
+       protected $height = null;
+       protected $data = array();
+       protected $title = null;
+       protected $legend = true;
+
+       public function __construct($width = 600, $height = 400)
+       {
+               $this->width = (int) $width;
+               $this->height = (int) $height;
+       }
+
+       public function add(SVGPie_Data $data)
+       {
+               $this->data[] = $data;
+               return true;
+       }
+
+       public function setTitle($title)
+       {
+               $this->title = $title;
+               return true;
+       }
+
+       public function toggleLegend()
+       {
+               $this->legend = !$this->legend;
+       }
+
+       public function display()
+       {
+               header('Content-Type: image/svg+xml');
+               echo $this->output();
+       }
+
+       public function output()
+       {
+               $out = '<?xml version="1.0" encoding="utf-8" standalone="no"?>' . PHP_EOL;
+               $out.= '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/SVG/DTD/svg10.dtd">' . PHP_EOL;
+               $out.= '<svg width="'.$this->width.'" height="'.$this->height.'" viewBox="0 0 '.$this->width.' '.$this->height.'" xmlns="http://www.w3.org/2000/svg" version="1.1">' . PHP_EOL;
+
+               $circle_size = min($this->width, $this->height);
+               $cx = $circle_size / 2;
+               $cy = $this->height / 2;
+               $circle_size *= 0.98;
+               $radius = $circle_size / 2;
+
+               if (count($this->data) == 1)
+               {
+                       $row = current($this->data);
+                       $out .= "<circle cx=\"{$cx}\" cy=\"{$cy}\" r=\"{$radius}\" fill=\"{$row->fill}\" "
+                               .       "stroke=\"white\" stroke-width=\"".($circle_size * 0.005)."\" stroke-linecap=\"round\" "
+                               .       "stroke-linejoin=\"round\" />";
+               }
+               else
+               {
+                       $sum = 0;
+                       $start_angle = 0;
+                       $end_angle = 0;
+
+                       foreach ($this->data as $row)
+                       {
+                               $sum += $row->data;
+                       }
+
+                       foreach ($this->data as $row)
+                       {
+                               $row->angle = ceil(360 * $row->data / $sum);
+
+                   $start_angle = $end_angle;
+                   $end_angle = $start_angle + $row->angle;
+
+                               $x1 = $cx + $radius * cos(deg2rad($start_angle));
+                               $y1 = $cy + $radius * sin(deg2rad($start_angle));
+
+                               $x2 = $cx + $radius * cos(deg2rad($end_angle));
+                               $y2 = $cy + $radius * sin(deg2rad($end_angle));
+
+                               $arc = $row->angle > 180 ? 1 : 0;
+
+                               $out .= "<path d=\"M{$cx},{$cy} L{$x1},{$y1} A{$radius},{$radius} 0 {$arc},1 {$x2},{$y2} Z\" 
+                                       fill=\"{$row->fill}\" stroke=\"white\" stroke-width=\"".($circle_size * 0.005)."\" stroke-linecap=\"round\" 
+                                       stroke-linejoin=\"round\" />";
+                       }
+               }
+
+               if ($this->title)
+               {
+                       $out .= '<text x="'.($this->width * 0.98).'" y="'.($this->height * 0.07).'" font-size="'.($this->height * 0.05).'" fill="white" '
+                               .       'stroke="white" stroke-width="'.($this->height * 0.01).'" stroke-linejoin="round" stroke-linecap="round" '
+                               .       'text-anchor="end" style="font-family: Verdana, Arial, sans-serif; font-weight: bold;">'.$this->title.'</text>' . PHP_EOL;
+                       $out .= '<text x="'.($this->width * 0.98).'" y="'.($this->height * 0.07).'" font-size="'.($this->height * 0.05).'" fill="black" '
+                               .       'text-anchor="end" style="font-family: Verdana, Arial, sans-serif; font-weight: bold;">'.$this->title.'</text>' . PHP_EOL;
+               }
+
+               if ($this->legend)
+               {
+                       $x = $this->width - ($this->width * 0.06);
+                       $y = $this->height * 0.1;
+
+                       foreach ($this->data as $row)
+                       {
+                               $out .= '<rect x="'.$x.'" y="'.($y - $this->height * 0.01).'" width="'.($this->width * 0.04).'" height="'.($this->height * 0.04).'" fill="'.$row->fill.'" stroke="black" stroke-width="1" rx="2" />' . PHP_EOL;
+
+                               if ($row->label)
+                               {
+                                       $out .= '<text x="'.($x-($this->width * 0.02)).'" y="'.($y+($this->height * 0.025)).'" '
+                                               .       'font-size="'.($this->height * 0.05).'" fill="white" stroke="white" '
+                                               .       'stroke-width="'.($this->height * 0.01).'" stroke-linejoin="round" '
+                                               .       'stroke-linecap="round" text-anchor="end" style="font-family: Verdana, '
+                                               .       'Arial, sans-serif;">'.$row->label.'</text>' . PHP_EOL;
+                                       $out .= '<text x="'.($x-($this->width * 0.02)).'" y="'.($y+($this->height * 0.025)).'" '
+                                               .       'font-size="'.($this->height * 0.05).'" fill="black" text-anchor="end" '
+                                               .       'style="font-family: Verdana, Arial, sans-serif;">'.$row->label.'</text>' . PHP_EOL;
+                               }
+
+                               $y += ($this->height * 0.05);
+                       }
+               }
+
+               $out .= '</svg>';
+               return $out;
+       }
+}
+
+class SVGPie_Data
+{
+       public $fill = 'blue';
+       public $data = 0.0;
+       public $label = null;
+
+       public function __construct($data, $label = null, $fill = 'blue')
+       {
+               $this->data = $data;
+               $this->fill = $fill;
+               $this->label = $label;
+       }
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/svgplot/lib.svgplot.php b/include/libs/svgplot/lib.svgplot.php
new file mode 100644 (file)
index 0000000..2799804
--- /dev/null
@@ -0,0 +1,229 @@
+<?php
+
+class SVGPlot
+{
+       protected $width = null;
+       protected $height = null;
+       protected $data = array();
+       protected $title = null;
+       protected $labels = array();
+       protected $legend = true;
+
+       public function __construct($width = 600, $height = 400)
+       {
+               $this->width = (int) $width;
+               $this->height = (int) $height;
+       }
+
+       public function setTitle($title)
+       {
+               $this->title = $title;
+               return true;
+       }
+
+       public function toggleLegend()
+       {
+               $this->legend = !$this->legend;
+       }
+
+       public function setLabels($labels)
+       {
+               $this->labels = $labels;
+               return true;
+       }
+
+       public function add(SVGPlot_Data $data)
+       {
+               $this->data[] = $data;
+               return true;
+       }
+
+       public function display()
+       {
+               header('Content-Type: image/svg+xml');
+               echo $this->output();
+       }
+
+       public function output()
+       {
+               $out = '<?xml version="1.0" encoding="utf-8" standalone="no"?>' . PHP_EOL;
+               $out.= '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/SVG/DTD/svg10.dtd">' . PHP_EOL;
+               $out.= '<svg width="'.$this->width.'" height="'.$this->height.'" viewBox="0 0 '.$this->width.' '.$this->height.'" xmlns="http://www.w3.org/2000/svg" version="1.1">' . PHP_EOL;
+
+               if ($this->title)
+               {
+                       $out .= '<text x="'.round($this->width/2).'" y="'.($this->height * 0.07).'" font-size="'.($this->height * 0.05).'" fill="white" '
+                               .       'stroke="white" stroke-width="'.($this->height * 0.01).'" stroke-linejoin="round" stroke-linecap="round" '
+                               .       'text-anchor="middle" style="font-family: Verdana, Arial, sans-serif; font-weight: bold;">'.$this->title.'</text>' . PHP_EOL;
+                       $out .= '<text x="'.round($this->width/2).'" y="'.($this->height * 0.07).'" font-size="'.($this->height * 0.05).'" fill="black" '
+                               .       'text-anchor="middle" style="font-family: Verdana, Arial, sans-serif; font-weight: bold;">'.$this->title.'</text>' . PHP_EOL;
+               }
+
+               $out .= $this->_renderLinegraph();
+
+               if ($this->legend)
+               {
+                       $x = $this->width - ($this->width * 0.06);
+                       $y = $this->height * 0.1;
+
+                       foreach ($this->data as $row)
+                       {
+                               $out .= '<rect x="'.$x.'" y="'.($y - $this->height * 0.01).'" width="'.($this->width * 0.04).'" height="'.($this->height * 0.04).'" fill="'.$row->color.'" stroke="black" stroke-width="1" rx="2" />' . PHP_EOL;
+
+                               if ($row->title)
+                               {
+                                       $out .= '<text x="'.($x-($this->width * 0.02)).'" y="'.($y+($this->height * 0.025)).'" '
+                                               .       'font-size="'.($this->height * 0.05).'" fill="white" stroke="white" '
+                                               .       'stroke-width="'.($this->height * 0.01).'" stroke-linejoin="round" '
+                                               .       'stroke-linecap="round" text-anchor="end" style="font-family: Verdana, Arial, '
+                                               .       'sans-serif;">'.$row->title.'</text>' . PHP_EOL;
+                                       $out .= '<text x="'.($x-($this->width * 0.02)).'" y="'.($y+($this->height * 0.025)).'" '
+                                               .       'font-size="'.($this->height * 0.05).'" fill="black" text-anchor="end" '
+                                               .       'style="font-family: Verdana, Arial, sans-serif;">'.$row->title.'</text>' . PHP_EOL;
+                               }
+
+                               $y += ($this->height * 0.07);
+                       }
+               }
+
+               $out .= '</svg>';
+
+               return $out;
+       }
+
+       protected function _renderLinegraph()
+       {
+               $out = '';
+
+               if (empty($this->data))
+               {
+                       return $out;
+               }
+
+               // Figure out the maximum Y-axis value
+               $max_value = 0;
+               $nb_elements = 0;
+
+               foreach ($this->data as $row)
+               {
+                       if ($max_value == 0)
+                       {
+                               $nb_elements = count($row->get());
+                       }
+
+                       $max = max($row->get());
+
+                       if ($max > $max_value)
+                       {
+                               $max_value = $max;
+                       }
+               }
+
+               if ($nb_elements < 1)
+               {
+                       return $out;
+               }
+
+               $divide = round($max_value / ($this->height * 0.8), 2) ?: 1;
+               $y_axis_val = ceil(abs($max_value) / ($this->height * 0.8)) * 50;
+               $space = round(($this->width - ($this->width * 0.1)) / $nb_elements, 2);
+
+               for ($i = 0; $i < 10; $i++)
+               {
+                       if (($y_axis_val * $i) <= $max_value)
+                       {
+                               $line_y = ($this->height * 0.93) - (($y_axis_val / $divide) * $i);
+                               $out .= '<line x1="'.($this->width * 0.1).'" y1="'.($line_y).'" x2="'.$this->width.'" y2="'.($line_y).'" stroke-width="1" stroke="#ccc" />' . PHP_EOL;
+                               $out .= '<g><text x="'.($this->width * 0.08).'" y="'.($line_y).'" font-size="'.($this->height * 0.04).'" fill="gray" text-anchor="end" style="font-family: Verdana, Arial, sans-serif;">'.($y_axis_val * $i).'</text></g>' . PHP_EOL;
+                       }
+               }
+
+               // X-axis lines
+               $y = $this->height - ($this->height * 0.07);
+               $x = $this->width * 0.1;
+               $i = 0;
+
+               foreach ($this->data[0]->get() as $k=>$v)
+               {
+                       if ($x >= $this->width)
+                               break;
+
+                       $out .= '<line x1="'.$x.'" y1="'.($y).'" x2="'.$x.'" y2="'.($this->height * 0.1).'" stroke-width="1" stroke="#ccc" />' . PHP_EOL;
+                       $x += $space + $this->data[0]->width;
+               }
+
+               // labels for x axis
+               $y = $this->height - ($this->height * 0.07);
+               $i = 0;
+               $step = round($nb_elements / 5);
+
+               for ($i = 0; $i <= $nb_elements; $i += $step)
+               {
+                       //echo
+                       $x = ($i * ($space + $this->data[0]->width)) + ($this->width * 0.1);
+
+                       if ($x >= $this->width)
+                               break;
+
+                       if (isset($this->labels[$i]))
+                       {
+                               $out .= '<g><text x="'.$x.'" y="'.($y+($this->height * 0.06)).'" '
+                                       .       'font-size="'.($this->height * 0.04).'" fill="gray" text-anchor="middle" '
+                                       .       'style="font-family: Verdana, Arial, sans-serif;">'
+                                       .       ($this->labels[$i]).'</text></g>' . PHP_EOL;
+                       }
+               }
+
+               $y = ($this->height * 0.1);
+               $w = $this->width - ($this->width * 0.1);
+               $h = $this->height - ($this->height * 0.17);
+
+               foreach ($this->data as $row)
+               {
+                       $out .= '<polyline fill="none" stroke="'.$row->color.'" stroke-width="'.$row->width.'" '
+                               .'stroke-linecap="round" points="';
+
+                       $x = ($this->width * 0.1);
+
+                       foreach ($row->get() as $k=>$v)
+                       {
+                               $_y = $y + ($h - round($v / $divide, 2)) + round($row->width / 2);
+                               $out.= $x.','.$_y.' ';
+                               $x += $space + $row->width;
+                       }
+
+                       $out .= '" />' . PHP_EOL;
+               }
+
+               return $out;
+       }
+}
+
+class SVGPlot_Data
+{
+       public $color = 'blue';
+       public $width = '10';
+       public $title = null;
+       protected $data = array();
+
+       public function __construct($data)
+       {
+               if (is_array($data))
+                       $this->data = $data;
+               elseif (!is_object($data))
+                       $this->append($data);
+       }
+
+       public function append($data)
+       {
+               $this->data[] = $data;
+               return true;
+       }
+
+       public function get()
+       {
+               return $this->data;
+       }
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/class.compiler.php b/include/libs/template_lite/class.compiler.php
new file mode 100644 (file)
index 0000000..8ce617d
--- /dev/null
@@ -0,0 +1,1013 @@
+<?php
+/*
+ * Project:    template_lite, a smarter template engine
+ * File:       class.compiler.php
+ * Author:     Paul Lockaby <paul@paullockaby.com>, Mark Dickenson <akapanamajack@sourceforge.net>
+ * Copyright:  2003,2004,2005 by Paul Lockaby, 2005,2006 Mark Dickenson
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * The latest version of template_lite can be obtained from:
+ * http://templatelite.sourceforge.net
+ *
+ */
+
+class Template_Lite_Compiler extends Template_Lite {
+       // public configuration variables
+       var $left_delimiter                     = "";
+       var $right_delimiter                    = "";
+       var $plugins_dir                        = "";
+       var $template_dir               = "";
+       var $reserved_template_varname = "";
+       var $default_modifiers          = array();
+
+       var $php_extract_vars           =       true;   // Set this to false if you do not want the $this->_tpl variables to be extracted for use by PHP code inside the template.
+
+       // private internal variables
+       var $_vars                      =       array();        // stores all internal assigned variables
+       var $_confs                     =       array();        // stores all internal config variables
+       var $_plugins                   =       array();        // stores all internal plugins
+       var $_linenum                   =       0;              // the current line number in the file we are processing
+       var $_file                      =       "";             // the current file we are processing
+       var $_literal                   =       array();        // stores all literal blocks
+       var $_foreachelse_stack         =       array();
+       var $_for_stack                 =       0;
+       var $_sectionelse_stack  =   array();   // keeps track of whether section had 'else' part
+       var $_switch_stack              =       array();
+       var $_tag_stack                 =       array();
+       var $_require_stack             =       array();        // stores all files that are "required" inside of the template
+       var $_php_blocks                =       array();        // stores all of the php blocks
+       var $_error_level               =       null;
+       var $_sl_md5                    =       '39fc70570b8b60cbc1b85839bf242aff';
+
+       var $_db_qstr_regexp            =       null;           // regexps are setup in the constructor
+       var $_si_qstr_regexp            =       null;
+       var $_qstr_regexp               =       null;
+       var $_func_regexp               =       null;
+       var $_var_bracket_regexp        =       null;
+       var $_dvar_regexp               =       null;
+       var $_cvar_regexp               =       null;
+       var $_svar_regexp               =       null;
+       var $_mod_regexp                =       null;
+       var $_var_regexp                =       null;
+    var $_obj_params_regexp     =   null;
+       var $_templatelite_vars         =       array();
+
+       function Template_Lite_compiler()
+       {
+               // matches double quoted strings:
+               // "foobar"
+               // "foo\"bar"
+               // "foobar" . "foo\"bar"
+               $this->_db_qstr_regexp = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
+
+               // matches single quoted strings:
+               // 'foobar'
+               // 'foo\'bar'
+               $this->_si_qstr_regexp = '\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'';
+
+               // matches single or double quoted strings
+               $this->_qstr_regexp = '(?:' . $this->_db_qstr_regexp . '|' . $this->_si_qstr_regexp . ')';
+
+               // matches bracket portion of vars
+               // [0]
+               // [foo]
+               // [$bar]
+               // [#bar#]
+               $this->_var_bracket_regexp = '\[[\$|\#]?\w+\#?\]';
+//             $this->_var_bracket_regexp = '\[\$?[\w\.]+\]';
+
+               // matches section vars:
+               // %foo.bar%
+               $this->_svar_regexp = '\%\w+\.\w+\%';
+
+               // matches $ vars (not objects):
+               // $foo
+               // $foo[0]
+               // $foo[$bar]
+               // $foo[5][blah]
+//             $this->_dvar_regexp = '\$[a-zA-Z0-9_]{1,}(?:' . $this->_var_bracket_regexp . ')*(?:' . $this->_var_bracket_regexp . ')*';
+               $this->_dvar_regexp = '\$[a-zA-Z0-9_]{1,}(?:' . $this->_var_bracket_regexp . ')*(?:\.\$?\w+(?:' . $this->_var_bracket_regexp . ')*)*';
+
+               // matches config vars:
+               // #foo#
+               // #foobar123_foo#
+               $this->_cvar_regexp = '\#[a-zA-Z0-9_]{1,}(?:' . $this->_var_bracket_regexp . ')*(?:' . $this->_var_bracket_regexp . ')*\#';
+
+               // matches valid variable syntax:
+               // $foo
+               // 'text'
+               // "text"
+               $this->_var_regexp = '(?:(?:' . $this->_dvar_regexp . '|' . $this->_cvar_regexp . ')|' . $this->_qstr_regexp . ')';
+
+               // matches valid modifier syntax:
+               // |foo
+               // |@foo
+               // |foo:"bar"
+               // |foo:$bar
+               // |foo:"bar":$foobar
+               // |foo|bar
+               $this->_mod_regexp = '(?:\|@?[0-9a-zA-Z_]+(?::(?>-?\w+|' . $this->_dvar_regexp . '|' . $this->_qstr_regexp .'))*)';
+
+               // matches valid function name:
+               // foo123
+               // _foo_bar
+               $this->_func_regexp = '[a-zA-Z_]+';
+//             $this->_func_regexp = '[a-zA-Z_]\w*';
+
+               $this->_const_regexp = '(?:[a-zA-Z_]+\\\\)*(?:[a-zA-Z_]+::)?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*';
+       }
+
+       function _compile_file($file_contents)
+       {
+               // Remove BOM from templates
+               if(substr($file_contents, 0,3) == pack("CCC",0xef,0xbb,0xbf)) {
+                       $file_contents = substr($file_contents, 3);
+               }
+
+               $ldq = preg_quote($this->left_delimiter);
+               $rdq = preg_quote($this->right_delimiter);
+               $_match         = array();              // a temp variable for the current regex match
+               $tags           = array();              // all original tags
+               $text           = array();              // all original text
+               $compiled_text  = '<?php /* '.$this->_version.' '.strftime("%Y-%m-%d %H:%M:%S %Z").' */ ?>'; // stores the compiled result
+               $compiled_tags  = array();              // all tags and stuff
+
+               $this->_require_stack = array();
+
+               $this->_load_filters();
+
+               if (count($this->_plugins['prefilter']) > 0)
+               {
+                       foreach ($this->_plugins['prefilter'] as $function)
+                       {
+                               if ($function === false)
+                               {
+                                       continue;
+                               }
+                               $file_contents = $function($file_contents, $this);
+                       }
+               }
+
+               // remove all comments
+               $file_contents = preg_replace("!{$ldq}\*.*?\*{$rdq}!s","",$file_contents);
+
+               // replace all php start and end tags
+               $file_contents = preg_replace('%(<\?(?!php|=|$))%i', '<?php echo \'\\1\'?>', $file_contents);
+
+               // remove literal blocks
+               preg_match_all("!{$ldq}\s*literal\s*{$rdq}(.*?){$ldq}\s*/literal\s*{$rdq}!s", $file_contents, $_match);
+               $this->_literal = $_match[1];
+               $file_contents = preg_replace("!{$ldq}\s*literal\s*{$rdq}(.*?){$ldq}\s*/literal\s*{$rdq}!s", stripslashes($ldq . "literal" . $rdq), $file_contents);
+
+               // remove php blocks
+               preg_match_all("!{$ldq}\s*php\s*{$rdq}(.*?){$ldq}\s*/php\s*{$rdq}!s", $file_contents, $_match);
+               $this->_php_blocks = $_match[1];
+               $file_contents = preg_replace("!{$ldq}\s*php\s*{$rdq}(.*?){$ldq}\s*/php\s*{$rdq}!s", stripslashes($ldq . "php" . $rdq), $file_contents);
+
+               // gather all template tags
+               preg_match_all("!{$ldq}\s*(.*?)\s*{$rdq}!s", $file_contents, $_match);
+               $tags = $_match[1];
+
+               // put all of the non-template tag text blocks into an array, using the template tags as delimiters
+               $text = preg_split("!{$ldq}.*?{$rdq}!s", $file_contents);
+
+               // compile template tags
+               $count_tags = count($tags);
+               for ($i = 0, $for_max = $count_tags; $i < $for_max; $i++)
+               {
+                       $this->_linenum += substr_count($text[$i], "\n");
+                       $compiled_tags[] = $this->_compile_tag($tags[$i]);
+                       $this->_linenum += substr_count($tags[$i], "\n");
+               }
+
+               // build the compiled template by replacing and interleaving text blocks and compiled tags
+               $count_compiled_tags = count($compiled_tags);
+               for ($i = 0, $for_max = $count_compiled_tags; $i < $for_max; $i++)
+               {
+                       if ($compiled_tags[$i] == '') {
+                               $text[$i+1] = preg_replace('~^(\r\n|\r|\n)~', '', $text[$i+1]);
+                       }
+                       $compiled_text .= $text[$i].$compiled_tags[$i];
+               }
+               $compiled_text .= $text[$i];
+
+               foreach ($this->_require_stack as $key => $value)
+               {
+                       $compiled_text = '<?php require_once(\''. $this->_get_plugin_dir($key) . $key . '\'); $this->register_' . $value[0] . '("' . $value[1] . '", "' . $value[2] . '", false); ?>' . $compiled_text;
+               }
+
+               // remove unnecessary close/open tags
+               $compiled_text = preg_replace('!\?>\n?<\?php!', '', $compiled_text);
+
+               if (count($this->_plugins['postfilter']) > 0)
+               {
+                       foreach ($this->_plugins['postfilter'] as $function)
+                       {
+                               if ($function === false)
+                               {
+                                       continue;
+                               }
+                               $compiled_text = $function($compiled_text, $this);
+                       }
+               }
+
+               return $compiled_text;
+       }
+
+       function _compile_tag($tag)
+       {
+               $_match         = array();              // stores the tags
+               $_result        = "";                   // the compiled tag
+               $_variable      = "";                   // the compiled variable
+
+               // extract the tag command, modifier and arguments
+               preg_match_all('/(?:(' . $this->_const_regexp  . '|' . $this->_var_regexp . '|' . $this->_svar_regexp . '|\/?' . $this->_func_regexp . ')(' . $this->_mod_regexp . '*)(?:\s*[,\.]\s*)?)(?:\s+(.*))?/xs', $tag, $_match);
+
+               if ($_match[1][0][0] == '$' || ($_match[1][0][0] == '#' && $_match[1][0][strlen($_match[1][0]) - 1] == '#') || $_match[1][0][0] == "'" || $_match[1][0][0] == '"' || $_match[1][0][0] == '%')
+               {
+                       $_result = $this->_parse_variables($_match[1], $_match[2]);
+                       return "<?php echo $_result; ?>";
+               }
+               elseif (defined($_match[1][0]))
+               {
+                       return "<?php echo constant('".$_match[1][0]."'); ?>";
+               }
+               // process a function
+               $tag_command = $_match[1][0];
+               $tag_modifiers = !empty($_match[2][0]) ? $_match[2][0] : null;
+               $tag_arguments = !empty($_match[3][0]) ? $_match[3][0] : null;
+               $_result = $this->_parse_function($tag_command, $tag_modifiers, $tag_arguments);
+               return $_result;
+       }
+
+       function _parse_function($function, $modifiers, $arguments)
+       {
+               switch ($function) {
+                       case 'include':
+                               if (!function_exists('compile_include'))
+                               {
+                                       require_once(TEMPLATE_LITE_DIR . "internal/compile.include.php");
+                               }
+                               return compile_include($arguments, $this);
+                               break;
+                       case 'insert':
+                               $_args = $this->_parse_arguments($arguments);
+                               if (!isset($_args['name']))
+                               {
+                                       throw new Template_Exception("missing 'name' attribute in 'insert'", $this);
+                               }
+                               foreach ($_args as $key => $value)
+                               {
+                                       if (is_bool($value))
+                                       {
+                                               $value = $value ? 'true' : 'false';
+                                       }
+                                       $arg_list[] = "'$key' => $value";
+                               }
+                               return '<?php echo $this->_run_insert(array(' . implode(', ', (array)$arg_list) . ')); ?>';
+                               break;
+                       case 'ldelim':
+                               return $this->left_delimiter;
+                               break;
+                       case 'rdelim':
+                               return $this->right_delimiter;
+                               break;
+                       case 'literal':
+                               list (,$literal) = each($this->_literal);
+                               $this->_linenum += substr_count($literal, "\n");
+                               return "<?php echo '" . str_replace("'", "\'", str_replace("\\", "\\\\", $literal)) . "'; ?>";
+                               break;
+                       case 'php':
+                               list (,$php_block) = each($this->_php_blocks);
+                               $this->_linenum += substr_count($php_block, "\n");
+                               $php_extract = '';
+                               if($this->php_extract_vars)
+                               {
+                                       if (strnatcmp(PHP_VERSION, '4.3.0') >= 0)
+                                       {
+                                               $php_extract = '<?php extract($this->_vars, EXTR_REFS); ?>' . "\n";
+                                       }
+                                       else
+                                       {
+                                               $php_extract = '<?php extract($this->_vars); ?>' . "\n";
+                                       }
+                               }
+                               return $php_extract . '<?php '.$php_block.' ?>';
+                               break;
+                       case 'foreach':
+                               array_push($this->_foreachelse_stack, false);
+                               $_args = $this->_parse_arguments($arguments);
+                               if (!isset($_args['from']))
+                               {
+                                       throw new Template_Exception("missing 'from' attribute in 'foreach'", $this);
+                               }
+                               if (!isset($_args['value']) && !isset($_args['item']))
+                               {
+                                       throw new Template_Exception("missing 'value' attribute in 'foreach'", $this);
+                               }
+                               if (isset($_args['value']))
+                               {
+                                       $_args['value'] = $this->_dequote($_args['value']);
+                               }
+                               elseif (isset($_args['item']))
+                               {
+                                       $_args['value'] = $this->_dequote($_args['item']);
+                               }
+                               isset($_args['key']) ? $_args['key'] = "\$this->_vars['".$this->_dequote($_args['key'])."'] => " : $_args['key'] = '';
+                               $_result = '<?php if (count((array)' . $_args['from'] . ')): ?>';
+                               if (isset($_args['name']))
+                               {
+                                               $foreach_props = '$this->_templatelite_vars[\'foreach\'][' . $_args['name'] . ']';
+                                               $_result .= '<?php ' . $foreach_props . '[\'iteration\'] = 0; ' . $foreach_props . '[\'total\'] = count((array)' . $_args['from'] . '); ?>';
+                               }
+                               $_result .= '<?php foreach ((array)' . $_args['from'] . ' as ' . $_args['key'] . '$this->_vars[\'' . $_args['value'] . '\']): ?>';
+                               if (isset($_args['name']))
+                               {
+                                               $_result .= '<?php ' . $foreach_props . '[\'iteration\']++; '
+                                                       .       $foreach_props . '[\'first\'] = ' . $foreach_props . '[\'iteration\'] == 1 ? true : false; '
+                                                       .       $foreach_props . '[\'last\'] = ' . $foreach_props . '[\'iteration\'] == ' . $foreach_props . '[\'total\'] ? true : false; '
+                                                       .       '?>';
+                               }
+                               return $_result;
+                               break;
+                               case 'foreachelse':
+                               $this->_foreachelse_stack[count($this->_foreachelse_stack)-1] = true;
+                               return "<?php endforeach; else: ?>";
+                               break;
+                       case '/foreach':
+                               if (array_pop($this->_foreachelse_stack))
+                               {
+                                       return "<?php endif; ?>";
+                               }
+                               else
+                               {
+                                       return "<?php endforeach; endif; ?>";
+                               }
+                               break;
+                       case 'for':
+                               $this->_for_stack++;
+                               $_args = $this->_parse_arguments($arguments);
+                               if (!isset($_args['start']))
+                               {
+                                       throw new Template_Exception("missing 'start' attribute in 'for'", $this);
+                               }
+                               if (!isset($_args['stop']))
+                               {
+                                       throw new Template_Exception("missing 'stop' attribute in 'for'", $this);
+                               }
+                               if (!isset($_args['step']))
+                               {
+                                       $_args['step'] = 1;
+                               }
+                               $_result = '<?php for($for' . $this->_for_stack . ' = ' . $_args['start'] . '; ((' . $_args['start'] . ' < ' . $_args['stop'] . ') ? ($for' . $this->_for_stack . ' < ' . $_args['stop'] . ') : ($for' . $this->_for_stack . ' > ' . $_args['stop'] . ')); $for' . $this->_for_stack . ' += ((' . $_args['start'] . ' < ' . $_args['stop'] . ') ? ' . $_args['step'] . ' : -' . $_args['step'] . ')): ?>';
+                               if (isset($_args['value']))
+                               {
+                                       $_result .= '<?php $this->assign(\'' . $this->_dequote($_args['value']) . '\', $for' . $this->_for_stack . '); ?>';
+                               }
+                               return $_result;
+                               break;
+                       case '/for':
+                               $this->_for_stack--;
+                               return "<?php endfor; ?>";
+                               break;
+                       case 'section':
+                               array_push($this->_sectionelse_stack, false);
+                               if (!function_exists('compile_section_start'))
+                               {
+                                       require_once(TEMPLATE_LITE_DIR . "internal/compile.section_start.php");
+                               }
+                               return compile_section_start($arguments, $this);
+                               break;
+                       case 'sectionelse':
+                               $this->_sectionelse_stack[count($this->_sectionelse_stack)-1] = true;
+                               return "<?php endfor; else: ?>";
+                               break;
+                       case '/section':
+                               if (array_pop($this->_sectionelse_stack))
+                               {
+                                       return "<?php endif; ?>";
+                               }
+                               else
+                               {
+                                       return "<?php endfor; endif; ?>";
+                               }
+                               break;
+                       case 'while':
+                               $_args = $this->_compile_if($arguments, false, true);
+                               return '<?php while(' . $_args . '): ?>';
+                               break;
+                       case '/while':
+                               return "<?php endwhile; ?>";
+                               break;
+                       case 'if':
+                               return $this->_compile_if($arguments);
+                               break;
+                       case 'else':
+                               return "<?php else: ?>";
+                               break;
+                       case 'elseif':
+                               return $this->_compile_if($arguments, true);
+                               break;
+                       case '/if':
+                               return "<?php endif; ?>";
+                               break;
+                       case 'assign':
+                               $_args = $this->_parse_arguments($arguments);
+                               if (!isset($_args['var']))
+                               {
+                                       throw new Template_Exception("missing 'var' attribute in 'assign'", $this);
+                               }
+                               if (!isset($_args['value']))
+                               {
+                                       throw new Template_Exception("missing 'value' attribute in 'assign'", $this);
+                               }
+                               return '<?php $this->assign(\'' . $this->_dequote($_args['var']) . '\', ' . $_args['value'] . '); ?>';
+                               break;
+                       case 'switch':
+                               $_args = $this->_parse_arguments($arguments);
+                               if (!isset($_args['from']))
+                               {
+                                       throw new Template_Exception("missing 'from' attribute in 'switch'", $this);
+                               }
+                               array_push($this->_switch_stack, array("matched" => false, "var" => $this->_dequote($_args['from'])));
+                               return;
+                               break;
+                       case '/switch':
+                               array_pop($this->_switch_stack);
+                               return '<?php break; endswitch; ?>';
+                               break;
+                       case 'case':
+                               if (count($this->_switch_stack) > 0)
+                               {
+                                       $_result = "<?php ";
+                                       $_args = $this->_parse_arguments($arguments);
+                                       $_index = count($this->_switch_stack) - 1;
+                                       if (!$this->_switch_stack[$_index]["matched"])
+                                       {
+                                               $_result .= 'switch(' . $this->_switch_stack[$_index]["var"] . '): ';
+                                               $this->_switch_stack[$_index]["matched"] = true;
+                                       }
+                                       else
+                                       {
+                                               $_result .= 'break; ';
+                                       }
+                                       if (!empty($_args['value']))
+                                       {
+                                               $_result .= 'case '.$_args['value'].': ';
+                                       }
+                                       else
+                                       {
+                                               $_result .= 'default: ';
+                                       }
+                                       return $_result . ' ?>';
+                               }
+                               else
+                               {
+                                       throw new Template_Exception("unexpected 'case', 'case' can only be in a 'switch'", $this);
+                               }
+                               break;
+                       case 'config_load':
+                               $_args = $this->_parse_arguments($arguments);
+                               if (empty($_args['file']))
+                               {
+                                       throw new Template_Exception("missing 'file' attribute in 'config_load' tag", $this);
+                               }
+                               isset($_args['section']) ? null : $_args['section'] = 'null';
+                               isset($_args['var']) ? null : $_args['var'] = 'null';
+                               return '<?php $this->config_load(' . $_args['file'] . ', ' . $_args['section'] . ', ' . $_args['var'] . '); ?>';
+                               break;
+                       default:
+                               $_result = "";
+                               if ($this->_compile_compiler_function($function, $arguments, $_result))
+                               {
+                                       return $_result;
+                               }
+                               else if ($this->_compile_custom_block($function, $modifiers, $arguments, $_result))
+                               {
+                                       return $_result;
+                               }
+                               elseif ($this->_compile_custom_function($function, $modifiers, $arguments, $_result))
+                               {
+                                       return $_result;
+                               }
+                               else
+                               {
+                                       throw new Template_Exception($function." function does not exist", $this);
+                               }
+                               break;
+               }
+       }
+
+       function _compile_compiler_function($function, $arguments, &$_result)
+       {
+               if ($function = $this->_plugin_exists($function, "compiler"))
+               {
+                       $_args = $this->_parse_arguments($arguments);
+                       $_result = '<?php ' . $function($_args, $this) . ' ?>';
+                       return true;
+               }
+               else
+               {
+                       return false;
+               }
+       }
+
+       function _compile_custom_function($function, $modifiers, $arguments, &$_result)
+       {
+               if (!function_exists('compile_compile_custom_function'))
+               {
+                       require_once(TEMPLATE_LITE_DIR . "internal/compile.compile_custom_function.php");
+               }
+               return compile_compile_custom_function($function, $modifiers, $arguments, $_result, $this);
+       }
+
+       function _compile_custom_block($function, $modifiers, $arguments, &$_result)
+       {
+               if (!function_exists('compile_compile_custom_block'))
+               {
+                       require_once(TEMPLATE_LITE_DIR . "internal/compile.compile_custom_block.php");
+               }
+               return compile_compile_custom_block($function, $modifiers, $arguments, $_result, $this);
+       }
+
+       function _compile_if($arguments, $elseif = false, $while = false)
+       {
+               if (!function_exists('compile_compile_if'))
+               {
+                       require_once(TEMPLATE_LITE_DIR . "internal/compile.compile_if.php");
+               }
+               return compile_compile_if($arguments, $elseif, $while, $this);
+       }
+
+       function _parse_is_expr($is_arg, $_arg)
+       {
+               if (!function_exists('compile_parse_is_expr'))
+               {
+                       require_once(TEMPLATE_LITE_DIR . "internal/compile.parse_is_expr.php");
+               }
+               return compile_parse_is_expr($is_arg, $_arg, $this);
+       }
+
+       function _compile_config($variable)
+       {
+               if (!function_exists('compile_compile_config'))
+               {
+                       require_once(TEMPLATE_LITE_DIR . "internal/compile.compile_config.php");
+               }
+               return compile_compile_config($variable, $this);
+       }
+
+       function _dequote($string)
+       {
+               if (($string[0] == "'" || $string[0] == '"') && $string{strlen($string)-1} == $string[0])
+               {
+                       return substr($string, 1, -1);
+               }
+               else
+               {
+                       return $string;
+               }
+       }
+
+       function _parse_arguments($arguments)
+       {
+               $_match         = array();
+               $_result        = array();
+               $_variables     = array();
+               preg_match_all('/(?:' . $this->_qstr_regexp . ' | (?>[^"\'=\s]+))+|[=]/x', $arguments, $_match);
+               /*
+                  Parse state:
+                        0 - expecting attribute name
+                        1 - expecting '='
+                        2 - expecting attribute value (not '=')
+               */
+               $state = 0;
+               foreach($_match[0] as $value)
+               {
+                       switch($state) {
+                               case 0:
+                                       // valid attribute name
+                                       if (is_string($value))
+                                       {
+                                               $a_name = $value;
+                                               $state = 1;
+                                       }
+                                       else
+                                       {
+                                               throw new Template_Exception("invalid attribute name: '$token'", $this);
+                                       }
+                                       break;
+                               case 1:
+                                       if ($value == '=')
+                                       {
+                                               $state = 2;
+                                       }
+                                       else
+                                       {
+                                               throw new Template_Exception("expecting '=' after '$last_value'", $this);
+                                       }
+                                       break;
+                               case 2:
+                                       if ($value != '=')
+                                       {
+                                               if ($value == 'yes' || $value == 'on' || $value == 'true')
+                                               {
+                                                       $value = true;
+                                               }
+                                               elseif ($value == 'no' || $value == 'off' || $value == 'false')
+                                               {
+                                                       $value = false;
+                                               }
+                                               elseif ($value == 'null')
+                                               {
+                                                       $value = null;
+                                               }
+
+                                               if(!preg_match_all('/(?:(' . $this->_const_regexp . '|' . $this->_var_regexp . '|' . $this->_svar_regexp . ')(' . $this->_mod_regexp . '*))(?:\s+(.*))?/xs', $value, $_variables))
+                                               {
+                                                       $_result[$a_name] = $value;
+                                               }
+                                               else
+                                               {
+                                                       $_result[$a_name] = $this->_parse_variables($_variables[1], $_variables[2]);
+                                               }
+                                               $state = 0;
+                                       }
+                                       else
+                                       {
+                                               throw new Template_Exception("'=' cannot be an attribute value", $this);
+                                       }
+                                       break;
+                       }
+                       $last_value = $value;
+               }
+               if($state != 0)
+               {
+                       if($state == 1)
+                       {
+                               throw new Template_Exception("expecting '=' after attribute name '$last_value'", $this);
+                       }
+                       else
+                       {
+                               throw new Template_Exception("missing attribute value", $this);
+                       }
+               }
+               return $_result;
+       }
+
+       function _parse_variables($variables, $modifiers)
+       {
+               $_result = "";
+               foreach($variables as $key => $value)
+               {
+                       $tag_variable = trim($variables[$key]);
+                       if(!empty($this->default_modifiers) && !preg_match('!(^|\|)templatelite:nodefaults($|\|)!',$modifiers[$key]))
+                       {
+                               $_default_mod_string = implode('|',(array)$this->default_modifiers);
+                               $modifiers[$key] = empty($modifiers[$key]) ? $_default_mod_string : $_default_mod_string . '|' . $modifiers[$key];
+                       }
+                       if (empty($modifiers[$key]))
+                       {
+                               $_result .= $this->_parse_variable($tag_variable).'.';
+                       }
+                       else
+                       {
+                               $_result .= $this->_parse_modifier($this->_parse_variable($tag_variable), $modifiers[$key]).'.';
+                       }
+               }
+               return substr($_result, 0, -1);
+       }
+
+       function _parse_variable($variable)
+       {
+               // replace variable with value
+               if ($variable[0] == "\$")
+               {
+                       // replace the variable
+                       return $this->_compile_variable($variable);
+               }
+               elseif ($variable[0] == '#')
+               {
+                       // replace the config variable
+                       return $this->_compile_config($variable);
+               }
+               elseif ($variable[0] == '"')
+               {
+                       // expand the quotes to pull any variables out of it
+                       // fortunately variables inside of a quote aren't fancy, no modifiers, no quotes
+                       //   just get everything from the $ to the ending space and parse it
+                       // if the $ is escaped, then we won't expand it
+                       $_result = "";
+                       preg_match_all('/(?:[^\\\]' . $this->_dvar_regexp . ')/', substr($variable, 1, -1), $_expand);  // old match
+//                     preg_match_all('/(?:[^\\\]' . $this->_dvar_regexp . '[^\\\])/', $variable, $_expand);
+                       $_expand = array_unique($_expand[0]);
+                       foreach($_expand as $key => $value)
+                       {
+                               $_expand[$key] = trim($value);
+                               if (strpos($_expand[$key], '$') > 0)
+                               {
+                                       $_expand[$key] = substr($_expand[$key], strpos($_expand[$key], '$'));
+                               }
+                       }
+                       $_result = $variable;
+                       foreach($_expand as $value)
+                       {
+                               $value = trim($value);
+                               $_result = str_replace($value, '" . ' . $this->_parse_variable($value) . ' . "', $_result);
+                       }
+                       $_result = str_replace("`", "", $_result);
+                       return $_result;
+               }
+               elseif ($variable[0] == "'")
+               {
+                       // return the value just as it is
+                       return $variable;
+               }
+               elseif ($variable[0] == "%")
+               {
+                       return $this->_parse_section_prop($variable);
+               }
+               elseif (defined($variable))
+               {
+                       return '"".constant("'.addslashes($variable).'")';
+               }
+               else
+               {
+                       // return it as is; i believe that there was a reason before that i did not just return it as is,
+                       // but i forgot what that reason is ...
+                       // the reason i return the variable 'as is' right now is so that unquoted literals are allowed
+                       return $variable;
+               }
+       }
+
+       function _parse_section_prop($section_prop_expr)
+       {
+               $parts = explode('|', $section_prop_expr, 2);
+               $var_ref = $parts[0];
+               $modifiers = isset($parts[1]) ? $parts[1] : '';
+
+               preg_match('!%(\w+)\.(\w+)%!', $var_ref, $match);
+               $section_name = $match[1];
+               $prop_name = $match[2];
+
+               $output = "\$this->_sections['$section_name']['$prop_name']";
+
+               $this->_parse_modifier($output, $modifiers);
+
+               return $output;
+       }
+
+       function _compile_variable($variable)
+       {
+               $_result        = "";
+
+               // remove the $
+               $variable = substr($variable, 1);
+
+               // get [foo] and .foo and (...) pieces
+               preg_match_all('!(?:^\w+)|(?:' . $this->_var_bracket_regexp . ')|\.\$?\w+|\S+!', $variable, $_match);
+               $variable = $_match[0];
+               $var_name = array_shift($variable);
+
+               if ($var_name == $this->reserved_template_varname)
+               {
+                       if ($variable[0][0] == '[' || $variable[0][0] == '.')
+                       {
+                               $find = array("[", "]", ".");
+                               switch(strtoupper(str_replace($find, "", $variable[0])))
+                               {
+                                       case 'GET':
+                                               $_result = "\$_GET";
+                                               break;
+                                       case 'POST':
+                                               $_result = "\$_POST";
+                                               break;
+                                       case 'COOKIE':
+                                               $_result = "\$_COOKIE";
+                                               break;
+                                       case 'ENV':
+                                               $_result = "\$_ENV";
+                                               break;
+                                       case 'SERVER':
+                                               $_result = "\$_SERVER";
+                                               break;
+                                       case 'SESSION':
+                                               $_result = "\$_SESSION";
+                                               break;
+                                       case 'NOW':
+                                               $_result = "time()";
+                                               break;
+                                       case 'SECTION':
+                                               $_result = "\$this->_sections";
+                                               break;
+                                       case 'LDELIM':
+                                               $_result = "\$this->left_delimiter";
+                                               break;
+                                       case 'RDELIM':
+                                               $_result = "\$this->right_delimiter";
+                                               break;
+                                       case 'VERSION':
+                                               $_result = "\$this->_version";
+                                               break;
+                                       case 'CONFIG':
+                                               $_result = "\$this->_confs";
+                                               break;
+                                       case 'TEMPLATE':
+                                               $_result = "\$this->_file";
+                                               break;
+                                       case 'CONST':
+                                               $constant = str_replace($find, "", $_match[0][2]);
+                                               $_result = "constant('$constant')";
+                                               $variable = array();
+                                               break;
+                                       default:
+                                               $_var_name = str_replace($find, "", $variable[0]);
+                                               $_result = "\$this->_templatelite_vars['$_var_name']";
+                                               break;
+                               }
+                               array_shift($variable);
+                       }
+                       else
+                       {
+                               throw new Template_Exception('$' . $var_name.implode('', $variable) . ' is an invalid $templatelite reference', $this);
+                       }
+               }
+               else
+               {
+                       $_result = "\$this->_vars['$var_name']";
+               }
+
+               foreach ($variable as $var)
+               {
+                       if ($var[0] == '[')
+                       {
+                               $var = substr($var, 1, -1);
+                               if (is_numeric($var))
+                               {
+                                       $_result .= "[$var]";
+                               }
+                               elseif ($var[0] == '$')
+                               {
+                                       $_result .= "[" . $this->_compile_variable($var) . "]";
+                               }
+                               elseif ($var[0] == '#')
+                               {
+                                       $_result .= "[" . $this->_compile_config($var) . "]";
+                               }
+                               else
+                               {
+//                                     $_result .= "['$var']";
+                                       $parts = explode('.', $var);
+                                       $section = $parts[0];
+                                       $section_prop = isset($parts[1]) ? $parts[1] : 'index';
+                                       $_result .= "[\$this->_sections['$section']['$section_prop']]";
+                               }
+                       }
+                       else if ($var[0] == '.')
+                       {
+                               if ($var[1] == '$')
+                               {
+                                       $_result .= "[\$this->_TPL['" . substr($var, 2) . "']]";
+                               }
+                               else
+                               {
+                                       $_result .= "['" . substr($var, 1) . "']";
+                               }
+                       }
+                       else if (substr($var,0,2) == '->')
+                       {
+                               if(substr($var,2,2) == '__')
+                               {
+                                       throw new Template_Exception('call to internal object members is not allowed', $this);
+                               }
+                               else if (substr($var, 2, 1) == '$')
+                               {
+                                       $_output .= '->{(($var=$this->_TPL[\''.substr($var,3).'\']) && substr($var,0,2)!=\'__\') ? $_var : throw new Template_Exception("cannot access property \\"$var\\"", $this)}';
+                               }
+                       }
+                       else
+                       {
+                               throw new Template_Exception('$' . $var_name.implode('', $variable) . ' is an invalid reference', $this);
+                       }
+               }
+               return $_result;
+       }
+
+       function _parse_modifier($variable, $modifiers)
+       {
+               $_match         = array();
+               $_mods          = array();              // stores all modifiers
+               $_args          = array();              // modifier arguments
+
+               preg_match_all('!\|(@?\w+)((?>:(?:'. $this->_qstr_regexp . '|[^|]+))*)!', '|' . $modifiers, $_match);
+               list(, $_mods, $_args) = $_match;
+
+               $count_mods = count($_mods);
+               for ($i = 0, $for_max = $count_mods; $i < $for_max; $i++)
+               {
+                       preg_match_all('!:(' . $this->_qstr_regexp . '|[^:]+)!', $_args[$i], $_match);
+                       $_arg = $_match[1];
+
+                       if ($_mods[$i][0] == '@')
+                       {
+                               $_mods[$i] = substr($_mods[$i], 1);
+                               $_map_array = 0;
+                       } else {
+                               $_map_array = 1;
+                       }
+
+                       foreach($_arg as $key => $value)
+                       {
+                               $_arg[$key] = $this->_parse_variable($value);
+                       }
+
+                       if ($this->_plugin_exists($_mods[$i], "modifier") || function_exists($_mods[$i]))
+                       {
+                               if (count($_arg) > 0)
+                               {
+                                       $_arg = ', '.implode(', ', $_arg);
+                               }
+                               else
+                               {
+                                       $_arg = '';
+                               }
+
+                               $php_function = "PHP";
+                               if ($this->_plugin_exists($_mods[$i], "modifier"))
+                               {
+                                       $php_function = "plugin";
+                               }
+                               $variable = "\$this->_run_modifier($variable, '$_mods[$i]', '$php_function', $_map_array$_arg)";
+                       }
+                       else
+                       {
+                               throw new Template_Exception("'" . $_mods[$i] . "' modifier does not exist", $this);
+                       }
+               }
+               return $variable;
+       }
+
+       function _plugin_exists($function, $type)
+       {
+               // check for object functions
+               if (isset($this->_plugins[$type][$function]) && is_array($this->_plugins[$type][$function]) && is_object($this->_plugins[$type][$function][0]) && method_exists($this->_plugins[$type][$function][0], $this->_plugins[$type][$function][1]))
+               {
+                       return '$this->_plugins[\'' . $type . '\'][\'' . $function . '\'][0]->' . $this->_plugins[$type][$function][1];
+               }
+               // check for standard functions
+               if (isset($this->_plugins[$type][$function]) && is_callable($this->_plugins[$type][$function]))
+               {
+                       return $this->_plugins[$type][$function];
+               }
+               // check for a plugin in the plugin directory
+               if (file_exists($this->_get_plugin_dir($type . '.' . $function . '.php') . $type . '.' . $function . '.php'))
+               {
+                       require_once($this->_get_plugin_dir($type . '.' . $function . '.php') . $type . '.' . $function . '.php');
+                       if (function_exists('tpl_' . $type . '_' . $function))
+                       {
+                               $this->_require_stack[$type . '.' . $function . '.php'] = array($type, $function, 'tpl_' . $type . '_' . $function);
+                               return ('tpl_' . $type . '_' . $function);
+                       }
+               }
+               return false;
+       }
+
+       function _load_filters()
+       {
+               if (count($this->_plugins['prefilter']) > 0)
+               {
+                       foreach ($this->_plugins['prefilter'] as $filter_name => $prefilter)
+                       {
+                               if (!function_exists($prefilter))
+                               {
+                                       @include_once( $this->_get_plugin_dir("prefilter." . $filter_name . ".php") . "prefilter." . $filter_name . ".php");
+                               }
+                       }
+               }
+               if (count($this->_plugins['postfilter']) > 0)
+               {
+                       foreach ($this->_plugins['postfilter'] as $filter_name => $postfilter)
+                       {
+                               if (!function_exists($postfilter))
+                               {
+                                       @include_once( $this->_get_plugin_dir("postfilter." . $filter_name . ".php") . "postfilter." . $filter_name . ".php");
+                               }
+                       }
+               }
+       }
+}
+
+?>
diff --git a/include/libs/template_lite/class.config.php b/include/libs/template_lite/class.config.php
new file mode 100644 (file)
index 0000000..a16b16c
--- /dev/null
@@ -0,0 +1,165 @@
+<?php\r
+/*\r
+ * Project:    template_lite, a smarter template engine\r
+ * File:       class.config.php\r
+ * Author:     Paul Lockaby <paul@paullockaby.com>, Mark Dickenson <akapanamajack@sourceforge.net>\r
+ * Copyright:  2003,2004,2005 by Paul Lockaby, 2005,2006 Mark Dickenson\r
+ *\r
+ * This library is free software; you can redistribute it and/or\r
+ * modify it under the terms of the GNU Lesser General Public\r
+ * License as published by the Free Software Foundation; either\r
+ * version 2.1 of the License, or (at your option) any later version.\r
+ *\r
+ * This library is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+ * Lesser General Public License for more details.\r
+ *\r
+ * You should have received a copy of the GNU Lesser General Public\r
+ * License along with this library; if not, write to the Free Software\r
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+ *\r
+ * The latest version of template_lite can be obtained from:\r
+ * http://templatelite.sourceforge.net\r
+ *\r
+ */\r
+\r
+class config {\r
+       var $overwrite                  = false;        // overwrite variables of the same name? if false, an array will be created\r
+       var $booleanize                 = true;         // turn true/false, yes/no, on/off, into 1/0\r
+       var $fix_new_lines              = true;         // turns \r\n into \n?\r
+       var $read_hidden                = true;         // read hidden sections?\r
+\r
+       var $_db_qstr_regexp            = null;\r
+       var $_bool_true_regexp          = null;\r
+       var $_bool_false_regexp         = null;\r
+       var $_qstr_regexp               = null;\r
+\r
+       function config()\r
+       {\r
+               $this->_db_qstr_regexp = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';\r
+               $this->_bool_true_regexp = 'true|yes|on';\r
+               $this->_bool_false_regexp = 'false|no|off';\r
+               $this->_qstr_regexp = '(?:' . $this->_db_qstr_regexp . '|' . $this->_bool_true_regexp . '|' . $this->_bool_false_regexp . ')';\r
+       }\r
+\r
+       function config_load($file, $section_name = null, $var_name = null)\r
+       {\r
+               $_result = array();\r
+               $contents = file_get_contents($file);\r
+               if (empty($contents))\r
+               {\r
+                       die("Could not open $file");\r
+               }\r
+\r
+               // insert new line into beginning of file\r
+               $contents = "\n" . $contents;\r
+               // fix new-lines\r
+               if ($this->fix_new_lines)\r
+               {\r
+                       $contents = str_replace("\r\n","\n",$contents);\r
+               }\r
+\r
+               // match globals\r
+               if (preg_match("/^(.*?)(\n\[|\Z)/s", $contents, $match))\r
+               {\r
+                       $_result["globals"] = $this->_parse_config_section($match[1]);\r
+               }\r
+\r
+               // match sections\r
+               if (preg_match_all("/^\[(.*?)\]/m", $contents, $match))\r
+               {\r
+                       foreach ($match[1] as $section)\r
+                       {\r
+                               if ($section{0} == '.' && !$this->read_hidden)\r
+                               {\r
+                                       continue;\r
+                               }\r
+                               preg_match("/\[".preg_quote($section)."\](.*?)(\n\[|\Z)/s",$contents,$match);\r
+                               if ($section{0} == '.')\r
+                               {\r
+                                       $section = substr($section, 1);\r
+                               }\r
+                               $_result[$section] = $this->_parse_config_section($match[1]);\r
+                       }\r
+               }\r
+\r
+\r
+               if (!empty($var_name))\r
+               {\r
+                       if (empty($section_name))\r
+                       {\r
+                               return $_result["globals"][$var_name];\r
+                       }\r
+                       else\r
+                       {\r
+                               if(isset($_result[$section_name][$var_name]))\r
+                               {\r
+                                       return $_result[$section_name][$var_name];\r
+                               }\r
+                               else\r
+                               {\r
+                                       return array();\r
+                               }\r
+                       }\r
+               }\r
+               else\r
+               {\r
+                       if (empty($section_name))\r
+                       {\r
+                               return $_result;\r
+                       }\r
+                       else\r
+                       {\r
+                               if(isset($_result[$section_name]))\r
+                               {\r
+                                       return $_result[$section_name];\r
+                               }\r
+                               else\r
+                               {\r
+                                       return array();\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+\r
+       function _parse_config_section($body)\r
+       {\r
+               $_result = array();\r
+               preg_match_all('!(\n\s*[a-zA-Z0-9_]+)\s*=\s*(' . $this->_qstr_regexp . ')!s', $body, $ini);\r
+               $keys = $ini[1];\r
+               $values = $ini[2];\r
+               for($i = 0, $for_max = count($ini[0]); $i < $for_max; $i++)\r
+               {\r
+                       if ($this->booleanize)\r
+                       {\r
+                               if (preg_match('/^(' . $this->_bool_true_regexp . ')$/i', $values[$i]))\r
+                               {\r
+                                       $values[$i] = true;\r
+                               }\r
+                               elseif (preg_match('/^(' . $this->_bool_false_regexp . ')$/i', $values[$i]))\r
+                               {\r
+                                       $values[$i] = false;\r
+                               }\r
+                       }\r
+                       if (!is_numeric($values[$i]) && !is_bool($values[$i]))\r
+                       {\r
+                               $values[$i] = str_replace("\n",'',stripslashes(substr($values[$i], 1, -1)));\r
+                       }\r
+                       if ($this->overwrite || !isset($_result[trim($keys[$i])]))\r
+                       {\r
+                               $_result[trim($keys[$i])] = $values[$i];\r
+                       }\r
+                       else\r
+                       {\r
+                               if (!is_array($_result[trim($keys[$i])]))\r
+                               {\r
+                                       $_result[trim($keys[$i])] = array($_result[trim($keys[$i])]);\r
+                               }\r
+                               $_result[trim($keys[$i])][] = $values[$i];\r
+                       }\r
+               }\r
+               return $_result;\r
+       }\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/class.parser.php b/include/libs/template_lite/class.parser.php
new file mode 100644 (file)
index 0000000..8937dfc
--- /dev/null
@@ -0,0 +1,344 @@
+<?php
+
+class Template_Syntax_Exception extends Exception
+{
+}
+
+class Template_Parser
+{
+    const ATTR_STATE_NAME = 0;
+    const ATTR_STATE_SEPARATOR = 1;
+    const ATTR_STATE_VALUE = 2;
+
+    const RESERVED_TPL_VAR_NAME = '(?:smarty|tpl|templatelite)';
+
+    // Methods to extend and rewrite
+    // These are actually just rewriting your template to the same code, for testing purpose
+    public function processString($content)
+    {
+        if (is_array($content))
+        {
+            return '"'.implode('', $content).'"';
+        }
+        else
+        {
+            return '"'.$content.'"';
+        }
+    }
+
+    public function processModifier($name, $content, $arguments, $map_array)
+    {
+        return "$content|$name";
+    }
+
+    public function processVariable($name)
+    {
+        return '$'.$name;
+    }
+
+    // These are parser methods, you should not extend or rewrite them
+
+    public function __construct()
+    {
+        // matches double quoted strings:
+        // "foobar"
+        // "foo\"bar"
+        // "foobar" . "foo\"bar"
+        $this->_db_qstr_regexp = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
+
+        // matches single quoted strings:
+        // 'foobar'
+        // 'foo\'bar'
+        $this->_si_qstr_regexp = '\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'';
+
+        // matches single or double quoted strings
+        $this->_qstr_regexp = '(?:' . $this->_db_qstr_regexp . '|' . $this->_si_qstr_regexp . ')';
+
+        // matches bracket portion of vars
+        // [0]
+        // [foo]
+        // [$bar]
+        // [#bar#]
+        $this->_var_bracket_regexp = '\[[\$|\#]?\w+\#?\]';
+        //             $this->_var_bracket_regexp = '\[\$?[\w\.]+\]';
+
+        // matches section vars:
+        // %foo.bar%
+        $this->_svar_regexp = '\%\w+\.\w+\%';
+
+        // matches $ vars (not objects):
+        // $foo
+        // $foo[0]
+        // $foo[$bar]
+        // $foo[5][blah]
+        # $this->_dvar_regexp = '\$[a-zA-Z0-9_]{1,}(?:' . $this->_var_bracket_regexp . ')*(?:' . $this->_var_bracket_regexp . ')*';
+        $this->_dvar_regexp = '\$[a-zA-Z0-9_]{1,}(?:' . $this->_var_bracket_regexp . ')*(?:\.\$?\w+(?:' . $this->_var_bracket_regexp . ')*)*';
+
+        // matches config vars:
+        // #foo#
+        // #foobar123_foo#
+        $this->_cvar_regexp = '\#[a-zA-Z0-9_]{1,}(?:' . $this->_var_bracket_regexp . ')*(?:' . $this->_var_bracket_regexp . ')*\#';
+
+        // matches valid variable syntax:
+        // $foo
+        // 'text'
+        // "text"
+        $this->_var_regexp = '(?:(?:' . $this->_dvar_regexp . '|' . $this->_cvar_regexp . ')|' . $this->_qstr_regexp . ')';
+
+        // matches valid modifier syntax:
+        // |foo
+        // |@foo
+        // |foo:"bar"
+        // |foo:$bar
+        // |foo:"bar":$foobar
+        // |foo|bar
+        $this->_mod_regexp = '(?:\|@?[0-9a-zA-Z_]+(?::(?>-?\w+|' . $this->_dvar_regexp . '|' . $this->_qstr_regexp .'))*)';
+
+        // matches valid function name:
+        // foo123
+        // _foo_bar
+        $this->_func_regexp = '(?:[a-zA-Z_]+\:\:)?[a-zA-Z_]+';
+        //             $this->_func_regexp = '[a-zA-Z_]\w*';
+    }
+
+    protected function parseArguments($args_str)
+    {
+        $result = array();
+        $last_value = '';
+        $state = self::ATTR_STATE_NAME;
+
+        preg_match_all('/(?:' . $this->_qstr_regexp . ' | (?>[^"\'=\s]+))+|[=]/x', $args_str, $match);
+
+        foreach ($match[0] as $value)
+        {
+            if ($state == self::ATTR_STATE_NAME)
+            {
+                if (!is_string($value))
+                    throw new Template_Syntax_Exception("Invalid attribute name '".$value."'.");
+
+                $attr_name = $value;
+                $state = self::ATTR_STATE_SEPARATOR;
+            }
+            elseif ($state == self::ATTR_STATE_SEPARATOR)
+            {
+                if ($value != '=')
+                    throw new Template_Syntax_Exception("Expecting '=' after '".$last_value."'");
+
+                $state = self::ATTR_STATE_VALUE;
+            }
+            elseif ($state == self::ATTR_STATE_VALUE)
+            {
+                if ($value == '=')
+                    throw new Template_Syntax_Exception("Unexpected '=' after '".$last_value."'");
+
+                if ($value == 'yes' || $value == 'on' || $value == 'true')
+                    $value = true;
+                elseif ($value == 'no' || $value == 'off' || $value == 'false')
+                    $value = false;
+                elseif ($value == 'null')
+                    $value = null;
+
+                if (preg_match_all('/(?:(' . $this->_var_regexp . '|' . $this->_svar_regexp . ')(' . $this->_mod_regexp . '*))(?:\s+(.*))?/xs', $value, $variables))
+                {
+                    list($value) = $this->parseVariables($variables[1], $variables[2]);
+                    $result[$attr_name] = $value;
+                }
+                else
+                {
+                    $result[$attr_name] = $value;
+                }
+
+                $state = self::ATTR_STATE_NAME;
+            }
+
+            $last_value = $value;
+        }
+
+        if ($state == self::ATTR_STATE_SEPARATOR)
+            throw new Template_Syntax_Exception("Expecting '=' after '".$last_value."'");
+        elseif ($state == self::ATTR_STATE_VALUE)
+            throw new Template_Syntax_Exception("Missing attribute value after '".$last_value."'");
+
+        return $result;
+    }
+
+    protected function parseVariables($variables, $modifiers)
+    {
+        $result = array();
+
+        foreach($variables as $key => $value)
+        {
+            $tag_variable = trim($variables[$key]);
+            /*
+            if(!empty($this->default_modifiers) && !preg_match('!(^|\|)templatelite:nodefaults($|\|)!',$modifiers[$key]))
+            {
+                $_default_mod_string = implode('|',(array)$this->default_modifiers);
+                $modifiers[$key] = empty($modifiers[$key]) ? $_default_mod_string : $_default_mod_string . '|' . $modifiers[$key];
+            }*/
+
+            if (empty($modifiers[$key]))
+            {
+                $result[] = $this->parseVariable($tag_variable);
+            }
+            else
+            {
+                $result[] = $this->parseModifier($this->parseVariable($tag_variable), $modifiers[$key]);
+            }
+        }
+
+        return $result;
+    }
+
+    protected function parseVariable($variable)
+    {
+        // replace variable with value
+        if ($variable[0] == '$')
+        {
+            // replace the variable
+            #return $this->_compile_variable($variable);
+            return $this->processVariable(substr($variable, 1));
+        }
+        elseif ($variable[0] == '#')
+        {
+            // replace the config variable
+            #return $this->_compile_config($variable);
+            return $this->processConfigVariable(substr($variable, 1));
+        }
+        elseif ($variable[0] == '"')
+        {
+            // Parse classic string
+            $variable = substr($variable, 1, -1);
+            $result = array();
+
+            // replace all quoted variables by simple variables
+            $variable = preg_replace('!`('.$this->_dvar_regexp.')`!', '\\1', $variable);
+
+            // Split string between variables, if they are not escaped
+            // (will parse "hi $name" but no "hi \$name"
+            $parts = preg_split('!(^|[^\\])('.$this->_dvar_regexp.')!', $variable, -1,
+                PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_NO_EMPTY);
+
+            foreach ($parts as $key=>$part)
+            {
+                if ($part[0][0] == '$')
+                    $result[] = $this->processVariable(substr($part[0], 1));
+                else
+                    $result[] = $part[0];
+            }
+
+            return $this->processString($result);
+
+/*
+            // expand the quotes to pull any variables out of it
+            // fortunately variables inside of a quote aren't fancy, no modifiers, no quotes
+            //   just get everything from the $ to the ending space and parse it
+            // if the $ is escaped, then we won't expand it
+            //preg_match_all('/(?:[^\\\]' . $this->_dvar_regexp . ')/', $variable, $expand);  // old match
+            // preg_match_all('/(?:[^\\\]' . $this->_dvar_regexp . '[^\\\])/', $variable, $_expand);
+            if (($pos = strpos($variable, '$')) !== false)
+            {
+                while ($pos !== false)
+                {
+                }
+            }
+
+            foreach($expand as $key => $value)
+            {
+                $expand[$key] = trim($value);
+                if (($pos = strpos($expand[$key], '$')) > 0)
+                {
+                    $expand[$key] = substr($expand[$key], $pos);
+                }
+            }
+
+            $result = $variable;
+            foreach($expand as $value)
+            {
+                $value = trim($value);
+                $result = str_replace($value, '" . ' . $this->parseVariable($value) . ' . "', $result);
+            }
+            $result = str_replace("`", "", $result);
+
+            return $result;
+        */
+        }
+        elseif ($variable[0] == "'")
+        {
+            // return the value just as it is
+            return $this->processString(substr($variable, 1, -1));
+        }
+        elseif ($variable[0] == '%')
+        {
+            return $this->parseSection($variable);
+        }
+        else
+        {
+            // return it as is; i believe that there was a reason before that i did not just return it as is,
+            // but i forgot what that reason is ...
+            // the reason i return the variable 'as is' right now is so that unquoted literals are allowed
+            return $this->processString($variable);
+        }
+    }
+
+    protected function parseModifier($variable, $modifiers)
+    {
+        $mods = array(); // stores all modifiers
+        $args = array(); // modifier arguments
+
+        preg_match_all('!\|(@?\w+)((?>:(?:'. $this->_qstr_regexp . '|[^|]+))*)!', '|' . $modifiers, $match);
+        list(, $mods, $args) = $match;
+
+        $count_mods = count($mods);
+        for ($i = 0, $for_max = $count_mods; $i < $for_max; $i++)
+        {
+            preg_match_all('!:(' . $this->_qstr_regexp . '|[^:]+)!', $args[$i], $match);
+            $arg = $match[1];
+
+            if ($mods[$i]{0} == '@')
+            {
+                $mods[$i] = substr($mods[$i], 1);
+                $map_array = 0;
+            }
+            else
+            {
+                $map_array = 1;
+            }
+
+            foreach($arg as $key => $value)
+            {
+                $arg[$key] = $this->parseVariable($value);
+            }
+
+            //$variable = $this->callCompiler('modifier', array('name' => $mods[$i], 'content' => $variable, 'misc' => $map_array, 'args' => $arg));
+            $variable = $this->processModifier($mods[$i], $variable, $arg, $map_array);
+            /*
+            if ($this->_plugin_exists($_mods[$i], "modifier") || function_exists($_mods[$i]))
+            {
+                if (count($_arg) > 0)
+                {
+                    $_arg = ', '.implode(', ', $_arg);
+                }
+                else
+                {
+                    $_arg = '';
+                }
+
+                $php_function = "PHP";
+                if ($this->_plugin_exists($_mods[$i], "modifier"))
+                {
+                    $php_function = "plugin";
+                }
+                $variable = "\$this->_run_modifier($variable, '$_mods[$i]', '$php_function', $_map_array$_arg)";
+            }
+            else
+            {
+                $variable = "\$this->trigger_error(\"'" . $_mods[$i] . "' modifier does not exist\", E_USER_NOTICE, __FILE__, __LINE__);";
+            }*/
+        }
+
+        return $variable;
+    }
+
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/class.template.php b/include/libs/template_lite/class.template.php
new file mode 100644 (file)
index 0000000..009b317
--- /dev/null
@@ -0,0 +1,969 @@
+<?php
+/*
+ * Project:    template_lite, a smarter template engine
+ * File:       class.template.php
+ * Author:     Paul Lockaby <paul@paullockaby.com>, Mark Dickenson <akapanamajack@sourceforge.net>
+ * Copyright:  2003,2004,2005 by Paul Lockaby, 2005,2006 Mark Dickenson
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * The latest version of template_lite can be obtained from:
+ * http://templatelite.sourceforge.net
+ *
+ */
+
+if (!defined('TEMPLATE_LITE_DIR')) {
+       define('TEMPLATE_LITE_DIR', dirname(__FILE__) . DIRECTORY_SEPARATOR);
+}
+
+class Template_Exception extends RuntimeException
+{
+       public function __construct($message, &$tpl = null)
+       {
+               if (!is_null($tpl) && is_object($tpl))
+               {
+                       $message = '[in ' . $tpl->_file . ' line ' . $tpl->_linenum . '] ' . $message;
+               }
+
+               parent::__construct($message);
+       }
+}
+
+class Template_Lite {
+       // public configuration variables
+       var $left_delimiter                     = "{";          // the left delimiter for template tags
+       var $right_delimiter                    = "}";          // the right delimiter for template tags
+       var $cache                      = false;        // whether or not to allow caching of files
+       var $force_compile              = false;        // force a compile regardless of saved state
+       var $template_dir               = "templates";  // where the templates are to be found
+       var $plugins_dir                        = array("plugins");     // where the plugins are to be found
+       var $compile_dir                = "compiled";   // the directory to store the compiled files in
+       var $config_dir                 = "templates";  // where the config files are
+       var $cache_dir                  = "cached";     // where cache files are stored
+       var $config_overwrite           = false;
+       var $config_booleanize          = true;
+       var $config_fix_new_lines       = true;
+       var $config_read_hidden         = true;
+       var $cache_lifetime             = 0;            // how long the file in cache should be considered "fresh"
+       var $encode_file_name           =       true;   // Set this to false if you do not want the name of the compiled/cached file to be md5 encoded.
+       var $php_extract_vars           =       false;  // Set this to true if you want the $this->_tpl variables to be extracted for use by PHP code inside the template.
+       var $reserved_template_varname = "templatelite";
+       var $default_modifiers          = array();
+       var $debugging     =  false;
+
+       var $compiler_file        =    'class.compiler.php';
+       var $compiler_class        =   'Template_Lite_Compiler';
+       var $config_class          =   'config';
+
+       // gzip output configuration
+       var $send_now                   =  1;
+       var $force_compression  =  0;
+       var $compression_level  =  9;
+       var $enable_gzip                =  1;
+
+       // private internal variables
+       var $_vars              = array();      // stores all internal assigned variables
+       var $_confs             = array();      // stores all internal config variables
+       var $_plugins           = array(           'modifier'     => array(),
+                                                                          'function'     => array(),
+                                                                          'block'               => array(),
+                                                                          'compiler'     => array(),
+                                                                          'resource'     => array(),
+                                                                          'prefilter'   => array(),
+                                                                          'postfilter' => array(),
+                                                                          'outputfilter'  => array());
+       var $_linenum           = 0;            // the current line number in the file we are processing
+       var $_file              = "";           // the current file we are processing
+       var $_config_obj        = null;
+       var $_compile_obj       = null;
+       var $_cache_id          = null;
+       var $_cache_dir         = "";           // stores where this specific file is going to be cached
+       var $_cache_info        = array('config' => array(), 'template' => array());
+       var $_sl_md5            = '39fc70570b8b60cbc1b85839bf242aff';
+       var $_version           = 'V2.10 Template Lite 4 January 2007  (c) 2005-2007 Mark Dickenson. All rights reserved. Released LGPL.';
+       var $_version_date      = "2007-01-04 10:34:21";
+       var $_config_module_loaded = false;
+       var $_templatelite_debug_info   = array();
+       var $_templatelite_debug_loop   = false;
+       var $_templatelite_debug_dir    = "";
+       var $_inclusion_depth     = 0;
+       var $_null = null;
+       var $_resource_type = 1;
+       var $_resource_time;
+       var $_sections = array();
+       var $_foreach = array();
+
+       function Template_Lite()
+       {
+               $this->_version_date = strtotime($this->_version_date);
+               $this->_base_plugins = $this->_plugins;
+       }
+
+       function load_filter($type, $name)
+       {
+               switch ($type)
+               {
+                       case 'output':
+                               include_once( $this->_get_plugin_dir($type . "filter." . $name . ".php") . $type . "filter." . $name . ".php");
+                               $this->_plugins['outputfilter'][$name] = "template_" . $type . "filter_" . $name;
+                          break;
+                       case 'pre':
+                       case 'post':
+                               if (!isset($this->_plugins[$type . 'filter'][$name]))
+                               {
+                                       $this->_plugins[$type . 'filter'][$name] = "template_" . $type . "filter_" . $name;
+                               }
+                               break;
+               }
+       }
+
+       function assign($key, $value = null)
+       {
+               if (is_array($key))
+               {
+                       foreach($key as $var => $val)
+                               if ($var != "")
+                               {
+                                       $this->_vars[$var] = $val;
+                               }
+               }
+               else
+               {
+                       if ($key != "")
+                       {
+                               $this->_vars[$key] = $value;
+                       }
+               }
+       }
+
+       function assign_by_ref($key, $value = null)
+       {
+               if ($key != '')
+               {
+                       $this->_vars[$key] = &$value;
+               }
+       }
+
+       function assign_config($key, $value = null)
+       {
+               if (is_array($key))
+               {
+                       foreach($key as $var => $val)
+                       {
+                               if ($var != "")
+                               {
+                                       $this->_confs[$var] = $val;
+                               }
+                       }
+               }
+               else
+               {
+                       if ($key != "")
+                       {
+                               $this->_confs[$key] = $value;
+                       }
+               }
+       }
+
+       function append($key, $value=null, $merge=false)
+       {
+               if (is_array($key))
+               {
+                       foreach ($key as $_key => $_value)
+                       {
+                               if ($_key != '')
+                               {
+                                       if(!@is_array($this->_vars[$_key]))
+                                       {
+                                               settype($this->_vars[$_key],'array');
+                                       }
+                                       if($merge && is_array($_value))
+                                       {
+                                               foreach($_value as $_mergekey => $_mergevalue)
+                                               {
+                                                       $this->_vars[$_key][$_mergekey] = $_mergevalue;
+                                               }
+                                       }
+                                       else
+                                       {
+                                               $this->_vars[$_key][] = $_value;
+                                       }
+                               }
+                       }
+               }
+               else
+               {
+                       if ($key != '' && isset($value))
+                       {
+                               if(!@is_array($this->_vars[$key]))
+                               {
+                                       settype($this->_vars[$key],'array');
+                               }
+                               if($merge && is_array($value))
+                               {
+                                       foreach($value as $_mergekey => $_mergevalue)
+                                       {
+                                               $this->_vars[$key][$_mergekey] = $_mergevalue;
+                                       }
+                               }
+                               else
+                               {
+                                       $this->_vars[$key][] = $value;
+                               }
+                       }
+               }
+       }
+
+       function append_by_ref($key, &$value, $merge=false)
+       {
+               if ($key != '' && isset($value))
+               {
+                       if(!@is_array($this->_vars[$key]))
+                       {
+                               settype($this->_vars[$key],'array');
+                       }
+                       if ($merge && is_array($value))
+                       {
+                               foreach($value as $_key => $_val)
+                               {
+                                       $this->_vars[$key][$_key] = &$value[$_key];
+                               }
+                       }
+                       else
+                       {
+                               $this->_vars[$key][] = &$value;
+                       }
+               }
+       }
+
+       function clear_assign($key = null)
+       {
+               if ($key == null)
+               {
+                       $this->_vars = array();
+               }
+               else
+               {
+                       if (is_array($key))
+                       {
+                               foreach($key as $index => $value)
+                               {
+                                       if (in_array($value, $this->_vars))
+                                       {
+                                               unset($this->_vars[$index]);
+                                       }
+                               }
+                       }
+                       else
+                       {
+                               if (in_array($key, $this->_vars))
+                               {
+                                       unset($this->_vars[$index]);
+                               }
+                       }
+               }
+       }
+
+       function clear_all_assign()
+       {
+               $this->_vars = array();
+       }
+
+       function clear_config($key = null)
+       {
+               if ($key == null)
+               {
+                       $this->_conf = array();
+               }
+               else
+               {
+                       if (is_array($key))
+                       {
+                               foreach($key as $index => $value)
+                               {
+                                       if (in_array($value, $this->_conf))
+                                       {
+                                               unset($this->_conf[$index]);
+                                       }
+                               }
+                       }
+                       else
+                       {
+                               if (in_array($key, $this->_conf))
+                               {
+                                       unset($this->_conf[$key]);
+                               }
+                       }
+               }
+       }
+
+       function &get_template_vars($key = null)
+       {
+               if ($key == null)
+               {
+                       return $this->_vars;
+               }
+               else
+               {
+                       if (isset($this->_vars[$key]))
+                       {
+                               return $this->_vars[$key];
+                       }
+                       else
+                       {
+                               return $this->_null;
+                       }
+               }
+       }
+
+       function &get_config_vars($key = null)
+       {
+               if ($key == null)
+               {
+                       return $this->_confs;
+               }
+               else
+               {
+                       if (isset($this->_confs[$key]))
+                       {
+                               return $this->_confs[$key];
+                       }
+                       else
+                       {
+                               return $this->_null;
+                       }
+               }
+       }
+
+       function clear_compiled_tpl($file = null)
+       {
+               $this->_destroy_dir($file, null, $this->_get_dir($this->compile_dir));
+       }
+
+       function clear_cache($file = null, $cache_id = null, $compile_id = null, $exp_time = null)
+       {
+               $this->_destroy_dir($file, $cache_id, $this->_get_dir($this->cache_dir));
+       }
+
+       function clear_all_cache($exp_time = null)
+       {
+               $this->clear_cache();
+       }
+
+       function is_cached($file, $cache_id = null)
+       {
+               if (!$this->force_compile && $this->_is_cached($file, $cache_id))
+               {
+                       return true;
+               }
+               else
+               {
+                       return false;
+               }
+       }
+
+       // Warning : don't use $base, it's a temporary fix because plugin management is very crappy actually
+       function register_modifier($modifier, $implementation, $base=true)
+       {
+               if (!is_callable($implementation))
+                       throw new Template_Exception("'$implementation' modifier doesn't seem to be a valid callback");
+
+               $this->_plugins['modifier'][$modifier] = $implementation;
+               if ($base)
+                       $this->_base_plugins['modifier'][$modifier] = $implementation;
+       }
+
+       function unregister_modifier($modifier)
+       {
+               unset($this->_plugins['modifier'][$modifier]);
+       }
+
+       function register_function($function, $implementation, $base=true)
+       {
+               if (!is_callable($implementation))
+                       throw new Template_Exception("Function '$function' doesn't seem to be a valid callback");
+
+               // Non-static object callbacks are not supported now
+               if (is_array($implementation) && !is_string($function[0]))
+               {
+                       throw new Template_Exception("Unsupported object callback for function '$function'");
+               }
+
+               if (is_object($implementation))
+               {
+                       throw new Template_Exception("Closures are not supported for functions.");
+               }
+
+               $this->_plugins['function'][$function] = $implementation;
+               if ($base)
+                       $this->_base_plugins['function'][$function] = $implementation;
+       }
+
+       function unregister_function($function)
+       {
+               unset($this->_plugins['function'][$function]);
+       }
+
+       function register_block($function, $implementation, $base=true)
+       {
+               if (!is_callable($implementation))
+                       throw new Template_Exception("'$implementation' block function doesn't seem to be a valid callback");
+
+               $this->_plugins['block'][$function] = $implementation;
+               if ($base)
+                       $this->_base_plugins['block'][$function] = $implementation;
+       }
+
+       function unregister_block($function)
+       {
+               unset($this->_plugins['block'][$function]);
+       }
+
+       function register_compiler($function, $implementation, $base=true)
+       {
+               if (!is_callable($implementation))
+                       throw new Template_Exception("'$implementation' compiler function doesn't seem to be a valid callback");
+
+               $this->_plugins['compiler'][$function] = $implementation;
+               if ($base)
+                       $this->_base_plugins['compiler'][$function] = $implementation;
+       }
+
+       function unregister_compiler($function)
+       {
+               unset($this->_plugins['compiler'][$function]);
+       }
+
+       function register_prefilter($function)
+       {
+               $_name = (is_array($function)) ? $function[1] : $function;
+               $this->_plugins['prefilter'][$_name] = $_name;
+       }
+
+       function unregister_prefilter($function)
+       {
+               unset($this->_plugins['prefilter'][$function]);
+       }
+
+       function register_postfilter($function)
+       {
+               $_name = (is_array($function)) ? $function[1] : $function;
+               $this->_plugins['postfilter'][$_name] = $_name;
+       }
+
+       function unregister_postfilter($function)
+       {
+               unset($this->_plugins['postfilter'][$function]);
+       }
+
+       function register_outputfilter($function)
+       {
+               $_name = (is_array($function)) ? $function[1] : $function;
+               $this->_plugins['outputfilter'][$_name] = $_name;
+       }
+
+       function unregister_outputfilter($function)
+       {
+               unset($this->_plugins['outputfilter'][$function]);
+       }
+
+       function register_resource($type, $functions)
+       {
+               if (count($functions) == 4)
+               {
+                       $this->_plugins['resource'][$type] = $functions;
+               }
+               else
+               {
+                       throw new Template_Exception("malformed function-list for '$type' in register_resource", $this);
+               }
+       }
+
+       function unregister_resource($type)
+       {
+               unset($this->_plugins['resource'][$type]);
+       }
+
+       function template_exists($file)
+       {
+               if (file_exists($this->_get_dir($this->template_dir).$file))
+               {
+                       $this->_resource_time = filemtime($this->_get_dir($this->template_dir).$file);
+                       $this->_resource_type = 1;
+                       return true;
+               }
+               else
+               {
+                       if (file_exists($file))
+                       {
+                               $this->_resource_time = filemtime($file);
+                               $this->_resource_type = "file";
+                               return true;
+                       }
+                       return false;
+               }
+       }
+
+       function _get_resource($file)
+       {
+               $_resource_name = explode(':', trim($file));
+
+               if (count($_resource_name) == 1 || $_resource_name[0] == "file" || $_resource_name[0] == 'phar')
+        {
+                       if($_resource_name[0] == "file")
+                       {
+                               $file = substr($file, 5);
+                       }
+
+                       $exists = $this->template_exists($file);
+
+                       if (!$exists)
+                       {
+                               throw new Template_Exception("file '$file' does not exist", $this);
+                       }
+               }
+               else
+               {
+                       $this->_resource_type = $_resource_name[0];
+                       $file = substr($file, strlen($this->_resource_type) + 1);
+                       $exists = isset($this->_plugins['resource'][$this->_resource_type]) && call_user_func_array($this->_plugins['resource'][$this->_resource_type][1], array($file, &$resource_timestamp, &$this));
+
+                       if (!$exists)
+                       {
+                               throw new Template_Exception("file '$file' does not exist", $this);
+                       }
+                       $this->_resource_time = $resource_timestamp;
+               }
+               return $file;
+       }
+
+       function display($file, $cache_id = null)
+       {
+               $this->fetch($file, $cache_id, true);
+       }
+
+       function fetch($file, $cache_id = null, $display = false)
+       {
+               $file = $this->_get_resource($file);
+
+               if ($this->debugging)
+               {
+                       $this->_templatelite_debug_info[] = array('type'          => 'template',
+                                                                                               'filename'  => $file,
+                                                                                               'depth'  => 0,
+                                                                                               'exec_time' => array_sum(explode(' ', microtime())) );
+                       $included_tpls_idx = count($this->_templatelite_debug_info) - 1;
+               }
+
+               $this->_cache_id = $cache_id;
+               $this->template_dir = $this->_get_dir($this->template_dir);
+               $this->compile_dir = $this->_get_dir($this->compile_dir);
+               if ($this->cache)
+               {
+                       $this->_cache_dir = $this->_build_dir($this->cache_dir, $this->_cache_id);
+               }
+
+               $name = ($this->encode_file_name) ? md5((($this->_resource_type == 1) ? $this->template_dir.$file : $this->_resource_type . "_" . $file)).'.php' : str_replace(".", "_", str_replace("/", "_", $this->_resource_type . "_" . $file)).'.php';
+
+               $this->_error_level = $this->debugging ? error_reporting() : error_reporting(error_reporting() & ~E_NOTICE);
+//             $this->_error_level = error_reporting(E_ALL);
+
+               if (!$this->force_compile && $this->cache && $this->_is_cached($file, $cache_id))
+               {
+                       ob_start();
+                       include($this->_cache_dir.$name);
+                       $output = ob_get_contents();
+                       ob_end_clean();
+                       $output = substr($output, strpos($output, "\n") + 1);
+               }
+               else
+               {
+
+                       $output = $this->_fetch_compile($file);
+
+                       if ($this->cache)
+                       {
+                               $f = fopen($this->_cache_dir.$name, "w");
+                               fwrite($f, serialize($this->_cache_info) . "\n" . str_replace('<?xml', "<?php echo '<?xml'; ?>", $output));
+                               fclose($f);
+                       }
+               }
+
+               if (strpos($output, $this->_sl_md5) !== false)
+               {
+                       preg_match_all('!' . $this->_sl_md5 . '{_run_insert (.*)}' . $this->_sl_md5 . '!U',$output,$_match);
+                       foreach($_match[1] as $value)
+                       {
+                               $arguments = unserialize($value);
+                               $output = str_replace($this->_sl_md5 . '{_run_insert ' . $value . '}' . $this->_sl_md5, call_user_func_array('insert_' . $arguments['name'], array((array)$arguments, $this)), $output);
+                       }
+               }
+
+               foreach ($this->_plugins['outputfilter'] as $function)
+               {
+                       $output = $function($output, $this);
+               }
+
+               error_reporting($this->_error_level);
+
+               if ($this->debugging)
+               {
+                       $this->_templatelite_debug_info[$included_tpls_idx]['exec_time'] = array_sum(explode(' ', microtime())) - $this->_templatelite_debug_info[$included_tpls_idx]['exec_time'];
+               }
+
+               if ($display)
+               {
+                       echo $output;
+                       if($this->debugging && !$this->_templatelite_debug_loop)
+                       {
+                               $this->debugging = false;
+                               if(!function_exists("template_generate_debug_output"))
+                               {
+                                       require_once(TEMPLATE_LITE_DIR . "internal/template.generate_debug_output.php");
+                               }
+                               $debug_output = template_generate_debug_output($this);
+                               $this->debugging = true;
+                               echo $debug_output;
+                       }
+               }
+               else
+               {
+                       return $output;
+               }
+       }
+
+       function config_load($file, $section_name = null, $var_name = null)
+       {
+               require_once(TEMPLATE_LITE_DIR . "internal/template.config_loader.php");
+       }
+
+       function _is_cached($file, $cache_id)
+       {
+               $this->_cache_dir = $this->_get_dir($this->cache_dir, $cache_id);
+               $this->config_dir = $this->_get_dir($this->config_dir);
+               $this->template_dir = $this->_get_dir($this->template_dir);
+
+               $file = $this->_get_resource($file);
+
+               $name = ($this->encode_file_name) ? md5((($this->_resource_type == 1) ? $this->template_dir.$file : $this->_resource_type . "_" . $file)).'.php' : str_replace(".", "_", str_replace("/", "_", $this->_resource_type . "_" . $file)).'.php';
+
+               if (file_exists($this->_cache_dir.$name) && (((time() - filemtime($this->_cache_dir.$name)) < $this->cache_lifetime) || $this->cache_lifetime == -1) && (filemtime($this->_cache_dir.$name) > $this->_resource_time))
+               {
+                       $fh = fopen($this->_cache_dir.$name, "r");
+                       if (!feof($fh) && ($line = fgets($fh, filesize($this->_cache_dir.$name))))
+                       {
+                               $includes = unserialize($line);
+                               if (isset($includes['template']))
+                               {
+                                       foreach($includes['template'] as $value)
+                                       {
+                                               if (!(file_exists($this->template_dir.$value) && (filemtime($this->_cache_dir.$name) > filemtime($this->template_dir.$value))))
+                                               {
+                                                       return false;
+                                               }
+                                       }
+                               }
+                               if (isset($includes['config']))
+                               {
+                                       foreach($includes['config'] as $value)
+                                       {
+                                               if (!(file_exists($this->config_dir.$value) && (filemtime($this->_cache_dir.$name) > filemtime($this->config_dir.$value))))
+                                               {
+                                                       return false;
+                                               }
+                                       }
+                               }
+                       }
+                       fclose($fh);
+               }
+               else
+               {
+                       return false;
+               }
+               return true;
+       }
+
+       function _fetch_compile_include($_templatelite_include_file, $_templatelite_include_vars)
+       {
+               if(!function_exists("template_fetch_compile_include"))
+               {
+                       require_once(TEMPLATE_LITE_DIR . "internal/template.fetch_compile_include.php");
+               }
+               return template_fetch_compile_include($_templatelite_include_file, $_templatelite_include_vars, $this);
+       }
+
+       function _fetch_compile($file, $include=false)
+       {
+               $this->template_dir = $this->_get_dir($this->template_dir);
+
+               $name = ($this->encode_file_name) ? md5((($this->_resource_type == 1) ? $this->template_dir.$file : $this->_resource_type . "_" . $file)).'.php' : str_replace(".", "_", str_replace("/", "_", $this->_resource_type . "_" . $file)).'.php';
+
+               if ($this->cache)
+               {
+                       array_push($this->_cache_info['template'], $file);
+               }
+
+               if (!$this->force_compile && file_exists($this->compile_dir.'c_'.$name)
+                && (filemtime($this->compile_dir.'c_'.$name) > $this->_resource_time)
+                && (filemtime($this->compile_dir.'c_'.$name) > $this->_version_date))
+               {
+                       ob_start();
+                       include($this->compile_dir.'c_'.$name);
+                       $output = ob_get_contents();
+                       ob_end_clean();
+                       error_reporting($this->_error_level);
+                       return $output;
+               }
+
+               $file_contents = "";
+               if($this->_resource_type == 1)
+               {
+                       $f = fopen($this->template_dir . $file, "r");
+                       $size = filesize($this->template_dir . $file);
+                       if ($size > 0)
+                       {
+                               $file_contents = fread($f, $size);
+                       }
+               }
+               else
+               if($this->_resource_type == "file")
+               {
+                       $f = fopen($file, "r");
+                       $size = filesize($file);
+                       if ($size > 0)
+                       {
+                               $file_contents = fread($f, $size);
+                       }
+               }
+               else
+               {
+                       call_user_func_array($this->_plugins['resource'][$this->_resource_type][0], array($file, &$file_contents, &$this));
+               }
+
+               $this->_file = $file;
+               fclose($f);
+
+               if (!is_object($this->_compile_obj))
+               {
+                       if (file_exists(TEMPLATE_LITE_DIR . $this->compiler_file)) {
+                               require_once(TEMPLATE_LITE_DIR . $this->compiler_file);
+                       } else {
+                               require_once($this->compiler_file);
+                       }
+                       $this->_compile_obj = new $this->compiler_class;
+               }
+               $this->_compile_obj->left_delimiter = $this->left_delimiter;
+               $this->_compile_obj->right_delimiter = $this->right_delimiter;
+               $this->_compile_obj->plugins_dir = &$this->plugins_dir;
+               $this->_compile_obj->template_dir = &$this->template_dir;
+               $this->_compile_obj->_vars = &$this->_vars;
+               $this->_compile_obj->_confs = &$this->_confs;
+               $this->_compile_obj->_linenum = &$this->_linenum;
+               $this->_compile_obj->_file = &$this->_file;
+               $this->_compile_obj->php_extract_vars = &$this->php_extract_vars;
+               $this->_compile_obj->reserved_template_varname = &$this->reserved_template_varname;
+               $this->_compile_obj->default_modifiers = $this->default_modifiers;
+
+               // FIXME: the is a lot of bugs with _plugins because it's crappy
+               // _plugins is used to register plugins, and that's cool, but register_plugins is used also
+               // in compiled templates, so it creates a lot of bugs, we'll have to rewrite this later,
+               // but for now the most simple patch is to use a new thing : _base_plugins
+               $this->_compile_obj->_plugins = $this->_base_plugins;
+
+               $output = $this->_compile_obj->_compile_file($file_contents);
+
+               $f = fopen($this->compile_dir.'c_'.$name, "w");
+               fwrite($f, $output);
+               fclose($f);
+
+               ob_start();
+               eval(' ?>' . $output . '<?php ');
+               $output = ob_get_contents();
+               ob_end_clean();
+
+               // We're putting back used plugins before the inclusion
+               if ($include && isset($old_plugins))
+               {
+                       $this->_plugins = $old_plugins;
+                       $this->_compile_obj->_plugins = &$this->_plugins;
+               }
+
+               return $output;
+       }
+
+       function _run_modifier()
+       {
+               $arguments = func_get_args();
+               list($variable, $modifier, $php_function, $_map_array) = array_splice($arguments, 0, 4);
+               array_unshift($arguments, $variable);
+               if ($_map_array && is_array($variable))
+               {
+                       foreach($variable as $key => $value)
+                       {
+                               if($php_function == "PHP")
+                               {
+                                       $variable[$key] = call_user_func_array($modifier, $arguments);
+                               }
+                               else
+                               {
+                                       $variable[$key] = call_user_func_array($this->_plugins["modifier"][$modifier], $arguments);
+                               }
+                       }
+               }
+               else
+               {
+                       if($php_function == "PHP")
+                       {
+                               $variable = call_user_func_array($modifier, $arguments);
+                       }
+                       else
+                       {
+                               $variable = call_user_func_array($this->_plugins["modifier"][$modifier], $arguments);
+                       }
+               }
+
+               return $variable;
+       }
+
+       function _run_insert($arguments)
+       {
+               if ($this->cache)
+               {
+                       return $this->_sl_md5 . '{_run_insert ' . serialize((array)$arguments) . '}' . $this->_sl_md5;
+               }
+               else
+               {
+                       if (!function_exists('insert_' . $arguments['name']))
+                       {
+                               throw new Template_Exception("function 'insert_" . $arguments['name'] . "' does not exist in 'insert'", $this);
+                       }
+                       if (isset($arguments['assign']))
+                       {
+                               $this->assign($arguments['assign'], call_user_func_array('insert_' . $arguments['name'], array((array)$arguments, $this)));
+                       }
+                       else
+                       {
+                               return call_user_func_array('insert_' . $arguments['name'], array((array)$arguments, $this));
+                       }
+               }
+       }
+
+       function _get_dir($dir, $id = null)
+       {
+               if (empty($dir))
+               {
+                       $dir = '.';
+               }
+               if (substr($dir, -1) != DIRECTORY_SEPARATOR)
+               {
+                       $dir .= DIRECTORY_SEPARATOR;
+               }
+               if (!empty($id))
+               {
+                       $_args = explode('|', $id);
+                       if (count($_args) == 1 && empty($_args[0]))
+                       {
+                               return $dir;
+                       }
+                       foreach($_args as $value)
+                       {
+                               $dir .= $value.DIRECTORY_SEPARATOR;
+                       }
+               }
+               return $dir;
+       }
+
+       function _get_plugin_dir($plugin_name)
+       {
+               static $_path_array = null;
+
+               $plugin_dir_path = "";
+               $_plugin_dir_list = is_array($this->plugins_dir) ? $this->plugins_dir : (array)$this->plugins_dir;
+               foreach ($_plugin_dir_list as $_plugin_dir)
+               {
+                       if (!preg_match("/^([\/\\\\]|[a-zA-Z]:[\/\\\\])/", $_plugin_dir))
+                       {
+                               // path is relative
+                               if (file_exists(dirname(__FILE__) . DIRECTORY_SEPARATOR . $_plugin_dir . DIRECTORY_SEPARATOR . $plugin_name))
+                               {
+                                       $plugin_dir_path = dirname(__FILE__) . DIRECTORY_SEPARATOR . $_plugin_dir . DIRECTORY_SEPARATOR;
+                                       break;
+                               }
+                       }
+                       else
+                       {
+                               // path is absolute
+                               if(!isset($_path_array))
+                               {
+                                       $_ini_include_path = ini_get('include_path');
+
+                                       if(strstr($_ini_include_path,';'))
+                                       {
+                                               // windows pathnames
+                                               $_path_array = explode(';',$_ini_include_path);
+                                       }
+                                       else
+                                       {
+                                               $_path_array = explode(':',$_ini_include_path);
+                                       }
+                               }
+
+                               if(!in_array($_plugin_dir,$_path_array))
+                               {
+                                       array_unshift($_path_array,$_plugin_dir);
+                               }
+
+                               foreach ($_path_array as $_include_path)
+                               {
+                                       if (file_exists($_include_path . DIRECTORY_SEPARATOR . $plugin_name))
+                                       {
+                                               $plugin_dir_path = $_include_path . DIRECTORY_SEPARATOR;
+                                               break 2;
+                                       }
+                               }
+                       }
+               }
+               return $plugin_dir_path;
+       }
+
+//     function _parse_resource_link($resource_link)
+//     {
+//             $stuffing = "file:/this/is/the/time_5-23.tpl";
+//             $stuffing_data = explode(":", $stuffing);
+//             preg_match_all('/(?:([0-9a-z._-]+))/i', $stuffing, $stuff);
+//             print_r($stuff);
+//             echo "<br>Path: " . str_replace($stuff[0][count($stuff[0]) - 1], "", $stuffing);
+//             echo "<br>Filename: " . $stuff[0][count($stuff[0]) - 1];
+//     }
+
+       function _build_dir($dir, $id)
+       {
+               if(!function_exists("template_build_dir"))
+               {
+                       require_once(TEMPLATE_LITE_DIR . "internal/template.build_dir.php");
+               }
+               return template_build_dir($dir, $id, $this);
+       }
+
+       function _destroy_dir($file, $id, $dir)
+       {
+               if(!function_exists("template_destroy_dir"))
+               {
+                       require_once(TEMPLATE_LITE_DIR . "internal/template.destroy_dir.php");
+               }
+               return template_destroy_dir($file, $id, $dir, $this);
+       }
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/class.tokenparser.php b/include/libs/template_lite/class.tokenparser.php
new file mode 100644 (file)
index 0000000..39caa22
--- /dev/null
@@ -0,0 +1,291 @@
+<?php
+
+class Template_Syntax_Exception extends Exception
+{
+}
+
+class Template_Parser
+{
+    const CONTEXT_ARGUMENT = 10;
+    const CONTEXT_STRING = 11;
+
+    public $left_delimiter = '{';
+    public $right_delimiter = '}';
+
+    protected $_allowed_in_variable = array(T_VARIABLE, T_OBJECT_OPERATOR, T_STRING,
+        T_CONSTANT_ENCAPSED_STRING, T_ENCAPSED_AND_WHITESPACE, T_DOUBLE_COLON, '(', ')');
+
+    protected $_allowed_in_string = array(T_STRING,
+        T_CONSTANT_ENCAPSED_STRING, T_ENCAPSED_AND_WHITESPACE);
+
+    public $reserved_var_name = '(?:smarty|tpl|templatelite)';
+
+    public function __construct()
+    {
+    }
+
+    protected function processVariable($variable, $modifiers = array())
+    {
+        return '--var('.$variable.')';
+    }
+
+/*
+    public function parse($content)
+    {
+        $in_literal = false;
+
+        $pos = strpos($content, $this->left_delimiter);
+
+        while ($pos !== false)
+        {
+            if (($end = strpos($content,
+            if ($in_literal)
+        }
+
+        $tokens = token_get_all('<?php '.$content.' ?>');
+
+        foreach ($tokens as $token)
+        {
+            if (is_array($token))
+                list($token, $text, $line) = $token;
+
+            switch ($token)
+            {
+                case T_OPEN_TAG:
+                    break;
+                default:
+                    if ($token >= 300)
+                        echo token_name($token) . ": ".$text;
+                    else echo $token;
+                    echo "\n";
+                    break;
+            }
+        }
+    }
+*/
+
+    public function parseArguments($string)
+    {
+        $args = array();
+        $status = 0; // 0 = nothing, 1 = arg named, waiting for equal sign, 2 = waiting for content
+        $arg_name = null;
+        $arg_value = null;
+
+        foreach (token_get_all('<?php '.$string.'?>') as $t)
+        {
+            switch ($t[0])
+            {
+                case T_OPEN_TAG:
+                case T_CLOSE_TAG:
+                    continue(2);
+                case T_STRING:
+                    if ($status == 0)
+                    {
+                        $arg_name = $t[1];
+                        $arg_value = '';
+                        $status++;
+                    }
+                    elseif ($status == 2)
+                    {
+                        $arg_value .= $t[1];
+                    }
+                    break;
+                case "\"":
+                case "\'":
+                    if ($status != 2)
+                    {
+                        throw new Template_Syntax_Exception("Expecting '=' sign after argument name");
+                    }
+                    $arg_value .= $t;
+                    break;
+                case '=':
+                    if ($status != 1)
+                    {
+                        throw new Template_Syntax_Exception("Unexpected '=' sign");
+                    }
+                    $status++;
+                    break;
+                case T_ENCAPSED_AND_WHITESPACE:
+                case T_VARIABLE:
+                case T_OBJECT_OPERATOR:
+                case T_CONSTANT_ENCAPSED_STRING:
+                case T_DOUBLE_COLON:
+                    if ($status != 2)
+                    {
+                        throw new Template_Syntax_Exception("Expecting '=' sign after argument name");
+                    }
+
+                    if ($t[0] == T_CONSTANT_ENCAPSED_STRING)
+                        $arg_value = substr($t[1], 1, -1);
+                    else
+                        $arg_value .= $t[1];
+
+                    break;
+                case T_WHITESPACE:
+                    if ($status == 2)
+                    {
+                        $args[$arg_name] = $arg_value;
+                        $arg_name = $arg_value = null;
+                        $status = 0;
+                    }
+                    break;
+                case !isset($t[1]):
+                    if ($status != 2)
+                    {
+                        throw new Template_Syntax_Exception("Expecting '=' sign after argument name");
+                    }
+
+                    $arg_value .= $t;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        if ($arg_value != null && $arg_name != null && !array_key_exists($arg_name, $args))
+        {
+            $args[$arg_name] = $arg_value;
+        }
+
+        return $args;
+    }
+
+    public function parseArgumentContent($content)
+    {
+        $content = trim($content);
+
+        if (empty($content))
+            return $content;
+
+        $quotes = $content[0] . substr($content, -1);
+
+        if ($quotes == "\"\"" || $quotes == '\'\'')
+        {
+            $content = substr($content, 1, -1);
+            $inline = ($quotes == "\'\'") ? false : true;
+            $out = '';
+            $current_var = false;
+
+            foreach (token_get_all('<?php "'.$content.'" ?>') as $t)
+            {
+                if ($current_var === true)
+                {
+                    if ($t[0] == T_VARIABLE || ($t[0] == T_STRING && $t[1][0] == '$'))
+                    {
+                        $current_var = $t[1];
+                    }
+                    else
+                    {
+                        $out .= '`';
+                        $current_var = false;
+                    }
+                }
+
+                if ($current_var)
+                {
+                    list($variable, $modifiers) = $this->parseVariable($current_var);
+                    $out .= $this->processVariable($variable, $modifiers, self::CONTEXT_STRING);
+                    $current_var = false;
+                }
+                elseif ($inline && $t[0] == T_VARIABLE)
+                {
+                    $current_var = $t[1];
+                }
+                elseif ($t[0] == '`')
+                {
+                    $current_var = true;
+                }
+
+                if ($current_var === false && in_array($t[0], $this->_allowed_in_string))
+                {
+                    $out .= isset($t[1]) ? $t[1] : $t[0];
+                }
+                else
+                {
+                    echo (isset($t[1]) ? token_name($t[0]) . ' ' . htmlspecialchars($t[1]) : htmlspecialchars($t[0])). "<br />";
+                }
+            }
+
+            return $out;
+        }
+        else
+        {
+            list($variable, $modifiers) = $this->parseVariable($content);
+            return $this->processVariable($variable, $modifiers, self::CONTEXT_ARGUMENT);
+        }
+    }
+
+    public function parseVariable($string)
+    {
+        $variable = '';
+        $modifiers = array();
+        $current_modifier = false;
+        $current_arg = false;
+
+        foreach (token_get_all('<?php '.$string.'?>') as $t)
+        {
+            if ($t[0] == T_STRING)
+            {
+                if ($current_modifier === true)
+                {
+                    $modifiers[] = array($t[1]);
+                    $current_modifier = count($modifiers) - 1;
+                    $current_arg = 1;
+                    continue;
+                }
+            }
+
+            $content = isset($t[1]) ? $t[1] : $t[0];
+
+            if ($t[0] == T_CONSTANT_ENCAPSED_STRING)
+                $content = substr($content, 1, -1);
+
+            if ($t[0] == '|')
+                $current_modifier = true;
+            elseif ($t[0] == ':')
+                $current_arg++;
+            elseif ($current_modifier === false && in_array($t[0], $this->_allowed_in_variable))
+            {
+                $variable .= $content;
+            }
+            elseif (is_int($current_modifier) && in_array($t[0], $this->_allowed_in_variable))
+            {
+                if (!array_key_exists($current_arg, $modifiers[$current_modifier]))
+                {
+                    $modifiers[$current_modifier][$current_arg] = '';
+                }
+
+                $modifiers[$current_modifier][$current_arg] .= $content;
+            }
+            else
+            {
+            }
+        }
+
+        return array($variable, $modifiers);
+    }
+
+    public function parseTokens($string)
+    {
+        echo '<table>';
+        foreach (token_get_all('<?php '.$string.'?>') as $t)
+        {
+            echo "<tr>";
+            if (is_array($t))
+            {
+                echo '<th>'.token_name($t[0]).'</th>';
+                echo '<td>'.htmlspecialchars($t[1]).'</td>';
+                echo '<td>'.($t[2]).'</td>';
+            }
+            else
+            {
+                echo '<th>--</th>';
+                echo '<td>'.htmlspecialchars($t).'</td>';
+                echo '<td></td>';
+            }
+            echo '</tr>';
+        }
+    }
+}
+
+?>
diff --git a/include/libs/template_lite/internal/compile.compile_config.php b/include/libs/template_lite/internal/compile.compile_config.php
new file mode 100644 (file)
index 0000000..9cdc8bc
--- /dev/null
@@ -0,0 +1,74 @@
+<?php\r
+/**\r
+ * Template Lite compile config variables - template internal module\r
+ *\r
+ * Type:        template\r
+ * Name:        compile_config\r
+ */\r
+\r
+function compile_compile_config($variable, &$object)\r
+{\r
+       $_result        = "";\r
+\r
+       // remove the beginning and ending #\r
+       $variable = substr($variable, 1, -1);\r
+\r
+       // get [foo] and .foo and (...) pieces\r
+       preg_match_all('!(?:^\w+)|(?:' . $object->_var_bracket_regexp . ')|\.\$?\w+|\S+!', $variable, $_match);\r
+       $variable = $_match[0];\r
+       $var_name = array_shift($variable);\r
+\r
+       $_result = "\$this->_confs['$var_name']";\r
+       foreach ($variable as $var)\r
+       {\r
+               if ($var{0} == '[')\r
+               {\r
+                       $var = substr($var, 1, -1);\r
+                       if (is_numeric($var))\r
+                       {\r
+                               $_result .= "[$var]";\r
+                       }\r
+                       elseif ($var{0} == '$')\r
+                       {\r
+                               $_result .= "[" . $object->_compile_variable($var) . "]";\r
+                       }\r
+                       elseif ($var{0} == '#')\r
+                       {\r
+                               $_result .= "[" . $object->_compile_config($var) . "]";\r
+                       }\r
+                       else\r
+                       {\r
+                               $_result .= "['$var']";\r
+                       }\r
+          }\r
+          else if ($var{0} == '.')\r
+          {\r
+                               if ($var{1} == '$')\r
+                       {\r
+                               $_result .= "[\$this->_TPL['" . substr($var, 2) . "']]";\r
+                       }\r
+                       else\r
+                       {\r
+                               $_result .= "['" . substr($var, 1) . "']";\r
+                       }\r
+               }\r
+               else if (substr($var,0,2) == '->')\r
+               {\r
+                       if(substr($var,2,2) == '__')\r
+                       {\r
+                               throw new Template_Exception('call to internal object members is not allowed', $object);\r
+                       }\r
+                       else if (substr($var, 2, 1) == '$')\r
+                       {\r
+                               $_output .= '->{(($var=$this->_TPL[\''.substr($var,3).'\']) && substr($var,0,2)!=\'__\') ? $_var : throw new Template_Exception("cannot access property \\"$var\\"", $this)}';\r
+                       }\r
+               }\r
+               else\r
+               {\r
+                       throw new Template_Exception('#' . $var_name.implode('', $variable) . '# is an invalid reference', $object);\r
+               }\r
+       }\r
+       return $_result;\r
+}\r
+\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/compile.compile_custom_block.php b/include/libs/template_lite/internal/compile.compile_custom_block.php
new file mode 100644 (file)
index 0000000..b0f559f
--- /dev/null
@@ -0,0 +1,65 @@
+<?php\r
+/**\r
+ * Template Lite compile custom block - template internal module\r
+ *\r
+ * Type:        template\r
+ * Name:        compile_custom_block\r
+ */\r
+\r
+function compile_compile_custom_block($function, $modifiers, $arguments, &$_result, &$object)\r
+{\r
+       if ($function{0} == '/')\r
+       {\r
+               $start_tag = false;\r
+               $function = substr($function, 1);\r
+       }\r
+       else\r
+       {\r
+               $start_tag = true;\r
+       }\r
+\r
+       if ($function = $object->_plugin_exists($function, "block"))\r
+       {\r
+               if ($start_tag)\r
+               {\r
+                       $_args = $object->_parse_arguments($arguments);\r
+                       foreach($_args as $key => $value)\r
+                       {\r
+                               if (is_bool($value))\r
+                               {\r
+                                       $value = $value ? 'true' : 'false';\r
+                               }\r
+                               elseif (is_null($value))\r
+                               {\r
+                                       $value = 'null';\r
+                               }\r
+                elseif ($value[0] != '$')\r
+                {\r
+                    $value = '"'.addslashes($value).'"';\r
+                }\r
+\r
+                               $_args[$key] = "'$key' => $value";\r
+                       }\r
+                       $_result = "<?php \$this->_tag_stack[] = array('$function', array(".implode(',', (array)$_args).")); ";\r
+                       $_result .= $function . '(array(' . implode(',', (array)$_args) .'), null, $this); ';\r
+                       $_result .= 'ob_start(); ?>';\r
+               }\r
+               else\r
+               {\r
+                       $_result .= '<?php $this->_block_content = ob_get_contents(); ob_end_clean(); ';\r
+                       $_result .= '$this->_block_content = ' . $function . '($this->_tag_stack[count($this->_tag_stack) - 1][1], $this->_block_content, $this); ';\r
+                       if (!empty($modifiers))\r
+                       {\r
+                               $_result .= '$this->_block_content = ' . $object->_parse_modifier('$this->_block_content', $modifiers) . '; ';\r
+                       }\r
+                       $_result .= 'echo $this->_block_content; array_pop($this->_tag_stack); ?>';\r
+               }\r
+               return true;\r
+       }\r
+       else\r
+       {\r
+               return false;\r
+       }\r
+}\r
+\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/compile.compile_custom_function.php b/include/libs/template_lite/internal/compile.compile_custom_function.php
new file mode 100644 (file)
index 0000000..38431cd
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Template Lite compile custom function - template internal module
+ *
+ * Type:        template
+ * Name:        compile_custom_function
+ */
+
+function compile_compile_custom_function($function, $modifiers, $arguments, &$_result, &$object)
+{
+       if ($function = $object->_plugin_exists($function, "function"))
+       {
+               $_args = $object->_parse_arguments($arguments);
+               foreach($_args as $key => $value)
+               {
+                       if (is_bool($value))
+                       {
+                               $value = $value ? 'true' : 'false';
+                       }
+                       elseif (is_null($value))
+                       {
+                               $value = 'null';
+                       }
+                       elseif ($value[0] != '$' && $value[0] != '"')
+                       {
+                               $value = '"'.addslashes($value).'"';
+                       }
+
+                       $_args[$key] = "'$key' => $value";
+               }
+
+               if (is_array($function))
+               {
+                       if (!is_string($function[0]))
+                       {
+                               throw new Template_Exception("Unsupported callback.");
+                       }
+
+                       $function = implode('::', $function);
+               }
+
+               $_result = '<?php echo ';
+               if (!empty($modifiers))
+               {
+                       $_result .= $object->_parse_modifier($function . '(array(' . implode(',', (array)$_args) . '), $this)', $modifiers) . '; ';
+               }
+               else
+               {
+                       $_result .= $function . '(array(' . implode(',', (array)$_args) . '), $this);';
+               }
+               $_result .= '?>';
+               return true;
+       }
+       else
+       {
+               return false;
+       }
+}
+
+?>
diff --git a/include/libs/template_lite/internal/compile.compile_if.php b/include/libs/template_lite/internal/compile.compile_if.php
new file mode 100644 (file)
index 0000000..35ecb30
--- /dev/null
@@ -0,0 +1,159 @@
+<?php\r
+/**\r
+ * Template Lite compile IF tag - template internal module\r
+ *\r
+ * Type:        template\r
+ * Name:        compile_parse_is_expr\r
+ */\r
+\r
+function compile_compile_if($arguments, $elseif, $while, &$object)\r
+{\r
+       $_result        = "";\r
+       $_match         = array();\r
+       $_args          = array();\r
+       $_is_arg_stack  = array();\r
+\r
+       // extract arguments from the equation\r
+       preg_match_all('/(?>(' . $object->_const_regexp . '|\/?' . $object->_var_regexp . '|\/?' . $object->_svar_regexp . '|\/?' . $object->_func_regexp . ')(?:' . $object->_mod_regexp . '*)?|\-?0[xX][0-9a-fA-F]+|\-?\d+(?:\.\d+)?|\.\d+|!==|===|==|!=|<>|<<|>>|<=|>=|\&\&|\|\||\(|\)|,|\!|\^|=|\&|\~|<|>|\%|\+|\-|\/|\*|\@|\b\w+\b|\S+)/x', $arguments, $_match);\r
+       $_args = $_match[0];\r
+\r
+       // make sure we have balanced parenthesis\r
+       $_args_count = array_count_values($_args);\r
+       if(isset($_args_count['(']) && $_args_count['('] != $_args_count[')'])\r
+       {\r
+               throw new Template_Exception("unbalanced parenthesis in if statement", $object);\r
+       }\r
+\r
+       $count_args = count($_args);\r
+       for ($i = 0, $for_max = $count_args; $i < $for_max; $i++)\r
+       {\r
+               $_arg = &$_args[$i];\r
+               switch (strtolower($_arg))\r
+               {\r
+                       case '!':\r
+                       case '%':\r
+                       case '!==':\r
+                       case '==':\r
+                       case '===':\r
+                       case '>':\r
+                       case '<':\r
+                       case '!=':\r
+                       case '<>':\r
+                       case '<<':\r
+                       case '>>':\r
+                       case '<=':\r
+                       case '>=':\r
+                       case '&&':\r
+                       case '||':\r
+                       case '^':\r
+                       case '&':\r
+                       case '~':\r
+                       case ')':\r
+                       case ',':\r
+                       case '+':\r
+                       case '-':\r
+                       case '*':\r
+                       case '/':\r
+                       case '@':\r
+                               break;\r
+                       case 'eq':\r
+                               $_arg = '==';\r
+                               break;\r
+                       case 'ne':\r
+                       case 'neq':\r
+                               $_arg = '!=';\r
+                               break;\r
+                       case 'lt':\r
+                               $_arg = '<';\r
+                               break;\r
+                       case 'le':\r
+                       case 'lte':\r
+                               $_arg = '<=';\r
+                               break;\r
+                       case 'gt':\r
+                               $_arg = '>';\r
+                               break;\r
+                       case 'ge':\r
+                       case 'gte':\r
+                               $_arg = '>=';\r
+                               break;\r
+                       case 'and':\r
+                               $_arg = '&&';\r
+                               break;\r
+                       case 'or':\r
+                               $_arg = '||';\r
+                               break;\r
+                       case 'not':\r
+                               $_arg = '!';\r
+                               break;\r
+                       case 'mod':\r
+                               $_arg = '%';\r
+                               break;\r
+                       case '(':\r
+                               array_push($_is_arg_stack, $i);\r
+                               break;\r
+                       case 'is':\r
+                               if ($_args[$i-1] == ')')\r
+                               {\r
+                                       $is_arg_start = array_pop($is_arg_stack);\r
+                               }\r
+                               else\r
+                               {\r
+                                       $_is_arg_count = count($_args);\r
+                                       $is_arg = implode(' ', array_slice($_args, $is_arg_start, $i - $is_arg_start));\r
+                                       $_arg_tokens = $object->_parse_is_expr($is_arg, array_slice($_args, $i+1));\r
+                                       array_splice($_args, $is_arg_start, count($_args), $_arg_tokens);\r
+                                       $i = $_is_arg_count - count($_args);\r
+                               }\r
+                               break;\r
+                       default:\r
+                               if (defined($_arg) && preg_match('/^'.$object->_const_regexp.'$/', $_arg))\r
+                               {\r
+                                       $_arg = "constant('".$_arg."')";\r
+                                       break;\r
+                               }\r
+                               preg_match('/(?:(' . $object->_var_regexp . '|' . $object->_svar_regexp . '|' . $object->_func_regexp . ')(' . $object->_mod_regexp . '*)(?:\s*[,\.]\s*)?)(?:\s+(.*))?/xs', $_arg, $_match);\r
+                               if (isset($_match[0]{0}) && ($_match[0]{0} == '$' || ($_match[0]{0} == '#' && $_match[0]{strlen($_match[0]) - 1} == '#') || $_match[0]{0} == "'" || $_match[0]{0} == '"' || $_match[0]{0} == '%'))\r
+                               {\r
+                                       // process a variable\r
+                                       $_arg = $object->_parse_variables(array($_match[1]), array($_match[2]));\r
+                               }\r
+                               elseif (is_numeric($_arg))\r
+                               {\r
+                                       // pass the number through\r
+                               }\r
+                               elseif (function_exists($_match[0]) || $_match[0] == "empty" || $_match[0] == "isset" || $_match[0] == "unset" || strtolower($_match[0]) == "true" || strtolower($_match[0]) == "false" || strtolower($_match[0]) == "null")\r
+                               {\r
+                                       // pass the function through\r
+                               }\r
+                               elseif (empty($_arg))\r
+                               {\r
+                                       // pass the empty argument through\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("unidentified token '$_arg'", $object);\r
+                               }\r
+                               break;\r
+               }\r
+       }\r
+\r
+       if($while)\r
+       {\r
+               return implode(' ', $_args);\r
+       }\r
+       else\r
+       {\r
+               if ($elseif)\r
+               {\r
+                       return '<?php elseif ('.implode(' ', $_args).'): ?>';\r
+               }\r
+               else\r
+               {\r
+                       return '<?php if ('.implode(' ', $_args).'): ?>';\r
+               }\r
+       }\r
+       return $_result;\r
+}\r
+\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/compile.generate_compiler_debug_output.php b/include/libs/template_lite/internal/compile.generate_compiler_debug_output.php
new file mode 100644 (file)
index 0000000..e9faccd
--- /dev/null
@@ -0,0 +1,35 @@
+<?php\r
+/**\r
+ * Template Lite generate_debug_output template internal module\r
+ *\r
+ * Type:        template\r
+ * Name:        generate_debug_output\r
+ */\r
+\r
+function generate_compiler_debug_output(&$object)\r
+{\r
+    $debug_output = "\$assigned_vars = \$this->_vars;\n";\r
+    $debug_output .= "ksort(\$assigned_vars);\n";\r
+    $debug_output .= "if (@is_array(\$this->_config[0])) {\n";\r
+    $debug_output .= "    \$config_vars = \$this->_config[0];\n";\r
+    $debug_output .= "    ksort(\$config_vars);\n";\r
+    $debug_output .= "    \$this->assign('_debug_config_keys', array_keys(\$config_vars));\n";\r
+    $debug_output .= "    \$this->assign('_debug_config_vals', array_values(\$config_vars));\n";\r
+    $debug_output .= "}   \n";\r
+       \r
+    $debug_output .= "\$included_templates = \$this->_templatelite_debug_info;\n";\r
+\r
+    $debug_output .= "\$this->assign('_debug_keys', array_keys(\$assigned_vars));\n";\r
+    $debug_output .= "\$this->assign('_debug_vals', array_values(\$assigned_vars));\n";\r
+    $debug_output .= "\$this->assign('_debug_tpls', \$included_templates);\n";\r
+\r
+       $debug_output .= "\$this->_templatelite_debug_loop = true;\n";\r
+       $debug_output .= "\$this->_templatelite_debug_dir = \$this->template_dir;\n";\r
+       $debug_output .= "\$this->template_dir = TEMPLATE_LITE_DIR . 'internal/';\n";\r
+       $debug_output .= "echo \$this->_fetch_compile('debug.tpl');\n";\r
+       $debug_output .= "\$this->template_dir = \$this->_templatelite_debug_dir;\n";\r
+       $debug_output .= "\$this->_templatelite_debug_loop = false; \n";\r
+       return $debug_output;\r
+}\r
+\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/compile.include.php b/include/libs/template_lite/internal/compile.include.php
new file mode 100644 (file)
index 0000000..0c4875e
--- /dev/null
@@ -0,0 +1,56 @@
+<?php\r
+/**\r
+ * Template Lite\r
+ *\r
+ * Type:        compile\r
+ * Name:        section_start\r
+ */\r
+\r
+function compile_include($arguments, &$object)\r
+{\r
+       $_args = $object->_parse_arguments($arguments);\r
+\r
+       $arg_list = array();\r
+       if (empty($_args['file']))\r
+       {\r
+               throw new Template_Exception("missing 'file' attribute in include tag", $object);\r
+       }\r
+\r
+       foreach ($_args as $arg_name => $arg_value)\r
+       {\r
+               if ($arg_name == 'file')\r
+               {\r
+                       $include_file = $arg_value;\r
+                       continue;\r
+               }\r
+               else if ($arg_name == 'assign')\r
+               {\r
+                       $assign_var = $arg_value;\r
+                       continue;\r
+               }\r
+               if (is_bool($arg_value))\r
+               {\r
+                       $arg_value = $arg_value ? 'true' : 'false';\r
+               }\r
+               $arg_list[] = "'$arg_name' => $arg_value";\r
+       }\r
+\r
+       if (isset($assign_var))\r
+       {\r
+               $output = '<?php $_templatelite_tpl_vars = $this->_vars;' .\r
+                       "\n\$this->assign(" . $assign_var . ", \$this->_fetch_compile_include(" . $include_file . ", array(".implode(',', (array)$arg_list).")));\n" .\r
+                       "\$this->_vars = \$_templatelite_tpl_vars;\n" .\r
+                       "unset(\$_templatelite_tpl_vars);\n" .\r
+                       ' ?>';\r
+       }\r
+       else\r
+       {\r
+               $output = '<?php $_templatelite_tpl_vars = $this->_vars;' .\r
+                       "\necho \$this->_fetch_compile_include(" . $include_file . ", array(".implode(',', (array)$arg_list)."));\n" .\r
+                       "\$this->_vars = \$_templatelite_tpl_vars;\n" .\r
+                       "unset(\$_templatelite_tpl_vars);\n" .\r
+                       ' ?>';\r
+       }\r
+       return $output;\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/compile.parse_is_expr.php b/include/libs/template_lite/internal/compile.parse_is_expr.php
new file mode 100644 (file)
index 0000000..fe4d742
--- /dev/null
@@ -0,0 +1,77 @@
+<?php\r
+/**\r
+ * Template Lite compile IS exprenssion in IF tag - template internal module\r
+ *\r
+ * Type:        template\r
+ * Name:        compile_parse_is_expr\r
+ */\r
+\r
+function compile_parse_is_expr($is_arg, $_args, &$object)\r
+{\r
+       $expr_end = 0;\r
+       $negate_expr = false;\r
+\r
+       if (($first_arg = array_shift($_args)) == 'not') {\r
+               $negate_expr = true;\r
+               $expr_type = array_shift($_args);\r
+       }\r
+       else\r
+       {\r
+               $expr_type = $first_arg;\r
+       }\r
+\r
+       switch ($expr_type) {\r
+               case 'even':\r
+                       if (isset($_args[$expr_end]) && $_args[$expr_end] == 'by')\r
+                       {\r
+                               $expr_end++;\r
+                               $expr_arg = $_args[$expr_end++];\r
+                               $expr = "!(1 & ($is_arg / " . $object->_parse_variable($expr_arg) . "))";\r
+                       }\r
+                       else\r
+                       {\r
+                               $expr = "!(1 & $is_arg)";\r
+                       }\r
+                       break;\r
+\r
+               case 'odd':\r
+                       if (isset($_args[$expr_end]) && $_args[$expr_end] == 'by')\r
+                       {\r
+                               $expr_end++;\r
+                               $expr_arg = $_args[$expr_end++];\r
+                               $expr = "(1 & ($is_arg / " . $object->_parse_variable($expr_arg) . "))";\r
+                               }\r
+                               else\r
+                               {\r
+                                       $expr = "(1 & $is_arg)";\r
+                               }\r
+                               break;\r
+\r
+                       case 'div':\r
+                               if (@$_args[$expr_end] == 'by')\r
+                               {\r
+                                       $expr_end++;\r
+                                       $expr_arg = $_args[$expr_end++];\r
+                                       $expr = "!($is_arg % " . $object->_parse_variable($expr_arg) . ")";\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("expecting 'by' after 'div'", $object);\r
+                               }\r
+                       break;\r
+\r
+                       default:\r
+                               throw new Template_Exception("unknown 'is' expression - '$expr_type'", $object);\r
+                               break;\r
+               }\r
+\r
+       if ($negate_expr) {\r
+               $expr = "!($expr)";\r
+       }\r
+\r
+       array_splice($_args, 0, $expr_end, $expr);\r
+\r
+       return $_args;\r
+}\r
+\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/compile.section_start.php b/include/libs/template_lite/internal/compile.section_start.php
new file mode 100644 (file)
index 0000000..c107423
--- /dev/null
@@ -0,0 +1,129 @@
+<?php\r
+/**\r
+ * Template Lite section_start compile plugin converted from Smarty\r
+ *\r
+ * Type:        compile\r
+ * Name:        section_start\r
+ */\r
+\r
+function compile_section_start($arguments, &$object)\r
+{\r
+       $attrs = $object->_parse_arguments($arguments);\r
+       $arg_list = array();\r
+\r
+       $output = '<?php ';\r
+       $section_name = $attrs['name'];\r
+       if (empty($section_name))\r
+       {\r
+               throw new Template_Exception("missing section name", $object);\r
+       }\r
+\r
+       $output .= "if (isset(\$this->_sections['$section_name'])) unset(\$this->_sections['$section_name']);\n";\r
+       $section_props = "\$this->_sections['$section_name']";\r
+\r
+       foreach ($attrs as $attr_name => $attr_value)\r
+       {\r
+               switch ($attr_name)\r
+               {\r
+                       case 'loop':\r
+                               $output .= "{$section_props}['loop'] = is_array($attr_value) ? count($attr_value) : max(0, (int)$attr_value);\n";\r
+                               break;\r
+\r
+                       case 'show':\r
+                               if (is_bool($attr_value))\r
+                               {\r
+                                       $show_attr_value = $attr_value ? 'true' : 'false';\r
+                               }\r
+                               else\r
+                               {\r
+                                       $show_attr_value = "(bool)$attr_value";\r
+                               }\r
+                               $output .= "{$section_props}['show'] = $show_attr_value;\n";\r
+                               break;\r
+\r
+                       case 'name':\r
+                               $output .= "{$section_props}['$attr_name'] = '$attr_value';\n";\r
+                               break;\r
+\r
+                       case 'max':\r
+                       case 'start':\r
+                               $output .= "{$section_props}['$attr_name'] = (int)$attr_value;\n";\r
+                               break;\r
+\r
+                       case 'step':\r
+                               $output .= "{$section_props}['$attr_name'] = ((int)$attr_value) == 0 ? 1 : (int)$attr_value;\n";\r
+                               break;\r
+\r
+                       default:\r
+                               throw new Template_Exception("unknown section attribute - '$attr_name'", $object);\r
+                               break;\r
+               }\r
+       }\r
+\r
+       if (!isset($attrs['show']))\r
+       {\r
+               $output .= "{$section_props}['show'] = true;\n";\r
+       }\r
+\r
+       if (!isset($attrs['loop']))\r
+       {\r
+               $output .= "{$section_props}['loop'] = 1;\n";\r
+       }\r
+\r
+       if (!isset($attrs['max']))\r
+       {\r
+               $output .= "{$section_props}['max'] = {$section_props}['loop'];\n";\r
+       }\r
+       else\r
+       {\r
+               $output .= "if ({$section_props}['max'] < 0)\n" .\r
+                                       "       {$section_props}['max'] = {$section_props}['loop'];\n";\r
+       }\r
+\r
+       if (!isset($attrs['step']))\r
+       {\r
+               $output .= "{$section_props}['step'] = 1;\n";\r
+       }\r
+\r
+       if (!isset($attrs['start']))\r
+       {\r
+               $output .= "{$section_props}['start'] = {$section_props}['step'] > 0 ? 0 : {$section_props}['loop']-1;\n";\r
+       }\r
+       else\r
+       {\r
+               $output .= "if ({$section_props}['start'] < 0)\n" .\r
+                                  "    {$section_props}['start'] = max({$section_props}['step'] > 0 ? 0 : -1, {$section_props}['loop'] + {$section_props}['start']);\n" .\r
+                                  "else\n" .\r
+                                  "    {$section_props}['start'] = min({$section_props}['start'], {$section_props}['step'] > 0 ? {$section_props}['loop'] : {$section_props}['loop']-1);\n";\r
+       }\r
+\r
+       $output .= "if ({$section_props}['show']) {\n";\r
+       if (!isset($attrs['start']) && !isset($attrs['step']) && !isset($attrs['max']))\r
+       {\r
+               $output .= "    {$section_props}['total'] = {$section_props}['loop'];\n";\r
+       }\r
+       else\r
+       {\r
+               $output .= "    {$section_props}['total'] = min(ceil(({$section_props}['step'] > 0 ? {$section_props}['loop'] - {$section_props}['start'] : {$section_props}['start']+1)/abs({$section_props}['step'])), {$section_props}['max']);\n";\r
+       }\r
+       $output .= "    if ({$section_props}['total'] == 0)\n" .\r
+                          "            {$section_props}['show'] = false;\n" .\r
+                          "} else\n" .\r
+                          "    {$section_props}['total'] = 0;\n";\r
+\r
+       $output .= "if ({$section_props}['show']):\n";\r
+       $output .= "\r
+               for ({$section_props}['index'] = {$section_props}['start'], {$section_props}['iteration'] = 1;\r
+                        {$section_props}['iteration'] <= {$section_props}['total'];\r
+                        {$section_props}['index'] += {$section_props}['step'], {$section_props}['iteration']++):\n";\r
+       $output .= "{$section_props}['rownum'] = {$section_props}['iteration'];\n";\r
+       $output .= "{$section_props}['index_prev'] = {$section_props}['index'] - {$section_props}['step'];\n";\r
+       $output .= "{$section_props}['index_next'] = {$section_props}['index'] + {$section_props}['step'];\n";\r
+       $output .= "{$section_props}['first']     = ({$section_props}['iteration'] == 1);\n";\r
+       $output .= "{$section_props}['last']       = ({$section_props}['iteration'] == {$section_props}['total']);\n";\r
+\r
+       $output .= "?>";\r
+\r
+       return $output;\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/debug.tpl b/include/libs/template_lite/internal/debug.tpl
new file mode 100644 (file)
index 0000000..c5c2dd1
--- /dev/null
@@ -0,0 +1,77 @@
+{* templatelite debug console *}\r
+\r
+{if isset($_templatelite_debug_output) and $_templatelite_debug_output eq "html"}\r
+       <table border=0 width=100%>\r
+       <tr bgcolor=#cccccc><th colspan=2>Template Lite Debug Console</th></tr>\r
+       <tr bgcolor=#cccccc><td colspan=2><b>Included templates & config files (load time in seconds):</b></td></tr>\r
+       {foreach key=key value=templates from=$_debug_tpls}\r
+               <tr bgcolor={if $key % 2}#eeeeee{else}#fafafa{/if}>\r
+               <td colspan=2><tt>{for start=0 stop=$_debug_tpls[$key].depth}&nbsp;&nbsp;&nbsp;{/for}\r
+               <font color={if $_debug_tpls[$key].type eq "template"}brown{elseif $_debug_tpls[$key].type eq "insert"}black{else}green{/if}>\r
+               {$_debug_tpls[$key].filename}</font>{if isset($_debug_tpls[$key].exec_time)} \r
+               <font size=-1><i>({$_debug_tpls[$key].exec_time|string_format:"%.5f"} seconds){if $key eq 0} (total){/if}\r
+               </i></font>{/if}</tt></td></tr>\r
+       {foreachelse}\r
+               <tr bgcolor=#eeeeee><td colspan=2><tt><i>No template assigned</i></tt></td></tr>        \r
+       {/foreach}\r
+       <tr bgcolor=#cccccc><td colspan=2><b>Assigned template variables:</b></td></tr>\r
+       {foreach key=key value=vars from=$_debug_keys}\r
+               <tr bgcolor={if $key % 2}#eeeeee{else}#fafafa{/if}>\r
+               <td valign=top><tt><font color=blue>{ldelim}${$_debug_keys[$key]}{rdelim}</font></tt></td>\r
+               <td nowrap><tt><font color=green>{$_debug_vals[$key]|@debug_print_var}</font></tt></td></tr>\r
+       {foreachelse}\r
+               <tr bgcolor=#eeeeee><td colspan=2><tt><i>No template variables assigned</i></tt></td></tr>      \r
+       {/foreach}\r
+       <tr bgcolor=#cccccc><td colspan=2><b>Assigned config file variables (outer template scope):</b></td></tr>\r
+       {foreach key=key value=config_vars from=$_debug_config_keys}\r
+               <tr bgcolor={if $key % 2}#eeeeee{else}#fafafa{/if}>\r
+               <td valign=top><tt><font color=maroon>{ldelim}#{$_debug_config_keys[$key]}#{rdelim}</font></tt></td>\r
+               <td><tt><font color=green>{$_debug_config_vals[$key]|@debug_print_var}</font></tt></td></tr>\r
+       {foreachelse}\r
+               <tr bgcolor=#eeeeee><td colspan=2><tt><i>No config vars assigned</i></tt></td></tr>     \r
+       {/foreach}\r
+       </table>\r
+{else}\r
+<SCRIPT language=javascript>\r
+       if( self.name == '' ) {ldelim}\r
+          var title = 'Console';\r
+       {rdelim}\r
+       else {ldelim}\r
+          var title = 'Console_' + self.name;\r
+       {rdelim}\r
+       _templatelite_console = window.open("",title.value,"width=680,height=600,resizable,scrollbars=yes");\r
+       _templatelite_console.document.write("<HTML><TITLE>Template Lite Debug Console_"+self.name+"</TITLE><BODY bgcolor=#ffffff>");\r
+       _templatelite_console.document.write("<table border=0 width=100%>");\r
+       _templatelite_console.document.write("<tr bgcolor=#cccccc><th colspan=2>Template Lite Debug Console</th></tr>");\r
+       _templatelite_console.document.write("<tr bgcolor=#cccccc><td colspan=2><b>Included templates & config files (load time in seconds):</b></td></tr>");\r
+       {foreach key=key value=templates from=$_debug_tpls}\r
+               _templatelite_console.document.write("<tr bgcolor={if $key % 2}#eeeeee{else}#fafafa{/if}>");\r
+               _templatelite_console.document.write("<td colspan=2><tt>{for start=0 stop=$_debug_tpls[$key].depth}&nbsp;&nbsp;&nbsp;{/for}");\r
+               _templatelite_console.document.write("<font color={if $_debug_tpls[$key].type eq "template"}brown{elseif $_debug_tpls[$key].type eq "insert"}black{else}green{/if}>");\r
+               _templatelite_console.document.write("{$_debug_tpls[$key].filename}</font>{if isset($_debug_tpls[$key].exec_time)} ");\r
+               _templatelite_console.document.write("<font size=-1><i>({$_debug_tpls[$key].exec_time|string_format:"%.5f"} seconds){if $key eq 0} (total){/if}");\r
+               _templatelite_console.document.write("</i></font>{/if}</tt></td></tr>");\r
+       {foreachelse}\r
+               _templatelite_console.document.write("<tr bgcolor=#eeeeee><td colspan=2><tt><i>No template assigned</i></tt></td></tr>  ");\r
+       {/foreach}\r
+       _templatelite_console.document.write("<tr bgcolor=#cccccc><td colspan=2><b>Assigned template variables:</b></td></tr>");\r
+       {foreach key=key value=vars from=$_debug_keys}\r
+               _templatelite_console.document.write("<tr bgcolor={if $key % 2}#eeeeee{else}#fafafa{/if}>");\r
+               _templatelite_console.document.write("<td valign=top><tt><font color=blue>{ldelim}${$_debug_keys[$key]}{rdelim}</font></tt></td>");\r
+               _templatelite_console.document.write("<td nowrap><tt><font color=green>{$_debug_vals[$key]|@debug_print_var}</font></tt></td></tr>");\r
+       {foreachelse}\r
+               _templatelite_console.document.write("<tr bgcolor=#eeeeee><td colspan=2><tt><i>No template variables assigned</i></tt></td></tr>");\r
+       {/foreach}\r
+       _templatelite_console.document.write("<tr bgcolor=#cccccc><td colspan=2><b>Assigned config file variables (outer template scope):</b></td></tr>");\r
+       {foreach key=key value=config_vars from=$_debug_config_keys}\r
+               _templatelite_console.document.write("<tr bgcolor={if $key % 2}#eeeeee{else}#fafafa{/if}>");\r
+               _templatelite_console.document.write("<td valign=top><tt><font color=maroon>{ldelim}#{$_debug_config_keys[$key]}#{rdelim}</font></tt></td>");\r
+               _templatelite_console.document.write("<td><tt><font color=green>{$_debug_config_vals[$key]|@debug_print_var}</font></tt></td></tr>");\r
+       {foreachelse}\r
+               _templatelite_console.document.write("<tr bgcolor=#eeeeee><td colspan=2><tt><i>No config vars assigned</i></tt></td></tr>");\r
+       {/foreach}\r
+       _templatelite_console.document.write("</table>");\r
+       _templatelite_console.document.write("</BODY></HTML>");\r
+       _templatelite_console.document.close();\r
+</SCRIPT>\r
+{/if}
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/template.build_dir.php b/include/libs/template_lite/internal/template.build_dir.php
new file mode 100644 (file)
index 0000000..365ce92
--- /dev/null
@@ -0,0 +1,29 @@
+<?php\r
+/**\r
+ * Template Lite template_build_dir template internal module\r
+ *\r
+ * Type:        template\r
+ * Name:        template_build_dir\r
+ */\r
+\r
+function template_build_dir($dir, $id, &$object)\r
+{\r
+       $_args = explode('|', $id);\r
+       if (count($_args) == 1 && empty($_args[0]))\r
+       {\r
+               return $object->_get_dir($dir);\r
+       }\r
+       $_result = $object->_get_dir($dir);\r
+       foreach($_args as $value)\r
+       {\r
+               $_result .= $value;\r
+               if (!is_dir($_result))\r
+               {\r
+                       mkdir($_result, 0777);\r
+               }\r
+               $_result.= DIRECTORY_SEPARATOR;\r
+       }\r
+       return $_result;\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/internal/template.config_loader.php b/include/libs/template_lite/internal/template.config_loader.php
new file mode 100644 (file)
index 0000000..1da4338
--- /dev/null
@@ -0,0 +1,76 @@
+<?php\r
+/**\r
+ * Template Lite config_load template internal module\r
+ *\r
+ * Type:        template\r
+ * Name:        config_load\r
+ */\r
+\r
+$this->_config_module_loaded = true;\r
+$this->template_dir = $this->_get_dir($this->template_dir);\r
+$this->config_dir = $this->_get_dir($this->config_dir);\r
+$this->compile_dir = $this->_get_dir($this->compile_dir);\r
+$name = ($this->encode_file_name) ? md5($this->template_dir . $file . $section_name . $var_name).'.php' : str_replace(".", "_", str_replace("/", "_", $file."_".$section_name."_".$var_name)).'.php';\r
+\r
+if ($this->debugging)\r
+{\r
+       $debug_start_time = array_sum(explode(' ', microtime()));\r
+}\r
+\r
+if ($this->cache)\r
+{\r
+       array_push($this->_cache_info['config'], $file);\r
+}\r
+\r
+if (!$this->force_compile && file_exists($this->compile_dir.'c_'.$name) && (filemtime($this->compile_dir.'c_'.$name) > filemtime($this->config_dir.$file)))\r
+{\r
+       include($this->compile_dir.'c_'.$name);\r
+       return true;\r
+}\r
+\r
+if (!is_object($this->_config_obj))\r
+{\r
+       require_once(TEMPLATE_LITE_DIR . "class.config.php");\r
+       $this->_config_obj = new $this->config_class;\r
+       $this->_config_obj->overwrite = $this->config_overwrite;\r
+       $this->_config_obj->booleanize = $this->config_booleanize;\r
+       $this->_config_obj->fix_new_lines = $this->config_fix_new_lines;\r
+       $this->_config_obj->read_hidden = $this->config_read_hidden;\r
+}\r
+\r
+if (!($_result = $this->_config_obj->config_load($this->config_dir.$file, $section_name, $var_name)))\r
+{\r
+       return false;\r
+}\r
+\r
+if (!empty($var_name) || !empty($section_name))\r
+{\r
+       $output = "\$this->_confs = " . var_export($_result, true) . ";";\r
+}\r
+else\r
+{\r
+       // must shift of the bottom level of the array to get rid of the section labels\r
+       $_temp = array();\r
+       foreach($_result as $value)\r
+       {\r
+               $_temp = array_merge($_temp, $value);\r
+       }\r
+       $output = "\$this->_confs = " . var_export($_temp, true) . ";";\r
+}\r
+\r
+$f = fopen($this->compile_dir.'c_'.$name, "w");\r
+fwrite($f, '<?php ' . $output . ' ?>');\r
+fclose($f);\r
+eval($output);\r
+\r
+if ($this->debugging)\r
+{\r
+       $this->_templatelite_debug_info[] = array('type'          => 'config',\r
+                                                                               'filename'  => $file.' ['.$section_name.'] '.$var_name,\r
+                                                                               'depth'  => 0,\r
+                                                                               'exec_time' => array_sum(explode(' ', microtime())) - $debug_start_time );\r
+}\r
+\r
+return true;\r
+\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/template.destroy_dir.php b/include/libs/template_lite/internal/template.destroy_dir.php
new file mode 100644 (file)
index 0000000..9538c28
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Template Lite template_destroy_dir template internal module
+ *
+ * Type:        template
+ * Name:        template_destroy_dir
+ */
+
+function template_destroy_dir($file, $id, $dir, &$object)
+{
+       if ($file == null && $id == null)
+       {
+               if (is_dir($dir))
+               {
+                       if($d = opendir($dir))
+                       {
+                               while(($f = readdir($d)) !== false)
+                               {
+                                       if ($f != '.' && $f != '..')
+                                       {
+                                               template_rm_dir($dir.$f.DIRECTORY_SEPARATOR);
+                                       }
+                               }
+                       }
+               }
+       }
+       else
+       {
+               if ($id == null)
+               {
+                       $object->template_dir = $object->_get_dir($object->template_dir);
+
+                       $name = ($object->encode_file_name) ? md5($object->template_dir.$file).'.php' : str_replace(".", "_", str_replace("/", "_", $file)).'.php';
+                       @unlink($dir.$name);
+               }
+               else
+               {
+                       $_args = "";
+                       foreach(explode('|', $id) as $value)
+                       {
+                               $_args .= $value.DIRECTORY_SEPARATOR;
+                       }
+                       template_rm_dir($dir.DIRECTORY_SEPARATOR.$_args);
+               }
+       }
+}
+
+function template_rm_dir($dir)
+{
+       if (is_file(substr($dir, 0, -1)))
+       {
+               @unlink(substr($dir, 0, -1));
+               return;
+       }
+       
+       if (!file_exists($dir))
+           return;
+       
+       if ($d = opendir($dir))
+       {
+               while(($f = readdir($d)) !== false)
+               {
+                       if ($f != '.' && $f != '..')
+                       {
+                               template_rm_dir($dir.$f.DIRECTORY_SEPARATOR);
+                       }
+               }
+               @rmdir($dir.$f);
+       }
+}
+
+?>
diff --git a/include/libs/template_lite/internal/template.fetch_compile_include.php b/include/libs/template_lite/internal/template.fetch_compile_include.php
new file mode 100644 (file)
index 0000000..491fe53
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Template Lite template_fetch_compile_include template internal module
+ *
+ * Type:        template
+ * Name:        template_fetch_compile_include
+ */
+
+function template_fetch_compile_include($_templatelite_include_file, $_templatelite_include_vars, &$object)
+{
+       if ($object->debugging)
+       {
+               $object->_templatelite_debug_info[] = array('type'        => 'template',
+                                                                                       'filename'  => $_templatelite_include_file,
+                                                                                       'depth'  => ++$object->_inclusion_depth,
+                                                                                       'exec_time' => array_sum(explode(' ', microtime())) );
+               $included_tpls_idx = count($object->_templatelite_debug_info) - 1;
+       }
+
+       $object->_vars = array_merge($object->_vars, $_templatelite_include_vars);
+       $_templatelite_include_file = $object->_get_resource($_templatelite_include_file);
+       if(isset($object->_confs[0]))
+       {
+               array_unshift($object->_confs, $object->_confs[0]);
+               $_compiled_output = $object->_fetch_compile($_templatelite_include_file, true);
+               array_shift($object->_confs);
+       }
+       else
+       {
+               $_compiled_output = $object->_fetch_compile($_templatelite_include_file, true);
+       }
+
+       $object->_inclusion_depth--;
+
+       if ($object->debugging)
+       {
+               $object->_templatelite_debug_info[$included_tpls_idx]['exec_time'] = array_sum(explode(' ', microtime())) - $object->_templatelite_debug_info[$included_tpls_idx]['exec_time'];
+       }
+       return $_compiled_output;
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/template.generate_debug_output.php b/include/libs/template_lite/internal/template.generate_debug_output.php
new file mode 100644 (file)
index 0000000..3214b57
--- /dev/null
@@ -0,0 +1,37 @@
+<?php\r
+/**\r
+ * Template Lite template_generate_debug_output template internal module\r
+ *\r
+ * Type:        template\r
+ * Name:        template_generate_debug_output\r
+ */\r
+\r
+function template_generate_debug_output(&$object)\r
+{\r
+    $assigned_vars = $object->_vars;\r
+    ksort($assigned_vars);\r
+    if (@is_array($object->_config[0]))\r
+       {\r
+        $config_vars = $object->_config[0];\r
+        ksort($config_vars);\r
+        $object->assign("_debug_config_keys", array_keys($config_vars));\r
+        $object->assign("_debug_config_vals", array_values($config_vars));\r
+    }   \r
+\r
+    $included_templates = $object->_templatelite_debug_info;\r
+\r
+    $object->assign("_debug_keys", array_keys($assigned_vars));\r
+    $object->assign("_debug_vals", array_values($assigned_vars));\r
+    $object->assign("_debug_tpls", $included_templates);\r
+    $object->assign("_templatelite_debug_output", "");\r
+\r
+       $object->_templatelite_debug_loop = true;\r
+       $object->_templatelite_debug_dir = $object->template_dir;\r
+       $object->template_dir = TEMPLATE_LITE_DIR . "internal/";\r
+       $debug_output = $object->fetch("debug.tpl");\r
+       $object->template_dir = $object->_templatelite_debug_dir;\r
+       $object->_templatelite_debug_loop = false;\r
+       return $debug_output;\r
+}\r
+\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/block.capture.php b/include/libs/template_lite/plugins/block.capture.php
new file mode 100644 (file)
index 0000000..5ae6533
--- /dev/null
@@ -0,0 +1,29 @@
+<?php\r
+/**\r
+ * template_lite {capture}{/capture} block plugin\r
+ *\r
+ * Type:     block function\r
+ * Name:     capture\r
+ * Purpose:  removes content and stores it in a variable\r
+ */\r
+function tpl_block_capture($params, $content, &$tpl)\r
+{\r
+       extract($params);\r
+\r
+       if (isset($name))\r
+       {\r
+               $buffer = $name;\r
+       }\r
+       else\r
+       {\r
+               $buffer = "'default'";\r
+       }\r
+\r
+       $tpl->_templatelite_vars['capture'][$buffer] = $content;\r
+       if (isset($assign))\r
+       {\r
+               $tpl->assign($assign, $content);\r
+       }\r
+       return;\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/block.strip.php b/include/libs/template_lite/plugins/block.strip.php
new file mode 100644 (file)
index 0000000..c3d79a8
--- /dev/null
@@ -0,0 +1,23 @@
+<?php\r
+/**\r
+ * template_lite {strip}{/strip} block plugin\r
+ *\r
+ * Type:     block function\r
+ * Name:     strip\r
+ * Purpose:  strip unwanted white space from text\r
+ * Credit:   Taken from the original Smarty\r
+ *           http://smarty.php.net\r
+ */\r
+function tpl_block_strip($params, $content, &$tpl)\r
+{\r
+       $_strip_search = array(\r
+               "![\t ]+$|^[\t ]+!m",           // remove leading/trailing space chars\r
+               '%[\r\n]+%m',                   // remove CRs and newlines\r
+       );\r
+       $_strip_replace = array(\r
+               '',\r
+               '',\r
+       );\r
+       return preg_replace($_strip_search, $_strip_replace, $content);\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/block.textformat.php b/include/libs/template_lite/plugins/block.textformat.php
new file mode 100644 (file)
index 0000000..da4fe74
--- /dev/null
@@ -0,0 +1,78 @@
+<?php\r
+\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     block function\r
+ * Name:     textformat\r
+ * Purpose:  format text a certain way with preset styles\r
+ *           or custom wrap/indent settings\r
+ * Params:   style: string (email)\r
+ *           indent: integer (0)\r
+ *           wrap: integer (80)\r
+ *           wrap_char string ("\n")\r
+ *           indent_char: string (" ")\r
+ *           wrap_boundary: boolean (true)\r
+ * Taken from the original Smarty\r
+ * http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_block_textformat($params, $content, &$template_object)\r
+{\r
+       $style = null;\r
+       $indent = 0;\r
+       $indent_first = 0;\r
+       $indent_char = ' ';\r
+       $wrap = 80;\r
+       $wrap_char = "\n";\r
+       $wrap_cut = false;\r
+       $assign = null;\r
+       \r
+       if($content == null)\r
+       {\r
+               return true;\r
+       }\r
+\r
+    extract($params);\r
+\r
+       if($style == 'email')\r
+       {\r
+               $wrap = 72;\r
+       }\r
+       // split into paragraphs        \r
+       $paragraphs = preg_split('![\r\n][\r\n]!',$content);\r
+\r
+       foreach($paragraphs as $paragraph)\r
+       {\r
+               if($paragraph == '')\r
+               {\r
+                       continue;\r
+               }\r
+               // convert mult. spaces & special chars to single space\r
+               $paragraph = preg_replace(array('!\s+!','!(^\s+)|(\s+$)!'),array(' ',''),$paragraph);\r
+               // indent first line\r
+               if($indent_first > 0)\r
+               {\r
+                       $paragraph = str_repeat($indent_char,$indent_first) . $paragraph;\r
+               }\r
+               // wordwrap sentences\r
+               $paragraph = wordwrap($paragraph, $wrap - $indent, $wrap_char, $wrap_cut);\r
+               // indent lines\r
+               if($indent > 0)\r
+               {\r
+                       $paragraph = preg_replace('!^!m',str_repeat($indent_char,$indent),$paragraph);\r
+               }\r
+               $output .= $paragraph . $wrap_char . $wrap_char;\r
+       }\r
+       if($assign != null)\r
+       {\r
+               $template_object->assign($assign,$output);\r
+       }\r
+       else\r
+       {\r
+               echo $output;\r
+       }\r
+       //echo $content;\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/plugins/compiler.debug.php b/include/libs/template_lite/plugins/compiler.debug.php
new file mode 100644 (file)
index 0000000..efdfda9
--- /dev/null
@@ -0,0 +1,33 @@
+<?php\r
+\r
+/*\r
+ * Template Lite plugin converted from Smarty\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     debug\r
+ * Version:  1.0\r
+ * Date:     July 1, 2002\r
+ * Author:      Monte Ohrt <monte@ispi.net>\r
+ * Purpose:  popup debug window\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_compiler_debug($params, &$tpl)\r
+{\r
+       if($params['output'])\r
+       {\r
+           $debug_output = '$this->assign("_templatelite_debug_output", ' . $params['output'] . ');';\r
+       }\r
+       else\r
+       {\r
+               $debug_output = "";\r
+       }\r
+\r
+       if(!function_exists("generate_compiler_debug_output"))\r
+       {\r
+               require_once(TEMPLATE_LITE_DIR . "internal/compile.generate_compiler_debug_output.php");\r
+       }\r
+       $debug_output .= generate_compiler_debug_output($tpl);\r
+       return $debug_output;\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/plugins/compiler.tplheader.php b/include/libs/template_lite/plugins/compiler.tplheader.php
new file mode 100644 (file)
index 0000000..bad0151
--- /dev/null
@@ -0,0 +1,16 @@
+<?php\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * File:     compiler.tplheader.php\r
+ * Type:     compiler\r
+ * Name:     tplheader\r
+ * Purpose:  Output header containing the source file name and\r
+ *           the time it was compiled.\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_compiler_tplheader($arguments, &$tpl)\r
+{\r
+    return "\necho '" . $tpl->_file . " compiled at " . date('Y-m-d H:M'). "';";\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.counter.php b/include/libs/template_lite/plugins/function.counter.php
new file mode 100644 (file)
index 0000000..a90c98e
--- /dev/null
@@ -0,0 +1,97 @@
+<?php\r
+/*\r
+ * template_lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     counter\r
+ * Purpose:  print out a counter value\r
+ * Credit:   Taken from the original Smarty\r
+ *           http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_counter($params, &$tpl)\r
+{\r
+       static $count = array();\r
+       static $skipval = array();\r
+       static $dir = array();\r
+       static $name = "default";\r
+       static $printval = array();\r
+       static $assign = "";\r
+\r
+       extract($params);\r
+\r
+       if (!isset($name))\r
+       {\r
+               if(isset($id))\r
+               {\r
+                       $name = $id;\r
+               }\r
+               else\r
+               {\r
+                       $name = "default";\r
+               }\r
+       }\r
+\r
+       if (isset($start))\r
+       {\r
+               $count[$name] = $start;\r
+       }\r
+       elseif (!isset($count[$name]))\r
+       {\r
+               $count[$name]=1;\r
+       }\r
+\r
+       if (!isset($print))\r
+       {\r
+               $printval[$name]=true;\r
+       }\r
+       else\r
+       {\r
+               $printval[$name]=$print;\r
+       }\r
+\r
+       if (!empty($assign))\r
+       {\r
+               $printval[$name] = false;\r
+               $tpl->assign($assign, $count[$name]);\r
+       }\r
+\r
+       if ($printval[$name])\r
+       {\r
+               $retval = $count[$name];\r
+       }\r
+       else\r
+       {\r
+               $retval = null;\r
+       }\r
+\r
+       if (isset($skip))\r
+       {\r
+               $skipval[$name] = $skip;\r
+       }\r
+       elseif (empty($skipval[$name]))\r
+       {\r
+               $skipval[$name] = 1;\r
+       }\r
+\r
+       if (isset($direction))\r
+       {\r
+               $dir[$name] = $direction;\r
+       }\r
+       elseif (!isset($dir[$name]))\r
+       {\r
+               $dir[$name] = "up";\r
+       }\r
+\r
+       if ($dir[$name] == "down")\r
+       {\r
+               $count[$name] -= $skipval[$name];\r
+       }\r
+       else\r
+       {\r
+               $count[$name] += $skipval[$name];\r
+       }\r
+\r
+       return $retval;\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.cycle.php b/include/libs/template_lite/plugins/function.cycle.php
new file mode 100644 (file)
index 0000000..770b619
--- /dev/null
@@ -0,0 +1,101 @@
+<?php\r
+/*\r
+ * template_lite plugin\r
+ *\r
+ * Type:     function\r
+ * Name:     cycle\r
+ * Version:  1.3\r
+ * Date:     May 3, 2002\r
+ * Author:   Monte Ohrt <monte@ispi.net>\r
+ * Credits:  Mark Priatel <mpriatel@rogers.com>\r
+ *           Gerard <gerard@interfold.com>\r
+ *           Jason Sweat <jsweat_php@yahoo.com>\r
+ * Purpose:  cycle through given values\r
+ * Input:    name = name of cycle (optional)\r
+ *           values = comma separated list of values to cycle,\r
+ *                    or an array of values to cycle\r
+ *                    (this can be left out for subsequent calls)\r
+ *           reset = boolean - resets given var to true\r
+ *           print = boolean - print var or not. default is true\r
+ *           advance = boolean - whether or not to advance the cycle\r
+ *           delimiter = the value delimiter, default is ","\r
+ *           assign = boolean, assigns to template var instead of\r
+ *                    printed.\r
+ * Examples: {cycle values="#eeeeee,#d0d0d0d"}\r
+ *           {cycle name=row values="one,two,three" reset=true}\r
+ *           {cycle name=row}\r
+ * Credit:   Taken from the original Smarty\r
+ *           http://smarty.php.net\r
+ */\r
+function tpl_function_cycle($params, &$tpl)\r
+{\r
+       static $cycle_vars;\r
+\r
+       $name    = (empty($params['name']))    ? 'default' : $params['name'];\r
+       $print   = (isset($params['print']))   ? (bool)$params['print'] : true;\r
+       $advance = (isset($params['advance'])) ? (bool)$params['advance'] : true;\r
+       $reset   = (isset($params['reset']))   ? (bool)$params['reset'] : false;\r
+\r
+       if (!in_array('values', array_keys($params)))\r
+       {\r
+               if(!isset($cycle_vars[$name]['values']))\r
+               {\r
+                       throw new Template_Exception("cycle: missing 'values' parameter", $tpl);\r
+                       return;\r
+               }\r
+       }\r
+       else\r
+       {\r
+               if(isset($cycle_vars[$name]['values']) && $cycle_vars[$name]['values'] != $params['values'] )\r
+               {\r
+                       $cycle_vars[$name]['index'] = 0;\r
+               }\r
+               $cycle_vars[$name]['values'] = $params['values'];\r
+       }\r
+\r
+       $cycle_vars[$name]['delimiter'] = (isset($params['delimiter'])) ? $params['delimiter'] : ',';\r
+\r
+       if(is_array($cycle_vars[$name]['values']))\r
+       {\r
+               $cycle_array = $cycle_vars[$name]['values'];\r
+       }\r
+       else\r
+       {\r
+               $cycle_array = explode($cycle_vars[$name]['delimiter'],$cycle_vars[$name]['values']);\r
+       }\r
+\r
+       if(!isset($cycle_vars[$name]['index']) || $reset )\r
+       {\r
+               $cycle_vars[$name]['index'] = 0;\r
+       }\r
+\r
+       if (isset($params['assign']))\r
+       {\r
+               $print = false;\r
+               $tpl->assign($params['assign'], $cycle_array[$cycle_vars[$name]['index']]);\r
+       }\r
+\r
+       if($print)\r
+       {\r
+               $retval = $cycle_array[$cycle_vars[$name]['index']];\r
+       }\r
+       else\r
+       {\r
+               $retval = null;\r
+       }\r
+\r
+       if($advance)\r
+       {\r
+               if ( $cycle_vars[$name]['index'] >= count($cycle_array) -1 )\r
+               {\r
+                       $cycle_vars[$name]['index'] = 0;\r
+               }\r
+               else\r
+               {\r
+                       $cycle_vars[$name]['index']++;\r
+               }\r
+       }\r
+\r
+       return $retval;\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.db_function_call.php b/include/libs/template_lite/plugins/function.db_function_call.php
new file mode 100644 (file)
index 0000000..2c61c38
--- /dev/null
@@ -0,0 +1,67 @@
+<?php\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:        function\r
+ * Name:        db_function_call\r
+ * Purpose:  Interface with ADOdb Lite to query database.\r
+ *\r
+ * db_object = Database object\r
+ * db_function = Database function to execute\r
+ * db_query = query string to pass to the database\r
+ * db_assign = variable name to assign result data\r
+ * db_errornumber_assign = variable name to assign the database error number\r
+ * db_error_assign = the variable name to assign the database error message\r
+ * db_EOF_assign = the variable name to assign the database end of file flag\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_db_function_call($params, &$template_object)\r
+{\r
+       if (empty($params['db_object']))\r
+       {\r
+               throw new Template_Exception("db_function_call: missing db_object parameter", $template_object);\r
+               return;\r
+       }\r
+\r
+       if (!is_object($params['db_object']))\r
+       {\r
+               throw new Template_Exception("db_function_call: db_object isn't an object", $template_object);\r
+               return;\r
+       }\r
+\r
+       $db = $params['db_object'];\r
+\r
+       if (empty($params['db_assign']))\r
+       {\r
+               throw new Template_Exception("db_function_call: missing db_assign parameter", $template_object);\r
+               return;\r
+       }\r
+\r
+       if (empty($params['db_function']))\r
+       {\r
+               throw new Template_Exception("db_function_call: missing db_function parameter", $template_object);\r
+               return;\r
+       }\r
+\r
+       $db_function = $params['db_function'];\r
+\r
+       $result = $db->$db_function($params['db_query']);\r
+\r
+       $template_object->assign($params['db_assign'], $result);\r
+\r
+       if (!empty($params['db_errornumber_assign']))\r
+       {\r
+               $template_object->assign($params['db_errornumber_assign'], $db->ErrorNo());\r
+       }\r
+\r
+       if (!empty($params['db_error_assign']))\r
+       {\r
+               $template_object->assign($params['db_error_assign'], $db->ErrorMsg());\r
+       }\r
+\r
+       if (!empty($params['db_EOF_assign']))\r
+       {\r
+               $template_object->assign($params['db_EOF_assign'], $result->EOF);\r
+       }\r
+}\r
+?>\r
diff --git a/include/libs/template_lite/plugins/function.db_result_call.php b/include/libs/template_lite/plugins/function.db_result_call.php
new file mode 100644 (file)
index 0000000..5d166e0
--- /dev/null
@@ -0,0 +1,75 @@
+<?php\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:        function\r
+ * Name:        db_result_call\r
+ * Purpose:  Interface with ADOdb Lite to return all result elements to assigned variable.\r
+ *\r
+ * db_object = Database object\r
+ * db_function = Database result function to execute\r
+ * db_result_object = Database result object\r
+ * db_assign = variable name to assign result data\r
+ * db_errornumber_assign = variable name to assign the database error number\r
+ * db_error_assign = the variable name to assign the database error message\r
+ * db_EOF_assign = the variable name to assign the database end of file flag\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_db_result_call($params, &$template_object)\r
+{\r
+       if (empty($params['db_object']))\r
+       {\r
+               throw new Template_Exception("db_result_call: missing db_object parameter", $template_object);\r
+       }\r
+\r
+       if (!is_object($params['db_object']))\r
+       {\r
+               throw new Template_Exception("db_result_call: db_object isn't an object", $template_object);\r
+       }\r
+\r
+       $db = $params['db_object'];\r
+\r
+       if (empty($params['db_result_object']))\r
+       {\r
+               throw new Template_Exception("db_result_call: missing db_result_object parameter", $template_object);\r
+       }\r
+\r
+       if (!is_object($params['db_result_object']))\r
+       {\r
+               throw new Template_Exception("db_result_call: db_result_object isn't an object", $template_object);\r
+       }\r
+\r
+       $result_object = $params['db_result_object'];\r
+\r
+       if (empty($params['db_assign']))\r
+       {\r
+               throw new Template_Exception("db_result_call: missing db_assign parameter", $template_object);\r
+       }\r
+\r
+       if (empty($params['db_function']))\r
+       {\r
+               throw new Template_Exception("db_result_call: missing db_function parameter", $template_object);\r
+       }\r
+\r
+       $db_function = $params['db_function'];\r
+\r
+       $result = $result_object->$db_function();\r
+\r
+       $template_object->assign($params['db_assign'], $result);\r
+\r
+       if (!empty($params['db_errornumber_assign']))\r
+       {\r
+               $template_object->assign($params['db_errornumber_assign'], $db->ErrorNo());\r
+       }\r
+\r
+       if (!empty($params['db_error_assign']))\r
+       {\r
+               $template_object->assign($params['db_error_assign'], $db->ErrorMsg());\r
+       }\r
+\r
+       if (!empty($params['db_EOF_assign']))\r
+       {\r
+               $template_object->assign($params['db_EOF_assign'], $result_object->EOF);\r
+       }\r
+}\r
+?>\r
diff --git a/include/libs/template_lite/plugins/function.html_checkboxes.php b/include/libs/template_lite/plugins/function.html_checkboxes.php
new file mode 100644 (file)
index 0000000..5a14a81
--- /dev/null
@@ -0,0 +1,70 @@
+<?php\r
+/**\r
+ * Template_Lite {html_checkbox} function plugin\r
+ *\r
+ * Type:     function\r
+ * Name:     textbox\r
+ * Purpose:  Creates a checkbox\r
+ * Input:\r
+ *           - name = the name of the checkbox\r
+ *           - value = optional value for the checkbox\r
+ *           - checked = boolean - whether the box is checked or not\r
+ * Author:   Paul Lockaby <paul@paullockaby.com>\r
+ */\r
+function tpl_function_html_checkboxes($params, &$tpl)\r
+{\r
+       require_once("shared.escape_chars.php");\r
+       $name = null;\r
+       $value = null;\r
+       $checked = null;\r
+       $extra = '';\r
+\r
+       foreach($params as $_key => $_value)\r
+       {\r
+               switch($_key)\r
+               {\r
+                       case 'name':\r
+                       case 'value':\r
+                               $$_key = $_value;\r
+                               break;\r
+                       case 'checked':\r
+                               if ($_key == 'true' || $_key == 'yes' || $_key == 'on')\r
+                               {\r
+                                       $$_key = true;\r
+                               }\r
+                               else\r
+                               {\r
+                                       $$_key = false;\r
+                               }\r
+                               break;\r
+                       default:\r
+                               if(!is_array($_key))\r
+                               {\r
+                                       $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("html_checkbox: attribute '$_key' cannot be an array", $tpl);\r
+                               }\r
+               }\r
+       }\r
+\r
+       if (!isset($name) || empty($name))\r
+       {\r
+               throw new Template_Exception("html_checkbox: missing 'name' parameter", $tpl);\r
+               return;\r
+       }\r
+\r
+       $toReturn = '<input type="checkbox" name="' . tpl_escape_chars($name) . '"';\r
+       if (isset($checked))\r
+       {\r
+               $toReturn .= ' checked';\r
+       }\r
+       if (isset($value))\r
+       {\r
+               $toReturn .= ' value="' . tpl_escape_chars($value) . '"';\r
+       }\r
+       $toReturn .= ' ' . $extra . ' />';\r
+       return $toReturn;\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_hidden.php b/include/libs/template_lite/plugins/function.html_hidden.php
new file mode 100644 (file)
index 0000000..7f53279
--- /dev/null
@@ -0,0 +1,49 @@
+<?php\r
+/**\r
+ * template_lite {html_hidden} function plugin\r
+ *\r
+ * Type:     function\r
+ * Name:     html_hidden\r
+ * Purpose:  Creates a hidden box\r
+ * Input:\r
+ *           - name = the name of the hidden field\r
+ *           - value = the value of the hidden field\r
+ * Author:   Paul Lockaby <paul@paullockaby.com>\r
+ */\r
+function tpl_function_html_hidden($params, &$tpl)\r
+{\r
+       require_once("shared.escape_chars.php");\r
+       $name = null;\r
+       $value = '';\r
+       $extra = '';\r
+\r
+       foreach($params as $_key => $_value)\r
+       {\r
+               switch($_key)\r
+               {\r
+                       case 'name':\r
+                       case 'value':\r
+                               $$_key = $_value;\r
+                               break;\r
+                       default:\r
+                               if(!is_array($_key))\r
+                               {\r
+                                       $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("html_hidden: attribute '$_key' cannot be an array", $tpl);\r
+                               }\r
+               }\r
+       }\r
+\r
+       if (!isset($name) || empty($name))\r
+       {\r
+               throw new Template_Exception("html_input: missing 'name' parameter", $tpl);\r
+               return;\r
+       }\r
+\r
+       $toReturn = '<input type="hidden" name="' . tpl_escape_chars($name) . '" value="' . tpl_escape_chars($value) . '" ' . $extra . ' />';\r
+       return $toReturn;\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_image.php b/include/libs/template_lite/plugins/function.html_image.php
new file mode 100644 (file)
index 0000000..a3f3dd3
--- /dev/null
@@ -0,0 +1,203 @@
+<?php\r
+/**\r
+ * template_lite {html_image} function plugin\r
+ *\r
+ * Type:     function\r
+ * Name:     html_image\r
+ * Purpose:  Outputs an image tag along with resized height/width\r
+ * Input:\r
+ *           - url = the url of the picture\r
+ *           - width = optional width\r
+ *           - height = optional height\r
+ *           - limit = boolean - will resize image to the above height\r
+ *                     and width if the above height and width are\r
+ *                     smaller than the real height and width\r
+ *           - border = optional size of the border, default is "0"\r
+ *           - alt = optional alternate text to display\r
+ * Examples:<br>\r
+ * <pre>\r
+ * {html_image url="http://www.yoursite.com/image.jpg"}\r
+ * {html_image url="images/me.gif" alt="A picture of me!"}\r
+ * </pre>\r
+ * Author:   Paul Lockaby <paul@paullockaby.com>\r
+ */\r
+function tpl_function_html_image($params, &$tpl)\r
+{\r
+       require_once("shared.escape_chars.php");\r
+       $alt = '';\r
+       $url = '';\r
+       $height = '';\r
+       $width = '';\r
+       $extra = '';\r
+       $prefix = '';\r
+       $suffix = '';\r
+       $basedir = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '';\r
+       foreach($params as $_key => $_val)\r
+       {\r
+               switch($_key)\r
+               {\r
+                       case 'url':\r
+                       case 'height':\r
+                       case 'width':\r
+                               $$_key = $_val;\r
+                               break;\r
+                       case 'limit':\r
+                               $$_key = true;\r
+                               break;\r
+                       case 'alt':\r
+                               if(!is_array($_val))\r
+                               {\r
+                                       $$_key = tpl_escape_chars($_val);\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("html_image: attribute '$_key' cannot be an array", $tpl);\r
+                               }\r
+                               break;\r
+                       case 'link':\r
+                       case 'href':\r
+                               $prefix = '<a href="' . $_val . '">';\r
+                               $suffix = '</a>';\r
+                               break;\r
+                       default:\r
+                               if(!is_array($_val))\r
+                               {\r
+                                       $extra .= ' '.$_key.'="'.template_function_escape_special_chars($_val).'"';\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("html_image: extra attribute '$_key' cannot be an array", $tpl);\r
+                               }\r
+                               break;\r
+                       }\r
+       }\r
+\r
+       if (empty($url))\r
+       {\r
+               throw new Template_Exception("html_image: missing 'url' parameter", $tpl);\r
+               return;\r
+       }\r
+\r
+       if (substr($file,0,1) == '/')\r
+       {\r
+               $_image_path = $basedir . $file;\r
+       }\r
+       else\r
+       {\r
+               $_image_path = $file;\r
+       }\r
+\r
+       // 0 = width, 1 = height\r
+       if ($size = @getimagesize($url))\r
+       {\r
+               if (empty($limit) || $limit == false)\r
+               {\r
+                       // only a height was specified; we will fill in the width\r
+                       if (!empty($height))\r
+                       {\r
+                               $width = $size[0];\r
+                       }\r
+                       // only a width was specified; we will fill in the height\r
+                       if (!empty($width))\r
+                       {\r
+                               $height = $size[1];\r
+                       }\r
+                       // neither a height nor a width was specified; we will fill in both\r
+                       if (empty($width) && empty($height))\r
+                       {\r
+                               $width = $size[0];\r
+                               $height = $size[1];\r
+                       }\r
+               }\r
+               else\r
+               {\r
+                       if ((!empty($width) && ($size[0] > $width)) || (!empty($height) && ($size[1] > $height)))\r
+                       {\r
+                               if (!empty($height) && !empty($width))\r
+                               {\r
+                                       // compare the ratios to determine how much each dimension needs to be changed\r
+\r
+                                       // this will return the width if the height is set to specified\r
+                                       $bth_width = round($size[0]*($height/$size[1]));\r
+\r
+                                       // this will return the height if the width is set to specified\r
+                                       $bth_height = round($size[1]*($width/$size[0]));\r
+\r
+                                       // first we set the width to the max and see how big the height will be\r
+                                       if (!($bth_height > $height))\r
+                                       {\r
+                                               // returned height is acceptable (i.e. less than specified)\r
+                                               $fin1_height = $bth_height;\r
+                                               $fin1_width = $width;\r
+                                       }\r
+\r
+                                       // now we set the height to the max and see how big the width will be\r
+                                       if (!($bth_width > $width))\r
+                                       {\r
+                                               // returned width is acceptable (i.e. less than specified)\r
+                                               $fin2_height = $height;\r
+                                               $fin2_width = $bth_width;\r
+                                       }\r
+\r
+                                       // check to see if both of them went through\r
+                                       if (isset($fin1_height) && isset($fin1_width) && isset($fin2_height) && isset($fin2_width))\r
+                                       {\r
+                                               // now check the difference between abs($fin1_height-$fin1_width) and abs($fin2_height-$fin2_width)\r
+                                               // since we obviously want the larger image, take whichever one has the smaller difference\r
+                                               if (abs($fin1_height - $fin1_width) < abs($fin2_height - $fin2_width))\r
+                                               {\r
+                                                       $new_height = $fin1_height;\r
+                                                       $new_width = $fin1_width;\r
+                                               }\r
+                                               else\r
+                                               {\r
+                                                       $new_height = $fin2_height;\r
+                                                       $new_width = $fin2_width;\r
+                                               }\r
+                                       }\r
+                                       else\r
+                                       {\r
+                                               // since $new_height and $new_width weren't set above, we have to set them here\r
+                                               if (isset($fin1_height) && isset($fin1_width))\r
+                                               {\r
+                                                       $new_height = $fin1_height;\r
+                                                       $new_width = $fin1_width;\r
+                                               }\r
+                                               else\r
+                                               {\r
+                                                       $new_height = $fin2_height;\r
+                                                       $new_width = $fin2_width;\r
+                                               }\r
+                                       }\r
+                               }\r
+                               else\r
+                               {\r
+                                       // only a height or only a width was specified\r
+                                       // much easier\r
+                                       if (!empty($height))\r
+                                       {\r
+                                               // working with only a height now\r
+                                               $new_height = $height;\r
+                                               $new_width = round($size[0]*($height/$size[1]));\r
+                                       }\r
+                                       if (!empty($width))\r
+                                       {\r
+                                               // working with only a width now\r
+                                               $new_height = round($size[1]*($width/$size[0]));\r
+                                               $new_width = $width;\r
+                                       }\r
+                               }\r
+                               $width = $new_width;\r
+                               $height = $new_height;\r
+                       }\r
+                       else\r
+                       {\r
+                               $width = $size[0];\r
+                               $height = $size[1];\r
+                       }\r
+               }\r
+       }\r
+\r
+       return $prefix . '<img src="' . $url . '" alt="' . $alt . '" width="' . $width . '" height="' . $height . '"'.$extra.' />' . $suffix;\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_input.php b/include/libs/template_lite/plugins/function.html_input.php
new file mode 100644 (file)
index 0000000..453554d
--- /dev/null
@@ -0,0 +1,58 @@
+<?php\r
+/**\r
+ * template_lite {html_input} function plugin\r
+ *\r
+ * Type:     function\r
+ * Name:     html_input\r
+ * Purpose:  Creates an input text or password box\r
+ * Input:\r
+ *           - name = the name of the textbox\r
+ *           - password = boolean - if set, this box will be a password box\r
+ *           - value = optional default value for the input box\r
+ *           - size = optional size for the input box\r
+ *           - length = optional maxlength for the input box\r
+ * Author:   Paul Lockaby <paul@paullockaby.com>\r
+ */\r
+function tpl_function_html_input($params, &$tpl)\r
+{\r
+       require_once("shared.escape_chars.php");\r
+       $name = null;\r
+       $value = '';\r
+       $password = false;\r
+       $extra = '';\r
+\r
+       foreach($params as $_key => $_value)\r
+       {\r
+               switch($_key)\r
+               {\r
+                       case 'name':\r
+                       case 'value':\r
+                               $$_key = $_value;\r
+                               break;\r
+                       case 'password':\r
+                               $$_key = true;\r
+                               break;\r
+                       default:\r
+                               if(!is_array($_key))\r
+                               {\r
+                                       $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("html_input: attribute '$_key' cannot be an array", $tpl);\r
+                               }\r
+               }\r
+       }\r
+\r
+       if (!isset($name) || empty($name))\r
+       {\r
+               throw new Template_Exception("html_input: missing 'name' parameter", $tpl);\r
+               return;\r
+       }\r
+\r
+       $toReturn = '<input type="';\r
+       $toReturn .= $password ? 'password' : 'text';\r
+       $toReturn .= '" name="' . tpl_escape_chars($name) . '" value="' . tpl_escape_chars($value) . '" ' . $extra . ' />';\r
+       return $toReturn;\r
+}\r
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_options.php b/include/libs/template_lite/plugins/function.html_options.php
new file mode 100644 (file)
index 0000000..b19f097
--- /dev/null
@@ -0,0 +1,104 @@
+<?php\r
+/*\r
+ * template_lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     html_options\r
+ * Purpose:  prints out the options for an html_select item\r
+ * Credit:   Taken from the original Smarty\r
+ *           http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_html_options($params, &$tpl)\r
+{\r
+       require_once("shared.escape_chars.php");\r
+       $name = null;\r
+       $options = null;\r
+       $selected = array();\r
+       $extra = '';\r
+\r
+       foreach($params as $_key => $_val)\r
+       {\r
+               switch($_key)\r
+               {\r
+                       case 'name':\r
+                               $$_key = (string)$_val;\r
+                               break;\r
+                       case 'options':\r
+                               $$_key = (array)$_val;\r
+                               break;\r
+                       case 'values':\r
+                       case 'output':\r
+                               $$_key = array_values((array)$_val);\r
+                               break;\r
+                       case 'selected':\r
+                               $$_key = array_values((array)$_val);\r
+                               break;\r
+                       default:\r
+                               if(!is_array($_key))\r
+                               {\r
+                                       $extra .= ' ' . $_key . '="' . tpl_escape_chars($_val) . '"';\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("html_select: attribute '$_key' cannot be an array", $tpl);\r
+                               }\r
+                               break;\r
+               }\r
+       }\r
+\r
+       $_html_result = '';\r
+       if (is_array($options))\r
+       {\r
+               foreach ($options as $_key=>$_val)\r
+               {\r
+                       $_html_result .= tpl_function_html_options_optoutput($tpl, $_key, $_val, $selected);\r
+               }\r
+       }\r
+       else\r
+       {\r
+               foreach ((array)$values as $_i=>$_key)\r
+               {\r
+                       $_val = isset($output[$_i]) ? $output[$_i] : '';\r
+                       $_html_result .= tpl_function_html_options_optoutput($tpl, $_key, $_val, $selected);\r
+               }\r
+        }\r
+\r
+       if(!empty($name))\r
+       {\r
+               $_html_result = '<select name="' . tpl_escape_chars($name) . '"' . $extra . '>' . "\n" . $_html_result . '</select>' . "\n";\r
+       }\r
+\r
+       return $_html_result;\r
+}\r
+\r
+function tpl_function_html_options_optoutput(&$tpl, $key, $value, $selected)\r
+{\r
+       if(!is_array($value))\r
+       {\r
+               $_html_result = '<option label="' . tpl_escape_chars($value) . '" value="' . tpl_escape_chars($key) . '"';\r
+               if (in_array($key, $selected))\r
+               {\r
+                       $_html_result .= ' selected="selected"';\r
+               }\r
+               $_html_result .= '>' . tpl_escape_chars($value) . '</option>' . "\n";\r
+       }\r
+       else\r
+       {\r
+               $_html_result = tpl_function_html_options_optgroup($tpl, $key, $value, $selected);\r
+       }\r
+       return $_html_result;\r
+}\r
+\r
+function tpl_function_html_options_optgroup(&$tpl, $key, $values, $selected)\r
+{\r
+       $optgroup_html = '<optgroup label="' . tpl_escape_chars($key) . '">' . "\n";\r
+       foreach ($values as $key => $value)\r
+       {\r
+               $optgroup_html .= tpl_function_html_options_optoutput($tpl, $key, $value, $selected);\r
+       }\r
+       $optgroup_html .= "</optgroup>\n";\r
+       return $optgroup_html;\r
+}\r
+\r
+?>
\ 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 (file)
index 0000000..7b2f236
--- /dev/null
@@ -0,0 +1,55 @@
+<?php\r
+/**\r
+ * Template_Lite {html_radios} function plugin\r
+ *\r
+ * Type:     function\r
+ * Name:     radio\r
+ * Purpose:  Creates a radio button\r
+ * Input:\r
+ *           - name = the name of the radio button\r
+ *           - value = optional value for the checkbox\r
+ *           - checked = boolean - whether the box is checked or not\r
+ * Author:   Paul Lockaby <paul@paullockaby.com>\r
+ */\r
+function tpl_function_html_radios($params, &$tpl)\r
+{\r
+       require_once("shared.escape_chars.php");\r
+       $name = null;\r
+       $value = '';\r
+       $extra = '';\r
+\r
+       foreach($params as $_key => $_value)\r
+       {\r
+               switch($_key)\r
+               {\r
+                       case 'name':\r
+                       case 'value':\r
+                               $$_key = $_value;\r
+                               break;\r
+                       default:\r
+                               if(!is_array($_key))\r
+                               {\r
+                                       $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("html_radio: attribute '$_key' cannot be an array", $tpl);\r
+                               }\r
+               }\r
+       }\r
+\r
+       if (!isset($name) || empty($name))\r
+       {\r
+               throw new Template_Exception("html_radio: missing 'name' parameter", $tpl);\r
+               return;\r
+       }\r
+\r
+       $toReturn = '<input type="radio" name="' . tpl_escape_chars($name) . '" value="' . tpl_escape_chars($value) . '"';\r
+       if (isset($checked))\r
+       {\r
+               $toReturn .= ' checked';\r
+       }\r
+       $toReturn .= ' ' . $extra . ' />';\r
+       return $toReturn;\r
+}\r
+?>
\ 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 (file)
index 0000000..c93eb2b
--- /dev/null
@@ -0,0 +1,269 @@
+<?php\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     html_select_date\r
+ * Version:  1.3\r
+ * Purpose:  Prints the dropdowns for date selection.\r
+ * Author:   Andrei Zmievski\r
+ *\r
+ * ChangeLog: 1.0 initial release\r
+ *            1.1 added support for +/- N syntax for begin\r
+ *                and end year values. (Monte)\r
+ *            1.2 added support for yyyy-mm-dd syntax for\r
+ *                time value. (Jan Rosier)\r
+ *            1.3 added support for choosing format for \r
+ *                month values (Gary Loescher)\r
+ *            1.3.1 added support for choosing format for\r
+ *                day values (Marcus Bointon)\r
+ * Taken from the original Smarty\r
+ * http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_html_select_date($params, &$template_object)\r
+{\r
+       require_once("shared.make_timestamp.php");\r
+       require_once("function.html_options.php");\r
+\r
+    /* Default values. */\r
+    $prefix          = "Date_";\r
+    $start_year      = strftime("%Y");\r
+    $end_year        = $start_year;\r
+    $display_days    = true;\r
+    $display_months  = true;\r
+    $display_years   = true;\r
+    $month_format    = "%B";\r
+    /* Write months as numbers by default  GL */\r
+    $month_value_format = "%m";\r
+    $day_format      = "%02d";\r
+    /* Write day values using this format MB */\r
+    $day_value_format = "%d";\r
+    $year_as_text    = false;\r
+    /* Display years in reverse order? Ie. 2000,1999,.... */\r
+    $reverse_years   = false;\r
+    /* Should the select boxes be part of an array when returned from PHP?\r
+       e.g. setting it to "birthday", would create "birthday[Day]",\r
+       "birthday[Month]" & "birthday[Year]". Can be combined with prefix */\r
+    $field_array     = null;\r
+    /* <select size>'s of the different <select> tags.\r
+       If not set, uses default dropdown. */\r
+    $day_size        = null;\r
+    $month_size      = null;\r
+    $year_size       = null;\r
+    /* Unparsed attributes common to *ALL* the <select>/<input> tags.\r
+       An example might be in the template: all_extra ='class ="foo"'. */\r
+    $all_extra       = null;\r
+    /* Separate attributes for the tags. */\r
+    $day_extra       = null;\r
+    $month_extra     = null;\r
+    $year_extra      = null;\r
+    /* Order in which to display the fields.\r
+       "D" -> day, "M" -> month, "Y" -> year. */\r
+    $field_order      = 'MDY';\r
+    /* String printed between the different fields. */\r
+    $field_separator = "\n";\r
+       $time = time();\r
+\r
+    extract($params);\r
+\r
+       // If $time is not in format yyyy-mm-dd\r
+       if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $time))\r
+       {\r
+               // then $time is empty or unix timestamp or mysql timestamp\r
+               // using smarty_make_timestamp to get an unix timestamp and\r
+               // strftime to make yyyy-mm-dd\r
+               $time = strftime('%Y-%m-%d', tpl_make_timestamp($time));\r
+       }\r
+       // Now split this in pieces, which later can be used to set the select\r
+       $time = explode("-", $time);\r
+\r
+       // make syntax "+N" or "-N" work with start_year and end_year\r
+       if (preg_match('!^(\+|\-)\s*(\d+)$!', $end_year, $match))\r
+       {\r
+               if ($match[1] == '+')\r
+               {\r
+                       $end_year = strftime('%Y') + $match[2];\r
+               }\r
+               else\r
+               {\r
+                       $end_year = strftime('%Y') - $match[2];\r
+               }\r
+       }\r
+       if (preg_match('!^(\+|\-)\s*(\d+)$!', $start_year, $match))\r
+       {\r
+               if ($match[1] == '+')\r
+               {\r
+                       $start_year = strftime('%Y') + $match[2];\r
+               }\r
+               else\r
+               {\r
+                       $start_year = strftime('%Y') - $match[2];\r
+               }\r
+       }\r
+\r
+    $field_order = strtoupper($field_order);\r
+    $html_result = $month_result = $day_result = $year_result = "";\r
+\r
+    if ($display_months)\r
+       {\r
+        $month_names = array();\r
+        $month_values = array();\r
+\r
+        for ($i = 1; $i <= 12; $i++)\r
+               {\r
+            $month_names[] = strftime($month_format, mktime(0, 0, 0, $i, 1, 2000));\r
+            $month_values[] = strftime($month_value_format, mktime(0, 0, 0, $i, 1, 2000));\r
+        }\r
+\r
+        $month_result .= '<select name=';\r
+        if (null !== $field_array)\r
+               {\r
+            $month_result .= '"' . $field_array . '[' . $prefix . 'Month]"';\r
+        }\r
+               else\r
+               {\r
+            $month_result .= '"' . $prefix . 'Month"';\r
+        }\r
+        if (null !== $month_size)\r
+               {\r
+            $month_result .= ' size="' . $month_size . '"';\r
+        }\r
+        if (null !== $month_extra)\r
+               {\r
+            $month_result .= ' ' . $month_extra;\r
+        }\r
+        if (null !== $all_extra)\r
+               {\r
+            $month_result .= ' ' . $all_extra;\r
+        }\r
+        $month_result .= '>'."\n";\r
+        $month_result .= tpl_function_html_options(array('output'     => $month_names,\r
+                                                            'values'     => $month_values,\r
+                                                            'selected'   => $month_values[$time[1]-1],\r
+                                                            'print_result' => false),\r
+                                                      $template_object);\r
+        $month_result .= '</select>';\r
+    }\r
+\r
+    if ($display_days)\r
+       {\r
+        $days = array();\r
+        for ($i = 1; $i <= 31; $i++)\r
+               {\r
+            $days[] = sprintf($day_format, $i);\r
+            $day_values[] = sprintf($day_value_format, $i);\r
+        }\r
+\r
+        $day_result .= '<select name=';\r
+        if (null !== $field_array)\r
+               {\r
+            $day_result .= '"' . $field_array . '[' . $prefix . 'Day]"';\r
+        }\r
+               else\r
+               {\r
+            $day_result .= '"' . $prefix . 'Day"';\r
+        }\r
+        if (null !== $day_size)\r
+               {\r
+            $day_result .= ' size="' . $day_size . '"';\r
+        }\r
+        if (null !== $all_extra)\r
+               {\r
+            $day_result .= ' ' . $all_extra;\r
+        }\r
+        if (null !== $day_extra)\r
+               {\r
+            $day_result .= ' ' . $day_extra;\r
+        }\r
+        $day_result .= '>'."\n";\r
+        $day_result .= tpl_function_html_options(array('output'     => $days,\r
+                                                          'values'     => $day_values,\r
+                                                          'selected'   => $time[2],\r
+                                                          'print_result' => false),\r
+                                                    $template_object);\r
+        $day_result .= '</select>';\r
+    }\r
+\r
+    if ($display_years)\r
+       {\r
+        if (null !== $field_array)\r
+               {\r
+            $year_name = $field_array . '[' . $prefix . 'Year]';\r
+        }\r
+               else\r
+               {\r
+            $year_name = $prefix . 'Year';\r
+        }\r
+        if ($year_as_text)\r
+               {\r
+            $year_result .= '<input type="text" name="' . $year_name . '" value="' . $time[0] . '" size="4" maxlength="4"';\r
+            if (null !== $all_extra)\r
+                       {\r
+                $year_result .= ' ' . $all_extra;\r
+            }\r
+            if (null !== $year_extra)\r
+                       {\r
+                $year_result .= ' ' . $year_extra;\r
+            }\r
+            $year_result .= '>';\r
+        }\r
+               else\r
+               {\r
+            $years = range((int)$start_year, (int)$end_year);\r
+            if ($reverse_years)\r
+                       {\r
+                rsort($years, SORT_NUMERIC);\r
+            }\r
+\r
+            $year_result .= '<select name="' . $year_name . '"';\r
+            if (null !== $year_size)\r
+                       {\r
+                $year_result .= ' size="' . $year_size . '"';\r
+            }\r
+            if (null !== $all_extra)\r
+                       {\r
+                $year_result .= ' ' . $all_extra;\r
+            }\r
+            if (null !== $year_extra)\r
+                       {\r
+                $year_result .= ' ' . $year_extra;\r
+            }\r
+            $year_result .= '>'."\n";\r
+            $year_result .= tpl_function_html_options(array('output' => $years,\r
+                                                               'values' => $years,\r
+                                                               'selected'   => $time[0],\r
+                                                               'print_result' => false),\r
+                                                         $template_object);\r
+            $year_result .= '</select>';\r
+        }\r
+    }\r
+\r
+    // Loop thru the field_order field\r
+    for ($i = 0; $i <= 2; $i++)\r
+       {\r
+               $c = substr($field_order, $i, 1);\r
+               switch ($c)\r
+               {\r
+                       case 'D':\r
+                               $html_result .= $day_result;\r
+                               break;\r
+\r
+                       case 'M':\r
+                               $html_result .= $month_result;\r
+                               break;\r
+\r
+                       case 'Y':\r
+                               $html_result .= $year_result;\r
+                               break;\r
+               }\r
+               // Add the field seperator\r
+               if($i != 2)\r
+               {\r
+                       $html_result .= $field_separator;\r
+               }\r
+       }\r
+    return $html_result;\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..48c6b8f
--- /dev/null
@@ -0,0 +1,177 @@
+<?php\r
+\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     html_select_time\r
+ * Purpose:  Prints the dropdowns for time selection\r
+ * Taken from the original Smarty\r
+ * http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_html_select_time($params, &$template_object)\r
+{\r
+       require_once("shared.make_timestamp.php");\r
+       require_once("function.html_options.php");\r
+\r
+    /* Default values. */\r
+    $prefix             = "Time_";\r
+    $time               = time();\r
+    $display_hours      = true;\r
+    $display_minutes    = true;\r
+    $display_seconds    = true;\r
+    $display_meridian   = true;\r
+    $use_24_hours       = true;\r
+    $minute_interval    = 1;\r
+    $second_interval    = 1;\r
+    /* Should the select boxes be part of an array when returned from PHP?\r
+       e.g. setting it to "birthday", would create "birthday[Hour]",\r
+       "birthday[Minute]", "birthday[Seconds]" & "birthday[Meridian]".\r
+       Can be combined with prefix. */\r
+    $field_array        = null;\r
+    $all_extra          = null;\r
+    $hour_extra         = null;\r
+    $minute_extra       = null;\r
+    $second_extra       = null;\r
+    $meridian_extra     = null;\r
+\r
+    extract($params);\r
+\r
+    $time = tpl_make_timestamp($time);\r
+\r
+    $html_result = '';\r
+\r
+    if ($display_hours)\r
+       {\r
+        $hours       = $use_24_hours ? range(0, 23) : range(1, 12);\r
+        $hour_fmt = $use_24_hours ? '%H' : '%I';\r
+        for ($i = 0, $for_max = count($hours); $i < $for_max; $i++)\r
+               {\r
+            $hours[$i] = sprintf('%02d', $hours[$i]);\r
+               }\r
+        $html_result .= '<select name=';\r
+        if (null !== $field_array)\r
+               {\r
+            $html_result .= '"' . $field_array . '[' . $prefix . 'Hour]"';\r
+        }\r
+               else\r
+               {\r
+            $html_result .= '"' . $prefix . 'Hour"';\r
+        }\r
+        if (null !== $hour_extra)\r
+               {\r
+            $html_result .= ' ' . $hour_extra;\r
+        }\r
+        if (null !== $all_extra)\r
+               {\r
+            $html_result .= ' ' . $all_extra;\r
+        }\r
+        $html_result .= '>'."\n";\r
+        $html_result .= tpl_function_html_options(array('output'          => $hours,\r
+                                                           'values'          => $hours,\r
+                                                           'selected'      => strftime($hour_fmt, $time),\r
+                                                           'print_result' => false),\r
+                                                     $template_object);\r
+        $html_result .= "</select>\n";\r
+    }\r
+\r
+    if ($display_minutes)\r
+       {\r
+        $all_minutes = range(0, 59);\r
+        for ($i = 0, $for_max = count($all_minutes); $i < $for_max; $i+= $minute_interval)\r
+               {\r
+            $minutes[] = sprintf('%02d', $all_minutes[$i]);\r
+               }\r
+        $selected = intval(floor(strftime('%M', $time) / $minute_interval) * $minute_interval);\r
+        $html_result .= '<select name=';\r
+        if (null !== $field_array)\r
+               {\r
+            $html_result .= '"' . $field_array . '[' . $prefix . 'Minute]"';\r
+        }\r
+               else\r
+               {\r
+            $html_result .= '"' . $prefix . 'Minute"';\r
+        }\r
+        if (null !== $minute_extra)\r
+               {\r
+            $html_result .= ' ' . $minute_extra;\r
+        }\r
+        if (null !== $all_extra)\r
+               {\r
+            $html_result .= ' ' . $all_extra;\r
+        }\r
+        $html_result .= '>'."\n";\r
+        $html_result .= tpl_function_html_options(array('output'          => $minutes,\r
+                                                           'values'          => $minutes,\r
+                                                           'selected'      => $selected,\r
+                                                           'print_result' => false),\r
+                                                     $template_object);\r
+        $html_result .= "</select>\n";\r
+    }\r
+\r
+    if ($display_seconds)\r
+       {\r
+        $all_seconds = range(0, 59);\r
+        for ($i = 0, $for_max = count($all_seconds); $i < $for_max; $i+= $second_interval)\r
+               {\r
+            $seconds[] = sprintf('%02d', $all_seconds[$i]);\r
+               }\r
+        $selected = intval(floor(strftime('%S', $time) / $second_interval) * $second_interval);\r
+        $html_result .= '<select name=';\r
+        if (null !== $field_array)\r
+               {\r
+            $html_result .= '"' . $field_array . '[' . $prefix . 'Second]"';\r
+        }\r
+               else\r
+               {\r
+            $html_result .= '"' . $prefix . 'Second"';\r
+        }\r
+        if (null !== $second_extra)\r
+               {\r
+            $html_result .= ' ' . $second_extra;\r
+        }\r
+        if (null !== $all_extra)\r
+               {\r
+            $html_result .= ' ' . $all_extra;\r
+        }\r
+        $html_result .= '>'."\n";\r
+        $html_result .= tpl_function_html_options(array('output'          => $seconds,\r
+                                                           'values'          => $seconds,\r
+                                                           'selected'      => $selected,\r
+                                                           'print_result' => false),\r
+                                                     $template_object);\r
+        $html_result .= "</select>\n";\r
+    }\r
+\r
+    if ($display_meridian && !$use_24_hours)\r
+       {\r
+        $html_result .= '<select name=';\r
+        if (null !== $field_array)\r
+               {\r
+            $html_result .= '"' . $field_array . '[' . $prefix . 'Meridian]"';\r
+        }\r
+               else\r
+               {\r
+            $html_result .= '"' . $prefix . 'Meridian"';\r
+        }\r
+        if (null !== $meridian_extra)\r
+               {\r
+            $html_result .= ' ' . $meridian_extra;\r
+        }\r
+        if (null !== $all_extra)\r
+               {\r
+            $html_result .= ' ' . $all_extra;\r
+        }\r
+        $html_result .= '>'."\n";\r
+        $html_result .= tpl_function_html_options(array('output'          => array('AM', 'PM'),\r
+                                                           'values'          => array('am', 'pm'),\r
+                                                           'selected'      => strtolower(strftime('%p', $time)),\r
+                                                           'print_result' => false),\r
+                                                     $template_object);\r
+        $html_result .= "</select>\n";\r
+    }\r
+    return $html_result;\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..1b04dd7
--- /dev/null
@@ -0,0 +1,88 @@
+<?php\r
+\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     html_table\r
+ * Version:  1.0\r
+ * Date:     Feb 17, 2003\r
+ * Author:      Monte Ohrt <monte@ispi.net>\r
+ * Purpose:  make an html table from an array of data\r
+ * Input:    loop = array to loop through\r
+ *           cols = number of columns\r
+ *           table_attr = table attributes\r
+ *           tr_attr = table row attributes (arrays are cycled)\r
+ *           td_attr = table cell attributes (arrays are cycled)\r
+ *           trailpad = value to pad trailing cells with\r
+ *\r
+ * Examples: {table loop=$data}\r
+ *           {$table loop=$data cols=4 tr_attr='"bgcolor=red"'}\r
+ *           {$table loop=$data cols=4 tr_attr=$colors}\r
+ * Taken from the original Smarty\r
+ * http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_html_table($params, &$template_object)\r
+{\r
+       $table_attr = 'border="1"';\r
+       $tr_attr = '';\r
+       $td_attr = '';\r
+       $cols = 3;\r
+       $trailpad = '&nbsp;';\r
+\r
+       extract($params);\r
+\r
+    if (!isset($loop))\r
+       {\r
+        throw new Template_Exception("html_table: missing 'loop' parameter", $template_object);\r
+        return;\r
+       }\r
+\r
+       $output = "<table $table_attr>\n";\r
+       $output .= "<tr " . tpl_function_html_table_cycle('tr', $tr_attr) . ">\n";\r
+\r
+       for($x = 0, $y = count($loop); $x < $y; $x++)\r
+       {\r
+               $output .= "<td " . tpl_function_html_table_cycle('td', $td_attr) . ">" . $loop[$x] . "</td>\n";\r
+               if((!(($x+1) % $cols)) && $x < $y-1)\r
+               {\r
+                       // go to next row\r
+                       $output .= "</tr>\n<tr " . tpl_function_html_table_cycle('tr', $tr_attr) . ">\n";\r
+               }\r
+               if($x == $y-1)\r
+               {\r
+                       // last row, pad remaining cells\r
+                       $cells = $cols - $y % $cols;\r
+                       if($cells != $cols) {\r
+                               for($padloop = 0; $padloop < $cells; $padloop++) {\r
+                                       $output .= "<td " . tpl_function_html_table_cycle('td', $td_attr) . ">$trailpad</td>\n";\r
+                               }\r
+                       }\r
+                       $output .= "</tr>\n";\r
+               }\r
+       }\r
+       $output .= "</table>\n";\r
+       return $output;\r
+}\r
+\r
+function tpl_function_html_table_cycle($name, $var)\r
+{\r
+       static $names = array();\r
+\r
+       if(!is_array($var))\r
+       {\r
+               return $var;\r
+       }\r
+\r
+       if(!isset($names[$name]) || $names[$name] == count($var)-1)\r
+       {\r
+               $names[$name] = 0;\r
+               return $var[0];\r
+       }\r
+\r
+       $names[$name]++;\r
+       return $var[$names[$name]];\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..da6dbd4
--- /dev/null
@@ -0,0 +1,51 @@
+<?php\r
+/**\r
+ * Template_Lite {html_textbox} function plugin\r
+ *\r
+ * Type:     function\r
+ * Name:     html_textbox\r
+ * Purpose:  Creates a textbox\r
+ * Input:\r
+ *           - name = the name of the textbox\r
+ *           - rows = optional number of rows in the textbox\r
+ *           - cols = optional number of columns in the textbox\r
+ *           - value = optional preset value to put in the textbox\r
+ * Author:   Paul Lockaby <paul@paullockaby.com>\r
+ */\r
+function tpl_function_html_textbox($params, &$tpl)\r
+{\r
+       require_once("shared.escape_chars.php");\r
+       $name = null;\r
+       $value = '';\r
+       $extra = '';\r
+\r
+       foreach($params as $_key => $_value)\r
+       {\r
+               switch($_key)\r
+               {\r
+                       case 'name':\r
+                       case 'value':\r
+                               $$_key = $_value;\r
+                               break;\r
+                       default:\r
+                               if(!is_array($_key))\r
+                               {\r
+                                       $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';\r
+                               }\r
+                               else\r
+                               {\r
+                                       throw new Template_Exception("html_textbox: attribute '$_key' cannot be an array", $tpl);\r
+                               }\r
+               }\r
+       }\r
+\r
+       if (!isset($name) || empty($name))\r
+       {\r
+               throw new Template_Exception("html_textbox: missing 'name' parameter", $tpl);\r
+               return;\r
+       }\r
+\r
+       $toReturn = '<textarea name="' . tpl_escape_chars($name) . '" ' . $extra . '>' . tpl_escape_chars($value) . '</textarea>';\r
+       return $toReturn;\r
+}\r
+?>
\ 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 (file)
index 0000000..a37aa9e
--- /dev/null
@@ -0,0 +1,21 @@
+<?php \r
+/** \r
+ * template_lite in_array plugin \r
+ * \r
+ * Type:     function \r
+ * Name:     in_array \r
+ * Purpose:  Checks to see if there is an item in the array that matches and returns the returnvalue if true. \r
+ */ \r
+function tpl_function_in_array($params, &$tpl)\r
+{\r
+       extract($params);\r
+\r
+       if (is_array($array))\r
+       {\r
+               if (in_array($match, $array))\r
+               {\r
+                       return $returnvalue;\r
+               }\r
+       }\r
+}\r
+?>
\ 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 (file)
index 0000000..af9273f
--- /dev/null
@@ -0,0 +1,148 @@
+<?php\r
+\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     mailto\r
+ * Version:  1.2\r
+ * Date:     May 21, 2002\r
+ * Author:      Monte Ohrt <monte@ispi.net>\r
+ * Credits:  Jason Sweat (added cc, bcc and subject functionality)\r
+ * Purpose:  automate mailto address link creation, and optionally\r
+ *           encode them.\r
+ * Input:    address = e-mail address\r
+ *           text = (optional) text to display, default is address\r
+ *           encode = (optional) can be one of:\r
+ *                 none : no encoding (default)\r
+ *                 javascript : encode with javascript\r
+ *                 hex : encode with hexidecimal (no javascript)\r
+ *           cc = (optional) address(es) to carbon copy\r
+ *           bcc = (optional) address(es) to blind carbon copy\r
+ *           subject = (optional) e-mail subject\r
+ *           newsgroups = (optional) newsgroup(s) to post to\r
+ *           followupto = (optional) address(es) to follow up to\r
+ *           extra = (optional) extra tags for the href link\r
+ *\r
+ * Examples: {mailto address="me@domain.com"}\r
+ *           {mailto address="me@domain.com" encode="javascript"}\r
+ *           {mailto address="me@domain.com" encode="hex"}\r
+ *           {mailto address="me@domain.com" subject="Hello to you!"}\r
+ *           {mailto address="me@domain.com" cc="you@domain.com,they@domain.com"}\r
+ *           {mailto address="me@domain.com" extra='class="mailto"'}\r
+ * Taken from the original Smarty\r
+ * http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_mailto($params, &$template_object)\r
+{\r
+    extract($params);\r
+\r
+    if (empty($address))\r
+       {\r
+        throw new Template_Exception("mailto: missing 'address' parameter", $template_object);\r
+        return;\r
+    }\r
+\r
+    if (empty($text))\r
+       {\r
+               $text = $address;\r
+    }\r
+\r
+    if (empty($extra))\r
+       {\r
+               $extra = "";\r
+    }\r
+\r
+       // netscape and mozilla do not decode %40 (@) in BCC field (bug?)\r
+       // so, don't encode it.\r
+\r
+       $mail_parms = array();\r
+       if (!empty($cc))\r
+       {\r
+               $mail_parms[] = 'cc='.str_replace('%40','@',rawurlencode($cc));\r
+       }\r
+\r
+       if (!empty($bcc))\r
+       {\r
+               $mail_parms[] = 'bcc='.str_replace('%40','@',rawurlencode($bcc));\r
+       }\r
+\r
+       if (!empty($subject))\r
+       {\r
+               $mail_parms[] = 'subject='.rawurlencode($subject);\r
+       }\r
+\r
+       if (!empty($newsgroups))\r
+       {\r
+               $mail_parms[] = 'newsgroups='.rawurlencode($newsgroups);\r
+       }\r
+\r
+       if (!empty($followupto))\r
+       {\r
+               $mail_parms[] = 'followupto='.str_replace('%40','@',rawurlencode($followupto));\r
+       }\r
+\r
+       $mail_parm_vals = "";\r
+       for ($i=0; $i<count($mail_parms); $i++)\r
+       {\r
+               $mail_parm_vals .= (0==$i) ? '?' : '&';\r
+               $mail_parm_vals .= $mail_parms[$i];\r
+       }\r
+       $address .= $mail_parm_vals;\r
+\r
+       if (empty($encode))\r
+       {\r
+               $encode = 'none';\r
+    }\r
+       elseif (!in_array($encode,array('javascript','hex','none')) )\r
+       {\r
+        throw new Template_Exception("mailto: 'encode' parameter must be none, javascript or hex", $template_object);\r
+        return;\r
+       }\r
+\r
+       if ($encode == 'javascript' )\r
+       {\r
+               $string = 'document.write(\'<a href="mailto:'.$address.'" '.$extra.'>'.$text.'</a>\');';\r
+               $js_encode = '';\r
+               for ($x=0; $x < strlen($string); $x++)\r
+               {\r
+                       $js_encode .= '%' . bin2hex($string[$x]);\r
+               }\r
+               return '<script type="text/javascript" language="javascript">eval(unescape(\''.$js_encode.'\'))</script>';\r
+       }\r
+       elseif ($encode == 'hex')\r
+       {\r
+               preg_match('!^(.*)(\?.*)$!',$address,$match);\r
+               if(!empty($match[2]))\r
+               {\r
+               throw new Template_Exception("mailto: hex encoding does not work with extra attributes. Try javascript.", $template_object);\r
+               return;\r
+               }\r
+               $address_encode = "";\r
+               for ($x=0; $x < strlen($address); $x++)\r
+               {\r
+                       if(preg_match('!\w!',$address[$x]))\r
+                       {\r
+                               $address_encode .= '%' . bin2hex($address[$x]);\r
+                       }\r
+                       else\r
+                       {\r
+                               $address_encode .= $address[$x];\r
+                       }\r
+               }\r
+               $text_encode = "";\r
+               for ($x=0; $x < strlen($text); $x++)\r
+               {\r
+                       $text_encode .= '&#x' . bin2hex($text[$x]).';';\r
+               }\r
+               return '<a href="mailto:'.$address_encode.'" '.$extra.'>'.$text_encode.'</a>';\r
+       }\r
+       else\r
+       {\r
+               // no encoding\r
+               return '<a href="mailto:'.$address.'" '.$extra.'>'.$text.'</a>';\r
+       }\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/plugins/function.math.php b/include/libs/template_lite/plugins/function.math.php
new file mode 100644 (file)
index 0000000..7369ef7
--- /dev/null
@@ -0,0 +1,90 @@
+<?php\r
+\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     math\r
+ * Purpose:  handle math computations in template\r
+ * Taken from the original Smarty\r
+ * http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_math($params, &$template_object)\r
+{\r
+    // be sure equation parameter is present\r
+    if (empty($params['equation']))\r
+       {\r
+        throw new Template_Exception("math: missing equation parameter", $template_object);\r
+        return;\r
+    }\r
+\r
+    $equation = $params['equation'];\r
+\r
+    // make sure parenthesis are balanced\r
+    if (substr_count($equation,"(") != substr_count($equation,")"))\r
+       {\r
+        throw new Template_Exception("math: unbalanced parenthesis", $template_object);\r
+        return;\r
+    }\r
+\r
+    // match all vars in equation, make sure all are passed\r
+    preg_match_all("![a-zA-Z][a-zA-Z0-9_]*!",$equation, $match);\r
+    $allowed_funcs = array('int','abs','ceil','cos','exp','floor','log','log10',\r
+                           'max','min','pi','pow','rand','round','sin','sqrt','srand','tan');\r
+\r
+    foreach($match[0] as $curr_var)\r
+       {\r
+        if (!in_array($curr_var,array_keys($params)) && !in_array($curr_var, $allowed_funcs))\r
+               {\r
+            throw new Template_Exception("math: parameter $curr_var not passed as argument", $template_object);\r
+            return;\r
+        }\r
+    }\r
+\r
+    foreach($params as $key => $val)\r
+       {\r
+        if ($key != "equation" && $key != "format" && $key != "assign")\r
+               {\r
+            // make sure value is not empty\r
+            if (strlen($val)==0)\r
+                       {\r
+                throw new Template_Exception("math: parameter $key is empty", $template_object);\r
+                return;\r
+            }\r
+            if (!is_numeric($val))\r
+                       {\r
+                throw new Template_Exception("math: parameter $key: is not numeric", $template_object);\r
+                return;\r
+            }\r
+            $equation = preg_replace("/\b$key\b/",$val, $equation);\r
+        }\r
+    }\r
+\r
+    eval("\$template_object_math_result = ".$equation.";");\r
+\r
+    if (empty($params['format']))\r
+       {\r
+        if (empty($params['assign']))\r
+               {\r
+            return $template_object_math_result;\r
+        }\r
+               else\r
+               {\r
+            $template_object->assign($params['assign'],$template_object_math_result);\r
+        }\r
+    }\r
+       else\r
+       {\r
+        if (empty($params['assign']))\r
+               {\r
+            printf($params['format'],$template_object_math_result);\r
+        }\r
+               else\r
+               {\r
+            $template_object->assign($params['assign'],sprintf($params['format'],$template_object_math_result));\r
+        }\r
+    }\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/plugins/function.popup.php b/include/libs/template_lite/plugins/function.popup.php
new file mode 100644 (file)
index 0000000..3f68fcc
--- /dev/null
@@ -0,0 +1,81 @@
+<?php\r
+\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     popup\r
+ * Purpose:  make text pop up in windows via overlib\r
+ * Taken from the original Smarty\r
+ * http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_popup($params, &$template_object)\r
+{\r
+    extract($params);\r
+\r
+    if (empty($text) && !isset($inarray) && empty($function))\r
+       {\r
+        throw new Template_Exception("overlib: attribute 'text' or 'inarray' or 'function' required", $template_object);\r
+        return false;\r
+    }\r
+\r
+    if (empty($trigger))\r
+       {\r
+               $trigger = "onmouseover";\r
+       }\r
+\r
+    $retval = $trigger . '="return overlib(\''.preg_replace(array("!'!","![\r\n]!"),array("\'",'\r'),$text).'\'';\r
+    if ($sticky) { $retval .= ",STICKY"; }\r
+    if (!empty($caption)) { $retval .= ",CAPTION,'".str_replace("'","\'",$caption)."'"; }\r
+    if (!empty($fgcolor)) { $retval .= ",FGCOLOR,'$fgcolor'"; }\r
+    if (!empty($bgcolor)) { $retval .= ",BGCOLOR,'$bgcolor'"; }\r
+    if (!empty($textcolor)) { $retval .= ",TEXTCOLOR,'$textcolor'"; }\r
+    if (!empty($capcolor)) { $retval .= ",CAPCOLOR,'$capcolor'"; }\r
+    if (!empty($closecolor)) { $retval .= ",CLOSECOLOR,'$closecolor'"; }\r
+    if (!empty($textfont)) { $retval .= ",TEXTFONT,'$textfont'"; }\r
+    if (!empty($captionfont)) { $retval .= ",CAPTIONFONT,'$captionfont'"; }\r
+    if (!empty($closefont)) { $retval .= ",CLOSEFONT,'$closefont'"; }\r
+    if (!empty($textsize)) { $retval .= ",TEXTSIZE,$textsize"; }\r
+    if (!empty($captionsize)) { $retval .= ",CAPTIONSIZE,$captionsize"; }\r
+    if (!empty($closesize)) { $retval .= ",CLOSESIZE,$closesize"; }\r
+    if (!empty($width)) { $retval .= ",WIDTH,$width"; }\r
+    if (!empty($height)) { $retval .= ",HEIGHT,$height"; }\r
+    if (!empty($left)) { $retval .= ",LEFT"; }\r
+    if (!empty($right)) { $retval .= ",RIGHT"; }\r
+    if (!empty($center)) { $retval .= ",CENTER"; }\r
+    if (!empty($above)) { $retval .= ",ABOVE"; }\r
+    if (!empty($below)) { $retval .= ",BELOW"; }\r
+    if (isset($border)) { $retval .= ",BORDER,$border"; }\r
+    if (isset($offsetx)) { $retval .= ",OFFSETX,$offsetx"; }\r
+    if (isset($offsety)) { $retval .= ",OFFSETY,$offsety"; }\r
+    if (!empty($fgbackground)) { $retval .= ",FGBACKGROUND,'$fgbackground'"; }\r
+    if (!empty($bgbackground)) { $retval .= ",BGBACKGROUND,'$bgbackground'"; }\r
+    if (!empty($closetext)) { $retval .= ",CLOSETEXT,'".str_replace("'","\'",$closetext)."'"; }\r
+    if (!empty($noclose)) { $retval .= ",NOCLOSE"; }\r
+    if (!empty($status)) { $retval .= ",STATUS,'".str_replace("'","\'",$status)."'"; }\r
+    if (!empty($autostatus)) { $retval .= ",AUTOSTATUS"; }\r
+    if (!empty($autostatuscap)) { $retval .= ",AUTOSTATUSCAP"; }\r
+    if (isset($inarray)) { $retval .= ",INARRAY,'$inarray'"; }\r
+    if (isset($caparray)) { $retval .= ",CAPARRAY,'$caparray'"; }\r
+    if (!empty($capicon)) { $retval .= ",CAPICON,'$capicon'"; }\r
+    if (!empty($snapx)) { $retval .= ",SNAPX,$snapx"; }\r
+    if (!empty($snapy)) { $retval .= ",SNAPY,$snapy"; }\r
+    if (isset($fixx)) { $retval .= ",FIXX,$fixx"; }\r
+    if (isset($fixy)) { $retval .= ",FIXY,$fixy"; }\r
+    if (!empty($background)) { $retval .= ",BACKGROUND,'$background'"; }\r
+    if (!empty($padx)) { $retval .= ",PADX,$padx"; }\r
+    if (!empty($pady)) { $retval .= ",PADY,$pady"; }\r
+    if (!empty($fullhtml)) { $retval .= ",FULLHTML"; }\r
+    if (!empty($frame)) { $retval .= ",FRAME,'$frame'"; }\r
+    if (isset($timeout)) { $retval .= ",TIMEOUT,$timeout"; }\r
+    if (!empty($function)) { $retval .= ",FUNCTION,'$function'"; }\r
+    if (isset($delay)) { $retval .= ",DELAY,$delay"; }\r
+    if (!empty($hauto)) { $retval .= ",HAUTO"; }\r
+    if (!empty($vauto)) { $retval .= ",VAUTO"; }\r
+    $retval .= ');" onmouseout="nd();"';\r
+\r
+       return $retval;\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..bbcd2b7
--- /dev/null
@@ -0,0 +1,32 @@
+<?php\r
+\r
+/*\r
+ * Template Lite plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     function\r
+ * Name:     popup_init\r
+ * Purpose:  initialize overlib\r
+ * Taken from the original Smarty\r
+ * http://smarty.php.net\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_function_popup_init($params, &$template_object)\r
+{\r
+       $zindex = 1000;\r
+    if (!empty($params['zindex']))\r
+       {\r
+               $zindex = $params['zindex'];\r
+       }\r
+\r
+    if (!empty($params['src']))\r
+       {\r
+       return '<div id="overDiv" style="position:absolute; visibility:hidden; z-index:'.$zindex.';"></div>' . "\n"\r
+         . '<script type="text/javascript" language="JavaScript" src="'.$params['src'].'"></script>' . "\n";\r
+    }\r
+       else\r
+       {\r
+        throw new Template_Exception("popup_init: missing src parameter", $template_object);\r
+    }\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..206a0df
--- /dev/null
@@ -0,0 +1,239 @@
+<?/**\r
+ * Template-Lite {resize_image} function plugin Using Image Magic\r
+ *\r
+ * Type:     function\r
+ * Name:     resize_image\r
+ * Purpose:  Outputs resized image  based on values passed.\r
+ * Input:\r
+ *                     - img_src tag path\r
+ *                     - directory = full directory path where images are located\r
+ *                     - thumbdir = Optional directory path to store thumbnail images.  directory will be used if not supplied.\r
+ *                     - filename = Name of the file\r
+ *                     - xscale = Image max width size default 2000 px\r
+ *                     - yscale = Image max height size default 2000 px\r
+ *                     - thumbname = prefix name for new image, the thumb name is not needed if you are using a return type of 1\r
+ *                     - returntype =  1 - return image tag with full size image name and path but with height and width attributes adjusted\r
+ *                                                     0 - resize image and store thumbnail in directory and return that thumbnail img tag.  Default setting\r
+ *                     - url = Optional URL to download image from for resizing if the image doesn't exist in the image directory (CURL must be installed to use this feature)\r
+ *                     - binpath = Optional path to the ImageMagick mogrify command.  If missing slower GD code will be used.\r
+ *                     - alt = Optional alt attribute for the img tag Default is "image"\r
+ *                     - border = Optional border attribute for img tag Default is 0\r
+ *                     - class = Optional class attribute for img tag\r
+ *                     - daystokeep = Optional number of days to cache the thumbnail image.  Default is 5 days\r
+ *\r
+ * Examples:<br>\r
+ * <pre>\r
+ * {resize_image img_src="/thumbnails/" directory="/html/mysite/ad_images/" thumbdir="/html/mysite/thumbnails/" filename="Myfile.jpg" xscale="150" yscale="200" thumbname="thumb_"}\r
+ * </pre>\r
+ *\r
+ * Output <img src="/thumbnails/thumb_Myfile.jpg" width="150" height="200" alt="image" border="0">\r
+ *\r
+ * Author:   Rick Thomson rick@oznet.com\r
+ * Author:   Mark Dickenson akapanamajack@sourceforge.net\r
+*/\r
+\r
+// Calculate percentage between width and height\r
+function tpl_function_resize_percent($maximum, $current)\r
+{\r
+       return (real)(100 * ($maximum / $current));\r
+}\r
+\r
+function tpl_function_resize_unpercent($percent, $whole)\r
+{\r
+       return (real)(($percent * $whole) / 100);\r
+}\r
+\r
+function tpl_function_resize_image($params, &$tpl)\r
+{\r
+       extract($params);\r
+\r
+       if (empty($directory))\r
+       {\r
+               throw new Template_Exception("resize_image: missing 'directory' parameter", $tpl);\r
+       }\r
+\r
+       if (empty($thumbdir))\r
+       {\r
+               $thumbdir = $directory;\r
+       }\r
+\r
+       if (empty($filename))\r
+       {\r
+               throw new Template_Exception("resize_image: missing 'filename' parameter", $tpl);\r
+       }\r
+\r
+       if (empty($xscale))\r
+       {\r
+               $xscale = 2000;\r
+       }\r
+       $maximagewidth=$xscale;\r
+\r
+       if (empty($xscale))\r
+       {\r
+               $yscale = 2000;\r
+       }\r
+       $maximageheight=$yscale;\r
+\r
+       if (empty($alt))\r
+       {\r
+               $alt = "image";\r
+       }\r
+\r
+       if (empty($border))\r
+       {\r
+               $border = 0;\r
+       }\r
+\r
+       if (empty($daystokeep))\r
+       {\r
+               $daystokeep = 5;\r
+       }\r
+\r
+       if (!function_exists('gd_info'))\r
+       {\r
+               throw new Template_Exception("resize_image: the GD library is not installed", $tpl);\r
+       }\r
+\r
+       if(!file_exists($directory . $filename) && !empty($url) && function_exists('curl_init'))\r
+       {\r
+               $ch = curl_init ($url . $filename);\r
+               $fp = fopen ($directory . $filename, "w");\r
+               curl_setopt ($ch, CURLOPT_FILE, $fp);\r
+               curl_setopt ($ch, CURLOPT_HEADER, 0);\r
+               curl_exec ($ch);\r
+               curl_close ($ch);\r
+               fclose ($fp);\r
+       }\r
+\r
+       if(file_exists($directory . $filename))\r
+       {\r
+               $imageinfo = @getimagesize($directory . $filename);\r
+               if(empty($imageinfo))\r
+               {\r
+                       return;\r
+               }\r
+\r
+               if ($returntype == 1)\r
+               {\r
+                       $imagewidth = $imageinfo[0];\r
+                       $imageheight = $imageinfo[1];\r
+\r
+                       if($maximagewidth < $imagewidth) {\r
+                               $imageheight = tpl_function_resize_unpercent(tpl_function_resize_percent($maximagewidth, $imagewidth), $imageheight);\r
+                               $imagewidth = $maximagewidth;\r
+                       }\r
+\r
+                       if($maximageheight < $imageheight) {\r
+                               $imagewidth = tpl_function_resize_unpercent(tpl_function_resize_percent($maximageheight, $imageheight), $imagewidth);\r
+                               $imageheight = $maximageheight;\r
+                       }\r
+                       return "<img " . $class . " src=\"" . $img_src . $filename . "\" width=\"" . $imagewidth . "\" height=\"" . $imageheight . "\" alt=\"" . $alt . "\" border=\"" . $border . "\">";\r
+               }\r
+\r
+               if (empty($thumbname))\r
+               {\r
+                       throw new Template_Exception("resize_image: missing 'thumbname' parameter", $tpl);\r
+               }\r
+\r
+               $now=urlencode(date("F j, Y, g:i a"));\r
+\r
+               $newimagepath = $thumbdir . $thumbname . $filename;\r
+\r
+               $newthumbnail = 0;\r
+               if(!file_exists($newimagepath))\r
+               {\r
+                       copy($directory . $filename, $newimagepath);\r
+                       $newthumbnail = 1;\r
+               }\r
+\r
+               $datechanged = date("j", time()) - date("j", filemtime($newimagepath));\r
+               if(($datechanged > -$daystokeep && $datechanged < $daystokeep) && $newthumbnail = 0)\r
+               {\r
+                       // Do not rebuild\r
+                       $imagewidth = $imageinfo[0];\r
+                       $imageheight = $imageinfo[1];\r
+\r
+                       if($maximagewidth < $imagewidth)\r
+                       {\r
+                               $imageheight = tpl_function_resize_unpercent(tpl_function_resize_percent($maximagewidth, $imagewidth), $imageheight);\r
+                               $imagewidth = $maximagewidth;\r
+                       }\r
+\r
+                       if($maximageheight < $imageheight)\r
+                       {\r
+                               $imagewidth = tpl_function_resize_unpercent(tpl_function_resize_percent($maximageheight, $imageheight), $imagewidth);\r
+                               $imageheight = $maximageheight;\r
+                       }\r
+               }\r
+               else\r
+               {\r
+                       // rebuild\r
+                       copy($directory . $filename, $newimagepath);\r
+\r
+                       $imagewidth = $imageinfo[0];\r
+                       $imageheight = $imageinfo[1];\r
+\r
+                       if($maximagewidth < $imagewidth)\r
+                       {\r
+                               $imageheight = tpl_function_resize_unpercent(tpl_function_resize_percent($maximagewidth, $imagewidth), $imageheight);\r
+                               $imagewidth = $maximagewidth;\r
+                       }\r
+\r
+                       if($maximageheight < $imageheight)\r
+                       {\r
+                               $imagewidth = tpl_function_resize_unpercent(tpl_function_resize_percent($maximageheight, $imageheight), $imagewidth);\r
+                               $imageheight = $maximageheight;\r
+                       }\r
+                       $imagewidth = round($imagewidth);\r
+                       $imageheight = round($imageheight);\r
+                       $scale = $imagewidth . "x" . $imageheight . "!";\r
+\r
+                       if (empty($binpath))\r
+                       {\r
+                               if($imageinfo[2] == 1)\r
+                               {\r
+                                       $sourceimage = imagecreatefromgif($directory . $filename);\r
+                               }\r
+                               elseif($imageinfo[2] == 2)\r
+                               {\r
+                                       $sourceimage = imagecreatefromjpeg($directory . $filename);\r
+                               }\r
+                               elseif($imageinfo[2] == 3)\r
+                               {\r
+                                       $sourceimage = imagecreatefrompng($directory . $filename);\r
+                               }\r
+\r
+                               $destinationimage = imagecreatetruecolor($imagewidth, $imageheight);\r
+                               imagecopyresized($destinationimage, $sourceimage, 0, 0, 0, 0, $imagewidth, $imageheight, $imageinfo[0], $imageinfo[1]);\r
+                               if($imageinfo[2] == 1)\r
+                               {\r
+                                       imagegif($destinationimage, $newimagepath);\r
+                               }\r
+                               elseif($imageinfo[2] == 2)\r
+                               {\r
+                                       imageJPEG($destinationimage, $newimagepath, 75);\r
+                               }\r
+                               elseif($imageinfo[2] == 3)\r
+                               {\r
+                                       imagepng($destinationimage, $newimagepath);\r
+                               }\r
+                               imagedestroy($sourceimage);\r
+                               imagedestroy($destinationimage);\r
+                       }\r
+                       else\r
+                       {\r
+                               if ($imageinfo[2] == 2)\r
+                               {\r
+                                       system( $binpath . "mogrify  -quality 75 -geometry $scale $newimagepath");\r
+                               }\r
+                               else\r
+                               {\r
+                                       system( $binpath . "mogrify  -geometry $scale $newimagepath");\r
+                               }\r
+                       }\r
+               }\r
+\r
+               return "<img " . $class . " src=\"" . $img_src . $thumbname . $filename . "?" . $now . "\" width=\"" . $imagewidth . "\" height=\"" . $imageheight . "\" alt=\"" . $alt . "\" border=\"" . $border . "\">";\r
+       }\r
+}\r
+?>
\ 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 (file)
index 0000000..b1c6f76
--- /dev/null
@@ -0,0 +1,44 @@
+<?php\r
+/**\r
+ * Template_Lite bbcode modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     bbcode2html\r
+ * Purpose:  converts special bbcode syntax into standard html\r
+ * Input:<br>\r
+ *         - string: data to convert\r
+ */\r
+function tpl_modifier_bbcode2html($data)\r
+{\r
+       $data = nl2br(stripslashes(addslashes($data)));\r
+\r
+       $search = array("\n", "\r", "[b]", "[/b]", "[i]", "[/i]", "[u]", "[/u]");\r
+       $replace = array("", "", "<b>", "</b>", "<i>", "</i>", "<u>", "</u>");\r
+       $data = str_replace($search, $replace, $data);\r
+\r
+       $search = array(\r
+               "/\[email\](.*?)\[\/email\]/si",\r
+               "/\[email=(.*?)\](.*?)\[\/email\]/si",\r
+               "/\[url\](.*?)\[\/url\]/si",\r
+               "/\[url=(.*?)\](.*?)\[\/url\]/si",\r
+               "/\[img\](.*?)\[\/img\]/si",\r
+               "/\[code\](.*?)\[\/code\]/si",\r
+               "/\[pre\](.*?)\[\/pre\]/si",\r
+               "/\[list\](.*?)\[\/list\]/si",\r
+               "/\[\*\](.*?)/si"\r
+       );\r
+       $replace = array(\r
+               "<a href=\"mailto:\\1\">\\1</a>",\r
+               "<a href=\"mailto:\\1\">\\2</a>",\r
+               "<a href=\"\\1\" target=\"_blank\">\\1</a>",\r
+               "<a href=\"\\1\" target=\"_blank\">\\2</a>",\r
+               "<img src=\"\\1\" border=\"0\">",\r
+               "<p><blockquote><font size=\"1\">code:</font><hr noshade size=\"1\"><pre>\\1</pre><br><hr noshade size=\"1\"></blockquote></p>",\r
+               "<pre>\\1<br></pre>",\r
+               "<ul>\\1</ul>",\r
+               "<li>\\1</li>"\r
+       );\r
+       $data = preg_replace($search, $replace, $data);\r
+       return $data;\r
+}\r
+?>
\ 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 (file)
index 0000000..8797d8c
--- /dev/null
@@ -0,0 +1,13 @@
+<?php\r
+/**\r
+ * template_lite capitalize modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     capitalize\r
+ * Purpose:  Wrapper for the PHP 'ucwords' function\r
+ */\r
+function tpl_modifier_capitalize($string)\r
+{\r
+       return ucwords($string);\r
+}\r
+?>
\ 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 (file)
index 0000000..ee655c5
--- /dev/null
@@ -0,0 +1,31 @@
+<?php\r
+/**\r
+ * Template Lite plugin converted from Smarty\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+\r
+/**\r
+ * Smarty cat modifier plugin\r
+ *\r
+ * Type:     modifier<br>\r
+ * Name:     cat<br>\r
+ * Date:     Feb 24, 2003\r
+ * Purpose:  catenate a value to a variable\r
+ * Input:    string to catenate\r
+ * Example:  {$var|cat:"foo"}\r
+ * @link http://smarty.php.net/manual/en/language.modifier.cat.php cat\r
+ *          (Smarty online manual)\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @version 1.0\r
+ * @param string\r
+ * @param string\r
+ * @return string\r
+ */\r
+function tpl_modifier_cat($string, $cat)\r
+{\r
+    return $string . $cat;\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..56e98d3
--- /dev/null
@@ -0,0 +1,32 @@
+<?php\r
+/**\r
+ * Template Lite plugin converted from Smarty\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+\r
+/**\r
+ * Smarty count_characters modifier plugin\r
+ *\r
+ * Type:     modifier<br>\r
+ * Name:     count_characteres<br>\r
+ * Purpose:  count the number of characters in a text\r
+ * @link http://smarty.php.net/manual/en/language.modifier.count.characters.php\r
+ *          count_characters (Smarty online manual)\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @param string\r
+ * @param boolean include whitespace in the character count\r
+ * @return integer\r
+ */\r
+function tpl_modifier_count_characters($string, $include_spaces = false)\r
+{\r
+    if ($include_spaces)\r
+       {\r
+       return(strlen($string));\r
+       }\r
+\r
+    return preg_match_all("/[^\s]/",$string, $match);\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..bf7cef9
--- /dev/null
@@ -0,0 +1,27 @@
+<?php\r
+/**\r
+ * Smarty plugin\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+\r
+/**\r
+ * Smarty count_paragraphs modifier plugin\r
+ *\r
+ * Type:     modifier<br>\r
+ * Name:     count_paragraphs<br>\r
+ * Purpose:  count the number of paragraphs in a text\r
+ * @link http://smarty.php.net/manual/en/language.modifier.count.paragraphs.php\r
+ *          count_paragraphs (Smarty online manual)\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @param string\r
+ * @return integer\r
+ */\r
+function tpl_modifier_count_paragraphs($string)\r
+{\r
+    // count \r or \n characters\r
+    return count(preg_split('/[\r\n]+/', $string));\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..80fb6a0
--- /dev/null
@@ -0,0 +1,27 @@
+<?php\r
+/**\r
+ * Template Lite plugin converted from Smarty\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+\r
+/**\r
+ * Smarty count_sentences modifier plugin\r
+ *\r
+ * Type:     modifier<br>\r
+ * Name:     count_sentences\r
+ * Purpose:  count the number of sentences in a text\r
+ * @link http://smarty.php.net/manual/en/language.modifier.count.paragraphs.php\r
+ *          count_sentences (Smarty online manual)\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @param string\r
+ * @return integer\r
+ */\r
+function tpl_modifier_count_sentences($string)\r
+{\r
+    // find periods with a word before but not after.\r
+    return preg_match_all('/[^\s]\.(?!\w)/', $string, $match);\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..0e49474
--- /dev/null
@@ -0,0 +1,31 @@
+<?php\r
+/**\r
+ * Template Lite plugin converted from Smarty\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+\r
+/**\r
+ * Smarty count_words modifier plugin\r
+ *\r
+ * Type:     modifier<br>\r
+ * Name:     count_words<br>\r
+ * Purpose:  count the number of words in a text\r
+ * @link http://smarty.php.net/manual/en/language.modifier.count.words.php\r
+ *          count_words (Smarty online manual)\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @param string\r
+ * @return integer\r
+ */\r
+function tpl_modifier_count_words($string)\r
+{\r
+    // split text by ' ',\r,\n,\f,\t\r
+    $split_array = preg_split('/\s+/',$string);\r
+    // count matches that contain alphanumerics\r
+    $word_count = preg_grep('/[a-zA-Z0-9\\x80-\\xff]/', $split_array);\r
+\r
+    return count($word_count);\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/plugins/modifier.date.php b/include/libs/template_lite/plugins/modifier.date.php
new file mode 100644 (file)
index 0000000..9d2d628
--- /dev/null
@@ -0,0 +1,63 @@
+<?php\r
+/**\r
+ * template_lite date modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     date\r
+ * Purpose:  formats a date given a UNIX timestamp, based on the\r
+ *           PHP "date" function\r
+ * Input:\r
+ *         - string: input date string\r
+ *         - format: date format for output\r
+ *         - default_date: default date if $string is empty\r
+ */\r
+function tpl_modifier_date($string, $format="r", $default_date=null)\r
+{\r
+       if($string != '')\r
+       {\r
+               return date($format, tpl_make_timestamp($string));\r
+       }\r
+       elseif (isset($default_date) && $default_date != '')\r
+       {               \r
+               return date($format, tpl_make_timestamp($default_date));\r
+       }\r
+       else\r
+       {\r
+               return;\r
+       }\r
+}\r
+\r
+if(!function_exists('tpl_make_timestamp'))\r
+{\r
+       function tpl_make_timestamp($string)\r
+       {\r
+               if(empty($string))\r
+               {\r
+                       $string = "now";\r
+               }\r
+               $time = strtotime($string);\r
+               if (is_numeric($time) && $time != -1)\r
+               {\r
+                       return $time;\r
+               }\r
+\r
+               // is mysql timestamp format of YYYYMMDDHHMMSS?\r
+               if (is_numeric($string) && strlen($string) == 14)\r
+               {\r
+                       $time = mktime(substr($string,8,2),substr($string,10,2),substr($string,12,2),substr($string,4,2),substr($string,6,2),substr($string,0,4));\r
+                       return $time;\r
+               }\r
+\r
+               // couldn't recognize it, try to return a time\r
+               $time = (int) $string;\r
+               if ($time > 0)\r
+               {\r
+                       return $time;\r
+               }\r
+               else\r
+               {\r
+                       return time();\r
+               }\r
+       }\r
+}\r
+?>
\ 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 (file)
index 0000000..762562e
--- /dev/null
@@ -0,0 +1,64 @@
+<?php\r
+\r
+/*\r
+ * Template Lite plugin converted from Smarty\r
+ * -------------------------------------------------------------\r
+ * Type:     modifier\r
+ * Name:     date_format\r
+ * Purpose:  format datestamps via strftime\r
+ * Input:    string: input date string\r
+ *           format: strftime format for output\r
+ *           default_date: default date if $string is empty\r
+ * -------------------------------------------------------------\r
+ */\r
+\r
+function tpl_modifier_date_format($string, $format="%b %e, %Y", $default_date=null)\r
+{\r
+       if($string != '')\r
+       {\r
+       return strftime($format, tpl_make_timestamp($string));\r
+       }\r
+       elseif (isset($default_date) && $default_date != '')\r
+       {\r
+       return strftime($format, tpl_make_timestamp($default_date));\r
+       }\r
+       else\r
+       {\r
+               return;\r
+       }\r
+}\r
+\r
+if(!function_exists('tpl_make_timestamp'))\r
+{\r
+       function tpl_make_timestamp($string)\r
+       {\r
+               if(empty($string))\r
+               {\r
+                       $string = "now";\r
+               }\r
+               $time = strtotime($string);\r
+               if (is_numeric($time) && $time != -1)\r
+               {\r
+                       return $time;\r
+               }\r
+\r
+               // is mysql timestamp format of YYYYMMDDHHMMSS?\r
+               if (is_numeric($string) && strlen($string) == 14)\r
+               {\r
+                       $time = mktime(substr($string,8,2),substr($string,10,2),substr($string,12,2),substr($string,4,2),substr($string,6,2),substr($string,0,4));\r
+                       return $time;\r
+               }\r
+\r
+               // couldn't recognize it, try to return a time\r
+               $time = (int) $string;\r
+               if ($time > 0)\r
+               {\r
+                       return $time;\r
+               }\r
+               else\r
+               {\r
+                       return time();\r
+               }\r
+       }\r
+}\r
+?>\r
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 (file)
index 0000000..6ad7a52
--- /dev/null
@@ -0,0 +1,54 @@
+<?php\r
+\r
+/*\r
+ * Smarty plugin\r
+ * -------------------------------------------------------------\r
+ * Type:     modifier\r
+ * Name:     debug_print_var\r
+ * Purpose:  formats variable contents for display in the console\r
+ * -------------------------------------------------------------\r
+ */\r
+function tpl_modifier_debug_print_var($var, $depth = 0, $length = 40)\r
+{\r
+    if (is_array($var))\r
+       {\r
+        $results = "<b>Array (".count($var).")</b>";\r
+        foreach ($var as $curr_key => $curr_val)\r
+               {\r
+            $return = tpl_modifier_debug_print_var($curr_val, $depth+1, $length);\r
+            $results .= '<br>\r'.str_repeat('&nbsp;', $depth*2)."<b>$curr_key</b> =&gt; $return";\r
+        }\r
+        return $results;\r
+    }\r
+       else if (is_object($var))\r
+       {\r
+        $object_vars = get_object_vars($var);\r
+        $results = "<b>".get_class($var)." Object (".count($object_vars).")</b>";\r
+        foreach ($object_vars as $curr_key => $curr_val)\r
+               {\r
+            $return = tpl_modifier_debug_print_var($curr_val, $depth+1, $length);\r
+            $results .= '<br>\r'.str_repeat('&nbsp;', $depth*2)."<b>$curr_key</b> =&gt; $return";\r
+        }\r
+        return $results;\r
+    }\r
+       else\r
+       {\r
+        if (empty($var) && $var != "0")\r
+               {\r
+            return '<i>empty</i>';\r
+        }\r
+        if (strlen($var) > $length )\r
+               {\r
+            $results = substr($var, 0, $length-3).'...';\r
+        }\r
+               else\r
+               {\r
+            $results = $var;\r
+        }\r
+        $results = preg_replace("![\r\t\n]!", " ", $results);\r
+        $results = htmlspecialchars($results);\r
+        return $results;\r
+    }\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/plugins/modifier.default.php b/include/libs/template_lite/plugins/modifier.default.php
new file mode 100644 (file)
index 0000000..0dcaf6d
--- /dev/null
@@ -0,0 +1,22 @@
+<?php\r
+/**\r
+ * template_lite default modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     default\r
+ * Purpose:  designate default value for empty variables\r
+ * Credit:   Taken from the original Smarty\r
+ *           http://smarty.php.net\r
+ */\r
+function tpl_modifier_default($string, $default = '')\r
+{\r
+       if (!isset($string) || $string === '')\r
+       {\r
+               return $default;\r
+       }\r
+       else\r
+       {\r
+               return $string;\r
+       }\r
+}\r
+?>
\ 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 (file)
index 0000000..d04c408
--- /dev/null
@@ -0,0 +1,102 @@
+<?php\r
+/**\r
+ * Template Lite plugin converted from Smarty\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+\r
+\r
+/**\r
+ * Smarty escape modifier plugin\r
+ *\r
+ * Type:     modifier<br>\r
+ * Name:     escape<br>\r
+ * Purpose:  Escape the string according to escapement type\r
+ * @link http://smarty.php.net/manual/en/language.modifier.escape.php\r
+ *          escape (Smarty online manual)\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @param string\r
+ * @param html|htmlall|url|quotes|hex|hexentity|javascript\r
+ * @return string\r
+ */\r
+function tpl_modifier_escape($string, $esc_type = 'html', $char_set = 'ISO-8859-1', $double_encode = true)\r
+{\r
+    switch ($esc_type)\r
+       {\r
+        case 'html':\r
+            if (version_compare(PHP_VERSION, '5.2.3') === 1)\r
+                return htmlspecialchars($string, ENT_QUOTES, $char_set, $double_encode);\r
+            else\r
+                return htmlspecialchars($string, ENT_QUOTES, $char_set);\r
+\r
+        case 'htmlall':\r
+            if (version_compare(PHP_VERSION, '5.2.3') === 1)\r
+                return htmlentities($string, ENT_QUOTES, $char_set, $double_encode);\r
+            else\r
+                return htmlentities($string, ENT_QUOTES, $char_set);\r
+\r
+        case 'url':\r
+            return rawurlencode($string);\r
+\r
+        case 'urlpathinfo':\r
+            return str_replace('%2F','/',rawurlencode($string));\r
+\r
+        case 'quotes':\r
+            // escape unescaped single quotes\r
+            return preg_replace("%(?<!\\\\)'%", "\\'", $string);\r
+\r
+        case 'hex':\r
+            // escape every character into hex\r
+            $return = '';\r
+            for ($x=0; $x < strlen($string); $x++) {\r
+                $return .= '%' . bin2hex($string[$x]);\r
+            }\r
+            return $return;\r
+\r
+        case 'hexentity':\r
+            $return = '';\r
+            for ($x=0; $x < strlen($string); $x++) {\r
+                $return .= '&#x' . bin2hex($string[$x]) . ';';\r
+            }\r
+            return $return;\r
+\r
+        case 'decentity':\r
+            $return = '';\r
+            for ($x=0; $x < strlen($string); $x++) {\r
+                $return .= '&#' . ord($string[$x]) . ';';\r
+            }\r
+            return $return;\r
+\r
+        case 'javascript':\r
+            // escape quotes and backslashes, newlines, etc.\r
+            return strtr($string, array('\\'=>'\\\\',"'"=>"\\'",'"'=>'\\"',"\r"=>'\\r',"\n"=>'\\n','</'=>'<\/'));\r
+\r
+        case 'mail':\r
+            // safe way to display e-mail address on a web page\r
+            return str_replace(array('@', '.'),array(' [AT] ', ' [DOT] '), $string);\r
+\r
+        case 'nonstd':\r
+           // escape non-standard chars, such as ms document quotes\r
+           $_res = '';\r
+           for($_i = 0, $_len = strlen($string); $_i < $_len; $_i++)\r
+                  {\r
+               $_ord = ord(substr($string, $_i, 1));\r
+               // non-standard char, escape it\r
+               if($_ord >= 126)\r
+                          {\r
+                   $_res .= '&#' . $_ord . ';';\r
+               }\r
+               else\r
+                          {\r
+                   $_res .= substr($string, $_i, 1);\r
+               }\r
+           }\r
+           return $_res;\r
+\r
+        default:\r
+            return $string;\r
+    }\r
+}\r
+\r
+?>
\ 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 (file)
index 0000000..46661c4
--- /dev/null
@@ -0,0 +1,28 @@
+<?php\r
+/**\r
+ * Template Lite plugin converted from Smarty\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+\r
+/**\r
+ * Smarty indent modifier plugin\r
+ *\r
+ * Type:     modifier<br>\r
+ * Name:     indent<br>\r
+ * Purpose:  indent lines of text\r
+ * @link http://smarty.php.net/manual/en/language.modifier.indent.php\r
+ *          indent (Smarty online manual)\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @param string\r
+ * @param integer\r
+ * @param string\r
+ * @return string\r
+ */\r
+function tpl_modifier_indent($string,$chars=4,$char=" ")\r
+{\r
+    return preg_replace('!^!m',str_repeat($char,$chars),$string);\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/plugins/modifier.lower.php b/include/libs/template_lite/plugins/modifier.lower.php
new file mode 100644 (file)
index 0000000..b59e74f
--- /dev/null
@@ -0,0 +1,13 @@
+<?php\r
+/**\r
+ * template_lite lower modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     lower\r
+ * Purpose:  Wrapper for the PHP 'strtolower' function\r
+ */\r
+function tpl_modifier_lower($string)\r
+{\r
+       return strtolower($string);\r
+}\r
+?>
\ 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 (file)
index 0000000..d988429
--- /dev/null
@@ -0,0 +1,33 @@
+<?php\r
+/**\r
+ * Template Lite plugin converted from Smarty\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+\r
+/**\r
+ * Smarty regex_replace modifier plugin\r
+ *\r
+ * Type:     modifier<br>\r
+ * Name:     regex_replace<br>\r
+ * Purpose:  regular expression search/replace\r
+ * @link http://smarty.php.net/manual/en/language.modifier.regex.replace.php\r
+ *          regex_replace (Smarty online manual)\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @param string\r
+ * @param string|array\r
+ * @param string|array\r
+ * @return string\r
+ */\r
+function tpl_modifier_regex_replace($string, $search, $replace)\r
+{\r
+    if (preg_match('!([a-zA-Z\s]+)$!s', $search, $match) && (strpos($match[1], 'e') !== false))\r
+       {\r
+        /* remove eval-modifier from $search */\r
+        $search = substr($search, 0, -strlen($match[1])) . preg_replace('![e\s]+!', '', $match[1]);\r
+    }\r
+    return preg_replace($search, $replace, $string);\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/plugins/modifier.replace.php b/include/libs/template_lite/plugins/modifier.replace.php
new file mode 100644 (file)
index 0000000..059fa70
--- /dev/null
@@ -0,0 +1,15 @@
+<?php\r
+/**\r
+ * template_lite replace modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     replace\r
+ * Purpose:  Wrapper for the PHP 'str_replace' function\r
+ * Credit:   Taken from the original Smarty\r
+ *           http://smarty.php.net\r
+ */\r
+function tpl_modifier_replace($string, $search, $replace)\r
+{\r
+       return str_replace($search, $replace, $string);\r
+}\r
+?>
\ 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 (file)
index 0000000..3b72a17
--- /dev/null
@@ -0,0 +1,27 @@
+<?php\r
+/**\r
+ * Template Lite plugin converted from Smarty\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+\r
+/**\r
+ * Smarty spacify modifier plugin\r
+ *\r
+ * Type:     modifier<br>\r
+ * Name:     spacify<br>\r
+ * Purpose:  add spaces between characters in a string\r
+ * @link http://smarty.php.net/manual/en/language.modifier.spacify.php\r
+ *          spacify (Smarty online manual)\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @param string\r
+ * @param string\r
+ * @return string\r
+ */\r
+function tpl_modifier_spacify($string, $spacify_char = ' ')\r
+{\r
+    return implode($spacify_char, preg_split('//', $string, -1, PREG_SPLIT_NO_EMPTY));\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..3d777b1
--- /dev/null
@@ -0,0 +1,15 @@
+<?php\r
+/**\r
+ * template_lite string_format modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     string_format\r
+ * Purpose:  Wrapper for the PHP 'vsprintf' function\r
+ */\r
+function tpl_modifier_string_format()\r
+{\r
+       $_args = func_get_args();\r
+       $string = array_shift($_args);\r
+       return vsprintf($string, $_args);\r
+}\r
+?>
\ 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 (file)
index 0000000..8dcc118
--- /dev/null
@@ -0,0 +1,16 @@
+<?php\r
+/**\r
+ * template_lite strip modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     strip\r
+ * Purpose:  Removes all repeated spaces, newlines, tabs\r
+ *           with a single space or supplied character\r
+ * Credit:   Taken from the original Smarty\r
+ *           http://smarty.php.net\r
+ */\r
+function tpl_modifier_strip($string, $replace = ' ')\r
+{\r
+       return preg_replace('!\s+!', $replace, $string);\r
+}\r
+?>
\ 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 (file)
index 0000000..24b8025
--- /dev/null
@@ -0,0 +1,34 @@
+<?php\r
+/**\r
+ * template_lite truncate modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     truncate\r
+ * Purpose:  Truncate a string to a certain length if necessary,\r
+ *           optionally splitting in the middle of a word, and \r
+ *           appending the $etc string.\r
+ * Credit:   Taken from the original Smarty\r
+ *           http://smarty.php.net\r
+ */\r
+function tpl_modifier_truncate($string, $length = 80, $etc = '...', $break_words = false)\r
+{\r
+       if ($length == 0)\r
+       {\r
+               return '';\r
+       }\r
+\r
+       if (strlen($string) > $length)\r
+       {\r
+               $length -= strlen($etc);\r
+               if (!$break_words)\r
+               {\r
+                       $string = preg_replace('/\s+?(\S+)?$/', '', substr($string, 0, $length+1));\r
+               }\r
+               return substr($string, 0, $length).$etc;\r
+       }\r
+       else\r
+       {\r
+               return $string;\r
+       }\r
+}\r
+?>
\ 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 (file)
index 0000000..d077830
--- /dev/null
@@ -0,0 +1,13 @@
+<?php\r
+/**\r
+ * template_lite upper modifier plugin\r
+ *\r
+ * Type:     modifier\r
+ * Name:     upper\r
+ * Purpose:  Wrapper for the PHP 'strtoupper' function\r
+ */\r
+function tpl_modifier_upper($string)\r
+{\r
+       return strtoupper($string);\r
+}\r
+?>
\ 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 (file)
index 0000000..7ef473b
--- /dev/null
@@ -0,0 +1,61 @@
+<?php\r
+/*\r
+ * Author: Mark Dickenson, akapanamajack@wildmail.com\r
+ * You can stack multiple template display commands to have the entire page output as a compressed file.\r
+ *\r
+ * This output filter was specifically written to work with Alien Assault Traders but can be used on other projects.\r
+ *\r
+ * $send_now = 0 will cache the output and not send the data until $send_now = 1\r
+ * $_tpl_saved is a reserved variable for storing the cached output\r
+ * $force_compression = 1 will cause all output to be compressed and ignore what the browser or server indicates to gzip support\r
+ * $compression_level is the amount of compression to use on the output 0 is the leasat and 9 is maximum\r
+ * $template_object->enable_gzip = 0 output is not compressed $template_object->enable_gzip = 1 output is compressed\r
+ */\r
+\r
+function template_outputfilter_gzip($tpl_source, &$template_object)\r
+{\r
+       static $_tpl_saved = '';\r
+\r
+       $gzipped = 0;\r
+       if($template_object->enable_gzip)\r
+       {\r
+               if(extension_loaded("zlib") && !get_cfg_var('zlib.output_compression') && !$template_object->cache && (strstr($_SERVER["HTTP_ACCEPT_ENCODING"],"gzip") || $template_object->force_compression))\r
+               {\r
+                       $_tpl_saved .= $tpl_source . "\n<!-- zlib compression level " . $template_object->compression_level . " -->\n\n";\r
+                       $tpl_source = "";\r
+\r
+                       if($template_object->send_now == 1)\r
+                       {\r
+                               $gzipped = 1;\r
+                               $tpl_source = gzencode($_tpl_saved, $template_object->compression_level);\r
+                               $_tpl_saved = "";\r
+                       }\r
+               }\r
+       }\r
+       else\r
+       {\r
+               if(!$template_object->caching && !get_cfg_var('zlib.output_compression'))\r
+               {\r
+                       $_tpl_saved .= $tpl_source."\n<!-- normal saved output -->\n\n";\r
+                       $tpl_source = "";\r
+\r
+                       if($template_object->send_now == 1)\r
+                       {\r
+                               $tpl_source = $_tpl_saved;\r
+                               $_tpl_saved = "";\r
+                       }\r
+               }\r
+       }\r
+\r
+       if($template_object->send_now == 1 && $template_object->enable_gzip == 1)\r
+       {\r
+               if($gzipped == 1)\r
+               {\r
+                       header("Content-Encoding: gzip");\r
+                       header("Content-Length: " . strlen($tpl_source));\r
+               }\r
+       }\r
+\r
+       return $tpl_source;\r
+}\r
+?>
\ 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 (file)
index 0000000..38a9bff
--- /dev/null
@@ -0,0 +1,81 @@
+<?php\r
+/**\r
+ * Template Lite plugin converted from Smarty\r
+ * @package Smarty\r
+ * @subpackage plugins\r
+ */\r
+\r
+/**\r
+ * Smarty trimwhitespace outputfilter plugin\r
+ *\r
+ * File:     outputfilter.trimwhitespace.php<br>\r
+ * Type:     outputfilter<br>\r
+ * Name:     trimwhitespace<br>\r
+ * Date:     Jan 25, 2003<br>\r
+ * Purpose:  trim leading white space and blank lines from\r
+ *           template source after it gets interpreted, cleaning\r
+ *           up code and saving bandwidth. Does not affect\r
+ *           <<PRE>></PRE> and <SCRIPT></SCRIPT> blocks.<br>\r
+ * Install:  Drop into the plugin directory, call\r
+ *           <code>$template_object->load_filter('output','trimwhitespace');</code>\r
+ *           from application.\r
+ * @author   Monte Ohrt <monte at ohrt dot com>\r
+ * @author Contributions from Lars Noschinski <lars@usenet.noschinski.de>\r
+ * @version  1.3\r
+ * @param string\r
+ * @param Smarty\r
+ */\r
\r
+function template_outputfilter_trimwhitespace($tpl_source, &$template_object)\r
+{\r
+    // Pull out the script blocks\r
+    preg_match_all("!<script[^>]+>.*?</script>!is", $tpl_source, $match);\r
+    $_script_blocks = $match[0];\r
+    $tpl_source = preg_replace("!<script[^>]+>.*?</script>!is",\r
+                           '@@@TEMPLATELITE:TRIM:SCRIPT@@@', $tpl_source);\r
+\r
+    // Pull out the pre blocks\r
+    preg_match_all("!<pre>.*?</pre>!is", $tpl_source, $match);\r
+    $_pre_blocks = $match[0];\r
+    $tpl_source = preg_replace("!<pre>.*?</pre>!is",\r
+                           '@@@TEMPLATELITE:TRIM:PRE@@@', $tpl_source);\r
+\r
+    // Pull out the textarea blocks\r
+    preg_match_all("!<textarea[^>]+>.*?</textarea>!is", $tpl_source, $match);\r
+    $_textarea_blocks = $match[0];\r
+    $tpl_source = preg_replace("!<textarea[^>]+>.*?</textarea>!is",\r
+                           '@@@TEMPLATELITE:TRIM:TEXTAREA@@@', $tpl_source);\r
+\r
+    // remove all leading spaces, tabs and carriage returns NOT\r
+    // preceeded by a php close tag.\r
+    $tpl_source = trim(preg_replace('/((?<!\?>)\n)[\s]+/m', '\1', $tpl_source));\r
+\r
+    // replace script blocks\r
+    template_outputfilter_trimwhitespace_replace("@@@TEMPLATELITE:TRIM:SCRIPT@@@",$_script_blocks, $tpl_source);\r
+\r
+    // replace pre blocks\r
+    template_outputfilter_trimwhitespace_replace("@@@TEMPLATELITE:TRIM:PRE@@@",$_pre_blocks, $tpl_source);\r
+\r
+    // replace textarea blocks\r
+    template_outputfilter_trimwhitespace_replace("@@@TEMPLATELITE:TRIM:TEXTAREA@@@",$_textarea_blocks, $tpl_source);\r
+\r
+    return $tpl_source;\r
+}\r
+\r
+function template_outputfilter_trimwhitespace_replace($search_str, $replace, &$subject) {\r
+    $_len = strlen($search_str);\r
+    $_pos = 0;\r
+    for ($_i=0, $_count=count($replace); $_i<$_count; $_i++)\r
+       {\r
+        if (($_pos=strpos($subject, $search_str, $_pos))!==false)\r
+               {\r
+            $subject = substr_replace($subject, $replace[$_i], $_pos, $_len);\r
+               }\r
+        else\r
+               {\r
+            break;\r
+               }\r
+       }\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/plugins/postfilter.showtemplatevars.php b/include/libs/template_lite/plugins/postfilter.showtemplatevars.php
new file mode 100644 (file)
index 0000000..108a067
--- /dev/null
@@ -0,0 +1,16 @@
+<?php\r
+/*\r
+ * Template Lite plugin converted from Smarty\r
+ * -------------------------------------------------------------\r
+ * File:     postfilter.showtemplatevars.php\r
+ * Type:     postfilter\r
+ * Name:     showtemplatevars\r
+ * Purpose:  Output code that lists all current template vars.\r
+ * -------------------------------------------------------------\r
+ */\r
+ function template_postfilter_showtemplatevars($compiled, &$template_object)\r
+ {\r
+     $compiled = "<pre>\n<?php print_r(\$this->_vars); ?>\n</pre>" . $compiled;\r
+     return $compiled;\r
+ }\r
+?>
\ 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 (file)
index 0000000..9b24cb2
--- /dev/null
@@ -0,0 +1,130 @@
+<?php\r
+/*\r
+ * Template Lite plugin converted from Smarty\r
+ * -------------------------------------------------------------\r
+ * File:     prefilter.jstrip.php\r
+ * Type:     prefilter\r
+ * Name:     jstrip\r
+ * Version:  1.0\r
+ * Date:     01 Nov 2004\r
+ * Purpose:  dummy compiler to compress javascript\r
+ * Install:  Drop into the plugin directory,\r
+ *           call load_filter('pre','jstrip');\r
+ *           from your application.\r
+ * -------------------------------------------------------------\r
+ */\r
+\r
+function template_prefilter_jstrip($tpl_source, &$template_object)\r
+{\r
+       return preg_replace_callback("/\{jstrip\}(.*?)\{\/jstrip\}/s","template_prefilter_jstrip_cb", $tpl_source);\r
+}\r
+\r
+function template_prefilter_jstrip_one($code)\r
+{\r
+       return template_prefilter_jstrip_cb(array("", $code), false);\r
+} \r
+\r
+function template_prefilter_jstrip_cb($m, $literal=true)\r
+{\r
+       $c=$m[1];\r
+       $o=""; //stripped output\r
+       $comment=0; //comments\r
+       $string=""; //current string delimiter\r
+       $last=""; //last char in the output\r
+       for ($i=0;$i<strlen($c);$i++)\r
+       {\r
+               //if ($i%100==0) {\r
+               //print_v(array($i,$string,$comment));\r
+               //}\r
+               $s=true; //save the character ?\r
+               //if we're in a string or phpcode\r
+               if (!empty($string))\r
+               {\r
+                       //end of the string\r
+                       if ($c[$i]==$string OR substr($c,$i,2)==$string)\r
+                       {\r
+                               $string="";\r
+                       }\r
+                       //not in a string\r
+               }\r
+               else\r
+               {\r
+                       //strip comments\r
+                       if (substr($c,$i,2)=="//")\r
+                       {\r
+                               $comment=1;\r
+                       }\r
+\r
+                       if (substr($c,$i,2)=="/*")\r
+                       {\r
+                               $comment=2;\r
+                       }\r
+\r
+                       if ($comment==1 AND $c[$i]=="\n")\r
+                       {\r
+                               $comment=0;\r
+                       }\r
+\r
+                       if ($comment==2 AND substr($c,$i-1,2)=="*/")\r
+                       {\r
+                               $comment=0;\r
+                               $s=false;\r
+                       }\r
+\r
+                       if ($comment==0)\r
+                       {\r
+                               //start a string \r
+                               if ($c[$i]=="'" OR $c[$i]=='"')\r
+                               {\r
+                                       $string=$c[$i];\r
+                               }\r
+\r
+                               //start phpcode\r
+                               if (substr($c,$i,2)=="<"."?")\r
+                               {\r
+                                       $string="?".">";\r
+                               }\r
+\r
+                               //line break\r
+                               if ($c[$i]=="\n" OR $c[$i]=="\r")\r
+                               {\r
+                                       //is the current line finished ?\r
+                                       // ")" and "}" is not OK ! (var x=function a() {}.......var )\r
+                                       $finishers=array(";","{","(",",","\n",":");\r
+                                       if (in_array($last,$finishers))\r
+                                       {\r
+                                               $s=false;\r
+                                       }\r
+                               }\r
+\r
+                               //a space ! can we cut it ?\r
+                               if ($c[$i]==" " OR $c[$i]=="\t")\r
+                               {\r
+                                       $cutme=array(" ","\t","}","{",")","(","[","]","<",">","=",";","+","-","/","*","\n",":","&");\r
+                                       if (in_array($c[$i-1],$cutme) OR in_array($c[$i+1],$cutme))\r
+                                       {\r
+                                               $s=false;\r
+                                       }\r
+                               }\r
+                               //todo : rename vars/functions !!\r
+                       }\r
+               }\r
+               //save the character\r
+               if ($s AND $comment==0)\r
+               {\r
+                       $o.=$c[$i];\r
+                       $last=$c[$i];\r
+               }\r
+       }\r
+\r
+       if ($literal)\r
+       {\r
+               return "{literal}".$o."{/literal}";\r
+       }\r
+       else\r
+       {\r
+               return $o;\r
+       }\r
+}\r
+\r
+?>?>
\ 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 (file)
index 0000000..11f96a3
--- /dev/null
@@ -0,0 +1,23 @@
+<?php\r
+/*\r
+ * Template Lite plugin converted from Smarty\r
+ * -------------------------------------------------------------\r
+ * File:     prefilter.showinfoheader.php\r
+ * Type:     prefilter\r
+ * Name:     showinfoheader\r
+ * Version:  1.0\r
+ * Date:     March 14th, 2002\r
+ * Purpose:  Add a header stating smarty version\r
+ *           and current date.\r
+ * Install:  Drop into the plugin directory,\r
+ *           call load_filter('pre','showinfoheader');\r
+ *           from your application.\r
+ * Author:   Monte Ohrt <monte@ohrt.com>\r
+ * -------------------------------------------------------------\r
+ */\r
\r
+ function template_prefilter_showinfoheader($tpl_source, &$template_object)\r
+ {\r
+       return '<!-- Template Lite '.$template_object->_version.' '.strftime("%Y-%m-%d %H:%M:%S %Z").' -->'."\n\n".$tpl_source; \r
+ }\r
+?>
\ 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 (file)
index 0000000..777884e
--- /dev/null
@@ -0,0 +1,18 @@
+<?php\r
+/**\r
+ * template_lite tpl_escape_chars function\r
+ *\r
+ */\r
+\r
+function tpl_escape_chars($string)\r
+{\r
+       if(!is_array($string))\r
+       {\r
+               $string = preg_replace('!&(#?\w+);!', '%%%TEMPLATE_START%%%\\1%%%TEMPLATE_END%%%', $string);\r
+               $string = htmlspecialchars($string);\r
+               $string = str_replace(array('%%%TEMPLATE_START%%%','%%%TEMPLATE_END%%%'), array('&',';'), $string);\r
+       }\r
+       return $string;\r
+}\r
+\r
+?>\r
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 (file)
index 0000000..7bc7f1e
--- /dev/null
@@ -0,0 +1,40 @@
+<?php\r
+/**\r
+ * template_lite tpl_create_timestamp function\r
+ *\r
+ * Taken from the original Smarty\r
+ * http://smarty.php.net\r
+ *\r
+ */\r
+function tpl_make_timestamp($string)\r
+{\r
+       if(empty($string))\r
+       {\r
+               $string = "now";\r
+       }\r
+       $time = strtotime($string);\r
+       if (is_numeric($time) && $time != -1)\r
+       {\r
+               return $time;\r
+       }\r
+\r
+       // is mysql timestamp format of YYYYMMDDHHMMSS?\r
+       if (is_numeric($string) && strlen($string) == 14)\r
+       {\r
+               $time = mktime(substr($string,8,2),substr($string,10,2),substr($string,12,2),substr($string,4,2),substr($string,6,2),substr($string,0,4));\r
+               return $time;\r
+       }\r
+\r
+       // couldn't recognize it, try to return a time\r
+       $time = (int) $string;\r
+       if ($time > 0)\r
+       {\r
+               return $time;\r
+       }\r
+       else\r
+       {\r
+               return time();\r
+       }\r
+}\r
+\r
+?>\r
diff --git a/include/libs/template_lite/tests/parser.php b/include/libs/template_lite/tests/parser.php
new file mode 100644 (file)
index 0000000..7023313
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+require dirname(__FILE__) . '/../class.parser.php';
+
+class Template_Tester extends Template_Parser
+{
+    public $debug = array();
+
+    public function processString($content)
+    {
+        $this->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('<?php '.$args.'?>') 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 (file)
index 0000000..0f6f469
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+error_reporting(E_ALL);
+
+require dirname(__FILE__) . '/../class.tokenparser.php';
+
+$parser = new Template_Parser;
+
+$t = '$bla->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 '<pre>';
+
+print_r($parser->parseArguments($args));
+$parser->parseTokens($args);
+
+/*
+$content = '
+
+{literal}
+
+Miam
+
+function ()
+{
+}
+
+{/literal}
+
+<?xml version="1.0" encoding="UTF-8"?>
+
+';
+
+$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 (file)
index 0000000..3bbf58a
--- /dev/null
+++ b/index.php
@@ -0,0 +1,13 @@
+<?php
+
+// Ce fichier n'est pas censé être appelé sauf si l'installation de Garradin
+// n'est pas effectuée correctement avec le vhost pointant sur le répertoire www/
+// auquel cas on limite les dégâts
+
+// Juste une vérification avant de continuer
+if (!version_compare(phpversion(), '5.4.0', '>='))
+{
+       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 (file)
index 0000000..9a31a28
--- /dev/null
@@ -0,0 +1 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>
\ No newline at end of file
diff --git a/templates/admin/_foot.tpl b/templates/admin/_foot.tpl
new file mode 100644 (file)
index 0000000..6c65910
--- /dev/null
@@ -0,0 +1,19 @@
+</div>
+
+<script type="text/javascript" defer="defer">
+{literal}
+(function () {
+    var keep_session_url = "{/literal}{$www_url}{literal}admin/login.php?keepSessionAlive&amp;";
+
+    function refreshSession()
+    {
+        var _RIMAGE = new Image(1,1);
+        _RIMAGE.src = keep_session_url + Math.round(Math.random()*1000000000);
+    }
+    window.setInterval(refreshSession, 10 * 60 * 1000);
+} ());
+{/literal}
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/templates/admin/_head.tpl b/templates/admin/_head.tpl
new file mode 100644 (file)
index 0000000..6654c4b
--- /dev/null
@@ -0,0 +1,112 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>{$title|escape}</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
+    <link rel="stylesheet" type="text/css" href="{$admin_url}static/admin.css" media="all" />
+    <link rel="stylesheet" type="text/css" href="{$admin_url}static/print.css" media="print" />
+    <link rel="stylesheet" type="text/css" href="{$admin_url}static/handheld.css" media="handheld,screen and (max-width:981px)" />
+    {if isset($js)}
+        <script type="text/javascript" src="{$admin_url}static/global.js"></script>
+    {/if}
+    {if isset($custom_js)}
+        {foreach from=$custom_js item="js"}
+            <script type="text/javascript" src="{$admin_url}static/{$js|escape}"></script>
+        {/foreach}
+    {/if}
+    {if isset($plugin_css)}
+        {foreach from=$plugin_css item="css"}
+            <link rel="stylesheet" type="text/css" href="{plugin_url file=$css}" />
+        {/foreach}
+    {/if}
+    {if isset($plugin_js)}
+        {foreach from=$plugin_js item="hs"}
+            <script type="text/javascript" src="{plugin_url file=$js}"></script>
+        {/foreach}
+    {/if}
+</head>
+
+<body{if !empty($body_id)} id="{$body_id|escape}"{/if} data-url="{$admin_url|escape}">
+
+{if empty($is_popup)}
+<div class="header">
+    <ul class="menu">
+    {if !$is_logged}
+        <li><a href="{$www_url}">&larr; Retour au site</a></li>
+        <li><a href="{$admin_url}">Connexion</a>
+            <ul>
+                <li><a href="{$admin_url}password.php">Mot de passe perdu</a>
+            </ul>
+        </li>
+    {else}
+        <li class="home{if $current == 'home'} current{/if}"><a href="{$admin_url}"><b class="icn">⌂</b> Accueil</a></li>
+        {if !empty($plugins_menu)}
+            <li class="plugins">
+                <ul>
+                {foreach from=$plugins_menu key="id" item="name"}
+                    <li class="plugins {if $current == "plugin_`$id`"} current{/if}"><a href="{plugin_url id=$id}">{$name|escape}</a></li>
+                {/foreach}
+                </ul>
+            </li>
+        {/if}
+        {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES}
+            <li class="member list{if $current == 'membres'} current{/if}"><a href="{$admin_url}membres/"><b class="icn">👪</b> Membres</a>
+            {if $user.droits.membres >= Garradin\Membres::DROIT_ECRITURE}
+            <ul>
+                <li class="member new{if $current == 'membres/ajouter'} current{/if}"><a href="{$admin_url}membres/ajouter.php">Ajouter</a></li>
+                <li class="member cotisations{if $current == 'membres/cotisations'} current{/if}"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
+                {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+                <li class="member admin config{if $current == 'membres/categories'} current{/if}"><a href="{$admin_url}membres/categories.php">Catégories</a></li>
+                <li class="members admin mail{if $current == 'membres/message_collectif'} current{/if}"><a href="{$admin_url}membres/message_collectif.php">Message collectif</a></li>
+                {/if}
+            </ul>
+            {/if}
+            </li>
+        {/if}
+        {if $user.droits.compta >= Garradin\Membres::DROIT_ACCES}
+            <li class="compta{if $current == 'compta'} current{/if}"><a href="{$admin_url}compta/"><b>€</b> Comptabilité</a>
+            <ul>
+            {if $user.droits.compta >= Garradin\Membres::DROIT_ECRITURE}
+                <li class="compta new{if $current == 'compta/saisie'} current{/if}"><a href="{$admin_url}compta/operations/saisir.php">Saisie</a></li>
+            {/if}
+                <li class="compta list{if $current == 'compta/gestion'} current{/if}"><a href="{$admin_url}compta/operations/">Suivi des opérations</a></li>
+                <li class="compta banks{if $current == 'compta/banques'} current{/if}"><a href="{$admin_url}compta/banques/">Banques &amp; caisse</a></li>
+            {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}
+                <li class="compta admin config{if $current == 'compta/categories'} current{/if}"><a href="{$admin_url}compta/categories/">Catégories &amp; comptes</a></li>
+            {/if}
+                <li class="compta admin reports{if $current == 'compta/exercices'} current{/if}"><a href="{$admin_url}compta/exercices/">Exercices</a></li>
+            </ul>
+            </li>
+        {/if}
+        {if $user.droits.wiki >= Garradin\Membres::DROIT_ACCES}
+            <li class="wiki{if $current == 'wiki'} current{/if}"><a href="{$admin_url}wiki/"><b class="icn">✎</b> Wiki</a>
+            <ul>
+                <li class="wiki list{if $current == 'wiki/recent'} current{/if}"><a href="{$admin_url}wiki/recent.php">Dernières modifications</a>
+                <li class="wiki search{if $current == 'wiki/chercher'} current{/if}"><a href="{$admin_url}wiki/chercher.php">Recherche</a>
+                {if $user.droits.wiki >= Garradin\Membres::DROIT_ECRITURE}
+                {/if}
+                {*<li class="wiki follow{if $current == 'wiki/suivi'} current{/if}"><a href="{$admin_url}wiki/suivi.php">Mes pages suivies</a>*}
+                {*<li class="wiki follow{if $current == 'wiki/contribution'} current{/if}"><a href="{$admin_url}wiki/contributions.php">Mes contributions</a>*}
+            </ul>
+            </li>
+        {/if}
+        {if $user.droits.config >= Garradin\Membres::DROIT_ADMIN}
+            <li class="main config{if $current == 'config'} current{/if}"><a href="{$admin_url}config/"><b class="icn">☸</b>Configuration</a>
+        {/if}
+        <li class="my config{if $current == 'mes_infos'} current{/if}"><a href="{$admin_url}mes_infos.php"><b class="icn">👤</b> Mes infos personnelles</a>
+            <ul>
+                <li class="my cotisations{if $current == 'mes_cotisations'} current{/if}"><a href="{$admin_url}mes_cotisations.php">Mes cotisations</a></li>
+            </ul>
+        </li>
+        {if !defined('Garradin\LOCAL_LOGIN')}
+        <li class="logout"><a href="{$admin_url}logout.php"><b class="icn">⤝</b> Déconnexion</a></li>
+        {/if}
+    {/if}
+    </ul>
+
+    <h1>{$title|escape}</h1>
+</div>
+{/if}
+
+<div class="page">
\ 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 (file)
index 0000000..8e3e429
--- /dev/null
@@ -0,0 +1,32 @@
+{include file="admin/_head.tpl" title="Ajouter un compte" current="compta/banques"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Ajouter un compte bancaire</legend>
+        <dl>
+            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle}" required="required" /></dd>
+            <dt><label for="f_banque">Nom de la banque</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="banque" id="f_banque" value="{form_field name=banque}" required="required" /></dd>
+            <dt><label for="f_iban">Numéro IBAN</label></dt>
+            <dd><input type="text" size="30" name="iban" id="f_iban" value="{form_field name=iban}" /></dd>
+            <dt><label for="f_bic">Code BIC/SWIFT de la banque</label></dt>
+            <dd><input type="text" size="10" name="bic" id="f_bic" value="{form_field name=bic}" /></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_ajout_banque"}
+        <input type="submit" name="add" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..a2c87f3
--- /dev/null
@@ -0,0 +1,34 @@
+{include file="admin/_head.tpl" title="Comptes bancaires" current="compta/banques"}
+
+<ul class="actions">
+    <li class="current"><a href="{$www_url}admin/compta/banques/">Comptes bancaires</a></li>
+    <li><a href="{$www_url}admin/compta/comptes/journal.php?id={Garradin\Compta_Comptes::CAISSE}&amp;suivi">Journal de caisse</a></li>
+    {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}<li><strong><a href="{$www_url}admin/compta/banques/ajouter.php">Ajouter un compte bancaire</a></strong></li>{/if}
+</ul>
+
+    {if !empty($liste)}
+        <dl class="catList">
+        {foreach from=$liste item="compte"}
+            <dt>{$compte.libelle|escape} {if !empty($compte.banque)}({$compte.banque|escape}){/if}</dt>
+            <dd class="desc">
+                IBAN : {$compte.iban|escape|format_iban}<br />
+                BIC : {$compte.bic|escape}<br />
+                {$compte.iban|escape|format_rib}
+            </dd>
+            <dd class="desc">Solde : {$compte.solde|html_money} {$config.monnaie|escape}</dd>
+            <dd class="actions">
+                <a href="{$www_url}admin/compta/comptes/journal.php?id={$compte.id|escape}&amp;suivi">Journal</a>
+            {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}
+                | <a href="{$www_url}admin/compta/banques/modifier.php?id={$compte.id|escape}">Modifier</a>
+                | <a href="{$www_url}admin/compta/banques/supprimer.php?id={$compte.id|escape}">Supprimer</a>
+            {/if}
+            </dd>
+        {/foreach}
+        </dl>
+    {else}
+        <p class="alert">
+            Aucun compte bancaire trouvé.
+        </p>
+    {/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 (file)
index 0000000..d73d93f
--- /dev/null
@@ -0,0 +1,32 @@
+{include file="admin/_head.tpl" title="Modifier un compte" current="compta/banques"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Modifier un compte bancaire</legend>
+        <dl>
+            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle data=$compte}" required="required" /></dd>
+            <dt><label for="f_banque">Nom de la banque</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="banque" id="f_banque" value="{form_field name=banque data=$compte}" required="required" /></dd>
+            <dt><label for="f_iban">Numéro IBAN</label></dt>
+            <dd><input type="text" size="30" name="iban" id="f_iban" value="{form_field name=iban data=$compte}" /></dd>
+            <dt><label for="f_bic">Code BIC/SWIFT de la banque</label></dt>
+            <dd><input type="text" size="10" name="bic" id="f_bic" value="{form_field name=bic data=$compte}" /></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_edit_banque_`$compte.id`"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..b0c7512
--- /dev/null
@@ -0,0 +1,29 @@
+{include file="admin/_head.tpl" title="Supprimer un compte" current="compta/banques"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Supprimer le compte ?</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer le compte «&nbsp;{$compte.id|escape} - {$compte.libelle|escape}&nbsp;» ?
+        </h3>
+        <p class="help">
+            Attention, le compte ne pourra pas être supprimé si des opérations y sont
+            affectées.
+        </p>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_delete_banque_`$compte.id`"}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..be0590c
--- /dev/null
@@ -0,0 +1,39 @@
+{include file="admin/_head.tpl" title="Ajouter une catégorie" current="compta/categories"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Ajouter une catégorie</legend>
+        <dl>
+            <dt><label for="f_type">Type</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="type" id="f_type" required="required">
+                    <option value="{Garradin\Compta_Categories::RECETTES}"{if $type == Garradin\Compta_Categories::RECETTES} selected="selected"{/if}>Recette</option>
+                    <option value="{Garradin\Compta_Categories::DEPENSES}"{if $type == Garradin\Compta_Categories::DEPENSES} selected="selected"{/if}>Dépense</option>
+                </select>
+            </dd>
+            <dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="intitule" id="f_intitule" value="{form_field name=intitule}" required="required" /></dd>
+            <dt><label for="f_description">Description</label></dt>
+            <dd><textarea name="description" id="f_description" rows="4" cols="30">{form_field name=description}</textarea></dd>
+            <dt><label for="f_compte">Compte affecté</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                {select_compte comptes=$comptes name="compte"}
+            </dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_ajout_cat"}
+        <input type="submit" name="add" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..a70f3a6
--- /dev/null
@@ -0,0 +1,31 @@
+{include file="admin/_head.tpl" title="Catégories" current="compta/categories"}
+
+<ul class="actions">
+    <li{if $type == Garradin\Compta_Categories::RECETTES} class="current"{/if}><a href="?recettes">Recettes</a></li>
+    <li{if $type == Garradin\Compta_Categories::DEPENSES} class="current"{/if}><a href="?depenses">Dépenses</a></li>
+    <li><strong><a href="{$www_url}admin/compta/categories/ajouter.php">Ajouter une catégorie</a></strong></li>
+    <li><em><a href="{$www_url}admin/compta/comptes/">Plan comptable</a></em></li>
+</ul>
+
+    {if !empty($liste)}
+        <dl class="catList">
+        {foreach from=$liste item="cat"}
+            <dt>{$cat.intitule|escape}</dt>
+            {if !empty($cat.description)}
+                <dd class="desc">{$cat.description|escape}</dd>
+            {/if}
+            <dd class="compte"><strong>{$cat.compte|escape}</strong> - {$cat.compte_libelle|escape}</dd>
+            <dd class="actions">
+                <a href="{$www_url}admin/compta/operations/?cat={$cat.id|escape}">Voir</a>
+                | <a href="{$www_url}admin/compta/categories/modifier.php?id={$cat.id|escape}">Modifier</a>
+                | <a href="{$www_url}admin/compta/categories/supprimer.php?id={$cat.id|escape}">Supprimer</a>
+            </dd>
+        {/foreach}
+        </dl>
+    {else}
+        <p class="alert">
+            Aucune catégorie trouvée.
+        </p>
+    {/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 (file)
index 0000000..5c43c9a
--- /dev/null
@@ -0,0 +1,28 @@
+{include file="admin/_head.tpl" title="Modifier une catégorie" current="compta/categories"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Modifier une catégorie</legend>
+        <dl>
+            <dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="intitule" id="f_intitule" value="{form_field name=intitule data=$cat}" required="required" /></dd>
+            <dt><label for="f_description">Description</label></dt>
+            <dd><textarea name="description" id="f_description" rows="4" cols="70">{form_field name=description data=$cat}</textarea></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_edit_cat_`$cat.id`"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..4d90ccc
--- /dev/null
@@ -0,0 +1,29 @@
+{include file="admin/_head.tpl" title="Supprimer une catégorie" current="compta/categories"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Supprimer la catégorie comptable ?</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer la catégorie «&nbsp;{$cat.intitule|escape}&nbsp;» ?
+        </h3>
+        <p class="help">
+            Attention, la catégorie ne pourra pas être supprimée si des opérations y sont
+            affectées.
+        </p>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="delete_compta_cat_"|cat:$cat.id}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..609c812
--- /dev/null
@@ -0,0 +1,39 @@
+{include file="admin/_head.tpl" title="Ajouter un compte" current="compta/categories"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Ajouter un compte</legend>
+        <dl>
+            <dt><label for="f_parent">Compte parent</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                {select_compte comptes=$comptes name="parent" create=true}
+            </dd>
+            <dt><label for="f_numero">Numéro de compte</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" size="10" name="numero" id="f_numero" value="{form_field name=numero}" required="required" /></dd>
+            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle}" required="required" /></dd>
+            <dt><label for="f_position_1">Position</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            {foreach from=$positions item="pos" key="id"}
+            <dd>
+                <input type="radio" name="position" id="f_position_{$id|escape}" value="{$id|escape}" {if $position == $id}checked="checked"{/if} />
+                <label for="f_position_{$id|escape}">{$pos|escape}</label>
+            </dd>
+            {/foreach}
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_ajout_compte"}
+        <input type="submit" name="add" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..ce3011c
--- /dev/null
@@ -0,0 +1,51 @@
+{if empty($classe)}
+    {include file="admin/_head.tpl" title="Comptes" current="compta/categories"}
+    <ul class="accountList">
+    {foreach from=$classes item="_classe"}
+        <li><h4><a href="{$www_url}admin/compta/comptes/?classe={$_classe.id|escape}">{$_classe.libelle|escape}</a></h4></li>
+    {/foreach}
+    </ul>
+{else}
+    {include file="admin/_head.tpl" title=$classe_compte.libelle current="compta/categories"}
+
+    <ul class="actions">
+        <li><a href="{$www_url}admin/compta/comptes/">Liste des classes</a></li>
+        <li><a href="{$www_url}admin/compta/comptes/ajouter.php?classe={$classe|escape}">Ajouter un compte dans cette classe</a></li>
+    </ul>
+
+    <p class="help">
+        Les comptes avec la mention <em>*</em> font partie du plan comptable standard
+        et ne peuvent être modifiés ou supprimés.
+    </p>
+
+    {if !empty($liste)}
+        <table class="list accountList">
+        {foreach from=$liste item="compte"}
+            <tr class="niveau_{$compte.id|strlen}">
+                <th>{$compte.id|escape}</th>
+                <td class="libelle">{$compte.libelle|escape}</td>
+                <td class="actions">
+                    {if !empty($compte.desactive)}
+                        <em>Désactivé</em>
+                    {else}
+                        {$compte.position|get_position}
+                        {if !$compte.plan_comptable}
+                            | <a href="{$www_url}admin/compta/comptes/modifier.php?id={$compte.id|escape}">Modifier</a>
+                            | <a href="{$www_url}admin/compta/comptes/supprimer.php?id={$compte.id|escape}">Supprimer</a>
+                        {else}
+                            <em>*</em>
+                        {/if}
+                    {/if}
+                </td>
+            </tr>
+        {/foreach}
+        </table>
+
+    {else}
+        <p class="alert">
+            Aucun compte trouvé.
+        </p>
+    {/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 (file)
index 0000000..444b7f1
--- /dev/null
@@ -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)}
+<ul class="actions">
+    <li><a href="{$www_url}admin/compta/banques/">Comptes bancaires</a></li>
+    <li><a href="{$www_url}admin/compta/comptes/journal.php?id={Garradin\Compta_Comptes::CAISSE}">Journal de caisse</a></li>
+    {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}<li><strong><a href="{$www_url}admin/compta/banques/ajouter.php">Ajouter un compte bancaire</a></strong></li>{/if}
+</ul>
+{/if}
+
+
+<table class="list">
+    <colgroup>
+        <col width="3%" />
+        <col width="3%" />
+        <col width="12%" />
+        <col width="10%" />
+        <col width="12%" />
+        <col />
+    </colgroup>
+    <thead>
+        <tr>
+            <td></td>
+            <td></td>
+            <td>Date</td>
+            <td>Montant</td>
+            <td>Solde cumulé</td>
+            <th>Libellé</th>
+        </tr>
+    </thead>
+    <tbody>
+    {foreach from=$journal item="ligne"}
+        <tr>
+            <td class="num"><a href="{$admin_url}compta/operations/voir.php?id={$ligne.id|escape}">{$ligne.id|escape}</a></td>
+            <td class="actions">
+            {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}
+                <a class="icn" href="{$admin_url}compta/operations/modifier.php?id={$ligne.id|escape}" title="Modifier cette opération">✎</a>
+            {/if}
+            </td>
+            <td>{$ligne.date|date_fr:'d/m/Y'|escape}</td>
+            <td>{if $ligne.compte_credit == $compte.id}{$credit}{else}{$debit}{/if}{$ligne.montant|html_money}</td>
+            <td>{$ligne.solde|html_money}</td>
+            <th>{$ligne.libelle|escape}</th>
+        </tr>
+    {/foreach}
+    </tbody>
+    <tfoot>
+        <tr>
+            <td colspan="3"></td>
+            <th>Solde</th>
+            <td colspan="2">{$solde|html_money} {$config.monnaie|escape}</td>
+        </tr>
+    </tfoot>
+</table>
+
+{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 (file)
index 0000000..3c0168b
--- /dev/null
@@ -0,0 +1,33 @@
+{include file="admin/_head.tpl" title="Modifier un compte" current="compta/categories"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Modifier un compte</legend>
+        <dl>
+            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle data=$compte}" required="required" /></dd>
+            <dt><label for="f_position_1">Position</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            {foreach from=$positions item="pos" key="id"}
+            <dd>
+                <input type="radio" name="position" id="f_position_{$id|escape}" value="{$id|escape}" {if $position == $id}checked="checked"{/if} />
+                <label for="f_position_{$id|escape}">{$pos|escape}</label>
+            </dd>
+            {/foreach}
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_edit_compte_`$compte.id`"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..2fc25bb
--- /dev/null
@@ -0,0 +1,53 @@
+{include file="admin/_head.tpl" title="Supprimer un compte" current="compta/categories"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+{if !$can_delete && !$can_disable}
+    <p class="alert">
+        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.
+    </p>
+{elseif $can_disable && !$can_delete}
+
+    <form method="post" action="{$self_url|escape}">
+
+        <fieldset>
+            <legend>Désactiver le compte ?</legend>
+            <h3 class="warning">
+                Êtes-vous sûr de vouloir désactiver le compte «&nbsp;{$compte.id|escape} - {$compte.libelle|escape}&nbsp;»&nbsp;?
+            </h3>
+            <p class="help">
+                Une fois désactivé il ne sera plus possible de l'utiliser, mais il pourra par contre être réactivé.
+            </p>
+        </fieldset>
+
+        <p class="submit">
+            {csrf_field key="compta_disable_compte_`$compte.id`"}
+            <input type="submit" name="disable" value="Désactiver &rarr;" />
+        </p>
+
+    </form>
+{else}
+    <form method="post" action="{$self_url|escape}">
+
+        <fieldset>
+            <legend>Supprimer le compte ?</legend>
+            <h3 class="warning">
+                Êtes-vous sûr de vouloir supprimer le compte «&nbsp;{$compte.id|escape} - {$compte.libelle|escape}&nbsp;»&nbsp;?
+            </h3>
+        </fieldset>
+
+        <p class="submit">
+            {csrf_field key="compta_delete_compte_`$compte.id`"}
+            <input type="submit" name="delete" value="Supprimer &rarr;" />
+        </p>
+
+    </form>
+{/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 (file)
index 0000000..3661f69
--- /dev/null
@@ -0,0 +1,30 @@
+{include file="admin/_head.tpl" title="Commencer un exercice" current="compta/exercices" js=1}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Commencer un nouvel exercice</legend>
+        <dl>
+            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle}" required="required" /></dd>
+            <dt><label for="f_debut">Début de l'exercice</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="date" name="debut" id="f_debut" value="{form_field name=debut}" size="10" required="required" /></dd>
+            <dt><label for="f_fin">Fin de l'exercice</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="date" name="fin" id="f_fin" value="{form_field name=fin}" size="10" required="required" /></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_ajout_exercice"}
+        <input type="submit" name="add" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..4704b04
--- /dev/null
@@ -0,0 +1,84 @@
+{include file="admin/_head.tpl" title="Bilan" current="compta/exercices" body_id="rapport"}
+
+<div class="exercice">
+    <h2>{$config.nom_asso|escape}</h2>
+    <p>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'}</p>
+</div>
+
+<table>
+    <colgroup>
+        <col width="50%" />
+        <col width="50%" />
+    </colgroup>
+    <tbody>
+        <tr>
+            <td>
+                <table>
+                    <caption><h3>Actif</h3></caption>
+                    <tbody>
+                    {foreach from=$bilan.actif.comptes key="parent_code" item="parent"}
+                        <tr class="parent">
+                            <th>{$parent_code|get_nom_compte|escape}</th>
+                            <td>{$parent.solde|html_money}</td>
+                        </tr>
+                        {foreach from=$parent.comptes item="solde" key="compte"}
+                        <tr class="compte">
+                            <th>{$compte|get_nom_compte|escape}</th>
+                            <td>{$solde|html_money}</td>
+                        </tr>
+                        {/foreach}
+                    {/foreach}
+                    </tbody>
+                </table>
+            </td>
+            <td>
+                <table>
+                    <caption><h3>Passif</h3></caption>
+                    <tbody>
+                    {foreach from=$bilan.passif.comptes key="parent_code" item="parent"}
+                        <tr class="parent">
+                            <th>{$parent_code|get_nom_compte|escape}</th>
+                            <td>{$parent.solde|html_money}</td>
+                        </tr>
+                        {foreach from=$parent.comptes item="solde" key="compte"}
+                        <tr class="compte">
+                            <th>{$compte|get_nom_compte|escape}</th>
+                            <td>{$solde|html_money}</td>
+                        </tr>
+                        {/foreach}
+                    {/foreach}
+                    </tbody>
+                </table>
+            </td>
+        </tr>
+    </tbody>
+    <tfoot>
+        <tr>
+            <td>
+                <table>
+                    <tfoot>
+                        <tr>
+                            <th>Total actif</th>
+                            <td>{$bilan.actif.total|html_money}</td>
+                        </tr>
+                    </tfoot>
+                </table>
+            </td>
+           <td>
+                <table>
+                    <tfoot>
+                        <tr>
+                            <th>Total passif</th>
+                            <td>{$bilan.passif.total|html_money}</td>
+                        </tr>
+                    </tfoot>
+                </table>
+            </td>
+        </tr>
+    </tfoot>
+</table>
+
+<p class="help">Toutes les opérations sont libellées en {$config.monnaie|escape}.</p>
+
+{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 (file)
index 0000000..675bae3
--- /dev/null
@@ -0,0 +1,41 @@
+{include file="admin/_head.tpl" title="Clôturer un exercice" current="compta/exercices" js=1}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Clôturer un exercice</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir clôturer l'exercice «&nbsp;{$exercice.libelle|escape}&nbsp;» ?
+        </h3>
+        <p class="help">
+            Attention, une fois clôturé, les opérations de cet exercice ne pourront plus être supprimées ou modifiées.
+        </p>
+        <dl>
+            <dt>Début de l'exercice</dt>
+            <dd>{$exercice.debut|date_fr:'d/m/Y'}</dd>
+            <dt><label for="f_fin">Fin de l'exercice</label></dt>
+            <dd class="help">Si des opérations existent après cette date, elles seront automatiquement
+                attribuées à un nouvel exercice.</dd>
+            <dd><input type="date" name="fin" id="f_fin" value="{form_field name=fin default=$exercice.fin|date_fr:'Y-m-d'}" size="10" /></dd>
+            <dt>
+                <input type="checkbox" name="reports" {form_field name=reports default="1" checked=true} id="f_reports" /> <label for="f_reports">Exécuter automatiquement les reports à nouveau</label>
+            </dt>
+            <dd class="help">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.</dd>
+        </h3>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_cloturer_exercice_`$exercice.id`"}
+        <input type="submit" name="close" value="Clôturer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..7ca80df
--- /dev/null
@@ -0,0 +1,110 @@
+{include file="admin/_head.tpl" title="Compte de résultat" current="compta/exercices" body_id="rapport"}
+
+<div class="exercice">
+    <h2>{$config.nom_asso|escape}</h2>
+    <p>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'}</p>
+</div>
+
+<table>
+    <colgroup>
+        <col width="50%" />
+        <col width="50%" />
+    </colgroup>
+    <tbody>
+        <tr>
+            <td>
+                <table>
+                    <caption><h3>Charges</h3></caption>
+                    <tbody>
+                    {foreach from=$compte_resultat.charges.comptes key="parent_code" item="parent"}
+                        <tr class="parent">
+                            <th>{$parent_code|get_nom_compte|escape}</th>
+                            <td>{$parent.solde|html_money}</td>
+                        </tr>
+                        {foreach from=$parent.comptes item="solde" key="compte"}
+                        <tr class="compte">
+                            <th>{$compte|get_nom_compte|escape}</th>
+                            <td>{$solde|html_money}</td>
+                        </tr>
+                        {/foreach}
+                    {/foreach}
+                    </tbody>
+                </table>
+            </td>
+            <td>
+                <table>
+                    <caption><h3>Produits</h3></caption>
+                    <tbody>
+                    {foreach from=$compte_resultat.produits.comptes key="parent_code" item="parent"}
+                        <tr class="parent">
+                            <th>{$parent_code|get_nom_compte|escape}</th>
+                            <td>{$parent.solde|html_money}</td>
+                        </tr>
+                        {foreach from=$parent.comptes item="solde" key="compte"}
+                        <tr class="compte">
+                            <th>{$compte|get_nom_compte|escape}</th>
+                            <td>{$solde|html_money}</td>
+                        </tr>
+                        {/foreach}
+                    {/foreach}
+                    </tbody>
+                </table>
+            </td>
+        </tr>
+    </tbody>
+    <tfoot>
+        <tr>
+            <td>
+                <table>
+                    <tfoot>
+                        <tr>
+                            <th>Total charges</th>
+                            <td>{$compte_resultat.charges.total|html_money}</td>
+                        </tr>
+                    </tfoot>
+                </table>
+            </td>
+           <td>
+                <table>
+                    <tfoot>
+                        <tr>
+                            <th>Total produits</th>
+                            <td>{$compte_resultat.produits.total|html_money}</td>
+                        </tr>
+                    </tfoot>
+                </table>
+            </td>
+        </tr>
+        <tr>
+            <td>
+            {if ($compte_resultat.resultat >= 0)}
+                <table>
+                    <tfoot>
+                        <tr>
+                            <th>Résultat (excédent)</th>
+                            <td>{$compte_resultat.resultat|html_money}</td>
+                        </tr>
+                    </tfoot>
+                </table>
+            {/if}
+            </td>
+            <td>
+            {if ($compte_resultat.resultat < 0)}
+                <table>
+                    <tfoot>
+                        <tr>
+                            <th>Résultat (déficit)</th>
+                            <td>{$compte_resultat.resultat|html_money}</td>
+                        </tr>
+                    </tfoot>
+                </table>
+            {/if}
+            </td>
+        </tr>
+    </tfoot>
+</table>
+
+<p class="help">Toutes les opérations sont libellées en {$config.monnaie|escape}.</p>
+
+{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 (file)
index 0000000..4aac7a5
--- /dev/null
@@ -0,0 +1,89 @@
+{include file="admin/_head.tpl" title="Grand livre" current="compta/exercices" body_id="rapport"}
+
+<div class="exercice">
+    <h2>{$config.nom_asso|escape}</h2>
+    <p>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'}</p>
+</div>
+
+{foreach from=$livre.classes key="classe" item="comptes"}
+<h3>{$classe|get_nom_compte|escape}</h3>
+
+{foreach from=$comptes item="compte" key="code"}
+    {foreach from=$compte.comptes item="souscompte" key="souscode"}
+    <table class="list">
+        <caption><h4>{$souscode|escape} — {$souscode|get_nom_compte|escape}</h4></caption>
+        <colgroup>
+            <col width="15%" />
+            <col width="65%" />
+            <col width="10%" />
+            <col width="10%" />
+        </colgroup>
+        <thead>
+            <tr>
+                <td>Date</td>
+                <th>Intitulé</th>
+                <td>Débit</td>
+                <td>Crédit</td>
+            </tr>
+        </thead>
+        <tbody>
+        {foreach from=$souscompte.journal item="ligne"}
+            <tr>
+                <td>{$ligne.date|date_fr:'d/m/Y'|escape}</td>
+                <th>{$ligne.libelle|escape}</th>
+                <td>{if $ligne.compte_debit == $souscode}{$ligne.montant|html_money}{/if}</td>
+                <td>{if $ligne.compte_credit == $souscode}{$ligne.montant|html_money}{/if}</td>
+            </tr>
+        {/foreach}
+        </tbody>
+        <tfoot>
+            <tr>
+                <td></td>
+                <th>Solde final</th>
+                <td>{if $souscompte.debit > 0}{$souscompte.debit|html_money}{/if}</td>
+                <td>{if $souscompte.credit > 0}{$souscompte.credit|html_money}{/if}</td>
+            </tr>
+        </tfoot>
+    </table>
+    {/foreach}
+
+    <table class="list">
+        <colgroup>
+            <col width="15%" />
+            <col width="65%" />
+            <col width="10%" />
+            <col width="10%" />
+        </colgroup>
+        <tfoot>
+            <tr>
+                <td>Total</td>
+                <th>{$code|get_nom_compte|escape}</th>
+                <td>{if $compte.total > 0}{$compte.total|abs|html_money}{/if}</td>
+                <td>{if $compte.total < 0}{$compte.total|abs|html_money}{/if}</td>
+            </tr>
+        </tfoot>
+    </table>
+    {/foreach}
+{/foreach}
+
+<table class="list">
+    <colgroup>
+        <col width="15%" />
+        <col width="65%" />
+        <col width="10%" />
+        <col width="10%" />
+    </colgroup>
+    <tfoot>
+        <tr>
+            <td><strong>Total</strong></td>
+            <th></th>
+            <td>{$livre.debit|html_money}</td>
+            <td>{$livre.credit|html_money}</td>
+        </tr>
+    </tfoot>
+</table>
+
+<p class="help">Toutes les opérations sont libellées en {$config.monnaie|escape}.</p>
+
+{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 (file)
index 0000000..888fdfd
--- /dev/null
@@ -0,0 +1,43 @@
+{include file="admin/_head.tpl" title="Exercices" current="compta/exercices"}
+
+{if !$current}
+<ul class="actions">
+    <li><strong><a href="{$www_url}admin/compta/exercices/ajouter.php">Commencer un nouvel exercice</a></strong></li>
+</ul>
+{/if}
+
+{if !empty($liste)}
+    <dl class="catList">
+    {foreach from=$liste item="exercice"}
+        <dt>{$exercice.libelle|escape}</dt>
+        <dd class="desc">
+            {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'}
+        </dd>
+        <dd class="compte">
+            <strong>{$exercice.nb_operations|escape}</strong> opérations enregistrées.
+        </dd>
+        <dd class="desc">
+            <a href="{$www_url}admin/compta/exercices/journal.php?id={$exercice.id|escape}">Journal général</a>
+            | <a href="{$www_url}admin/compta/exercices/grand_livre.php?id={$exercice.id|escape}">Grand livre</a>
+            | <a href="{$www_url}admin/compta/exercices/compte_resultat.php?id={$exercice.id|escape}">Compte de résultat</a>
+            | <a href="{$www_url}admin/compta/exercices/bilan.php?id={$exercice.id|escape}">Bilan</a>
+        </dd>
+        {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}
+        <dd class="actions">
+            {if !$exercice.cloture}
+            <a href="{$www_url}admin/compta/exercices/modifier.php?id={$exercice.id|escape}">Modifier</a>
+            | <a href="{$www_url}admin/compta/exercices/cloturer.php?id={$exercice.id|escape}">Clôturer</a>
+            | <a href="{$www_url}admin/compta/exercices/supprimer.php?id={$exercice.id|escape}">Supprimer</a>
+            {/if}
+        </dd>
+        {/if}
+    {/foreach}
+    </dl>
+{else}
+    <p class="alert">
+        Il n'y a pas d'exercice en cours.
+    </p>
+{/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 (file)
index 0000000..5e0e7e6
--- /dev/null
@@ -0,0 +1,39 @@
+{include file="admin/_head.tpl" title="Journal général" current="compta/exercices" body_id="rapport"}
+
+<div class="exercice">
+    <h2>{$config.nom_asso|escape}</h2>
+    <p>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'}</p>
+</div>
+
+<table class="list multi">
+    <thead>
+        <tr>
+            <td>Date</td>
+            <th>Intitulé</th>
+            <td>Comptes</td>
+            <td>Débit</td>
+            <td>Crédit</td>
+        </tr>
+    </thead>
+    <tbody>
+    {foreach from=$journal item="ligne"}
+        <tr>
+            <td rowspan="2">{$ligne.date|date_fr:'d/m/Y'|escape}</td>
+            <th rowspan="2">{$ligne.libelle|escape}</th>
+            <td>{$ligne.compte_debit|escape} - {$ligne.compte_debit|get_nom_compte|escape}</td>
+            <td>{$ligne.montant|html_money}</td>
+            <td></td>
+        </tr>
+        <tr>
+            <td>{$ligne.compte_credit|escape} - {$ligne.compte_credit|get_nom_compte|escape}</td>
+            <td></td>
+            <td>{$ligne.montant|html_money}</td>
+        </tr>
+    {/foreach}
+    </tbody>
+</table>
+
+<p class="help">Toutes les opérations sont libellées en {$config.monnaie|escape}.</p>
+
+{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 (file)
index 0000000..88c271b
--- /dev/null
@@ -0,0 +1,30 @@
+{include file="admin/_head.tpl" title="Modifier un exercice" current="compta/exercices" js=1}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Modifier un exercice</legend>
+        <dl>
+            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle data=$exercice}" required="required" /></dd>
+            <dt><label for="f_debut">Début de l'exercice</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="date" name="debut" id="f_debut" value="{form_field name=debut default=$exercice.debut|date_fr:'Y-m-d'}" size="10" required="required" /></dd>
+            <dt><label for="f_fin">Fin de l'exercice</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="date" name="fin" id="f_fin" value="{form_field name=fin default=$exercice.fin|date_fr:'Y-m-d'}" size="10" required="required" /></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_modif_exercice_`$exercice.id`"}
+        <input type="submit" name="edit" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..603faf7
--- /dev/null
@@ -0,0 +1,30 @@
+{include file="admin/_head.tpl" title="Supprimer un exercice" current="compta/exercices"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Supprimer un exercice</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer l'exercice «&nbsp;{$exercice.libelle|escape}&nbsp;»
+            du {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'} ?
+        </h3>
+        <p class="help">
+            Attention, l'exercice ne pourra pas être supprimé si des opérations y sont
+            toujours affectées.
+        </p>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_supprimer_exercice_`$exercice.id`"}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..c00a844
--- /dev/null
@@ -0,0 +1,61 @@
+{include file="admin/_head.tpl" title="Import / Export" current="compta"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{elseif $ok}
+    <p class="confirm">
+        L'import s'est bien déroulé.
+    </p>
+{/if}
+
+<ul class="actions">
+    <li class="current"><a href="{$www_url}admin/compta/import.php">Importer</a></li>
+    <li><a href="{$www_url}admin/compta/import.php?export">Exporter en CSV</a></li>
+</ul>
+
+<form method="post" action="{$self_url|escape}" enctype="multipart/form-data">
+
+    <fieldset>
+        <legend>Importer depuis un fichier</legend>
+        <dl>
+            <dt><label for="f_file">Fichier à importer</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="file" name="upload" id="f_file" required="required" /></dd>
+            <dt><label for="f_type">Type de fichier</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <input type="radio" name="type" id="f_type" value="garradin" {form_field name=type checked="garradin" default="garradin"} />
+                <label for="f_type">Export CSV de Garradin</label>
+            </dd>
+            <dd class="help">
+                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.
+            </dd>
+            <dd>
+                <input type="radio" name="type" id="f_type_citizen" value="citizen" {form_field name=type checked="citizen"} />
+                <label for="f_type_citizen">Export CSV de Citizen Comptabilité</label>
+            </dd>
+            <dd class="help">
+                Export des données au format CSV provenant du logiciel de comptabilité de
+                <a href="http://www.citizenplace.com/">Citizen Place</a>.
+            </dd>
+            <dd class="help">
+                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à.
+            </dd>
+        </dl>
+    </fieldset>
+
+    <p class="alert">
+        Si le fichier comporte des opérations dont la date est en dehors de l'exercice courant,
+        elles seront ignorées.
+    </p>
+
+    <p class="submit">
+        {csrf_field key="compta_import"}
+        <input type="submit" name="import" value="Importer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..fff906f
--- /dev/null
@@ -0,0 +1,21 @@
+{include file="admin/_head.tpl" title="Comptabilité" current="compta"}
+
+{if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}
+<ul class="actions">
+    <li><a href="{$www_url}admin/compta/import.php">Import / export</a></li>
+    <li><a href="{$www_url}admin/compta/operations/recherche_sql.php">Recherche par requête SQL</a></li>
+</ul>
+{/if}
+
+<div class="infos">
+    <p>
+        <img src="{$www_url}admin/compta/graph.php?g=recettes_depenses" />
+        <img src="{$www_url}admin/compta/graph.php?g=banques_caisses" />
+    </p>
+    <p>
+        <img src="{$www_url}admin/compta/pie.php?g=recettes" />
+        <img src="{$www_url}admin/compta/pie.php?g=depenses" />
+    </p>
+</div>
+
+{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 (file)
index 0000000..015be37
--- /dev/null
@@ -0,0 +1,77 @@
+{include file="admin/_head.tpl" title="Suivi des opérations" current="compta/gestion"}
+
+<ul class="actions">
+    <li class="recettes{if $type == Garradin\Compta_Categories::RECETTES} current{/if}"><a href="{$www_url}admin/compta/operations/?recettes">Recettes</a></li>
+    <li class="depenses{if $type == Garradin\Compta_Categories::DEPENSES} current{/if}"><a href="{$www_url}admin/compta/operations/?depenses">Dépenses</a></li>
+    <li class="autres{if $type == Garradin\Compta_Categories::AUTRES} current{/if}"><a href="{$www_url}admin/compta/operations/?autres">Autres</a></li>
+    {*<li><a href="{$www_url}admin/compta/operations/recherche.php">Recherche d'opération</a></li>*}
+    {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$www_url}admin/compta/operations/recherche_sql.php">Recherche par requête SQL</a></li>
+    {/if}
+</ul>
+
+{if $type != Garradin\Compta_Categories::AUTRES}
+<form method="get" action="{$self_url}">
+    <fieldset>
+        <legend>Filtrer par catégorie</legend>
+        <select name="cat" onchange="if (!this.value) location.href = '?{if $type == Garradin\Compta_Categories::RECETTES}recettes{else}depenses{/if}'; else this.form.submit();">
+            <option value="">-- Toutes</option>
+        {foreach from=$liste_cats item="cat"}
+            <option value="{$cat.id|escape}"{if $cat.id == $categorie.id} selected="selected"{/if}>{$cat.intitule|escape}</option>
+        {/foreach}
+        </select>
+        <input type="submit" value="OK" />
+    </fieldset>
+</form>
+{/if}
+
+<table class="list">
+    <colgroup>
+        <col width="3%" />
+        <col width="3%" />
+        <col width="12%" />
+        <col width="10%" />
+        <col />
+        {if !$categorie && $type}
+        <col width="20%" />
+        {/if}
+    </colgroup>
+    <tbody>
+    {foreach from=$journal item="ligne"}
+        <tr>
+            <td class="num"><a href="{$admin_url}compta/operations/voir.php?id={$ligne.id|escape}">{$ligne.id|escape}</a></td>
+            <td class="actions">
+            {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}
+                <a class="icn" href="{$admin_url}compta/operations/modifier.php?id={$ligne.id|escape}" title="Modifier cette opération">✎</a>
+            {/if}
+            </td>
+            <td>{$ligne.date|date_fr:'d/m/Y'|escape}</td>
+            <td>{$ligne.montant|html_money} {$config.monnaie|escape}</td>
+            <th>{$ligne.libelle|escape}</th>
+            {if !$categorie && $type}
+            <td>{$ligne.categorie|escape}</td>
+            {/if}
+        </tr>
+    {foreachelse}
+        <tr>
+            <td colspan="3"></td>
+            <td colspan="2">
+                Aucune opération.
+            </td>
+            {if !$categorie && $type}<td></td>{/if}
+        </tr>
+    {/foreach}
+    </tbody>
+    <tfoot>
+        <tr>
+            <td></td>
+            <td></td>
+            <th>Total</th>
+            <td>{$total|html_money} {$config.monnaie|escape}</td>
+            <td></td>
+            {if !$categorie && $type}<td></td>{/if}
+        </tr>
+    </tfoot>
+</table>
+
+{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 (file)
index 0000000..260e8bf
--- /dev/null
@@ -0,0 +1,76 @@
+{include file="admin/_head.tpl" title="Écritures réalisées par le membre" current="compta/gestion"}
+
+<ul class="actions">
+    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}"><b>{$membre.identite|escape}</b></a></li>
+    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id|escape}">Modifier</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id|escape}">Supprimer</a></li>
+    {/if}
+    <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id|escape}">Suivi des cotisations</a></li>
+</ul>
+
+<form method="get" action="{$self_url|escape}">
+    <fieldset>
+        <legend>Exercice à visualiser</legend>
+        <p>
+            <input type="hidden" name="id" value="{$membre.id|escape}" />
+            <select name="exercice" id="f_exercice" onchange="this.form.submit();">
+                {foreach from=$exercices item="e"}
+                <option value="{$e.id|escape}" {form_field name="exercice" selected=$e.id default=$exercice}>{$e.libelle} —
+                {if $e.cloture}Clôturé{else}En cours{/if}
+                — Du {$e.debut|date_fr:'d/m/Y'} au {$e.fin|date_fr:'d/m/Y'}
+                </option>
+                {/foreach}
+            </select>
+        </p>
+        <noscript>
+            <p>
+                <input type="submit" value="Visualiser &rarr;" />
+            </p>
+        </noscript>
+    </fieldset>
+</form>
+
+{if empty($journal)}
+    <p class="alert">Aucune écriture comptable n'est associée à ce membre pour l'exercice demandé.</p>
+{else}
+<table class="list">
+    <colgroup>
+        <col width="3%" />
+        <col width="3%" />
+        <col width="12%" />
+        <col width="10%" />
+        <col />
+    </colgroup>
+    <thead>
+        <tr>
+            <td></td>
+            <td></td>
+            <td>Date</td>
+            <td>Montant</td>
+            <th>Libellé</th>
+            <td>Compte débité</td>
+            <td>Compte crédité</td>
+        </tr>
+    </thead>
+    <tbody>
+    {foreach from=$journal item="ligne"}
+        <tr>
+            <td><a href="{$admin_url}compta/operations/voir.php?id={$ligne.id|escape}">{$ligne.id|escape}</a></td>
+            <td class="actions">
+            {if $user.droits.compta >= Garradin\Membres::DROIT_ADMIN}
+                <a class="icn" href="{$admin_url}compta/operations/modifier.php?id={$ligne.id|escape}" title="Modifier cette opération">✎</a>
+            {/if}
+            </td>
+            <td>{$ligne.date|format_sqlite_date_to_french|escape}</td>
+            <td>{$ligne.montant|html_money}</td>
+            <th>{$ligne.libelle|escape}</th>
+            <td>{$ligne.compte_debit|escape} — {$ligne.compte_debit|get_nom_compte}</td>
+            <td>{$ligne.compte_credit|escape} — {$ligne.compte_credit|get_nom_compte}</td>
+        </tr>
+    {/foreach}
+    </tbody>
+</table>
+{/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 (file)
index 0000000..9d6e2f2
--- /dev/null
@@ -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}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Informations sur l'opération</legend>
+        <dl>
+            <dt><label for="f_date">Date</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="date" name="date" id="f_date" value="{form_field name=date default=$operation.date|date_fr:'Y-m-d'}" size="10" required="required" /></dd>
+            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle data=$operation}" required="required" /></dd>
+            <dt><label for="f_montant">Montant</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="number" size="5" name="montant" id="f_montant" value="{form_field name=montant data=$operation}" min="0.00" step="0.01" required="required" /> {$config.monnaie|escape}</dd>
+
+{if is_null($type)}
+            <dt><label for="f_compte_debit">Compte débité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                {select_compte comptes=$comptes name="compte_debit" data=$operation}
+            </dd>
+            <dt><label for="f_compte_credit">Compte crédité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                {select_compte comptes=$comptes name="compte_credit" data=$operation}
+            </dd>
+{else}
+            <dt><label for="f_moyen_paiement">Moyen de paiement</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="moyen_paiement" id="f_moyen_paiement">
+                {foreach from=$moyens_paiement item="moyen"}
+                    <option value="{$moyen.code|escape}"{if $moyen.code == $operation.moyen_paiement} selected="selected"{/if}>{$moyen.nom|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+            <dt class="f_cheque"><label for="f_numero_cheque">Numéro de chèque</label></dt>
+            <dd class="f_cheque"><input type="text" name="numero_cheque" id="f_numero_cheque" value="{form_field name=numero_cheque data=$operation}" /></dd>
+            <dt class="f_banque"><label for="f_banque">Compte bancaire</label></dt>
+            <dd class="f_banque">
+                <select name="banque" id="f_banque">
+                {foreach from=$comptes_bancaires item="compte"}
+                    <option value="{$compte.id|escape}"{if ($type == Garradin\Compta_Categories::DEPENSES && $compte.id == $operation.compte_credit) || $compte.id == $operation.compte_debit} selected="selected"{/if}>{$compte.libelle|escape} - {$compte.banque|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+{/if}
+
+            <dt><label for="f_numero_piece">Numéro de pièce comptable</label></dt>
+            <dd><input type="text" name="numero_piece" id="f_numero_piece" value="{form_field name=numero_piece data=$operation}" /></dd>
+            <dt><label for="f_remarques">Remarques</label></dt>
+            <dd><textarea name="remarques" id="f_remarques" rows="4" cols="30">{form_field name=remarques data=$operation}</textarea></dd>
+        </dl>
+    </fieldset>
+
+{if !is_null($type)}
+    <fieldset>
+        <legend>Catégorie</legend>
+        <dl class="catList">
+        {foreach from=$categories item="cat"}
+            <dt>
+                <input type="radio" name="id_categorie" value="{$cat.id|escape}" id="f_cat_{$cat.id|escape}" {form_field name="id_categorie" checked=$cat.id data=$operation} />
+                <label for="f_cat_{$cat.id|escape}">{$cat.intitule|escape}</label>
+            </dt>
+            {if !empty($cat.description)}
+                <dd class="desc">{$cat.description|escape}</dd>
+            {/if}
+        {/foreach}
+        </dl>
+    </fieldset>
+
+    <script type="text/javascript">
+    {literal}
+    (function () {
+
+        window.changeMoyenPaiement = function()
+        {
+            var elm = $('#f_moyen_paiement');
+            toggleElementVisibility('.f_cheque', elm.value == 'CH');
+            toggleElementVisibility('.f_banque', elm.value != 'ES');
+        };
+
+        changeMoyenPaiement();
+
+        $('#f_moyen_paiement').onchange = changeMoyenPaiement;
+    } ());
+    {/literal}
+    </script>
+{/if}
+
+    <p class="submit">
+        {csrf_field key="compta_modifier_`$operation.id`"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..7e1296e
--- /dev/null
@@ -0,0 +1,61 @@
+{include file="admin/_head.tpl" title="Recherche par requête SQL" current="compta"}
+
+<form method="get" action="{$admin_url}compta/operations/recherche_sql.php">
+    <fieldset>
+        <legend>Schéma des tables SQL</legend>
+        <pre class="sql_schema">{$schema.journal|escape}</pre>
+        <dl>
+            <dt><label for="f_query">Requête SQL</label></dt>
+            <dd class="help">Si aucune limite n'est précisée, une limite de 100 résultats sera appliquée.</dd>
+            <dd><textarea name="query" id="f_query" cols="50" rows="7" required="required">{$query|escape}</textarea></dd>
+        </dl>
+        <p class="submit">
+            <input type="submit" value="Exécuter &rarr;" />
+        </p>
+    </fieldset>
+</form>
+
+{if !empty($error)}
+<p class="error">
+    <strong>Erreur dans la requête SQL :</strong><br />
+    {$error|escape}
+</p>
+{/if}
+
+{if !empty($result)}
+<p class="alert">{$result|@count} résultats renvoyés.</p>
+<table class="list search">
+    <thead>
+        {foreach from=$result[0] key="col" item="ignore"}
+            <td>{$col|escape}</td>
+        {/foreach}
+        {if array_key_exists('id', $result[0])}
+        <td></td>
+        {/if}
+    </thead>
+    <tbody>
+        {foreach from=$result item="row"}
+            <tr>
+                {foreach from=$row item="col"}
+                    <td>{$col|escape}</td>
+                {/foreach}
+                {if array_key_exists('id', $result[0])}
+                <td class="actions">
+                    {if !empty($row.id)}
+                    <a class="icn" href="voir.php?id={$row.id|escape}" title="Fiche opération">❓</a>
+                    <a class="icn" href="modifier.php?id={$row.id|escape}" title="Modifier cette opération">✎</a>
+                    {/if}
+                </td>
+                {/if}
+            </tr>
+        {/foreach}
+    </tbody>
+</table>
+
+{else}
+<p class="alert">
+    Aucun résultat trouvé.
+</p>
+{/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 (file)
index 0000000..30c103e
--- /dev/null
@@ -0,0 +1,143 @@
+{include file="admin/_head.tpl" title="Saisie d'une opération" current="compta/saisie" js=1}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+{if $ok}
+    <p class="confirm">
+        L'opération numéro <a href="{$www_url}admin/compta/operations/voir.php?id={$ok|escape}">{$ok|escape}</a> a été ajoutée.
+        (<a href="{$www_url}admin/compta/operations/voir.php?id={$ok|escape}">Voir l'opération</a>)
+    </p>
+{/if}
+
+<ul class="actions">
+    <li{if $type == Garradin\Compta_Categories::RECETTES} class="current"{/if}><a href="{$www_url}admin/compta/operations/saisir.php?recette">Recette</a></li>
+    <li{if $type == Garradin\Compta_Categories::DEPENSES} class="current"{/if}><a href="{$www_url}admin/compta/operations/saisir.php?depense">Dépense</a></li>
+    <li{if $type === 'virement'} class="current"{/if}><a href="{$www_url}admin/compta/operations/saisir.php?virement">Virement interne</a></li>
+    <li{if $type === 'dette'} class="current"{/if}><a href="{$www_url}admin/compta/operations/saisir.php?dette">Dette</a></li>
+    <li{if is_null($type)} class="current"{/if}><a href="{$www_url}admin/compta/operations/saisir.php?avance">Saisie avancée</a></li>
+</ul>
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Informations sur l'opération</legend>
+        <dl>
+            <dt><label for="f_date">Date</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="date" name="date" id="f_date" value="{form_field name=date default=$date}" size="10" required="required" /></dd>
+            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle}" required="required" /></dd>
+            <dt><label for="f_montant">Montant</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="number" size="5" name="montant" id="f_montant" value="{form_field name=montant default=0.00}" min="0.00" step="0.01" required="required" /> {$config.monnaie|escape}</dd>
+
+{if is_null($type)}
+            <dt><label for="f_compte_debit">Compte débité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                {select_compte comptes=$comptes name="compte_debit"}
+            </dd>
+            <dt><label for="f_compte_credit">Compte crédité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                {select_compte comptes=$comptes name="compte_credit"}
+            </dd>
+{elseif $type === 'virement'}
+            <dt><label for="f_compte1">Compte débité</label></dt>
+            <dd>
+                <select name="compte1" id="f_compte1">
+                    <option value="{Garradin\Compta_Comptes::CAISSE}">Caisse</option>
+                {foreach from=$comptes_bancaires item="compte"}
+                    <option value="{$compte.id|escape}"{if $compte.id == $banque} selected="selected"{/if}>{$compte.libelle|escape} - {$compte.banque|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+            <dt><label for="f_compte2">Compte crédité</label></dt>
+            <dd>
+                <select name="compte2" id="f_compte2">
+                    <option value="{Garradin\Compta_Comptes::CAISSE}">Caisse</option>
+                {foreach from=$comptes_bancaires item="compte"}
+                    <option value="{$compte.id|escape}"{if $compte.id == $banque} selected="selected"{/if}>{$compte.libelle|escape} - {$compte.banque|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+{elseif $type === 'dette'}
+            <dt><label for="f_compte_usager">Type de dette</label></dt>
+            <dd>
+                <input type="radio" name="compte" id="f_compte_usager" value="4110" {form_field name=compte checked=4110 default=4110} />
+                <label for="f_compte_usager">Dette envers un membre ou usager</label>
+            </dd>
+            <dd>
+                <input type="radio" name="compte" id="f_compte_fournisseur" value="4010" {form_field name=compte checked=4010} />
+                <label for="f_compte_fournisseur">Dette envers un fournisseur</label>
+            </dd>
+{else}
+            <dt><label for="f_moyen_paiement">Moyen de paiement</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="moyen_paiement" id="f_moyen_paiement" required="required">
+                {foreach from=$moyens_paiement item="moyen"}
+                    <option value="{$moyen.code|escape}"{if $moyen.code == $moyen_paiement} selected="selected"{/if}>{$moyen.nom|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+            <dt class="f_cheque"><label for="f_numero_cheque">Numéro de chèque</label></dt>
+            <dd class="f_cheque"><input type="text" name="numero_cheque" id="f_numero_cheque" value="{form_field name=numero_cheque}" /></dd>
+            <dt class="f_banque"><label for="f_banque">Compte bancaire</label></dt>
+            <dd class="f_banque">
+                <select name="banque" id="f_banque">
+                {foreach from=$comptes_bancaires item="compte"}
+                    <option value="{$compte.id|escape}"{if $compte.id == $banque} selected="selected"{/if}>{$compte.libelle|escape} - {$compte.banque|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+{/if}
+            <dt><label for="f_numero_piece">Numéro de pièce comptable</label></dt>
+            <dd><input type="text" name="numero_piece" id="f_numero_piece" value="{form_field name=numero_piece}" /></dd>
+            <dt><label for="f_remarques">Remarques</label></dt>
+            <dd><textarea name="remarques" id="f_remarques" rows="4" cols="30">{form_field name=remarques}</textarea></dd>
+        </dl>
+    </fieldset>
+
+{if $type == Garradin\Compta_Categories::DEPENSES || $type == Garradin\Compta_Categories::RECETTES || $type == 'dette'}
+    <fieldset>
+        <legend>Catégorie</legend>
+        <dl class="catList">
+        {foreach from=$categories item="cat"}
+            <dt>
+                <input type="radio" name="categorie" value="{$cat.id|escape}" id="f_cat_{$cat.id|escape}" {form_field name="categorie" checked=$cat.id} />
+                <label for="f_cat_{$cat.id|escape}">{$cat.intitule|escape}</label>
+            </dt>
+            {if !empty($cat.description)}
+                <dd class="desc">{$cat.description|escape}</dd>
+            {/if}
+        {/foreach}
+        </dl>
+    </fieldset>
+
+    <script type="text/javascript">
+    {literal}
+    (function () {
+
+        window.changeMoyenPaiement = function()
+        {
+            var elm = $('#f_moyen_paiement');
+            toggleElementVisibility('.f_cheque', elm.value == 'CH');
+            toggleElementVisibility('.f_banque', elm.value != 'ES');
+        };
+
+        changeMoyenPaiement();
+
+        $('#f_moyen_paiement').onchange = changeMoyenPaiement;
+    } ());
+    {/literal}
+    </script>
+{/if}
+
+    <p class="submit">
+        {csrf_field key="compta_saisie"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..a9b7006
--- /dev/null
@@ -0,0 +1,26 @@
+{include file="admin/_head.tpl" title="Supprimer une opération" current="compta/gestion"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Supprimer cette opération ?</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer l'opération n°{$operation.id|escape}
+            «&nbsp;{$operation.libelle|escape}&nbsp;» du {$operation.date|date_fr:'d/m/Y'} ?
+        </h3>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="compta_supprimer_`$operation.id`"}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..2d9e37a
--- /dev/null
@@ -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}
+<ul class="actions">
+    <li class="edit"><a href="{$admin_url}compta/operations/modifier.php?id={$operation.id|escape}">Modifier cette opération</a></li>
+    <li class="delete"><a href="{$admin_url}compta/operations/supprimer.php?id={$operation.id|escape}">Supprimer cette opération</a></li>
+</ul>
+{/if}
+
+<dl class="describe">
+    <dt>Date</dt>
+    <dd>{$operation.date|date_fr:'l j F Y (d/m/Y)'}</dd>
+    <dt>Libellé</dt>
+    <dd>{$operation.libelle|escape}</dd>
+    <dt>Montant</dt>
+    <dd>{$operation.montant|html_money}&nbsp;{$config.monnaie|escape}</dd>
+    <dt>Numéro pièce comptable</dt>
+    <dd>{if trim($operation.numero_piece)}{$operation.numero_piece|escape}{else}<em>Non renseigné</em>{/if}</dd>
+
+    {if $operation.id_categorie}
+
+        <dt>Moyen de paiement</dt>
+        <dd>{if trim($operation.moyen_paiement)}{$moyen_paiement|escape}{else}<em>Non renseigné</em>{/if}</dd>
+
+        {if $operation.moyen_paiement == 'CH'}
+            <dt>Numéro de chèque</dt>
+            <dd>{if trim($operation.numero_cheque)}{$operation.numero_cheque|escape}{else}<em>Non renseigné</em>{/if}</dd>
+        {/if}
+
+        {if $operation.moyen_paiement && $operation.moyen_paiement != 'ES'}
+            <dt>Compte bancaire</dt>
+            <dd>{$compte|escape}</dd>
+        {/if}
+
+        <dt>Catégorie</dt>
+        <dd>
+            <a href="{$www_url}admin/compta/operations/?{if $categorie.type == Garradin\Compta_Categories::DEPENSES}depenses{else}recettes{/if}">{if $categorie.type == Garradin\Compta_Categories::DEPENSES}Dépense{else}Recette{/if}</a>&nbsp;:
+            <a href="{$www_url}admin/compta/operations/?cat={$operation.id_categorie|escape}">{$categorie.intitule|escape}</a>
+        </dd>
+    {/if}
+
+    <dt>Exercice</dt>
+    <dd>
+        <a href="{$www_url}admin/compta/exercices/">{$exercice.libelle|escape}</a>
+        | Du {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'}
+        | <strong>{if $exercice.cloture}Clôturé{else}En cours{/if}</strong>
+    </dd>
+
+    <dt>Opération créée par</dt>
+    <dd>
+        {if $operation.id_auteur}
+            {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES}
+                <a href="{$www_url}admin/membres/fiche.php?id={$operation.id_auteur|escape}">{$nom_auteur|escape}</a>
+            {else}
+                {$nom_auteur|escape}
+            {/if}
+        {else}
+            <em>membre supprimé</em>
+        {/if}
+    </dd>
+
+    <dt>Remarques</dt>
+    <dd>{if trim($operation.remarques)}{$operation.remarques|escape}{else}Non renseigné{/if}</dd>
+</dl>
+
+<table class="list multi">
+    <thead>
+        <tr>
+            <th colspan="2">Comptes</th>
+            <td>Débit</td>
+            <td>Crédit</td>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td><a href="{$admin_url}compta/comptes/journal.php?id={$operation.compte_debit|escape}">{$operation.compte_debit|escape}</a></td>
+            <td>{$nom_compte_debit}</td>
+            <td>{$operation.montant|html_money}&nbsp;{$config.monnaie|escape}</td>
+            <td></td>
+        </tr>
+        <tr>
+            <td><a href="{$admin_url}compta/comptes/journal.php?id={$operation.compte_credit|escape}">{$operation.compte_credit|escape}</a></td>
+            <td>{$nom_compte_credit}</td>
+            <td></td>
+            <td>{$operation.montant|html_money}&nbsp;{$config.monnaie|escape}</td>
+        </tr>
+    </tbody>
+</table>
+
+{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 (file)
index 0000000..4881e2b
--- /dev/null
@@ -0,0 +1,8 @@
+<ul class="actions">
+    <li{if $current == 'index'} class="current"{/if}><a href="{$www_url}admin/config/">Général</a></li>
+    <li{if $current == 'membres'} class="current"{/if}><a href="{$www_url}admin/config/membres.php">Fiche des membres</a></li>
+    <li{if $current == 'site'} class="current"{/if}><a href="{$www_url}admin/config/site.php">Site public</a></li>
+    <li{if $current == 'donnees'} class="current"{/if}><a href="{$www_url}admin/config/donnees.php">Données&nbsp;: sauvegarde et restauration</a></li>
+    <li{if $current == 'import'} class="current"{/if}><a href="{$www_url}admin/config/import.php">Import &amp; export</a></li>
+    <li{if $current == 'plugins'} class="current"{/if}><a href="{$www_url}admin/config/plugins.php">Extensions</a></li>
+</ul>
diff --git a/templates/admin/config/donnees.tpl b/templates/admin/config/donnees.tpl
new file mode 100644 (file)
index 0000000..cf4fe88
--- /dev/null
@@ -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}
+    <p class="error">{$error|escape}</p>
+{elseif $ok}
+    <p class="confirm">
+        {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 <em>date-du-jour.avant_restauration.sqlite</em>, sinon vous pouvez l'effacer.
+        {elseif $ok == 'remove'}La sauvegarde a été supprimée.
+        {/if}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+<fieldset>
+    <legend>Sauvegarde automatique</legend>
+    <p class="help">
+        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é.
+    </p>
+    <dl>
+        <dt><label for="f_frequency">Intervalle de sauvegarde</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+        <dd>
+            <select name="frequence_sauvegardes" required="required" id="f_frequency">
+                <option value="0"{form_field name=frequence_sauvegardes data=$config selected=0}>Aucun — les sauvegardes automatiques sont désactivées</option>
+                <option value="1"{form_field name=frequence_sauvegardes data=$config selected=1}>Quotidien, tous les jours</option>
+                <option value="7"{form_field name=frequence_sauvegardes data=$config selected=7}>Hebdomadaire, tous les 7 jours</option>
+                <option value="15"{form_field name=frequence_sauvegardes data=$config selected=15}>Bimensuel, tous les 15 jours</option>
+                <option value="30"{form_field name=frequence_sauvegardes data=$config selected=30}>Mensuel</option>
+                <option value="90"{form_field name=frequence_sauvegardes data=$config selected=90}>Trimestriel</option>
+                <option value="365{form_field name=frequence_sauvegardes data=$config selected=365}">Annuel</option>
+            </select>
+        </dd>
+        <dt><label for="f_max_backups">Nombre de sauvegardes conservées</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+        <dd class="help">
+            Par exemple avec l'intervalle mensuel, en indiquant de conserver 12 sauvegardes,
+            vous pourrez garder un an d'historique de sauvegardes.
+        </dd>
+        <dd class="help">
+            <strong>Attention :</strong> si vous choisissez un nombre important et un intervalle réduit,
+            l'espace disque occupé par vos sauvegardes va rapidement augmenter.
+        </dd>
+        <dd><input type="number" name="nombre_sauvegardes" value="{form_field name=nombre_sauvegardes data=$config}" if="f_max_backups" min="1" max="90" required="required" /></dd>
+    </dl>
+    <p>
+        {csrf_field key="backup_config"}
+        <input type="submit" name="config" value="Enregistrer &rarr;" />
+    </p>
+</fieldset>
+
+</form>
+<form method="post" action="{$self_url|escape}">
+
+<fieldset>
+    <legend>Sauvegarde manuelle</legend>
+    <p>
+        {csrf_field key="backup_create"}
+        <input type="submit" name="create" value="Créer une nouvelle sauvegarde des données &rarr;" />
+    </p>
+</fieldset>
+
+</form>
+<form method="post" action="{$self_url|escape}">
+
+<fieldset>
+    <legend>Copies de sauvegarde disponibles</legend>
+    {if empty($liste)}
+        <p class="help">Aucune copie de sauvegarde disponible.</p>
+    {else}
+        <dl>
+        {foreach from=$liste key="f" item="d"}
+            <dd>
+                <label>
+                    <input type="radio" name="file" value="{$f|escape}" />
+                    {$f|escape} — {$d|date_fr:'d/m/Y à H:i'}
+                </label>
+            </dd>
+        {/foreach}
+        </dl>
+        <p class="alert">
+            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.
+        </p>
+        <p>
+            {csrf_field key="backup_manage"}
+            <input type="submit" name="restore" value="Restaurer cette sauvegarde" />
+            <input type="submit" name="remove" value="Supprimer cette sauvegarde" />
+        </p>
+    {/if}
+</fieldset>
+
+</form>
+<form method="post" action="{$self_url|escape}">
+
+<fieldset>
+    <legend>Téléchargement</legend>
+    <p>
+        {csrf_field key="backup_download"}
+        <input type="submit" name="download" value="Télécharger une copie des données sur mon ordinateur" />
+    </p>
+</fieldset>
+
+</form>
+<form method="post" action="{$self_url|escape}" enctype="multipart/form-data">
+
+<fieldset>
+    <legend><label for="f_file">Restaurer depuis un fichier</label></legend>
+    <p class="alert">
+        Attention, l'intégralité des données courantes seront effacées et remplacées par celles
+        contenues dans le fichier fourni.
+    </p>
+    <p class="help">
+        Une sauvegarde des données courantes sera effectuée avant le remplacement,
+        en cas de besoin d'annuler cette restauration.
+    </p>
+    <p>
+        {csrf_field key="backup_restore"}
+        <input type="hidden" name="MAX_FILE_SIZE" value="{$max_file_size|escape}" />
+        <input type="file" name="file" id="f_file" required="required" />
+        (maximum {$max_file_size|format_bytes})
+        <input type="submit" name="restore_file" value="Restaurer depuis le fichier sélectionné &rarr;" />
+    </p>
+</fieldset>
+
+</form>
+
+{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 (file)
index 0000000..4de8e90
--- /dev/null
@@ -0,0 +1,16 @@
+{include file="admin/_head.tpl" title="Import & export" current="config"}
+
+{include file="admin/config/_menu.tpl" current="import"}
+
+<fieldset>
+<dl>
+       <dt>Membres</dt>
+    <dd><a href="{$admin_url}membres/import.php">Import de la liste des membres</a></dd>
+    <dd><a href="{$admin_url}membres/import.php?export">Export de la liste des membres en CSV (pour tableurs)</a></dd>
+    <dt>Comptabilité</dt>
+    <dd><a href="{$admin_url}compta/import.php">Import des données comptables</a></dd>
+    <dd><a href="{$admin_url}compta/import.php?export">Export des données comptables en CSV</a></dd>
+</dl>
+</fieldset>
+
+{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 (file)
index 0000000..2ed36b3
--- /dev/null
@@ -0,0 +1,121 @@
+{include file="admin/_head.tpl" title="Configuration" current="config"}
+
+{if $error}
+    {if $error == 'OK'}
+    <p class="confirm">
+        La configuration a bien été enregistrée.
+    </p>
+    {else}
+    <p class="error">
+        {$error|escape}
+    </p>
+    {/if}
+{/if}
+
+{include file="admin/config/_menu.tpl" current="index"}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Garradin</legend>
+        <dl>
+            <dt>Version installée</dt>
+            <dd class="help">{$garradin_version|escape} <a href="{Garradin\WEBSITE}">[Vérifier la disponibilité d'une nouvelle version]</a></dd>
+            <dt>Informations système</dt>
+            <dd class="help">PHP version {$php_version|escape} — SQLite version {$sqlite_version|escape}</dd>
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Informations sur l'association</legend>
+        <dl>
+            <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="nom_asso" id="f_nom_asso" required="required" value="{form_field data=$config name=nom_asso}" /></dd>
+            <dt><label for="f_email_asso">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="email" name="email_asso" id="f_email_asso" required="required" value="{form_field data=$config name=email_asso}" /></dd>
+            <dt><label for="f_adresse_asso">Adresse postale</label></dt>
+            <dd><textarea cols="50" rows="5" name="adresse_asso" id="f_adresse_asso">{form_field data=$config name=adresse_asso}</textarea></dd>
+            <dt><label for="f_site_asso">Site web</label></dt>
+            <dd><input type="url" name="site_asso" id="f_site_asso" value="{form_field name=site_asso data=$config}" /></dd>
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Localisation</legend>
+        <dl>
+            <dt><label for="f_monnaie">Monnaie</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="monnaie" id="f_monnaie" required="required" value="{form_field name=monnaie data=$config}" size="5" /></dd>
+            <dt><label for="f_pays">Pays</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="pays" id="f_pays" required="required">
+                {foreach from=$pays key="cc" item="nom"}
+                    <option value="{$cc|escape}"{if $cc == $config.pays} selected="selected"{/if}>{$nom|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Envois par E-Mail</legend>
+        <dl>
+            <dt><label for="f_email_envoi_automatique">Adresse E-Mail expéditeur des messages automatiques</label></dt>
+            <dd><input type="text" name="email_envoi_automatique" id="f_email_envoi_automatique" value="{form_field data=$config name=email_envoi_automatique}" /></dd>
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Wiki</legend>
+        <dl>
+            <dt><label for="f_accueil_wiki">Page d'accueil du wiki</label> 
+                <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>Indiquer ici l'adresse unique de la page qui sera utilisée comme page d'accueil du wiki.</dd>
+            <dd><input type="text" name="accueil_wiki" id="f_accueil_wiki" required="required" value="{form_field data=$config name=accueil_wiki}" /></dd>
+            <dt><label for="f_accueil_connexion">Page d'accueil à la connexion</label> 
+                <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>Indiquer ici l'adresse unique de la page qui sera affichée à la connexion d'un membre.</dd>
+            <dd><input type="text" name="accueil_connexion" id="f_accueil_connexion" required="required" value="{form_field data=$config name=accueil_connexion}" /></dd>
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Membres</legend>
+        <dl>
+            <dt><label for="f_categorie_membres">Catégorie par défaut des nouveaux membres</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="categorie_membres" required="required" id="f_categorie_membres">
+                {foreach from=$membres_cats key="id" item="nom"}
+                    <option value="{$id|escape}"{if $config.categorie_membres == $id} selected="selected"{/if}>{$nom|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+            <dt><label for="f_champ_identite">Champ utilisé pour définir l'identité des membres</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="help">Ce champ des fiches membres sera utilisé comme identité du membre dans les emails, les fiches, les pages, etc.</dd>
+            <dd>
+                <select name="champ_identite" required="required" id="f_champ_identite">
+                    {foreach from=$champs key="c" value="champ"}
+                        <option value="{$c|escape}" {form_field selected=$c name="champ_identite" data=$config}>{$champ.title|escape}</option>
+                    {/foreach}
+                </select>
+            </dd>
+            <dt><label for="f_champ_identifiant">Champ utilisé comme identifiant de connexion</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="help">Ce champ des fiches membres sera utilisé en guise d'identifiant pour se connecter à Garradin. Pour cela le champ doit être unique (pas de doublons).</dd>
+            <dd>
+                <select name="champ_identifiant" required="required" id="f_champ_identifiant">
+                    {foreach from=$champs key="c" value="champ"}
+                        <option value="{$c|escape}" {form_field selected=$c name="champ_identifiant" data=$config}>{$champ.title|escape}</option>
+                    {/foreach}
+                </select>
+            </dd>
+            
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="config"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..8a6649b
--- /dev/null
@@ -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'}
+    <p class="confirm">
+        La configuration a bien été enregistrée.
+    </p>
+    {elseif $error == 'ADD_OK'}
+    <p class="confirm">
+        Le champ a été ajouté à la fin de la liste.
+    </p>
+    {else}
+    <p class="error">
+        {$error|escape}
+    </p>
+    {/if}
+{/if}
+
+{if $review}
+    <fieldset>
+        <legend>Fiche membre exemple</legend>
+        <dl>
+            {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)}
+                <dd>
+                    {if !empty($champ.private)}
+                        (Champ privé)
+                    {elseif empty($champ.editable)}
+                        (Non-modifiable par les membres)
+                    {/if}
+                </dd>
+                {/if}
+            {/foreach}
+        </dl>
+    </fieldset>
+
+    <fieldset id="f_passe">
+        <legend>Connexion</legend>
+        <dl>
+            <dt><label for="f_passe">Mot de passe</label>{if !empty($champs.passe.mandatory)} <b title="(Champ obligatoire)">obligatoire</b>{/if}</dt>
+            <dd><input type="password" id="f_passe" disabled="disabled" /></dd>
+            {if empty($champs.passe.editable) || !empty($champs.passe.private)}
+            <dd>
+                {if !empty($champs.passe.private)}
+                    (Champ privé)
+                {elseif empty($champs.passe.editable)}
+                    (Non-modifiable par les membres)
+                {/if}
+            </dd>
+            {/if}
+        </dl>
+    </fieldset>
+
+    <form method="post" action="{$admin_url}config/membres.php">
+        <p class="submit">
+            {csrf_field key="config_membres"}
+            <input type="submit" name="back" value="&larr; Retour à l'édition" class="minor" />
+            <input type="submit" name="reset" value="Annuler les changements" class="minor" />
+            <input type="submit" name="save" value="Enregistrer &rarr;" />
+        </p>
+    </form>
+{else}
+    <p class="help">
+        Cette page vous permet de personnaliser les fiches d'information des membres de l'association.<br />
+        <strong>Attention :</strong> 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.
+    </p>
+
+    <fieldset>
+        <legend>Champs non-personnalisables</legend>
+        <dl>
+            <dt>Numéro unique</dt>
+            <dd>Ce numéro identifie de manière unique chacun des membres. 
+                Il est incrémenté à chaque nouveau membre ajouté.</dd>
+            <dt>Catégorie</dt>
+            <dd>Identifie la catégorie du membre.</dd>
+            <dt>Date de dernière connexion</dt>
+            <dd>Mémorise la date de dernière connexion à l'administration de Garradin.</dd>
+            <dt>Date d'inscription</dt>
+            <dd>Enregistre la date de création de la fiche du membre.</dd>
+        </dl>
+    </fieldset>
+
+    {if !empty($presets)}
+    <form method="post" action="{$self_url|escape}">
+    <fieldset>
+        <legend>Ajouter un champ pré-défini</legend>
+        <p>
+            <select name="preset" required="required">
+                <option></option>
+                {foreach from=$presets key="name" item="preset"}
+                <option value="{$name|escape}">{$name|escape} &mdash; {$preset.title|escape}</option>
+                {/foreach}
+            </select>
+            <input type="hidden" name="{$csrf_name|escape}" value="{$csrf_value|escape}" />
+            <input type="submit" name="add" value="Ajouter ce champ à la fiche membre" />
+        </p>
+    </fieldset>
+    </form>
+    {/if}
+
+<form method="post" action="{$self_url|escape}">
+    <fieldset>
+        <legend>Ajouter un champ personnalisé</legend>
+        <dl>
+            <dt><label for="f_name">Nom unique</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="help">Ne peut comporter que des lettres minuscules et des tirets bas.</dd>
+            <dd><input type="text" name="new" id="f_name" value="{form_field name=new}" size="30" required="required" /></dd>
+            <dt><label for="f_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="new_title" id="f_title" value="{form_field name=new_title}" size="60" required="required" /></dd>
+            <dt><label for="f_type">Type de champ</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="new_type" id="f_type" required="required">
+                    {foreach from=$types key="type" item="nom"}
+                    <option value="{$type|escape}" {form_field name=new_type selected=$type}>{$nom|escape}</option>
+                    {/foreach}
+                </select>
+            </dd>
+        </dl>
+        <p>
+            <input type="hidden" name="{$csrf_name|escape}" value="{$csrf_value|escape}" />
+            <input type="submit" name="add" value="Ajouter ce champ à la fiche membre" />
+        </p>
+    </fieldset>
+</form>
+
+<form method="post" action="{$self_url|escape}">
+    <div id="orderFields">
+        {foreach from=$champs item="champ" key="nom"}
+        {if $nom == 'passe'}{continue}{/if}
+        <fieldset id="f_{$nom|escape}">
+            <legend>{$nom|escape}</legend>
+            <dl>
+                <dt><label>Type</label></dt>
+                <dd>{$champ.type|get_type}</dd>
+                <dt><label for="f_{$nom|escape}_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+                <dd><input type="text" name="champs[{$nom|escape}][title]" id="f_{$nom|escape}_title" value="{form_field data=$champs[$nom] name=title}" size="60" required="required" /></dd>
+                <dt><label for="f_{$nom|escape}_help">Aide</label></dt>
+                <dd><input type="text" name="champs[{$nom|escape}][help]" id="f_{$nom|escape}_help" value="{form_field data=$champs[$nom] name=help}" size="100" /></dd>
+                <dt><label><input type="checkbox" name="champs[{$nom|escape}][editable]" value="1" {form_field data=$champs[$nom] name=editable checked="1"} /> Modifiable par les membres</label></dt>
+                <dd class="help">Si coché, les membres pourront changer cette information depuis leur espace personnel.</dd>
+                <dt><label><input type="checkbox" name="champs[{$nom|escape}][mandatory]" value="1" {form_field data=$champs[$nom] name=mandatory checked="1"} /> Champ obligatoire</label></dt>
+                <dd class="help">Si coché, ce champ ne pourra rester vide.</dd>
+                <dt><label><input type="checkbox" name="champs[{$nom|escape}][private]" value="1" {form_field data=$champs[$nom] name=private checked="1"} /> Champ privé</label></dt>
+                <dd class="help">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.</dd>
+                {if $champ.type == 'select' || $champ.type == 'multiple'}
+                    <dt><label>Options disponibles</label></dt>
+                    {if $champ.type == 'multiple'}
+                        <dd class="help">Attention changer l'ordre des options peut avoir des effets indésirables.</dd>
+                    {else}
+                        <dd class="help">Attention renommer ou supprimer une option n'affecte pas ce qui a déjà été enregistré dans les fiches des membres.</dd>
+                    {/if}
+                    <dd>
+                        <{if $champ.type == 'multiple'}ol{else}ul{/if} class="options">
+                        {if !empty($champ.options)}
+                            {foreach from=$champ.options key="key" item="opt"}
+                                <li><input type="text" name="champs[{$nom|escape}][options][]" value="{$opt|escape}" size="50" /></li>
+                            {/foreach}
+                        {/if}
+                        {if $champ.type == 'select' || empty($champ.options) || count($champ.options) < 32}
+                            <li><input type="text" name="champs[{$nom|escape}][options][]" value="" size="50" /></li>
+                        {/if}
+                    </dd>
+                {/if}
+                <dt><label for="f_list_row">Numéro de colonne dans la liste des membres</label></dt>
+                <dd class="help">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.</dd>
+                <dd><input type="number" id="f_list_row" name="champs[{$nom|escape}][list_row]" min="0" max="10" value="{form_field data=$champs[$nom] name=list_row}" /></dd>
+            </dl>
+        </fieldset>
+        {/foreach}
+    </div>
+
+    <fieldset id="f_passe">
+        <legend>Mot de passe</legend>
+        <dl>
+            <dt><label><input type="checkbox" name="champs[passe][editable]" value="1" {form_field data=$champs.passe name=editable checked="1"} /> Modifiable par les membres</label></dt>
+            <dd class="help">Si coché, les membres pourront changer cette information depuis leur espace personnel.</dd>
+            <dt><label><input type="checkbox" name="champs[passe][mandatory]" value="1" {form_field data=$champs.passe name=mandatory checked="1"} /> Champ obligatoire</label></dt>
+            <dd class="help">Si coché, ce champ ne pourra rester vide.</dd>
+            <dt><label><input type="checkbox" name="champs[passe][private]" value="1" {form_field data=$champs.passe name=private checked="1"} /> Champ privé</label></dt>
+            <dd class="help">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.</dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        <input type="hidden" name="{$csrf_name|escape}" value="{$csrf_value|escape}" />
+        <input type="submit" name="reset" value="Annuler les changements" class="minor" />
+        <input type="submit" name="review" value="Enregistrer &rarr;" />
+        (un récapitulatif sera présenté et une confirmation sera demandée)
+    </p>
+</form>
+
+<script type="text/javascript">
+{literal}
+(function () {
+    if (!document.querySelector || !document.querySelectorAll)
+    {
+        return false;
+    }
+
+    var fields = document.querySelectorAll('#orderFields fieldset');
+
+    for (i = 0; i < fields.length; i++)
+    {
+        var field = fields[i];
+        field.querySelector('dl').style.display = 'none';
+
+        var legend = field.querySelector('legend');
+
+        legend.onclick = function () {
+            var content = this.parentNode.querySelector('dl');
+            if (content.style.display.toLowerCase() == 'none')
+                content.style.display = 'block';
+            else
+                content.style.display = 'none';
+        }
+
+        legend.className = 'interactive';
+        legend.title = 'Cliquer pour modifier ce champ';
+
+        var actions = document.createElement('div');
+        actions.className = 'actions';
+        field.appendChild(actions);
+
+        var up = document.createElement('a');
+        up.className = 'icn up';
+        up.innerHTML = '&uarr;';
+        up.title = 'Déplacer vers le haut';
+        up.onclick = function (e) {
+            var field = this.parentNode.parentNode;
+            var p = field.previousSibling;
+            while (p.nodeType == 3) { p = p.previousSibling; }
+            field.parentNode.insertBefore(field, p);
+            return false;
+        };
+        actions.appendChild(up);
+
+        var down = document.createElement('a');
+        down.className = 'icn down';
+        down.innerHTML = '&darr;';
+        down.title = 'Déplacer vers le bas';
+        down.onclick = function (e) {
+            var field = this.parentNode.parentNode;
+            var p = field.nextSibling;
+
+            if (!p.nextSibling)
+            {
+                field.parentNode.appendChild(field);
+            }
+            else
+            {
+                while (p.nodeType == 3) { p = p.nextSibling; }
+                p = p.nextSibling;
+                while (p.nodeType == 3) { p = p.nextSibling; }
+                field.parentNode.insertBefore(field, p);
+            }
+            return false;
+        };
+        actions.appendChild(down);
+
+        var edit = document.createElement('a');
+        edit.className = 'icn edit';
+        edit.innerHTML = '&#x270e;';
+        edit.title = 'Modifier ce champ';
+        edit.onclick = function (e) {
+            var content = this.parentNode.parentNode.querySelector('dl');
+            if (content.style.display.toLowerCase() == 'none')
+                content.style.display = 'block';
+            else
+                content.style.display = 'none';
+            return false;
+        };
+        actions.appendChild(edit);
+
+        if (field.id != 'f_email' && field.id != 'f_passe')
+        {
+            var rem = document.createElement('a');
+            rem.className = 'icn remove';
+            rem.innerHTML = '✘';
+            rem.title = 'Enlever ce champ de la fiche';
+            rem.onclick = function (e) {
+                if (!window.confirm('Êtes-vous sûr de supprimer ce champ des fiches de membre ?'))
+                {
+                    return false;
+                }
+
+                var field = this.parentNode.parentNode;
+                field.parentNode.removeChild(field);
+                return false;
+            };
+            actions.appendChild(rem);
+        }
+
+        if (field.querySelector('.options'))
+        {
+            var options = field.querySelectorAll('.options li');
+            var options_nb = options.length;
+
+            if (options[0].parentNode.tagName.toLowerCase() == 'ul')
+            {
+                // champ select
+                for (j = 0; j < options_nb; j++)
+                {
+                    var remove = document.createElement('input');
+                    remove.type = 'button';
+                    remove.className = 'icn';
+                    remove.value = '-';
+                    remove.title = 'Enlever cette option';
+                    remove.onclick = function (e) {
+                        var p = this.parentNode;
+                        p.parentNode.removeChild(p);
+                    };
+                    options[j].appendChild(remove);
+                }
+            }
+
+            var add = document.createElement('input');
+            add.type = 'button';
+            add.className = 'icn add';
+            add.value = '+';
+            add.title = 'Ajouter une option';
+            add.onclick = function (e) {
+                var p = this.parentNode.parentNode;
+                var options = p.querySelectorAll('li');
+                var new_option = this.parentNode.cloneNode(true);
+                var btn = new_option.querySelector('input.add');
+                new_option.getElementsByTagName('input')[0].value = '';
+
+                if (options.length >= 30)
+                {
+                    new_option.removeChild(btn);
+                }
+                else
+                {
+                    btn.onclick = this.onclick;
+                }
+
+                p.appendChild(new_option);
+                this.parentNode.removeChild(this);
+            };
+
+            options[options_nb - 1].appendChild(add);
+        }
+    }
+}());
+{/literal}
+</script>
+{/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 (file)
index 0000000..24b50d0
--- /dev/null
@@ -0,0 +1,107 @@
+{include file="admin/_head.tpl" title="Extensions" current="config"}
+
+{include file="admin/config/_menu.tpl" current="plugins"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+{if !empty($delete)}
+    <form method="post" action="{$self_url|escape}">
+
+        <fieldset>
+            <legend>Désinstaller une extension</legend>
+            <h3 class="warning">
+                Êtes-vous sûr de vouloir supprimer l'extension «&nbsp;{$plugin.nom|escape}&nbsp;» ?
+            </h3>
+            <p class="alert">
+                <strong>Attention</strong> : cette action est irréversible et effacera toutes les
+                données associées à l'extension.
+            </p>
+        </fieldset>
+
+        <p class="submit">
+            {csrf_field key="delete_plugin_`$plugin.id`"}
+            <input type="submit" name="delete" value="Désinstaller &rarr;" />
+        </p>
+    </form>
+{else}
+    {if !empty($liste_installes)}
+        <table class="list">
+            <thead>
+                <tr>
+                    <th>Extension</th>
+                    <td>Auteur</td>
+                    <td>Version installée</td>
+                    <td></td>
+                </tr>
+            </thead>
+            <tbody>
+                {foreach from=$liste_installes item="plugin"}
+                <tr>
+                    <th>
+                        <h4>{$plugin.nom|escape}</h4>
+                        <small>{$plugin.description|escape}</small>
+                    </th>
+                    <td>
+                        <a href="{$plugin.url|escape}" onclick="return !window.open(this.href);">{$plugin.auteur|escape}</a>
+                    </td>
+                    <td>
+                        {$plugin.version|escape}
+                    </td>
+                    <td class="actions">
+                        {if empty($plugin.system)}
+                            <a href="{$admin_url}config/plugins.php?delete={$plugin.id|escape}">Désinstaller</a>
+                        {/if}
+                        {if !empty($plugin.config)}
+                            {if empty($plugin.system)}|{/if}
+                            <a href="{plugin_url id=$plugin.id file="config.php"}">Configurer</a>
+                        {/if}
+                    </td>
+                </tr>
+                {/foreach}
+            </tbody>
+        </table>
+    {else}
+        <p class="help">
+            Aucune extension n'est installée.
+            Vous pouvez consulter <a href="{Garradin\WEBSITE}">le site de Garradin</a> pour obtenir
+            des extensions à télécharger.
+        </p>
+    {/if}
+
+    {if !empty($liste_telecharges)}
+    <form method="post" action="{$self_url|escape}">
+
+        <fieldset>
+            <legend>Extensions à installer</legend>
+            <dl>
+                {foreach from=$liste_telecharges item="plugin" key="id"}
+                <dt>
+                    <label>
+                        <input type="radio" name="to_install" value="{$id|escape}" />
+                        {$plugin.nom|escape}
+                    </label>
+                    (version {$plugin.version|escape})
+                </dt>
+                <dd>[<a href="{$plugin.url|escape}" onclick="return !window.open(this.href);">{$plugin.auteur|escape}</a>] {$plugin.description|escape}</dd>
+                {/foreach}
+            </dl>
+        </fieldset>
+
+        <p class="help">
+            Attention : installer une extension non officielle peut présenter des risques de sécurité
+            et de stabilité.
+        </p>
+
+        <p class="submit">
+            {csrf_field key="install_plugin"}
+            <input type="submit" name="install" value="Installer &rarr;" />
+        </p>
+    </form>
+    {/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 (file)
index 0000000..f919dd1
--- /dev/null
@@ -0,0 +1,53 @@
+{include file="admin/_head.tpl" title="Configuration — Site public" current="config"}
+
+{if $error && $error != 'OK'}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+{include file="admin/config/_menu.tpl" current="site"}
+
+{if isset($edit)}
+    <form method="post" action="{$self_url|escape}">
+        <h3>Éditer un squelette</h3>
+
+        {if $error == 'OK'}
+        <p class="confirm">
+            Modifications enregistrées.
+        </p>
+        {/if}
+
+        <fieldset class="skelEdit">
+            <legend>{$edit.file|escape}</legend>
+            <p>
+                <textarea name="content" cols="90" rows="50" id="f_content">{form_field name=content data=$edit}</textarea>
+            </p>
+        </fieldset>
+
+        <p class="submit">
+            {csrf_field key=$csrf_key}
+            <input type="submit" name="save" value="Enregistrer &rarr;" />
+        </p>
+
+    </form>
+
+    <script type="text/javascript" src="{$admin_url}static/code_editor.min.js"></script>
+    <script type="text/javascript">
+    var doc_url = "{$admin_url}doc/skel/";
+    var skel_list = {$sources_json};
+    var skel_current = "{$edit.file|escape}";
+    </script>
+    <script type="text/javascript" src="{$admin_url}static/skel_editor.js"></script>
+{else}
+    <div class="templatesList">
+        <h3>Squelettes du site</h3>
+        <ul>
+        {foreach from=$sources item="source"}
+            <li><a href="?edit={$source|escape:'url'}">{$source|escape}</a></li>
+        {/foreach}
+        </ul>
+    </div>
+{/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 (file)
index 0000000..1cb5aed
--- /dev/null
@@ -0,0 +1,43 @@
+{include file="admin/_head.tpl" title="Bonjour `$user.identite` !" current="home"}
+
+<div class="infos_asso">
+    <h3>{$config.nom_asso|escape}</h3>
+    {if !empty($config.adresse_asso)}
+    <p>
+        {$config.adresse_asso|escape|nl2br}
+    </p>
+    {/if}
+    {if !empty($config.email_asso)}
+    <p>
+        E-Mail : {mailto address=$config.email_asso}
+    </p>
+    {/if}
+    {if !empty($config.site_asso)}
+    <p>
+        Web : <a href="{$config.site_asso|escape}">{$config.site_asso|escape}</a>
+    </p>
+    {/if}
+</div>
+
+<ul class="actions">
+    <li><a href="{$admin_url}mes_infos.php">Modifier mes informations personnelles</a></li>
+    {if $cotisation}
+    <li>
+        {if !$cotisation.a_jour}
+            <b class="error">Cotisation en retard&nbsp;!</b>
+        {else}
+            <b class="confirm">Cotisation à jour</b>
+            {if $cotisation.expiration}
+                (expire le {$cotisation.expiration|format_sqlite_date_to_french})
+            {/if}
+        {/if}
+    </li>
+    {/if}
+    <li><a href="{$admin_url}mes_cotisations.php">Suivi de mes cotisations</a></li>
+</ul>
+
+<div class="wikiContent">
+    {$page.contenu.contenu|format_wiki|liens_wiki:'wiki/?'}
+</div>
+
+{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 (file)
index 0000000..c0857d4
--- /dev/null
@@ -0,0 +1,81 @@
+{include file="admin/_head.tpl" title="Garradin - Installation" js=1}
+
+{if $disabled}
+    <p class="error">Garradin est déjà installé.</p>
+{else}
+    <p class="intro">
+        Bienvenue dans Garradin !
+        Veuillez remplir les quelques informations suivantes pour terminer
+        l'installation.
+    </p>
+
+    {if !empty($error)}
+        <p class="error">{$error|escape}</p>
+    {/if}
+
+    <form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Informations sur l'association</legend>
+        <dl>
+            <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="nom_asso" id="f_nom_asso" required="required" value="{form_field name=nom_asso}" /></dd>
+            <dt><label for="f_email_asso">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="email" name="email_asso" id="f_email_asso" required="required" value="{form_field name=email_asso}" /></dd>
+            <dt><label for="f_adresse_asso">Adresse postale</label></dt>
+            <dd><textarea cols="50" rows="5" name="adresse_asso" id="f_adresse_asso">{form_field name=adresse_asso}</textarea></dd>
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Informations sur le premier membre</legend>
+        <dl>
+            <dt><label for="f_nom_membre">Nom et prénom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="nom_membre" id="f_nom_membre" required="required" value="{form_field name=nom_membre}" /></dd>
+            <dt><label for="f_cat_membre">Catégorie du membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="tip">Par exemple : bureau, conseil d'administration, présidente, trésorier, etc.</dd>
+            <dd><input type="text" name="cat_membre" id="f_cat_membre" required="required" value="{form_field name=cat_membre}" /></dd>
+            <dt><label for="f_email_membre">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="email" name="email_membre" id="f_email_membre" required="required" value="{form_field name=email_membre}" /></dd>
+            <dt><label for="f_passe_membre">Mot de passe</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="help">
+                Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr 
+                et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
+            </dd>
+            <dd class="help">
+                Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
+                <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="password_suggest" value="{$passphrase|escape}" />
+            </dd>
+            <dd><input type="password" name="passe_membre" id="f_passe_membre" value="{form_field name=passe_membre}" pattern=".{ldelim}5,{rdelim}" required="required" /></dd>
+            <dt><label for="f_repasse_membre">Encore le mot de passe</label> (vérification) <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="password" name="repasse_membre" id="f_repasse_membre" value="{form_field name=repasse_membre}" pattern=".{ldelim}5,{rdelim}" required="required" /></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="install"}
+        <input type="submit" id="f_submit" name="save" value="Terminer l'installation &rarr;" />
+    </p>
+
+    <script type="text/javascript" src="{$admin_url}static/password.js"></script>
+    <script type="text/javascript" src="{$admin_url}static/loader.js"></script>
+    <script type="text/javascript">
+    {literal}
+    initPasswordField('password_suggest', 'f_passe_membre', 'f_repasse_membre');
+    
+    var form = $('form')[0];
+    form.onsubmit = function () {
+        $('#f_submit').style.opacity = 0;
+        var loader = document.createElement('div');
+        loader.className = 'loader install';
+        loader.innerHTML = '<b>Garradin est en cours d\'installation…</b>';
+        $('#f_submit').parentNode.appendChild(loader);
+        animatedLoader(loader, 5);
+    };
+    {/literal}
+    </script>
+
+    </form>
+{/if}
+
+{include file="admin/_foot.tpl"}
\ No newline at end of file
diff --git a/templates/admin/login.tpl b/templates/admin/login.tpl
new file mode 100644 (file)
index 0000000..ce50855
--- /dev/null
@@ -0,0 +1,36 @@
+{include file="admin/_head.tpl" title="Connexion"}
+
+{if $error}
+    <p class="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}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Connexion</legend>
+        <dl>
+            <dt><label for="f_id">{$champ.title}</label></dt>
+            <dd><input type="text" name="id" id="f_id" value="{form_field name=id}" /></dd>
+            <dt><label for="f_passe">Mot de passe</label></dt>
+            <dd><input type="password" name="passe" id="f_passe" value="" /></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="login"}
+        <input type="submit" name="login" value="Se connecter &rarr;" />
+    </p>
+
+    <p class="help">
+        <a href="{$www_url}admin/password.php">Pas de mot de passe ou mot de passe perdu ?</a>
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..002e56e
--- /dev/null
@@ -0,0 +1,62 @@
+{include file="admin/_head.tpl" title="Action collective sur les membres" current="membres"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+    {foreach from=$selected item="id"}
+        <input type="hidden" name="selected[]" value="{$id|escape}" />
+    {/foreach}
+
+    </fieldset>
+
+    {if $action == 'move'}
+    <fieldset>
+        <legend>Changer la catégorie des {$nb_selected|escape} membres sélectionnés</legend>
+        <dl>
+            <dt><label for="f_cat">Nouvelle catégorie</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="id_categorie" id="f_cat">
+                    <option value="0" selected="selected">-- Pas de changement</option>
+                {foreach from=$membres_cats key="id" item="nom"}
+                    <option value="{$id|escape}">{$nom|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="membres_action"}
+        <input type="submit" name="move_ok" value="Enregistrer &rarr;" />
+    </p>
+
+    {elseif $action == 'delete'}
+    <fieldset>
+        <legend>Supprimer les membres sélectionnés ?</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer les {$nb_selected|escape} membres sélectionnés ?
+        </h3>
+        <p class="alert">
+            <strong>Attention</strong> : cette action est irréversible et effacera toutes les
+            données personnelles et l'historique de ces membres.
+        </p>
+        <p class="help">
+            Alternativement, il est aussi possible de déplacer les membres qui ne font plus
+            partie de l'association dans une catégorie «&nbsp;Anciens membres&nbsp;», plutôt
+            que de les effacer complètement.
+        </p>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="membres_action"}
+        <input type="submit" name="delete_ok" value="Oui, supprimer ces membres &rarr;" />
+    </p>
+    {/if}
+
+</form>
+
+{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 (file)
index 0000000..4028ebd
--- /dev/null
@@ -0,0 +1,67 @@
+{include file="admin/_head.tpl" title="Ajouter un membre" current="membres/ajouter" js=1}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Informations personnelles</legend>
+        <dl>
+            {foreach from=$champs item="champ" key="nom"}
+                {html_champ_membre config=$champ name=$nom}
+            {/foreach}
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Connexion</legend>
+        <dl>
+            <dt><label for="f_passe">Mot de passe</label>{if $champs.passe.mandatory} <b title="(Champ obligatoire)">obligatoire</b>{/if}</dt>
+            <dd class="help">
+                Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr 
+                et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
+            </dd>
+            <dd class="help">
+                Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
+                <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="password_suggest" value="{$passphrase|escape}" />
+            </dd>
+            <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern=".{ldelim}5,{rdelim}" /></dd>
+            <dt><label for="f_repasse">Encore le mot de passe</label> (vérification)</dt>
+            <dd><input type="password" name="repasse" id="f_repasse" value="{form_field name=repasse}" pattern=".{ldelim}5,{rdelim}" /></dd>
+        </dl>
+    </fieldset>
+
+    {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}
+    <fieldset>
+        <legend>Général</legend>
+        <dl>
+            <dt><label for="f_cat">Catégorie du membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="id_categorie" id="f_cat">
+                {foreach from=$membres_cats key="id" item="nom"}
+                    <option value="{$id|escape}"{if $current_cat == $id} selected="selected"{/if}>{$nom|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+        </dl>
+    </fieldset>
+    {/if}
+
+    <p class="submit">
+        {csrf_field key="new_member"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+<script type="text/javascript" src="{$admin_url}static/password.js"></script>
+<script type="text/javascript">
+initPasswordField('password_suggest', 'f_passe', 'f_repasse');
+</script>
+
+
+{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 (file)
index 0000000..242f19a
--- /dev/null
@@ -0,0 +1,154 @@
+{include file="admin/_head.tpl" title="Modifier une catégorie" current="membres/categories"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Informations générales</legend>
+        <dl>
+            <dt><label for="f_nom">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="nom" id="f_nom" value="{form_field data=$cat name=nom}" required="required" /></dd>
+            <dt><label for="f_description">Description</label></dt>
+            <dd><textarea name="description" id="f_description" rows="5" cols="50">{form_field data=$cat name=description}</textarea></dd>
+            <dt>
+                <input type="checkbox" name="cacher" value="1" id="f_cacher" {if $cat.cacher}checked="checked"{/if} />
+                <label for="f_cacher">Catégorie cachée</label>
+            </dt>
+            <dd class="help">
+                Si coché cette catégorie ne sera visible qu'aux administrateurs et ne recevra pas
+                de messages collectifs ou de rappels.
+            </dd>
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Cotisation obligatoire</legend>
+        <dl>
+            <dt><label for="f_id_cotisation_obligatoire">Cotisation obligatoire</label></dt>
+            <dd>
+                <select name="id_cotisation_obligatoire" id="f_id_cotisation_obligatoire">
+                    <option value="">-- Non</option>
+                    {foreach from=$cotisations item="cotisation"}
+                    <option value="{$cotisation.id|escape}" {form_field name="id_cotisation_obligatoire" selected=$cotisation.id data=$cat}>
+                        {$cotisation.intitule|escape} 
+                        — {$cotisation.montant|html_money} {$config.monnaie|escape}
+                        — {if $cotisation.duree}pour {$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}
+                    </option>
+                    {/foreach}
+                </select>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Droits</legend>
+        <dl class="droits">
+            <dt><label for="f_droit_connexion_aucun">Les membres de cette catégorie peuvent-ils se connecter ?</label></dt>
+            <dd>
+                <input type="radio" name="droit_connexion" value="{Garradin\Membres::DROIT_AUCUN}" id="f_droit_connexion_aucun" {if $cat.droit_connexion == Garradin\Membres::DROIT_AUCUN}checked="checked"{/if} />
+                <label for="f_droit_connexion_aucun"><b class="aucun">C</b> Non</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_connexion" value="{Garradin\Membres::DROIT_ACCES}" id="f_droit_connexion_acces" {if $cat.droit_connexion == Garradin\Membres::DROIT_ACCES}checked="checked"{/if} />
+                <label for="f_droit_connexion_acces"><b class="acces">C</b> Oui</label>
+            </dd>
+        </dl>
+        <dl class="droits">
+            <dt><label for="f_droit_inscription_aucun">Les membres de cette catégorie peuvent-ils s'inscrire d'eux-même ?</label></dt>
+            <dd>
+                <input type="radio" name="droit_inscription" value="{Garradin\Membres::DROIT_AUCUN}" id="f_droit_inscription_aucun" {if $cat.droit_inscription == Garradin\Membres::DROIT_AUCUN}checked="checked"{/if} />
+                <label for="f_droit_inscription_aucun"><b class="aucun">I</b> Non</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_inscription" value="{Garradin\Membres::DROIT_ACCES}" id="f_droit_inscription_acces" {if $cat.droit_inscription == Garradin\Membres::DROIT_ACCES}checked="checked"{/if} />
+                <label for="f_droit_inscription_acces"><b class="acces">I</b> Oui</label>
+            </dd>
+        </dl>
+        <dl class="droits">
+            <dt><label for="f_droit_membres_aucun">Gestion des membres :</label></dt>
+            <dd>
+                <input type="radio" name="droit_membres" value="{Garradin\Membres::DROIT_AUCUN}" id="f_droit_membres_aucun" {if $cat.droit_membres == Garradin\Membres::DROIT_AUCUN}checked="checked"{/if} />
+                <label for="f_droit_membres_aucun"><b class="aucun">M</b> Pas d'accès</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_membres" value="{Garradin\Membres::DROIT_ACCES}" id="f_droit_membres_acces" {if $cat.droit_membres == Garradin\Membres::DROIT_ACCES}checked="checked"{/if} />
+                <label for="f_droit_membres_acces"><b class="acces">M</b> Lecture uniquement</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_membres" value="{Garradin\Membres::DROIT_ECRITURE}" id="f_droit_membres_ecriture" {if $cat.droit_membres == Garradin\Membres::DROIT_ECRITURE}checked="checked"{/if} />
+                <label for="f_droit_membres_ecriture"><b class="ecriture">M</b> Lecture &amp; écriture</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_membres" value="{Garradin\Membres::DROIT_ADMIN}" id="f_droit_membres_admin" {if $cat.droit_membres == Garradin\Membres::DROIT_ADMIN}checked="checked"{/if} />
+                <label for="f_droit_membres_admin"><b class="admin">M</b> Administration</label>
+            </dd>
+        </dl>
+        <dl class="droits">
+            <dt><label for="f_droit_compta_aucun">Comptabilité :</label></dt>
+            <dd>
+                <input type="radio" name="droit_compta" value="{Garradin\Membres::DROIT_AUCUN}" id="f_droit_compta_aucun" {if $cat.droit_compta == Garradin\Membres::DROIT_AUCUN}checked="checked"{/if} />
+                <label for="f_droit_compta_aucun"><b class="aucun">€</b> Pas d'accès</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_compta" value="{Garradin\Membres::DROIT_ACCES}" id="f_droit_compta_acces" {if $cat.droit_compta == Garradin\Membres::DROIT_ACCES}checked="checked"{/if} />
+                <label for="f_droit_compta_acces"><b class="acces">€</b> Lecture uniquement</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_compta" value="{Garradin\Membres::DROIT_ECRITURE}" id="f_droit_compta_ecriture" {if $cat.droit_compta == Garradin\Membres::DROIT_ECRITURE}checked="checked"{/if} />
+                <label for="f_droit_compta_ecriture"><b class="ecriture">€</b> Lecture &amp; écriture</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_compta" value="{Garradin\Membres::DROIT_ADMIN}" id="f_droit_compta_admin" {if $cat.droit_compta == Garradin\Membres::DROIT_ADMIN}checked="checked"{/if} />
+                <label for="f_droit_compta_admin"><b class="admin">€</b> Administration</label>
+            </dd>
+        </dl>
+        <dl class="droits">
+            <dt><label for="f_droit_wiki_aucun">Wiki :</label></dt>
+            <dd>
+                <input type="radio" name="droit_wiki" value="{Garradin\Membres::DROIT_AUCUN}" id="f_droit_wiki_aucun" {if $cat.droit_wiki == Garradin\Membres::DROIT_AUCUN}checked="checked"{/if} />
+                <label for="f_droit_wiki_aucun"><b class="aucun">W</b> Pas d'accès</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_wiki" value="{Garradin\Membres::DROIT_ACCES}" id="f_droit_wiki_acces" {if $cat.droit_wiki == Garradin\Membres::DROIT_ACCES}checked="checked"{/if} />
+                <label for="f_droit_wiki_acces"><b class="acces">W</b> Lecture uniquement</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_wiki" value="{Garradin\Membres::DROIT_ECRITURE}" id="f_droit_wiki_ecriture" {if $cat.droit_wiki == Garradin\Membres::DROIT_ECRITURE}checked="checked"{/if} />
+                <label for="f_droit_wiki_ecriture"><b class="ecriture">W</b> Lecture &amp; écriture</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_wiki" value="{Garradin\Membres::DROIT_ADMIN}" id="f_droit_wiki_admin" {if $cat.droit_wiki == Garradin\Membres::DROIT_ADMIN}checked="checked"{/if} />
+                <label for="f_droit_wiki_admin"><b class="admin">W</b> Administration</label>
+            </dd>
+        </dl>
+        <dl class="droits">
+            <dt><label for="f_droit_config_aucun">Les membres de cette catégorie peuvent-ils modifier la configuration ?</label></dt>
+            <dd>
+                <input type="radio" name="droit_config" value="{Garradin\Membres::DROIT_AUCUN}" id="f_droit_config_aucun" {if $cat.droit_config == Garradin\Membres::DROIT_AUCUN}checked="checked"{/if} />
+                <label for="f_droit_config_aucun"><b class="aucun">&#x2611;</b> Non</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_config" value="{Garradin\Membres::DROIT_ADMIN}" id="f_droit_config_admin" {if $cat.droit_config == Garradin\Membres::DROIT_ADMIN}checked="checked"{/if} />
+                <label for="f_droit_config_admin"><b class="admin">&#x2611;</b> Oui</label>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="edit_cat_"|cat:$cat.id}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..f1113d6
--- /dev/null
@@ -0,0 +1,34 @@
+{include file="admin/_head.tpl" title="Supprimer une catégorie" current="membres/categories"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Supprimer la catégorie de membres ?</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer la catégorie «&nbsp;{$cat.nom|escape}&nbsp;» ?
+        </h3>
+        <p class="help">
+            Attention, la catégorie ne doit plus contenir de membres pour pouvoir
+            être supprimée.
+        </p>
+        <p class="help">
+            Notez que si des pages du wiki étaient restreintes à la lecture ou à l'écriture
+            aux seuls membres de ce groupe, elles redeviendront lisibles et modifiables
+            par tous les membres ayant accès au wiki !
+        </p>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="delete_cat_"|cat:$cat.id}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..50904bd
--- /dev/null
@@ -0,0 +1,51 @@
+{include file="admin/_head.tpl" title="Catégories de membres" current="membres/categories"}
+
+<table class="list">
+    <thead>
+        <th>Nom</th>
+        <td>Membres</td>
+        <td>Droits</td>
+        <td></td>
+    </thead>
+    <tbody>
+        {foreach from=$liste item="cat"}
+            <tr>
+                <th>{$cat.nom|escape}</th>
+                <td class="num">{$cat.nombre|escape}</td>
+                <td class="droits">
+                    {format_droits droits=$cat}
+                </td>
+                <td class="actions">
+                    <a href="cat_modifier.php?id={$cat.id|escape}">Modifier</a>
+                    | <a href="cat_supprimer.php?id={$cat.id|escape}">Supprimer</a>
+                </td>
+            </tr>
+        {/foreach}
+    </tbody>
+</table>
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Ajouter une catégorie</legend>
+        <dl>
+            <dt><label for="f_nom">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="nom" id="f_nom" value="{form_field name=nom}" required="required" /></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="new_cat"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+
+{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 (file)
index 0000000..efc295c
--- /dev/null
@@ -0,0 +1,99 @@
+{include file="admin/_head.tpl" title="Cotisations du membre" current="membres/cotisations"}
+
+<ul class="actions">
+    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}"><b>{$membre.identite|escape}</b></a></li>
+    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id|escape}">Modifier</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id|escape}">Supprimer</a></li>
+    {/if}
+    <li class="current"><a href="{$admin_url}membres/cotisations.php?id={$membre.id|escape}">Suivi des cotisations</a></li>
+</ul>
+
+<dl class="cotisation">
+{if $cotisation}
+    <dt>Cotisation obligatoire</dt>
+    <dd>{$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}
+    </dd>
+    <dt>À jour de cotisation ?</dt>
+    <dd>
+        {if !$cotisation.a_jour}
+            <span class="error"><b>Non</b>, cotisation non payée</span>
+        {else}
+            <b class="confirm">&#10003; Oui</b>
+            {if $cotisation.expiration}
+                (expire le {$cotisation.expiration|format_sqlite_date_to_french})
+            {/if}
+        {/if}
+    </dd>
+{/if}
+    <dt>
+        {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} 
+    </dt>
+{if !empty($cotisations_membre)}
+    {foreach from=$cotisations_membre item="co"}
+    <dd>{$co.intitule|escape} — 
+        {if $co.a_jour}
+            <span class="confirm">À jour</span>{if $co.expiration} — Expire le {$co.expiration|format_sqlite_date_to_french}{/if}
+        {else}
+            <span class="error">En retard</span>
+            — <a href="{$admin_url}membres/cotisations/rappels.php?id={$membre.id|escape}">Suivi des rappels</a>
+        {/if}
+    </dd>
+    {/foreach}
+{/if}
+    <dt><form method="get" action="{$admin_url}membres/cotisations/ajout.php"><input type="submit" value="Enregistrer une cotisation &rarr;" /><input type="hidden" name="id" value="{$membre.id|escape}" /></form></dt>
+</dl>
+
+{if !empty($cotisations)}
+<table class="list">
+    <thead>
+        <th>Date</th>
+        <td>Cotisation</td>
+        <td></td>
+        <td class="actions"></td>
+    </thead>
+    <tbody>
+        {foreach from=$cotisations item="c"}
+            <tr>
+                <td>{$c.date|format_sqlite_date_to_french}</td>
+                <td>
+                    {$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}
+                </td>
+                <td>
+                    {if $user.droits.compta >= Garradin\Membres::DROIT_ECRITURE && !empty($c.nb_operations)}
+                        {$c.nb_operations} écritures
+                    {/if}
+                </td>
+                <td class="actions">
+                    <a class="icn" href="{$admin_url}membres/cotisations/voir.php?id={$c.id_cotisation|escape}" title="Liste des membres inscrits à cette cotisation">👪</a>
+                    <a class="icn" href="{$admin_url}membres/cotisations/supprimer.php?id={$c.id|escape}" title="Supprimer cette cotisation pour ce membre">✘</a>
+                </td>
+            </tr>
+        {/foreach}
+    </tbody>
+</table>
+{/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 (file)
index 0000000..6cd623b
--- /dev/null
@@ -0,0 +1,126 @@
+{if $membre}
+    {include file="admin/_head.tpl" title="Enregistrer une cotisation pour le membre" current="membres/cotisations" js=1}
+
+    <ul class="actions">
+        <li><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}"><b>{$membre.identite|escape}</b></a></li>
+        <li><a href="{$admin_url}membres/modifier.php?id={$membre.id|escape}">Modifier</a></li>
+        {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+            <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id|escape}">Supprimer</a></li>
+        {/if}
+        <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id|escape}">Suivi des cotisations</a></li>
+    </ul>
+{else}
+    {include file="admin/_head.tpl" title="Enregistrer une cotisation" current="membres/cotisations" js=1}
+
+    <ul class="actions">
+        <li><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
+        <li class="current"><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
+        {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+            <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
+        {/if}
+    </ul>
+{/if}
+
+{if $error}
+    <p class="error">{$error|escape}</p>
+{else if $user.droits.compta >= Garradin\Membres::DROIT_ECRITURE}
+    <p class="help">
+        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 <a href="{$admin_url}compta/operations/saisir.php">saisir une opération comptable</a>.
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+    <fieldset>
+        <legend>Enregistrer une cotisation</legend>
+        <dl>
+            <dt><label for="f_id_cotisation">Cotisation</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select id="f_id_cotisation" required="required" name="id_cotisation">
+                    {foreach from=$cotisations item="co"}
+                    <option value="{$co.id|escape}" {form_field name="id_cotisation" selected=$co.id default=$default_co} data-compta="{$co.id_categorie_compta|escape}" data-amount="{$co.montant|escape}">
+                        {$co.intitule|escape}
+                        — {$co.montant|html_money} {$config.monnaie|escape}
+                        — {if $co.duree}pour {$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}
+                    </option>
+                    {/foreach}
+                </select>
+            </dd>
+            <dt class="f_compta"><label for="f_montant">Montant</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="f_compta"><input type="number" name="montant" step="0.01" min="0.00" id="f_montant" value="{form_field name=montant default=$default_amount}" /></dd>
+            <dt class="f_compta"><label for="f_moyen_paiement">Moyen de paiement</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="f_compta">
+                <select name="moyen_paiement" id="f_moyen_paiement">
+                {foreach from=$moyens_paiement item="moyen"}
+                    <option value="{$moyen.code|escape}"{if $moyen.code == $moyen_paiement} selected="selected"{/if}>{$moyen.nom|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+            <dt class="f_cheque"><label for="f_numero_cheque">Numéro de chèque</label></dt>
+            <dd class="f_cheque"><input type="text" name="numero_cheque" id="f_numero_cheque" value="{form_field name=numero_cheque}" /></dd>
+            <dt class="f_banque"><label for="f_banque">Compte bancaire</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="f_banque">
+                <select name="banque" id="f_banque">
+                {foreach from=$comptes_bancaires item="compte"}
+                    <option value="{$compte.id|escape}"{if $compte.id == $banque} selected="selected"{/if}>{$compte.libelle|escape} - {$compte.banque|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+            <dt><label for="f_date">Date</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="date" name="date" id="f_date" value="{form_field name=date default=$default_date}" required="required" /></dd>
+            {if !$membre}
+            <dt><label for="f_id_membre">Numéro de membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="number" name="id_membre" id="f_id_membre" value="{form_field name=id_membre}" step="1" min="1" required="required" /></dd>
+            {/if}
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="add_cotisation"}
+        {if $membre}<input type="hidden" name="id_membre" value="{$membre.id|escape}" />{/if}
+        <input type="submit" name="add" value="Enregistrer &rarr;" />
+    </p>
+</form>
+
+<script type="text/javascript">
+{literal}
+(function () {
+    window.changeMoyenPaiement = function()
+    {
+        var elm = $('#f_moyen_paiement');
+        toggleElementVisibility('.f_cheque', elm.value == 'CH');
+        toggleElementVisibility('.f_banque', elm.value != 'ES');
+    };
+
+    changeMoyenPaiement();
+
+    $('#f_moyen_paiement').onchange = changeMoyenPaiement;
+
+    $('#f_id_cotisation').onchange = function () {
+        if (this.options[this.selectedIndex].getAttribute('data-compta'))
+        {
+            $('#f_montant').value = this.options[this.selectedIndex].getAttribute('data-amount'); 
+            toggleElementVisibility('.f_compta', true);
+            changeMoyenPaiement();
+        }
+        else
+        {
+            toggleElementVisibility('.f_compta', false);
+            changeMoyenPaiement();
+        }
+    };
+
+    if (!$('#f_id_cotisation').options[$('#f_id_cotisation').selectedIndex].getAttribute('data-compta'))
+    {
+        toggleElementVisibility('.f_compta', false);
+    }
+} ());
+{/literal}
+</script>
+
+{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 (file)
index 0000000..7df8bf1
--- /dev/null
@@ -0,0 +1,114 @@
+{include file="admin/_head.tpl" title="Modifier une cotisation" current="membres/cotisations" js=1}
+
+<ul class="actions">
+    <li class="current"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
+    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
+    {/if}
+</ul>
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Modifier une cotisation</legend>
+        <dl>
+            <dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="intitule" id="f_intitule" value="{form_field name=intitule data=$cotisation}" required="required" /></dd>
+            <dt><label for="f_description">Description</label></dt>
+            <dd><textarea name="description" id="f_description" cols="50" rows="3">{form_field name=description data=$cotisation}</textarea></dd>
+            <dt><label for="f_montant">Montant</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="number" name="montant" step="0.01" min="0.00" id="f_montant" value="{form_field default=20 name=montant default=0.00 data=$cotisation}" required="required" /></dd>
+
+            <dt><label for="f_periodicite_jours">Période de validité</label></dt>
+            <dd><input type="radio" name="periodicite" id="f_periodicite_ponctuel" value="ponctuel" {form_field checked="ponctuel" name=periodicite default="ponctuel" data=$cotisation} /> <label for="f_periodicite_ponctuel">Pas de période (activité ou cotisation ponctuelle)</label></dd>
+
+            <dd><input type="radio" name="periodicite" id="f_periodicite_jours" value="jours" {form_field checked="jours" name=periodicite data=$cotisation} /> <label for="f_periodicite_jours">En nombre de jours</label>
+                <dl class="periode_jours">
+                    <dt><label for="f_duree">Durée de validité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+                    <dd><input type="number" step="1" size="5" min="1" name="duree" id="f_duree" value="{form_field name="duree" data=$cotisation}" /></dd>
+                </dl>
+            </dd>
+            <dd><input type="radio" name="periodicite" id="f_periodicite_dates" value="date" {form_field checked="date" name=periodicite data=$cotisation} /> <label for="f_periodicite_dates">Période définie</label>
+                <dl class="periode_dates">
+                    <dt><label for="f_date_debut">Date de début</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+                    <dd><input type="date" name="debut" value="{form_field name=debut data=$cotisation}" id="f_date_debut" /></dd>
+                    <dt><label for="f_date_fin">Date de fin</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+                    <dd><input type="date" name="fin" value="{form_field name=fin data=$cotisation}" id="f_date_fin" /></dd>
+                </dl>
+            </dd>
+            <dt>
+                <input type="checkbox" name="categorie" id="f_categorie" value="1" {form_field name="categorie" checked=1 data=$cotisation} /> <label for="f_categorie">Enregistrer les cotisations des membres dans la comptabilité</label>
+            </dt>
+            <dd class="help cat_compta">
+                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.
+            </dd>
+            <dt class="cat_compta"><label for="f_id_categorie_compta">Catégorie comptable</label></dt>
+            <dd class="cat_compta">
+                <select name="id_categorie_compta" id="f_id_categorie_compta">
+                {foreach from=$categories item="cat"}
+                    <option value="{$cat.id|escape}" {form_field name="id_categorie_compta" selected=$cat.id data=$cotisation}>{$cat.intitule|escape}
+                    {if !empty($cat.description)}
+                        — <em>{$cat.description|escape}</em>
+                    {/if}
+                    </option>
+                {/foreach}
+                </select>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="edit_co_`$cotisation.id`"}
+        <input type="submit" name="save" value="Ajouter &rarr;" />
+    </p>
+
+</form>
+
+<script type="text/javascript">
+{literal}
+(function () {
+    var hide = [];
+
+    if (!$('#f_categorie').checked)
+        hide.push('.cat_compta');
+
+    if (!$('#f_periodicite_jours').checked)
+        hide.push('.periode_jours');
+
+    if (!$('#f_periodicite_dates').checked)
+        hide.push('.periode_dates');
+
+    toggleElementVisibility(hide, false);
+
+    $('#f_categorie').onchange = function() {
+        toggleElementVisibility('.cat_compta', this.checked);
+        return true;
+    };
+
+    function togglePeriode()
+    {
+        toggleElementVisibility(['.periode_jours', '.periode_dates'], false);
+
+        if (this.checked && this.value == 'jours')
+            toggleElementVisibility('.periode_jours', true);
+        else if (this.checked && this.value == 'date')
+            toggleElementVisibility('.periode_dates', true);
+    }
+
+    $('#f_periodicite_ponctuel').onchange = togglePeriode;
+    $('#f_periodicite_dates').onchange = togglePeriode;
+    $('#f_periodicite_jours').onchange = togglePeriode;
+})();
+{/literal}
+</script>
+
+{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 (file)
index 0000000..a0d5c64
--- /dev/null
@@ -0,0 +1,65 @@
+{include file="admin/_head.tpl" title="Modifier un rappel automatique" current="membres/cotisations" js=1}
+
+<ul class="actions">
+    <li><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
+    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
+    <li class="current"><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
+</ul>
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}" id="f_add">
+
+    <fieldset>
+        <legend>Modifier un rappel automatique</legend>
+        <dl>
+            <dt><label for="f_id_cotisation">Cotisation associée</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="id_cotisation" id="f_id_cotisation" required="required">
+                    <option value="">--</option>
+                    {foreach from=$cotisations item="co"}
+                    <option value="{$co.id|escape}" {form_field name="id_cotisation" selected=$co.id data=$rappel}>
+                        {$co.intitule|escape}
+                        — {$co.montant|html_money} {$config.monnaie|escape}
+                        — {if $co.duree}pour {$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}
+                    </option>
+                    {/foreach}
+                </select>
+            </dd>
+            <dt><label for="f_sujet">Sujet du mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="sujet" id="f_sujet" value="{form_field name=sujet data=$rappel}" required="required" size="50" /></dd>
+            <dt><label for="f_delai">Délai d'envoi</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><label><input type="radio" name="delai_choix" value="0" {form_field name="delai_choix" checked=0 default=0 data=$rappel} /> Le jour de l'expiration de la cotisation</label></dd>
+            <dd>
+                <input type="radio" name="delai_choix" id="f_delai_pre" value="-1" {form_field name="delai_choix" checked=-1 data=$rappel} />
+                <input type="number" name="delai_pre" id="f_delai_pre_nb" step="1" min="1" max="900" size="4" id="f_delai" value="{form_field name=delai_pre data=$rappel default=30}" />
+                <label for="f_delai_pre">jours avant expiration</label>
+            </dd>
+            <dd>
+                <input type="radio" name="delai_choix" id="f_delai_post" value="1" {form_field name="delai_choix" checked=1 data=$rappel} /> 
+                <input type="number" name="delai_post" id="f_delai_post_nb" step="1" min="1" max="900" size="4" id="f_delai" value="{form_field name=delai_post default=30 data=$rappel}" />
+                <label for="f_delai_post">jours après expiration</label>
+            </dd>
+            <dt><label for="f_texte">Texte du mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><textarea name="texte" id="f_texte" cols="70" rows="15" required="required">{form_field name=texte data=$rappel}</textarea></dd>
+            <dd class="help">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.</dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="edit_rappel_`$rappel.id`"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..c2c3d57
--- /dev/null
@@ -0,0 +1,46 @@
+{include file="admin/_head.tpl" title="Supprimer un rappel automatique" current="membres/cotisations"}
+
+<ul class="actions">
+    <li><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
+    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
+    <li class="current"><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
+</ul>
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Supprimer ce rappel automatique ?</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer le rappel «&nbsp;{$rappel.sujet|escape}&nbsp;» ?
+        </h3>
+        <dl>
+            <dt><label for="f_delete_history">Effacer aussi l'historique des e-mails envoyés par le biais de ce rappel&nbsp;?</label></dt>
+            <dd>
+                <label>
+                    <input type="radio" name="delete_history" value="0" checked="checked" />
+                    Non, conserver l'historique
+                </label> (toutefois il ne sera plus associé à ce rappel)
+            </dd>
+            <dd>
+                <label>
+                    <input type="radio" name="delete_history" value="1" />
+                    Oui, effacer l'historique des e-mails envoyés
+                </label>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="delete_rappel_"|cat:$rappel.id}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..1dee49a
--- /dev/null
@@ -0,0 +1,126 @@
+{include file="admin/_head.tpl" title="Gestion des rappels automatiques" current="membres/cotisations" js=1}
+
+<ul class="actions">
+    <li><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
+    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
+    <li class="current"><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
+</ul>
+
+<p class="help">
+    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.
+</p>
+
+{if empty($liste)}
+    <p class="alert">Aucun rappel automatique n'est enregistré.</p>
+{else}
+    <table class="list">
+        <thead>
+            <td>Cotisation</td>
+            <td>Délai de rappel</td>
+            <th>Sujet</th>
+            <td></td>
+        </thead>
+        <tbody>
+            {foreach from=$liste item="rappel"}
+                <tr>
+                    <td>
+                        {$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}
+                    </td>
+                    <td>
+                        {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}
+                    </td>
+                    <th><a href="{$admin_url}membres/cotisations/rappel.php?id={$rappel.id|escape}">{$rappel.sujet|escape}</a></th>
+                    <td class="actions">
+                        <a class="icn" href="{$admin_url}membres/cotisations/gestion/rappel_modifier.php?id={$rappel.id|escape}" title="Modifier">✎</a>
+                        <a class="icn" href="{$admin_url}membres/cotisations/gestion/rappel_supprimer.php?id={$rappel.id|escape}" title="Supprimer">✘</a>
+                    </td>
+                </tr>
+            {/foreach}
+        </tbody>
+    </table>
+{/if}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}" id="f_add">
+
+    <fieldset>
+        <legend>Ajouter un rappel automatique</legend>
+        <dl>
+            <dt><label for="f_id_cotisation">Cotisation associée</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="id_cotisation" id="f_id_cotisation" required="required">
+                    <option value="">--</option>
+                    {foreach from=$cotisations item="co"}
+                    <option value="{$co.id|escape}" {form_field name="id_cotisation" selected=$co.id}>
+                        {$co.intitule|escape}
+                        — {$co.montant|html_money} {$config.monnaie|escape}
+                        — {if $co.duree}pour {$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}
+                    </option>
+                    {/foreach}
+                </select>
+            </dd>
+            <dt><label for="f_sujet">Sujet du mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="sujet" id="f_sujet" value="{form_field name=sujet default=$default_subject}" required="required" size="50" /></dd>
+            <dt><label for="f_delai">Délai d'envoi</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><label><input type="radio" name="delai_choix" value="0" {form_field name="delai_choix" checked=0 default=0} /> Le jour de l'expiration de la cotisation</label></dd>
+            <dd>
+                <input type="radio" name="delai_choix" id="f_delai_pre" value="-1" {form_field name="delai_choix" checked=-1} />
+                <input type="number" name="delai_pre" id="f_delai_pre_nb" step="1" min="1" max="900" size="4" id="f_delai" value="{form_field name=delai_pre default=30}" />
+                <label for="f_delai_pre">jours avant expiration</label>
+            </dd>
+            <dd>
+                <input type="radio" name="delai_choix" id="f_delai_post" value="1" {form_field name="delai_choix" checked=1} /> 
+                <input type="number" name="delai_post" id="f_delai_post_nb" step="1" min="1" max="900" size="4" id="f_delai" value="{form_field name=delai_post default=30}" />
+                <label for="f_delai_post">jours après expiration</label>
+            </dd>
+            <dt><label for="f_texte">Texte du mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><textarea name="texte" id="f_texte" cols="70" rows="15" required="required">{form_field name=texte default=$default_text}</textarea></dd>
+            <dd class="help">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.</dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="new_rappel"}
+        <input type="submit" name="save" value="Ajouter &rarr;" />
+    </p>
+
+</form>
+
+<script type="text/javascript">
+{literal}
+(function () {
+    $('#f_delai_pre_nb').onclick = function () {
+        $('#f_delai_pre').checked = true;
+    };
+    $('#f_delai_post_nb').onclick = function () {
+        $('#f_delai_post').checked = true;
+    };
+})();
+{/literal}
+</script>
+
+{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 (file)
index 0000000..44cda28
--- /dev/null
@@ -0,0 +1,36 @@
+{include file="admin/_head.tpl" title="Supprimer une cotisation" current="membres/cotisations"}
+
+<ul class="actions">
+    <li class="current"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
+    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
+    {/if}
+</ul>
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Supprimer cette cotisation ?</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer la cotisation «&nbsp;{$cotisation.intitule|escape}&nbsp;» ?
+        </h3>
+        <p class="help">
+            Attention, l'historique des membres ayant cotisé à cette cotisation sera supprimé.
+        </p>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="delete_co_"|cat:$cotisation.id}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..3aa6ab1
--- /dev/null
@@ -0,0 +1,160 @@
+{include file="admin/_head.tpl" title="Cotisations" current="membres/cotisations" js=1}
+
+<ul class="actions">
+    <li class="current"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
+    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
+    {/if}
+</ul>
+
+<table class="list">
+    <thead>
+        <th>Cotisation</th>
+        <td>Période</td>
+        <td>Montant</td>
+        <td>Membres inscrits</td>
+        <td>Membres à jour</td>
+        <td></td>
+    </thead>
+    <tbody>
+        {foreach from=$liste item="co"}
+            <tr>
+                <th><a href="{$admin_url}membres/cotisations/voir.php?id={$co.id|escape}">{$co.intitule|escape}</a></th>
+                <td>
+                    {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}
+                </td>
+                <td class="num">{$co.montant|html_money} {$config.monnaie|escape}</td>
+                <td class="num">{$co.nb_membres|escape}</td>
+                <td class="num">{$co.nb_a_jour|escape}</td>
+                <td class="actions">
+                    <a class="icn" href="{$admin_url}membres/cotisations/voir.php?id={$co.id|escape}" title="Liste des membres cotisants">👪</a>
+                    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+                        <a class="icn" href="{$admin_url}membres/cotisations/gestion/modifier.php?id={$co.id|escape}" title="Modifier">✎</a>
+                        <a class="icn" href="{$admin_url}membres/cotisations/gestion/supprimer.php?id={$co.id|escape}" title="Supprimer">✘</a>
+                    {/if}
+                </td>
+            </tr>
+        {/foreach}
+    </tbody>
+</table>
+
+{if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{else}
+    <p class="help">
+        Idée : les cotisations peuvent également être utilisées pour suivre les activités auxquels
+        sont inscrits les membres de l'association.
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}" id="f_add">
+
+    <fieldset>
+        <legend>Ajouter une cotisation</legend>
+        <dl>
+            <dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="intitule" id="f_intitule" value="{form_field name=intitule}" required="required" /></dd>
+            <dt><label for="f_description">Description</label></dt>
+            <dd><textarea name="description" id="f_description" cols="50" rows="3">{form_field name=description}</textarea></dd>
+            <dt><label for="f_montant">Montant minimal</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="number" name="montant" step="0.01" min="0.00" id="f_montant" value="{form_field name=montant default=0.00}" required="required" /></dd>
+
+            <dt><label for="f_periodicite_jours">Période de validité</label></dt>
+            <dd><input type="radio" name="periodicite" id="f_periodicite_ponctuel" value="ponctuel" {form_field checked="ponctuel" name=periodicite default="ponctuel"} /> <label for="f_periodicite_ponctuel">Pas de période (cotisation ponctuelle)</label></dd>
+
+            <dd><input type="radio" name="periodicite" id="f_periodicite_jours" value="jours" {form_field checked="jours" name=periodicite} /> <label for="f_periodicite_jours">En nombre de jours</label>
+                <dl class="periode_jours">
+                    <dt><label for="f_duree">Durée de validité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+                    <dd><input type="number" step="1" size="5" min="1" name="duree" id="f_duree" value="{form_field name="duree"}" /></dd>
+                </dl>
+            </dd>
+            <dd><input type="radio" name="periodicite" id="f_periodicite_dates" value="date" {form_field checked="date" name=periodicite} /> <label for="f_periodicite_dates">Période définie</label>
+                <dl class="periode_dates">
+                    <dt><label for="f_date_debut">Date de début</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+                    <dd><input type="date" name="debut" value="{form_field name=debut}" id="f_date_debut" /></dd>
+                    <dt><label for="f_date_fin">Date de fin</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+                    <dd><input type="date" name="fin" value="{form_field name=fin}" id="f_date_fin" /></dd>
+                </dl>
+            </dd>
+            <dt>
+                <input type="checkbox" name="categorie" id="f_categorie" value="1" {form_field name="categorie" checked=1} /> <label for="f_categorie">Enregistrer les cotisations des membres dans la comptabilité</label>
+            </dt>
+            <dd class="help cat_compta">
+                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.
+            </dd>
+            <dt class="cat_compta"><label for="f_id_categorie_compta">Catégorie comptable</label></dt>
+            <dd class="cat_compta">
+                <select name="id_categorie_compta" id="f_id_categorie_compta">
+                {foreach from=$categories item="cat"}
+                    <option value="{$cat.id|escape}" {form_field name="id_categorie_compta" selected=$cat.id}>{$cat.intitule|escape}
+                    {if !empty($cat.description)}
+                        — <em>{$cat.description|escape}</em>
+                    {/if}
+                    </option>
+                {/foreach}
+                </select>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="new_cotisation"}
+        <input type="submit" name="save" value="Ajouter &rarr;" />
+    </p>
+
+</form>
+
+<script type="text/javascript">
+{literal}
+(function () {
+    var hide = [];
+
+    if (!$('#f_categorie').checked)
+        hide.push('.cat_compta');
+
+    if (!$('#f_periodicite_jours').checked)
+        hide.push('.periode_jours');
+
+    if (!$('#f_periodicite_dates').checked)
+        hide.push('.periode_dates');
+
+    toggleElementVisibility(hide, false);
+
+    $('#f_categorie').onchange = function() {
+        toggleElementVisibility('.cat_compta', this.checked);
+        return true;
+    };
+
+    function togglePeriode()
+    {
+        toggleElementVisibility(['.periode_jours', '.periode_dates'], false);
+
+        if (this.checked && this.value == 'jours')
+            toggleElementVisibility('.periode_jours', true);
+        else if (this.checked && this.value == 'date')
+            toggleElementVisibility('.periode_dates', true);
+    }
+
+    $('#f_periodicite_ponctuel').onchange = togglePeriode;
+    $('#f_periodicite_dates').onchange = togglePeriode;
+    $('#f_periodicite_jours').onchange = togglePeriode;
+})();
+{/literal}
+</script>
+
+{/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 (file)
index 0000000..e61a3f7
--- /dev/null
@@ -0,0 +1,102 @@
+{include file="admin/_head.tpl" title="Rappels pour cotisations du membre" current="membres/cotisations" js=1}
+
+<ul class="actions">
+    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}"><b>{$membre.identite|escape}</b></a></li>
+    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id|escape}">Modifier</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id|escape}">Supprimer</a></li>
+    {/if}
+    <li class="current"><a href="{$admin_url}membres/cotisations.php?id={$membre.id|escape}">Suivi des cotisations</a></li>
+</ul>
+
+<form method="post" action="{$self_url}">
+    <fieldset>
+        <legend>Enregistrer un rappel fait à ce membre</legend>
+        <dl>
+            <dt><label for="f_id_cotisation">Cotisation</label></dt>
+            <dd>
+                <select id="f_id_cotisation" name="id_cotisation">
+                {foreach from=$cotisations item="co"}
+                    <option value="{$co.id}">{$co.intitule|escape} — 
+                    {if $co.a_jour}
+                        Expire dans {$co.nb_jours|escape} jours
+                    {else}
+                        EXPIRÉE depuis {$co.nb_jours|escape} jours
+                    {/if}
+                    </option>
+                {/foreach}
+                </select>
+            </dd>
+            <dt><label for="f_date">Date du rappel</label></dt>
+            <dd><input type="date" name="date" id="f_date" required="required" value="{form_field name="date" default=$default_date}" /></dd>
+            <dt><label for="f_media_email">Moyen de communication utilisé</label></dt>
+            <dd>
+                <label>
+                    <input id="f_media_email" type="radio" name="media" value="{Garradin\Rappels_envoyes::MEDIA_EMAIL}" />
+                    E-Mail
+                </label>
+            </dd>
+            {* FIXME: proposer d'envoyer un email au membre *}
+            <dd>
+                <label>
+                    <input type="radio" name="media" value="{Garradin\Rappels_envoyes::MEDIA_TELEPHONE}" />
+                    Téléphone
+                </label>
+            </dd>
+            {* FIXME: afficher les différents numéros de téléphone de la fiche membre *}
+            <dd>
+                <label>
+                    <input type="radio" name="media" value="{Garradin\Rappels_envoyes::MEDIA_COURRIER}" />
+                    Courrier postal
+                </label>
+            </dd>
+            <dd>
+                <label>
+                    <input type="radio" name="media" value="{Garradin\Rappels_envoyes::MEDIA_AUTRE}" />
+                    Autre
+                </label>
+            </dd>
+        </dl>
+        <p class="submit">
+            {csrf_field key="add_rappel_`$membre.id`"}
+            <input type="submit" name="save" value="Enregistrer le rappel &rarr;" />
+        </p>
+    </fieldset>
+</form>
+
+{if !empty($rappels)}
+<table class="list">
+    <thead>
+        <th>Date du rappel</th>
+        <td>Moyen de communication</td>
+        <td>Cotisation</td>
+        <td class="actions"></td>
+    </thead>
+    <tbody>
+        {foreach from=$rappels item="r"}
+            <tr>
+                <th>{$r.date|format_sqlite_date_to_french}</th>
+                <td>
+                    {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}
+                </td>
+                <td>
+                    {$r.intitule|escape} — 
+                    {$r.montant|html_money} {$config.monnaie|escape}
+                </td>
+                <td class="actions">
+                </td>
+            </tr>
+        {/foreach}
+    </tbody>
+</table>
+{/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 (file)
index 0000000..1a480b6
--- /dev/null
@@ -0,0 +1,35 @@
+{include file="admin/_head.tpl" title="Supprimer une cotisation pour le membre n°`$membre.id`" current="membres/cotisations"}
+
+<ul class="actions">
+    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}">Membre n°{$membre.id|escape}</a></li>
+    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id|escape}">Modifier</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id|escape}">Supprimer</a></li>
+    {/if}
+    <li class="current"><a href="{$admin_url}membres/cotisations.php?id={$membre.id|escape}">Suivi des cotisations</a></li>
+</ul>
+
+{if $error}
+    <p class="error">{$error|escape}</p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+    <fieldset>
+        <legend>Supprimer une cotisation membre</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer la cotisation membre
+            du {$cotisation.date|format_sqlite_date_to_french}&nbsp;?
+        </h3>
+        <p class="alert">Attention si des écritures comptables sont liées à cette cotisation
+            elles ne seront pas supprimées.</p>
+    </fieldset>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="del_cotisation_`$cotisation.id`"}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+</form>
+
+
+{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 (file)
index 0000000..7ad9179
--- /dev/null
@@ -0,0 +1,59 @@
+{include file="admin/_head.tpl" title="Membres ayant cotisé" current="membres/cotisations"}
+
+<ul class="actions">
+    <li class="current"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
+    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
+    {/if}
+</ul>
+
+<dl class="cotisation">
+    <dt>Cotisation</dt>
+    <dd>{$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}
+    </dd>
+    <dt>Nombre de membres ayant cotisé</dt>
+    <dd>{$cotisation.nb_membres|escape}</dd>
+</dl>
+
+{if !empty($liste)}
+    <table class="list">
+        <thead class="userOrder">
+            <tr>
+                <td class="{if $order == "id"} cur {if $desc}desc{else}asc{/if}{/if}"><a href="?id={$cotisation.id|escape}&amp;o=id&amp;a" class="icn up">&uarr;</a><a href="?id={$cotisation.id|escape}&amp;o=id&amp;d" class="icn dn">&darr;</a></td>
+                <th class="{if $order == "identite"} cur {if $desc}desc{else}asc{/if}{/if}">Membre <a href="?id={$cotisation.id|escape}&amp;o=identite&amp;a" class="icn up">&uarr;</a><a href="?id={$cotisation.id|escape}&amp;o=identite&amp;d" class="icn dn">&darr;</a></th>
+                <td class="{if $order == "a_jour"} cur {if $desc}desc{else}asc{/if}{/if}">Statut <a href="?id={$cotisation.id|escape}&amp;o=a_jour&amp;a" class="icn up">&uarr;</a><a href="?id={$cotisation.id|escape}&amp;o=a_jour&amp;d" class="icn dn">&darr;</a></td>
+                <td class="{if $order == "date"} cur {if $desc}desc{else}asc{/if}{/if}">Date de cotisation <a href="?id={$cotisation.id|escape}&amp;o=date&amp;a" class="icn up">&uarr;</a><a href="?id={$cotisation.id|escape}&amp;o=date&amp;d" class="icn dn">&darr;</a></td>
+                <td></td>
+            </tr>
+        </thead>
+        <tbody>
+            {foreach from=$liste item="co"}
+                <tr>
+                    <td class="num"><a href="{$admin_url}membres/fiche.php?id={$co.id_membre|escape}">{$co.id_membre|escape}</a></td>
+                    <th>{$co.nom|escape}</th>
+                    <td>{if $co.a_jour}<b class="confirm">À jour</b>{else}<b class="error">En retard</b>{/if}</td>
+                    <td>{$co.date|format_sqlite_date_to_french}</td>
+                    <td class="actions">
+                        <a href="{$admin_url}membres/cotisations/ajout.php?id={$co.id_membre|escape}&amp;cotisation={$cotisation.id|escape}">Saisir</a>
+                        | <a href="{$admin_url}membres/cotisations.php?id={$co.id_membre|escape}" title="Voir toutes les cotisations de ce membre">Cotisations</a>
+                        | <a href="{$admin_url}membres/cotisations/rappels.php?id={$co.id_membre|escape}">Rappels</a>
+                    </td>
+                </tr>
+            {/foreach}
+        </tbody>
+    </table>
+
+    {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 (file)
index 0000000..f7107b2
--- /dev/null
@@ -0,0 +1,106 @@
+{include file="admin/_head.tpl" title="`$membre.identite` (`$categorie.nom`)" current="membres"}
+
+<ul class="actions">
+    <li class="current"><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}"><b>{$membre.identite|escape}</b></a></li>
+    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id|escape}">Modifier</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id|escape}">Supprimer</a></li>
+    {/if}
+    <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id|escape}">Suivi des cotisations</a></li>
+</ul>
+
+<dl class="cotisation">
+{if $cotisation}
+    <dt>Cotisation obligatoire</dt>
+    <dd>{$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}
+    </dd>
+    <dt>À jour de cotisation ?</dt>
+    <dd>
+        {if !$cotisation.a_jour}
+            <span class="error"><b>Non</b>, cotisation non payée</span>
+        {else}
+            <b class="confirm">&#10003; Oui</b>
+            {if $cotisation.expiration}
+                (expire le {$cotisation.expiration|format_sqlite_date_to_french})
+            {/if}
+        {/if}
+    </dd>
+{/if}
+    <dt>
+        {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} 
+    </dt>
+    <dd>
+        <a href="{$admin_url}membres/cotisations.php?id={$membre.id|escape}">Voir l'historique</a>
+    </dd>
+    <dd><form method="get" action="{$admin_url}membres/cotisations/ajout.php"><input type="submit" value="Enregistrer une cotisation &rarr;" /><input type="hidden" name="id" value="{$membre.id|escape}" /></form></dd>
+{if !empty($nb_operations)}
+    <dt>Écritures comptables</dt>
+    <dd>{$nb_operations|escape} écritures comptables
+        — <a href="{$admin_url}compta/operations/membre.php?id={$membre.id|escape}">Voir la liste des écritures ajoutées par ce membre</a>
+    </dd>
+ {/if}
+</dl>
+
+<dl class="describe">
+    <dt>Numéro d'adhérent</dt>
+    <dd>{$membre.id|escape}</dd>
+    <dt>Catégorie</dt>
+    <dd>{$categorie.nom|escape} <span class="droits">{format_droits droits=$categorie}</span></dd>
+    <dt>Inscription</dt>
+    <dd>{$membre.date_inscription|date_fr:'d/m/Y'}</dd>
+    <dt>Dernière connexion</dt>
+    <dd>{if empty($membre.date_connexion)}Jamais{else}{$membre.date_connexion|date_fr:'d/m/Y à H:i'}{/if}</dd>
+    {foreach from=$champs key="c" item="config"}
+    <dt>{$config.title|escape}</dt>
+    <dd>
+        {if $config.type == 'checkbox'}
+            {if $membre[$c]}Oui{else}Non{/if}
+        {elseif empty($membre[$c])}
+            <em>(Non renseigné)</em>
+        {elseif $c == 'nom'}
+            <strong>{$membre[$c]|escape}</strong>
+        {elseif $c == 'email'}
+            <a href="mailto:{$membre[$c]|escape}">{$membre[$c]|escape}</a>
+            | <a href="{$www_url}admin/membres/message.php?id={$membre.id|escape}"><b class="icn action">✉</b> Envoyer un message</a>
+        {elseif $config.type == 'email'}
+            <a href="mailto:{$membre[$c]|escape}">{$membre[$c]|escape}</a>
+        {elseif $config.type == 'tel'}
+            <a href="tel:{$membre[$c]|escape}">{$membre[$c]|escape|format_tel}</a>
+        {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'}
+            <ul>
+            {foreach from=$config.options key="b" item="name"}
+                {if $membre[$c] & (0x01 << $b)}
+                    <li>{$name|escape}</li>
+                {/if}
+            {/foreach}
+            </ul>
+        {else}
+            {$membre[$c]|escape|rtrim|nl2br}
+        {/if}
+    </dd>
+    {/foreach}
+</dl>
+
+{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 (file)
index 0000000..882b28b
--- /dev/null
@@ -0,0 +1,88 @@
+{include file="admin/_head.tpl" title="Import & export des membres" current="membres" js=1}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{elseif $ok}
+    <p class="confirm">
+        L'import s'est bien déroulé.
+    </p>
+{/if}
+
+<ul class="actions">
+    <li class="current"><a href="{$www_url}admin/membres/import.php">Importer</a></li>
+    <li><a href="{$www_url}admin/membres/import.php?export">Exporter en CSV</a></li>
+</ul>
+
+<form method="post" action="{$self_url|escape}" enctype="multipart/form-data">
+
+    <fieldset>
+        <legend>Importer depuis un fichier</legend>
+        <dl>
+            <dt><label for="f_file">Fichier à importer</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="file" name="upload" id="f_file" required="required" /></dd>
+            <dt><label for="f_type">Type de fichier</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <input type="radio" name="type" id="f_type" value="garradin" {form_field name=type checked="garradin" default="garradin"} />
+                <label for="f_type">Export CSV de Garradin</label>
+            </dd>
+            <dd class="help">
+                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.
+            </dd>
+            <dd>
+                <input type="radio" name="type" id="f_type_galette" value="galette" {form_field name=type checked="galette"} />
+                <label for="f_type_galette">Export CSV de Galette</label>
+            </dd>
+            <dd class="help">
+                Export des données au format CSV provenant du logiciel libre
+                <a href="http://galette.eu/">Galette</a>.
+            </dd>
+            <dt class="galette"><label>Correspondance des champs</label></dt>
+            <dd class="help">Indiquer quels champs des fiches membre de Garradin les données de Galette doivent remplir.</dd>
+            <dd class="galette">
+                <table class="list auto">
+                    <tbody>
+                    {foreach from=$galette_champs item="galette"}
+                        {if is_int($galette)}{continue}{/if}
+                        <tr>
+                            <th>{$galette|escape}</th>
+                            <td><select name="galette_translate[{$galette|escape}]">
+                                <option value="">-- Ne pas importer ce champ</option>
+                                {foreach from=$garradin_champs item="champ" key="name"}
+                                {if $champ.type == 'checkbox' || $champ.type == 'multiple'}{continue}{/if}
+                                <option value="{$name|escape}" {if (!empty($translate[$galette]) && $translate[$galette] == $name)}selected="selected"{/if}>{$champ.title|escape}</option>
+                                {/foreach}
+                            </select></td>
+                        </tr>
+                    {/foreach}
+                    </tbody>
+                </table>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="membres_import"}
+        <input type="submit" name="import" value="Importer &rarr;" />
+    </p>
+
+</form>
+
+<script type="text/javascript">
+{literal}
+(function () {
+    function toggleGalette() {
+        toggleElementVisibility('.galette', $('#f_type_galette').checked);
+    }
+
+    $('#f_type').onchange = toggleGalette;
+    $('#f_type_galette').onchange = toggleGalette;
+    toggleGalette();
+})();
+{/literal}
+</script>
+
+{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 (file)
index 0000000..44ffd62
--- /dev/null
@@ -0,0 +1,156 @@
+{include file="admin/_head.tpl" title="Liste des membres" current="membres"}
+
+{if $user.droits.membres >= Garradin\Membres::DROIT_ECRITURE}
+<ul class="actions">
+    <li class="current"><a href="{$admin_url}membres/">Liste des membres</a></li>
+    <li><a href="{$admin_url}membres/recherche.php">Recherche avancée</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/import.php">Import &amp; export</a></li>
+        <li><a href="{$admin_url}membres/recherche_sql.php">Recherche par requête SQL</a></li>
+    {/if}
+</ul>
+{/if}
+
+{if isset($tpl.get.sent)}
+    <p class="confirm">Votre message a été envoyé.</p>
+{/if}
+
+{if !empty($membres_cats)}
+<form method="get" action="{$self_url|escape}" class="filterCategory">
+    <fieldset>
+        <legend>Filtrer par catégorie</legend>
+        <select name="cat" id="f_cat" onchange="this.form.submit();">
+            <option value="0" {if $current_cat == 0} selected="selected"{/if}>-- Toutes</option>
+        {foreach from=$membres_cats key="id" item="nom"}
+            {if $user.droits.membres >= Garradin\Membres::DROIT_ECRITURE
+                || !array_key_exists($id, $membres_cats_cachees)}
+            <option value="{$id|escape}"{if $current_cat == $id} selected="selected"{/if}>{$nom|escape}</option>
+            {/if}
+        {/foreach}
+        </select>
+        <noscript><input type="submit" value="Filtrer &rarr;" /></noscript>
+    </fieldset>
+</form>
+{/if}
+
+<form method="get" action="{$admin_url}membres/{if $user.droits.membres >= Garradin\Membres::DROIT_ECRITURE}recherche.php{/if}" class="searchMember">
+    <fieldset>
+        <legend>Rechercher un membre</legend>
+        <input type="text" name="r" value="" />
+        <input type="submit" value="Chercher &rarr;" />
+    </fieldset>
+</form>
+
+{if $user.droits.membres >= Garradin\Membres::DROIT_ECRITURE}
+
+    <form method="post" action="action.php" class="memberList">
+
+    {if !empty($liste)}
+    <table class="list">
+        <thead class="userOrder">
+            {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}<td class="check"><input type="checkbox" value="Tout cocher / décocher" onclick="checkUncheck();" /></td>{/if}
+            <td class="{if $order == 'id'} cur {if $desc}desc{else}asc{/if}{/if}" title="Numéro unique"><a href="?o=id&amp;a" class="icn up">&uarr;</a><a href="?o=id&amp;d" class="icn dn">&darr;</a></td>
+            {foreach from=$champs key="c" item="champ"}
+                <td class="{if $order == $c} cur {if $desc}desc{else}asc{/if}{/if}">{$champ.title|escape} <a href="?o={$c|escape}&amp;a" class="icn up">&uarr;</a><a href="?o={$c|escape}&amp;d" class="icn dn">&darr;</a></td>
+            {/foreach}
+            <td></td>
+        </thead>
+        <tbody>
+            {foreach from=$liste item="membre"}
+                <tr>
+                    {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}<td class="check"><input type="checkbox" name="selected[]" value="{$membre.id|escape}" /></td>{/if}
+                    <td class="num"><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}">{$membre.id|escape}</a></th>
+                    {foreach from=$champs key="c" item="cfg"}
+                        <td>{$membre[$c]|escape|display_champ_membre:$cfg}</td>
+                    {/foreach}
+                    <td class="actions">
+                        {if !empty($membre.email)}<a class="icn" href="{$admin_url}membres/message.php?id={$membre.id|escape}" title="Envoyer un message">✉</a> {/if}
+                        <a class="icn" href="{$admin_url}membres/fiche.php?id={$membre.id|escape}" title="Fiche membre">👤</a>
+                        <a class="icn" href="{$admin_url}membres/modifier.php?id={$membre.id|escape}" title="Modifier la fiche membre">✎</a>
+                    </td>
+                </tr>
+            {/foreach}
+        </tbody>
+    </table>
+
+    {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}
+    <p class="checkUncheck">
+        <input type="button" value="Tout cocher / décocher" onclick="checkUncheck();" />
+    </p>
+    <p class="actions">
+        <em>Pour les membres cochés :</em>
+        <input type="submit" name="move" value="Changer de catégorie" />
+        <input type="submit" name="delete" value="Supprimer" />
+        {csrf_field key="membres_action"}
+    </p>
+    {/if}
+
+    {pagination url=$pagination_url page=$page bypage=$bypage total=$total}
+    {else}
+    <p class="alert">
+        Aucun membre trouvé.
+    </p>
+    {/if}
+
+    </form>
+
+    <script type="text/javascript">
+    {literal}
+    (function() {
+        var checked = false;
+
+        window.checkUncheck = function()
+        {
+            var elements = document.getElementsByTagName('input');
+            var el_length = elements.length;
+
+            for (i = 0; i < el_length; i++)
+            {
+                var elm = elements[i];
+
+                if (elm.type == 'checkbox')
+                {
+                    if (checked)
+                        elm.checked = false;
+                    else
+                        elm.checked = true;
+                }
+            }
+
+            checked = checked ? false : true;
+            return true;
+        }
+    }())
+    {/literal}
+    </script>
+{else}
+    {if !empty($liste)}
+    <table class="list">
+        <thead>
+            <th>Membre</th>
+            <td></td>
+        </thead>
+        <tbody>
+            {foreach from=$liste item="membre"}
+                <tr>
+                    <th>{$membre.identite|escape}</th>
+                    <td class="actions">
+                        {if !empty($membre.email)}<a href="{$www_url}admin/membres/message.php?id={$membre.id|escape}">Envoyer un message</a>{/if}
+                    </td>
+                </tr>
+            {/foreach}
+        </tbody>
+    </table>
+
+    {if !empty($pagination_url)}
+        {pagination url=$pagination_url page=$page bypage=$bypage total=$total}
+    {/if}
+
+    {else}
+    <p class="alert">
+        Aucun membre trouvé.
+    </p>
+    {/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 (file)
index 0000000..57e9457
--- /dev/null
@@ -0,0 +1,38 @@
+{include file="admin/_head.tpl" title="Contacter un membre" current="membres"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+    <fieldset class="memberMessage">
+        <legend>Message</legend>
+        <dl>
+            <dt>Expéditeur</dt>
+            <dd>{$user.identite|escape} &lt;{$user.email|escape}&gt;</dd>
+            <dd class="help">
+                Votre adresse E-Mail apparaîtra dans le champ "expéditeur" du message reçu par le destinataire.
+            </dd>
+            <dt>Destinataire</dt>
+            <dd>{$membre.identite|escape} ({$categorie.nom|escape})</dd>
+            <dt><label for="f_sujet">Sujet</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="sujet" id="f_sujet" value="{form_field name=sujet}" required="required" /></dd>
+            <dt><label for="f_message">Message</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><textarea name="message" id="f_message" cols="72" rows="25" required="required">{form_field name=message}</textarea></dd>
+            <dd>
+                <input type="checkbox" name="copie" id="f_copie" value="1" />
+                <label for="f_copie">Recevoir par e-mail une copie du message envoyé</label>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="send_message_"|cat:$membre.id}
+        <input type="submit" name="save" value="Envoyer &rarr;" />
+    </p>
+</form>
+
+
+{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 (file)
index 0000000..b699bd6
--- /dev/null
@@ -0,0 +1,43 @@
+{include file="admin/_head.tpl" title="Envoyer un message collectif" current="membres/message_collectif"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}" onsubmit="return confirm('Envoyer vraiment ce message collectif ?');">
+    <fieldset class="memberMessage">
+        <legend>Message</legend>
+        <dl>
+            <dt>Expéditeur</dt>
+            <dd>{$config.nom_asso|escape} &lt;{$config.email_asso|escape}&gt;</dd>
+            <dt><label for="f_dest">Membres destinataires</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="dest">
+                    <option value="0">Toutes les catégories qui ne sont pas cachées</option>
+                {foreach from=$cats_liste key="id" item="nom"}
+                    <option value="{$id|escape}">{$nom|escape} {if array_key_exists($id, $cats_cachees)}[cachée]{/if}</option>
+                {/foreach}
+                </select>
+            </dd>
+            <dd>
+                <input type="checkbox" id="f_subscribed" name="subscribed" value="1" {form_field name="subscribed" default="1" checked="1"} />
+                <label for="f_subscribed">Seulement les membres inscrits à la lettre d'information</label>
+            </dd>
+            <dt><label for="f_sujet">Sujet</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="help">Sera automatiquement précédé de la mention [{$config.nom_asso|escape}]</dd>
+            <dd><input type="text" name="sujet" id="f_sujet" value="{form_field name=sujet}" required="required" /></dd>
+            <dt><label for="f_message">Message</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><textarea name="message" id="f_message" cols="72" rows="25" required="required">{form_field name=message}</textarea></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="send_message_collectif"}
+        <input type="submit" name="save" value="Envoyer &rarr;" />
+    </p>
+</form>
+
+
+{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 (file)
index 0000000..723abe2
--- /dev/null
@@ -0,0 +1,84 @@
+{include file="admin/_head.tpl" title="Modifier un membre" current="membres" js=1}
+
+<ul class="actions">
+    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}"><b>{$membre.identite|escape}</b></a></li>
+    <li class="current"><a href="{$admin_url}membres/modifier.php?id={$membre.id|escape}">Modifier</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id|escape}">Supprimer</a></li>
+    {/if}
+    <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id|escape}">Suivi des cotisations</a></li>
+</ul>
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Informations personnelles</legend>
+        <dl>
+        {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}
+            <dt><label for="f_id">Numéro de membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="id" id="f_id" value="{form_field data=$membre name=id}" /></dd>
+        {/if}
+            {foreach from=$champs item="champ" key="nom"}
+                {html_champ_membre config=$champ name=$nom data=$membre}
+            {/foreach}
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>{if $membre.passe}Changer le mot de passe{else}Choisir un mot de passe{/if}</legend>
+        <dl>
+        {if $membre.passe}
+            <dd>Ce membre a déjà un mot de passe, mais vous pouvez le changer si besoin.</dd>
+        {else}
+            <dd>Ce membre n'a pas encore de mot de passe et ne peut donc se connecter.</dd>
+        {/if}
+            <dt><label for="f_passe">Nouveau mot de passe</label>{if $champs.passe.mandatory} <b title="(Champ obligatoire)">obligatoire</b>{/if}</dt>
+            <dd class="help">
+                Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr 
+                et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
+            </dd>
+            <dd class="help">
+                Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
+                <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="password_suggest" value="{$passphrase|escape}" />
+            </dd>
+            <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern=".{ldelim}5,{rdelim}" /></dd>
+            <dt><label for="f_repasse">Encore le mot de passe</label> (vérification){if $champs.passe.mandatory} <b title="(Champ obligatoire)">obligatoire</b>{/if}</dt>
+            <dd><input type="password" name="repasse" id="f_repasse" value="{form_field name=repasse}" pattern=".{ldelim}5,{rdelim}" /></dd>
+        </dl>
+    </fieldset>
+
+    {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}
+    <fieldset>
+        <legend>Général</legend>
+        <dl>
+            <dt><label for="f_cat">Catégorie du membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <select name="id_categorie" id="f_cat">
+                {foreach from=$membres_cats key="id" item="nom"}
+                    <option value="{$id|escape}"{if $current_cat == $id} selected="selected"{/if}>{$nom|escape}</option>
+                {/foreach}
+                </select>
+            </dd>
+        </dl>
+    </fieldset>
+    {/if}
+
+    <p class="submit">
+        {csrf_field key="edit_member_"|cat:$membre.id}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+<script type="text/javascript" src="{$admin_url}static/password.js"></script>
+<script type="text/javascript">
+initPasswordField('password_suggest', 'f_passe', 'f_repasse');
+</script>
+
+{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 (file)
index 0000000..698f4ba
--- /dev/null
@@ -0,0 +1,209 @@
+{include file="admin/_head.tpl" title="Recherche de membre" current="membres"}
+
+{if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+<ul class="actions">
+    <li><a href="{$admin_url}membres/">Liste des membres</a></li>
+    <li class="current"><a href="{$admin_url}membres/recherche.php">Recherche avancée</a></li>
+    <li><a href="{$admin_url}membres/recherche_sql.php">Recherche par requête SQL</a></li>
+</ul>
+{/if}
+
+
+<form method="get" action="{$admin_url}membres/recherche.php" class="searchMember">
+    <fieldset>
+        <legend>Rechercher un membre</legend>
+        <dl>
+            <dt><label for="f_champ">Champ</label></dt>
+            <dd>
+                <select name="c" id="f_champ">
+                    {foreach from=$champs_liste key="k" item="v"}
+                    <option value="{$k|escape}"{form_field name="c" default=$champ selected=$k}>{$v.title|escape}</option>
+                    {/foreach}
+                </select>
+            </dd>
+            <dt><label for="f_texte">Recherche</label></dt>
+            <dd id="f_free"><input id="f_texte" type="text" name="r" value="{$recherche|escape}" required="required" /></dd>
+            {foreach from=$champs_liste key="k" item="v"}
+                {if $v.type == 'select'}
+                    <dd class="special" id="f_{$k|escape}">
+                        <select name="r" disabled="disabled">
+                            {foreach from=$v.options item="opt"}
+                            <option value="{$opt|escape}"{form_field name="r" default=$recherche selected=$opt}>{$opt|escape}</option>
+                            {/foreach}
+                        </select>
+                    </dd>
+                {elseif $v.type == 'multiple'}
+                    <dd class="special" id="f_{$k|escape}">
+                        <select name="r" disabled="disabled">
+                            {foreach from=$v.options key="opt_k" item="opt"}
+                            <option value="{$opt_k|escape}"{form_field name="r" default=$recherche selected=$opt_k}>{$opt|escape}</option>
+                            {/foreach}
+                        </select>
+                    </dd>
+                {elseif $v.type == 'checkbox'}
+                    <dd class="special" id="f_{$k|escape}">
+                        <select name="r" disabled="disabled">
+                            <option value="1"{form_field name="r" default=$recherche selected=1}>Oui</option>
+                            <option value="0"{form_field name="r" default=$recherche selected=0}>Non</option>
+                        </select>
+                    </dd>
+                {/if}
+            {/foreach}
+        </dl>
+        <p class="submit">
+            <input type="submit" value="Chercher &rarr;" />
+        </p>
+    </fieldset>
+</form>
+
+{if $user.droits.membres >= Garradin\Membres::DROIT_ECRITURE}
+
+    <form method="post" action="{$admin_url}membres/action.php" class="memberList">
+
+    {if !empty($liste)}
+    <table class="list search">
+        <thead>
+            {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}<td class="check"><input type="checkbox" value="Tout cocher / décocher" onclick="checkUncheck();" /></td>{/if}
+            <td></td>
+            {foreach from=$champs_entete key="c" item="cfg"}
+                {if $champ == $c}
+                    <th><strong>{$cfg.title|escape}</strong></th>
+                {else}
+                    <td>{$cfg.title|escape}</td>
+                {/if}
+            {/foreach}
+            <td></td>
+        </thead>
+        <tbody>
+            {foreach from=$liste item="membre"}
+                <tr>
+                    {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}<td class="check"><input type="checkbox" name="selected[]" value="{$membre.id|escape}" /></td>{/if}
+                    <td class="num"><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}">{$membre.id|escape}</a></th>
+                    {foreach from=$champs_entete key="c" item="cfg"}
+                        {if $champ == $c}
+                            <th><strong>{$membre[$c]|escape|display_champ_membre:$cfg}</strong></th>
+                        {else}
+                            <td>{$membre[$c]|escape|display_champ_membre:$cfg}</td>
+                        {/if}
+                    {/foreach}
+                    <td class="actions">
+                        {if !empty($membre.email)}<a class="icn" href="{$www_url}admin/membres/message.php?id={$membre.id|escape}" title="Envoyer un message">✉</a> {/if}
+                        <a class="icn" href="modifier.php?id={$membre.id|escape}" title="Modifier la fiche membre">✎</a>
+                    </td>
+                </tr>
+            {/foreach}
+        </tbody>
+    </table>
+
+    {if $user.droits.membres == Garradin\Membres::DROIT_ADMIN}
+    <p class="checkUncheck">
+        <input type="button" value="Tout cocher / décocher" onclick="checkUncheck();" />
+    </p>
+    <p class="actions">
+        <em>Pour les membres cochés :</em>
+        <input type="submit" name="move" value="Changer de catégorie" />
+        <input type="submit" name="delete" value="Supprimer" />
+        {csrf_field key="membres_action"}
+    </p>
+    {/if}
+
+    {elseif $recherche != ''}
+    <p class="alert">
+        Aucun membre trouvé.
+    </p>
+    {/if}
+
+    </form>
+
+    <script type="text/javascript">
+    {literal}
+    (function() {
+        var checked = false;
+
+        window.checkUncheck = function()
+        {
+            var elements = document.getElementsByTagName('input');
+            var el_length = elements.length;
+
+            for (i = 0; i < el_length; i++)
+            {
+                var elm = elements[i];
+
+                if (elm.type == 'checkbox')
+                {
+                    if (checked)
+                        elm.checked = false;
+                    else
+                        elm.checked = true;
+                }
+            }
+
+            checked = checked ? false : true;
+            return true;
+        }
+    }())
+    {/literal}
+    </script>
+{else}
+    {if !empty($liste)}
+    <table class="list">
+        <thead>
+            <th>Membre</th>
+            <td></td>
+        </thead>
+        <tbody>
+            {foreach from=$liste item="membre"}
+                <tr>
+                    <th>{$membre.identite|escape}</th>
+                    <td class="actions">
+                        {if !empty($membre.email)}<a href="{$www_url}admin/membres/message.php?id={$membre.id|escape}">Envoyer un message</a>{/if}
+                    </td>
+                </tr>
+            {/foreach}
+        </tbody>
+    </table>
+    {else}
+    <p class="info">
+        Aucun membre trouvé.
+    </p>
+    {/if}
+{/if}
+
+<script type="text/javascript">
+{literal}
+(function() {
+    var current = false;
+
+    var selectField = function(elm)
+    {
+        if (current)
+        {
+            document.getElementById('f_' + current).style.display = 'none';
+            document.getElementById('f_' + current).querySelector('select').disabled = true;
+            current = false;
+        }
+        
+        if (document.getElementById('f_' + elm.value))
+        {
+            document.getElementById('f_' + elm.value).style.display = 'block';
+            document.getElementById('f_' + elm.value).querySelector('select').disabled = false;
+            document.getElementById('f_free').style.display = 'none';
+            document.getElementById('f_texte').disabled = true;
+            current = elm.value;
+        }
+        else
+        {
+            document.getElementById('f_texte').disabled = false;
+            document.getElementById('f_free').style.display = 'block';
+        }
+
+        return true;
+    }
+
+    document.getElementById('f_champ').onchange = function() { selectField(this); };
+    window.onload = selectField(document.getElementById('f_champ'));
+}())
+{/literal}
+</script>
+
+{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 (file)
index 0000000..68a50b7
--- /dev/null
@@ -0,0 +1,111 @@
+{include file="admin/_head.tpl" title="Recherche par requête SQL" current="membres"}
+
+<form method="get" action="{$admin_url}membres/recherche_sql.php">
+    <fieldset>
+        <legend>Schéma des tables SQL</legend>
+        <pre class="sql_schema">{$schema.membres|escape}</pre>
+        <dl>
+            <dt><label for="f_query">Requête SQL</label></dt>
+            <dd class="help">Si aucune limite n'est précisée, une limite de 100 résultats sera appliquée.</dd>
+            <dd><textarea name="query" id="f_query" cols="50" rows="7" required="required">{$query|escape}</textarea></dd>
+        </dl>
+        <p class="submit">
+            <input type="submit" value="Exécuter &rarr;" />
+        </p>
+    </fieldset>
+</form>
+
+{if !empty($error)}
+<p class="error">
+    <strong>Erreur dans la requête SQL :</strong><br />
+    {$error|escape}
+</p>
+{/if}
+
+<form method="post" action="{$admin_url}membres/action.php" class="memberList">
+
+{if !empty($result)}
+<p class="alert">{$result|@count} résultats renvoyés.</p>
+<table class="list search">
+    <thead>
+        {if array_key_exists('id', $result[0])}
+        <td class="check"><input type="checkbox" value="Tout cocher / décocher" onclick="checkUncheck();" /></td>
+        {/if}
+        {foreach from=$result[0] key="col" item="ignore"}
+            <td>{$col|escape}</td>
+        {/foreach}
+        {if array_key_exists('id', $result[0])}
+        <td></td>
+        {/if}
+    </thead>
+    <tbody>
+        {foreach from=$result item="row"}
+            <tr>
+                {if array_key_exists('id', $result[0])}
+                    <td class="check">{if !empty($row.id)}<input type="checkbox" name="selected[]" value="{$row.id|escape}" />{/if}</td>
+                {/if}
+                {foreach from=$row item="col"}
+                    <td>{$col|escape}</td>
+                {/foreach}
+                {if array_key_exists('id', $result[0])}
+                <td class="actions">
+                    {if !empty($row.id)}
+                    <a class="icn" href="{$admin_url}membres/fiche.php?id={$row.id|escape}" title="Fiche membre">👤</a>
+                    <a class="icn" href="{$admin_url}membres/modifier.php?id={$row.id|escape}" title="Modifier ce membre">✎</a>
+                    {/if}
+                </td>
+                {/if}
+            </tr>
+        {/foreach}
+    </tbody>
+</table>
+
+<p class="checkUncheck">
+    <input type="button" value="Tout cocher / décocher" onclick="checkUncheck();" />
+</p>
+<p class="actions">
+    <em>Pour les membres cochés :</em>
+    <input type="submit" name="move" value="Changer de catégorie" />
+    <input type="submit" name="delete" value="Supprimer" />
+    {csrf_field key="membres_action"}
+</p>
+
+{else}
+<p class="alert">
+    Aucun membre trouvé.
+</p>
+{/if}
+
+</form>
+
+<script type="text/javascript">
+{literal}
+(function() {
+    var checked = false;
+
+    window.checkUncheck = function()
+    {
+        var elements = document.getElementsByTagName('input');
+        var el_length = elements.length;
+
+        for (i = 0; i < el_length; i++)
+        {
+            var elm = elements[i];
+
+            if (elm.type == 'checkbox')
+            {
+                if (checked)
+                    elm.checked = false;
+                else
+                    elm.checked = true;
+            }
+        }
+
+        checked = checked ? false : true;
+        return true;
+    }
+}())
+{/literal}
+</script>
+
+{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 (file)
index 0000000..b972539
--- /dev/null
@@ -0,0 +1,43 @@
+{include file="admin/_head.tpl" title="Supprimer un membre" current="membres"}
+
+<ul class="actions">
+    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id|escape}"><b>{$membre.identite|escape}</b></a></li>
+    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id|escape}">Modifier</a></li>
+    {if $user.droits.membres >= Garradin\Membres::DROIT_ADMIN}
+        <li class="current"><a href="{$admin_url}membres/supprimer.php?id={$membre.id|escape}">Supprimer</a></li>
+    {/if}
+    <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id|escape}">Suivi des cotisations</a></li>
+</ul>
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Supprimer ce membre ?</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer le membre «&nbsp;{$membre.identite|escape}&nbsp;» ?
+        </h3>
+        <p class="alert">
+            <strong>Attention</strong> : cette action est irréversible et effacera toutes les
+            données personnelles et l'historique de ces membres.
+        </p>
+        <p class="help">
+            Alternativement, il est aussi possible de déplacer les membres qui ne font plus
+            partie de l'association dans une catégorie «&nbsp;Anciens membres&nbsp;», plutôt
+            que de les effacer complètement.
+        </p>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="delete_membre_"|cat:$membre.id}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..a6bc74c
--- /dev/null
@@ -0,0 +1,81 @@
+{include file="admin/_head.tpl" title="Mes cotisations" current="mes_cotisations"}
+
+<dl class="cotisation">
+    <dt>
+        {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} 
+    </dt>
+{if $cotisation}
+    <dt>Cotisation obligatoire</dt>
+    <dd>{$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}
+    </dd>
+    <dd>
+        {if !$cotisation.a_jour}
+            <b class="error">Vous n'êtes pas à jour de cotisation</b>
+        {else}
+            <b class="confirm">&#10003; À jour de cotisation</b>
+            {if $cotisation.expiration}
+                (expire le {$cotisation.expiration|format_sqlite_date_to_french})
+            {/if}
+        {/if}
+    </dd>
+{/if}
+{if !empty($cotisations_membre)}
+    <dt>Cotisations en cours</dt>
+    {foreach from=$cotisations_membre item="co"}
+    <dd>{$co.intitule|escape} — 
+        {if $co.a_jour}
+            <span class="confirm">À jour</span>{if $co.expiration} — Expire le {$co.expiration|format_sqlite_date_to_french}{/if}
+        {else}
+            <span class="error">En retard</span>
+        {/if}
+    </dd>
+    {/foreach}
+{/if}
+</dl>
+
+{if !empty($cotisations)}
+<div class="infos">
+    <h3>Historique des cotisations</h3>
+</div>
+
+<table class="list">
+    <thead>
+        <th>Date</th>
+        <td>Cotisation</td>
+    </thead>
+    <tbody>
+        {foreach from=$cotisations item="c"}
+            <tr>
+                <td>{$c.date|format_sqlite_date_to_french}</td>
+                <td>
+                    {$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}
+                </td>
+            </tr>
+        {/foreach}
+    </tbody>
+</table>
+{/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 (file)
index 0000000..cd99ee1
--- /dev/null
@@ -0,0 +1,58 @@
+{include file="admin/_head.tpl" title="Mes informations personnelles" current="mes_infos" js=1}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+
+    <fieldset>
+        <legend>Informations personnelles</legend>
+        <dl>
+            {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}
+        </dl>
+    </fieldset>
+
+    <fieldset>
+        <legend>Changer mon mot de passe</legend>
+        {if $user.droits.membres < Garradin\Membres::DROIT_ADMIN && (!empty($champs.passe.private) || empty($champs.passe.editable))}
+            <p class="help">Vous devez contacter un administrateur pour changer votre mot de passe.</p>
+        {else}
+            <dl>
+                <dd>Vous avez déjà un mot de passe, ne remplissez les champs suivants que si vous souhaitez en changer.</dd>
+                <dt><label for="f_passe">Nouveau mot de passe</label></dt>
+                <dd class="help">
+                    Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr 
+                    et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
+                </dd>
+                <dd class="help">
+                    Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
+                    <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="password_suggest" value="{$passphrase|escape}" />
+                </dd>
+                <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern=".{ldelim}5,{rdelim}" /></dd>
+                <dt><label for="f_repasse">Encore le mot de passe</label> (vérification)</dt>
+                <dd><input type="password" name="repasse" id="f_repasse" value="{form_field name=repasse}" pattern=".{ldelim}5,{rdelim}" /></dd>
+            </dl>
+        {/if}
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="edit_me"}
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+<script type="text/javascript" src="{$admin_url}static/password.js"></script>
+<script type="text/javascript">
+initPasswordField('password_suggest', 'f_passe', 'f_repasse');
+</script>
+
+{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 (file)
index 0000000..2c231fc
--- /dev/null
@@ -0,0 +1,54 @@
+{include file="admin/_head.tpl" title="Mot de passe oublié ou pas de mot de passe ?"}
+
+{if !empty($sent)}
+    <p class="confirm">
+        Un e-mail vous a été envoyé, cliquez sur le lien dans cet e-mail
+        pour recevoir un nouveau mot de passe.
+    </p>
+    <p class="alert">
+        <strong>Ne fermez pas cette fenêtre tant que vous n'avez pas cliqué sur le lien.</strong>
+        Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables.
+    </p>
+{elseif !empty($new_sent)}
+    <p class="confirm">
+        <strong>Un e-mail contenant votre nouveau mot de passe vous a été envoyé.</strong>
+        Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables.
+    </p>
+    <p><a href="{$www_url}admin/login.php">Connexion &rarr;</a></p>
+{else}
+
+    {if $error}
+        <p class="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}
+        </p>
+    {/if}
+
+    <form method="post" action="{$self_url|escape}">
+
+        <fieldset>
+            <legend>Recevoir un e-mail avec un nouveau mot de passe</legend>
+            <p class="help">
+                Inscrivez ici votre {$champ.title}.
+                Nous vous enverrons un message vous indiquant un lien permettant de recevoir un
+                nouveau mot de passe.
+            </p>
+            <dl>
+                <dt><label for="f_id">{$champ.title}</label></dt>
+                <dd><input type="text" name="id" id="f_id" value="{form_field name=id}" /></dd>
+            </dl>
+        </fieldset>
+
+        <p class="submit">
+            {csrf_field key="recoverPassword"}
+            <input type="submit" name="recover" value="Envoyer &rarr;" />
+        </p>
+
+    </form>
+{/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 (file)
index 0000000..4dbfc46
--- /dev/null
@@ -0,0 +1,40 @@
+{include file="admin/_head.tpl" title="Choisir la page parent" current="wiki" body_id="popup" is_popup=true}
+
+<div class="wikiTree">
+    <p class="choice">
+        <input type="button" onclick="chooseParent();" value="Choisir la page sélectionnée" />
+    </p>
+
+    {display_tree tree=$list}
+
+</div>
+
+{literal}
+<script type="text/javascript">
+(function() {
+    window.chooseParent = function()
+    {
+        var elm = document.getElementsByClassName("current")[0].getElementsByTagName("a")[0];
+
+        if (match = elm.href.match(/=(\d+)$/))
+        {
+            var id = parseInt(match[1], 10);
+            var titre = (id == 0 ? 'la racine du site' : elm.innerHTML);
+
+            if (window.opener.changeParent(id, titre))
+            {
+                self.close();
+            }
+            else
+            {
+                alert("Impossible de choisir la page comme parent d'elle-même !");
+            }
+
+            return false;
+        }
+    };
+}());
+</script>
+{/literal}
+
+{include file="admin/_foot.tpl"}
\ No newline at end of file
diff --git a/templates/admin/wiki/chercher.tpl b/templates/admin/wiki/chercher.tpl
new file mode 100644 (file)
index 0000000..dd472f2
--- /dev/null
@@ -0,0 +1,31 @@
+{include file="admin/_head.tpl" title="Recherche" current="wiki/chercher"}
+
+<form method="get" action="{$www_url}admin/wiki/chercher.php" class="wikiSearch">
+    <fieldset>
+        <legend>Rechercher une page</legend>
+        <p>
+            <input type="text" name="q" value="{$recherche|escape}" size="25" />
+            <input type="submit" value="Chercher" />
+        </p>
+    </fieldset>
+</form>
+
+
+{if !$recherche}
+    <p class="alert">
+        Aucun terme recherché.
+    </p>
+{else}
+    <p class="alert">
+        <strong>{$nb_resultats|escape}</strong> pages trouvées pour «&nbsp;{$recherche|escape}&nbsp;»
+    </p>
+
+    <div class="wikiResults">
+    {foreach from=$resultats item="page"}
+        <h3><a href="./?{$page.uri|escape}">{$page.titre|escape}</a></h3>
+        <p>{$page.snippet|escape|clean_snippet}</p>
+    {/foreach}
+    </div>
+{/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 (file)
index 0000000..ad28c0e
--- /dev/null
@@ -0,0 +1,27 @@
+{include file="admin/_head.tpl" title="Créer une page" current="wiki"}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Informations</legend>
+        <dl>
+            <dt><label for="f_titre">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="titre" id="f_titre" value="{form_field name=titre}" /></dd>
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="wiki_create"}
+        <input type="submit" name="create" value="Créer cette page" />
+    </p>
+
+</form>
+
+
+{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 (file)
index 0000000..2deb67d
--- /dev/null
@@ -0,0 +1,164 @@
+{include file="admin/_head.tpl" title="Éditer une page" current="wiki" js=1}
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}" id="f_form">
+
+    <fieldset class="wikiMain">
+        <legend>Informations générales</legend>
+        <dl>
+            <dt><label for="f_titre">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd><input type="text" name="titre" id="f_titre" value="{form_field data=$page name=titre}" /></dd>
+            <dt><label for="f_uri">Adresse unique</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd class="help">
+                Ne peut comporter que des lettres, des chiffres, des tirets et des tirets bas.
+            </dd>
+            <dd><input type="text" name="uri" id="f_uri" value="{form_field data=$page name=uri}" /></dd>
+            <dt><label for="f_browse_parent">Cette page est une sous-rubrique de...</label></dt>
+            <dd>
+                <input type="hidden" name="parent" id="f_parent" value="{form_field data=$page name=parent}" />
+                {if $page.parent == 0}
+                    <samp id="current_parent_name">la racine du site</samp>
+                {else}
+                    <samp id="current_parent_name">{$parent|escape}</samp>
+                {/if}
+                <input type="button" id="f_browse_parent" onclick="browseWikiForParent();" value="Changer" />
+            </dd>
+            <dt><label for="f_date">Date</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
+            <dd>
+                <input type="date" size="10" name="date" id="f_date" value="{$date|date_fr:'Y-m-d'|escape}" />
+                <input type="text" class="time" size="2" name="date_h" value="{$date|date_fr:'H'|escape}" /> h
+                <input type="text" class="time" size="2" name="date_min" value="{$date|date_fr:'i'|escape}" />
+            </dd>
+        </dl>
+    </fieldset>
+
+    <fieldset class="wikiRights">
+        <legend>Droits d'accès</legend>
+        <dl>
+            <dt><label for="f_droit_lecture_public">Cette page est visible :</label></dt>
+            <dd>
+                <input type="radio" name="droit_lecture" id="f_droit_lecture_public" value="{Garradin\Wiki::LECTURE_PUBLIC}" {form_field data=$page name="droit_lecture" checked=Garradin\Wiki::LECTURE_PUBLIC} />
+                <label for="f_droit_lecture_public"><strong>Sur le site de l'association</strong></label>
+                &mdash; cette page apparaîtra sur le site public de l'association, accessible à tous les visiteurs
+            </dd>
+            <dd>
+                <input type="radio" name="droit_lecture" id="f_droit_lecture_normal" value="{Garradin\Wiki::LECTURE_NORMAL}"  {form_field data=$page name="droit_lecture" checked=Garradin\Wiki::LECTURE_NORMAL} />
+                <label for="f_droit_lecture_normal"><strong>Sur le wiki uniquement</strong></label>
+                &mdash; seuls les membres ayant accès au wiki pourront la voir
+            </dd>
+            <dd>
+                <input type="radio" name="droit_lecture" id="f_droit_lecture_categorie" value="{$user.id_categorie}"  {if $page.droit_lecture >= Garradin\Wiki::LECTURE_CATEGORIE}checked="checked"{/if} />
+                <label for="f_droit_lecture_categorie"><strong>Aux membres de ma catégorie</strong></label>
+                &mdash; seuls les membres de la même catégorie que moi pourront voir cette page
+            </dd>
+            <dt><label for="f_droit_ecriture_normal">Cette page peut être modifiée par :</label></dt>
+            <dd>
+                <input type="radio" name="droit_ecriture" id="f_droit_ecriture_normal" value="{Garradin\Wiki::ECRITURE_NORMAL}" {form_field data=$page name="droit_ecriture" checked=Garradin\Wiki::ECRITURE_NORMAL} {if $page.droit_lecture >= Garradin\Wiki::LECTURE_CATEGORIE}disabled="disabled"{/if} />
+                <label for="f_droit_ecriture_normal">Les membres qui ont accès au wiki en écriture</label>
+            </dd>
+            <dd>
+                <input type="radio" name="droit_ecriture" id="f_droit_ecriture_categorie" value="{$user.id_categorie}" {if $page.droit_ecriture >= 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} />
+                <label for="f_droit_ecriture_categorie">Les membres de ma catégorie</label>
+            </dd>
+        </dl>
+    </fieldset>
+
+    <fieldset class="wikiEncrypt">
+        <dl>
+            <dt>
+                <input type="checkbox" name="chiffrement" id="f_chiffrement" {form_field name=chiffrement data=$page default=0 checked=1} value="1" onchange="checkEncryption(this);" />
+                <label for="f_chiffrement">Chiffrer le contenu</label> <i>(facultatif)</i>
+            </dt>
+            <noscript>
+            <dd>Nécessite JavaScript activé pour fonctionner !</dd>
+            </noscript>
+            <dd>Mot de passe : <i id="encryptPasswordDisplay" title="Chiffrement désactivé">désactivé</i></dd>
+            <dd class="help">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.</dd>
+        </dl>
+    </fieldset>
+
+
+    <fieldset class="wikiText">
+        <p>
+            <textarea name="contenu" id="f_contenu" cols="70" rows="35">{form_field data=$page name=contenu}</textarea>
+        </p>
+    </fieldset>
+
+    <fieldset class="wikiRevision">
+        <dl>
+            <dt><label for="f_modification">Résumé des modifications</label>  <i>(facultatif)</i></dt>
+            <dd><input type="text" name="modification" id="f_modification" value="{form_field data=$page name=modification}" /></dd>
+            {* FIXME
+            <dt>
+                <input type="checkbox" name="suivi" value="1" id="f_suivi" />
+                <label for="f_suivi">Suivre les modifications de cette page</label>
+            </dt>
+            *}
+        </dl>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="wiki_edit_`$page.id`"}
+        <input type="hidden" name="revision_edition" value="{form_field name=revision_edition default=$page.revision}" />
+        <input type="hidden" name="debut_edition" value="{form_field name=debut_edition default=$time}" />
+        <input type="submit" name="save" value="Enregistrer &rarr;" />
+    </p>
+
+</form>
+
+<script type="text/javascript">
+var page_id = '{$page.id|escape}';
+{literal}
+(function() {
+    document.getElementById('f_droit_lecture_categorie').onchange = function()
+    {
+        document.getElementById('f_droit_ecriture_normal').checked = false;
+        document.getElementById('f_droit_ecriture_normal').disabled = true;
+
+        document.getElementById('f_droit_ecriture_categorie').checked = true;
+        document.getElementById('f_droit_ecriture_categorie').disabled = true;
+    };
+
+    document.getElementById('f_droit_lecture_normal').onchange = function() {
+        document.getElementById('f_droit_ecriture_normal').disabled = false;
+        document.getElementById('f_droit_ecriture_categorie').disabled = false;
+    };
+
+    document.getElementById('f_droit_lecture_public').onchange = function() {
+        document.getElementById('f_droit_ecriture_normal').disabled = false;
+        document.getElementById('f_droit_ecriture_categorie').disabled = false;
+    };
+
+    window.changeParent = function(parent, title)
+    {
+        if (parent == page_id)
+        {
+            return false;
+        }
+
+        document.getElementById('f_parent').value = parent;
+        document.getElementById('current_parent_name').innerHTML = title;
+        return true;
+    };
+
+    window.browseWikiForParent = function()
+    {
+        window.open('_chercher_parent.php?parent=' + document.getElementById('f_parent').value, 'browseParent',
+            'width=500,height=600,top=150,left=150,scrollbars=1,location=false');
+    };
+
+    if (document.getElementById('f_chiffrement').checked)
+    {
+        wikiDecrypt(true);
+    }
+}());
+</script>
+{/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 (file)
index 0000000..4e9880f
--- /dev/null
@@ -0,0 +1,81 @@
+{include file="admin/_head.tpl" title="Historique : `$page.titre`" current="wiki"}
+
+<ul class="actions">
+    <li><a href="{$www_url}admin/wiki/?{$page.uri|escape}">Voir la page</a></li>
+</ul>
+
+{if !empty($revisions)}
+    <table class="list wikiRevisions">
+    {foreach from=$revisions item="rev"}
+        <tr>
+            <td>
+                {if $rev.chiffrement}
+                    <del title="Contenu chiffré">chiffré</del>
+                {else}
+                    {if $rev.revision == $page.revision}
+                        actu
+                    {else}
+                        <a href="?id={$page.id|escape}&amp;diff={$rev.revision|escape}.{$page.revision|escape}">actu</a>
+                    {/if}
+                    |
+                    {if $rev.revision == 1}
+                        diff
+                    {else}
+                        <a href="?id={$page.id|escape}&amp;diff={math equation="x-1" x=$rev.revision}.{$rev.revision|escape}">diff</a>
+                    {/if}
+                {/if}
+            </td>
+            <th>{$rev.date|date_fr:'d/m/Y à H:i'}</th>
+            <td>
+                {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES}
+                <a href="{$www_url}admin/membres/fiche.php?id={$rev.id_auteur|escape}">{$rev.nom_auteur|escape}</a>
+                {/if}
+            </td>
+            <td class="length">
+                {$rev.taille|escape} octets
+                {if $rev.revision > 1 && !$rev.chiffrement}
+                    {if $rev.diff_taille > 0}
+                        <ins>(+{$rev.diff_taille|escape})</ins>
+                    {elseif $rev.diff_taille < 0}
+                        <del>({$rev.diff_taille|escape})</del>
+                    {else}
+                        <i>({$rev.diff_taille|escape})</i>
+                    {/if}
+                {/if}
+            </td>
+            <td>
+            {if $rev.modification}
+                <em>{$rev.modification|escape}</em>
+            {/if}
+            </td>
+        </tr>
+    {/foreach}
+    </table>
+{elseif !empty($diff)}
+    <div class="wikiRevision revisionLeft">
+        <h3>Version du {$rev1.date|date_fr:'d/m/Y à H:i'}</h3>
+        {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES}
+            <h4>De <a href="{$www_url}admin/membres/fiche.php?id={$rev1.id_auteur|escape}">{$rev1.nom_auteur|escape}</a></h4>
+        {/if}
+        {if $rev1.modification}
+            <p><em>{$rev1.modification|escape}</em></p>
+        {/if}
+    </div>
+    <div class="wikiRevision revisionRight">
+        <h3>Version {if $rev2.revision == $page.revision}actuelle en date{/if} du {$rev2.date|date_fr:'d/m/Y à H:i'}</h3>
+        {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES}
+            <h4>De <a href="{$www_url}admin/membres/fiche.php?id={$rev2.id_auteur|escape}">{$rev2.nom_auteur|escape}</a></h4>
+        {/if}
+        {if $rev2.modification}
+            <p><em>{$rev2.modification|escape}</em></p>
+        {/if}
+    </div>
+    {diff old=$rev1.contenu new=$rev2.contenu}
+{else}
+    <p class="alert">
+        Cette page n'a pas d'historique.
+    </p>
+{/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 (file)
index 0000000..877f588
--- /dev/null
@@ -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}
+
+<ul class="actions">
+    {if $user.droits.wiki >= Garradin\Membres::DROIT_ECRITURE}
+        <li><a href="{$www_url}admin/wiki/creer.php?parent={if $config.accueil_wiki == $page.uri}0{else}{$page.id|escape}{/if}"><strong>Créer une nouvelle page</strong></a></li>
+    {/if}
+    {if $can_edit}
+        <li><a href="{$www_url}admin/wiki/editer.php?id={$page.id|escape}">Éditer</a></li>
+    {/if}
+    {if $can_read && $page && $page.contenu}
+        <li><a href="{$www_url}admin/wiki/historique.php?id={$page.id|escape}">Historique</a>
+        {if $page.droit_lecture == Garradin\Wiki::LECTURE_PUBLIC}
+            <li><a href="{$www_url}{$page.uri|escape}">Voir sur le site</a>
+        {/if}
+    {/if}
+    {if $user.droits.wiki >= Garradin\Membres::DROIT_ADMIN}
+        <li><a href="{$www_url}admin/wiki/supprimer.php?id={$page.id|escape}">Supprimer</a></li>
+    {/if}
+</ul>
+
+{if !$can_read}
+    <p class="alert">Vous n'avez pas le droit de lire cette page.</p>
+{else}
+    <div class="breadCrumbs">
+        <ul>
+            <li><a href="./">Wiki</a></li>
+            {if !empty($breadcrumbs)}
+            {foreach from=$breadcrumbs item="crumb"}
+            <li><a href="?{$crumb.uri|escape}">{$crumb.titre|escape}</a></li>
+            {/foreach}
+            {/if}
+        </ul>
+    </div>
+
+    {if !$page}
+        <p class="error">
+            Cette page n'existe pas.
+        </p>
+
+        {if $can_edit}
+        <form method="post" action="{$www_url}admin/wiki/creer.php">
+            <p class="submit">
+                {csrf_field key="wiki_create"}
+                <input type="hidden" name="titre" value="{$uri|escape}" />
+                <input type="submit" name="create" value="Créer cette page" />
+            </p>
+        </form>
+        {/if}
+    {else}
+
+        {if !empty($children)}
+        <div class="wikiChildren">
+            <h4>Dans cette rubrique</h4>
+            <ul>
+            {foreach from=$children item="child"}
+                <li><a href="?{$child.uri|escape}">{$child.titre|escape}</a></li>
+            {/foreach}
+            </ul>
+        </div>
+        {/if}
+
+        {if !$page.contenu}
+            <p class="alert">Cette page est vide, cliquez sur « Éditer » pour la modifier.</p>
+        {else}
+
+            {if $page.contenu.chiffrement}
+                <noscript>
+                    <div class="error">
+                        Vous dever activer javascript pour pouvoir déchiffrer cette page.
+                    </div>
+                </noscript>
+                <script type="text/javascript" src="{$admin_url}static/wiki-encryption.js"></script>
+                <div id="wikiEncryptedMessage">
+                    <p class="alert">Cette page est chiffrée.
+                        <input type="button" onclick="return wikiDecrypt(false);" value="Entrer le mot de passe" />
+                    </p>
+                </div>
+                <div class="wikiContent" style="display: none;" id="wikiEncryptedContent">
+                    {$page.contenu.contenu|escape}
+                </div>
+            {else}
+                <div class="wikiContent">
+                    {$page.contenu.contenu|format_wiki|liens_wiki:'?'}
+                </div>
+            {/if}
+
+            <p class="wikiFooter">
+                Dernière modification le {$page.date_modification|date_fr:'d/m/Y à H:i'}
+                {if $user.droits.membres >= Garradin\Membres::DROIT_ACCES}
+                par <a href="{$www_url}admin/membres/fiche.php?id={$page.contenu.id_auteur|escape}">{$auteur|escape}</a>
+                {/if}
+            </p>
+        {/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 (file)
index 0000000..0393290
--- /dev/null
@@ -0,0 +1,20 @@
+{include file="admin/_head.tpl" title="Pages modifiées récemment" current="wiki/recent"}
+
+{if !empty($list)}
+    <table class="list">
+        <tbody>
+        {foreach from=$list item="page"}
+        <tr>
+            <th><a href="{$www_url}admin/wiki/?{$page.uri|escape}">{$page.titre|escape}</a></th>
+            <td>{$page.date_modification|date_fr:'d/m/Y à H:i'}</td>
+        </tr>
+        {/foreach}
+        </tbody>
+    </table>
+
+    {pagination url="?p=[ID]" page=$page bypage=$bypage total=$total}
+{else}
+    <p class="alert">Pas de modification récente.</p>
+{/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 (file)
index 0000000..c35746f
--- /dev/null
@@ -0,0 +1,36 @@
+{include file="admin/_head.tpl" title="Supprimer : `$page.titre`" current="wiki"}
+
+<ul class="actions">
+    <li><a href="{$www_url}admin/wiki/"><strong>Wiki</strong></a></li>
+    <li><a href="{$www_url}admin/wiki/chercher.php">Rechercher</a></li>
+    <li><a href="{$www_url}admin/wiki/?{$page.uri|escape}">Voir la page</a></li>
+    <li><a href="{$www_url}admin/wiki/editer.php?id={$page.id|escape}">Éditer</a></li>
+</ul>
+
+{if $error}
+    <p class="error">
+        {$error|escape}
+    </p>
+{/if}
+
+<form method="post" action="{$self_url|escape}">
+
+    <fieldset>
+        <legend>Supprimer cette page du wiki ?</legend>
+        <h3 class="warning">
+            Êtes-vous sûr de vouloir supprimer la page «&nbsp;{$page.titre|escape}&nbsp;» ?
+        </h3>
+        <p class="help">
+            La page ne pourra pas être supprimée si d'autres pages l'utilisent comme rubrique
+            parente.
+        </p>
+    </fieldset>
+
+    <p class="submit">
+        {csrf_field key="delete_wiki_"|cat:$page.id}
+        <input type="submit" name="delete" value="Supprimer &rarr;" />
+    </p>
+
+</form>
+
+{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 (file)
index 0000000..55e93e5
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>Erreur</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <style type="text/css">
+    {literal}
+    * { margin: 0; padding: 0; }
+
+    html { width: 100%; height: 100%; }
+    body {
+        font-size: 100%;
+        color: #000;
+        font-family: "Trebuchet MS", Helvetica, Sans-serif;
+        background: #fff;
+        padding: 1em;
+    }
+
+    h1 { color: #9c4f15; margin-bottom: 10px; }
+
+    p.error {
+        border: 1px solid #c00;
+        background: #fcc;
+        padding: 0.5em;
+        margin-bottom: 1em;
+    }
+    {/literal}
+    </style>
+</head>
+
+<body>
+
+<h1>Erreur</h1>
+
+<p class="error">
+    {$error|escape}
+</p>
+
+<p>
+    <a href="{$www_url}" onclick="history.back(); return false;">&larr; Retour</a>
+</p>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
new file mode 100644 (file)
index 0000000..9a31a28
--- /dev/null
@@ -0,0 +1 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>
\ No newline at end of file
diff --git a/templates/index.tpl b/templates/index.tpl
new file mode 100644 (file)
index 0000000..5b5b86a
--- /dev/null
@@ -0,0 +1,26 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>{$config.nom_asso|escape}</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+
+<body>
+
+<div class="header">
+    <h1>{$config.nom_asso|escape}</h1>
+</div>
+
+<div class="page">
+    <p>
+        {$config.adresse_asso|escape}
+    </p>
+</div>
+
+<p>
+    <a href="admin/">Administration</a>
+</p>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/www/.htaccess b/www/.htaccess
new file mode 100644 (file)
index 0000000..b3ec8cd
--- /dev/null
@@ -0,0 +1,11 @@
+<IfModule mod_rewrite.c>
+    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]
+</IfModule>
+
+<IfModule !mod_rewrite.c>
+    ErrorDocument 404 /index.php
+</IfModule>
diff --git a/www/_inc.php b/www/_inc.php
new file mode 100644 (file)
index 0000000..d869c93
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../include/init.php';
+
+?>
\ No newline at end of file
diff --git a/www/_route.php b/www/_route.php
new file mode 100644 (file)
index 0000000..2e3a10f
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace Garradin;
+
+// Routeur pour l'utilisation avec le serveur web intégré à PHP
+
+const WWW_URI = '/';
+//const WWW_URL = '/';
+
+$uri = $_SERVER['REQUEST_URI'];
+
+if (($pos = strpos($uri, '?')) !== false)
+{
+       $uri = substr($uri, 0, $pos);
+}
+
+if (file_exists(__DIR__ . $uri))
+{
+       return false;
+}
+elseif (preg_match('!/admin/plugin/(.+)/(.*)!', $uri, $match))
+{
+       $_GET['_p'] = $match[1];
+       $_GET['_u'] = $match[2];
+       require __DIR__ . '/admin/plugin.php';
+}
+else
+{
+       require __DIR__ . '/index.php';
+}
\ No newline at end of file
diff --git a/www/admin/.htaccess b/www/admin/.htaccess
new file mode 100644 (file)
index 0000000..3fcd725
--- /dev/null
@@ -0,0 +1 @@
+ErrorDocument 404 "<h1>Erreur 404</h1><h2>Page non trouv&eacute;e</h2><p><a href=../>Retour</a></p>"
diff --git a/www/admin/_inc.php b/www/admin/_inc.php
new file mode 100644 (file)
index 0000000..1805382
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+namespace Garradin;
+
+require_once __DIR__ . '/../../include/init.php';
+
+$tpl = Template::getInstance();
+$tpl->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 (file)
index 0000000..11754f4
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ACCES)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$comptes = new Compta_Comptes;
+
+?>
\ 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 (file)
index 0000000..5ea4e4e
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$banque = new Compta_Comptes_Bancaires;
+
+$error = false;
+
+if (!empty($_POST['add']))
+{
+    if (!utils::CSRF_check('compta_ajout_banque'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        try
+        {
+            $id = $banque->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 (file)
index 0000000..e7494be
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+$banques = new Compta_Comptes_Bancaires;
+$journal = new Compta_Journal;
+
+$liste = $banques->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 = '<table class="rib"><thead><tr><th>Banque</th><th>Guichet</th><th>Compte</th><th>Clé</th></tr></thead>';
+    $out.= '<tbody><tr><td>'.$rib[0].'</td><td>'.$rib[1].'</td><td>'.$rib[2].'</td><td>'.$rib[3].'</td></tr></tbody></table>';
+    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 (file)
index 0000000..91af26a
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$banque = new Compta_Comptes_Bancaires;
+
+$compte = $banque->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 (file)
index 0000000..97f5a08
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$banque = new Compta_Comptes_Bancaires;
+
+$compte = $banque->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 (file)
index 0000000..4acad7b
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$cats = new Compta_Categories;
+
+$error = false;
+
+if (!empty($_POST['add']))
+{
+    if (!utils::CSRF_check('compta_ajout_cat'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        try
+        {
+            $id = $cats->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 (file)
index 0000000..3370521
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$cats = new Compta_Categories;
+
+if (isset($_GET['depenses']))
+    $type = Compta_Categories::DEPENSES;
+else
+    $type = Compta_Categories::RECETTES;
+
+$tpl->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 (file)
index 0000000..deb1822
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$cats = new Compta_Categories;
+
+$id = (int)utils::get('id');
+$cat = $cats->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 (file)
index 0000000..a09eb5f
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$cats = new Compta_Categories;
+
+$id = (int)utils::get('id');
+$cat = $cats->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 (file)
index 0000000..13e6b70
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$classe = (int) utils::get('classe');
+
+if (!$classe || $classe < 1 || $classe > 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 (file)
index 0000000..aa66463
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$classe = (int) utils::get('classe');
+
+$tpl->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 (file)
index 0000000..9d96838
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+$compte = $comptes->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 (file)
index 0000000..e6c5f08
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$id = utils::get('id');
+$compte = $comptes->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 (file)
index 0000000..2e70c04
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$id = utils::get('id');
+$compte = $comptes->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 (file)
index 0000000..f3e5f26
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$e = new Compta_Exercices;
+
+$error = false;
+
+if (!empty($_POST['add']))
+{
+    if (!utils::CSRF_check('compta_ajout_exercice'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        try
+        {
+            $id = $e->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 (file)
index 0000000..741e8f2
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+namespace Garradin;
+require_once __DIR__ . '/../_inc.php';
+
+$exercices = new Compta_Exercices;
+
+$exercice = $exercices->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 (file)
index 0000000..4ebfb8b
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$e = new Compta_Exercices;
+
+$exercice = $e->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 (file)
index 0000000..f3e0f12
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+$exercices = new Compta_Exercices;
+
+$exercice = $exercices->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 (file)
index 0000000..bf61f04
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+$exercices = new Compta_Exercices;
+
+$exercice = $exercices->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 (file)
index 0000000..98e5fff
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+$e = new Compta_Exercices;
+
+$tpl->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 (file)
index 0000000..8885012
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+$exercices = new Compta_Exercices;
+
+$exercice = $exercices->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 (file)
index 0000000..56b1e0c
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$e = new Compta_Exercices;
+
+$exercice = $e->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 (file)
index 0000000..89a617a
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$e = new Compta_Exercices;
+
+$exercice = $e->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 (file)
index 0000000..95a900f
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+if (!in_array(utils::get('g'), ['recettes_depenses', 'banques_caisses']))
+{
+       throw new UserException('Graphique inconnu.');
+}
+
+$graph = utils::get('g');
+
+if (Static_Cache::expired('graph_' . $graph))
+{
+       $stats = new Compta_Stats;
+
+       require_once ROOT . '/include/libs/svgplot/lib.svgplot.php';
+
+       $plot = new \SVGPlot(400, 300);
+
+       if ($graph == 'recettes_depenses')
+       {
+               $r = new \SVGPlot_Data($stats->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 (file)
index 0000000..22a1137
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$e = new Compta_Exercices;
+$import = new Compta_Import;
+
+if (isset($_GET['export']))
+{
+    header('Content-type: application/csv');
+    header('Content-Disposition: attachment; filename="Export comptabilité - ' . $config->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 (file)
index 0000000..3accfbf
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$journal = new Compta_Journal;
+
+$tpl->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 (file)
index 0000000..e257d68
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+$cats = new Compta_Categories;
+$cat = $type = false;
+
+if (utils::get('cat'))
+{
+       $cat = $cats->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 (file)
index 0000000..934f203
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+$exercices = new Compta_Exercices;
+$journal = new Compta_Journal;
+
+$exercice = utils::get('exercice') ?: $exercices->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 (file)
index 0000000..660b879
--- /dev/null
@@ -0,0 +1,152 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$journal = new Compta_Journal;
+$cats = new Compta_Categories;
+$banques = new Compta_Comptes_Bancaires;
+
+$operation = $journal->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 (file)
index 0000000..70f852a
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$journal = new Compta_Journal;
+
+$query = trim(utils::get('query'));
+
+$tpl->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 (file)
index 0000000..8847861
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$journal = new Compta_Journal;
+
+$journal->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 (file)
index 0000000..e187f0f
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['compta'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$journal = new Compta_Journal;
+
+$operation = $journal->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 (file)
index 0000000..57d2ca3
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+$journal = new Compta_Journal;
+
+$operation = $journal->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 (file)
index 0000000..7017969
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+if (!in_array(utils::get('g'), ['recettes', 'depenses']))
+{
+       throw new UserException('Graphique inconnu.');
+}
+
+$graph = utils::get('g');
+
+if (Static_Cache::expired('pie_' . $graph))
+{
+       $stats = new Compta_Stats;
+       $categories = new Compta_Categories;
+
+       require_once ROOT . '/include/libs/svgplot/lib.svgpie.php';
+
+       $pie = new \SVGPie(400, 250);
+
+       if ($graph == 'recettes')
+       {
+               $data = $stats->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 (file)
index 0000000..d2d283d
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['config'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+?>
\ No newline at end of file
diff --git a/www/admin/config/donnees.php b/www/admin/config/donnees.php
new file mode 100644 (file)
index 0000000..88955c0
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$s = new Sauvegarde;
+$error = false;
+
+if (utils::post('config'))
+{
+    if (!utils::CSRF_check('backup_config'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        try {
+            $config->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 (file)
index 0000000..3f3cd9b
--- /dev/null
@@ -0,0 +1,8 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$tpl->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 (file)
index 0000000..f4f291c
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$error = false;
+
+if (isset($_GET['ok']))
+{
+    $error = 'OK';
+}
+
+if (!empty($_POST['save']))
+{
+    if (!utils::CSRF_check('config'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        try {
+            $config->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 (file)
index 0000000..b0d8653
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$error = false;
+
+// Restauration de ce qui était en session
+if ($champs = $membres->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 (file)
index 0000000..2514b5a
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$error = false;
+
+if (!empty($_POST['install']))
+{
+    if (!utils::CSRF_check('install_plugin'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        try {
+            Plugin::install(utils::post('to_install'), false);
+            
+            utils::redirect('/admin/config/plugins.php');
+        }
+        catch (UserException $e)
+        {
+            $error = $e->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 (file)
index 0000000..8bb2424
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$error = false;
+
+if (isset($_GET['ok']))
+{
+    $error = 'OK';
+}
+
+if (!empty($_POST['save']))
+{
+    if (!utils::CSRF_check('config_site'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        try {
+            $config->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 (file)
index 0000000..d51ab83
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$cats = new Membres_Categories;
+$categorie = $cats->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 (file)
index 0000000..ca1fe6c
--- /dev/null
@@ -0,0 +1,246 @@
+<?php
+namespace Garradin;
+
+/*
+ * Tests : vérification que les conditions pour s'exécuter sont remplies
+ */
+
+function test_requis($condition, $message)
+{
+    if ($condition)
+    {
+        return true;
+    }
+
+    if (PHP_SAPI != 'cli')
+    {
+        header('Content-Type: text/html; charset=utf-8');
+        echo "<!DOCTYPE html>\n<html>\n<head>\n<title>Erreur</title>\n<meta charset=\"utf-8\" />\n";
+        echo '<style type="text/css">body { font-family: sans-serif; } ';
+        echo '.error { color: darkred; padding: .5em; margin: 1em; border: 3px double red; background: yellow; }</style>';
+        echo "\n</head>\n<body>\n<h2>Erreur</h2>\n<h3>Le problème suivant empêche Garradin de fonctionner :</h3>\n";
+        echo '<p class="error">' . htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . '</p>';
+        echo '<hr /><p>Pour plus d\'informations consulter ';
+        echo '<a href="http://dev.kd2.org/garradin/Probl%C3%A8mes%20fr%C3%A9quents">l\'aide sur les problèmes à l\'installation</a>.</p>';
+        echo "\n</body>\n</html>";
+    }
+    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 (file)
index 0000000..7e33834
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+namespace Garradin;
+
+const LOGIN_PROCESS = true;
+
+require_once __DIR__ . '/_inc.php';
+
+if ($membres->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 (file)
index 0000000..0a7c910
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+namespace Garradin;
+
+const LOGIN_PROCESS = true;
+require_once __DIR__ . '/_inc.php';
+
+$membres->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 (file)
index 0000000..f32476f
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+if (empty($_POST['selected']))
+{
+    throw new UserException("Aucun membre sélectionné.");
+}
+
+foreach ($_POST['selected'] as &$id)
+{
+    $id = (int) $id;
+
+    // On ne permet pas d'action collective sur l'utilisateur courant pour éviter les risques
+    // d'erreur genre "oh je me suis supprimé du coup j'ai plus accès à rien"
+    if ($id == $user['id'])
+    {
+        throw new UserException("Il n'est pas possible de se modifier ou supprimer soi-même.");
+    }
+}
+
+$error = false;
+
+if (!empty($_POST['move_ok']))
+{
+    if (!utils::CSRF_check('membres_action'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        if (!empty($_POST['id_categorie']))
+        {
+            $membres->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 (file)
index 0000000..83cb269
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$cats = new Membres_Categories;
+$champs = $config->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 (file)
index 0000000..090c6d4
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$cats = new Membres_Categories;
+
+if (empty($_GET['id']) || !is_numeric($_GET['id']))
+{
+    throw new UserException("Argument du numéro de catégorie manquant.");
+}
+
+$id = (int) $_GET['id'];
+
+$cat = $cats->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 (file)
index 0000000..36275f6
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$cats = new Membres_Categories;
+
+if (empty($_GET['id']) || !is_numeric($_GET['id']))
+{
+    throw new UserException("Argument du numéro de catégorie manquant.");
+}
+
+$id = (int) $_GET['id'];
+
+$cat = $cats->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 (file)
index 0000000..27d9edc
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$cats = new Membres_Categories;
+
+$error = false;
+
+if (!empty($_POST['save']))
+{
+    if (!utils::CSRF_check('new_cat'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        try {
+            $cats->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 (file)
index 0000000..0766514
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+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("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 (file)
index 0000000..c904499
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$membre = false;
+
+if (!empty($_GET['id']) && is_numeric($_GET['id']))
+{
+    $membre = $membres->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 (file)
index 0000000..0470c31
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+if (!utils::get('id') || !is_numeric(utils::get('id')))
+{
+    throw new UserException("Argument du numéro de cotisation manquant.");
+}
+
+$cotisations = new Cotisations;
+
+$co = $cotisations->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 (file)
index 0000000..7ada0f0
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+if (!utils::get('id') || !is_numeric(utils::get('id')))
+{
+    throw new UserException("Argument du numéro de rappel manquant.");
+}
+
+$rappels = new Rappels;
+
+$rappel = $rappels->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 (file)
index 0000000..7dcb180
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+if (!utils::get('id') || !is_numeric(utils::get('id')))
+{
+    throw new UserException("Argument du numéro de rappel manquant.");
+}
+
+$rappels = new Rappels;
+
+$rappel = $rappels->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 (file)
index 0000000..3231011
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$rappels = new Rappels;
+$cotisations = new Cotisations;
+
+$error = false;
+
+if (!empty($_POST['save']))
+{
+    if (!utils::CSRF_check('new_rappel'))
+    {
+        $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->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 (file)
index 0000000..5ee7999
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+if (!utils::get('id') || !is_numeric(utils::get('id')))
+{
+    throw new UserException("Argument du numéro de cotisation manquant.");
+}
+
+$cotisations = new Cotisations;
+
+$co = $cotisations->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 (file)
index 0000000..3077b9b
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$cotisations = new Cotisations;
+
+if ($user['droits']['membres'] >= 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 (file)
index 0000000..26bd0a9
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+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("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 (file)
index 0000000..c3bfe5a
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$membre = false;
+
+$cotisations = new Cotisations;
+$m_cotisations = new Cotisations_Membres;
+
+if (empty($_GET['id']) || !is_numeric($_GET['id']))
+{
+    throw new UserException("Argument du numéro de cotisation membre manquant.");
+}
+
+$id = (int) $_GET['id'];
+
+$co = $m_cotisations->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 (file)
index 0000000..faf8802
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+if (empty($_GET['id']) || !is_numeric($_GET['id']))
+{
+    throw new UserException("Argument du numéro de cotisation manquant.");
+}
+
+$id = (int) $_GET['id'];
+
+$cotisations = new Cotisations;
+$m_cotisations = new Cotisations_Membres;
+
+$co = $cotisations->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'] . '&amp;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 (file)
index 0000000..e108f86
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+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("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 (file)
index 0000000..394d81f
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$import = new Membres_Import;
+
+if (isset($_GET['export']))
+{
+    header('Content-type: application/csv');
+    header('Content-Disposition: attachment; filename="Export membres - ' . $config->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 (file)
index 0000000..450264b
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ACCES)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+
+// Recherche de membre (pour ceux qui n'ont qu'un accès à la liste des membres)
+if (utils::get('r'))
+{
+       $recherche = trim(utils::get('r'));
+
+       $result = $membres->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]&amp;o=' . $order . ($desc ? '&amp;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 (file)
index 0000000..67caabd
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ACCES)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+if (empty($user['email']))
+{
+    throw new UserException("Vous devez renseigner l'adresse e-mail dans vos informations pour pouvoir contacter les autres membres.");
+}
+
+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("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 (file)
index 0000000..c302a14
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$error = false;
+
+if (!empty($_POST['save']))
+{
+    if (!utils::CSRF_check('send_message_collectif'))
+    {
+        $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->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 (file)
index 0000000..9bb1df6
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+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("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 (file)
index 0000000..f40fb5e
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$recherche = trim(utils::get('r'));
+$champ = trim(utils::get('c'));
+
+$champs = $config->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 (file)
index 0000000..0afa37e
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$query = trim(utils::get('query'));
+
+$tpl->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 (file)
index 0000000..4c92d79
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['membres'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$membre = $membres->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 (file)
index 0000000..9bc825a
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$membre = $membres->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 (file)
index 0000000..f12394f
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$membre = $membres->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 (file)
index 0000000..eff1c27
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+namespace Garradin;
+
+const LOGIN_PROCESS = true;
+
+require_once __DIR__ . '/_inc.php';
+
+$error = false;
+
+if (trim(utils::get('c')))
+{
+    if ($membres->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 (file)
index 0000000..633e3a0
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$page = utils::get('_u') ?: 'index.php';
+
+$plugin = new Plugin(utils::get('_p'));
+
+define('Garradin\PLUGIN_ROOT', $plugin->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 (file)
index 0000000..21762a9
--- /dev/null
@@ -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 (file)
index 0000000..2026eef
Binary files /dev/null and b/www/admin/static/bg00.png differ
diff --git a/www/admin/static/bg01.png b/www/admin/static/bg01.png
new file mode 100644 (file)
index 0000000..d4325c1
Binary files /dev/null and b/www/admin/static/bg01.png differ
diff --git a/www/admin/static/code_editor.min.js b/www/admin/static/code_editor.min.js
new file mode 100644 (file)
index 0000000..2658cad
--- /dev/null
@@ -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<e;i++)if(c.value[i].search(/[\r\n]/)!=-1)a=a-.5;e=b;for(i=0;i<e;i++)if(c.value[i].search(/[\r\n]/)!=-1)b=b-.5;d.moveEnd('textedit',-1);d.moveStart('character',a);d.moveEnd('character',b-a);d.select();}return this.getSelection();};textEditor.prototype.scrollToSelection=function(a){var b=this.textarea;var c=b.value.substr(a.end);b.value=b.value.substr(0,a.end);b.scrollTop=100000;var d=b.scrollTop;b.value+=c;b.scrollTop=d;this.setSelection(a.start,a.end);};textEditor.prototype.wrapSelection=function(a,b,c){var d=this.textarea;var e=d.scrollTop;var f=a.text;var a=this.replaceSelection(a,b+f+c);if(f=='')a=this.setSelection(a.start+b.length,a.start+b.length);d.scrollTop=e;return a;};}());(function(){function a(a){function b(){}b.prototype=a;return new b();}String.prototype.repeat=function(a){return new Array(a+1).join(this);};window.codeEditor=function(a){textEditor.call(this,a);this.onlinechange=null;this.onlinenumberchange=null;this.fullscreen=false;this.nb_lines=0;this.current_line=0;this.search_str=null;this.search_pos=0;this.params={indent_size:4,tab_size:8,convert_tabs:true,lang:{search:"Text to search?\n(regexps allowed, begin them with '/')",replace:"Text for replacement?\n(use $1, $2... for regexp replacement)",search_selection:"Text to replace in selection?\n(regexps allowed, begin them with '/')",replace_result:"%d occurence found and replaced.",goto:"Line to go to:",no_search_result:"No search result found."}};that=this;this.init();this.textarea.spellcheck=false;this.shortcuts.push({shift:true,key:'tab',callback:this.indent});this.shortcuts.push({key:'tab',callback:this.indent});this.shortcuts.push({ctrl:true,key:'f',callback:this.search});this.shortcuts.push({ctrl:true,key:'h',callback:this.searchAndReplace});this.shortcuts.push({ctrl:true,key:'g',callback:this.goToLine});this.shortcuts.push({key:'F3',callback:this.searchNext});this.shortcuts.push({key:'backspace',callback:this.backspace});this.shortcuts.push({key:'enter',callback:this.enter});this.shortcuts.push({key:'"',callback:this.insertBrackets});this.shortcuts.push({key:'[',callback:this.insertBrackets});this.shortcuts.push({key:'{',callback:this.insertBrackets});this.shortcuts.push({key:'(',callback:this.insertBrackets});this.shortcuts.push({key:'F11',callback:this.toggleFullscreen});this.textarea.addEventListener('keypress',this.keyEvent,true);this.textarea.addEventListener('keydown',this.keyEvent,true);};codeEditor.prototype=a(textEditor.prototype);codeEditor.prototype.init=function(){var a=this;this.nb_lines=this.countLines();this.parent=document.createElement('div');this.parent.className='codeEditor';this.lineCounter=document.createElement('span');this.lineCounter.className='lineCount';for(i=1;i<=this.nb_lines;i++)this.lineCounter.innerHTML+='<b>'+i+'</b>';this.lineCounter.innerHTML+='<i>---</i>';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;e<c;e++){var g=document.createElement('b');g.innerHTML=e+1;this.lineCounter.insertBefore(g,f);}this.nb_lines=c;if(typeof this.onlinenumberchange==='function')this.onlinenumberchange.call(this);}if(b!=this.current_line){var d=this.lineCounter.getElementsByTagName('b');for(var e=0;e<this.nb_lines;e++)d[e].className='';d[b].className='current';this.current_line=b;if(typeof this.onlinechange==='function')this.onlinechange.call(this);}};codeEditor.prototype.countLines=function(){var a=this.textarea.value.match(/(\r?\n)/g);return a?a.length+1:1;};codeEditor.prototype.getLineNumberFromPosition=function(a){var a=a||this.getSelection();if(a.start==0)return 0;var b=this.textarea.value.substr(0,a.start).match(/(\r?\n)/g);return b?b.length:0;};codeEditor.prototype.getLines=function(){return this.textarea.value.split("\n");};codeEditor.prototype.getLine=function(a){return this.textarea.value.split("\n",a+1)[a];};codeEditor.prototype.getLinePosition=function(a,b){var c=0;for(i=0;i<a.length;i++){if(i==b)return{start:c+i,end:c+a[i].length,length:a[i].length,text:a[i]};c+=a[i].length;}return false;};codeEditor.prototype.goToLine=function(a){var b=window.prompt(that.params.lang.goto);if(!b)return;var c=this.textarea.value.split("\n",parseInt(b,10)).join("\n").length;this.scrollToSelection(this.setSelection(c,c));return true;};codeEditor.prototype.indent=function(a,b){var c=this.getSelection();var d=a.shiftKey;var e=this.getLines();var f=this.getLineNumberFromPosition(c);var g=this.getLinePosition(e,f);var h=(c.end>g.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<e.length;m++)e[m]=e[m].replace(l,'');}else for(var m=0;m<e.length;m++)e[m]=' '.repeat(this.params.indent_size)+e[m];k=e.join("\n");this.replaceSelection(c,k);return true;};codeEditor.prototype.search=function(){if(!(this.search_str=window.prompt(this.params.lang.search,this.search_str)))return;this.search_pos=0;return this.searchNext();};codeEditor.prototype.searchNext=function(){if(!this.search_str)return true;var a=this.getSelection();var b=a.end>=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<b.length;c++)if(b[c]=='fullscreen'){b.splice(c,1);this.parent.className=b.join(' ');this.fullscreen=false;return true;}b.push('fullscreen');this.parent.className=b.join(' ');this.fullscreen=true;return true;};}());
\ No newline at end of file
diff --git a/www/admin/static/datepickr.css b/www/admin/static/datepickr.css
new file mode 100644 (file)
index 0000000..2dc53e0
--- /dev/null
@@ -0,0 +1,97 @@
+.calendar {
+    font-family: 'Trebuchet MS', Tahoma, Verdana, Arial, sans-serif;
+    font-size: 0.9em;
+    background-color: #EEE;
+    color: #333;
+    border: 1px solid #DDD;
+    -moz-border-radius: 4px;
+    -webkit-border-radius: 4px;
+    border-radius: 4px;
+    padding: 0.2em;
+    width: 14em;
+    box-shadow: 2px 2px 5px #666;
+}
+
+.calendar a {
+    outline: none;
+}
+
+.calendar .months {
+    background-color: #F6AF3A;
+    border: 1px solid #E78F08;
+    -moz-border-radius: 4px;
+    -webkit-border-radius: 4px;
+    border-radius: 4px;
+    color: #FFF;
+    padding: 0.2em;
+    text-align: center;
+}
+
+.calendar .prev-month,
+.calendar .next-month {
+    padding: 0;
+}
+
+.calendar .prev-month {
+    float: left;
+}
+
+.calendar .next-month {
+    float: right;
+}
+
+.calendar .current-month {
+    margin: 0 auto;
+}
+
+.calendar .months a {
+    color: #FFF;
+    text-decoration: none;
+    padding: 0 0.4em;
+    -moz-border-radius: 4px;
+    -webkit-border-radius: 4px;
+    border-radius: 4px;
+}
+
+.calendar .months a:hover {
+    background-color: #FDF5CE;
+    color: #C77405;
+}
+
+.calendar table {
+    border-collapse: collapse;
+    padding: 0;
+    font-size: 0.8em;
+    width: 100%;
+}
+
+.calendar th {
+    text-align: center;
+}
+
+.calendar td {
+    text-align: right;
+    padding: 1px;
+    width: 14.3%;
+}
+
+.calendar td a {
+    display: block;
+    color: #1C94C4;
+    background-color: #F6F6F6;
+    border: 1px solid #CCC;
+    text-decoration: none;
+    padding: 0.2em;
+}
+
+.calendar td a:hover {
+    color: #C77405;
+    background-color: #FDF5CE;
+    border: 1px solid #FBCB09;
+}
+
+.calendar td.today a {
+    background-color: #FFF0A5;
+    border: 1px solid #FED22F;
+    color: #363636;
+}
diff --git a/www/admin/static/datepickr.js b/www/admin/static/datepickr.js
new file mode 100644 (file)
index 0000000..05c661e
--- /dev/null
@@ -0,0 +1,463 @@
+/*
+       datepickr - pick your date not your nose
+       Copyright (c) 2010 josh.salverda - 2012 bohwaz Apache License 2.0
+       https://code.google.com/p/datepickr/
+       http://dev.kd2.org/garradin/
+*/
+
+function datepickr(targetElement, userConfig) {
+
+       var config = {
+               fullCurrentMonth: true,
+               dateFormat: 'F jS, Y',
+               firstDayOfWeek: 1,
+               weekdays: ['Sun', 'Mon', 'Tues', 'Wednes', 'Thurs', 'Fri', 'Satur'],
+               months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+               suffix: { 1: 'st', 2: 'nd', 3: 'rd', 21: 'st', 22: 'nd', 23: 'rd', 31: 'st' },
+               defaultSuffix: 'th'
+       },
+       currentDate = new Date(),
+       currentPosition = new Array(0,0),
+       currentMaxRows = 4,
+       // shortcuts to get date info
+       get = {
+               current: {
+                       year: function() {
+                               return currentDate.getFullYear();
+                       },
+                       month: {
+                               integer: function() {
+                                       return currentDate.getMonth();
+                               },
+                               string: function(full) {
+                                       var date = currentDate.getMonth();
+                                       return monthToStr(date, full);
+                               }
+                       },
+                       day: function() {
+                               return currentDate.getDate();
+                       }
+               },
+               month: {
+                       integer: function() {
+                               return currentMonthView;
+                       },
+                       string: function(full) {
+                               var date = currentMonthView;
+                               return monthToStr(date, full);
+                       },
+                       numDays: function() {
+                               // checks to see if february is a leap year otherwise return the respective # of days
+                               return (get.month.integer() == 1 && !(currentYearView & 3) && (currentYearView % 1e2 || !(currentYearView % 4e2))) ? 29 : daysInMonth[get.month.integer()];
+                       }
+               }
+       },
+       // variables used throughout the class
+       daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
+       element, container, body, month, prevMonth, nextMonth,
+       currentYearView = get.current.year(),
+       currentMonthView = get.current.month.integer(),
+       i, x, buildCache = [];
+
+       function build(nodeName, attributes, content) {
+               var element;
+
+               if(!(nodeName in buildCache)) {
+                       buildCache[nodeName] = document.createElement(nodeName);
+               }
+
+               element = buildCache[nodeName].cloneNode(false);
+
+               if(attributes != null) {
+                       for(var attribute in attributes) {
+                               element[attribute] = attributes[attribute];
+                       }
+               }
+
+               if(content != null) {
+                       if(typeof(content) == 'object') {
+                               element.appendChild(content);
+                       } else {
+                               element.innerHTML = content;
+                       }
+               }
+
+               return element;
+       }
+
+       function monthToStr(date, full) {
+               return ((full == true) ? config.months[date] : ((config.months[date].length > 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: '#' }, '&lt;'));
+               nextMonth = build('span', { className: 'next-month' }, build('a', { href: '#' }, '&gt;'));
+               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 (file)
index 0000000..8a08392
--- /dev/null
@@ -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 (file)
index 0000000..e9a936b
Binary files /dev/null and b/www/admin/static/font/garradin.eot differ
diff --git a/www/admin/static/font/garradin.svg b/www/admin/static/font/garradin.svg
new file mode 100644 (file)
index 0000000..c959660
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Copyright (C) 2014 by original authors @ fontello.com</metadata>
+<defs>
+<font id="garradin" horiz-adv-x="1000" >
+<font-face font-family="garradin" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
+<missing-glyph horiz-adv-x="1000" />
+<glyph glyph-name="search" unicode="&#x1f50d;" d="m643 386q0 103-74 176t-176 74-177-74-73-176 73-177 177-73 176 73 74 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 152-31 126-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+<glyph glyph-name="check" unicode="&#x2611;" d="m786 331v-177q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-6-5-13-5-2 0-5 1-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v141q0 8 5 13l36 35q6 6 13 6 3 0 7-2 11-4 11-16z m129 273l-455-454q-13-14-31-14t-32 14l-240 240q-14 13-14 31t14 32l61 62q14 13 32 13t32-13l147-147 361 361q13 13 31 13t32-13l62-61q13-14 13-32t-13-32z" horiz-adv-x="928.6" />
+<glyph glyph-name="user" unicode="&#x1f464;" d="m786 66q0-67-41-106t-108-39h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q5 0 24-12t41-27 60-27 75-12 74 12 61 27 41 27 24 12q34 0 62-11t48-30 34-45 24-55 15-60 8-61 2-58z m-179 498q0-88-63-151t-151-63-152 63-62 151 62 152 152 63 151-63 63-152z" horiz-adv-x="785.7" />
+<glyph glyph-name="users" unicode="&#x1f46a;" d="m331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 23 12q35 0 63-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />
+<glyph glyph-name="delete" unicode="&#x2718;" d="m724 112q0-22-15-38l-76-76q-16-15-38-15t-38 15l-164 165-164-165q-16-15-38-15t-38 15l-76 76q-16 16-16 38t16 38l164 164-164 164q-16 16-16 38t16 38l76 76q16 16 38 16t38-16l164-164 164 164q16 16 38 16t38-16l76-76q15-15 15-38t-15-38l-164-164 164-164q15-15 15-38z" horiz-adv-x="785.7" />
+<glyph glyph-name="plus" unicode="&#x2795;" d="m786 439v-107q0-22-16-38t-38-15h-232v-233q0-22-16-37t-38-16h-107q-22 0-38 16t-15 37v233h-232q-23 0-38 15t-16 38v107q0 23 16 38t38 16h232v232q0 22 15 38t38 16h107q23 0 38-16t16-38v-232h232q22 0 38-16t16-38z" horiz-adv-x="785.7" />
+<glyph glyph-name="minus" unicode="&#x2796;" d="m786 439v-107q0-22-16-38t-38-15h-678q-23 0-38 15t-16 38v107q0 23 16 38t38 16h678q22 0 38-16t16-38z" horiz-adv-x="785.7" />
+<glyph glyph-name="help" unicode="&#x2753;" d="m393 149v-134q0-9-7-16t-15-6h-134q-9 0-16 6t-7 16v134q0 9 7 16t16 6h134q8 0 15-6t7-16z m176 335q0-30-8-56t-20-43-31-33-32-25-34-19q-23-13-38-37t-15-37q0-10-7-18t-16-9h-134q-8 0-14 10t-6 21v26q0 46 37 87t79 60q33 15 47 32t14 42q0 23-26 41t-60 18q-36 0-60-16-20-14-60-64-7-9-17-9-7 0-14 4l-91 70q-8 6-9 14t3 16q89 148 259 148 45 0 90-17t81-46 59-72 23-88z" horiz-adv-x="571.4" />
+<glyph glyph-name="home" unicode="&#x2302;" d="m786 296v-267q0-15-11-26t-25-10h-214v214h-143v-214h-214q-15 0-25 10t-11 26v267q0 1 0 2t0 2l321 264 321-264q1-1 1-4z m124 39l-34-41q-5-5-12-6h-2q-7 0-12 3l-386 322-386-322q-7-4-13-4-7 2-12 7l-35 41q-4 5-3 13t6 12l401 334q18 15 42 15t43-15l136-114v109q0 8 5 13t13 5h107q8 0 13-5t5-13v-227l122-102q5-5 6-12t-4-13z" horiz-adv-x="928.6" />
+<glyph glyph-name="attach" unicode="&#x1f4ce;" d="m783 77q0-65-44-109t-109-44q-75 0-131 55l-434 434q-63 64-63 151 0 88 62 150t150 62q88 0 152-63l338-338q5-5 5-12 0-9-17-26t-26-17q-7 0-13 5l-338 339q-44 43-101 43-59 0-100-42t-40-101q0-58 42-101l433-433q35-35 81-35 36 0 59 23t24 59q0 46-36 81l-324 324q-14 14-33 14-16 0-27-11t-11-27q0-18 14-33l229-228q6-6 6-13 0-9-18-26t-26-17q-7 0-12 5l-229 229q-35 34-35 83 0 46 32 78t77 32q49 0 83-36l325-324q55-54 55-131z" horiz-adv-x="785.7" />
+<glyph glyph-name="lock" unicode="&#x1f512;" d="m179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
+<glyph glyph-name="mail" unicode="&#x2709;" d="m1000 454v-443q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v443q25-28 56-49 202-137 278-192 32-24 51-37t53-27 61-13h2q28 0 61 13t53 27 51 37q95 68 278 192 32 22 56 49z m0 164q0-44-27-84t-68-69q-210-146-262-181-5-4-23-17t-30-22-29-18-33-15-27-5h-2q-12 0-27 5t-33 15-29 18-30 22-23 17q-51 35-147 101t-114 80q-35 23-65 64t-31 77q0 43 23 72t66 29h822q36 0 62-26t27-63z" horiz-adv-x="1000" />
+<glyph glyph-name="download" unicode="&#x21d3;" d="m714 100q0 15-10 25t-25 11-26-11-10-25 10-25 26-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-40l-250-250q-10-10-25-10t-25 10l-250 250q-17 17-8 40 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />
+<glyph glyph-name="edit" unicode="&#x270e;" d="m203-7l50 51-131 131-51-51v-60h72v-71h60z m291 518q0 12-12 12-5 0-9-4l-303-302q-4-4-4-10 0-12 13-12 5 0 9 4l303 302q3 4 3 10z m-30 107l232-232-464-465h-232v233z m381-54q0-29-20-50l-93-93-232 233 93 92q20 21 50 21 29 0 51-21l131-131q20-22 20-51z" horiz-adv-x="857.1" />
+<glyph glyph-name="print" unicode="&#x2399;" d="m214-7h500v143h-500v-143z m0 357h500v214h-89q-22 0-38 16t-16 38v89h-357v-357z m643-36q0 15-10 25t-26 11-25-11-10-25 10-25 25-10 26 10 10 25z m72 0v-232q0-7-6-12t-12-6h-125v-89q0-22-16-38t-38-16h-536q-22 0-37 16t-16 38v89h-125q-7 0-13 6t-5 12v232q0 44 32 76t75 31h36v304q0 22 16 38t37 16h375q23 0 50-12t42-26l85-85q15-16 27-43t11-49v-143h35q45 0 76-31t32-76z" horiz-adv-x="928.6" />
+<glyph glyph-name="alert" unicode="&#x26a0;" d="m571 83v106q0 8-5 13t-12 5h-108q-7 0-12-5t-5-13v-106q0-8 5-13t12-6h108q7 0 12 6t5 13z m-1 208l10 257q0 6-5 10-7 6-14 6h-122q-7 0-14-6-5-4-5-12l9-255q0-5 6-9t13-3h103q8 0 13 3t6 9z m-7 522l428-786q20-35-1-70-10-17-26-26t-35-10h-858q-18 0-35 10t-26 26q-21 35-1 70l429 786q9 17 26 27t36 10 36-10 27-27z" horiz-adv-x="1000" />
+<glyph glyph-name="menu" unicode="&#x1d362;" d="m857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
+<glyph glyph-name="settings" unicode="&#x2638;" d="m571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 15 20 15h124q7 0 13-4t7-12l15-103q28-9 50-21l80 60q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-13 0-6-4-12-9-12-29-38t-30-39q14-28 23-55l102-15q7-1 12-7t4-13z" horiz-adv-x="857.1" />
+<glyph glyph-name="down" unicode="&#x2193;" d="m571 457q0-14-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 11-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />
+<glyph glyph-name="up" unicode="&#x2191;" d="m571 171q0-14-10-25t-25-10h-500q-15 0-25 10t-11 25 11 26l250 250q10 10 25 10t25-10l250-250q10-11 10-26z" horiz-adv-x="571.4" />
+<glyph glyph-name="logout" unicode="&#x291d;" d="m857 350q0-87-34-166t-91-137-137-92-166-34-167 34-136 92-92 137-34 166q0 102 45 191t126 151q24 18 54 14t46-28q18-23 14-53t-28-47q-54-41-84-101t-30-127q0-58 22-111t62-91 91-61 111-23 110 23 92 61 61 91 22 111q0 68-30 127t-84 101q-24 18-28 47t14 53q17 24 47 28t53-14q81-61 126-151t45-191z m-357 429v-358q0-29-21-50t-50-21-51 21-21 50v358q0 29 21 50t51 21 50-21 21-50z" horiz-adv-x="857.1" />
+<glyph glyph-name="unlock" unicode="&#x1f513;" d="m929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" />
+</font>
+</defs>
+</svg>
\ 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 (file)
index 0000000..f865605
Binary files /dev/null and b/www/admin/static/font/garradin.ttf differ
diff --git a/www/admin/static/font/garradin.woff b/www/admin/static/font/garradin.woff
new file mode 100644 (file)
index 0000000..cbfe8e5
Binary files /dev/null and b/www/admin/static/font/garradin.woff differ
diff --git a/www/admin/static/garradin.png b/www/admin/static/garradin.png
new file mode 100644 (file)
index 0000000..c8c3de5
Binary files /dev/null and b/www/admin/static/garradin.png differ
diff --git a/www/admin/static/gibberish-aes.min.js b/www/admin/static/gibberish-aes.min.js
new file mode 100644 (file)
index 0000000..ccaa2e5
--- /dev/null
@@ -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;T<V.length;T++){W[T]=V[T]}return W},l=function(X,V){var T="",W,U;if(V){W=X[15];if(W>16){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<V.length;U++){T+=(V[U]<16?"0":"")+V[U].toString(16)}return T},G=function(U){var T=[];U.replace(/(..)/g,function(V){T.push(parseInt(V,16))});return T},o=function(T,W){var V=[],U;if(!W){T=S(T)}for(U=0;U<T.length;U++){V[U]=T.charCodeAt(U)}return V},d=function(T){switch(T){case 128:p=10;w=4;break;case 192:p=12;w=6;break;case 256:p=14;w=8;break;default:throw ("Invalid Key Size Specified:"+T)}},u=function(U){var T=[],V;for(V=0;V<U;V++){T=T.concat(Math.floor(Math.random()*256))}return T},r=function(X,Z){var aa=p>=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<aa;W++){T[W]=GibberishAES.Hash.MD5(T[W-1].concat(U));ab=ab.concat(T[W])}Y=ab.slice(0,4*w);V=ab.slice(4*w,4*w+16);return{key:Y,iv:V}},c=function(X,W,U){W=K(W);var Z=Math.ceil(X.length/16),Y=[],V,T=[];for(V=0;V<Z;V++){Y[V]=F(X.slice(V*16,V*16+16))}if(X.length%16===0){Y.push([16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16]);Z++}for(V=0;V<Y.length;V++){Y[V]=(V===0)?E(Y[V],U):E(Y[V],T[V-1]);T[V]=e(Y[V],W)}return T},A=function(Z,aa,V,W){aa=K(aa);var ab=Z.length/16,U=[],X,T=[],Y="";for(X=0;X<ab;X++){U.push(Z.slice(X*16,(X+1)*16))}for(X=U.length-1;X>=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<ab-1;X++){Y+=l(T[X])}Y+=l(T[X],true);return W?Y:P(Y)},e=function(W,V){g=false;var U=N(W,V,0),T;for(T=1;T<(p+1);T++){U=O(U);U=a(U);if(T<p){U=R(U)}U=N(U,V,T)}return U},J=function(W,V){g=true;var U=N(W,V,p),T;for(T=p-1;T>-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;X<w;X++){Z=[Y[4*X],Y[4*X+1],Y[4*X+2],Y[4*X+3]];T[X]=Z}for(X=w;X<(4*(p+1));X++){T[X]=[];for(W=0;W<4;W++){U[W]=T[X-1][W]}if(X%w===0){U=y(x(U));U[0]^=L[X/w-1]}else{if(w>6&&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;i<V.length;i+=U){T[i/U]=parseInt(V.substr(i,U),16)}return T},j=function(T){var U=[];for(i=0;i<T.length;i++){U[T[i]]=i}return U},q=function(U,T){var W,V;V=0;for(W=0;W<8;W++){V=((T&1)==1)?V^U:V;U=(U>127)?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<<ar)|(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<av){az=(aw-(aw%4))/4;ar=(aw%4)*8;aA[az]=(aA[az]|(ay[aw]<<ar));aw++}az=(aw-(aw%4))/4;ar=(aw%4)*8;aA[az]=aA[az]|(128<<ar);aA[ax-2]=av<<3;aA[ax-1]=av>>>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<ab.length;ah+=16){ai=aq;U=ap;aa=ao;ag=am;aq=ae(aq,ap,ao,am,ab[ah+0],7,Z[4]);am=ae(am,aq,ap,ao,ab[ah+1],12,Z[5]);ao=ae(ao,am,aq,ap,ab[ah+2],17,Z[6]);ap=ae(ap,ao,am,aq,ab[ah+3],22,Z[7]);aq=ae(aq,ap,ao,am,ab[ah+4],7,Z[8]);am=ae(am,aq,ap,ao,ab[ah+5],12,Z[9]);ao=ae(ao,am,aq,ap,ab[ah+6],17,Z[10]);ap=ae(ap,ao,am,aq,ab[ah+7],22,Z[11]);aq=ae(aq,ap,ao,am,ab[ah+8],7,Z[12]);am=ae(am,aq,ap,ao,ab[ah+9],12,Z[13]);ao=ae(ao,am,aq,ap,ab[ah+10],17,Z[14]);ap=ae(ap,ao,am,aq,ab[ah+11],22,Z[15]);aq=ae(aq,ap,ao,am,ab[ah+12],7,Z[16]);am=ae(am,aq,ap,ao,ab[ah+13],12,Z[17]);ao=ae(ao,am,aq,ap,ab[ah+14],17,Z[18]);ap=ae(ap,ao,am,aq,ab[ah+15],22,Z[19]);aq=an(aq,ap,ao,am,ab[ah+1],5,Z[20]);am=an(am,aq,ap,ao,ab[ah+6],9,Z[21]);ao=an(ao,am,aq,ap,ab[ah+11],14,Z[22]);ap=an(ap,ao,am,aq,ab[ah+0],20,Z[23]);aq=an(aq,ap,ao,am,ab[ah+5],5,Z[24]);am=an(am,aq,ap,ao,ab[ah+10],9,Z[25]);ao=an(ao,am,aq,ap,ab[ah+15],14,Z[26]);ap=an(ap,ao,am,aq,ab[ah+4],20,Z[27]);aq=an(aq,ap,ao,am,ab[ah+9],5,Z[28]);am=an(am,aq,ap,ao,ab[ah+14],9,Z[29]);ao=an(ao,am,aq,ap,ab[ah+3],14,Z[30]);ap=an(ap,ao,am,aq,ab[ah+8],20,Z[31]);aq=an(aq,ap,ao,am,ab[ah+13],5,Z[32]);am=an(am,aq,ap,ao,ab[ah+2],9,Z[33]);ao=an(ao,am,aq,ap,ab[ah+7],14,Z[34]);ap=an(ap,ao,am,aq,ab[ah+12],20,Z[35]);aq=V(aq,ap,ao,am,ab[ah+5],4,Z[36]);am=V(am,aq,ap,ao,ab[ah+8],11,Z[37]);ao=V(ao,am,aq,ap,ab[ah+11],16,Z[38]);ap=V(ap,ao,am,aq,ab[ah+14],23,Z[39]);aq=V(aq,ap,ao,am,ab[ah+1],4,Z[40]);am=V(am,aq,ap,ao,ab[ah+4],11,Z[41]);ao=V(ao,am,aq,ap,ab[ah+7],16,Z[42]);ap=V(ap,ao,am,aq,ab[ah+10],23,Z[43]);aq=V(aq,ap,ao,am,ab[ah+13],4,Z[44]);am=V(am,aq,ap,ao,ab[ah+0],11,Z[45]);ao=V(ao,am,aq,ap,ab[ah+3],16,Z[46]);ap=V(ap,ao,am,aq,ab[ah+6],23,Z[47]);aq=V(aq,ap,ao,am,ab[ah+9],4,Z[48]);am=V(am,aq,ap,ao,ab[ah+12],11,Z[49]);ao=V(ao,am,aq,ap,ab[ah+15],16,Z[50]);ap=V(ap,ao,am,aq,ab[ah+2],23,Z[51]);aq=ad(aq,ap,ao,am,ab[ah+0],6,Z[52]);am=ad(am,aq,ap,ao,ab[ah+7],10,Z[53]);ao=ad(ao,am,aq,ap,ab[ah+14],15,Z[54]);ap=ad(ap,ao,am,aq,ab[ah+5],21,Z[55]);aq=ad(aq,ap,ao,am,ab[ah+12],6,Z[56]);am=ad(am,aq,ap,ao,ab[ah+3],10,Z[57]);ao=ad(ao,am,aq,ap,ab[ah+10],15,Z[58]);ap=ad(ap,ao,am,aq,ab[ah+1],21,Z[59]);aq=ad(aq,ap,ao,am,ab[ah+8],6,Z[60]);am=ad(am,aq,ap,ao,ab[ah+15],10,Z[61]);ao=ad(ao,am,aq,ap,ab[ah+6],15,Z[62]);ap=ad(ap,ao,am,aq,ab[ah+13],21,Z[63]);aq=ad(aq,ap,ao,am,ab[ah+4],6,Z[64]);am=ad(am,aq,ap,ao,ab[ah+11],10,Z[65]);ao=ad(ao,am,aq,ap,ab[ah+2],15,Z[66]);ap=ad(ap,ao,am,aq,ab[ah+9],21,Z[67]);aq=ac(aq,ai);ap=ac(ap,U);ao=ac(ao,aa);am=ac(am,ag)}return T(aq).concat(T(ap),T(ao),T(am))},H=function(Y,X,U){Y=o(Y);X=o(X);for(var W=X.length;W<32;W++){X[W]=0}if(U==null){U=genIV()}else{U=o(U);for(var W=U.length;W<16;W++){U[W]=0}}var V=c(Y,X,U);var T=[U];for(var W=0;W<V.length;W++){T[T.length]=V[W]}return M.encode(T)},z=function(Y,X){var W=M.decode(Y);var T=W.slice(0,16);var V=W.slice(16,W.length);X=o(X);for(var U=X.length;U<32;U++){X[U]=0}var Z=A(V,X,T,false);return Z},M=(function(){var T="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",V=T.split(""),U=function(X,ab){var ac=[],aa="",Z,Y;totalChunks=Math.floor(X.length*16/3);for(Z=0;Z<X.length*16;Z++){ac.push(X[Math.floor(Z/16)][Z%16])}for(Z=0;Z<ac.length;Z=Z+3){aa+=V[ac[Z]>>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<Y.length;Z=Z+4){ab[0]=T.indexOf(Y.charAt(Z));ab[1]=T.indexOf(Y.charAt(Z+1));ab[2]=T.indexOf(Y.charAt(Z+2));ab[3]=T.indexOf(Y.charAt(Z+3));X[0]=(ab[0]<<2)|(ab[1]>>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 (file)
index 0000000..1e44d4e
--- /dev/null
@@ -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 (file)
index 0000000..1229829
--- /dev/null
@@ -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 (file)
index 0000000..f9b5b60
--- /dev/null
@@ -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 (file)
index 0000000..3f7bbe7
--- /dev/null
@@ -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<pass.length; i++) {
+               letters[pass[i]] = (letters[pass[i]] || 0) + 1;
+               score += 5.0 / letters[pass[i]];
+           }
+
+           // bonus points for mixing it up
+           var variations = {
+               digits: /\d/.test(pass),
+               lower: /[a-z]/.test(pass),
+               upper: /[A-Z]/.test(pass),
+               nonWords: /\W/.test(pass),
+           }
+
+           variationCount = 0;
+           for (var check in variations) {
+               variationCount += (variations[check] == true) ? 1 : 0;
+           }
+           score += (variationCount - 1) * 10;
+
+           return parseInt(score);
+       }
+
+       function checkPasswordStrength() {
+           if (pw_elm.value == '')
+           {
+               strength_elm.className = strength_elm.className.split(' ')[0];
+               strength_elm.innerHTML = '';
+               return true;
+           }
+
+           var score = scorePassword(pw_elm.value);
+
+           if (score > 80)
+           {
+               strength_elm.className = strength_elm.className.split(' ')[0] + ' ok';
+               strength_elm.innerHTML = 'Sécurité : <b>forte</b>';
+           }
+           else if (score > 60)
+           {
+               strength_elm.className = strength_elm.className.split(' ')[0] + ' medium';
+               strength_elm.innerHTML = 'Sécurité : <b>moyenne</b>';
+           }
+           else if (score >= 30)
+           {
+               strength_elm.className = strength_elm.className.split(' ')[0] + ' weak';
+               strength_elm.innerHTML = 'Sécurité : <b>mauvaise</b>';
+           }
+           else
+           {
+               strength_elm.className = strength_elm.className.split(' ')[0] + ' fail';
+               strength_elm.innerHTML = 'Sécurité : <b>aucune</b>';          
+           }
+
+           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 = '&#10003;';
+               }
+       }
+}());
\ No newline at end of file
diff --git a/www/admin/static/print.css b/www/admin/static/print.css
new file mode 100644 (file)
index 0000000..31b490a
--- /dev/null
@@ -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 (file)
index 0000000..357102c
--- /dev/null
@@ -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 (file)
index 0000000..2ad8806
--- /dev/null
@@ -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(/<BOUCLE(\d+|_[a-zA-Z0-9_-]+)\(([A-Z]+)\)(.*?)>/))
+               {
+                       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(/<INCLURE\{(.*?)\}>/))
+               {
+                       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 += '<a href="' + doc_url + '#' + doc[i].link + '" onclick="return !window.open(this.href);">' + doc[i].title + '</a>';
+                       else if (doc[i].tag)
+                               help.innerHTML += '<' + tag + '>' + doc[i].title + '</' + tag + '>';
+                       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 (file)
index 0000000..e0eb19e
--- /dev/null
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+                       .replace(/'/g, '&#039;').replace(/"/g, '&quot');
+
+               // HTML simple
+               content = content.replace(/&lt;(\/?(del|pre|ins|b|i|strong|em|h\d|code|samp|tt))&gt;/g, '<$1>');
+               console.log(content);
+
+               // Intertitres
+               content = content.replace(/\{{3}([^\n]*)\}{3}/g, '<h3>$1</h3>');
+
+               // Gras
+               content = content.replace(/\{{2}([^\n]*)\}{2}/g, '<strong>$1</strong>');
+
+               // Italique
+               content = content.replace(/\{([^\n]*)\}/g, '<em>$1</em>');
+
+               // Espaces typograhiques
+               content = content.replace(/\h*([?!;:»])(\s+|$)/g, '&nbsp;$1$2');
+               content = content.replace(/(^|\s+)([«])\h*/g, '$1$2&nbsp;');
+
+               // Liens
+               content = content.replace(/\[([^-]+)-&gt;([^\]]+)\]/g, '<a href="$2">$1</a>');
+               content = content.replace(/\[([^\]]+)\]/g, '<a href="$1">$1</a>');
+
+               // nl2br
+               content = content.replace(/\r/g, '').replace(/\n/g, '<br />');
+
+               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 = '&bull;'.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 = '&bull;'.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 (file)
index 0000000..cb00331
--- /dev/null
@@ -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, '<del>', '</del>'); } );
+               addBtn('code', 'Chasse fixe', function () { selection.wrap(txt, '<pre>', '</pre>'); } );
+               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 (file)
index 0000000..94564f6
--- /dev/null
@@ -0,0 +1,224 @@
+<?php
+namespace Garradin;
+
+const UPGRADE_PROCESS = true;
+
+require_once __DIR__ . '/../../include/init.php';
+
+$config = Config::getInstance();
+
+$v = $config->getVersion();
+
+if (version_compare($v, garradin_version(), '>='))
+{
+    throw new UserException("Pas de mise à jour à faire.");
+}
+
+$db = DB::getInstance();
+$redirect = true;
+
+echo '<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
+    <link rel="stylesheet" type="text/css" href="static/admin.css" media="all" />
+    <script type="text/javascript" src="static/loader.js"></script>
+    <title>Mise à jour</title>
+</head>
+<body>
+<div class="header"><h1>Mise à jour de Garradin '.$config->getVersion().' vers la version '.garradin_version().'...</h1></div>
+<div class="page">
+<div id="loader" class="loader" style="margin: 2em 0; height: 50px;"></div>
+<script>
+animatedLoader(document.getElementById("loader"), 5);
+</script>';
+
+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 '<h2>Mise à jour terminée.</h2>
+<p><a href="'.WWW_URL.'admin/">Retour</a></p>';
+
+if ($redirect)
+{
+    echo '
+    <script type="text/javascript">
+    window.setTimeout(function () { 
+        window.location.href = "'.WWW_URL.'admin/"; 
+        stopAnimatedLoader();
+    }, 1000);
+    </script>';
+}
+
+echo '
+</body>';
+
+?>
\ 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 (file)
index 0000000..9c260ad
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+if ((trim(utils::get('parent')) == '') || !is_numeric(utils::get('parent')))
+{
+    throw new UserException('Numéro de page parent invalide.');
+}
+
+$parent = (int) utils::get('parent');
+
+$tpl->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 = '<ul>';
+
+    foreach ($tree as $node)
+    {
+        $out .= '<li'.(utils::get('parent') == $node['id'] ? ' class="current"' : '').'><h3><a href="?parent='.(int)$node['id'].'">'.htmlspecialchars($node['titre'], ENT_QUOTES, 'UTF-8', false).'</a></h3>';
+
+        if (!empty($node['children']))
+        {
+            $out .= tpl_display_tree($node['children']);
+        }
+
+        $out .= '</li>';
+    }
+
+    $out .= '</ul>';
+
+    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 (file)
index 0000000..3057083
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+namespace Garradin;
+require_once __DIR__ . '/../_inc.php';
+
+if ($user['droits']['wiki'] < Membres::DROIT_ACCES)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+$wiki = new Wiki;
+$wiki->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 (file)
index 0000000..3425739
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$q = trim(utils::get('q'));
+
+$tpl->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('!&lt;(/?b)&gt;!', '<$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 (file)
index 0000000..a90603e
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$error = false;
+$parent = (int) utils::get('parent') ?: 0;
+
+if (!empty($_POST['create']))
+{
+    if (!utils::CSRF_check('wiki_create'))
+    {
+        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
+    }
+    else
+    {
+        try {
+            $id = $wiki->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 (file)
index 0000000..1851500
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+if ($user['droits']['wiki'] < Membres::DROIT_ECRITURE)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+if (!utils::get('id') || !is_numeric(utils::get('id')))
+{
+    throw new UserException('Numéro de page invalide.');
+}
+
+$page = $wiki->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 (file)
index 0000000..2da386c
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+if (!trim(utils::get('id')))
+{
+    throw new UserException("Page inconnue.");
+}
+
+$page = $wiki->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 (file)
index 0000000..b0b906e
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace Garradin;
+require_once __DIR__ . '/_inc.php';
+
+if (!empty($_SERVER['QUERY_STRING']))
+{
+    $page = $wiki->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 (file)
index 0000000..32d3cdf
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+$page = (int) utils::get('p') ?: 1;
+
+$tpl->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 (file)
index 0000000..3bb7a48
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+namespace Garradin;
+
+require_once __DIR__ . '/_inc.php';
+
+if ($user['droits']['wiki'] < Membres::DROIT_ADMIN)
+{
+    throw new UserException("Vous n'avez pas le droit d'accéder à cette page.");
+}
+
+if (!trim(utils::get('id')))
+{
+    throw new UserException("Page inconnue.");
+}
+
+$page = $wiki->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 (file)
index 0000000..711e401
--- /dev/null
@@ -0,0 +1,9 @@
+<?php
+namespace Garradin;
+
+require __DIR__ . '/_inc.php';
+
+$squelette = new Squelette;
+$squelette->dispatchURI();
+
+?>
diff --git a/www/squelettes-dist/article.html b/www/squelettes-dist/article.html
new file mode 100644 (file)
index 0000000..7fdd688
--- /dev/null
@@ -0,0 +1,29 @@
+<INCLURE{entete.html}>
+
+<B_article>
+       <article>
+           <BOUCLE_article(ARTICLES){uri}>
+           <h1>#TITRE</h1>
+           <div class="corps">
+               [(#TEXTE*|formatter_texte)]
+           </div>
+           </BOUCLE_article>
+       </article>
+
+       <B_enfants>
+       <section class="articles">
+           <BOUCLE_enfants(PAGES){parent}{par uri}>
+           <article>
+               <h3><a href="#URL">#TITRE</a></h3>
+               <p>[(#TEXTE|supprimer_spip|supprimer_tags|couper{200})]</p>
+           </article>
+           </BOUCLE_enfants>
+       </section>
+       </B_enfants>
+</B_article>
+<p class="error">
+    Cette page n'existe pas.
+</p>
+<//B_article>
+
+<INCLURE{pied.html}>
\ No newline at end of file
diff --git a/www/squelettes-dist/atom.xml b/www/squelettes-dist/atom.xml
new file mode 100644 (file)
index 0000000..9dfb274
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="fr">
+
+    <title type="html">[(#NOM_ASSO|echapper_xml)]</title>
+    <link href="[(#URL_ATOM|echapper_xml)]" rel="self"/>
+    <link href="[(#URL_RACINE|echapper_xml)]" rel="alternate" type="text/html" title="[(#NOM_ASSO|echapper_xml)]" />
+
+    <BOUCLE_derniere(PAGES){par date_creation}{inverse}{0,1}>
+    <updated>[(#DATE_CREATION|date_atom)]</updated>
+    </BOUCLE_derniere>
+
+    <author>
+        <name>[(#NOM_ASSO|echapper_xml)]</name>
+    </author>
+
+    <id>[(#URL_RACINE|echapper_xml)]</id>
+    <generator uri="http://dev.kd2.org/garradin/">Garradin</generator>
+
+    <BOUCLE_actu(PAGES){par date_creation}{inverse}{0,20}>
+    <entry>
+        <title>[(#TITRE|echapper_xml)]</title>
+        <link href="[(#URL|echapper_xml)]" rel="alternate" type="text/html" title="[(#TITRE|echapper_xml)]" />
+        <id>[(#URL|echapper_xml)]</id>
+        <updated>[(#DATE_CREATION|date_atom)]</updated>
+        <author><name>[(#NOM_ASSO|echapper_xml)]</name></author>
+        <content type="html">
+            [(#TEXTE*|formatter_texte|echapper_xml)]
+        </content>
+    </entry>
+    </BOUCLE_actu>
+
+</feed>
\ No newline at end of file
diff --git a/www/squelettes-dist/default.css b/www/squelettes-dist/default.css
new file mode 100644 (file)
index 0000000..8e0f0ca
--- /dev/null
@@ -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,iVBORw0KGgoAAAANSUhEUgAAAXMAAADPAQMAAAA9C6NrAAAABlBMVEXx9PD+//zYDo7WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gEIER05obn78wAACb1JREFUaN7V2TuOHLkZAGBSHKgmMEyFCgRRR1CoYD2lo+gI42wAyyoKCjbUEXZvshQUONwb2BwoULhcOHDJLhT9v/io6u7ZrXAFaKa76xsWi4+fP9kq13+K/+m84o8POUcN/+XTkU1UzculIS/o3+ecwC/i3alfypWEHmzCi54/tadeLk14I4N+htdYSqC7nvrE1cG/M6tCP/6c848WvTnjMxePv8gvavxcPtY7H7EJEhW/4l3Yu5j5/d7Ts8K9lby2K9xoVTZOWC2+2PvYtRk+hit+lMfCenaemsa0vx1XepAhQjuGrsPEr6q7J16fVgTgh9Lvrvdz/xneK5P3JppS1sbXoSBeV6+Lt73HKrwsnp6EfNB43XdliccPbqQMan746XCARh4etTHY0y1d4DZYqLLkk4rw2Xziqbfc0p59ZD/DUHN4d7iP7vzcjVh6k4t/jR8D8kp1PrYHyjz6sW9lZHyRB5yaD13v4h8P85TR06OXIdd5r65q79JI9ewz+bE8U/VQeB1RfDPxOHNwCKXSwVEGMDTAxq9T9WHgz2z1cDe9wHtqE+7OdfKW66aD5v5sPknlZ/I0m9axecX9OVQfpfKJnon8wh5LCnCtznjyQZ4eO5/LMosLVvoOu3guMziWkATyZyypeEt+IW9ljBcv0816iU9QV/H4x6/VEKZZHjEqKRHu7VSJDsrOhjzPRRPqjI+KBTx8fK5UHXuzjrbFgbjzz2nyPWvezSraOhJ1rBEiUpjkWz+jR0riX9gaUlWsMx59oq5Y1CtqYhLjLP05n/ER3ccEE96UGlTPa8LrGiHY67zYCH4ofkrd5C9+EB/AJwjyL+gTarvM/j91eem9h1ceOusFFdn7VILnpv54CZpUahhoqIgfpYE27U/R+3kpwdNQFD+UyNqNT3ikvyzQ+NIjnmqKfp1SjZ7d/IUm+zarp7237MdZhn4L9uT1fVJPSg3rEmwXN/OrbXyelfmAnSVF8H3IW5zUS188+8cwAF+c96asDW29TuraBPK6eBo3boZFVW+XX/QY/3xtsZUvkjeZb/m6xqbiW4fzakEeWtOXPKblMNhgQ2uClX+Tx2vunH/cBgj6MWCrOLq3PfFQ96vWg+ihIuynVFeFc36qycUCfgS/1FWh+TYAc/Nv2a+1Ic94XSegmW/wKeCGXu39uh2w4p+Lj7XlT32b4CZZ8XOdVcUvm/km4TPB+jR5SoFkDp3xU+e1R49RT+ZozSfn7nFjCbe0glC4DiVmnHgc6r2HWkxzrjFs3NfHQTEllaieWzn286tMuEyds0iqorBjpoUrkjb5SZDWT9VDU8Jrpf6+cDOkfr7LIoLVMeIn8XcrV3xW+/xBUdwa8i/V4+S6y+wXtckHsEo/4Yc2f+RE17O/nch9Xbbre82T3Dos1BOeMmjwHt2XvF3fWx6zDLz4iY8j9ew9VVjvPA6SmbzF15BxqzBiMIBurw3UeWrSgbNDTzGbvclxWvt4XjN6m+PALc5ee4gR6Mc6Ipqf8QM/cI96ytA1rqtQb8zIfFvvyuNqTJhpL4VXqQ6O3kZXcoDOc2aKfug8/kBfEqDmObQaegxqK4507Fe19yu+XclTbbX4gN7WLU/1Cye16POXr0EZ8dBAU7S8DPd+VjS7wet1+Fw87SUnWCYlKW0+cdJvqCHeB3pDjw71Ax8lvlYfuPXIWx8oGyUf0P+S9p5yvuIhwXDFJ/T/KDOgeqqf+OeyY+Aqov+8ygzYeHg4gxe+a9tOrCcsfJ/zztMyt75l/4rbrnnzKcseuN9vmuWO/a0q66jBhgEfqIE6P1OSKuW/4b6vfjGeElbTPG0I5lfsA3uc9F+LX0/9mNhrz2OXfCIPzRU4Inf73wm8Ri/BA/1nSmwe0eZw52FePKXyH7VMy3yYeebTrk01T1UWfyVzFRf+9zNPAiebkG5/DWP9mp9XYgF6vbC3uRxNVA9RoXmp42NcDsmvew+N5a+K11vvpfyxeu6gK4qyLVaqP6mJ/bD3dcC0pTiR9zSJB9nktPMBHsCmLB4fZVWZ0ML/f9GE6rzp/Zht2QlgChTV8DH2fuHRr6Se8NrKKjRiCgT++7T3U/OY3xc/U3Y88ETdlD9XD11tv4lf2eu19xSGm8f9wDdZWrPCJP0x54UnniOZg91yKj5g0HpMa33vTechsNjEi4r4K5oB1WN0+1+qHrdkxUcJdXSc0nn7a/W4HcGTouqzHHls/H0qSx3eFr2vnob73DzO6lA9Lic2up3npUm1Mwea87TU4QCWRegmcdA98dnH4mnLFGltUe8Shyla7zsP6csbbj4eeeixgLe3HKTV8AMtZSU/gYn1hmZc4D3LGGlTJR77agi9t83TCdEke7U78lCAwV1z87C2vKR5HmhHDN7QgLjjXSuesLzqfSavOeHEksTfvpTY+AgmJyyV7D3e/Al7KztcnnDxZduF7jxH4DDR4+eZj1niE2XrPr55U/195h3xzGlHYC9Z7Vg87oY47nzNvCNeNNXCP1Xt4KJ6Jd6WaOR+WhRHefF+58fOw3PejCu3q7pW3UGE23lXZv/dzTtVTh+6g4VLXvubqSTrctOd/1tqh6hY7LuxnNxIIeGcn5p3fqqZeDszrP6ve2/9VAptGYOt55m/thyf2mI1YdoeG8eNv48UN2dXvHpdNgqupSRD9Z8i7zHKkctKlZt7P/c+cEIRZDuk2S/dVwU5/7d57XkP5PXGrxvfnd+yd7wfFT+2o7lTL9FByXmfWesaf9Ybme1Ui87Hdi668Y/owCKf+PSQ1+Jl9zKWhe2MLxvsvV/bue6pt7LOe84xRuzth7zL3J68guHqT4n2RT/tfTA5bk8UNh7bWxLPkY5aPC6aFz0EQBdNOfxGjwd/lz3EKJs4uFLcpd5etycivYeczq5jacOAqYCi0HfBTzz6Kbn5hofF5MMlr3Pzw6d/Fx8vefg8lTPMP+uUxM/DBW87/8Skl+KXSx739a68M/GaRje8tRc8/p6ap9mAbf/xvG+P1bzfnS713p7xwZz3eTNL+VsOasvhss9n/OwuerPzu3F/4t32a0IblH7QT8e83n0N6X7D250fozIP+fGY93nnp6iGB/z3e58f9j/svD49KT33/Wxum6VjHnM9d8Bb+S7p93r3z9sDHkLPs6e7g9KH/dvvrs8Ozwtex1fqiMcvX/Uhf3vERzxSMId8OD98HvD2kPdHfMLU2h304xH/6KDfZQ2/4WelLgyHy14f9OagHw74ZbN8/D7vDvrxgF8vNv9ln495fcTni81/0Q8HvTvox0PeX2rOiz4f8+aYD8NB7w768ZiP0zF/n4/5rwd9/uP4/wOHV4ghb+WqVQAAAABJRU5ErkJggg==") 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 (file)
index 0000000..aebe645
--- /dev/null
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="fr">
+<head>
+    <meta charset="utf-8" />
+    <title>[(#TITRE) - ]#NOM_ASSO</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
+    <link rel="stylesheet" type="text/css" href="[(#URL_SITE)]squelettes-dist/default.css" media="screen,projection,handheld" />
+    <link rel="alternate" type="application/atom+xml" title="Actualité de #NOM_ASSO" href="#URL_ATOM" />
+    <!--[if lte IE 8]>
+    <script type="text/javascript">
+       'article aside footer header nav section time'.replace(/\w+/g,function(n){document.createElement(n)})
+       </script>
+    <![endif]-->
+</head>
+
+<body>
+
+<header class="nav">
+       <nav>
+               <ul>
+                       <li class="current"><a href="#URL_RACINE">Accueil</a></li>
+                       <li><a href="#URL_ADMIN">Administration</a></li>
+               </ul>
+       </nav>
+</header>
+
+<header class="main">
+    <h1><a href="#URL_RACINE">#NOM_ASSO</a></h1>
+    [<h4>(#ADRESSE_ASSO)</h4>]
+
+       <B_menu>
+       <nav>
+               <ul>
+               <BOUCLE_menu(RUBRIQUES){parent=0}{par titre}>
+                   <li><a href="#URL/">#TITRE</a></li>
+               </BOUCLE_menu>
+               </ul>
+       </nav>
+       </B_menu>
+</header>
+
+<section class="page">
\ No newline at end of file
diff --git a/www/squelettes-dist/pied.html b/www/squelettes-dist/pied.html
new file mode 100644 (file)
index 0000000..cde5e12
--- /dev/null
@@ -0,0 +1,8 @@
+</section>
+
+<footer class="main">
+    Propulsé par <a href="http://garradin.eu/" id="garradin">Garradin</a> — logiciel libre de gestion associative
+</footer>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/www/squelettes-dist/rubrique.html b/www/squelettes-dist/rubrique.html
new file mode 100644 (file)
index 0000000..f69d8c5
--- /dev/null
@@ -0,0 +1,31 @@
+<BOUCLE_rubrique(RUBRIQUES){uri}>
+
+       <INCLURE{entete.html}>
+
+       <article class="rubrique">
+           <h1>#TITRE</h1>
+           <div class="corps">
+               [(#TEXTE*|formatter_texte)]
+           </div>
+       </article>
+
+       <section class="articles">
+       <BOUCLE_articles(ARTICLES){rubrique}{par uri}>
+           <article>
+           <h3><a href="#URL">#TITRE</a></h3>
+           <p>[(#TEXTE|supprimer_spip|supprimer_tags|couper{200})]</p>
+               </article>
+       </BOUCLE_articles>
+       </section>
+
+       <INCLURE{pied.html}>
+
+</BOUCLE_rubrique>
+
+       <INCLURE{entete.html}>
+       <p class="error">
+           Cette page n'existe pas.
+       </p>
+       <INCLURE{pied.html}>
+
+<//B_rubrique>
diff --git a/www/squelettes-dist/sommaire.html b/www/squelettes-dist/sommaire.html
new file mode 100644 (file)
index 0000000..7facebc
--- /dev/null
@@ -0,0 +1,25 @@
+<INCLURE{entete.html}>
+
+<BOUCLE_une(ARTICLES){par date}{inverse}{0,1}>
+<section class="articles">
+    <article>
+    <h1><a href="#URL">#TITRE</a></h1>
+    <h5>Posté : [(#DATE_CREATION|date_intelligente)]</h5>
+    <p>[(#TEXTE|formatter_texte)]</p>
+       </article>
+</section>
+</BOUCLE_une>
+
+<B_derniers>
+       <section class="articles">
+       <BOUCLE_derniers(ARTICLES){par date}{inverse}{1,9}>
+           <article>
+           <h3><a href="#URL">#TITRE</a></h3>
+           <h5>Posté : [(#DATE_CREATION|date_intelligente)]</h5>
+           <p>[(#TEXTE|supprimer_spip|supprimer_tags|couper{200})]</p>
+               </article>
+       </BOUCLE_derniers>
+       </section>
+</B_derniers>
+
+<INCLURE{pied.html}>
\ No newline at end of file