Comment: | Merge dev branch |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | invoice_module |
Files: | files | file ages | folders |
SHA3-256: |
e292d08439f78c6626cf5543d9b91a00 |
User & Date: | alinaar on 2023-02-19 18:00:26 |
Other Links: | branch diff | manifest | tags |
2023-02-19
| ||
18:09 | Invoice module: zero quantity allowed check-in: 44c55767f6 user: alinaar tags: invoice_module | |
18:00 | Merge dev branch check-in: e292d08439 user: alinaar tags: invoice_module | |
15:01 | Invoice module: quotation's full edition implemented check-in: 33da505a8c user: alinaar tags: invoice_module | |
03:13 | Improve code editor: add save without closing, use dark theme, remove PNG icons check-in: 2b83852c0a user: bohwaz tags: dev | |
Name change from debian/config.debian.php to build/debian/config.debian.php.
︙ | ︙ |
Modified build/debian/makedeb.sh from [0add980db8] to [ed98f1ed48].
1 2 3 4 5 6 7 8 9 | #!/bin/bash # Ripped from fossil makdedeb.sh DEB_REV=${1-1} # .deb package build/revision number. PACKAGE_DEBNAME=paheko THISDIR=${PWD} DEB_ARCH_NAME=all | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #!/bin/bash # Ripped from fossil makdedeb.sh DEB_REV=${1-1} # .deb package build/revision number. PACKAGE_DEBNAME=paheko THISDIR=${PWD} DEB_ARCH_NAME=all PACKAGE_VERSION=`cat ../../src/VERSION` [ ! -f ../../src/paheko-${PACKAGE_VERSION}.tar.gz ] && (cd ../../src; make release) tar xzvf ../../src/paheko-${PACKAGE_VERSION}.tar.gz -C /tmp SRCDIR="/tmp/paheko-${PACKAGE_VERSION}" test -e ${SRCDIR} || { echo "This script must be run from a BUILT copy of the source tree." exit 1 } |
︙ | ︙ | |||
64 65 66 67 68 69 70 | echo "Creating .deb package [${DEBFILE}]..." echo "Generating md5 sums..." find ${DEBLOCALPREFIX} -type f -exec md5sum {} \; > DEBIAN/md5sums true && { echo "Generating Debian-specific files..." | | | 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | echo "Creating .deb package [${DEBFILE}]..." echo "Generating md5 sums..." find ${DEBLOCALPREFIX} -type f -exec md5sum {} \; > DEBIAN/md5sums true && { echo "Generating Debian-specific files..." cp ${THISDIR}/../../COPYING ${DEBLOCALPREFIX}/share/doc/${PACKAGE_DEBNAME}/copyright } || { echo "Fail." exit 1 } true && { cat <<EOF > DEBIAN/postinst |
︙ | ︙ | |||
104 105 106 107 108 109 110 | } # doc. DOCDIR=${DEBLOCALPREFIX}/share/doc/${PACKAGE_DEBNAME} true && { echo "Generating doc..." | | | 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | } # doc. DOCDIR=${DEBLOCALPREFIX}/share/doc/${PACKAGE_DEBNAME} true && { echo "Generating doc..." cp ${THISDIR}/../../README.md ${DOCDIR} a2x --doctype manpage --format manpage ${THISDIR}/manpage.txt mkdir -p ${DEBLOCALPREFIX}/share/man/man1 gzip -c ${THISDIR}/paheko.1 > ${DEBLOCALPREFIX}/share/man/man1/${PACKAGE_DEBNAME}.1.gz rm -f ${THISDIR}/paheko.1 } || { echo "Fail." exit 1 |
︙ | ︙ |
Name change from debian/manpage.txt to build/debian/manpage.txt.
︙ | ︙ |
Name change from debian/paheko to build/debian/paheko.
︙ | ︙ |
Name change from debian/paheko.desktop to build/debian/paheko.desktop.
Name change from debian/paheko.menu to build/debian/paheko.menu.
Name change from debian/paheko.png to build/debian/paheko.png.
cannot compute difference between binary files
Added build/windows/Makefile version [151de88204].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | .PHONY := php installer clean publish PHP_ARCHIVE := https://windows.php.net/downloads/releases/php-8.0.27-nts-Win32-vs16-x64.zip php.zip: wget ${PHP_ARCHIVE} -O php.zip php: php.zip mkdir -p install_dir/php unzip -o php.zip -d install_dir/php > /dev/null # Remove unused files @cd install_dir/php && rm -rf \ phpdbg.exe \ php8phpdbg.dll \ php8embed.lib \ php-cgi.exe \ php.ini-* \ dev \ phar* \ nghttp2.dll \ libpq.dll \ libenchant # Remove unused extensions @cd install_dir/php/ext && rm -f \ php_bz2.dll \ php_com_dotnet.dll \ php_curl.dll \ php_dba.dll \ php_dl_test.dll \ php_enchant.dll \ php_exif.dll \ php_ffi.dll \ php_ftp.dll \ php_gmp.dll \ php_imap.dll \ php_ldap.dll \ php_mysqli.dll \ php_oci8_19.dll \ php_odbc.dll \ php_opcache.dll \ php_pdo_firebird.dll \ php_pdo_mysql.dll \ php_pdo_oci.dll \ php_pdo_odbc.dll \ php_pdo_pgsql.dll \ php_pdo_sqlite.dll \ php_pgsql.dll \ php_shmop.dll \ php_snmp.dll \ php_soap.dll \ php_sysvshm.dll \ php_xsl.dll \ php_zend_test.dll du -hs install_dir/php installer: clean php $(eval VERSION=$(shell cat ../../src/VERSION)) mkdir -p install_dir cp ../../src/paheko-${VERSION}.tar.gz install_dir/ cd install_dir && tar xzf paheko-${VERSION}.tar.gz && mv paheko-${VERSION} paheko cp config.local.php install_dir/paheko/ cp php.ini install_dir/php cp launch.bat install_dir cp paheko.ico install_dir rm -f install_dir/paheko-${VERSION}.tar.gz makensis -V3 -DVERSION=${VERSION} paheko.nsis clean: rm -rf install_dir publish: $(eval VERSION=$(shell cat ../../src/VERSION)) fossil uv ls | grep '^paheko-.*\.exe' | xargs fossil uv rm fossil uv add paheko-${VERSION}.exe fossil uv sync |
Added build/windows/README.md version [5c56cc2aa0].
> > > > > | 1 2 3 4 5 | # Paheko Windows build ## Requirements NSIS: `apt install nsis` |
Added build/windows/config.local.php version [4f250a82d1].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <?php namespace Garradin; if (!empty(getenv('LOCALAPPDATA'))) { // Store data in user AppData directory define('Garradin\DATA_ROOT', trim(getenv('LOCALAPPDATA'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'Paheko'); } // Store secret key in user directory if (!defined('Garradin\SECRET_KEY')) { if (file_exists(DATA_ROOT . '/key')) { define('Garradin\SECRET_KEY', trim(file_get_contents(DATA_ROOT . '/key'))); } else { define('Garradin\SECRET_KEY', base64_encode(random_bytes(16))); file_put_contents(DATA_ROOT . '/key', SECRET_KEY); } } // Always log in as admin user const LOCAL_LOGIN = -1; // Disable PDF export const PDF_COMMAND = null; // Disable e-mails as Windows is not able to send e-mails const DISABLE_EMAIL = true; |
Added build/windows/launch.bat version [4318d01c86].
> > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @start "" http://127.0.0.1:8082/ @echo ================================================= @echo. @echo Demarrage du serveur PHP de Paheko. @echo. @echo Paheko est disponible a l'adresse suivante : @echo http://127.0.0.1:8082/ @echo. @echo Fermer cette fenetre pour arreter le serveur. @echo. @echo ================================================= @echo. php\php.exe -S 127.0.0.1:8082 -t paheko/www paheko/www/_route.php 2> NUL |
Added build/windows/paheko.ico version [969b2f968c].
cannot compute difference between binary files
Added build/windows/paheko.nsis version [8c0f2b68e1].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | # From https://www.conjur.org/blog/building-a-windows-installer-from-a-linux-ci-pipeline/ !define APP_NAME "Paheko" !define COMP_NAME "Paheko.cloud" #!define WEB_SITE "https://paheko.cloud/" #!define VERSION "0.0.0.1" !define COPYRIGHT "Paheko" !define DESCRIPTION "Gestion d'association simple et efficace" !define INSTALLER_NAME "paheko-${VERSION}.exe" !define MAIN_APP_EXE "launch.bat" !define ICON "paheko.ico" #!define BANNER "[CHANGEME Installer Banner Filename .bmp]" #!define LICENSE_TXT "[CHANGEME License Text Document]" !define INSTALL_DIR "$PROGRAMFILES64\${APP_NAME}" !define INSTALL_TYPE "SetShellVarContext all" !define REG_ROOT "HKLM" !define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}" !define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" !define REG_START_MENU "Start Menu Folder" var SM_Folder ###################################################################### VIProductVersion "${VERSION}.0" VIAddVersionKey "ProductName" "${APP_NAME}" VIAddVersionKey "CompanyName" "${COMP_NAME}" VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" VIAddVersionKey "FileDescription" "${DESCRIPTION}" VIAddVersionKey "FileVersion" "${VERSION}" ###################################################################### SetCompressor /SOLID Lzma Name "${APP_NAME}" Caption "${APP_NAME}" OutFile "${INSTALLER_NAME}" BrandingText "${APP_NAME}" #InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" "" InstallDir "${INSTALL_DIR}" ###################################################################### !define MUI_ICON "${ICON}" !define MUI_UNICON "${ICON}" Icon "${ICON}" !ifdef BANNER !define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}" !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${BANNER}" !endif ###################################################################### !include "MUI2.nsh" !define MUI_ABORTWARNING !define MUI_UNABORTWARNING !insertmacro MUI_PAGE_WELCOME !ifdef LICENSE_TXT !insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}" !endif !insertmacro MUI_PAGE_DIRECTORY !ifdef REG_START_MENU !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${APP_NAME}" !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}" !define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}" !insertmacro MUI_PAGE_STARTMENU Application $SM_Folder !endif !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_FINISH !insertmacro MUI_LANGUAGE "French" ###################################################################### Section -MainProgram ${INSTALL_TYPE} SetOverwrite ifnewer SetOutPath "$INSTDIR" File /r "install_dir\\" SectionEnd ###################################################################### Section -Icons_Reg SetOutPath "$INSTDIR" WriteUninstaller "$INSTDIR\uninstall.exe" !ifdef REG_START_MENU !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateDirectory "$SMPROGRAMS\$SM_Folder" CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\paheko.ico" CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\paheko.ico" CreateShortCut "$SMPROGRAMS\$SM_Folder\Desinstaller ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" !ifdef WEB_SITE WriteIniStr "$INSTDIR\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}" CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME} Website.lnk" "$INSTDIR\${APP_NAME} website.url" !endif !insertmacro MUI_STARTMENU_WRITE_END !endif !ifndef REG_START_MENU CreateDirectory "$SMPROGRAMS\${APP_NAME}" CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\paheko.ico" CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\paheko.ico" CreateShortCut "$SMPROGRAMS\${APP_NAME}\Desinstaller ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" !ifdef WEB_SITE WriteIniStr "$INSTDIR\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}" CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Website.lnk" "$INSTDIR\${APP_NAME} website.url" !endif !endif WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}" WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayName" "${APP_NAME}" WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\uninstall.exe" WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\paheko.ico" WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}" WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}" !ifdef WEB_SITE WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "URLInfoAbout" "${WEB_SITE}" !endif SectionEnd ###################################################################### Section Uninstall ${INSTALL_TYPE} RmDir /r "$INSTDIR" !ifdef REG_START_MENU !insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" Delete "$SMPROGRAMS\$SM_Folder\Desinstaller ${APP_NAME}.lnk" !ifdef WEB_SITE Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME} Website.lnk" !endif Delete "$DESKTOP\${APP_NAME}.lnk" RmDir "$SMPROGRAMS\$SM_Folder" !endif !ifndef REG_START_MENU Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" Delete "$SMPROGRAMS\${APP_NAME}\Desinstaller ${APP_NAME}.lnk" !ifdef WEB_SITE Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Website.lnk" !endif Delete "$DESKTOP\${APP_NAME}.lnk" RmDir "$SMPROGRAMS\${APP_NAME}" !endif DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}" SectionEnd |
Added build/windows/php.ini version [799dda8484].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | [PHP] engine = On short_open_tag = Off precision = 14 output_buffering = 4096 zlib.output_compression = Off implicit_flush = Off unserialize_callback_func = serialize_precision = -1 disable_functions = disable_classes = zend.enable_gc = On zend.exception_ignore_args = Off zend.exception_string_param_max_len = 15 expose_php = On max_execution_time = 30 max_input_time = 60 memory_limit = 128M error_reporting = E_ALL display_errors = On display_startup_errors = On log_errors = On ignore_repeated_errors = Off ignore_repeated_source = Off report_memleaks = On variables_order = "GPCS" request_order = "GP" register_argc_argv = Off auto_globals_jit = On post_max_size = 8M auto_prepend_file = auto_append_file = default_mimetype = "text/html" default_charset = "UTF-8" doc_root = user_dir = extension_dir = "ext" enable_dl = Off file_uploads = On upload_max_filesize = 2M max_file_uploads = 20 allow_url_fopen = On allow_url_include = Off default_socket_timeout = 60 extension=fileinfo extension=gd extension=gettext extension=intl extension=mbstring extension=openssl extension=sodium extension=sqlite3 extension=tidy [CLI Server] cli_server.color = On [mail function] SMTP = localhost smtp_port = 25 mail.add_x_header = Off [bcmath] bcmath.scale = 0 [Session] session.save_handler = files session.use_strict_mode = 0 session.use_cookies = 1 session.use_only_cookies = 1 session.name = PHPSESSID session.auto_start = 0 session.cookie_lifetime = 0 session.cookie_path = / session.cookie_domain = session.cookie_httponly = session.cookie_samesite = session.serialize_handler = php session.gc_probability = 1 session.gc_divisor = 1000 session.gc_maxlifetime = 1440 session.referer_check = session.cache_limiter = nocache session.cache_expire = 180 session.use_trans_sid = 0 session.sid_length = 26 session.trans_sid_tags = "a=href,area=href,frame=src,form=" session.sid_bits_per_character = 5 [Assertion] zend.assertions = -1 [Tidy] tidy.clean_output = Off |
Deleted doc/dev/odoo_accounts.sql version [6c994995cd].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added doc/icon.png version [13439ec707].
cannot compute difference between binary files
Deleted doc/img/garradin.svg version [7d26df8f0d].
cannot compute difference between binary files
Deleted doc/img/garradin_gecko.svg version [e94b930821].
cannot compute difference between binary files
Modified doc/index.md from [db1e5dddfa] to [cd33124bbd].
|
| | > > > > > > > > > > > > > > > > > > > > > > > < > | < < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | # La gestion d'association libre et simple <div id="prez"> <figure> <img src="./selfhost2.png" alt="Illustration d'une personne aidant une autre à installer Paheko sur un ordinateur" /> </figure> ### Paheko — la gestion d'association simple</h3> **Paheko** <small>(anciennement appelé *Garradin*)</small> est un logiciel de gestion d'association, libre, simple et efficace. Son but est de : * **réduire le temps** passé sur les tâches administratives ; * re-**donner de l'autonomie aux adhérent⋅e⋅s** dans la gestion de leurs données ; * **simplifier la gestion** de l'association, pour inciter à participer à la gestion de l'association ; * intégrer les outils habituels, afin de réduire le nombre de logiciels à gérer. Pour en savoir plus : [voir les principales fonctionnalités](#features). </div> <div id="warn"> <p><strong>Attention : ce site est dédié au logiciel libre Paheko.</strong><br /> Son installation, sur un serveur ou sur un ordinateur personnel, nécessite quelques compétences techniques.</p> <p>Si votre association n'a pas ces compétences, nous recommandons l'utilisation de notre service d'hébergement :<br /><strong class="cloud"><a href="https://paheko.cloud/" target="_blank"><img src="./icon.png" alt="" /> Paheko.cloud</a></strong> <small>(<strong>Essai gratuit</strong>, puis contribution à prix libre, à partir de 5 € par an)</small> </div> <nav id="gnav"> * [Guides d'installation](/wiki/?name=Installation) * [Documentation](/wiki/?name=Documentation) * [Entraide](/wiki/?name=Entraide) * <a href="https://paheko.cloud/" target="_blank">Essayer gratuitement sur <b><img src="./icon.png" alt="" /> Paheko.cloud</b></a> <ul id="news"> <li><a href="$ROOT/wiki/?name=Changelog">Nouveautés</a></li> <li><a href="$ROOT/uvlist">Anciennes versions</a></li> </ul> </nav> <p id="give"><a href="https://kd2.org/soutien.html" target="_blank">Soutenir Paheko en effectuant un don :-)</a></p> <form method="GET" action="$ROOT/wiki" onsubmit="var t = this.querySelector('[type=radio]:checked'); this.querySelector('[name=s]').name=t.dataset.name; this.action=t.dataset.action; this.target=t.dataset.target;"> <fieldset class="searchForm searchFormWiki"> <legend>Rechercher</legend> <input type="search" name="s" size="40" value="" /> <label><input type="radio" name="t" value="" data-name="s" data-action="/paheko/wiki" data-target="" checked="checked" /> Chercher dans la documentation technique</label> <label><input type="radio" name="t" value="1" data-action="https://paheko.cloud/search" data-name="search" data-target="_blank" /> Chercher dans l'aide utilisateur</label> <input type="submit" value="Rechercher" /> </fieldset> </form> <script type="text/javascript"> document.head.innerHTML += `<style type="text/css"> #prez { } #warn { border: 2px solid #990; padding: .5em; border-radius: .5em; background: #ffd; margin: 1em 0; clear: both; } #warn .cloud { font-size: 1.2em; } #prez figure { float: right; } .markdown img { display: inline-block; max-width: unset; vertical-align: middle; box-shadow: none; margin: 0; } /* #info { text-align: center; margin: 1em auto; background: #ddd; padding: .5em; border-radius: .5em; max-width: 40em; } */ #give { text-align: center; margin: 1em; } #give a { display: inline-block; padding: .5em; padding-left: 70px; border-radius: .5em; font-size: 1.5em; background: #ffc url("https://kd2.org/soutien/coins.png") no-repeat .5em .5em; border: 2px solid #990; } #gnav ul { display: flex; padding: 0; margin: 1em; margin-bottom: 1em; font-size: 1.1em; list-style: none; justify-content: center; align-items: center; } #gnav li { margin: 0; |
︙ | ︙ | |||
86 87 88 89 90 91 92 | } #gnav li a:hover { text-decoration: underline; opacity: 0.7; } | | > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > | > > > | 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | } #gnav li a:hover { text-decoration: underline; opacity: 0.7; } #news li { font-size: 1em; } #news li a { border-color: #060; background: #dfd; } #download > h2 { text-align: center; } #download nav { display: flex; flex-direction: row; align-items: center; justify-content: center; } #download div, #download div h3 a, #download div h4 a { display: flex; flex-direction: column; align-items: center; justify-content: center; } #download div { margin: 0 20px; } #download div h3 a, #download div h4 a { background: #eef; border: 2px solid #ccf; padding: 5px; border-radius: 8px; } #download a:hover { background: #fee; border-color: #fcc; } #download img { height: 124px; box-shadow: none; padding: 5px; margin: 0; } #download p, #download h3, #download h4 { margin: 0; margin-bottom: 8px; text-align: center; } #download p em { color: #333; background: #ddd; padding: 2px; border-radius: 4px; display: inline-block; } .searchForm { border: 1px solid #ccc; border-radius: 5px; padding: .5em; margin: 1em auto; max-width: 30em; |
︙ | ︙ | |||
123 124 125 126 127 128 129 | if (a < b) return false } return false } fetch('/paheko/juvlist?'+(+(new Date))).then((r) => { r.json().then((list) => { | | | > > > | > > | > | > > > | > > > > | > > > > > | > > | > > > > > | > > > > > > > > > > > > > | > | > > > > > > > > > > > > > > | 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 | if (a < b) return false } return false } fetch('/paheko/juvlist?'+(+(new Date))).then((r) => { r.json().then((list) => { let last = {}; let selected; list.forEach((file) => { var v = file.name.match(/^paheko-(\d+\.\d+\.\d+)\.(deb|exe|tar\.gz)$/); if (!v || v[1].match(/-(alpha|rc|beta)/)) { return; } file.type = v[2]; file.version = v[1]; file.human_size = (Math.round((file.size / 1024 / 1024) * 10) / 10 + ' Mo').replace(/\./, ','); if (!last.hasOwnProperty(file.type) || isNewerVersion(last[file.type].version, file.version)) { last[file.type] = file; if (file.type == 'tar.gz') { selected = file; } } }); let days = ((+new Date)/1000 - selected.mtime) / 3600 / 24; if (days < 31) { time = Math.ceil(days) + ' jours'; } else if (days >= 31) { time = Math.round(days / 30.5) + ' mois'; } document.querySelector('#news').innerHTML = `<li class="last"><strong>Dernière version : ${last['tar.gz'].version}</strong></li> <li class="last"><em>il y a ${time}</em></li>` + document.querySelector('#news').innerHTML; document.querySelector('#news').insertAdjacentHTML('afterend', `<div id="download"> <h2>Télécharger :</h2> <nav> <div> <h3><a href="$ROOT/uv/${last['tar.gz'].name}"><img src="data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMjAiIGhlaWdodD0iOCIgcng9IjIiIHJ5PSIyIiAvPgogIDxyZWN0IHg9IjIiIHk9IjE0IiB3aWR0aD0iMjAiIGhlaWdodD0iOCIgcng9IjIiIHJ5PSIyIiAvPgogIDxsaW5lIHgxPSI2IiB5MT0iNiIgeDI9IjYuMDEiIHkyPSI2IiAvPgogIDxsaW5lIHgxPSI2IiB5MT0iMTgiIHgyPSI2LjAxIiB5Mj0iMTgiIC8+Cjwvc3ZnPgo=" alt="" /><span>Serveur</span></a></h3> <p>pour auto-hébergement<br /> <em>(.tar.gz, ${last['tar.gz'].human_size})</em><br /> <small><a href="$ROOT/wiki/?name=Installation">Guides d'installation</a></small> </p> </div> <div> <h4><a href="$ROOT/uv/${last['deb'].name}"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGoAAAB8AgMAAAD8wM2CAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAAlwSFlzAAAF3gAABd4BiRluJgAAAAxQTFRFAAAAR3BMAAAAAAAAoOHXxQAAAAR0Uk5T/QBQozL7B+YAAAPySURBVEjHndc9aBxHFADgd7u64hTv4eYgxRVulijYpQURpDgIKVwILtbN7GAt9jquEl/gItUOl5gUhgjUbNKkMBhhkDE2rhOY2LhwYVvNgiEu0qhOwKiISSAzszPz3q7mhIlKfczuzJv3swds4d8Y3tEu/fNyb4FxCQBl2HJFkIRtpI0spGYIVkM2qS0JWV5bFLKiNnwhsZm1KmAja2nA5ta6AZMnmCXonWDJCdY5btxZ9D8tDtkpgD5fYKVUFzQKWqT+P9ARD5yh80UB1b/jkPHkh1346dTPwXXJ8gG8Wn6NF0hsuXMaXkXDsMVnYBifCxucgXMwxIsn9hz69z6G00F7LP+8MYXdoD355qvP2KNx0KTOaP4XqOAcz6XSZmnAbrrMT4/bt6Y8+WLbyrCSiH2it7IqMEFpzqtnTcsiaPNfGf9bZ3cSsN/fvinFa0zQxtm/3GPbE0wmas/VXnpqnxDIszslKyqO1YmWwWRtMzFlkbZNxEzGlSnRbtvGEbu0V5d20rZZx7eLqG2yh22mZdyFWPiNehNud5nf6BgPWmEddps2cinLfcl7k0DqN25YhiXpe5ozgS1Aus04y7GtzN1mnBVoM9ebnI2wVRWxrF/gbI7tL49tP3Amsb9P4IO6Pp3B+wNMgKSuCWscditMnGhuDuFtRufLzLzdWhbdZKThH5jma23S6TAyKIbmRNZEvzG/VkwkrOVLPTrrVkzkrY0/wvGl4n6e2myYMjLQlkxgrI0OBi0rvcnbeHR1z0vmdq3BrGzZwFkWj9gim9CwKDtPLI++a1vqrOg8JsbV2dFmnd8a66jNG8Z18/UmW+sOTOOqDaKdxrrbZB3E6411V6Q3VR73qSX7P0piA2pnISoGaF1qfwCs+LisTSNid1W1vOfuIVOdCu9BxHrouvublGyOObGhO0V8aE2oiOJDv9adIn5q7ZrOLX/x67rikwvGcujrHPFJuK4rfvWGse39Uh8jQlMVX101dst2B/fQW2aOf27syBbuqrdCveCisdQWT+KNv6nYZWJj33wu1BEgJvwMri2r8H2Zv4vaWBcNO4W1bWJ+o89syFNl37tKrl+cuQDtoOV2L8LZNWWf2qqzQSuc5Wg8bc4CJnb9M/1Z6LqjRjwKbxvKXPpdtd/RZJ+Z/3zQ+5z6dBSHYxA+v9T1X8dvzBcqLlfId7LEj2ihY30RS4R+YO8wapxaZmolL1sfoMY2zHXzfstS+0h9R9N+ecy469fX45KajkXuZ8CmQQ5v1bfNA6ltC2fHpn5ntob3cUTmynTNxqzOCdGYfw8PmWtpWzqPGjP1yS8u58WHm73WfN/3xfnQ7jv426l8h99V/wEOtd7r5KQmrQAAAABJRU5ErkJggg==" alt="" /><span>Linux</span></a></h4> <p>hors-ligne, pour ordinateur<br /> <em>(.deb, ${last['deb'].human_size})</em><br /> <small><a href="$ROOT/wiki/?name=Fonctionnement+hors-ligne">Guide d'installation</a></small> </p> </div> <div> <h4><a href="$ROOT/uv/${last['exe'].name}"><img src="data:image/svg+xml;base64,PHN2ZyBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyNCAyNCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Im0yMiAyLTEwLjggMS42djhsMTAuOC0uMXptLTExLjggMTAuNS04LjItLjF2Ni44bDguMSAxLjF6bS04LjItNy43djYuOGg4LjF2LTcuOXptOS4xIDcuN3Y3LjlsMTAuOSAxLjZ2LTkuNHoiLz48L3N2Zz4=" alt="" /><span>Windows</span></a></h4> <p>hors-ligne, pour ordinateur<br /> <em>(.exe, ${last['exe'].human_size})</em><br /> <small><a href="$ROOT/wiki/?name=Installation/Windows">Guide d'installation</a></small> </p> </div> </nav> </div>`); }); }); </script> <a name="features"></a> <a href="$ROOT/raw/7bb068963b9f6301b27b81fe925caae9e86a229b?m=image/png" target="_blank" style="float: right; margin: 1em;"><img src="/paheko/raw/7bb068963b9f6301b27b81fe925caae9e86a229b?m=image/png" alt="Liste des membres" width="400" /></a> ## C'est quoi ? * **100% libre :** placé sous la licence [AGPL v3](https://www.gnu.org/licenses/why-affero-gpl.fr.html). * Gestion des **adhérent⋅e⋅s** : fiches de membre personnalisables, recherches personnalisées… * Gestion des **cotisations** et **activités** : suivi des adhérent⋅e⋅s à jour, des paiements en attente, **rappels automatiques** de cotisation par e-mail, etc. * Envoi de **newsletters** avec suivi des adresses e-mail invalides * **Comptabilité** puissante (à double entrée), **simple à utiliser par les débutant⋅e⋅s** : recettes, dépenses, suivi des dettes et créances, bilan et compte de résultat annuel, **comptabilité analytique**, export PDF, etc. * Stockage et **partage** de **documents** : édition collaborative, synchronisation des fichiers sur un ordinateur, etc. * Gestion du **site web** de l'association * Comptabilisation du **temps bénévole** et sa **valorisation** * Gestion de la **caisse informatisée** d'un atelier ou d'une boutique * **Conforme au RGPD** : export des données de l'adhérent⋅e, désabonnement des e-mails, chiffrement des mots de passe… ## Dans quels buts ? Le but est de permettre : * la gestion des __adhérent⋅e⋅s__ : ajout, modification, suppression, possibilité de choisir les informations présentes sur les fiches adhérent, envoi de mails collectifs aux adhérent⋅e⋅s * la tenue de la __comptabilité__ : avoir une gestion comptable complète à même de satisfaire un expert-comptable tout en restant à la portée de celles et ceux qui ne savent pas ce qu'est la comptabilité à double entrée, permettre la production des rapports et bilans annuels et de suivre au jour le jour le budget de l'association * la gestion des __cotisations__ et __activités__ : suivi des cotisations à jour, inscriptions et paiement des activités, rappels automatiques par e-mail, etc. * le travail __collaboratif__ et __collectif__ : gestion fine des droits d'accès aux fonctions, échange de mails entre membres… * la __simplification administrative__ : prise de notes en réunion, archivage et partage de fichiers (afin d'éliminer le besoin d'archiver les documents papier), etc. * la publication d'un __site web__ pour l'association, simple mais suffisamment flexible pour pouvoir adapter le fonctionnement à la plupart des besoins |
︙ | ︙ |
Deleted doc/manuel/Comptabilité avancée (partie double).md version [4caaa3768c].
|
| < < < < < < < |
Deleted doc/manuel/Sauvegarde et restauration.md version [a2332341db].
|
| < < < < < < < < < < < < < < < < |
Deleted doc/manuel/compta/Détails écriture.svg version [a9f0b60686].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted doc/manuel/compta/Lignes écriture chèques.svg version [a8247584d9].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added doc/selfhost2.png version [701da39174].
cannot compute difference between binary files
Modified src/.htaccess.www from [3ae6d113cf] to [1fa848cf6a].
︙ | ︙ | |||
20 21 22 23 24 25 26 | # Objectif: supprimer le /www/ de l'URL # Note: il est probable qu'il soit nécessaire d'adapter la configuration # à votre hébergeur ! <IfModule mod_rewrite.c> RewriteEngine on ## Remplacer dans les lignes suivantes | | | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | # Objectif: supprimer le /www/ de l'URL # Note: il est probable qu'il soit nécessaire d'adapter la configuration # à votre hébergeur ! <IfModule mod_rewrite.c> RewriteEngine on ## Remplacer dans les lignes suivantes ## /paheko/ par le nom du sous-répertoire où est installé Paheko RewriteBase /paheko/ FallbackResource /paheko/www/_route.php ## Ne pas modifier les lignes suivantes, les décommenter simplement ! RewriteCond %{REQUEST_URI} !www/ RewriteRule ^(.*)$ www/$1 [QSA,L] </IfModule> |
Modified src/Makefile from [9e2c4e5dbe] to [a6977744af].
︙ | ︙ | |||
38 39 40 41 42 43 44 | mv www/admin/static/mini.css /tmp/paheko-build/paheko/src/www/admin/static/admin.css cd /tmp/paheko-build/paheko/src/www/admin/static; \ rm -f styles/[0-9]*.css; \ rm -f font/*.css font/*.json cd /tmp/paheko-build/paheko/src; \ rm -f Makefile include/lib/KD2/data/countries.en.json cd /tmp/paheko-build/paheko/src/data; mkdir plugins && cd plugins; \ | | > > > > > > | > > > | | | > | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | mv www/admin/static/mini.css /tmp/paheko-build/paheko/src/www/admin/static/admin.css cd /tmp/paheko-build/paheko/src/www/admin/static; \ rm -f styles/[0-9]*.css; \ rm -f font/*.css font/*.json cd /tmp/paheko-build/paheko/src; \ rm -f Makefile include/lib/KD2/data/countries.en.json cd /tmp/paheko-build/paheko/src/data; mkdir plugins && cd plugins; \ wget https://fossil.kd2.org/paheko-plugins/uv/welcome.tar.gz \ && wget https://fossil.kd2.org/paheko-plugins/uv/caisse.tar.gz \ && wget https://fossil.kd2.org/paheko-plugins/uv/taima.tar.gz \ && wget https://fossil.kd2.org/paheko-plugins/uv/dompdf.tar.gz \ && wget https://fossil.kd2.org/paheko-plugins/uv/reservations.tar.gz \ && wget https://fossil.kd2.org/paheko-plugins/uv/webstats.tar.gz \ && wget https://fossil.kd2.org/paheko-plugins/uv/stock_velos.tar.gz mv /tmp/paheko-build/paheko/src /tmp/paheko-build/paheko-${VERSION} @#cd /tmp/paheko-build/; zip -r -9 paheko-${VERSION}.zip paheko-${VERSION}; @#mv -f /tmp/paheko-build/paheko-${VERSION}.zip ./ tar czvfh paheko-${VERSION}.tar.gz --hard-dereference -C /tmp/paheko-build paheko-${VERSION} deb: cd ../build/debian; ./makedeb.sh windows: cd ../build/windows; make installer publish: release deb windows $(eval VERSION=$(shell cat VERSION)) gpg --armor -r dev@paheko.cloud --detach-sign paheko-${VERSION}.tar.gz fossil uv sync #fossil uv ls | fgrep -v 'paheko-0.8.5' | grep '^paheko-.*\.(tar\.bz2|deb)' | xargs fossil uv rm fossil uv add paheko-${VERSION}.tar.gz fossil uv add paheko-${VERSION}.tar.gz.asc cd ../build/debian && fossil uv add paheko-${VERSION}.deb cd ../tools && php make_installer.php > install.php && fossil uv add install.php && rm install.php fossil uv sync cd ../build/windows && make publish check-dependencies: grep -hEo '^use \\?KD2\\[^; ]+|\\KD2\\[^\(:; ]+' -R include/lib/Garradin www | sed -r 's/^use \\?KD2\\|^\\KD2\\//' | sort | uniq minify: cat `ls www/admin/static/styles/[0-9]*.css` | sed 's/\.\.\///' > www/admin/static/mini.css @# Minify is only gaining 500 gzipped bytes (4kB uncompressed) but making things hard to read/hack @#yui-compressor --nomunge www/admin/static/mini.css -o www/admin/static/mini.css |
Modified src/config.dist.php from [5e8ac1ce22] to [65125f1971].
1 2 3 4 | <?php /** * Ce fichier représente un exemple des constantes de configuration | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | <?php /** * Ce fichier représente un exemple des constantes de configuration * disponibles pour Paheko. * * NE PAS MODIFIER CE FICHIER! * * Pour configurer Paheko, copiez ce fichier en 'config.local.php' * puis décommentez et modifiez ce dont vous avez besoin. */ // Nécessaire pour situer les constantes dans le bon namespace namespace Garradin; /** * Clé secrète, doit être unique à chaque instance de Paheko * * Ceci est utilisé afin de sécuriser l'envoi de formulaires * (protection anti-CSRF). * * Cette valeur peut être modifiée sans autre impact que la déconnexion des utilisateurs * actuellement connectés. * * Si cette constante n'est définie, Paheko ajoutera automatiquement * une valeur aléatoire dans le fichier config.local.php. */ //const SECRET_KEY = '3xUhIgGwuovRKOjVsVPQ5yUMfXUSIOX2GKzcebsz5OINrYC50r'; /** * @var null|int|array * * Forcer la connexion locale * * Si un numéro est spécifié, alors le membre avec l'ID correspondant à ce * numéro sera connecté (sans besoin de mot de passe). * * Exemple: LOCAL_LOGIN = 42 connectera automatiquement le membre avec id = 42 * Attention à ne pas utiliser en production ! * * Si le nombre spécifié est -1, alors c'est le premier membre trouvé qui * peut gérer la configuration (et donc modifier les droits des membres) * qui sera connecté. * * Si un tableau est spécifié, alors Paheko considérera que l'utilisateur * connecté fourni dans le tableau n'est pas un membre. * Voir la documentation sur l'utilisation avec SSO et LDAP pour plus de détails. * * Exemple : * const LOCAL_LOGIN = [ * 'user' => ['_name' => 'bohwaz'], * 'permissions' => ['users' => 9, 'config' => 9] |
︙ | ︙ | |||
71 72 73 74 75 76 77 | * * Défaut : true */ //const ALLOW_MODIFIED_IMPORT = true; /** | | | | | | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | * * Défaut : true */ //const ALLOW_MODIFIED_IMPORT = true; /** * Répertoire où se situe le code source de Paheko * * Défaut : répertoire racine de Paheko (__DIR__) */ //const ROOT = __DIR__; /** * Répertoire où sont situées les données de Paheko * (incluant la base de données SQLite, les sauvegardes, le cache, les fichiers locaux et les plugins) * * Défaut : sous-répertoire "data" de la racine */ //const DATA_ROOT = ROOT . '/data'; /** * Répertoire où est situé le cache, * exemples : graphiques de statistiques, templates Brindille, etc. * * Défaut : sous-répertoire 'cache' de DATA_ROOT */ //const CACHE_ROOT = DATA_ROOT . '/cache'; /** * Répertoire où est situé le cache partagé entre instances * Paheko utilisera ce répertoire pour stocker le cache susceptible d'être partagé entre instances, comme * le code PHP généré à partir des templates Smartyer. * * Défaut : sous-répertoire 'shared' de CACHE_ROOT */ //const SHARED_CACHE_ROOT = CACHE_ROOT . '/shared'; |
︙ | ︙ | |||
128 129 130 131 132 133 134 | * * @var null|string */ //const WEB_CACHE_ROOT = CACHE_ROOT . '/web/%host%'; /** | | | 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | * * @var null|string */ //const WEB_CACHE_ROOT = CACHE_ROOT . '/web/%host%'; /** * Emplacement du fichier de base de données de Paheko * * Défaut : DATA_ROOT . '/association.sqlite' */ //const DB_FILE = DATA_ROOT . '/association.sqlite'; /** |
︙ | ︙ | |||
158 159 160 161 162 163 164 | * La clé est le nom du signal, et la valeur est la fonction. * * Défaut: [] (tableau vide) */ //const SYSTEM_SIGNALS = [['files.delete' => 'MyNamespace\Signals::deleteFile'], ['entity.Accounting\Transaction.save.before' => 'MyNamespace\Signals::saveTransaction']]; /** | | | | | | | 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | * La clé est le nom du signal, et la valeur est la fonction. * * Défaut: [] (tableau vide) */ //const SYSTEM_SIGNALS = [['files.delete' => 'MyNamespace\Signals::deleteFile'], ['entity.Accounting\Transaction.save.before' => 'MyNamespace\Signals::saveTransaction']]; /** * Adresse URI de la racine du site Paheko * (doit se terminer par un slash) * * Défaut : découverte automatique à partir de SCRIPT_NAME */ //const WWW_URI = '/asso/'; /** * Adresse URL HTTP(S) de Paheko * * Défaut : découverte à partir de HTTP_HOST ou SERVER_NAME + WWW_URI */ //const WWW_URL = 'http://paheko.chezmoi.tld' . WWW_URI; /** * Adresse URL HTTP(S) de l'admin Paheko * * Défaut : WWW_URL + 'admin/' */ //const ADMIN_URL = 'https://admin.paheko.chezmoi.tld/'; /** * 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 : false |
︙ | ︙ | |||
208 209 210 211 212 213 214 | * * Défaut : false */ //const MAIL_ERRORS = false; /** | | | | | 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | * * Défaut : false */ //const MAIL_ERRORS = false; /** * Envoi des erreurs à une API compatible AirBrake/Errbit/Paheko * * Si renseigné avec une URL HTTP(S) valide, chaque erreur système sera envoyée * automatiquement à cette URL. * * Si laissé à null, aucun rapport ne sera envoyé. * * Paheko accepte aussi les rapports d'erreur venant d'autres instances. * * Pour cela utiliser l'URL https://login:password@paheko.site.tld/api/errors/report * (voir aussi API_USER et API_PASSWORD) * * Les erreurs seront ensuite visibles dans * Configuration -> Fonctions avancées -> Journal d'erreurs * * Défaut : null */ |
︙ | ︙ | |||
239 240 241 242 243 244 245 246 247 248 249 250 251 252 | * est affiché. Il est possible de personnaliser ce message avec cette constante. * * Voir include/init.php pour le template par défaut. */ // const ERRORS_TEMPLATE = null; /** * Activation des détails techniques (utile en auto-hébergement) : * - version de PHP * - page permettant de visualiser les erreurs présentes dans le error.log * - permettre de migrer d'un stockage de fichiers à l'autre * - vérification de nouvelle version (sur la page configuration) * | > > > > > > > > > > > > > > > > > | 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | * est affiché. Il est possible de personnaliser ce message avec cette constante. * * Voir include/init.php pour le template par défaut. */ // const ERRORS_TEMPLATE = null; /** * Loguer / envoyer par mail les erreurs utilisateur ? * * Si positionné à 1, *toutes* les erreurs utilisateur (champ mal rempli dans un formulaire, * formulaire dont le token CSRF a expiré, etc.) seront loguées et/ou envoyées par mail * (selon le réglage choisit ci-dessus). * * Si positionné à 2, alors l'exception sera remontée dans la stack, *et* loguée/envoyée. * * Utile pour le développement. * * Défaut : 0 (ne rien faire) * @var int */ // const REPORT_USER_EXCEPTIONS = 0; /** * Activation des détails techniques (utile en auto-hébergement) : * - version de PHP * - page permettant de visualiser les erreurs présentes dans le error.log * - permettre de migrer d'un stockage de fichiers à l'autre * - vérification de nouvelle version (sur la page configuration) * |
︙ | ︙ | |||
331 332 333 334 335 336 337 | * pour faire une mise à jour en deux clics. * * Il est conseillé de désactiver cette fonctionnalité si vous ne voulez pas * permettre à un utilisateur de casser l'installation ! * * Si cette constante est désactivée, mais que ENABLE_TECH_DETAILS est activé, * la vérification de nouvelle version se fera quand même, mais plutôt que de proposer | | | 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | * pour faire une mise à jour en deux clics. * * Il est conseillé de désactiver cette fonctionnalité si vous ne voulez pas * permettre à un utilisateur de casser l'installation ! * * Si cette constante est désactivée, mais que ENABLE_TECH_DETAILS est activé, * la vérification de nouvelle version se fera quand même, mais plutôt que de proposer * la mise à jour, Paheko proposera de se rendre sur le site officiel pour * télécharger la mise à jour. * * Défaut : true * * @var bool */ |
︙ | ︙ | |||
373 374 375 376 377 378 379 | * - Lighttpd * * N'activer que si vous êtes sûr que le module est installé et activé (sinon * les fichiers ne pourront être vus ou téléchargés). * Nginx n'est PAS supporté, car X-Accel-Redirect ne peut gérer que des fichiers * qui sont *dans* le document root du vhost, ce qui n'est pas le cas ici. * | | | | | 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 | * - Lighttpd * * N'activer que si vous êtes sûr que le module est installé et activé (sinon * les fichiers ne pourront être vus ou téléchargés). * Nginx n'est PAS supporté, car X-Accel-Redirect ne peut gérer que des fichiers * qui sont *dans* le document root du vhost, ce qui n'est pas le cas ici. * * Pour activer X-SendFile mettre dans la config du virtualhost de Paheko: * XSendFile On * XSendFilePath /var/www/paheko * * (remplacer le chemin par le répertoire racine de Paheko) * * Détails : https://tn123.org/mod_xsendfile/ * * Défaut : false */ //const ENABLE_XSENDFILE = false; |
︙ | ︙ | |||
442 443 444 445 446 447 448 | * Login utilisateur pour le server SMTP * * mettre à null pour utiliser un serveur local ou anonyme * * Défaut : null */ | | | 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 | * Login utilisateur pour le server SMTP * * mettre à null pour utiliser un serveur local ou anonyme * * Défaut : null */ //const SMTP_USER = 'paheko@monserveur.com'; /** * Mot de passe pour le serveur SMTP * * mettre à null pour utiliser un serveur local ou anonyme * * Défaut : null |
︙ | ︙ | |||
551 552 553 554 555 556 557 558 559 560 561 | * Utile pour s'assurer qu'on est sur une instance de test par exemple. * * Défault : false * @var bool */ //const FORCE_CUSTOM_COLORS = false; /** * Stockage des fichiers * * Indiquer ici le nom d'une classe de stockage de fichiers | > > > > > > > > > > > > | | 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 | * Utile pour s'assurer qu'on est sur une instance de test par exemple. * * Défault : false * @var bool */ //const FORCE_CUSTOM_COLORS = false; /** * Désactiver le formulaire d'installation * * Si TRUE, alors le formulaire d'installation renverra une erreur. * * Utile pour une installation multi-associations. * * Défaut : false * @var bool */ //const DISABLE_INSTALL_FORM = false; /** * Stockage des fichiers * * Indiquer ici le nom d'une classe de stockage de fichiers * (parmis celles disponibles dans lib/Paheko/Files/Backend) * * Indiquer NULL si vous souhaitez stocker les fichier dans la base * de données SQLite (valeur par défaut). * * Classes de stockage possibles : * - SQLite : enregistre dans la base de données (défaut) * - FileSystem : enregistrement des fichiers dans le système de fichier |
︙ | ︙ | |||
619 620 621 622 623 624 625 | * Défaut : null */ //const WOPI_DISCOVERY_URL = 'http://localhost:9980/hosting/discovery'; /** * PDF_COMMAND | | < | | | > > > > > > > > > | > < > > < > | > | > | > > > > > > > > > > > | | | 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 | * Défaut : null */ //const WOPI_DISCOVERY_URL = 'http://localhost:9980/hosting/discovery'; /** * PDF_COMMAND * Commande qui sera exécutée pour créer un fichier PDF à partir d'un HTML. * * Si laissé sur 'auto', Paheko essaiera de détecter une solution entre * PrinceXML, Chromium, wkhtmltopdf ou weasyprint (dans cet ordre). * Si aucune solution n'est disponible, une erreur sera affichée. * * Il est possible d'indiquer NULL pour désactiver l'export en PDF. * * Il est possible d'indiquer uniquement le nom du programme : * 'chromium', 'prince', 'weasyprint', ou 'wkhtmltopdf'. * Dans ce cas, Paheko utilisera les paramètres par défaut de ce programme. * * Alternativement, il est possible d'indiquer la commande complète avec * les options, par exemple '/usr/bin/chromium --headless --print-to-pdf=%2$s %1$s' * Dans ce cas : * - %1$s sera remplacé par le chemin du fichier HTML existant, * - %2$s sera remplacé par le chemin du fichier PDF à créer. * * Si vous utilisez une extension pour générer les PDF (comme DomPDF), alors * laisser cette constante sur 'auto'. * * Exemples : * 'weasyprint' * 'wkhtmltopdf -q --print-media-type --enable-local-file-access %s %s' * * Si vous utilisez Prince, un message mentionnant l'utilisation de Prince * sera joint aux e-mails utilisant des fichiers PDF, conformément à la licence : * https://www.princexml.com/purchase/license_faq/#non-commercial * * Défaut : 'auto' * @var null|string */ //const PDF_COMMAND = 'auto'; /** * PDF_USAGE_LOG * Chemin vers le fichier où enregistrer la date de chaque export en PDF * * Ceci est utilisé notamment pour estimer le prix de la licence PrinceXML. * * Défaut : NULL * @var null|string */ //const PDF_USAGE_LOG = null; /** * CALC_CONVERT_COMMAND * Outil de conversion de formats de tableur vers un format propriétaire * * Paheko gère nativement les exports en ODS (OpenDocument : LibreOffice) * et CSV, et imports en CSV. * * En indiquant ici le nom d'un outil, Paheko autorisera aussi * l'import en XLSX, XLS et ODS, et l'export en XLSX. * * Pour cela il procédera simplement à une conversion entre les formats natifs * ODS/CSV et XLSX ou XLS. * * Noter qu'installer ces commandes peut introduire des risques de sécurité sur le serveur. * |
︙ | ︙ | |||
671 672 673 674 675 676 677 | //const CALC_CONVERT_COMMAND = 'ssconvert'; //const CALC_CONVERT_COMMAND = 'unoconvert --interface localhost --port 2022'; /** * API_USER et API_PASSWORD * Login et mot de passe système de l'API * | | | | | 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 | //const CALC_CONVERT_COMMAND = 'ssconvert'; //const CALC_CONVERT_COMMAND = 'unoconvert --interface localhost --port 2022'; /** * API_USER et API_PASSWORD * Login et mot de passe système de l'API * * Une API est disponible via l'URL https://login:password@paheko.association.tld/api/... * Voir https://fossil.kd2.org/paheko/wiki?name=API pour la documentation * * Ces deux constantes permettent d'indiquer un nom d'utilisateur * et un mot de passe pour accès à l'API. * * Cet utilisateur est distinct de ceux définis dans la page de gestion des * identifiants d'accès à l'API, et aura accès à TOUT en écriture/administration. * * Défaut: null */ //const API_USER = 'coraline'; //const API_PASSWORD = 'thisIsASecretPassword42'; /** * DISABLE_INSTALL_PING * * Lors de l'installation, ou d'une mise à jour, la version installée de Paheko, * ainsi que celle de PHP et de SQLite, sont envoyées à Paheko.cloud. * * Cela permet de savoir quelles sont les versions utilisées, et également de compter * le nombre d'installations effectuées. * * Aucune donnée personnelle n'est envoyée. Un identifiant anonyme est envoyé, * permettant d'identifier l'installation et éviter les doublons. |
︙ | ︙ |
src/include/data/schema.sql became a symlink with target [57116110a2].
Modified src/include/init.php from [62c653a3cf] to [73265b0a9f].
︙ | ︙ | |||
100 101 102 103 104 105 106 | // Configuration par défaut, si les constantes ne sont pas définies dans CONFIG_FILE // (fallback) if (!defined('Garradin\ROOT')) { define('Garradin\ROOT', dirname(__DIR__)); } | | | > > > > > > > | 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | // Configuration par défaut, si les constantes ne sont pas définies dans CONFIG_FILE // (fallback) if (!defined('Garradin\ROOT')) { define('Garradin\ROOT', dirname(__DIR__)); } \spl_autoload_register(function (string $classname): void { $classname = ltrim($classname, '\\'); // Plugins if (substr($classname, 0, 16) == 'Garradin\\Plugin\\') { $classname = substr($classname, 16); $plugin_name = substr($classname, 0, strpos($classname, '\\')); $filename = str_replace('\\', '/', substr($classname, strpos($classname, '\\')+1)); $path = Plugins::getPath(strtolower($plugin_name)); // Plugin does not exist, just abort if (!$path) { return; } $path = $path . '/lib/' . $filename . '.php'; } else { // PSR-0 autoload $filename = str_replace('\\', '/', $classname); $path = ROOT . '/include/lib/' . $filename . '.php'; } |
︙ | ︙ | |||
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | 'DB_FILE' => DATA_ROOT . '/association.sqlite', 'DB_SCHEMA' => ROOT . '/include/data/schema.sql', 'PLUGINS_ROOT' => DATA_ROOT . '/plugins', 'ALLOW_MODIFIED_IMPORT' => true, 'SHOW_ERRORS' => true, 'MAIL_ERRORS' => false, 'ERRORS_REPORT_URL' => null, 'ENABLE_TECH_DETAILS' => true, 'HTTP_LOG_FILE' => null, 'ENABLE_UPGRADES' => true, 'USE_CRON' => false, 'ENABLE_XSENDFILE' => false, 'DISABLE_EMAIL' => false, 'SMTP_HOST' => false, 'SMTP_USER' => null, 'SMTP_PASSWORD' => null, 'SMTP_PORT' => 587, 'SMTP_SECURITY' => 'STARTTLS', 'MAIL_RETURN_PATH' => null, 'MAIL_BOUNCE_PASSWORD' => null, 'ADMIN_URL' => WWW_URL . 'admin/', 'NTP_SERVER' => 'fr.pool.ntp.org', 'ADMIN_COLOR1' => '#20787a', 'ADMIN_COLOR2' => '#85b9ba', 'ADMIN_BACKGROUND_IMAGE' => WWW_URL . 'admin/static/bg.png', 'FORCE_CUSTOM_COLORS' => false, 'FILE_STORAGE_BACKEND' => 'SQLite', 'FILE_STORAGE_CONFIG' => null, 'FILE_STORAGE_QUOTA' => null, 'API_USER' => null, 'API_PASSWORD' => null, | > > | > | 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | 'DB_FILE' => DATA_ROOT . '/association.sqlite', 'DB_SCHEMA' => ROOT . '/include/data/schema.sql', 'PLUGINS_ROOT' => DATA_ROOT . '/plugins', 'ALLOW_MODIFIED_IMPORT' => true, 'SHOW_ERRORS' => true, 'MAIL_ERRORS' => false, 'ERRORS_REPORT_URL' => null, 'REPORT_USER_EXCEPTIONS' => 0, 'ENABLE_TECH_DETAILS' => true, 'HTTP_LOG_FILE' => null, 'ENABLE_UPGRADES' => true, 'USE_CRON' => false, 'ENABLE_XSENDFILE' => false, 'DISABLE_EMAIL' => false, 'SMTP_HOST' => false, 'SMTP_USER' => null, 'SMTP_PASSWORD' => null, 'SMTP_PORT' => 587, 'SMTP_SECURITY' => 'STARTTLS', 'MAIL_RETURN_PATH' => null, 'MAIL_BOUNCE_PASSWORD' => null, 'ADMIN_URL' => WWW_URL . 'admin/', 'NTP_SERVER' => 'fr.pool.ntp.org', 'ADMIN_COLOR1' => '#20787a', 'ADMIN_COLOR2' => '#85b9ba', 'ADMIN_BACKGROUND_IMAGE' => WWW_URL . 'admin/static/bg.png', 'FORCE_CUSTOM_COLORS' => false, 'DISABLE_INSTALL_FORM' => false, 'FILE_STORAGE_BACKEND' => 'SQLite', 'FILE_STORAGE_CONFIG' => null, 'FILE_STORAGE_QUOTA' => null, 'API_USER' => null, 'API_PASSWORD' => null, 'PDF_COMMAND' => 'auto', 'PDF_USAGE_LOG' => null, 'CALC_CONVERT_COMMAND' => null, 'CONTRIBUTOR_LICENSE' => null, 'SQL_DEBUG' => null, 'SYSTEM_SIGNALS' => [], 'LOCAL_LOGIN' => null, 'LEGAL_LINE' => 'Hébergé par <strong>%1$s</strong>, %2$s', 'DISABLE_INSTALL_PING' => false, |
︙ | ︙ | |||
366 367 368 369 370 371 372 | $tpl->assign('admin_url', ADMIN_URL); $tpl->display(); } exit; } | > | | > | 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 | $tpl->assign('admin_url', ADMIN_URL); $tpl->display(); } exit; } if (REPORT_USER_EXCEPTIONS < 2) { // Message d'erreur simple pour les erreurs de l'utilisateur ErrorManager::setCustomExceptionHandler('\Garradin\UserException', '\Garradin\user_error'); } // Clé secrète utilisée pour chiffrer les tokens CSRF etc. if (!defined('Garradin\SECRET_KEY')) { if (!is_writable(ROOT)) { throw new \RuntimeException('Impossible de créer le fichier de configuration "'. CONFIG_FILE .'". Le répertoire "'. ROOT . '" n\'est pas accessible en écriture.'); } |
︙ | ︙ | |||
393 394 395 396 397 398 399 | /* * Vérifications pour enclencher le processus d'installation ou de mise à jour */ if (!defined('Garradin\INSTALL_PROCESS')) { | | > > | 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 | /* * Vérifications pour enclencher le processus d'installation ou de mise à jour */ if (!defined('Garradin\INSTALL_PROCESS')) { $exists = file_exists(DB_FILE); if (!$exists) { if (in_array('install.php', get_included_files())) { die('Erreur de redirection en boucle : problème de configuration ?'); } Utils::redirect(ADMIN_URL . 'install.php'); } |
︙ | ︙ |
Modified src/include/lib/Garradin/API.php from [ef07b4677c] to [1c14e4473d].
︙ | ︙ | |||
205 206 207 208 209 210 211 | } } elseif ($fn == 'years') { if ($this->method != 'GET') { throw new APIException('Wrong request method', 400); } | | > > > > > > > > > > > > | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | } } elseif ($fn == 'years') { if ($this->method != 'GET') { throw new APIException('Wrong request method', 400); } if (preg_match('!^(\d+|current)/(journal|account/journal)!', $param, $match)) { if ($match[1] == 'current') { $id_year = Years::getCurrentOpenYearId(); if (!$id_year) { throw new APIException('There are no currently open years', 404); } } else { $id_year = (int)$match[1]; } if ($match[2] == 'journal') { try { return iterator_to_array(Reports::getJournal(['year' => $id_year])); } catch (\LogicException $e) { throw new APIException('Missing parameter for journal: ' . $e->getMessage(), 400, $e); } } else { $year = Years::get($id_year); if (!$year) { throw new APIException('Invalid year.', 400, $e); } $a = $year->chart()->accounts(); if (!empty($_GET['code'])) { $account = $a->getWithCode($_GET['code']); } else { $account = $a->get((int)$_GET['code'] ?? null); } if (!$account) { throw new APIException('Unknown account id or code.', 400, $e); } $list = $account->listJournal($year->id, false); $list->setTitle(sprintf('Journal - %s - %s', $account->code, $account->label)); $list->loadFromQueryString(); $list->setPageSize(null); $list->orderBy('date', false); return iterator_to_array($list->iterate()); } } elseif ($param == '') { return Years::list(); } else { throw new APIException('Unknown years action', 404); |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Accounts.php from [fe546e2752] to [171b8add61].
︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 35 36 37 38 | $this->em = EntityManager::getInstance(Account::class); } static public function get(int $id) { return EntityManager::findOneById(Account::class, $id); } static public function getSelector(?int $id): ?array { if (!$id) { return null; } | > > > > > | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | $this->em = EntityManager::getInstance(Account::class); } static public function get(int $id) { return EntityManager::findOneById(Account::class, $id); } public function getWithCode(string $code): ?Account { return EntityManager::findOne(Account::class, 'SELECT * FROM @TABLE WHERE code = ? AND id_chart = ?', $code, $this->chart_id); } static public function getSelector(?int $id): ?array { if (!$id) { return null; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/AssistedReconciliation.php from [d7c91fad25] to [3d189be803].
︙ | ︙ | |||
54 55 56 57 58 59 60 | $line->amount = Utils::moneyToInteger($line->amount ?? 0); if (!empty($line->balance)) { $line->balance = (substr($line->balance, 0, 1) == '-' ? -1 : 1) * Utils::moneyToInteger($line->balance); } $line->new_params = http_build_query([ | | | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | $line->amount = Utils::moneyToInteger($line->amount ?? 0); if (!empty($line->balance)) { $line->balance = (substr($line->balance, 0, 1) == '-' ? -1 : 1) * Utils::moneyToInteger($line->balance); } $line->new_params = http_build_query([ '0' => abs($line->amount)/100, 'l' => $line->label, 'dt' => $date ? $date->format('Y-m-d') : '', 't' => $line->amount < 0 ? Transaction::TYPE_EXPENSE : Transaction::TYPE_REVENUE, 'account' => $account->id, ]); return $line; }); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Import.php from [0aadea6577] to [1198624fe0].
︙ | ︙ | |||
230 231 232 233 234 235 236 | // FEC does not define type, so don't change it if (isset($row->type)) { $transaction->type = $types[$row->type]; } $fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference'])); | | < < < | < < < | 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | // FEC does not define type, so don't change it if (isset($row->type)) { $transaction->type = $types[$row->type]; } $fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference'])); // Remove empty values $fields = array_filter($fields); $transaction->importForm($fields); // Set status if (!empty($row->status)) { $status_list = array_map('trim', explode(',', $row->status)); $status = 0; |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Reports.php from [428de3f988] to [da4d116997].
︙ | ︙ | |||
176 177 178 179 180 181 182 | else { $where = self::getWhereClause($criterias); $sql = sprintf('SELECT position, SUM(balance) FROM acc_accounts_balances WHERE %s GROUP BY position;', $where); } $balances = DB::getInstance()->getAssoc($sql); | < < | 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | else { $where = self::getWhereClause($criterias); $sql = sprintf('SELECT position, SUM(balance) FROM acc_accounts_balances WHERE %s GROUP BY position;', $where); } $balances = DB::getInstance()->getAssoc($sql); return ($balances[Account::REVENUE] ?? 0) - ($balances[Account::EXPENSE] ?? 0); } static public function getBalancesSQL(array $parts = []) { return sprintf('SELECT %s id_year, id, label, code, type, debit, credit, position, %s, is_debt FROM ( |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Transactions.php from [ea5b0a2b00] to [f94c739ea4].
︙ | ︙ | |||
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | return DB::getInstance()->count('acc_transactions_users', 'id_user = ?', $user_id); } static public function countForCreator(int $user_id): int { return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id); } static public function setProject(?int $id_project, ?array $transactions = null, ?array $lines = null) { $db = DB::getInstance(); if (null !== $id_project && !$db->test(Project::TABLE, 'id = ?', $id_project)) { throw new \InvalidArgumentException('Invalid project ID'); } if (isset($transactions, $lines) || ($transactions === null && $lines === null)) { throw new \BadMethodCallException('Only one of transactions or lines should be set'); } $selection = array_map('intval', $transactions ?? $lines); $where = sprintf($transactions ? 'id_transaction IN (%s)' : 'id IN (%s)', implode(', ', $selection)); return $db->exec(sprintf('UPDATE acc_transactions_lines SET id_project = %s WHERE %s;', (int)$id_project ?: 'NULL', $where)); } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | return DB::getInstance()->count('acc_transactions_users', 'id_user = ?', $user_id); } static public function countForCreator(int $user_id): int { return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id); } /** * Returns a dynamic list of all waiting credit and debt transactions for closed years */ static public function listPendingCreditAndDebtForClosedYears(): DynamicList { $columns = Account::LIST_COLUMNS; unset($columns['line_label'], $columns['sum'], $columns['debit'], $columns['credit']); unset($columns['project_code'], $columns['id_project'], $columns['line_reference']); $columns['change']['select'] = 'SUM(l.credit)'; $columns['change']['label'] = 'Montant'; $columns = [ 'year_label' => [ 'select' => 'y.label', 'label' => 'Exercice', ], 'type_label' => [ 'select' => 't.type', 'label' => 'Type', ]] + $columns; $conditions = sprintf('y.closed = 1 AND t.status & %d AND t.type IN (%d, %d)', Transaction::STATUS_WAITING, Transaction::TYPE_CREDIT, Transaction::TYPE_DEBT); $tables = 'acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction INNER JOIN acc_years y ON y.id = t.id_year'; $list = new DynamicList($columns, $tables, $conditions); $list->orderBy('date', true); $list->setCount('COUNT(DISTINCT t.id)'); $list->groupBy('t.id'); $list->setModifier(function (&$row) { $row->date = \DateTime::createFromFormat('!Y-m-d', $row->date); if (isset($row->type_label)) { $row->type_label = Transaction::TYPES_NAMES[(int)$row->type_label]; } }); $list->setExportCallback(function (&$row) { $row->change = Utils::money_format($row->change, '.', '', false); }); $list->setTitle('Dettes et créances en attente'); return $list; } static public function setProject(?int $id_project, ?array $transactions = null, ?array $lines = null) { $db = DB::getInstance(); if (null !== $id_project && !$db->test(Project::TABLE, 'id = ?', $id_project)) { throw new \InvalidArgumentException('Invalid project ID'); } if (isset($transactions, $lines) || ($transactions === null && $lines === null)) { throw new \BadMethodCallException('Only one of transactions or lines should be set'); } $selection = array_map('intval', $transactions ?? $lines); $where = sprintf($transactions ? 'id_transaction IN (%s)' : 'id IN (%s)', implode(', ', $selection)); return $db->exec(sprintf('UPDATE acc_transactions_lines SET id_project = %s WHERE %s;', (int)$id_project ?: 'NULL', $where)); } static public function listByType(int $year_id, ?int $type): DynamicList { $reverse = 1; $columns = Account::LIST_COLUMNS; unset($columns['line_label'], $columns['sum'], $columns['debit'], $columns['credit']); $columns['line_reference']['label'] = 'Réf. paiement'; $columns['change']['select'] = sprintf('SUM(l.credit) * %d', $reverse); |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Years.php from [dd4bd35675] to [f68afe4a33].
︙ | ︙ | |||
157 158 159 160 161 162 163 164 165 166 167 168 169 170 | $t = new Transaction; $t->type = $t::TYPE_ADVANCED; $t->id_year = $year->id(); $t->label = 'Affectation automatique du résultat'; $t->notes = 'Le résultat a été affecté automatiquement lors de la balance d\'ouverture'; $t->date = $year->start_date; $sum = 0; if (!empty($balances[Account::TYPE_NEGATIVE_RESULT])) { $account = $balances[Account::TYPE_NEGATIVE_RESULT]; $line = Line::create($account->id, abs($account->balance), 0); | > | 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | $t = new Transaction; $t->type = $t::TYPE_ADVANCED; $t->id_year = $year->id(); $t->label = 'Affectation automatique du résultat'; $t->notes = 'Le résultat a été affecté automatiquement lors de la balance d\'ouverture'; $t->date = $year->start_date; $t->addStatus($t::STATUS_OPENING_BALANCE); $sum = 0; if (!empty($balances[Account::TYPE_NEGATIVE_RESULT])) { $account = $balances[Account::TYPE_NEGATIVE_RESULT]; $line = Line::create($account->id, abs($account->balance), 0); |
︙ | ︙ |
Modified src/include/lib/Garradin/DynamicList.php from [04482bebe4] to [b3c71aaba9].
︙ | ︙ | |||
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | } } public function SQL() { $start = ($this->page - 1) * $this->per_page; $columns = []; foreach ($this->columns as $alias => $properties) { // Skip columns that require a certain order (eg. calculating a running sum) if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order)) { continue; } // Skip columns that require a certain order AND paginated result if (isset($properties['only_with_order']) && $this->page > 1) { continue; } | > | < < | > > > | < | | 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | } } public function SQL() { $start = ($this->page - 1) * $this->per_page; $columns = []; $db = DB::getInstance(); foreach ($this->columns as $alias => $properties) { // Skip columns that require a certain order (eg. calculating a running sum) if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order)) { continue; } // Skip columns that require a certain order AND paginated result if (isset($properties['only_with_order']) && $this->page > 1) { continue; } if (array_key_exists('select', $properties)) { $select = $properties['select'] ?? 'NULL'; $columns[] = sprintf('%s AS %s', $select, $db->quoteIdentifier($alias)); } else { $columns[] = $db->quoteIdentifier($alias); } } $columns = implode(', ', $columns); if (isset($this->columns[$this->order]['order'])) { $order = sprintf($this->columns[$this->order]['order'], $this->desc ? 'DESC' : 'ASC'); } else { $order = $db->quoteIdentifiers($this->order); if (true === $this->desc) { $order .= ' DESC'; } } $group = $this->group ? 'GROUP BY ' . $this->group : ''; |
︙ | ︙ |
Modified src/include/lib/Garradin/Email/Emails.php from [967d4a8042] to [95f634e297].
1 2 3 4 5 6 7 | <?php namespace Garradin\Email; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php namespace Garradin\Email; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; use Garradin\Plugins; use Garradin\UserException; use Garradin\Entities\Email\Email; use Garradin\Entities\Users\User; use Garradin\Users\DynamicFields; use Garradin\UserTemplate\UserTemplate; use Garradin\Web\Render\Render; use Garradin\Web\Skeleton; use const Garradin\{USE_CRON, MAIL_RETURN_PATH, DISABLE_EMAIL}; use const Garradin\{SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_SECURITY}; use KD2\SMTP; use KD2\Security; use KD2\Mail_Message; use KD2\DB\EntityManager as EM; |
︙ | ︙ | |||
112 113 114 115 116 117 118 | if (!count($list)) { return; } $recipients = $list; unset($list); | | | 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | if (!count($list)) { return; } $recipients = $list; unset($list); if (Plugins::fireSignal('email.queue.before', compact('context', 'recipients', 'sender', 'subject', 'content', 'render'))) { // queue handling was done by a plugin return; } $template = ($content instanceof UserTemplate) ? $content : null; $skel = null; $content_html = null; |
︙ | ︙ | |||
170 171 172 173 174 175 176 | 'recipient' => $to, 'data' => $variables, 'context' => $context, 'from' => $sender, ]); } | | | | 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | 'recipient' => $to, 'data' => $variables, 'context' => $context, 'from' => $sender, ]); } if (Plugins::fireSignal('email.queue.insert', compact('context', 'to', 'sender', 'subject', 'content', 'render', 'hash', 'content_html') + ['pgp_key' => $data['pgp_key'] ?? null])) { // queue insert was done by a plugin continue; } $st->bindValue(':sender', $sender); $st->bindValue(':subject', $subject); $st->bindValue(':context', $context); $st->bindValue(':recipient', $to); $st->bindValue(':recipient_pgp_key', $variables['pgp_key'] ?? null); $st->bindValue(':recipient_hash', $hash); $st->bindValue(':content', $content); $st->bindValue(':content_html', $content_html); $st->execute(); $st->reset(); $st->clear(); } $db->commit(); if (Plugins::fireSignal('email.queue.after', compact('context', 'recipients', 'sender', 'subject', 'content', 'render'))) { return; } // If no crontab is used, then the queue should be run now if (!USE_CRON) { self::runQueue(); } |
︙ | ︙ | |||
552 553 554 555 556 557 558 | static public function sendMessage(int $context, Mail_Message $message): void { if (DISABLE_EMAIL) { return; } | | | | | 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 | static public function sendMessage(int $context, Mail_Message $message): void { if (DISABLE_EMAIL) { return; } $email_sent_via_plugin = Plugins::fireSignal('email.send.before', compact('context', 'message')); if ($email_sent_via_plugin) { return; } if (SMTP_HOST) { $const = '\KD2\SMTP::' . strtoupper(SMTP_SECURITY); $secure = constant($const); $smtp = new SMTP(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, $secure); $smtp->send($message); } else { $message->send(); } Plugins::fireSignal('email.send.after', compact('context', 'message')); } /** * Handle a bounce message * @param string $raw_message Raw MIME message from SMTP */ static public function handleBounce(string $raw_message): ?array { $message = new Mail_Message; $message->parse($raw_message); $return = $message->identifyBounce(); if (Plugins::fireSignal('email.bounce', compact('message', 'return', 'raw_message'))) { return null; } if (!$return) { return null; } |
︙ | ︙ | |||
625 626 627 628 629 630 631 | $return = compact('recipient', 'type', 'message'); $email = self::getOrCreateEmail($return['recipient']); if (!$email) { return null; } | | | 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 | $return = compact('recipient', 'type', 'message'); $email = self::getOrCreateEmail($return['recipient']); if (!$email) { return null; } Plugins::fireSignal('email.bounce', compact('email', 'return')); $email->hasFailed($return); $email->save(); return $return; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Accounting/Account.php from [75230895b2] to [0d077ef548].
︙ | ︙ | |||
757 758 759 760 761 762 763 | $prev = $line->csv; } } return $lines; } | | > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | < < | < < > > > < | | | < < < < < < < < | 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 | $prev = $line->csv; } } return $lines; } public function getDepositJournal(int $year_id, array $checked = []): DynamicList { $columns = [ 'id' => [ 'label' => 'Num.', 'select' => 't.id', ], 'date' => [ 'select' => 't.date', 'label' => 'Date', 'order' => 't.date %s, t.id %1$s', ], 'reference' => [ 'select' => 't.reference', 'label' => 'Réf. écriture', ], 'line_reference' => [ 'select' => 'l.reference', 'label' => 'Réf. paiement', ], 'label' => [ 'label' => 'Libellé', 'select' => 't.label', ], 'amount' => [ 'label' => 'Montant', 'select' => 'l.debit', ], 'running_sum' => [ 'label' => 'Solde cumulé', 'only_with_order' => 'date', 'select' => null, ], 'credit' => [ 'select' => 'l.credit', ], 'debit' => [ 'select' => 'l.debit', ], 'id_account' => [ 'select' => 'l.id_account', ], 'id_line' => [ 'select' => 'l.id', ], ]; $tables = 'acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction'; $conditions = sprintf('t.id_year = %d AND l.id_account = %d AND l.credit = 0 AND NOT (t.status & %d)', $year_id, $this->id(), Transaction::STATUS_DEPOSIT); $list = new DynamicList($columns, $tables, $conditions); $list->setPageSize(null); $list->orderBy('date', true); $list->setModifier(function (&$row) use (&$sum, $checked) { $sum += ($row->credit - $row->debit); $row->running_sum = $sum; $row->checked = array_key_exists($row->id, $checked); }); return $list; } public function getDepositMissingBalance(int $year_id): int { $deposit_balance = DB::getInstance()->firstColumn('SELECT SUM(l.debit) FROM acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [a5e9d959a8] to [0a0f54d4f9].
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | use Garradin\Users\DynamicFields; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Accounting\Accounts; use Garradin\Accounting\Projects; use Garradin\ValidationException; class Transaction extends Entity { const NAME = 'Écriture'; const PRIVATE_URL = '!acc/transactions/details.php?id=%d'; const TABLE = 'acc_transactions'; const TYPE_ADVANCED = 0; const TYPE_REVENUE = 1; const TYPE_EXPENSE = 2; const TYPE_TRANSFER = 3; const TYPE_DEBT = 4; const TYPE_CREDIT = 5; const STATUS_WAITING = 1; const STATUS_PAID = 2; const STATUS_DEPOSIT = 4; const STATUS_ERROR = 8; const STATUS_NAMES = [ 1 => 'En attente de règlement', 2 => 'Réglé', 4 => 'Déposé en banque', ]; | > > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | use Garradin\Users\DynamicFields; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Accounting\Accounts; use Garradin\Accounting\Projects; use Garradin\Accounting\Years; use Garradin\ValidationException; class Transaction extends Entity { const NAME = 'Écriture'; const PRIVATE_URL = '!acc/transactions/details.php?id=%d'; const TABLE = 'acc_transactions'; const TYPE_ADVANCED = 0; const TYPE_REVENUE = 1; const TYPE_EXPENSE = 2; const TYPE_TRANSFER = 3; const TYPE_DEBT = 4; const TYPE_CREDIT = 5; const STATUS_WAITING = 1; const STATUS_PAID = 2; const STATUS_DEPOSIT = 4; const STATUS_ERROR = 8; const STATUS_OPENING_BALANCE = 16; const STATUS_NAMES = [ 1 => 'En attente de règlement', 2 => 'Réglé', 4 => 'Déposé en banque', ]; |
︙ | ︙ | |||
503 504 505 506 507 508 509 510 511 512 513 514 515 516 | } return true; } public function assertCanBeModified(): void { // We allow to change notes and id_project in a locked transaction if (!$this->canSaveChanges()) { throw new ValidationException('Il n\'est pas possible de modifier une écriture qui a été verrouillée'); } $db = DB::getInstance(); | > > > > > | 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 | } return true; } public function assertCanBeModified(): void { // Allow to change the status if (count($this->_modified) === 1 && array_key_exists('status', $this->_modified)) { return; } // We allow to change notes and id_project in a locked transaction if (!$this->canSaveChanges()) { throw new ValidationException('Il n\'est pas possible de modifier une écriture qui a été verrouillée'); } $db = DB::getInstance(); |
︙ | ︙ | |||
655 656 657 658 659 660 661 | public function selfCheck(): void { $db = DB::getInstance(); $this->assert(!empty($this->id_year), 'L\'ID de l\'exercice doit être renseigné.'); | | | > > > > > > > > | 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 | public function selfCheck(): void { $db = DB::getInstance(); $this->assert(!empty($this->id_year), 'L\'ID de l\'exercice doit être renseigné.'); $this->assert(!empty($this->label) && trim((string)$this->label) !== '', 'Le champ libellé ne peut rester vide.'); $this->assert(strlen($this->label) <= 200, 'Le champ libellé ne peut faire plus de 200 caractères.'); $this->assert(!isset($this->reference) || strlen($this->reference) <= 200, 'Le champ numéro de pièce comptable ne peut faire plus de 200 caractères.'); $this->assert(!isset($this->notes) || strlen($this->notes) <= 2000, 'Le champ remarques ne peut faire plus de 2000 caractères.'); $this->assert(!empty($this->date), 'Le champ date ne peut rester vide.'); $this->assert(null !== $this->id_year, 'Aucun exercice spécifié.'); $this->assert(array_key_exists($this->type, self::TYPES_NAMES), 'Type d\'écriture inconnu : ' . $this->type); $this->assert(null === $this->id_creator || $db->test('users', 'id = ?', $this->id_creator), 'Le membre créateur de l\'écriture n\'existe pas ou plus'); $is_in_year = $db->test(Year::TABLE, 'id = ? AND start_date <= ? AND end_date >= ?', $this->id_year, $this->date->format('Y-m-d'), $this->date->format('Y-m-d')); if (!$is_in_year) { $year = Years::get($this->id_year); throw new ValidationException(sprintf('La date (%s) de l\'écriture ne correspond pas à l\'exercice "%s" : la date doit être entre le %s et le %s.', Utils::shortDate($this->date), $year->label ?? '', Utils::shortDate($year->start_date), Utils::shortDate($year->end_date) )); } $total = 0; $lines = $this->getLines(); $count = count($lines); $this->assert($count > 0, 'Cette écriture ne comporte aucune ligne.'); |
︙ | ︙ | |||
946 947 948 949 950 951 952 953 954 955 956 957 958 959 | $source = $_POST; } $this->label = 'Balance d\'ouverture'; $this->date = $year->start_date; $this->id_year = $year->id(); $this->type = self::TYPE_ADVANCED; $this->importFromNewForm($source); $diff = $this->getLinesCreditSum() - $this->getLinesDebitSum(); if (!$diff) { return; | > | 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 | $source = $_POST; } $this->label = 'Balance d\'ouverture'; $this->date = $year->start_date; $this->id_year = $year->id(); $this->type = self::TYPE_ADVANCED; $this->addStatus(self::STATUS_OPENING_BALANCE); $this->importFromNewForm($source); $diff = $this->getLinesCreditSum() - $this->getLinesDebitSum(); if (!$diff) { return; |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Accounting/Year.php from [264d89b87d] to [19edb1963d].
︙ | ︙ | |||
240 241 242 243 244 245 246 | } } } return $out; } | > > > | > > > > > > > > > > > | 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | } } } return $out; } public function hasOpeningBalance(): bool { return DB::getInstance()->test(Transaction::TABLE, 'id_year = ? AND status & ?', $this->id(), Transaction::STATUS_OPENING_BALANCE); } public function deleteOpeningBalance(): void { $em = EntityManager::getInstance(Transaction::class); $list = $em->iterate('SELECT * FROM @TABLE WHERE id_year = ? AND status & ?', $this->id(), Transaction::STATUS_OPENING_BALANCE); foreach ($list as $t) { $t->delete(); } } } |
Modified src/include/lib/Garradin/Entities/Email/Email.php from [ceddb31aa6] to [36e29843f4].
︙ | ︙ | |||
136 137 138 139 140 141 142 143 144 145 | if ($percent > 90) { throw new UserException(sprintf('Adresse e-mail invalide : avez-vous fait une erreur, par exemple "%s" à la place de "%s" ?', $host, $common_domain)); } } throw new UserException('Adresse e-mail invalide : vérifiez que vous n\'avez pas fait une faute de frappe.'); } getmxrr($host, $mx_list); | > > > > > | | 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | if ($percent > 90) { throw new UserException(sprintf('Adresse e-mail invalide : avez-vous fait une erreur, par exemple "%s" à la place de "%s" ?', $host, $common_domain)); } } throw new UserException('Adresse e-mail invalide : vérifiez que vous n\'avez pas fait une faute de frappe.'); } // Windows does not support MX lookups if (PHP_OS_FAMILY == 'Windows') { return; } getmxrr($host, $mx_list); if (empty($mx_list)) { throw new UserException('Adresse e-mail invalide (le domaine indiqué n\'a pas de service e-mail) : vérifiez que vous n\'avez pas fait une faute de frappe.'); } foreach ($mx_list as $mx) { if (preg_match(self::BLACKLIST_MANUAL_VALIDATION_MX, $mx)) { throw new UserException('Adresse e-mail invalide : impossible d\'envoyer des mails à un service (de type mailinblack ou spamenmoins) qui demande une validation manuelle de l\'expéditeur. Merci de choisir une autre adresse e-mail.'); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Files/File.php from [7450a23eb6] to [89f8d9569c].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin\Entities\Files; use KD2\Graphics\Image; use KD2\Graphics\Blob; use KD2\DB\EntityManager as EM; use KD2\Security; use Garradin\Config; use Garradin\DB; use Garradin\Entity; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php namespace Garradin\Entities\Files; use KD2\Graphics\Image; use KD2\Graphics\Blob; use KD2\DB\EntityManager as EM; use KD2\Security; use Garradin\Config; use Garradin\DB; use Garradin\Entity; use Garradin\Plugins; use Garradin\Template; use Garradin\UserException; use Garradin\ValidationException; use Garradin\Users\Session; use Garradin\Static_Cache; use Garradin\Utils; use Garradin\Entities\Web\Page; |
︙ | ︙ | |||
198 199 200 201 202 203 204 | Files::callStorage('checkLock'); Web_Cache::delete($this->uri()); // Delete actual file content Files::callStorage('delete', $this); | | | 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | Files::callStorage('checkLock'); Web_Cache::delete($this->uri()); // Delete actual file content Files::callStorage('delete', $this); Plugins::fireSignal('files.delete', ['file' => $this]); // clean up thumbnails foreach (self::ALLOWED_THUMB_SIZES as $key => $operations) { Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $key)); } |
︙ | ︙ | |||
263 264 265 266 267 268 269 | throw new UserException(sprintf('Impossible de renommer "%s" vers "%s"', $this->path, $new_path)); } } Files::ensureDirectoryExists(Utils::dirname($new_path)); $return = Files::callStorage('move', $this, $new_path); | | | 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | throw new UserException(sprintf('Impossible de renommer "%s" vers "%s"', $this->path, $new_path)); } } Files::ensureDirectoryExists(Utils::dirname($new_path)); $return = Files::callStorage('move', $this, $new_path); Plugins::fireSignal('files.move', ['file' => $this, 'new_path' => $new_path]); return $return; } /** * Copy the current file to a new location * @param string $target Target path |
︙ | ︙ | |||
403 404 405 406 407 408 409 | fclose($pointer); } if (!$return) { throw new UserException('Le fichier n\'a pas pu être enregistré.'); } | | | 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 | fclose($pointer); } if (!$return) { throw new UserException('Le fichier n\'a pas pu être enregistré.'); } Plugins::fireSignal('files.store', ['file' => $this]); if ($index_search && $content) { $this->indexForSearch($content); } else { $this->removeFromSearch(); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Module.php from [df8c6b5f42] to [2994320732].
︙ | ︙ | |||
12 13 14 15 16 17 18 | use const Garradin\{ROOT, WWW_URL}; class Module extends Entity { const ROOT = File::CONTEXT_SKELETON . '/modules'; const DIST_ROOT = ROOT . '/skel-dist/modules'; | | | > | > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | use const Garradin\{ROOT, WWW_URL}; class Module extends Entity { const ROOT = File::CONTEXT_SKELETON . '/modules'; const DIST_ROOT = ROOT . '/skel-dist/modules'; const META_FILE = 'module.ini'; const ICON_FILE = 'icon.svg'; const README_FILE = 'README.md'; const CONFIG_FILE = 'config.html'; const INDEX_FILE = 'index.html'; // Snippets, don't forget to create alias constant in UserTemplate\Modules class const SNIPPET_TRANSACTION = 'snippets/transaction_details.html'; const SNIPPET_USER = 'snippets/user_details.html'; const SNIPPET_HOME_BUTTON = 'snippets/home_button.html'; const SNIPPETS = [ |
︙ | ︙ | |||
38 39 40 41 42 43 44 | /** * Directory name */ protected string $name; protected string $label; protected ?string $description; | > | > > > > > | | | | | | | | < < | | > | | < < | < | > > > > | > | < > | | | | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | /** * Directory name */ protected string $name; protected string $label; protected ?string $description; protected ?string $author; protected ?string $author_url; protected ?string $restrict_section; protected ?int $restrict_level; protected bool $home_button; protected bool $menu; protected ?\stdClass $config; protected bool $enabled; public function selfCheck(): void { $this->assert(preg_match('/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/', $this->name), 'Nom unique de module invalide: ' . $this->name); $this->assert(trim($this->label) !== '', 'Le libellé ne peut rester vide'); } /** * Fills information from module.ini file */ public function updateFromINI(bool $use_local = true): bool { if ($use_local && ($file = Files::get($this->path(self::META_FILE)))) { $ini = $file->fetch(); } elseif (file_exists($this->distPath(self::META_FILE))) { $ini = file_get_contents($this->distPath(self::META_FILE)); } else { return false; } $ini = @parse_ini_string($ini, false, \INI_SCANNER_TYPED); if (empty($ini)) { return false; } $ini = (object) $ini; if (!isset($ini->name)) { return false; } $this->set('label', $ini->name); $this->set('description', $ini->description ?? null); $this->set('author', $ini->author ?? null); $this->set('author_url', $ini->author_url ?? null); $this->set('home_button', !empty($ini->home_button)); $this->set('menu', !empty($ini->menu)); $this->set('restrict_section', $ini->restrict_section ?? null); $this->set('restrict_level', isset($ini->restrict_section, $ini->restrict_level, Session::ACCESS_WORDS[$ini->restrict_level]) ? Session::ACCESS_WORDS[$ini->restrict_level] : null); return true; } public function updateTemplates(): void { $check = self::SNIPPETS + [self::CONFIG_FILE => 'Config']; $templates = []; $db = DB::getInstance(); $db->begin(); $db->delete('modules_templates', 'id_module = ' . (int)$this->id()); foreach ($check as $file => $label) { if (Files::exists($this->path($file)) || file_exists($this->distPath($file))) { $templates[] = $file; $db->insert('modules_templates', ['id_module' => $this->id(), 'name' => $file]); } } $db->commit(); } public function icon_url(): ?string { if (!$this->hasFile(self::ICON_FILE)) { return null; } return $this->url(self::ICON_FILE); } public function path(string $file = null): string { return self::ROOT . '/' . $this->name . ($file ? '/' . $file : ''); } |
︙ | ︙ | |||
145 146 147 148 149 150 151 152 153 154 | return false; } public function hasDist(): bool { return file_exists($this->distPath()); } public function hasConfig(): bool { | > > > > > | > > > > > | | 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | return false; } public function hasDist(): bool { return file_exists($this->distPath()); } public function hasLocal(): bool { return Files::exists($this->path()); } public function hasConfig(): bool { return DB::getInstance()->test('modules_templates', 'id_module = ? AND name = ?', $this->id(), self::CONFIG_FILE); } public function hasData(): bool { return DB::getInstance()->test('sqlite_master', 'type = \'table\' AND name = ?', sprintf('modules_data_%s', $this->name)); } public function canDelete(): bool { return !empty($this->config) || $this->hasLocal() || $this->hasData(); } public function delete(): bool { $dir = $this->dir(); if ($dir) { |
︙ | ︙ | |||
187 188 189 190 191 192 193 | if (!preg_match('!^(?:snippets/)?[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $file)) { throw new \InvalidArgumentException('Invalid skeleton name'); } } public function template(string $file) { | | | < < | 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | if (!preg_match('!^(?:snippets/)?[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $file)) { throw new \InvalidArgumentException('Invalid skeleton name'); } } public function template(string $file) { if ($file == self::CONFIG_FILE) { Session::getInstance()->requireAccess(Session::SECTION_CONFIG, Session::ACCESS_ADMIN); } $this->validateFileName($file); $ut = new UserTemplate('modules/' . $this->name . '/' . $file); $ut->assign('module', array_merge($this->asArray(false), ['url' => $this->url()])); return $ut; } public function fetch(string $file, array $params): string { $ut = $this->template($file); $ut->assignArray($params); return $ut->fetch(); } } |
Added src/include/lib/Garradin/Entities/Plugin.php version [b18b04117d].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 | <?php namespace Garradin\Entities; use Garradin\Entity; use Garradin\DB; use Garradin\Plugins; use Garradin\Template; use Garradin\UserException; use Garradin\Files\Files; use Garradin\UserTemplate\UserTemplate; use Garradin\Users\Session; use Garradin\Web\Render\Parsedown; use Garradin\Entities\Files\File; use const Garradin\{PLUGINS_ROOT, WWW_URL, ROOT, ADMIN_URL}; class Plugin extends Entity { const META_FILE = 'plugin.ini'; const CONFIG_FILE = 'admin/config.php'; const INDEX_FILE = 'admin/index.php'; const ICON_FILE = 'admin/icon.svg'; const INSTALL_FILE = 'install.php'; const UPGRADE_FILE = 'upgrade.php'; const UNINSTALL_FILE = 'uninstall.php'; const README_FILE = 'admin/README.md'; const PROTECTED_FILES = [ self::META_FILE, self::INSTALL_FILE, self::UPGRADE_FILE, self::UNINSTALL_FILE, ]; const MIME_TYPES = [ '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/javascript', 'pdf' => 'application/pdf', 'png' => 'image/png', 'xml' => 'text/xml', 'svg' => 'image/svg+xml', 'webp' => 'image/webp', 'md' => 'text/x-markdown', ]; const TABLE = 'plugins'; protected ?int $id; /** * Directory name */ protected string $name; protected string $label; protected string $version; protected ?string $description; protected ?string $author; protected ?string $author_url; protected bool $home_button; protected bool $menu; protected ?string $restrict_section; protected ?int $restrict_level; protected ?\stdClass $config; protected bool $enabled; protected ?string $_broken_message = null; public function hasCode(): bool { return Plugins::exists($this->name); } public function selfCheck(): void { $this->assert(preg_match('/^' . Plugins::NAME_REGEXP . '$/', $this->name), 'Nom unique d\'extension invalide: ' . $this->name); $this->assert(isset($this->label) && trim($this->label) !== '', sprintf('%s : le nom de l\'extension ("name") ne peut rester vide', $this->name)); $this->assert(isset($this->label) && trim($this->version) !== '', sprintf('%s : la version ne peut rester vide', $this->name)); if ($this->hasCode() || $this->enabled) { $this->assert(!$this->menu || $this->hasFile(self::INDEX_FILE), 'Le fichier admin/index.php n\'existe pas alors que la directive "menu" est activée.'); $this->assert(!$this->home_button || $this->hasFile(self::INDEX_FILE), 'Le fichier admin/index.php n\'existe pas alors que la directive "home_button" est activée.'); $this->assert(!$this->home_button || $this->hasFile(self::ICON_FILE), 'Le fichier admin/icon.svg n\'existe pas alors que la directive "home_button" est activée.'); } } public function setBrokenMessage(string $str) { $this->_broken_message = $str; } public function getBrokenMessage(): ?string { return $this->_broken_message; } /** * Fills information from plugin.ini file */ public function updateFromINI(): bool { $ini = parse_ini_file($this->path(self::META_FILE), false, \INI_SCANNER_TYPED); if (empty($ini)) { return false; } $ini = (object) $ini; if (!isset($ini->name)) { return false; } $this->assert(empty($ini->min_version) || version_compare(\Garradin\garradin_version(), $ini->min_version, '>='), sprintf('L\'extension "%s" nécessite Paheko version %s ou supérieure.', $this->name, $ini->min_version)); $this->set('label', $ini->name); $this->set('version', $ini->version); $this->set('description', $ini->description ?? null); $this->set('author', $ini->author ?? null); $this->set('author_url', $ini->author_url ?? null); $this->set('home_button', !empty($ini->home_button)); $this->set('menu', !empty($ini->menu)); $this->set('restrict_section', $ini->restrict_section ?? null); $this->set('restrict_level', isset($ini->restrict_section, $ini->restrict_level, Session::ACCESS_WORDS[$ini->restrict_level]) ? Session::ACCESS_WORDS[$ini->restrict_level] : null); return true; } public function icon_url(): ?string { if (!$this->hasFile(self::ICON_FILE)) { return null; } return $this->url(self::ICON_FILE); } public function path(string $file = null): string { return Plugins::getPath($this->name) . ($file ? '/' . $file : ''); } public function hasFile(string $file): bool { return file_exists($this->path($file)); } public function hasConfig(): bool { return $this->hasFile(self::CONFIG_FILE); } public function url(string $file = '', array $params = null) { if (null !== $params) { $params = '?' . http_build_query($params); } if (substr($file, 0, 6) == 'admin/') { $url = ADMIN_URL; $file = substr($file, 6); } else { $url = WWW_URL; } return sprintf('%sp/%s/%s%s', $url, $this->name, $file, $params); } public function getConfig(string $key = null) { if (is_null($key)) { return $this->config; } if (property_exists($this->config, $key)) { return $this->config->$key; } return null; } public function setConfigProperty(string $key, $value = null) { if (null === $this->config) { $this->config = new \stdClass; } if (is_null($value)) { unset($this->config->$key); } else { $this->config->$key = $value; } $this->_modified['config'] = true; } public function setConfig(\stdClass $config) { $this->config = $config; $this->_modified['config'] = true; } /** * Associer un signal à un callback du plugin * @param string $signal Nom du signal (par exemple boucle.agenda pour la boucle de type AGENDA) * @param mixed $callback Callback, sous forme d'un nom de fonction ou de méthode statique * @return boolean TRUE */ public function registerSignal(string $signal, callable $callback): void { $callable_name = ''; if (!is_callable($callback, true, $callable_name) || !is_string($callable_name)) { throw new \LogicException('Le callback donné n\'est pas valide.'); } // pour empêcher d'appeler des méthodes de Garradin après un import de base de données "hackée" if (strpos($callable_name, 'Garradin\\Plugin\\') !== 0) { throw new \LogicException('Le callback donné n\'utilise pas le namespace Garradin\\Plugin : ' . $callable_name); } $db = DB::getInstance(); $callable_name = str_replace('Garradin\\Plugin\\', '', $callable_name); $db->preparedQuery('INSERT OR REPLACE INTO plugins_signals VALUES (?, ?, ?);', [$signal, $this->name, $callable_name]); } public function unregisterSignal(string $signal): void { DB::getInstance()->preparedQuery('DELETE FROM plugins_signals WHERE plugin = ? AND signal = ?;', [$this->name, $signal]); } public function delete(): bool { if ($this->hasFile(self::UNINSTALL_FILE)) { $this->call(self::UNINSTALL_FILE, true); } $db = DB::getInstance(); $db->delete('plugins_signals', 'plugin = ?', $this->name); return parent::delete(); } /** * 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 paheko_plugin.ini) * @return boolean TRUE si le plugin doit être mis à jour, FALSE sinon */ public function needUpgrade(): bool { $infos = (object) parse_ini_file($this->path(self::META_FILE), false); if (version_compare($this->version, $infos->version, '!=')) { return true; } return false; } /** * Mettre à jour le plugin * Appelle le fichier upgrade.php dans l'archive si celui-ci existe. */ public function upgrade(): void { $this->updateFromINI(); if ($this->hasFile(self::UPGRADE_FILE)) { $this->call(self::UPGRADE_FILE, true); } $this->save(); } public function oldVersion(): ?string { return $this->getModifiedProperty('version'); } public function call(string $file, bool $allow_protected = false): void { $file = ltrim($file, './'); if (preg_match('!(?:\.\.|[/\\\\]\.|\.[/\\\\])!', $file)) { throw new \UnexpectedValueException('Chemin de fichier incorrect.'); } if (!$allow_protected && in_array($file, self::PROTECTED_FILES)) { throw new UserException('Le fichier ' . $file . ' ne peut être appelé par cette méthode.'); } $path = $this->path($file); if (!file_exists($path)) { throw new UserException(sprintf('Le fichier "%s" n\'existe pas dans le plugin "%s"', $file, $this->name)); } if (is_dir($path)) { throw new UserException(sprintf('Sécurité : impossible de lister le répertoire "%s" du plugin "%s".', $file, $this->name)); } $is_private = (0 === strpos($file, 'admin/')); // Créer l'environnement d'exécution du plugin if (substr($file, -4) === '.php') { if (substr($file, 0, 6) == 'admin/' || substr($file, 0, 7) == 'public/') { define('Garradin\PLUGIN_ROOT', $this->path()); define('Garradin\PLUGIN_URL', WWW_URL . 'p/' . $this->name . '/'); define('Garradin\PLUGIN_ADMIN_URL', WWW_URL .'admin/p/' . $this->name . '/'); define('Garradin\PLUGIN_QSP', '?'); $tpl = Template::getInstance(); if ($is_private) { require ROOT . '/www/admin/_inc.php'; $tpl->assign('current', 'plugin_' . $this->name); } $tpl->assign('plugin', $this); $tpl->assign('plugin_url', \Garradin\PLUGIN_URL); $tpl->assign('plugin_admin_url', \Garradin\PLUGIN_ADMIN_URL); $tpl->assign('plugin_root', \Garradin\PLUGIN_ROOT); } $plugin = $this; include $path; } elseif (substr($file, -3) === '.md' && $is_private) { $p = new ParseDown(null, null); header('Content-Type: text/html'); printf('<!DOCYPE html><head> <style type="text/css">body { font-family: Verdana, sans-serif; padding: .5em; margin: 0; background: #fff; color: #000; }</style> <link rel="stylesheet" type="text/css" href="%scss.php" /></head><body>', ADMIN_URL); echo $p->text(file_get_contents($path)); } else { // Récupération du type MIME à partir de l'extension $pos = strrpos($path, '.'); $ext = substr($path, $pos+1); $mime = self::MIME_TYPES[$ext] ?? 'text/plain'; header('Content-Type: ' .$mime); header('Content-Length: ' . filesize($path)); header('Cache-Control: public, max-age=3600'); header('Last-Modified: ' . date(DATE_RFC7231, filemtime($path))); readfile($path); } } public function route(string $uri): void { $uri = ltrim($uri, '/'); if (0 === strpos($uri, 'admin/')) { if (!Session::getInstance()->isLogged()) { Utils::redirect('!login.php'); } } else { $uri = 'public/' . $uri; } if (!$uri || substr($uri, -1) == '/') { $uri .= 'index.php'; } try { $this->call($uri); } catch (\UnexpectedValueException $e) { http_response_code(404); throw new UserException($e->getMessage()); } } public function isAvailable(): bool { return $this->hasFile(self::META_FILE); } } |
Modified src/include/lib/Garradin/Entities/Services/Service_User.php from [06d5c19167] to [dcc6fb2e78].
︙ | ︙ | |||
185 186 187 188 189 190 191 192 193 194 195 196 197 198 | $transaction->type = Transaction::TYPE_REVENUE; $transaction->save(); $transaction->linkToUser($this->id_user, $this->id()); return $transaction; } static public function createFromForm(array $users, int $creator_id, bool $from_copy = false, ?array $source = null): self { if (null === $source) { $source = $_POST; } | > > > > > > > > > > > > | 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | $transaction->type = Transaction::TYPE_REVENUE; $transaction->save(); $transaction->linkToUser($this->id_user, $this->id()); return $transaction; } public function updateExpectedAmount(): void { $fee = $this->fee(); if ($fee && $fee->id_account && $this->id_user) { $this->set('expected_amount', $fee->getAmountForUser($this->id_user)); } else { $this->set('expected_amount', null); } } static public function createFromForm(array $users, int $creator_id, bool $from_copy = false, ?array $source = null): self { if (null === $source) { $source = $_POST; } |
︙ | ︙ | |||
209 210 211 212 213 214 215 | $su->importForm($source); $su->id_user = (int) $id; if (empty($su->id_service)) { throw new ValidationException('Aucune activité n\'a été sélectionnée.'); } | < | < | 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | $su->importForm($source); $su->id_user = (int) $id; if (empty($su->id_service)) { throw new ValidationException('Aucune activité n\'a été sélectionnée.'); } $su->updateExpectedAmount(); if ($su->isDuplicate($from_copy ? false : true)) { if ($from_copy) { continue; } else { throw new ValidationException(sprintf('%s : Cette activité a déjà été enregistrée pour ce membre et cette date', $name)); |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Users/DynamicField.php from [b95114f15e] to [74af5ec509].
︙ | ︙ | |||
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | const SYSTEM_FIELDS = [ 'id' => '?int', 'id_category' => 'int', 'pgp_key' => '?string', 'otp_secret' => '?string', 'date_login' => '?DateTime', 'id_parent' => '?int', 'is_parent' => 'bool', 'preferences' => '?stdClass', ]; const SYSTEM_FIELDS_SQL = [ 'id INTEGER PRIMARY KEY,', 'id_category INTEGER NOT NULL REFERENCES users_categories(id),', 'date_login TEXT NULL CHECK (date_login IS NULL OR datetime(date_login) = date_login),', 'otp_secret TEXT NULL,', 'pgp_key TEXT NULL,', 'id_parent INTEGER NULL REFERENCES users(id) ON DELETE SET NULL CHECK (id_parent IS NULL OR is_parent = 0),', 'is_parent INTEGER NOT NULL DEFAULT 0,', 'preferences TEXT NULL,' ]; | > > | 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | const SYSTEM_FIELDS = [ 'id' => '?int', 'id_category' => 'int', 'pgp_key' => '?string', 'otp_secret' => '?string', 'date_login' => '?DateTime', 'date_updated' => '?DateTime', 'id_parent' => '?int', 'is_parent' => 'bool', 'preferences' => '?stdClass', ]; const SYSTEM_FIELDS_SQL = [ 'id INTEGER PRIMARY KEY,', 'id_category INTEGER NOT NULL REFERENCES users_categories(id),', 'date_login TEXT NULL CHECK (date_login IS NULL OR datetime(date_login) = date_login),', 'date_updated TEXT NULL CHECK (date_updated IS NULL OR datetime(date_updated) = date_updated),', 'otp_secret TEXT NULL,', 'pgp_key TEXT NULL,', 'id_parent INTEGER NULL REFERENCES users(id) ON DELETE SET NULL CHECK (id_parent IS NULL OR is_parent = 0),', 'is_parent INTEGER NOT NULL DEFAULT 0,', 'preferences TEXT NULL,' ]; |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Users/User.php from [07dacbb826] to [71610b9fa9].
︙ | ︙ | |||
125 126 127 128 129 130 131 | // Check email addresses foreach (DynamicFields::getEmailFields() as $field) { $this->assert($this->$field === null || SMTP::checkEmailIsValid($this->$field, false), 'Cette adresse email n\'est pas valide.'); } // check user number $field = DynamicFields::getNumberField(); | | | 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | // Check email addresses foreach (DynamicFields::getEmailFields() as $field) { $this->assert($this->$field === null || SMTP::checkEmailIsValid($this->$field, false), 'Cette adresse email n\'est pas valide.'); } // check user number $field = DynamicFields::getNumberField(); $this->assert($this->$field !== null && ctype_digit((string)$this->$field), 'Numéro de membre invalide : ne peut contenir que des chiffres'); $db = DB::getInstance(); if (!$this->exists()) { $number_exists = $db->test(self::TABLE, sprintf('%s = ?', $db->quoteIdentifier($field)), $this->$field); } else { |
︙ | ︙ | |||
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | } return $out; } public function save(bool $selfcheck = true): bool { $columns = array_intersect(DynamicFields::getInstance()->getSearchColumns(), array_keys($this->_modified)); $login_field = DynamicFields::getLoginField(); $login_modified = $this->_modified[$login_field] ?? null; $password_modified = $this->_modified['password'] ?? null; parent::save($selfcheck); // We are not using a trigger as it would make modifying the users table from outside impossible // (because the transliterate_to_ascii function does not exist) if (count($columns)) { DynamicFields::getInstance()->rebuildUserSearchCache($this->id()); | > > > > > > | 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | } return $out; } public function save(bool $selfcheck = true): bool { if (!count($this->_modified) && $this->exists()) { return true; } $columns = array_intersect(DynamicFields::getInstance()->getSearchColumns(), array_keys($this->_modified)); $login_field = DynamicFields::getLoginField(); $login_modified = $this->_modified[$login_field] ?? null; $password_modified = $this->_modified['password'] ?? null; $this->set('date_updated', new \DateTime); parent::save($selfcheck); // We are not using a trigger as it would make modifying the users table from outside impossible // (because the transliterate_to_ascii function does not exist) if (count($columns)) { DynamicFields::getInstance()->rebuildUserSearchCache($this->id()); |
︙ | ︙ |
Modified src/include/lib/Garradin/Entity.php from [6c483405f0] to [f2feecdbe4].
︙ | ︙ | |||
113 114 115 116 117 118 119 | $this->selfCheck(); } $new = $this->exists() ? false : true; $modified = $this->isModified(); // Specific entity signal | | | | | | | | | | < | | | | 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | $this->selfCheck(); } $new = $this->exists() ? false : true; $modified = $this->isModified(); // Specific entity signal if (Plugins::fireSignal($name . '.before', ['entity' => $this, 'new' => $new])) { return true; } // Generic entity signal if (Plugins::fireSignal('entity.save.before', ['entity' => $this, 'new' => $new])) { return true; } $return = parent::save(false); // Log creation/edit, but don't record stuff that doesn't change anything if ($this::NAME && ($new || $modified)) { $type = str_replace('Garradin\Entities\\', '', get_class($this)); Log::add($new ? Log::CREATE : Log::EDIT, ['entity' => $type, 'id' => $this->id()]); } Plugins::fireSignal($name . '.after', ['entity' => $this, 'success' => $return, 'new' => $new]); Plugins::fireSignal('entity.save.after', ['entity' => $this, 'success' => $return, 'new' => $new]); return $return; } public function delete(): bool { $type = get_class($this); $type = str_replace('Garradin\Entities\\', '', $type); $name = 'entity.' . $type . '.delete'; $id = $this->id(); if (Plugins::fireSignal($name . '.before', ['entity' => $this, 'id' => $id])) { return true; } // Generic entity signal if (Plugins::fireSignal('entity.delete.before', ['entity' => $this, 'id' => $id])) { return true; } $return = parent::delete(); if ($this::NAME) { Log::add(Log::DELETE, ['entity' => $name, 'id' => $id]); } Plugins::fireSignal($name . '.after', ['entity' => $this, 'success' => $return, 'id' => $id]); Plugins::fireSignal('entity.delete.after', ['entity' => $this, 'success' => $return, 'id' => $id]); return $return; } } |
Modified src/include/lib/Garradin/Files/Files.php from [80bfb98411] to [c6af2950e6].
1 2 3 4 5 6 7 | <?php namespace Garradin\Files; use Garradin\Static_Cache; use Garradin\Config; use Garradin\DB; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin\Files; use Garradin\Static_Cache; use Garradin\Config; use Garradin\DB; use Garradin\Plugins; use Garradin\Utils; use Garradin\UserException; use Garradin\ValidationException; use Garradin\Users\Session; use Garradin\Entities\Files\File; use Garradin\Entities\Web\Page; |
︙ | ︙ | |||
867 868 869 870 871 872 873 | 'image' => false, ]); $file->modified = new \DateTime; Files::callStorage('mkdir', $file); | | | 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 | 'image' => false, ]); $file->modified = new \DateTime; Files::callStorage('mkdir', $file); Plugins::fireSignal('files.mkdir', compact('file')); return $file; } static public function ensureDirectoryExists(string $path): void { $db = DB::getInstance(); |
︙ | ︙ |
Modified src/include/lib/Garradin/Files/WebDAV/NextCloud.php from [98ea2e4b65] to [8231ba23c4].
︙ | ︙ | |||
19 20 21 22 23 24 25 26 27 28 29 30 31 32 | protected string $prefix = File::CONTEXT_DOCUMENTS . '/'; public function __construct() { $this->temporary_chunks_path = CACHE_ROOT . '/webdav.chunks'; $this->setRootURL(WWW_URL); } public function auth(?string $login, ?string $password): bool { $session = Session::getInstance(); if ($session->isLogged()) { return true; | > > > > > > > > > > > > | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | protected string $prefix = File::CONTEXT_DOCUMENTS . '/'; public function __construct() { $this->temporary_chunks_path = CACHE_ROOT . '/webdav.chunks'; $this->setRootURL(WWW_URL); } public function route(?string $uri = null): bool { $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; // Currently, iOS apps are broken if (stristr($ua, 'nextcloud-ios') || stristr($ua, 'owncloudapp')) { throw new WebDAV_Exception('Your client is not compatible with this server. Consider using a different WebDAV client.', 403); } return parent::route($uri); } public function auth(?string $login, ?string $password): bool { $session = Session::getInstance(); if ($session->isLogged()) { return true; |
︙ | ︙ |
Modified src/include/lib/Garradin/Files/WebDAV/Storage.php from [ca6c2a31dc] to [6f6b7f9e7f].
︙ | ︙ | |||
258 259 260 261 262 263 264 | $out[$name] = $v; } } return $out; } | | | 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | $out[$name] = $v; } } return $out; } public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash, ?int $mtime): bool { if (!strpos($uri, '/')) { throw new WebDAV_Exception('Impossible de créer un fichier ici', 403); } if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) { return false; |
︙ | ︙ | |||
283 284 285 286 287 288 289 | if ($new && !File::canCreate($uri, $this->session)) { throw new WebDAV_Exception('Vous n\'avez pas l\'autorisation de créer ce fichier', 403); } elseif (!$new && !$target->canWrite($this->session)) { throw new WebDAV_Exception('Vous n\'avez pas l\'autorisation de modifier ce fichier', 403); } | | | | 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | if ($new && !File::canCreate($uri, $this->session)) { throw new WebDAV_Exception('Vous n\'avez pas l\'autorisation de créer ce fichier', 403); } elseif (!$new && !$target->canWrite($this->session)) { throw new WebDAV_Exception('Vous n\'avez pas l\'autorisation de modifier ce fichier', 403); } $h = $hash ? hash_init($hash_algo == 'MD5' ? 'md5' : 'sha1') : null; while (!feof($pointer)) { if ($h) { hash_update($h, fread($pointer, 8192)); } else { fread($pointer, 8192); } } if ($h) { if (hash_final($h) != $hash) { throw new WebDAV_Exception('The data sent does not match the supplied hash', 400); } } // Check size $size = ftell($pointer); try { |
︙ | ︙ |
Modified src/include/lib/Garradin/Form.php from [dbe973dfc0] to [50a5e33df9].
︙ | ︙ | |||
27 28 29 30 31 32 33 | && strtoupper($_SERVER['REQUEST_METHOD']) == 'POST') { $this->addError('Le fichier envoyé dépasse la taille autorisée'); } } public function run(callable $fn, ?string $csrf_key = null, ?string $redirect = null, bool $follow_redirect = false): bool { | > | < > | < > > > > > > > > > > > > > > > > > > > | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | && strtoupper($_SERVER['REQUEST_METHOD']) == 'POST') { $this->addError('Le fichier envoyé dépasse la taille autorisée'); } } public function run(callable $fn, ?string $csrf_key = null, ?string $redirect = null, bool $follow_redirect = false): bool { try { if (null !== $csrf_key && !\KD2\Form::tokenCheck($csrf_key)) { throw new ValidationException('Une erreur est survenue, merci de bien vouloir renvoyer le formulaire.'); } call_user_func($fn); if (null !== $redirect) { if (array_key_exists('_dialog', $_GET)) { Utils::reloadParentFrame($follow_redirect ? $redirect : null); } Utils::redirect($redirect); } return true; } catch (UserException $e) { $this->addError($e); Form::reportUserException($e); return false; } } static public function reportUserException(UserException $e): void { if (REPORT_USER_EXCEPTIONS === 2) { throw $e; } elseif (REPORT_USER_EXCEPTIONS === 1) { \KD2\ErrorManager::reportExceptionSilent($e); } } public function runIf($condition, callable $fn, ?string $csrf_key = null, ?string $redirect = null, bool $follow_redirect = false): ?bool { if (is_string($condition) && empty($_POST[$condition])) { return null; } elseif (is_bool($condition) && !$condition) { return null; } return $this->run($fn, $csrf_key, $redirect, $follow_redirect); } /** * @deprecated */ public function check($token_action = '', Array $rules = null) { if (!\KD2\Form::tokenCheck($token_action)) { $this->errors[] = 'Une erreur est survenue, merci de bien vouloir renvoyer le formulaire.'; return false; } if (!is_null($rules) && !$this->validate($rules)) { return false; } return true; } /** * @deprecated */ public function validate(Array $rules, array $source = null) { return \KD2\Form::validate($rules, $this->errors, $source); } public function hasErrors() { |
︙ | ︙ |
Modified src/include/lib/Garradin/Install.php from [7414c2f446] to [83fa8d0d78].
︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | use Garradin\Entities\Users\Category; use Garradin\Entities\Users\User; use Garradin\Entities\Files\File; use Garradin\Entities\Search; use Garradin\Users\DynamicFields; use Garradin\Users\Session; use Garradin\Files\Files; use KD2\HTTP; /** * Pour procéder à l'installation de l'instance Garradin * Utile pour automatiser l'installation sans passer par la page d'installation */ class Install { /** * This sends the current installed version, as well as the PHP and SQLite versions * for statistics purposes. * * You can disable this by setting DISABLE_INSTALL_PING to TRUE in CONFIG_FILE */ static public function ping(): void | > > > > > > > > > > > > > > > > > > > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | use Garradin\Entities\Users\Category; use Garradin\Entities\Users\User; use Garradin\Entities\Files\File; use Garradin\Entities\Search; use Garradin\Users\DynamicFields; use Garradin\Users\Session; use Garradin\Files\Files; use Garradin\UserTemplate\Modules; use Garradin\Plugins; use KD2\HTTP; /** * Pour procéder à l'installation de l'instance Garradin * Utile pour automatiser l'installation sans passer par la page d'installation */ class Install { /** * List of plugins that should be displayed during installation (if present) */ const DEFAULT_PLUGINS = [ 'caisse', 'taima', ]; const DEFAULT_MODULES = [ 'recus_fiscaux', 'carte_membre', 'recu_don', 'recu_paiement', //'bilan_pc', //'invoice', ]; /** * This sends the current installed version, as well as the PHP and SQLite versions * for statistics purposes. * * You can disable this by setting DISABLE_INSTALL_PING to TRUE in CONFIG_FILE */ static public function ping(): void |
︙ | ︙ | |||
169 170 171 172 173 174 175 176 | self::assert(isset($source['password']) && isset($source['password_confirmed']) && trim($source['password']) !== '', 'Le mot de passe n\'est pas renseigné'); self::assert((bool)filter_var($source['user_email'], FILTER_VALIDATE_EMAIL), 'Adresse email invalide'); self::assert(strlen($source['password']) >= User::MINIMUM_PASSWORD_LENGTH, 'Le mot de passe est trop court'); self::assert($source['password'] === $source['password_confirmed'], 'La vérification du mot de passe ne correspond pas'); try { | > > > | | | 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | self::assert(isset($source['password']) && isset($source['password_confirmed']) && trim($source['password']) !== '', 'Le mot de passe n\'est pas renseigné'); self::assert((bool)filter_var($source['user_email'], FILTER_VALIDATE_EMAIL), 'Adresse email invalide'); self::assert(strlen($source['password']) >= User::MINIMUM_PASSWORD_LENGTH, 'Le mot de passe est trop court'); self::assert($source['password'] === $source['password_confirmed'], 'La vérification du mot de passe ne correspond pas'); $plugins = isset($source['plugins']) ? array_keys($source['plugins']) : []; $modules = isset($source['modules']) ? array_keys($source['modules']) : []; try { self::install($source['country'], $source['name'], $source['user_name'], $source['user_email'], $source['password'], $plugins, $modules); self::ping(); } catch (\Exception $e) { @unlink(DB_FILE); throw $e; } } static public function install(string $country_code, string $name, string $user_name, string $user_email, string $user_password, array $plugins = [], array $modules = []): void { if (file_exists(DB_FILE)) { throw new UserException('La base de données existe déjà.'); } self::checkAndCreateDirectories(); Files::disableQuota(); |
︙ | ︙ | |||
265 266 267 268 269 270 271 | 'password_confirmed' => $user_password, ]); $user->save(); $config->set('files', array_map(fn () => null, $config::FILES)); | | | 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | 'password_confirmed' => $user_password, ]); $user->save(); $config->set('files', array_map(fn () => null, $config::FILES)); $welcome_text = sprintf("Bienvenue dans l'administration de %s !\n\nUtilisez le menu à gauche pour accéder aux différentes sections.\n\nSi vous êtes perdu, n'hésitez pas à consulter l'aide :-)", $name); $config->setFile('admin_homepage', $welcome_text); // Import accounting chart $chart = Charts::installCountryDefault($country_code); // Create an example saved search (users) |
︙ | ︙ | |||
326 327 328 329 330 331 332 333 334 | 'label' => 'Écritures sans projet', 'target' => $search::TARGET_ACCOUNTING, 'type' => $search::TYPE_JSON, 'content' => json_encode($query), ]); $search->created = new \DateTime; $search->save(); // Install welcome plugin if available | > > | | > > > > > > > > > | > > | 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | 'label' => 'Écritures sans projet', 'target' => $search::TARGET_ACCOUNTING, 'type' => $search::TYPE_JSON, 'content' => json_encode($query), ]); $search->created = new \DateTime; $search->save(); $config->save(); // Install welcome plugin if available $has_welcome_plugin = Plugins::exists('welcome'); if ($has_welcome_plugin) { Plugins::install('welcome'); } foreach ($plugins as $plugin) { Plugins::install($plugin); } Modules::refresh(); foreach ($modules as $module) { $m = Modules::get($module); $m->set('enabled', true); $m->save(); } Files::enableQuota(); } static public function checkAndCreateDirectories() { // Vérifier que les répertoires vides existent, sinon les créer $paths = [ |
︙ | ︙ | |||
383 384 385 386 387 388 389 | } static public function setLocalConfig(string $key, $value, bool $overwrite = true): void { $path = ROOT . DIRECTORY_SEPARATOR . CONFIG_FILE; $new_line = sprintf('const %s = %s;', $key, var_export($value, true)); | | | 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | } static public function setLocalConfig(string $key, $value, bool $overwrite = true): void { $path = ROOT . DIRECTORY_SEPARATOR . CONFIG_FILE; $new_line = sprintf('const %s = %s;', $key, var_export($value, true)); if (@filesize($path)) { $config = file_get_contents($path); $pattern = sprintf('/^.*(?:const\s+%s|define\s*\(.*%1$s).*$/m', $key); $config = preg_replace($pattern, $new_line, $config, -1, $count); if ($count && !$overwrite) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Log.php from [88467b613d] to [1aaa0e26e7].
︙ | ︙ | |||
110 111 112 113 114 115 116 | if ($count >= self::SOFT_LOCKOUT_ATTEMPTS) { return -1; } return 0; } | | | | 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | if ($count >= self::SOFT_LOCKOUT_ATTEMPTS) { return -1; } return 0; } static public function list(array $params = []): DynamicList { $id_field = DynamicFields::getNameFieldsSQL('u'); $columns = [ 'id_user' => [ ], 'identity' => [ 'label' => isset($params['id_self']) ? null : (isset($params['history']) ? 'Membre à l\'origine de la modification' : 'Membre'), 'select' => $id_field, ], 'created' => [ 'label' => 'Date' ], 'type_icon' => [ 'select' => null, |
︙ | ︙ | |||
142 143 144 145 146 147 148 | 'ip_address' => [ 'label' => 'Adresse IP', ], ]; $tables = 'logs LEFT JOIN users u ON u.id = logs.id_user'; | > > > > > > > > > > | > | 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | 'ip_address' => [ 'label' => 'Adresse IP', ], ]; $tables = 'logs LEFT JOIN users u ON u.id = logs.id_user'; if (isset($params['id_user'])) { $conditions = 'logs.id_user = ' . (int)$params['id_user']; } elseif (isset($params['id_self'])) { $conditions = sprintf('logs.id_user = %d AND type < 10', (int)$params['id_self']); } elseif (isset($params['history'])) { $conditions = sprintf('logs.type IN (%d, %d, %d) AND json_extract(logs.details, \'$.entity\') = \'Users\\User\' AND json_extract(logs.details, \'$.id\') = %d', self::CREATE, self::EDIT, self::DELETE, (int)$params['history']); } else { $conditions = '1'; } $list = new DynamicList($columns, $tables, $conditions); $list->orderBy('created', true); $list->setCount('COUNT(logs.id)'); $list->setModifier(function (&$row) { $row->created = \DateTime::createFromFormat('!Y-m-d H:i:s', $row->created); $row->details = $row->details ? json_decode($row->details) : null; |
︙ | ︙ |
Modified src/include/lib/Garradin/Membres/Import.php from [efbfcdc67d] to [956a6ebd29].
︙ | ︙ | |||
161 162 163 164 165 166 167 168 169 170 171 172 173 174 | if ($found !== false) { $data[$name] |= 0x01 << $found; } } } } if (!empty($data['numero']) && $data['numero'] > 0) { $numero = (int)$data['numero']; } else { | > > > > | 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | if ($found !== false) { $data[$name] |= 0x01 << $found; } } } } if (!count($data)) { throw new UserException('Erreur sur la ligne ' . $line . ' : aucun champ de la fiche membre n\'a été trouvé'); } if (!empty($data['numero']) && $data['numero'] > 0) { $numero = (int)$data['numero']; } else { |
︙ | ︙ |
Deleted src/include/lib/Garradin/Plugin.php version [b780b1e451].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added src/include/lib/Garradin/Plugins.php version [7fc9edb137].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 | <?php namespace Garradin; use Garradin\Entities\Plugin; use Garradin\Entities\Module; use Garradin\Users\Session; use Garradin\DB; use Garradin\UserTemplate\CommonFunctions; use Garradin\UserTemplate\Modules; use \KD2\DB\EntityManager as EM; use const Garradin\{SYSTEM_SIGNALS, ADMIN_URL, WWW_URL, PLUGINS_ROOT}; class Plugins { const NAME_REGEXP = '[a-z][a-z0-9]*(?:_[a-z0-9]+)*'; /** * Set to false to disable signal firing * @var boolean */ static protected $signals = true; static public function toggleSignals(bool $enabled) { self::$signals = $enabled; } static public function getPrivateURL(string $id, string $path = '') { return ADMIN_URL . 'p/' . $id . '/' . ltrim($path, '/'); } static public function getPublicURL(string $id, string $path = '') { return WWW_URL . 'p/' . $id . '/' . ltrim($path, '/'); } static public function getPath(string $name): ?string { if (file_exists(PLUGINS_ROOT . '/' . $name)) { return PLUGINS_ROOT . '/' . $name; } elseif (file_exists(PLUGINS_ROOT . '/' . $name . '.tar.gz')) { return 'phar://' . PLUGINS_ROOT . '/' . $name . '.tar.gz'; } return null; } static public function exists(string $name): bool { return self::getPath($name) !== null; } /** * Déclenche le signal donné auprès des plugins enregistrés * @param string $signal Nom du signal * @param array $params Paramètres du callback (array ou null) * @return NULL NULL si aucun plugin n'a été appelé, * TRUE si un plugin a été appelé et a arrêté l'exécution, * FALSE si des plugins ont été appelés mais aucun n'a stoppé l'exécution */ static public function fireSignal($signal, $params = null, &$callback_return = null) { if (!self::$signals) { return null; } // Process SYSTEM_SIGNALS first foreach (SYSTEM_SIGNALS as $system_signal) { if (key($system_signal) != $signal) { continue; } if (!is_callable(current($system_signal))) { throw new \LogicException(sprintf('System signal: cannot call "%s" for signal "%s"', current($system_signal), key($system_signal))); } if (true === call_user_func_array(current($system_signal), [&$params, &$callback_return])) { return true; } } $list = DB::getInstance()->get('SELECT s.* FROM plugins_signals AS s INNER JOIN plugins p ON p.name = s.plugin WHERE s.signal = ? AND p.enabled = 1;', $signal); if (!count($list)) { return null; } if (null === $params) { $params = []; } foreach ($list as $row) { $path = self::getPath($row->plugin); // Ne pas appeler les plugins dont le code n'existe pas/plus, if (!$path) { continue; } $callback = 'Garradin\\Plugin\\' . $row->callback; if (!is_callable($callback)) { continue; } $params['plugin_root'] = $path; $return = call_user_func_array($callback, [&$params, &$callback_return]); if (true === $return) { return true; } } return false; } static public function listModulesAndPlugins(bool $installable = false): array { $list = []; if ($installable) { foreach (EM::getInstance(Module::class)->iterate('SELECT * FROM @TABLE WHERE enabled = 0;') as $m) { $list[$m->name] = ['module' => $m]; } foreach (self::listInstallable() as $name => $p) { $list[$name] = ['plugin' => $p]; } foreach (self::listInstalled() as $p) { if ($p->enabled) { continue; } $list[$p->name] = ['plugin' => $p]; } } else { foreach (EM::getInstance(Module::class)->iterate('SELECT * FROM @TABLE WHERE enabled = 1;') as $m) { $list[$m->name] = ['module' => $m]; } foreach (self::listInstalled() as $p) { if (!$p->enabled) { continue; } if (!$p->hasCode()) { $p->set('enabled', false); $p->save(); continue; } $list[$p->name] = ['plugin' => $p]; } } foreach ($list as &$item) { $c = isset($item['plugin']) ? $item['plugin'] : $item['module']; $item['icon_url'] = $c->icon_url(); $item['name'] = $c->name; $item['label'] = $c->label; $item['description'] = $c->description; $item['author'] = $c->author; $item['author_url'] = $c->author_url; $item['config_url'] = $c->hasConfig() ? $c->url($c::CONFIG_FILE) : null; $item['readme_url'] = $c->hasFile($c::README_FILE) ? $c->url($c::README_FILE) : null; $item['enabled'] = $c->enabled; $item['installed'] = isset($item['plugin']) ? $c->exists() : true; $item['broken'] = isset($item['plugin']) ? !$c->hasCode() : false; $item['broken_message'] = isset($item['plugin']) ? $c->getBrokenMessage() : false; $item['restrict_section'] = $c->restrict_section; $item['restrict_level'] = $c->restrict_level; $item['url'] = null; if ($c->hasFile($c::INDEX_FILE)) { $item['url'] = $c->url($c::INDEX_FILE); } } unset($item); usort($list, fn ($a, $b) => strnatcasecmp($a['label'] ?? $a['name'], $b['label'] ?? $b['name'])); return $list; } static public function listModulesAndPluginsMenu(Session $session): array { $list = []; $sql = 'SELECT \'module\' AS type, name, label, restrict_section, restrict_level FROM modules WHERE menu = 1 AND enabled = 1 UNION ALL SELECT \'plugin\' AS type, name, label, restrict_section, restrict_level FROM plugins WHERE menu = 1 AND enabled = 1;'; foreach (DB::getInstance()->get($sql) as $item) { if ($item->restrict_section && !$session->canAccess($item->restrict_section, $item->restrict_level)) { continue; } $list[$item->type . '_' . $item->name] = $item; } // Sort items by label uasort($list, fn ($a, $b) => strnatcasecmp($a->label, $b->label)); foreach ($list as &$item) { $item = sprintf('<a href="%s/%s/">%s</a>', $item->type == 'plugin' ? ADMIN_URL . 'p' : WWW_URL . 'm', $item->name, $item->label ); } unset($item); // Append plugins from signals self::fireSignal('menu.item', compact('session'), $list); return $list; } static public function listModulesAndPluginsHomeButtons(Session $session): array { $list = []; $sql = 'SELECT \'module\' AS type, name, label, restrict_section, restrict_level FROM modules WHERE home_button = 1 AND enabled = 1 UNION ALL SELECT \'plugin\' AS type, name, label, restrict_section, restrict_level FROM plugins WHERE home_button = 1 AND enabled = 1;'; foreach (DB::getInstance()->get($sql) as $item) { if ($item->restrict_section && !$session->canAccess($item->restrict_section, $item->restrict_level)) { continue; } $list[$item->type . '_' . $item->name] = $item; } // Sort items by label uasort($list, fn ($a, $b) => strnatcasecmp($a->label, $b->label)); foreach ($list as &$item) { $url = sprintf('%s/%s/', $item->type == 'plugin' ? ADMIN_URL . 'p' : WWW_URL . 'm', $item->name); $item = CommonFunctions::linkButton([ 'label' => $item->label, 'icon' => $url . 'icon.svg', 'href' => $url, ]); } unset($item); foreach (Modules::snippets(Modules::SNIPPET_HOME_BUTTON) as $name => $v) { $list['module_' . $name] = $v; } Plugins::fireSignal('home.button', ['user' => $session->getUser(), 'session' => $session], $list); return $list; } static public function get(string $name): ?Plugin { return EM::findOne(Plugin::class, 'SELECT * FROM @TABLE WHERE name = ?;', $name); } static public function listInstalled(): array { return EM::getInstance(Plugin::class)->all('SELECT * FROM @TABLE ORDER BY label COLLATE NOCASE ASC;'); } /** * Liste les plugins téléchargés mais non installés */ static public function listInstallable(bool $check_exists = true): array { $list = []; if ($check_exists) { $exists = DB::getInstance()->getAssoc('SELECT name, name FROM plugins;'); } else { $exists = []; } foreach (glob(PLUGINS_ROOT . '/*') as $file) { if (substr($file, 0, 1) == '.') { continue; } if (is_dir($file) && file_exists($file . '/' . Plugin::META_FILE)) { $file = basename($file); $name = $file; } elseif (substr($file, -7) == '.tar.gz') { $file = basename($file); $name = substr($file, 0, -7); } else { continue; } // Ignore existing plugins if (in_array($name, $exists)) { continue; } $list[$file] = null; $p = new Plugin; $p->name = $name; $p->updateFromINI(); $list[$name] = $p; try { $p->selfCheck(); } catch (ValidationException $e) { $p->setBrokenMessage($e->getMessage()); } } ksort($list); return $list; } static public function install(string $name): void { $plugin = self::get($name); if ($plugin) { $plugin->set('enabled', true); $plugin->save(); return; } $p = new Plugin; $p->name = $name; if (!$p->hasFile($p::META_FILE)) { throw new UserException(sprintf('Le plugin "%s" n\'est pas une extension Paheko : fichier plugin.ini manquant.', $name)); } $p->updateFromINI(); $db = DB::getInstance(); $db->begin(); $p->set('enabled', true); $p->save(); if ($p->hasFile($p::INSTALL_FILE)) { $p->call($p::INSTALL_FILE, true); } $db->commit(); } /** * Upgrade all plugins if required * This is run after an upgrade, a database restoration, or in the Plugins page */ static public function upgradeAllIfRequired(): bool { $i = 0; foreach (self::listInstalled() as $plugin) { // Ignore plugins if code is no longer available if (!$plugin->isAvailable()) { continue; } if ($plugin->needUpgrade()) { $plugin->upgrade(); $i++; } unset($plugin); } return $i > 0; } } |
Modified src/include/lib/Garradin/Sauvegarde.php from [02a4118000] to [59a4725000].
︙ | ︙ | |||
524 525 526 527 528 529 530 | // If logged-in user no longer exists, then login to first admin account if (!$session->refresh()) { $session->forceLogin(-1); $return |= self::CHANGED_USER; } // Check and upgrade plugins, if a software upgrade is necessary, plugins will be upgraded after the upgrade | | | 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 | // If logged-in user no longer exists, then login to first admin account if (!$session->refresh()) { $session->forceLogin(-1); $return |= self::CHANGED_USER; } // Check and upgrade plugins, if a software upgrade is necessary, plugins will be upgraded after the upgrade Plugins::upgradeAllIfRequired(); } return $return; } public function restore(string $file) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Services/Reminders.php from [90a6e75124] to [8c425b7b83].
1 2 3 4 5 6 7 | <?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; use Garradin\Plugins; use Garradin\Utils; use Garradin\Users\DynamicFields; use Garradin\Email\Emails; use Garradin\Entities\Services\Reminder; use KD2\DB\EntityManager; use const Garradin\WWW_URL; |
︙ | ︙ | |||
123 124 125 126 127 128 129 | $db->insert('services_reminders_sent', [ 'id_service' => $reminder->id_service, 'id_user' => $reminder->id_user, 'id_reminder' => $reminder->id_reminder, 'due_date' => $reminder->reminder_date, ]); | | | 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | $db->insert('services_reminders_sent', [ 'id_service' => $reminder->id_service, 'id_user' => $reminder->id_user, 'id_reminder' => $reminder->id_reminder, 'due_date' => $reminder->reminder_date, ]); Plugins::fireSignal('reminder.send.after', $reminder); return true; } /** * Envoi des rappels automatiques par e-mail * @return boolean TRUE en cas de succès |
︙ | ︙ |
Modified src/include/lib/Garradin/Template.php from [8cc6ebbbda] to [1f5a88a96f].
︙ | ︙ | |||
91 92 93 94 95 96 97 98 99 100 101 102 103 104 | $session = null; if (!defined('Garradin\INSTALL_PROCESS')) { $session = Session::getInstance(); $this->assign('config', Config::getInstance()); } $is_logged = $session ? $session->isLogged() : null; $this->assign('session', $session); $this->assign('is_logged', $is_logged); $this->assign('logged_user', $is_logged ? $session->getUser() : null); | > > > | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | $session = null; if (!defined('Garradin\INSTALL_PROCESS')) { $session = Session::getInstance(); $this->assign('config', Config::getInstance()); } else { $this->assign('config', null); } $is_logged = $session ? $session->isLogged() : null; $this->assign('session', $session); $this->assign('is_logged', $is_logged); $this->assign('logged_user', $is_logged ? $session->getUser() : null); |
︙ | ︙ | |||
168 169 170 171 172 173 174 175 176 177 178 179 180 181 | } foreach (CommonFunctions::FUNCTIONS_LIST as $key => $name) { $this->register_function(is_int($key) ? $name : $key, is_int($key) ? [CommonFunctions::class, $name] : $name); } $this->register_modifier('local_url', [Utils::class, 'getLocalURL']); } protected function formErrors($params) { $form = $this->getTemplateVars('form'); | > > > > | 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | } foreach (CommonFunctions::FUNCTIONS_LIST as $key => $name) { $this->register_function(is_int($key) ? $name : $key, is_int($key) ? [CommonFunctions::class, $name] : $name); } $this->register_modifier('local_url', [Utils::class, 'getLocalURL']); // Overwrite default money modifiers $this->register_modifier('money', [CommonModifiers::class, 'html_money']); $this->register_modifier('money_currency', [CommonModifiers::class, 'html_money_currency']); } protected function formErrors($params) { $form = $this->getTemplateVars('form'); |
︙ | ︙ | |||
281 282 283 284 285 286 287 | } return $n; } protected function customColors() { | | | | | 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | } return $n; } protected function customColors() { $config = defined('Garradin\INSTALL_PROCESS') ? null : Config::getInstance(); $c1 = ADMIN_COLOR1; $c2 = ADMIN_COLOR2; $bg = ADMIN_BACKGROUND_IMAGE; if (!FORCE_CUSTOM_COLORS && $config) { $c1 = $config->get('color1') ?: $c1; $c2 = $config->get('color2') ?: $c2; if ($url = $config->fileURL('admin_background')) { $bg = $url; } } $out = ' <style type="text/css"> :root { --gMainColor: %s; --gSecondColor: %s; --gBgImage: url("%s"); } </style>'; if ($config && $url = $config->fileURL('admin_css')) { $out .= "\n" . sprintf('<link rel="stylesheet" type="text/css" href="%s" />', $url); } return sprintf($out, CommonModifiers::css_hex_to_rgb($c1), CommonModifiers::css_hex_to_rgb($c2), $bg); } protected function displayDynamicField(array $params): string |
︙ | ︙ | |||
577 578 579 580 581 582 583 | $out .= '</table>'; return $out; } protected function displayPermissions(array $params): string { | > > > > > > > | > | < | | 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 | $out .= '</table>'; return $out; } protected function displayPermissions(array $params): string { $out = []; if (isset($params['section'], $params['level'])) { $list = [$params['section'] => Category::PERMISSIONS[$params['section']]]; $perms = (object) ['perm_' . $params['section'] => $params['level']]; } else { $perms = $params['permissions']; $list = Category::PERMISSIONS; } foreach ($list as $name => $config) { $access = $perms->{'perm_' . $name}; $label = sprintf('%s : %s', $config['label'], $config['options'][$access]); $out[$name] = sprintf('<b class="access_%s %s" title="%s">%s</b>', $access, $name, htmlspecialchars($label), $config['shape']); } return implode(' ', $out); } } |
Modified src/include/lib/Garradin/Upgrade.php from [c62ef681a0] to [71ae118229].
︙ | ︙ | |||
49 50 51 52 53 54 55 | static public function upgrade() { $db = DB::getInstance(); $backup = new Sauvegarde; $v = $db->version(); | | | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | static public function upgrade() { $db = DB::getInstance(); $backup = new Sauvegarde; $v = $db->version(); Plugins::toggleSignals(false); Static_Cache::store('upgrade', 'Updating'); // Créer une sauvegarde automatique $backup_file = sprintf(DATA_ROOT . '/association.pre_upgrade-%s.sqlite', garradin_version()); $backup->make($backup_file); |
︙ | ︙ | |||
148 149 150 151 152 153 154 | require ROOT . '/include/migrations/1.2/1.2.2.php'; } if (version_compare($v, '1.3.0', '<')) { require ROOT . '/include/migrations/1.3/1.3.0.php'; } | | | 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | require ROOT . '/include/migrations/1.2/1.2.2.php'; } if (version_compare($v, '1.3.0', '<')) { require ROOT . '/include/migrations/1.3/1.3.0.php'; } Plugins::upgradeAllIfRequired(); // Vérification de la cohérence des clés étrangères $db->foreignKeyCheck(); // Delete local cached files Utils::resetCache(USER_TEMPLATES_CACHE_ROOT); Utils::resetCache(STATIC_CACHE_ROOT); |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/CommonFunctions.php from [bc6f2d4af1] to [0663e40e65].
︙ | ︙ | |||
26 27 28 29 30 31 32 | // Extract params and keep attributes separated $attributes = array_diff_key($params, array_flip($params_list)); $params = array_intersect_key($params, array_flip($params_list)); extract($params, \EXTR_SKIP); if (!isset($name, $type)) { | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // Extract params and keep attributes separated $attributes = array_diff_key($params, array_flip($params_list)); $params = array_intersect_key($params, array_flip($params_list)); extract($params, \EXTR_SKIP); if (!isset($name, $type)) { throw new \RuntimeException('Missing name or type'); } $suffix = null; if ($type == 'datetime') { $type = 'date'; $tparams = func_get_arg(0); |
︙ | ︙ | |||
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | $out = sprintf('<dd class="radio-btn">%s <label for="f_%s_%s"><div><h3>%s</h3>%s</div></label> </dd>', $radio, htmlspecialchars((string)$name), htmlspecialchars((string)$value), htmlspecialchars((string)$label), isset($params['help']) ? '<p class="help">' . htmlspecialchars($params['help']) . '</p>' : ''); return $out; } if ($type == 'select') { $input = sprintf('<select %s>', $attributes_string); foreach ($options as $_key => $_value) { $input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', htmlspecialchars((string)$_value)); } $input .= '</select>'; } elseif ($type == 'select_groups') { $input = sprintf('<select %s>', $attributes_string); foreach ($options as $optgroup => $suboptions) { if (is_array($suboptions)) { $input .= sprintf('<optgroup label="%s">', htmlspecialchars((string)$optgroup)); foreach ($suboptions as $_key => $_value) { $input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', htmlspecialchars((string)$_value)); | > > > > > > > > | 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | $out = sprintf('<dd class="radio-btn">%s <label for="f_%s_%s"><div><h3>%s</h3>%s</div></label> </dd>', $radio, htmlspecialchars((string)$name), htmlspecialchars((string)$value), htmlspecialchars((string)$label), isset($params['help']) ? '<p class="help">' . htmlspecialchars($params['help']) . '</p>' : ''); return $out; } if ($type == 'select') { $input = sprintf('<select %s>', $attributes_string); if (empty($attributes['required'])) { $input .= '<option value=""></option>'; } foreach ($options as $_key => $_value) { $input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', htmlspecialchars((string)$_value)); } $input .= '</select>'; } elseif ($type == 'select_groups') { $input = sprintf('<select %s>', $attributes_string); if (empty($attributes['required'])) { $input .= '<option value=""></option>'; } foreach ($options as $optgroup => $suboptions) { if (is_array($suboptions)) { $input .= sprintf('<optgroup label="%s">', htmlspecialchars((string)$optgroup)); foreach ($suboptions as $_key => $_value) { $input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', htmlspecialchars((string)$_value)); |
︙ | ︙ | |||
299 300 301 302 303 304 305 | } return $out; } static public function icon(array $params): string { | | > > > > > > > > > > > | | > > > > > | | > > > > > > > | > > > > > > > > > > > > > > > > < < < | < | | 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 | } return $out; } static public function icon(array $params): string { if (isset($params['shape']) && isset($params['html']) && $params['html'] == false) { return Utils::iconUnicode($params['shape']); } if (!isset($params['shape']) && !isset($params['url'])) { throw new \RuntimeException('Missing parameter: shape or url'); } $html = ''; if (isset($params['url'])) { $html = self::getIconHTML(['icon' => $params['url']]); unset($params['url']); } $html .= htmlspecialchars($params['label'] ?? ''); unset($params['label']); self::setIconAttribute($params); $attributes = array_diff_key($params, ['shape']); $attributes = array_map(fn($v, $k) => sprintf('%s="%s"', $k, htmlspecialchars($v)), $attributes, array_keys($attributes)); $attributes = implode(' ', $attributes); return sprintf('<span %s>%s</span>', $attributes, $html); } static public function link(array $params): string { $href = $params['href']; $label = $params['label']; $prefix = $params['prefix'] ?? ''; if (!$href || !$label) { return ''; } // href can be prefixed with '!' to make the URL relative to ADMIN_URL if (substr($href, 0, 1) == '!') { $href = ADMIN_URL . substr($params['href'], 1); } // propagate _dialog param if we are in an iframe if (isset($_GET['_dialog']) && !isset($params['target'])) { $href .= (strpos($href, '?') === false ? '?' : '&') . '_dialog'; } if (!isset($params['class'])) { $params['class'] = ''; } unset($params['href'], $params['label'], $params['prefix']); array_walk($params, function (&$v, $k) { $v = sprintf('%s="%s"', $k, htmlspecialchars($v)); }); $params = implode(' ', $params); return sprintf('<a href="%s" %s>%s<span>%s</span></a>', htmlspecialchars($href), $params, $prefix, htmlspecialchars($label)); } static public function button(array $params): string { $label = isset($params['label']) ? htmlspecialchars($params['label']) : ''; unset($params['label']); self::setIconAttribute($params); if (!isset($params['type'])) { $params['type'] = 'button'; } if (!isset($params['class'])) { $params['class'] = ''; } if (isset($params['name']) && !isset($params['value'])) { $params['value'] = 1; } $prefix = ''; if (isset($params['icon'])) { $prefix = self::getIconHTML($params); unset($params['icon'], $params['icon_html']); } $params['class'] .= ' icn-btn'; // Remove NULL params $params = array_filter($params); array_walk($params, function (&$v, $k) { $v = sprintf('%s="%s"', $k, htmlspecialchars($v)); }); $params = implode(' ', $params); return sprintf('<button %s>%s%s</button>', $params, $prefix, $label); } static public function linkbutton(array $params): string { self::setIconAttribute($params); if (isset($params['icon']) || isset($params['icon_html'])) { $params['prefix'] = self::getIconHTML($params); unset($params['icon'], $params['icon_html']); } if (!isset($params['class'])) { $params['class'] = ''; } $params['class'] .= ' icn-btn'; return self::link($params); } static protected function getIconHTML(array $params): string { if (isset($params['icon_html'])) { return '<i class="icon">' . $params['icon_html'] . '</i>'; } return sprintf('<svg class="icon"><use xlink:href="%s#img" href="%1$s#img"></use></svg> ', htmlspecialchars(Utils::getLocalURL($params['icon'])) ); } static protected function setIconAttribute(array &$params): void { if (isset($params['shape'])) { $params['data-icon'] = Utils::iconUnicode($params['shape']); } unset($params['shape']); } } |
Modified src/include/lib/Garradin/UserTemplate/CommonModifiers.php from [0bf9c1d61a] to [1fa2bef475].
︙ | ︙ | |||
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | return call_user_func_array($name, $arguments); } const MODIFIERS_LIST = [ 'money', 'money_raw', 'money_currency', 'relative_date', 'relative_date_short', 'date_short', 'date_long', 'date_hour', 'date', 'strftime', 'size_in_bytes' => [Utils::class, 'format_bytes'], 'typo', 'css_hex_to_rgb', ]; | > > < < < | > > > | > > > | | | > > > > > > > > > > | | 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | return call_user_func_array($name, $arguments); } const MODIFIERS_LIST = [ 'money', 'money_raw', 'money_currency', 'money_html', 'money_currency_html', 'relative_date', 'relative_date_short', 'date_short', 'date_long', 'date_hour', 'date', 'strftime', 'size_in_bytes' => [Utils::class, 'format_bytes'], 'typo', 'css_hex_to_rgb', ]; static public function money($number, bool $hide_empty = true, bool $force_sign = false, bool $html = false): string { if ($hide_empty && !$number) { return ''; } $sign = ($force_sign && $number > 0) ? '+' : ''; $out = $sign . Utils::money_format($number, ',', $html ? ' ' : ' ', $hide_empty); if ($html) { $out = sprintf('<span class="money">%s</span>', $out); } return $out; } static public function money_raw($number, bool $hide_empty = true): string { return Utils::money_format($number, ',', '', $hide_empty); } static public function money_currency($number, bool $hide_empty = true, bool $force_sign = false, bool $html = false): string { $out = self::money($number, $hide_empty, $force_sign, $html); if ($out !== '') { $out .= ($html ? ' ' : ' ') . Config::getInstance()->get('currency'); } return $out; } static public function html_money($number, bool $hide_empty = true, bool $force_sign = false): string { return self::money($number, $hide_empty, $force_sign, false); } static public function html_money_currency($number, bool $hide_empty = true, bool $force_sign = false): string { return self::money_currency($number, $hide_empty, $force_sign, false); } static public function date_long($ts, bool $with_hour = false): ?string { return Utils::strftime_fr($ts, '%A %e %B %Y' . ($with_hour ? ' à %Hh%M' : '')); } static public function date_short($ts, bool $with_hour = false): ?string { return Utils::shortDate($ts, $with_hour); } static public function date_hour($ts, bool $minutes_only_if_required = false): ?string { $ts = Utils::get_datetime($ts); if (null === $ts) { |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Functions.php from [eb3ad6a62e] to [4ba2ad1378].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | <?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use KD2\ErrorManager; use KD2\JSONSchema; use Garradin\Config; use Garradin\DB; use Garradin\Template; use Garradin\Utils; use Garradin\UserException; use Garradin\Email\Emails; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Entities\Module; use const Garradin\{ROOT, WWW_URL}; class Functions { const FUNCTIONS_LIST = [ 'include', 'http', 'debug', 'error', 'read', 'save', 'admin_header', 'admin_footer', 'signature', 'mail', ]; static public function admin_header(array $params): string { $tpl = Template::getInstance(); $tpl->assign($params); return $tpl->fetch('_head.tpl'); } static public function admin_footer(array $params): string { $tpl = Template::getInstance(); $tpl->assign($params); | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | <?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use KD2\ErrorManager; use KD2\JSONSchema; use Garradin\Config; use Garradin\DB; use Garradin\Plugins; use Garradin\Template; use Garradin\Utils; use Garradin\UserException; use Garradin\Email\Emails; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Entities\Module; use Garradin\Entities\User\Email; use Garradin\Users\Session; use const Garradin\{ROOT, WWW_URL}; class Functions { const FUNCTIONS_LIST = [ 'include', 'http', 'debug', 'error', 'read', 'save', 'admin_header', 'admin_footer', 'signature', 'captcha', 'mail', ]; const COMPILE_FUNCTIONS_LIST = [ ':break' => [self::class, 'break'], ]; /** * Compile function to break inside a loop */ static public function break(string $name, string $params, Brindille $tpl, int $line) { $in_loop = false; foreach ($tpl->_stack as $element) { if ($element[0] == $tpl::SECTION) { $in_loop = true; break; } } if (!$in_loop) { throw new Brindille_Exception(sprintf('Error on line %d: break can only be used inside a section', $line)); } return '<?php break; ?>'; } static public function admin_header(array $params): string { $tpl = Template::getInstance(); $tpl->assign($params); $tpl->assign('plugins_menu', Plugins::listModulesAndPluginsMenu(Session::getInstance())); return $tpl->fetch('_head.tpl'); } static public function admin_footer(array $params): string { $tpl = Template::getInstance(); $tpl->assign($params); |
︙ | ︙ | |||
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | $tpl->assign($assign_new_id, $db->lastInsertId()); } } else { $db->update($table, compact('document'), sprintf('%s = :match', $field), ['match' => $where_value]); } } static public function mail(array $params, Brindille $tpl, int $line) { if (empty($params['to'])) { throw new Brindille_Exception(sprintf('Ligne %d: argument "to" manquant pour la fonction "mail"', $line)); } if (empty($params['subject'])) { throw new Brindille_Exception(sprintf('Ligne %d: argument "subject" manquant pour la fonction "mail"', $line)); } if (empty($params['body'])) { throw new Brindille_Exception(sprintf('Ligne %d: argument "body" manquant pour la fonction "mail"', $line)); } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > | 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | $tpl->assign($assign_new_id, $db->lastInsertId()); } } else { $db->update($table, compact('document'), sprintf('%s = :match', $field), ['match' => $where_value]); } } static public function captcha(array $params, Brindille $tpl, int $line) { $secret = md5(SECRET_KEY . Utils::getSelfURL(false)); if (isset($params['html'])) { $c = Security::createCaptcha($secret, $params['lang'] ?? 'fr'); return sprintf('<label for="f_c_42">Merci d\'écrire <strong><q>%s</q></strong> en chiffres :</label> <input type="text" name="f_c_42" id="f_c_42" placeholder="Exemple : 1234" /> <input type="hidden" name="f_c_43" value="%s" />', $c['spellout'], $c['hash']); } elseif (isset($params['assign_hash']) && isset($params['assign_number'])) { $c = Security::createCaptcha($secret, $params['lang'] ?? 'fr'); $tpl->assign($params['assign_hash'], $c['hash']); $tpl->assign($params['assign_number'], $c['spellout']); } elseif (isset($params['verify'])) { $hash = $_POST['f_c_43'] ?? ''; $number = $_POST['f_c_42'] ?? ''; } elseif (array_key_exists('verify_number', $params)) { $hash = $params['verify_hash'] ?? ''; $number = $params['verify_number'] ?? ''; } else { throw new Brindille_Exception(sprintf('Line %d: no valid arguments supplied for "captcha" function', $line)); } $error = 'Réponse invalide à la vérification anti-robot'; if (!Security::checkCaptcha($secret, trim($hash), trim($number))) { if (isset($params['assign_error'])) { $tpl->assign($params['assign_error'], $error); } else { throw new UserException($error); } } } static public function mail(array $params, Brindille $tpl, int $line) { if (empty($params['to'])) { throw new Brindille_Exception(sprintf('Ligne %d: argument "to" manquant pour la fonction "mail"', $line)); } if (empty($params['subject'])) { throw new Brindille_Exception(sprintf('Ligne %d: argument "subject" manquant pour la fonction "mail"', $line)); } if (empty($params['body'])) { throw new Brindille_Exception(sprintf('Ligne %d: argument "body" manquant pour la fonction "mail"', $line)); } if (!empty($params['block_urls']) && preg_match('!https?://!', $params['subject'] . $params['body'])) { throw new UserException('Merci de ne pas inclure d\'adresse web (http:…) dans le message'); } static $external = 0; static $internal = 0; if (is_string($params['to'])) { $params['to'] = [$params['to']]; } if (!count($params['to'])) { throw new Brindille_Exception(sprintf('Ligne %d: aucune adresse destinataire n\'a été précisée pour la fonction "mail"', $line)); } foreach ($params['to'] as &$to) { $to = trim($to); Email::validateAddress($to); } unset($to); $db = DB::getInstance(); $internal_count = $db->count('users', $db->where($email_field, 'IN', $params['to'])); $external_count = count($params['to']) - $internal_count; if (($external_count + $external) > 1) { throw new Brindille_Exception(sprintf('Ligne %d: l\'envoi d\'email à une adresse externe est limité à un envoi par page', $line)); } if (($internal_count + $internal) > 10) { throw new Brindille_Exception(sprintf('Ligne %d: l\'envoi d\'email à une adresse interne est limité à un envoi par page', $line)); } if ($external_count && preg_match_all('!(https?://.*?)(?=\s|$)!', $params['subject'] . ' ' . $params['body'], $match, PREG_PATTERN_ORDER)) { foreach ($match[1] as $m) { if (0 !== strpos($m, WWW_URL) && 0 !== strpos($m, ADMIN_URL)) { throw new Brindille_Exception(sprintf('Ligne %d: l\'envoi d\'email à une adresse externe interdit l\'utilisation d\'une adresse web autre que le site de l\'association : %s', $line, $m)); } } } $context = count($params['to']) == 1 ? Emails::CONTEXT_PRIVATE : Emails::CONTEXT_BULK; Emails::queue($context, $params['to'], null, $params['subject'], $params['body']); $internal += $internal_count; $external_count += $external_count; } static public function debug(array $params, Brindille $tpl) { if (!count($params)) { $params = $tpl->getAllVariables(); } |
︙ | ︙ | |||
254 255 256 257 258 259 260 | } $params['included_from'] = array_merge($from, [$path]); $include->assignArray(array_merge($ut->getAllVariables(), $params)); if (!empty($params['capture']) && preg_match('/^[a-z0-9_]+$/', $params['capture'])) { | | | | | 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 | } $params['included_from'] = array_merge($from, [$path]); $include->assignArray(array_merge($ut->getAllVariables(), $params)); if (!empty($params['capture']) && preg_match('/^[a-z0-9_]+$/', $params['capture'])) { $ut::__assign([$params['capture'] => $include->fetch()], $ut, $line); } else { $include->display(); } if (isset($params['keep'])) { $keep = explode(',', $params['keep']); $keep = array_map('trim', $keep); foreach ($keep as $name) { // Transmit variables $ut::__assign(['var' => $name, 'value' => $include->get($name)], $ut, $line); } } // Transmit nocache to parent template if ($include->get('nocache')) { $ut::__assign(['nocache' => true], $ut, $line); } } static public function http(array $params, UserTemplate $tpl): void { if (headers_sent()) { return; |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Modifiers.php from [c3be780789] to [d3236a70b7].
︙ | ︙ | |||
33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 'math', 'money_int' => [Utils::class, 'moneyToInteger'], 'array_transpose' => [Utils::class, 'array_transpose'], 'check_email', 'implode', 'quote_sql_identifier', 'quote_sql', ]; const LEADING_NUMBER_REGEXP = '/^([\d.]+)\s*[.\)]\s*/'; static public function replace($str, $find, $replace = null): string { if (is_array($find) && null === $replace) { | > > | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 'math', 'money_int' => [Utils::class, 'moneyToInteger'], 'array_transpose' => [Utils::class, 'array_transpose'], 'check_email', 'implode', 'quote_sql_identifier', 'quote_sql', 'sql_where', 'urlencode', ]; const LEADING_NUMBER_REGEXP = '/^([\d.]+)\s*[.\)]\s*/'; static public function replace($str, $find, $replace = null): string { if (is_array($find) && null === $replace) { |
︙ | ︙ | |||
259 260 261 262 263 264 265 | if (!is_array($array)) { throw new Brindille_Exception('Supplied argument is not an array'); } return implode($separator, $array); } | | | | > > > > > > > > > > | 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | if (!is_array($array)) { throw new Brindille_Exception('Supplied argument is not an array'); } return implode($separator, $array); } static public function quote_sql_identifier($in) { if (null === $in) { return ''; } $db = DB::getInstance(); if (is_array($in)) { return array_map([$db, 'quoteIdentifier'], $in); } return $db->quoteIdentifier($in); } static public function quote_sql($in) { if (null === $in) { return ''; } $db = DB::getInstance(); if (is_array($in)) { return array_map([$db, 'quote'], $in); } return $db->quote($in); } static public function sql_where(...$args) { return DB::getInstance()->where(...$args); } static public function urlencode($str): string { return rawurlencode($str ?? ''); } } |
Modified src/include/lib/Garradin/UserTemplate/Modules.php from [e01ba62dfa] to [e1712e8d0b].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <?php namespace Garradin\UserTemplate; use Garradin\Entities\Module; use Garradin\Files\Files; use Garradin\DB; use Garradin\Utils; use Garradin\UserException; use const Garradin\ROOT; use \KD2\DB\EntityManager as EM; class Modules { // Shortcuts so that code calling snippets method don't have to use Module entity const SNIPPET_TRANSACTION = Module::SNIPPET_TRANSACTION; const SNIPPET_USER = Module::SNIPPET_USER; const SNIPPET_HOME_BUTTON = Module::SNIPPET_HOME_BUTTON; /** * Lists all modules from files and stores a cache */ static public function refresh(): void { $existing = DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s;', Module::TABLE)); | > | < < < < < < < < < < < < < < < < < < < | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | <?php namespace Garradin\UserTemplate; use Garradin\Entities\Module; use Garradin\Files\Files; use Garradin\DB; use Garradin\Utils; use Garradin\UserException; use Garradin\Users\Session; use const Garradin\ROOT; use \KD2\DB\EntityManager as EM; class Modules { // Shortcuts so that code calling snippets method don't have to use Module entity const SNIPPET_TRANSACTION = Module::SNIPPET_TRANSACTION; const SNIPPET_USER = Module::SNIPPET_USER; const SNIPPET_HOME_BUTTON = Module::SNIPPET_HOME_BUTTON; /** * Lists all modules from files and stores a cache */ static public function refresh(): void { $existing = DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s;', Module::TABLE)); $list = self::listRaw(); $create = array_diff($list, $existing); $delete = array_diff($existing, $list); $existing = array_diff($list, $create); foreach ($create as $name) { self::create($name); } foreach ($delete as $name) { self::get($name)->delete(); } foreach ($existing as $name) { $f = self::get($name); $f->updateFromINI(); $f->save(); $f->updateTemplates(); } } /** * List modules names from locally installed directories */ static public function listRaw(bool $include_installed = true): array { $list = []; // First list modules bundled foreach (glob(Module::DIST_ROOT . '/*') as $file) { if (!is_dir($file)) { continue; } $name = Utils::basename($file); $list[$name] = $name; } if ($include_installed) { // Then add modules in files foreach (Files::list(Module::ROOT) as $file) { if ($file->type != $file::TYPE_DIRECTORY) { continue; } $list[$file->name] = $file->name; } } sort($list); return $list; } /** * List locally installed modules, directly from the filesystem, without creating them in the database cache * (used in Install form) */ static public function listLocal(): array { $list = self::listRaw(false); $out = []; foreach ($list as $name) { $m = new Module; $m->name = $name; if (!$m->updateFromINI(false)) { continue; } $out[$name] = $m; } return $out; } static public function create(string $name): ?Module { $module = new Module; $module->name = $name; if (!$module->updateFromINI()) { return null; } $module->save(); $module->updateTemplates(); return $module; } /** * List modules from the database */ static public function list(): array { return EM::getInstance(Module::class)->all('SELECT * FROM @TABLE ORDER BY label COLLATE NOCASE ASC;'); } static public function snippetsAsString(string $snippet, array $variables = []): string { return implode("\n", self::snippets($snippet, $variables)); } static public function snippets(string $snippet, array $variables = []): array { $out = []; foreach (self::listForSnippet($snippet) as $module) { $out[$module->name] = $module->fetch($snippet, $variables); } return array_filter($out, fn($a) => trim($a) !== ''); } static public function listForSnippet(string $snippet): array { return EM::getInstance(Module::class)->all('SELECT f.* FROM @TABLE f INNER JOIN modules_templates t ON t.id_module = f.id WHERE t.name = ? AND f.enabled = 1 |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Sections.php from [544ac36926] to [12e48946c9].
︙ | ︙ | |||
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 'transaction_users', 'accounts', 'balances', 'sql', 'restrict', 'module', ]; static protected $_cache = []; static protected function _debug(string $str): void { echo sprintf('<pre style="padding: 5px; margin: 5px; background: yellow; white-space: pre-wrap;">%s</pre>', htmlspecialchars($str)); } static protected function _debugExplain(string $sql): void | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | 'transaction_users', 'accounts', 'balances', 'sql', 'restrict', 'module', ]; const COMPILE_SECTIONS_LIST = [ '#select' => [self::class, 'selectStart'], '/select' => [self::class, 'selectEnd'], ]; /** * List of tables and columns that are restricted in SQL queries * * ~column means the column will always be returned as NULL * -column or !table means trying to access this column or table will return an error * see KD2/DB/SQLite3 code for details * * Note: column restrictions are only possible with PHP >= 8.0 */ const SQL_TABLES = [ // Allow access to all tables '*' => null, // Restrict access to private fields in users 'users' => ['~password', '~pgp_key', '~otp_secret'], // Restrict access to some private tables '!emails' => null, '!emails_queue' => null, '!compromised_passwords_cache' => null, '!compromised_passwords_cache_ranges' => null, '!api_credentials' => null, '!plugins_signals' => null, '!config' => null, '!users_sessions' => null, '!logs' => null, ]; static protected $_cache = []; static public function selectStart(string $name, string $sql, UserTemplate $tpl, int $line): string { $sql = strtok($sql, ';'); $extra_params = strtok(false); $i = 0; $params = ''; $sql = preg_replace_callback('/\{(.*?)\}/', function ($match) use (&$params, &$i) { // Raw SQL if ('!' === substr($match[1], 0, 1)) { $params .= ' !' . $i . '=' . substr($match[1], 1); return '!' . $i++; } else { $params .= ' :p' . $i . '=' . $match[1]; return ':p' . $i++; } }, $sql); $sql = 'SELECT ' . $sql; $sql = var_export($sql, true); $params .= ' sql=' . $sql . ' ' . $extra_params; return $tpl->_section('sql', $params, $line); } static public function selectEnd(string $name, string $params, UserTemplate $tpl, int $line): string { return $tpl->_close('sql', '{{/select}}'); } static protected function _debug(string $str): void { echo sprintf('<pre style="padding: 5px; margin: 5px; background: yellow; white-space: pre-wrap;">%s</pre>', htmlspecialchars($str)); } static protected function _debugExplain(string $sql): void |
︙ | ︙ | |||
455 456 457 458 459 460 461 | } $id_field = DynamicFields::getNameFieldsSQL(); $login_field = DynamicFields::getLoginField(); $number_field = DynamicFields::getNumberField(); if (empty($params['select'])) { | | | 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 | } $id_field = DynamicFields::getNameFieldsSQL(); $login_field = DynamicFields::getLoginField(); $number_field = DynamicFields::getNumberField(); if (empty($params['select'])) { $params['select'] = '*'; } $params['select'] .= sprintf(', %s AS _name, %s AS _login, %s AS _number', $id_field, $login_field, $number_field); $params['tables'] = 'users'; if (isset($params['id'])) { |
︙ | ︙ | |||
591 592 593 594 595 596 597 598 599 600 601 602 603 604 | static public function restrict(array $params, UserTemplate $tpl, int $line): ?\Generator { $session = Session::getInstance(); if (!$session->isLogged()) { if (!empty($params['block'])) { throw new UserException('Vous n\'avez pas accès à cette page.'); } return null; } | > > > > > | 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 | static public function restrict(array $params, UserTemplate $tpl, int $line): ?\Generator { $session = Session::getInstance(); if (!$session->isLogged()) { if (!empty($params['block'])) { if (!headers_sent()) { // FIXME: implement redirect to correct URL after login Utils::redirect('!login.php'); } throw new UserException('Vous n\'avez pas accès à cette page.'); } return null; } |
︙ | ︙ | |||
897 898 899 900 901 902 903 | 'select' => '*', 'order' => '1', 'begin' => 0, 'limit' => 100, 'where' => '', ]; | > > > > > > > > > > > > | | | | | | | | | | | | | | | | | | | | | | | | | | > | | 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 | 'select' => '*', 'order' => '1', 'begin' => 0, 'limit' => 100, 'where' => '', ]; if (isset($params['sql'])) { $sql = $params['sql']; // Replace raw SQL parameters (undocumented feature, this is for #select section) foreach ($params as $k => $v) { if (substr($k, 0, 1) == '!') { $r = '/' . preg_quote($k, '/') . '\b/'; $sql = preg_replace($r, $v, $sql); } } } else { if (empty($params['tables'])) { throw new Brindille_Exception(sprintf('"sql" section: missing parameter "tables" on line %d', $line)); } foreach ($defaults as $key => $default_value) { if (!isset($params[$key])) { $params[$key] = $default_value; } } // Allow for count=true, count=1 and also count="DISTINCT user_id" count="id" if (!empty($params['count'])) { $params['select'] = sprintf('COUNT(%s) AS count', $params['count'] == 1 ? '*' : $params['count']); $params['order'] = '1'; } if (!empty($params['where']) && !preg_match('/^\s*AND\s+/i', $params['where'])) { $params['where'] = ' AND ' . $params['where']; } $sql = sprintf('SELECT %s FROM %s WHERE 1 %s %s %s ORDER BY %s LIMIT %d,%d;', $params['select'], $params['tables'], $params['where'] ?? '', isset($params['group']) ? 'GROUP BY ' . $params['group'] : '', isset($params['having']) ? 'HAVING ' . $params['having'] : '', $params['order'], $params['begin'], $params['limit'] ); } $db = DB::getInstance(); try { $statement = $db->protectSelect(self::SQL_TABLES, $sql); $args = []; foreach ($params as $key => $value) { if (substr($key, 0, 1) == ':') { $args[$key] = $value; } |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/UserTemplate.php from [293ac1234e] to [26f1142bcc].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use KD2\Translate; use Garradin\Config; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use KD2\Translate; use Garradin\Config; use Garradin\Plugins; use Garradin\Utils; use Garradin\UserException; use Garradin\Users\Session; use Garradin\Entities\Files\File; use Garradin\Files\Files; |
︙ | ︙ | |||
85 86 87 88 89 90 91 | 'logged_user' => $is_logged ? $session->getUser() : null, 'dialog' => isset($_GET['_dialog']) ? ($_GET['_dialog'] ?: true) : false, ]; return self::$root_variables; } | | | | | | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | 'logged_user' => $is_logged ? $session->getUser() : null, 'dialog' => isset($_GET['_dialog']) ? ($_GET['_dialog'] ?: true) : false, ]; return self::$root_variables; } public function __construct(?string $path) { $this->_tpl_path = $path; if ($path && $file = Files::get(File::CONTEXT_SKELETON . '/' . $path)) { if ($file->type != $file::TYPE_FILE) { throw new \LogicException('Cannot construct a UserTemplate with a directory'); } $this->file = $file; $this->modified = $file->modified->getTimestamp(); } elseif ($path) { $this->path = self::DIST_ROOT . $path; if (!($this->modified = @filemtime($this->path))) { throw new \InvalidArgumentException('File not found: ' . $this->path); } } $this->assignArray(self::getRootVariables()); $this->registerAll(); Plugins::fireSignal('usertemplate.init', ['template' => $this]); } /** * Toggle safe mode * * If set to TRUE, then all functions and sections are removed, except foreach. * Only modifiers can be used. |
︙ | ︙ | |||
176 177 178 179 180 181 182 183 184 185 186 187 188 | $this->registerModifier(is_int($key) ? $name : $key, is_int($key) ? [Modifiers::class, $name] : $name); } // Local functions foreach (Functions::FUNCTIONS_LIST as $name) { $this->registerFunction($name, [Functions::class, $name]); } // Local sections foreach (Sections::SECTIONS_LIST as $name) { $this->registerSection($name, [Sections::class, $name]); } | > > > > > | < < < < < | < < < < < < < < | 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | $this->registerModifier(is_int($key) ? $name : $key, is_int($key) ? [Modifiers::class, $name] : $name); } // Local functions foreach (Functions::FUNCTIONS_LIST as $name) { $this->registerFunction($name, [Functions::class, $name]); } foreach (Functions::COMPILE_FUNCTIONS_LIST as $name => $callback) { $this->registerCompileBlock($name, $callback); } // Local sections foreach (Sections::SECTIONS_LIST as $name) { $this->registerSection($name, [Sections::class, $name]); } foreach (Sections::COMPILE_SECTIONS_LIST as $name => $callback) { $this->registerCompileBlock($name, $callback); } } public function setSource(string $path) { $this->file = null; $this->path = $path; $this->modified = filemtime($path); |
︙ | ︙ | |||
375 376 377 378 379 380 381 | if (!$is_web && $type != 'text/html' || !empty($this->_variables[0]['nocache'])) { $cache_as_uri = null; } if ($is_web && $type == 'text/html') { $scripts = []; | | | 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 | if (!$is_web && $type != 'text/html' || !empty($this->_variables[0]['nocache'])) { $cache_as_uri = null; } if ($is_web && $type == 'text/html') { $scripts = []; Plugins::fireSignal('usertemplate.appendscript', ['template' => $this, 'content' => $content], $scripts); if (count($scripts)) { $scripts = array_map(fn($a) => sprintf('<script type="text/javascript" defer src="%s"></script>', $a), $scripts); $scripts = implode("\n", $scripts); $content = str_ireplace('</body', $scripts . '</body', $content); } } |
︙ | ︙ |
Modified src/include/lib/Garradin/Users/Session.php from [67546be240] to [18019a2edb].
1 2 3 4 5 6 7 8 | <?php namespace Garradin\Users; use Garradin\Config; use Garradin\DB; use Garradin\Log; use Garradin\Utils; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin\Users; use Garradin\Config; use Garradin\DB; use Garradin\Log; use Garradin\Utils; use Garradin\Plugins; use Garradin\UserException; use Garradin\ValidationException; use Garradin\Users\Users; use Garradin\Email\Templates as EmailsTemplates; use Garradin\Files\Files; |
︙ | ︙ | |||
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | const SECTION_CONFIG = 'config'; const SECTION_SUBSCRIBE = 'subscribe'; const ACCESS_NONE = 0; const ACCESS_READ = 1; const ACCESS_WRITE = 2; const ACCESS_ADMIN = 9; // Personalisation de la config de UserSession protected $cookie_name = 'pko'; protected $remember_me_cookie_name = 'pkop'; protected $remember_me_expiry = '+3 months'; protected ?User $_user; protected ?array $_permissions; protected ?array $_files_permissions; | > > > > > > > > | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | const SECTION_CONFIG = 'config'; const SECTION_SUBSCRIBE = 'subscribe'; const ACCESS_NONE = 0; const ACCESS_READ = 1; const ACCESS_WRITE = 2; const ACCESS_ADMIN = 9; const ACCESS_WORDS = [ 'none' => self::ACCESS_NONE, 'read' => self::ACCESS_READ, 'write' => self::ACCESS_WRITE, 'admin' => self::ACCESS_ADMIN, ]; // Personalisation de la config de UserSession protected bool $non_locking = true; protected $cookie_name = 'pko'; protected $remember_me_cookie_name = 'pkop'; protected $remember_me_expiry = '+3 months'; protected ?User $_user; protected ?array $_permissions; protected ?array $_files_permissions; |
︙ | ︙ | |||
87 88 89 90 91 92 93 | $this->http = new \KD2\HTTP; } // Vérifier s'il n'y a pas un plugin qui gère déjà cet aspect // notamment en installation mutualisée c'est plus efficace $return = ['is_compromised' => null]; | | | 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | $this->http = new \KD2\HTTP; } // Vérifier s'il n'y a pas un plugin qui gère déjà cet aspect // notamment en installation mutualisée c'est plus efficace $return = ['is_compromised' => null]; if (Plugins::fireSignal('password.check', ['password' => $password], $return) && isset($return['is_compromised'])) { return (bool) $return['is_compromised']; } return parent::isPasswordCompromised($password); } protected function getUserForLogin($login) |
︙ | ︙ | |||
224 225 226 227 228 229 230 | elseif ($user = $this->getUserForLogin($login)) { Log::add(Log::LOGIN_FAIL, compact('user_agent'), $user->id); } else { Log::add(Log::LOGIN_FAIL, compact('user_agent')); } | | | 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | elseif ($user = $this->getUserForLogin($login)) { Log::add(Log::LOGIN_FAIL, compact('user_agent'), $user->id); } else { Log::add(Log::LOGIN_FAIL, compact('user_agent')); } Plugins::fireSignal('user.login', compact('login', 'password', 'remember_me', 'success')); // Clean up logs Log::clean(); return $success; } |
︙ | ︙ | |||
251 252 253 254 255 256 257 | // Mettre à jour la date de connexion $this->db->preparedQuery('UPDATE users SET date_login = datetime() WHERE id = ?;', [$user_id]); } else { Log::add(Log::LOGIN_FAIL, $details, $user_id); } | | | 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | // Mettre à jour la date de connexion $this->db->preparedQuery('UPDATE users SET date_login = datetime() WHERE id = ?;', [$user_id]); } else { Log::add(Log::LOGIN_FAIL, $details, $user_id); } Plugins::fireSignal('user.login.otp', compact('success', 'user_id')); return $success; } public function logout(bool $all = false) { $this->_user = null; |
︙ | ︙ | |||
405 406 407 408 409 410 411 412 413 414 415 416 417 418 | if (!$s->isLogged()) { return null; } return $s->getUser(); } public function getUser() { if (isset($this->_user)) { return $this->_user; } | > > > > > > > > > > > > > > | 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 | if (!$s->isLogged()) { return null; } return $s->getUser(); } /** * Returns cookie string for PDF printing */ static public function getCookie(): ?string { $i = self::getInstance(); if (!$i->isLogged()) { return null; } return sprintf('%s=%s', $i->cookie_name, $i->id()); } public function getUser() { if (isset($this->_user)) { return $this->_user; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Utils.php from [0a5c6bfd7e] to [61432880f7].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php namespace Garradin; use KD2\Security; use KD2\Form; use KD2\HTTP; use KD2\Translate; use KD2\SMTP; class Utils { static protected $collator; static protected $transliterator; const ICONS = [ 'up' => '↑', | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use KD2\Security; use KD2\Form; use KD2\HTTP; use KD2\Translate; use KD2\SMTP; use Garradin\Users\Session; class Utils { static protected $collator; static protected $transliterator; const ICONS = [ 'up' => '↑', |
︙ | ︙ | |||
172 173 174 175 176 177 178 179 180 181 182 183 184 185 | } $date = $ts->format($format); $date = strtr($date, self::FRENCH_DATE_NAMES); return $date; } /** * @deprecated */ static public function checkDate($str) { if (!preg_match('!^(\d{4})-(\d{2})-(\d{2})$!', $str, $match)) | > > > > > | 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | } $date = $ts->format($format); $date = strtr($date, self::FRENCH_DATE_NAMES); return $date; } static public function shortDate($ts, bool $with_hour = false): ?string { return self::date_fr($ts, 'd/m/Y' . ($with_hour ? ' à H\hi' : '')); } /** * @deprecated */ static public function checkDate($str) { if (!preg_match('!^(\d{4})-(\d{2})-(\d{2})$!', $str, $match)) |
︙ | ︙ | |||
957 958 959 960 961 962 963 | } // Check if string is already UTF-8 encoded or not if (preg_match('//u', $str)) { return $str; } | > > > > > | > > > > | > > > > > > > > > > > > > > > > > > > > > > | 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 | } // Check if string is already UTF-8 encoded or not if (preg_match('//u', $str)) { return $str; } return !preg_match('//u', $str) ? self::iso8859_1_to_utf8($str) : $str; } /** * Poly-fill to encode a ISO-8859-1 string to UTF-8 for PHP >= 9.0 * @see https://php.watch/versions/8.2/utf8_encode-utf8_decode-deprecated */ static public function iso8859_1_to_utf8(string $s): string { if (PHP_VERSION_ID < 90000) { return @utf8_encode($s); } $s .= $s; $len = strlen($s); for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) { switch (true) { case $s[$i] < "\x80": $s[$j] = $s[$i]; break; case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break; default: $s[$j] = "\xC3"; $s[++$j] = chr(ord($s[$i]) - 64); break; } } return substr($s, 0, $j); } /** * Transforms a unicode string to lowercase AND removes all diacritics * * @see https://www.matthecat.com/supprimer-les-accents-d-une-chaine-avec-php.html */ |
︙ | ︙ | |||
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 | } static public function knatcasesort(array $array) { uksort($array, [self::class, 'unicodeCaseComparison']); return $array; } /** * Displays a PDF from a string, only works when PDF_COMMAND constant is set to "prince" * @param string $str HTML string * @return void */ static public function streamPDF(string $str): void { if (!PDF_COMMAND) { // Try to see if there's a plugin $in = ['string' => $str]; | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | < < < < > | < < < < | < < < | < < < < | < < < | < < < < < < < > > > > > > > > > > | | 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 | } static public function knatcasesort(array $array) { uksort($array, [self::class, 'unicodeCaseComparison']); return $array; } static public function appendCookieToURLs(string $str): string { $cookie = Session::getCookie(); if (!$cookie) { return $str; } // Append session cookie to URLs, so that <img> tags and others work $r = preg_quote(WWW_URL, '!'); $r = '!(?<=["\'])(' . $r . '.*?)(?=["\'])!'; $str = preg_replace_callback($r, function ($match) use ($cookie): string { if (false !== strpos($match[1], '?')) { $separator = '&'; } else { $separator = '?'; } return $match[1] . $separator . $cookie; }, $str); return $str; } /** * Execute a system command with a timeout * @see https://blog.dubbelboer.com/2012/08/24/execute-with-timeout.html */ static public function exec(string $cmd, int $timeout, ?callable $stdin, ?callable $stdout, ?callable $stderr = null): int { if (!function_exists('proc_open') || !function_exists('proc_terminate') || preg_match('/proc_(?:open|terminate|get_status|close)/', ini_get('disable_functions'))) { throw new \RuntimeException('Execution of system commands is disabled.'); } $descriptorspec = [ 0 => ["pipe", "r"], // stdin is a pipe that the child will read from 1 => ["pipe", "w"], // stdout is a pipe that the child will write to 2 => ['pipe', 'w'], // stderr ]; $process = proc_open($cmd, $descriptorspec, $pipes); if (!is_resource($process)) { throw new \RuntimeException('Cannot execute command: ' . $cmd); } // $pipes now looks like this: // 0 => writeable handle connected to child stdin // 1 => readable handle connected to child stdout // Set to non-blocking stream_set_blocking($pipes[0], false); stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); $timeout_ms = $timeout * 1000000; // in microseconds if (null !== $stdin) { // Send STDIN fwrite($pipes[0], $stdin()); } fclose($pipes[0]); while ($timeout_ms > 0) { $start = microtime(true); // Wait until we have output or the timer expired. $read = [$pipes[1]]; $other = []; if (null !== $stderr) { $read[] = $pipes[2]; } // Wait every 0.5 seconds stream_select($read, $other, $other, 0, 500000); // Get the status of the process. // Do this before we read from the stream, // this way we can't lose the last bit of output if the process dies between these functions. $status = proc_get_status($process); // Read the contents from the buffer. // This function will always return immediately as the stream is none-blocking. $stdout(stream_get_contents($pipes[1])); if (null !== $stderr) { $stderr(stream_get_contents($pipes[2])); } if (!$status['running']) { // Break from this loop if the process exited before the timeout. break; } // Subtract the number of microseconds that we waited. $timeout_ms -= (microtime(true) - $start) * 1000000; } fclose($pipes[1]); fclose($pipes[2]); $status = proc_get_status($process); if ($status['running']) { proc_terminate($process, 9); throw new \RuntimeException(sprintf("Command killed after taking more than %d seconds: \n%s", $timeout, $cmd)); } $status = proc_get_status($process); proc_close($process); return $status['exitcode']; } /** * Displays a PDF from a string, only works when PDF_COMMAND constant is set to "prince" * @param string $str HTML string * @return void */ static public function streamPDF(string $str): void { if (!PDF_COMMAND) { return; } if (PDF_COMMAND == 'auto') { // Try to see if there's a plugin $in = ['string' => $str]; if (Plugins::fireSignal('pdf.stream', $in)) { return; } unset($in); } // Only Prince handles using STDIN and STDOUT if (PDF_COMMAND != 'prince') { $file = self::filePDF($str); readfile($file); unlink($file); return; } $str = self::appendCookieToURLs($str); // 3 seconds is plenty enough to fetch resources, right? $cmd = 'prince --http-timeout=3 -o - -'; // Prince is fast, right? Fingers crossed self::exec($cmd, 10, fn () => $str, fn ($data) => print($data)); if (PDF_USAGE_LOG) { file_put_contents(PDF_USAGE_LOG, date("Y-m-d H:i:s\n"), FILE_APPEND); } } /** * Creates a PDF file from a HTML string * @param string $str HTML string * @return string File path of the PDF file (temporary), you must delete or move it */ static public function filePDF(string $str): ?string { $cmd = PDF_COMMAND; if (!$cmd) { return null; } $source = sprintf('%s/print-%s.html', CACHE_ROOT, md5(random_bytes(16))); $target = str_replace('.html', '.pdf', $source); $str = self::appendCookieToURLs($str); file_put_contents($source, $str); if ($cmd == 'auto') { // Try to see if there's a plugin $in = ['source' => $source, 'target' => $target]; if (Plugins::fireSignal('pdf.create', $in)) { Utils::safe_unlink($source); return $target; } unset($in); // Try to find a local executable |
︙ | ︙ | |||
1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 | } // We still haven't found anything if (!$cmd) { throw new \LogicException('Aucun programme de création de PDF trouvé, merci d\'en installer un : https://fossil.kd2.org/garradin/wiki?name=Configuration'); } } switch ($cmd) { case 'prince': | > > > | | > < | > > > > > | > | | | 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 | } // We still haven't found anything if (!$cmd) { throw new \LogicException('Aucun programme de création de PDF trouvé, merci d\'en installer un : https://fossil.kd2.org/garradin/wiki?name=Configuration'); } } $timeout = 25; switch ($cmd) { case 'prince': $timeout = 10; $cmd = 'prince --http-timeout=3 -o %2$s %1$s'; break; case 'chromium': $cmd = 'chromium --headless --timeout=5000 --disable-gpu --run-all-compositor-stages-before-draw --print-to-pdf-no-header --print-to-pdf=%2$s %1$s'; break; case 'wkhtmltopdf': $cmd = 'wkhtmltopdf -q --print-media-type --enable-local-file-access --disable-smart-shrinking --encoding "UTF-8" %s %s'; break; case 'weasyprint': $cmd = 'weasyprint %1$s %2$s'; break; default: break; } $cmd = sprintf($cmd, escapeshellarg($source), escapeshellarg($target)); $cmd .= ' 2>&1'; $output = ''; try { self::exec($cmd, $timeout, null, fn ($data) => $output .= $data); } finally { Utils::safe_unlink($source); } if (!file_exists($target)) { throw new \RuntimeException('PDF command failed: ' . $output); } if (PDF_USAGE_LOG) { file_put_contents(PDF_USAGE_LOG, date("Y-m-d H:i:s\n"), FILE_APPEND); } return $target; } /** * Integer to A-Z, AA-ZZ, AAA-ZZZ, etc. |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Render/Markdown.php from [477f25b224] to [a1d7461fd1].
︙ | ︙ | |||
23 24 25 26 27 28 29 | $str = $content ?? $this->file->fetch(); $str = $parsedown->text($str); $str = CommonModifiers::typo($str); $str = preg_replace_callback(';<a href="((?!https?://|\w+:|#).+?)">;i', function ($matches) { | | | 23 24 25 26 27 28 29 30 31 32 33 34 35 | $str = $content ?? $this->file->fetch(); $str = $parsedown->text($str); $str = CommonModifiers::typo($str); $str = preg_replace_callback(';<a href="((?!https?://|\w+:|#).+?)">;i', function ($matches) { return sprintf('<a href="%s" target="_parent">', htmlspecialchars($this->resolveLink(htmlspecialchars_decode($matches[1])))); }, $str); return sprintf('<div class="web-content">%s</div>', $str); } } |
Modified src/include/lib/Garradin/Web/Render/Parsedown.php from [4ec75d240a] to [29e329fac8].
︙ | ︙ | |||
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | protected $skriv; protected $toc = []; function __construct(?File $file, ?string $user_prefix) { $this->BlockTypes['<'][] = 'SkrivExtension'; $this->BlockTypes['['][]= 'TOC'; # identify footnote definitions before reference definitions array_unshift($this->BlockTypes['['], 'Footnote'); # identify footnote markers before before links array_unshift($this->InlineTypes['['], 'FootnoteMarker'); $this->skriv = new Skriv($file, $user_prefix); } protected function blockSkrivExtension(array $line): ?array { $line = $line['text']; if (strpos($line, '<<') === 0 && preg_match('/^<<<?([a-z_]+)((?:(?!>>>?).)*?)(>>>?$|$)/i', trim($line), $match)) { $text = $this->skriv->callExtension($match); | > > > > > > > > > > > > > > > > > > > > > | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | protected $skriv; protected $toc = []; function __construct(?File $file, ?string $user_prefix) { $this->BlockTypes['<'][] = 'SkrivExtension'; $this->BlockTypes['['][]= 'TOC'; // Make Skriv extensions also available inline, before anything else array_unshift($this->InlineTypes['<'], 'SkrivExtension'); # identify footnote definitions before reference definitions array_unshift($this->BlockTypes['['], 'Footnote'); # identify footnote markers before before links array_unshift($this->InlineTypes['['], 'FootnoteMarker'); $this->skriv = new Skriv($file, $user_prefix); } protected function inlineSkrivExtension(array $str): ?array { if (preg_match('/<<<?([a-z_]+)((?:(?!>>>?).)*?)>>>?/i', $str['text'], $match)) { $text = $this->skriv->callExtension($match); return [ 'extent' => strlen($match[0]), 'element' => [ 'name' => 'div', 'rawHtml' => $text, 'allowRawHtmlInSafeMode' => true, ], ]; } return null; } protected function blockSkrivExtension(array $line): ?array { $line = $line['text']; if (strpos($line, '<<') === 0 && preg_match('/^<<<?([a-z_]+)((?:(?!>>>?).)*?)(>>>?$|$)/i', trim($line), $match)) { $text = $this->skriv->callExtension($match); |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Render/Skriv.php from [962e942dae] to [c9c1334af0].
1 2 3 4 5 6 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; use Garradin\Plugins; use Garradin\UserTemplate\CommonModifiers; use KD2\SkrivLite; use KD2\Garbage2xhtml; use const Garradin\{ADMIN_URL, WWW_URL}; |
︙ | ︙ | |||
24 25 26 27 28 29 30 | $this->skriv = new SkrivLite; $this->skriv->registerExtension('file', [$this, 'SkrivFile']); $this->skriv->registerExtension('fichier', [$this, 'SkrivFile']); $this->skriv->registerExtension('image', [$this, 'SkrivImage']); $this->skriv->registerExtension('html', [$this, 'SkrivHTML']); // Enregistrer d'autres extensions éventuellement | | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | $this->skriv = new SkrivLite; $this->skriv->registerExtension('file', [$this, 'SkrivFile']); $this->skriv->registerExtension('fichier', [$this, 'SkrivFile']); $this->skriv->registerExtension('image', [$this, 'SkrivImage']); $this->skriv->registerExtension('html', [$this, 'SkrivHTML']); // Enregistrer d'autres extensions éventuellement Plugins::fireSignal('skriv.init', ['skriv' => $this->skriv]); } public function render(?string $content = null): string { $skriv =& $this->skriv; $str = $content ?? $this->file->fetch(); $str = preg_replace_callback('/#file:\[([^\]\h]+)\]/', function ($match) { return $this->resolveAttachment($match[1]); }, $str); $str = $skriv->render($str); $str = CommonModifiers::typo($str); $str = preg_replace_callback(';<a href="((?!https?://|\w+:).+?)">;i', function ($matches) { return sprintf('<a href="%s" target="_parent">', htmlspecialchars($this->resolveLink(htmlspecialchars_decode($matches[1])))); }, $str); return sprintf('<div class="web-content">%s</div>', $str); } public function callExtension(array $match) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Router.php from [3220c288b9] to [fdc58e1a88].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Files\WebDAV\Server as WebDAV_Server; use Garradin\Web\Skeleton; use Garradin\Web\Web; use Garradin\API; use Garradin\Config; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php namespace Garradin\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Files\WebDAV\Server as WebDAV_Server; use Garradin\Web\Skeleton; use Garradin\Web\Web; use Garradin\API; use Garradin\Config; use Garradin\Plugins; use Garradin\UserException; use Garradin\Utils; use Garradin\Users\Session; use const Garradin\{WWW_URI, ADMIN_URL, ROOT, HTTP_LOG_FILE, ENABLE_XSENDFILE}; |
︙ | ︙ | |||
69 70 71 72 73 74 75 | if ($uri == 'feed/atom/') { Utils::redirect('/atom.xml'); } elseif ($uri == 'favicon.ico') { header('Location: ' . Config::getInstance()->fileURL('favicon'), true); return; } | > > > | | | | > | | | 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | if ($uri == 'feed/atom/') { Utils::redirect('/atom.xml'); } elseif ($uri == 'favicon.ico') { header('Location: ' . Config::getInstance()->fileURL('favicon'), true); return; } elseif (preg_match('!^(?:admin/p|p|m)/\w+$!', $uri)) { Utils::redirect('/' . $uri . '/'); } elseif (preg_match('!^(admin/p|p)/(' . Plugins::NAME_REGEXP . ')/(.*)$!', $uri, $match) && ($plugin = Plugins::get($match[2])) && $plugin->enabled) { $uri = ($match[1] == 'admin/p' ? 'admin/' : '') . $match[3]; $plugin->route($uri); return; } // Other admin/plugin routes are not found elseif ($first === 'admin' || $first === 'p') { http_response_code(404); throw new UserException('Cette page n\'existe pas.'); } elseif ('api' === $first) { API::dispatchURI(substr($uri, 4)); return; } elseif ((in_array($uri, self::DAV_ROUTES) || in_array($first, self::DAV_ROUTES)) && WebDAV_Server::route($uri)) { return; } |
︙ | ︙ | |||
106 107 108 109 110 111 112 | break; } } } $session = Session::getInstance(); | | | | 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | break; } } } $session = Session::getInstance(); if (Plugins::fireSignal('http.request.file.before', compact('file', 'uri', 'session'))) { // If a plugin handled the request, let's stop here return; } if ($size) { $file->serveThumbnail($session, $size); } else { $file->serve($session, isset($_GET['download']), $_GET['s'] ?? null, $_POST['p'] ?? null); } Plugins::fireSignal('http.request.file.after', compact('file', 'uri', 'session')); return; } Skeleton::route($uri); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Skeleton.php from [b64003a79f] to [bf267b1aff].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Entities\Web\Page; use Garradin\UserException; use Garradin\UserTemplate\Modules; use Garradin\UserTemplate\UserTemplate; use Garradin\Config; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Entities\Web\Page; use Garradin\UserException; use Garradin\UserTemplate\Modules; use Garradin\UserTemplate\UserTemplate; use Garradin\Config; use Garradin\Plugins; use Garradin\Utils; use KD2\Brindille_Exception; use KD2\DB\EntityManager as EM; use const Garradin\{ROOT, ADMIN_URL}; |
︙ | ︙ | |||
98 99 100 101 102 103 104 | } return null; } public function serve(string $uri, array $params = []): void { | | | 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | } return null; } public function serve(string $uri, array $params = []): void { if (Plugins::fireSignal('http.request.skeleton.before', $params)) { return; } $type = $this->type(); if (!$type) { throw new \InvalidArgumentException('Invalid skeleton type'); |
︙ | ︙ | |||
141 142 143 144 145 146 147 | else { Cache::link($uri, $this->defaultPath()); header(sprintf('Content-Type: %s;charset=utf-8', $type), true); readfile($this->defaultPath()); flush(); } | | | 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | else { Cache::link($uri, $this->defaultPath()); header(sprintf('Content-Type: %s;charset=utf-8', $type), true); readfile($this->defaultPath()); flush(); } Plugins::fireSignal('http.request.skeleton.after', $params); } public function file(): ?File { return Files::get(File::CONTEXT_SKELETON . '/' . $this->path); } |
︙ | ︙ |
Modified src/include/migrations/1.3/1.3.0.php from [0549342cfa] to [3c83dc2666].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; $db->beginSchemaUpdate(); // Get old keys $config = (object) $db->getAssoc('SELECT key, value FROM config WHERE key IN (\'champs_membres\', \'champ_identifiant\', \'champ_identite\');'); | | | > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; $db->beginSchemaUpdate(); // Get old keys $config = (object) $db->getAssoc('SELECT key, value FROM config WHERE key IN (\'champs_membres\', \'champ_identifiant\', \'champ_identite\');'); // Create config_users_fields table $db->exec(' CREATE TABLE IF NOT EXISTS config_users_fields ( id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, sort_order INTEGER NOT NULL, type TEXT NOT NULL, label TEXT NOT NULL, help TEXT NULL, required INTEGER NOT NULL DEFAULT 0, read_access INTEGER NOT NULL DEFAULT 0, write_access INTEGER NOT NULL DEFAULT 1, list_table INTEGER NOT NULL DEFAULT 0, options TEXT NULL, default_value TEXT NULL, sql TEXT NULL, system TEXT NULL );'); // Migrate users table $df = \Garradin\Users\DynamicFields::fromOldINI($config->champs_membres, $config->champ_identifiant, $config->champ_identite, 'numero'); $df->save(false); // Migrate other stuff $db->import(ROOT . '/include/migrations/1.3/1.3.0.sql'); |
︙ | ︙ |
Modified src/include/migrations/1.3/1.3.0.sql from [31fbb67a96] to [d138e6e6bc].
|
| < < < | 1 2 3 4 5 6 7 | -- The new users table has already been created and copied ALTER TABLE plugins RENAME TO plugins_old; ALTER TABLE plugins_signaux RENAME TO plugins_signaux_old; -- References old membres table ALTER TABLE services_users RENAME TO services_users_old; -- Also take id_fee into account for unique key ALTER TABLE services_reminders_sent RENAME TO services_reminders_sent_old; |
︙ | ︙ | |||
47 48 49 50 51 52 53 | INSERT INTO acc_transactions_users SELECT * FROM acc_transactions_users_old; DROP TABLE services_reminders_sent_old; DROP TABLE acc_transactions_users_old; DROP TABLE acc_transactions_old; DROP TABLE services_users_old; | > > > > > > | > > > | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | INSERT INTO acc_transactions_users SELECT * FROM acc_transactions_users_old; DROP TABLE services_reminders_sent_old; DROP TABLE acc_transactions_users_old; DROP TABLE acc_transactions_old; DROP TABLE services_users_old; -- Remove old plugin as it cannot be uninstalled as it no longer exists DELETE FROM plugins_old WHERE id = 'ouvertures'; DELETE FROM plugins_signaux_old WHERE plugin = 'ouvertures'; -- Rename plugins table columns to English INSERT INTO plugins (name, label, description, author, author_url, version, config, enabled, menu, restrict_level, restrict_section) SELECT id, nom, description, auteur, url, version, config, 1, menu, CASE WHEN menu_condition IS NOT NULL THEN 2 ELSE NULL END, CASE WHEN menu_condition IS NOT NULL THEN 'users' ELSE NULL END FROM plugins_old; INSERT INTO plugins_signals SELECT * FROM plugins_signaux_old; DROP TABLE plugins_signaux_old; DROP TABLE plugins_old; INSERT INTO searches SELECT * FROM recherches; UPDATE searches SET target = 'accounting' WHERE target = 'compta'; |
︙ | ︙ |
Modified src/include/migrations/1.3/schema.sql from [4232e4cc2b] to [a912bda722].
︙ | ︙ | |||
25 26 27 28 29 30 31 | system TEXT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS config_users_fields_name ON config_users_fields (name); CREATE TABLE IF NOT EXISTS plugins ( | | < > | > > > > | > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | system TEXT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS config_users_fields_name ON config_users_fields (name); CREATE TABLE IF NOT EXISTS plugins ( id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, label TEXT NOT NULL, description TEXT NULL, author TEXT NULL, author_url TEXT NULL, version TEXT NOT NULL, menu INT NOT NULL DEFAULT 0, home_button INT NOT NULL DEFAULT 0, restrict_section TEXT NULL, restrict_level INT NULL, config TEXT NULL, enabled INTEGER NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS plugins_name ON plugins (name); CREATE TABLE IF NOT EXISTS plugins_signals -- Link between plugins and signals ( signal TEXT NOT NULL, plugin TEXT NOT NULL REFERENCES plugins (name), callback TEXT NOT NULL, PRIMARY KEY (signal, plugin) ); CREATE TABLE IF NOT EXISTS modules -- List of modules ( id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, label TEXT NOT NULL, description TEXT NULL, author TEXT NULL, author_url TEXT NULL, menu INT NOT NULL DEFAULT 0, home_button INT NOT NULL DEFAULT 0, restrict_section TEXT NULL, restrict_level INT NULL, config TEXT NULL, enabled INTEGER NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS modules_name ON modules (name); CREATE TABLE IF NOT EXISTS modules_templates -- List of forms special templates ( id INTEGER NOT NULL PRIMARY KEY, id_module INTEGER NOT NULL REFERENCES modules (id) ON DELETE CASCADE, name TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS modules_templates_name ON modules_templates (id_module, name); CREATE TABLE IF NOT EXISTS api_credentials ( id INTEGER NOT NULL PRIMARY KEY, label TEXT NOT NULL, key TEXT NOT NULL, secret TEXT NOT NULL, created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, |
︙ | ︙ | |||
487 488 489 490 491 492 493 | CREATE UNIQUE INDEX IF NOT EXISTS web_pages_path ON web_pages (path); CREATE UNIQUE INDEX IF NOT EXISTS web_pages_uri ON web_pages (uri); CREATE UNIQUE INDEX IF NOT EXISTS web_pages_file_path ON web_pages (file_path); CREATE INDEX IF NOT EXISTS web_pages_parent ON web_pages (parent); CREATE INDEX IF NOT EXISTS web_pages_published ON web_pages (published); CREATE INDEX IF NOT EXISTS web_pages_title ON web_pages (title); | < < < < < < < < < < < < < < < < < < < < < < < | 523 524 525 526 527 528 529 | CREATE UNIQUE INDEX IF NOT EXISTS web_pages_path ON web_pages (path); CREATE UNIQUE INDEX IF NOT EXISTS web_pages_uri ON web_pages (uri); CREATE UNIQUE INDEX IF NOT EXISTS web_pages_file_path ON web_pages (file_path); CREATE INDEX IF NOT EXISTS web_pages_parent ON web_pages (parent); CREATE INDEX IF NOT EXISTS web_pages_published ON web_pages (published); CREATE INDEX IF NOT EXISTS web_pages_title ON web_pages (title); |
Modified src/pubkey.asc from [7eb815c6b8] to [e8974cff9c].
1 2 | -----BEGIN PGP PUBLIC KEY BLOCK----- | | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGPbkSYBEADJ6b2bY8Uva2JSdt/fsjhY0ZD/BEeD9ersNK1OOcZVyqI9Z+hR J2BIvbEyCiPxmG2+A7/KVCOJrrgt2dmw1soXS4ePepYoiq36Oqjp/Nn2kVMOF6e1 XeS+O/KVIfLMLZOFItlGFsMOdZRLMzjbI3aUTsKPuIpdJ6A5NEodBZvfGHdXzV03 sGEw2T9uPi3AqR+ioKxyDmTvcAWiI9NsZXDhN0mRg/IQoAs9pkHhkbUV1BmxNOFi uuaDbptH3jdYhiQb1E7BJJU5CNhr9Zv1F7PD0Gr4Q5OgoDnk/MZKv4MVsl+zP444 bHbuiAzfWkm7QbaM+NF+NCcb7ujULJH/Ujgbascatc5dNDF51cA4BDZekjeOUI7Z DIsg1UtkB8d3VRKlw+J0Lt9ZyH7zwKB7Jzk6Gbn1/YSBnVWq0SZmomqiUVeBWBv7 gogFsbUkD35mafdBVdkRV4Yce9nrmDwog+5d7jriOKbYQ0MmSwcBeHbHnEGt72kx Eov8YlzssdqNDTLZUFix5I0LZHAaNT8LmjvkVuyz18J8EDq+x150e4ThP4orhAkB c/Of3B8OmYlG0fgM3zeawbvE16gnQ1InH2AGNLxBghizjSgHRDOo5gzUDWjXlGN3 /z2/7+yLSiUEUskz4gCVLuOFhXrCRyGcSpUyBA/nGlfJ/tC3HQTyqHDFrwARAQAB tBlQYWhla28gPGRldkBwYWhla28uY2xvdWQ+iQJOBBMBCgA4FiEE2gznv2g1PQbF cEHuJiyrIeahkikFAmPbkSYCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ JiyrIeahkikITA/+Lt+0Qwu7qIrvJ7Pgt5EgC3QGGSwlNl9lGErSuOQ0jpNExwyB QhQRHA8lnidrFlRX5FMg6IeyEoyn7NXjY7dzA5lkeNn5mWePl4pkLPz7XXS7Pwqd gyr5pc7Y9BpTjPsp2izZr5sbBlzYdFDXaMo69DdZHLILTI0LK0qALfBDcqQn4e/Y /82m5nBqsmoGYup2HnTctBjgAamjhJMyGQ/JXDjBc6PV2O+Ew7n9kp8dwbtY4UpM Oy8Gs4dzk4Ra/t6O4Zo0MporLZ6zulyWRVgvU4dVbtJFNJEKa4DXMxUOklilFX// uu665gEAS+F6f/Cb6OFHM7IxdLp4ng6z4A+jVxj4vXDgq5CP1F/L8EUqtl2kcXdU IQuzba7s8pVDM8ujLwR/s8VbgG1w+xYbZtqkEqpoAIIezvXmx2gnnuCAakw+JaP9 QSHmmD/5MW/GVXPw3L2+cgmI5axmkPP02A9BSlMcdV82Od5s2Nnat2AAkqPbjK3u /TTFRAumSTv51ezBTRldSxgYHEoWQBdvv7i+SnZniffYdvxTKklIhK2xuXY1Bzku 5Dk9SR0oQ1DvQA+TFK5XFhaWl1iuuzoTD4TW6r5NLqEHRUdxHpe8QTAkQi4hhKyb 7nY2IgjREqnrLvdfm9KSWjrV26Fe04vcRSYbzcvr6EdDwGmT7PNbOx9gt+a5Ag0E Y9uRJgEQAKNek9274WllUatGVThIjZvpYtpu5Q53TEZozYTRp4WLJDNtH5W7D0vK 8icr9e7CUILIQKhL6peugJXLj1HXok2vuK0ITWLCseApuXyvdzX8+5Z3QC1gzC6z AjA0r7Kc6cPg+nUL7SHSKuTY1+LdadC4AYYTFN9hq+QcEBtx+dunTxdjjsyu4aJc h2g8565mRiccRY1LSgYpmM8Blypj+WjJ1KF7v+JNhvfiAoadPnVvUODMq1pdHW4E ucIP7Supw+qIVlRmaK+STDPEDU8diy7YMiC+dvxJLV1Yl91qLx9EK5RcvkcUUlvD w3ESotzEJSiRF1Rdv1bUAtIVi7ZJGYfqr2PFOnGLWJRWYxwFXpv9ADk1Onnu33T2 tybsVUN+zVHdo3q7vDbSYXbaeeFg4voaJuR9kn3nDKS5Lidh2HQiUWphVz0vScRD a//nEYwwsu93aTPK/gW5BxAp/LLEjQgjXSH3DK3FNksy3gyf5zdg8WqzPqWocXGR P6UqChW1ujXQ8dNDRH8arqz5q0kgoFgsY90Hf4aIB0HmCdlsPK8BtTMQUS4oZLNA G6l/52PnOvkhodYzvWShS4e8Uo6GH8b/mmo0Z7qzlkpuBScYDx02PirHOqrto842 xUvlk252n4ZCc5zNLa1Zv93afpt+1abihki/i4nPherUtGkoCby5ABEBAAGJAjYE GAEKACAWIQTaDOe/aDU9BsVwQe4mLKsh5qGSKQUCY9uRJgIbDAAKCRAmLKsh5qGS KZUPD/9CPZzOvWQCuJlPiDbENZRbLMudShseDlfddkDmAL/9mTprc7j0WWxnMVgC mObW6t/wiOP4ARw5/KCr3xrZ/O7aO7Fn98WxyTYdEr0gmE8m+nlalHuIDnfktp9O qyCZ2qjQaGKY0fJLUkyDCJRHa4jOST56LdpH/FxjAcJcP1MTJssy0LxgB2e+FUGy JdT3+4jUMBO1NBiM84LaV47tygEYdVbO0KP/uRHK3cKGLhGwMT/LdOLc2nmxfoty ZkM5nP0gxVizQOrXlDOEqRiZ/GyG8TZD91URzReZ8ssALbD+HQiuCllvodxqWiW8 ZsWi0/6Ht5mb1t4m1+Wy+Gukdh/a6/n/W4/ajWIOpUxN71e0wd+jqEll424DrLD6 t/M2rtz6BKHum2rSlIu9UOHDXmXKjEpz2XJP8kJsJc5AdOQS9gLn3aiNoyLgDAuf RlSHkj9nx7XmxNR7m7UKap7Glp9RrHo4PuCczQYbgAWfoeoESBq6bqGS/4vl5c8E FYB7uQ5YA71prEdDmuDsEq0tI7RkM68d4KqLM4Ag3OWVIDpDGhTtjizCe908o7zl uhpLMHCE963KKx+zL7ktThmpHz8V8/sqfbDylmSC2suaJhJCi+i5RakThDdw5DaS 5yJfKGPwOjx8m2A6POwV4CGnhAcE9VXzlUCP2XU73JVcMJ3QuA== =4SMJ -----END PGP PUBLIC KEY BLOCK----- |
Name change from src/pubkey.asc to src/pubkey_old.asc.
︙ | ︙ |
Added src/pubkey_signed.asc version [2005d5e930].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | -----BEGIN PGP MESSAGE----- owF9V2vsNNVZBwoVJ9BStAhYIhWKNhOY3dmZnRmBNnO/7s59d2exkLnszs59Zmdn dmcsGCG0XIoQwEiVUIFg2pRXaANN+9JWWzCNRai2sSi9KGjRIFVSUxoKFv9Fbfzk +XbOk+fDye/yPL/bTn/LCcCJt//Fs9Vnb3gROfHYaZQHlI2XrLpL3Nr3v3X7RRf/ 9FAsL07P13jtfM2mFJE+X2ad8ylFpeU3ywCQ6eKU4jUvMR2KJRlp7MGeg9utC0tm sIPWdbxxBksGotgVQ6y29VQeqqq/nHWVSCzBjQFIMCW2HtvRkXbIeBgkMUie0aq0 3YY7OMj2w7pYmMhKW5VOEVWjsVrFJTTN4WQ2UbnxaggsViaoHvWIa2WiLFVO3KU8 V0/UYGkokz72xJFrW7WsNWIZSGMSnbJFQC3bNS8Ei342GAE1z+5hi2i0aERWBhgV 8qFjMqv1yXkkEtN6uWA200FmhJCoF2RNlImwSTx7NqSyw1TlIqBpXMYrd8IoDpxN pHtDFqMkyUbp6WZLLNshh2nMgN8iOqqGBZMn0GQpt8hkVqdgryEIAniC10Rkv54n GaZ77gSccuCU9j2siW1FEiA7Dr0jVNydjwZThkOHPolQzHKVxCvVFrElwIh1OLR3 CYUHo5khp3tQGig7YtkJWL+XKUzqkzHv5UPIMal8Nq8G5jIrsiqyZytqTrUYEBYh V3t2wozQzF0H1CxIjBni+Csi32bMvghBNMDibaTKnqMPJpm596mV4Ak5y+8wODkA bNHiTtrXdVBNGUtZ2lx0QMWBshRId2rhSha3yazp+iEu4SxTgYchOlgh1kZDiu2G TCjAh9T1iMLVzEn5wTqcjPqVu/dadjgOc30o5gJM8lPlQIWbqI/NUDAYtUDD3mbm 8SLlpyMA6mEIAzvFjGzWrpMeCemZ0qjcZrGljY73zdLuKBLK+XQtQTt6JOhWVwkM t92TBqmTFLCjUt2Zb1IXxkONN9Igofb/c28c+NAGcx2MdEmlqAlFhyTCRSwLh33e wuFQ0z0O8FmhkaJuK67cTRIlHJm9qQua3084ep+I1F7kOb2QaLKmKcqhyT1LCyFL cwhJ0mGiA/+nWbRICFR24EDfN1glblsJ08Idyob0SOd5c59OUyLl2a3ZqPogLqfs Yd9RgL7RDYHE0zwKtlxqLFBuEo7FVccWXY5NF7GDBT2JpslqmqPZfKWlSJkoWo8t Fiam7asACLstWvqYQ1ClFWt1CUf9covWHpX2TsAxC3dSjAkmWAqKqFhH8MqDilTW FONXeo6sIAeAcDhDc6qqs4J3mhIWcsvfUXFIulm8kSYdr0PSgokpf6zNYBVk91hO JCUe7L2dg9jlBFA7nK+RoE8Qw4V2YxVZFoNJWWyV5bhv0m5uzMLWRoKZt5O4qcTK LsIsJgdbTdIo5RYQdKTG8RgNWdIEufEaor2xygkTTDwESonk4bhHSDCeHWKkXTBh hdLakIMUnLWrXQon/iKwAVFves/FarycMRO8iZW9AdX4zAv54R48ON5yVyVsVRak KK76dpEd4DDPG5p0kyPZuRoB6KaQZQyETuYQP1to+5ECg36Yiah7yBJNG8AkQZnp xA9mOKwGaA1Pc3cHk2RSaV4sjxoAsizOIJvMtFp0uOop64iM5iF0BLaY61TQtlgE mvkyj9ZrJ2gPlpyk4kaGD83CGVJ90gAokxCmMSj0IdPqJGhxMrrgNu48HUZN0xcW g1jz8RadKhUrGHZwEMoVrltkokfIZiN3HoDlDiyGscFW+VZpg3VGyOY83s7gMbca IK1vmI7X++12zAbMns8sTJt66oEId6CLkuGABRyiMaSQ1Ul5ukoIGEPmaWq7O35m bcR42ZbOrmxQHR1Z7LLoHcsokbkiMdOdgM4xZtDKAB75W2KF0baoiLq8Ucblqgml hRIPhUWRwG0jD0RrrtD1iiybRdcG/QIH0eVIp4dhT497gIzJwRaT/bGvhWBuK5gp mHJjOUNQCdyARkjHsbgpsalA3Wep3QEMmtw6BHFcdw3iSj6wgUMcHaOZEfm+4QwV M3TKbIJTaVfG4DyWhjKHtaA03bTriCzcQMtnra0yk2pYBsIcYYHGFzXMbMo9WImz 1MhcGTQtRmMZGw+iDnMmEQ0ewScps6GTEsNKORCsjBp+m/i2nbYMsB+xZrHrWcmM DG5oBO3Qs8mdOIuwpcQ762oLa5ya88pcMubOYc8typYgmWSoHvFxNLJgYNd59cye gv1MCIpRhbWMZzoLz12tuBBpC1dqDCLJRzkjm6gSBRtY0CN7Xm5m/aA1fYMBXAjK WWe/rxti5FqaDIVzlDqQJaQobKyH8cIURow84qZJ3Y3Cbo32QYjPq16r5oW/4A1A G9sVvZkPm3ih40fDyxBwd1v1aDVIwoILa4cYCGvEFamBkNFBWmsyTu2siW6bSLFU piTAj1MIhbVcbZNNETh9Ozc3R/sAbhdjXsA9KMuKwRKr+jQpG8r0HeYwgLVoK6jV dlfgCAwc7DZNYBTOkSXto/1UcYfL9ug363IHDl0vOnJbKEJybbPa2js+KWivQ0mK pUiSl8jYYQGeZGWSJueibrmMuoJcxiaoerbXV0imyPUGrXhT1m36vykvegxJyrRB /qwGyEtbYyCC1pa92s51upFSLWI8dro0PGXSBOamXjHpOggSJiMViMiscutj8WA+ P+STWUgDmerNxztoH6kaQhp7FJLp7eiwXUIq5qoYlxP4/NBZTsBuB2HG4hmYp24q NCKTr5NdSahA1dFLuIp1l5edwVpS7KRjaMkQXCRWTQsdK0EpQNwhJn3J14YTS6rr bqAcQgpegZzNd4AUWCMQie0JpQ6nVDTBEcWdIdiuC1nnyIvVgaxBjSHII1/mlc3R wLMgJVAVH86zw7rYdcAymaC5NggPs6jX1e0iZVS2MqIlxHc8bi0ZYmgbvbFa4nVN Kh4DHhGxodO0LYJDNY/mOLCs59EAGgs7NPOGOyQbgvMO5Jsk2EDuGMqhOQK58VxU S/swxYarwT4A44pNUwRGmK3CjIEdNIG3u35MyUKTwVszFRvCVgVmkS3kmC17eCFp eCLVko+SgaqbRKjkIzeaFp0SMmSzBozUFJKYyA/YkedPDSzDbNktMT4tCWMrFIjW 0H6vO15IztfFqmBNqhp7R/hDSJuiPs4CnENhjY46JDYst0fGmTVMzVaDnYgZyWSM B4hcKROEDEfqfCYyJcNvrF0c9fSKGOAF1qdAsymViUCzxHgkywewV7BkZ22yUujx GQ7V1dpjujQzabhuXGkj0REYoYabWBsm2KOMawJoJ61lXtur8QHPYHKsqfsZQvP5 hvRZYrboU5vW4IWNjaSZP5FGekNefjlwOWJOJODNfZ+dMv9PGLjxpNHJJ5wInHDe We8++YmPXtgdv+SmD1BfuvZnKeOUk36aK04Afv6M/315DXj7Gx98lH0r/k/PH9ug 3/rM185453s/YX3hbOWJ9Nar3/HImVf9SbKovg5AyHmXqc33f+8/XfuGiHroffY9 F5782IWv9+e8ev2lymnvOu9ff/N3vnn6n37kb8+485Zb7p9f+aR+4kUP9RcNHn75 K+2jV9/0/AUZgn3xS+2df/XpC5++m5Xe03/qh8eeqYnqhf2xd5966d3i333+qQvu jvQrz33xjx8/854P3Ptof9VnmWvf2D193Xc+f95Lp3/xbWdSLx37td/+3NmXG8de W99y7gC/49rv3faJm7Cbnvvuae+PHzn+0Puf++YrZ93c5b/7k/Gnf/SW1amZ1J39 qPNHffjh7Onn/n3/Q3WZX3ff8t5XH3rjiotd4rKvvTRZN5c/8Ad3xZ+77VRuoH33 y4+9+HXntf6Fuz4yfvbWx79z6cG97eLfuHTc32dd8soDXf8f9y+ue9/P3Rid8snk 96+7+dn7r1Ku8kpC+LdP/uUz7/ryY+/snr7vK9/4Begfbzj/xhP++eMPX/mDf7ke /BD7k2f6Pzzcds/bXn3g+Dkfa6/+s+df/sGvfGE/u+bEX/7V2+86xn3j+z9+9s7h V/e33/Tko9c8+ev7S/KTTj7+5/Vf/+LbH3no49d9G7/1sgcffOuPmcEr7We6Ry+T 30sj3M1fff34BWDV31o+/J7Tz33ihXMe/NA5w7855Spg8aMrtI8W+seg+B2fuvfG v3/gmtN+6x/UF+gr9NFVr+tnPN6j3149dR34vRueKl9If+ms48yZV9AfPC4un73j 5TuuJ/4L =apuZ -----END PGP MESSAGE----- |
Modified src/scripts/cron.php from [4119a18488] to [3b05c3796a].
︙ | ︙ | |||
19 20 21 22 23 24 25 | $s = new Sauvegarde; $s->auto(); } // Exécution des rappels automatiques Reminders::sendPending(); | | | 19 20 21 22 23 24 25 26 | $s = new Sauvegarde; $s->auto(); } // Exécution des rappels automatiques Reminders::sendPending(); Plugins::fireSignal('cron'); |
Modified src/skel-dist/modules/bilan_pc/icon.svg from [a7067a2e0b] to [c195a4c36b].
|
| | | 1 | <svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="img" stroke="none"><path d="m224 50h-192a6.00029 6.00029 0 0 0 -6 6v136a14.01572 14.01572 0 0 0 14 14h176a14.01572 14.01572 0 0 0 14-14v-136a6.00029 6.00029 0 0 0 -6-6zm-186 60h44v36h-44zm56 0h124v36h-124zm124-48v36h-180v-36zm-180 130v-34h44v36h-42a2.002 2.002 0 0 1 -2-2zm178 2h-122v-36h124v34a2.002 2.002 0 0 1 -2 2z"/></svg> |
Added src/skel-dist/modules/bilan_pc/module.ini version [8180b18867].
> > > > | 1 2 3 4 | name="Bilan expert" description="Bilan annuel selon le modèle du plan comptable des associations 2020" author="Paheko" author_url="https://paheko.cloud/" |
Deleted src/skel-dist/modules/bilan_pc/module.json version [8f119bbb23].
|
| < < < < |
Modified src/skel-dist/modules/carte_membre/carte.html from [1045e75f52] to [c7da92959a].
1 2 3 4 5 6 7 8 9 10 11 12 | {{#restrict block=true section="users" level="read"}} {{/restrict}} {{if !$_GET.id}} {{:admin_header title="Carte de membre" current="users"}} {{:error message="Aucun numéro de membre n'a été fourni"}} {{:admin_footer}} {{else}} {{#users id=$_GET.id}} | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {{#restrict block=true section="users" level="read"}} {{/restrict}} {{if !$_GET.id}} {{:admin_header title="Carte de membre" current="users"}} {{:error message="Aucun numéro de membre n'a été fourni"}} {{:admin_footer}} {{else}} {{#users id=$_GET.id}} {{:assign title="%s - Carte de membre"|args:$_name}} {{if $_GET.print == 'pdf'}} {{:http type="pdf" download="%s.pdf"|args:$title}} {{/if}} <!DOCTYPE html> <html> <head> |
︙ | ︙ | |||
49 50 51 52 53 54 55 | </style> </head> <body> <main> {{if $photo}} | | | | | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | </style> </head> <body> <main> {{if $photo}} <img src="{{$photo.0.url}}?150px" alt="" class="photo" /> {{else}} <img src="{{$config.files.logo}}?150px" alt="" class="logo" /> {{/if}} <h1>{{$_name}}</h1> <h2>N°{{$_number}}</h2> {{#subscriptions user=$id active=true}} <h3>{{$label}} valide jusqu'au {{$expiry_date|date_short}}</h3> {{/subscriptions}} <div style="clear:both"></div> {{:include file="modules/_footer.html"}} {{else}} {{:error message="Le numéro de membre fourni n'existe pas."}} {{/users}} {{/if}} |
Modified src/skel-dist/modules/carte_membre/icon.svg from [eb56079363] to [c561bd2bef].
|
| | | 1 | <svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="img" stroke="none"><g><path d="m15 3v10h-14v-10zm1-1h-16v12h16z"/><path d="m8 5h6v1h-6z"/><path d="m8 7h6v1h-6z"/><path d="m8 9h3v1h-3z"/><path d="m5.4 7h-.4v-.1c.6-.2 1-.8 1-1.4 0-.8-.7-1.5-1.5-1.5s-1.5.7-1.5 1.5c0 .7.4 1.2 1 1.4v.1h-.4c-.9 0-1.6.7-1.6 1.6v2.4h5v-2.4c0-.9-.7-1.6-1.6-1.6z"/></g></svg> |
Added src/skel-dist/modules/carte_membre/module.ini version [6e56d61037].
> > > > | 1 2 3 4 | name="Carte de membre" description="Impression de carte de membre, à l'unité, par planche de plusieurs membres, ou export en PDF." author="Paheko" author_url="https://paheko.cloud/" |
Deleted src/skel-dist/modules/carte_membre/module.json version [3b6ce77fe3].
|
| < < < < |
Modified src/skel-dist/modules/invoice/icon.svg from [20d1661429] to [d4539d8aff].
|
| | | 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" stroke-width="3" fill="none" width="100%" height="100%" id="img"><path d="M52.35,57.08H11.65v-50A.11.11,0,0,1,11.81,7l4.11,3.85a.11.11,0,0,0,.13,0L19.35,7a.09.09,0,0,1,.12,0l3.72,3.89a.11.11,0,0,0,.13,0L26.61,7a.09.09,0,0,1,.13,0l2.86,3.87a.1.1,0,0,0,.14,0L33,7a.09.09,0,0,1,.13,0l2.69,3.85a.1.1,0,0,0,.14,0L38.86,7A.1.1,0,0,1,39,7l2.85,3.85a.1.1,0,0,0,.14,0L44.7,7a.09.09,0,0,1,.15,0l2.25,3.84a.09.09,0,0,0,.13,0L52.2,7a.1.1,0,0,1,.15.09Z" stroke-linecap="round"/><line x1="19.42" y1="43.04" x2="46.02" y2="43.04" stroke-linecap="round"/><line x1="19.42" y1="49.29" x2="46.02" y2="49.29" stroke-linecap="round"/><path d="M40.21,34.51a9,9,0,1,1-5.48-16.15,8.86,8.86,0,0,1,3.78.83"/><line x1="21.22" y1="25.18" x2="36.21" y2="25.18"/><line x1="21.22" y1="29.76" x2="34.4" y2="29.76"/></svg> |
Modified src/skel-dist/modules/invoice/module.ini from [86ca7d801f] to [f6d67af1f5].
|
| < | | | > | < < < | 1 2 3 4 5 | name="Devis et factures" description="Permet de créer des devis et des factures, et de les imprimer" author="Paheko" author_url="https://paheko.cloud/" license="GNU AGPL v3" |
Modified src/skel-dist/modules/invoice/new_quotation.html from [ef5595bc77] to [0ef91b4f29].
︙ | ︙ | |||
15 16 17 18 19 20 21 | <h1>Création d'un devis</h1> {{if $_POST.quotation_submit}} {{:assign computed_total=0}} {{:assign errors=null}} {{:assign items=null}} {{#foreach from=$_POST.items key='index' item='item'}} | | | | | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <h1>Création d'un devis</h1> {{if $_POST.quotation_submit}} {{:assign computed_total=0}} {{:assign errors=null}} {{:assign items=null}} {{#foreach from=$_POST.items key='index' item='item'}} {{if $item.name === ''}}{{:assign var='errors.' value="Le nom de l'article est requis."}}{{/if}} {{if $item.unit_price < 0 || $item.unit_price != $item.unit_price|floatval|strtolower}} {{* Hack to check the data is a number *}} {{:assign var='errors.' value='Le prix saisi pour "%s" est invalide.'|args:$item.name}} {{/if}} {{if $item.quantity < 0 || $item.quantity != $item.quantity|floatval|strtolower}}{{:assign var='errors.' value='La quantité saisie pour "%s" est invalide.'|args:$item.name}}{{/if}} {{:assign var='computed_item' value=$item}} {{:assign var='computed_item.unit_price' value='%d * 100'|math:$item.unit_price|floatval}} {{:assign var='computed_item.quantity' value=$item.quantity|intval}} {{:assign var='items.' value=$computed_item}} {{:assign var='computed_total' value="%d + %d * %d"|args:$computed_total:$item.unit_price:$item.quantity|math}} {{/foreach}} {{if ($computed_total != $_POST.quotation_total) || ($computed_total < 0)}} {{:assign var='errors.' value='Erreur de calcul du total. Enregistrement du devis refusé.'}} {{/if}} {{if $errors|count}} {{#foreach from=$errors item='error'}} <p class="error block">{{$error}}</p> {{/foreach}} {{else}} |
︙ | ︙ |
Modified src/skel-dist/modules/ouvertures/config.html from [b92435bfba] to [8bce58e3d1].
1 2 3 4 5 6 7 8 9 10 11 | {{:admin_header title="Configuration des ouvertures"}} {{if $_POST.save}} {{#foreach from=$_POST.slots|array_transpose key="i" item="slot"}} {{:assign line="%d+1"|math:$i}} {{:assign var="slot" day=$slot.day frequency=$slot.frequency open='%02d:%02d'|args:$slot.open_hour:$slot.open_minutes close='%02d:%02d'|args:$slot.close_hour:$slot.close_minutes }} | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | {{:admin_header title="Configuration des ouvertures"}} {{if $_POST.save}} {{#foreach from=$_POST.slots|array_transpose key="i" item="slot"}} {{:assign line="%d+1"|math:$i}} {{:assign var="slot" day=$slot.day frequency=$slot.frequency open='%02d:%02d'|args:$slot.open_hour:$slot.open_minutes close='%02d:%02d'|args:$slot.close_hour:$slot.close_minutes }} {{:assign var="slots.%d"|args:$i value=$slot}} {{if !"%s %s"|args:$slot.frequency:$slot.day|trim|strtotime}} {{:assign error="Ouvertures - ligne %d : le sélecteur de jour est invalide: %s %s"|args:$line:$slot.frequency:$slot.day}} {{:break}} {{elseif !$slot.open|regexp_match:'/^(2[0-3]|[01][0-9]):([0-5][0-9])$/'}} {{:assign error="Ouvertures - ligne %d: heure d'ouverture invalide."|args:$line}} {{:break}} |
︙ | ︙ |
Modified src/skel-dist/modules/ouvertures/icon.svg from [a760737d4d] to [f809b11fbe].
|
| | | 1 | <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="img" stroke="none" ><path d="m13 2.03v.02 2c4.39.54 7.5 4.53 6.96 8.92-.46 3.64-3.32 6.53-6.96 6.96v2c5.5-.55 9.5-5.43 8.95-10.93-.45-4.75-4.22-8.5-8.95-8.97m-2 .03c-1.95.19-3.81.94-5.33 2.2l1.43 1.48c1.12-.9 2.47-1.48 3.9-1.68zm-6.74 3.61c-1.26 1.52-2.01 3.37-2.21 5.33h2c.19-1.42.75-2.77 1.64-3.9zm-2.2 7.33c.2 1.96.97 3.81 2.21 5.33l1.42-1.43c-.88-1.13-1.45-2.48-1.63-3.9zm5.04 5.37-1.43 1.37c1.51 1.26 3.37 2.05 5.33 2.26v-2c-1.42-.18-2.77-.75-3.9-1.63m5.4-11.37v5.25l4.5 2.67-.75 1.23-5.25-3.15v-6z"/></svg> |
Modified src/skel-dist/modules/ouvertures/module.ini from [80628e2296] to [f15df458dc].
|
| < | | < > > | 1 2 3 4 | name="Horaires d'ouverture" description="Permet d'afficher sur la page d'accueil les jours et horaires d'ouverture" author="Paheko" author_url="https://paheko.cloud/" |
Modified src/skel-dist/modules/recu_don/icon.svg from [dd01515720] to [adc409220d].
|
| | | 1 | <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="img" stroke="none" viewBox="0 0 24 24"><path d="M17.726 13.02L14 16H9v-1h4.065c.399 0 .638-.445.416-.777l-.888-1.332C12.223 12.334 11.599 12 10.93 12H9 3c-.553 0-1 .447-1 1v6c0 1.104.896 2 2 2h9.639c.865 0 1.688-.373 2.258-1.024L22 13l-1.452-.484C19.583 12.194 18.521 12.384 17.726 13.02zM19.258 7.39c.451-.465.73-1.108.73-1.818s-.279-1.353-.73-1.818C18.807 3.288 18.183 3 17.494 3c0 0-1.244-.003-2.494 1.286C13.75 2.997 12.506 3 12.506 3c-.689 0-1.313.288-1.764.753-.451.466-.73 1.108-.73 1.818s.279 1.354.73 1.818L15 12 19.258 7.39z"/></svg> |
Modified src/skel-dist/modules/recu_don/module.ini from [cd920ccb31] to [780f358803].
|
| < | | < > > | 1 2 3 4 | label="Reçu de don" description="Reçu de don simple, sans valeur fiscale" author="Paheko" author_url="https://paheko.cloud/" |
Deleted src/skel-dist/modules/recu_fiscal/snippets/home_button.html version [99f1ef488a].
|
| < < < |
Modified src/skel-dist/modules/recu_paiement/icon.svg from [c86a21c140] to [19540df506].
|
| | | 1 | <svg stroke="none" width="100%" height="100%" id="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="m21 16h2v2h-2z"/><path d="m9 16h8v2h-8z"/><path d="m21 12h2v2h-2z"/><path d="m9 12h8v2h-8z"/><path d="m9 8h14v2h-14z"/><path d="m25 2h-18a2.002 2.002 0 0 0 -2 2v25a1 1 0 0 0 1 1h1a.9987.9987 0 0 0 .8-.4l2.2-2.933 2.2 2.933a1.0353 1.0353 0 0 0 1.6 0l2.2-2.933 2.2 2.933a1.0353 1.0353 0 0 0 1.6 0l2.2-2.933 2.2 2.933a.9993.9993 0 0 0 .8.4h1a1 1 0 0 0 1-1v-25a2.0023 2.0023 0 0 0 -2-2zm0 25.333-2.2-2.933a1.0353 1.0353 0 0 0 -1.6 0l-2.2 2.933-2.2-2.933a1.0353 1.0353 0 0 0 -1.6 0l-2.2 2.933-2.2-2.933a1.0353 1.0353 0 0 0 -1.6 0l-2.2 2.933v-23.333h18z"/><path d="m0 0h32v32h-32z" fill="none"/></svg> |
Modified src/skel-dist/modules/recu_paiement/module.ini from [972c67d830] to [9fd092515c].
|
| < | | < > > | 1 2 3 4 | name="Reçu de paiement" description="Reçu de paiement, pour les écritures liées à un membre" author="Paheko" author_url="https://paheko.cloud/" |
Modified src/skel-dist/modules/recus_fiscaux/_config_default.tpl from [89b94cb59d] to [27055a1caf].
1 2 | {{if !$module.config}} {{* Valeurs par défaut *}} | | < < < < > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | {{if !$module.config}} {{* Valeurs par défaut *}} {{:assign var="module.config" objet_asso="" type_asso="" art200=false art238=false art978=false }} {{:assign var="module.config.comptes_don." value="754"}} {{:assign var="module.config.comptes_don_nature." value="75412"}} {{:assign var="module.config.comptes_especes." value="530"}} {{:assign var="module.config.comptes_cheques." value="5112"}} {{:assign var="module.config.champs_adresse" 0="adresse" 1="code_postal" 2="ville" }} {{/if}} |
Name change from src/skel-dist/modules/recu_fiscal/_recu.html to src/skel-dist/modules/recus_fiscaux/_recu.html.
︙ | ︙ |
Name change from src/skel-dist/modules/recu_fiscal/annuler.html to src/skel-dist/modules/recus_fiscaux/annuler.html.
︙ | ︙ |
Name change from src/skel-dist/modules/recu_fiscal/config.html to src/skel-dist/modules/recus_fiscaux/config.html.
︙ | ︙ |
Modified src/skel-dist/modules/recus_fiscaux/icon.svg from [0cc688f24c] to [9f7d289eef].
|
| | | 1 | <svg width="100%" height="100%" stroke="none" version="1.1" viewBox="0 0 67.73 67.73" xmlns="http://www.w3.org/2000/svg" id="img"><path transform="scale(.2646)" d="m186.1 13.22c-2.185 0.02461-4.384 0.169-6.592 0.4355-25.08 3.014-36.49 18.45-52.77 36.29-19.54-19.99-32.16-35.68-62.16-36.1-33.58-0.4489-62.77 24.48-61.45 63.38 1.89 55.53 59.24 100.4 94.4 136l29.23 29.58 35.37-36.89c12.97-13.53 31.49-31.38 48.22-49.59 15.25-16.59 29.03-33.49 35.8-47.71 22.67-47.57-15.62-95.86-60.04-95.36zm-48.95 61.32c7.899 0 14.76 1.435 20.59 4.303l-4.16 19.54c-3.997-3.997-9.828-5.996-17.49-5.996-7.664 1e-6 -13.87 2.751-18.62 8.252-2.68 3.103-4.584 6.982-5.713 11.64h38.79l-2.258 10.93h-37.94c-0.04702 1.081-0.07032 2.467-0.07032 4.16 0 1.646 0.04659 3.362 0.1406 5.148h35.9l-2.256 10.93h-32.16c1.175 5.031 2.987 8.981 5.432 11.85 4.702 5.548 10.81 8.322 18.34 8.322 9.028 0 16.22-2.774 21.58-8.322v21.58c-6.112 3.056-13.23 4.586-21.37 4.586-13.73 0-25.01-4.702-33.85-14.11-6.018-6.395-9.992-14.37-11.92-23.91h-10.44l2.258-10.93h6.91c-0.04702-1.128-0.07031-2.328-0.07031-3.598 0-2.163 0.04659-4.065 0.1406-5.711h-9.238l2.258-10.93h8.393c1.975-9.357 5.9-17.18 11.78-23.48 8.887-9.498 20.57-14.25 35.05-14.25z" stroke-width="2.084"/></svg> |
Modified src/skel-dist/modules/recus_fiscaux/index.html from [906e591f56] to [07a652d3c3].
︙ | ︙ | |||
8 9 10 11 12 13 14 | <nav class="tabs"> {{#restrict section="accounting" level="write"}} <aside> {{#restrict section="accounting" level="admin"}} {{:linkbutton href="config.html" label="Configuration" shape="settings" target="_dialog"}} {{/restrict}} {{*{{:linkbutton href="generer.html" label="Générer des reçus" shape="check"}}*}} | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <nav class="tabs"> {{#restrict section="accounting" level="write"}} <aside> {{#restrict section="accounting" level="admin"}} {{:linkbutton href="config.html" label="Configuration" shape="settings" target="_dialog"}} {{/restrict}} {{*{{:linkbutton href="generer.html" label="Générer des reçus" shape="check"}}*}} {{:linkbutton href="nouveau.html" label="Nouveau reçu" shape="plus"}} </aside> {{/restrict}} <ul> <li class="current"><a href="./">Liste des reçus</a></li> <li><a href="./recap.html">Récapitulatif annuel pour déclaration</a></li> </ul> </nav> <div class="shortForms"> <form method="get" action=""> <fieldset> <legend>Filtrer par année</legend> <p> {{:assign var="years." value="— Voir toutes les années —"}} {{#load select="SUBSTR($$.date, 1, 4) AS year" group="SUBSTR($$.date, 1, 4)"}} {{:assign var="years.%d"|args:$year value=$year}} {{/load}} {{:input type="select" name="year" options=$years default=$_GET.year onchange="this.form.submit();"}} </p> </fieldset> </form> |
︙ | ︙ |
Modified src/skel-dist/modules/recus_fiscaux/module.ini from [4a0b870aaa] to [d8663c68d8].
|
| < | | < > > > > > | 1 2 3 4 5 6 7 | name="Reçus fiscaux" description="Permet de générer des reçus fiscaux. Conforme aux exigences fiscales de 2022." author="Paheko" author_url="https://paheko.cloud/" home_button=true restrict_section="accounting" restrict_level="read" |
Modified src/skel-dist/modules/recus_fiscaux/nouveau.html from [f05920986a] to [b88823b0b8].
1 2 3 4 5 6 7 8 9 10 | {{#restrict block=true section="accounting" level="write"}} {{/restrict}} {{:include file="./_config_default.tpl" keep="module"}} {{if $_POST}} {{if !$_POST.date|trim|parse_date}} {{:assign error="Date d'émission invalide ou vide."}} {{elseif !$_POST.nom|trim}} {{:assign error="Le nom du donateur ne peut être laissé vide."}} {{elseif !$_POST.adresse|trim}} | > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {{#restrict block=true section="accounting" level="write"}} {{/restrict}} {{:include file="./_config_default.tpl" keep="module"}} {{if $_GET.user}} {{#foreach from=$_GET.user key="id" item="user"}} {{:assign id_user=$id|intval}} {{/foreach}} {{elseif $_GET.id_user}} {{:assign id_user=$_GET.id_user|intval}} {{/if}} {{if $_POST}} {{if !$_POST.date|trim|parse_date}} {{:assign error="Date d'émission invalide ou vide."}} {{elseif !$_POST.nom|trim}} {{:assign error="Le nom du donateur ne peut être laissé vide."}} {{elseif !$_POST.adresse|trim}} |
︙ | ︙ | |||
27 28 29 30 31 32 33 | linked_user=$_POST.id_user linked_transactions=$_POST.transactions annule=false recu=$recu }} {{:http redirect="voir.html?id=%d"|args:$new_id}} {{/if}} | > > > > | | > > > > > > > > > > > > | > > > > > > | < > > | < < < < | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | linked_user=$_POST.id_user linked_transactions=$_POST.transactions annule=false recu=$recu }} {{:http redirect="voir.html?id=%d"|args:$new_id}} {{/if}} {{/if}} {{:admin_header title="Créer un nouveau reçu fiscal" current="acc"}} {{if $id_user}} {{:assign var="champs_adresse" value=$module.config.champs_adresse|quote_sql_identifier|implode:" || ' — ' || "}} {{#users id=$id_user select="%s AS _adresse"|args:$champs_adresse}} {{:assign .="user"}} {{else}} {{:error message="Ce membre n'existe pas."}} {{/users}} {{* Récupération des comptes et soldes *}} {{#select SUM(l.credit) AS total, strftime('%Y', t.date) AS year, a.code AS account FROM acc_transactions t INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id INNER JOIN acc_transactions_users tu ON tu.id_transaction = t.id INNER JOIN acc_accounts a ON a.id = l.id_account AND a.!code_don WHERE tu.id_user = {$id_user} GROUP BY strftime('%Y', t.date), a.code; !code_don="code"|sql_where:"IN":$module.config.comptes_don }} {{:assign var="year_total" from="user_years.%d.total"|args:$year}} {{:assign .="user_years.%d"|args:$year}} {{:assign var="user_years.%d.total"|args:$year value="%d+%d"|math:$total:$year_total}} {{/select}} {{#foreach from=$user_years item="year"}} {{:assign total_money=$year.total|money_currency}} {{:assign var="user_select.%d"|args:$year.year value="%d (%s)"|args:$year.year:$total_money}} {{/foreach}} <script type="text/javascript"> var user_years = {{$user_years|json_encode}}; </script> {{elseif $_GET.id_transaction}} {{/if}} <nav class="tabs"> {{:linkbutton href="./" label="Retour à la liste des reçus" shape="left"}} </nav> {{if $error}} <p class="error block">{{$error}}</p> {{/if}} |
︙ | ︙ | |||
69 70 71 72 73 74 75 | {{:input type="radio-btn" name="type" value="transaction" label="Créer un reçu à partir d'une écriture"}} {{:input type="radio-btn" name="type" value="vierge" label="Créer un reçu vierge"}} </dl> </fieldset> <fieldset class="type-user hidden"> <legend>Reçu pour un membre</legend> <dl> | | | 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | {{:input type="radio-btn" name="type" value="transaction" label="Créer un reçu à partir d'une écriture"}} {{:input type="radio-btn" name="type" value="vierge" label="Créer un reçu vierge"}} </dl> </fieldset> <fieldset class="type-user hidden"> <legend>Reçu pour un membre</legend> <dl> {{:input type="list" multiple=false name="user" label="Sélectionner un membre" target="!users/selector.php" required=true}} </dl> </fieldset> <fieldset class="type-transaction hidden"> <legend>Reçu pour une écriture</legend> <dl> {{:input type="number" name="id_transaction" label="Indiquer le numéro de l'écriture" min=0 required=true}} </dl> |
︙ | ︙ | |||
97 98 99 100 101 102 103 | var type = $('[name="type"]:checked')[0].value; g.toggle('.type-user', type == 'user'); g.toggle('.type-transaction', type == 'transaction'); g.toggle('.submit', true); } </script> | < < | | | | | 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | var type = $('[name="type"]:checked')[0].value; g.toggle('.type-user', type == 'user'); g.toggle('.type-transaction', type == 'transaction'); g.toggle('.submit', true); } </script> {{else}} <form method="post" action=""> <fieldset> <legend>Nouveau reçu</legend> <dl> {{:input type="text" name="nom" label="Nom du bénéficiaire" required=true default=$user._name}} {{:input type="textarea" cols="50" rows="3" name="adresse" label="Adresse du bénéficiaire" required=true default=$user._adresse}} {{if $user_select}} {{:input type="select" name="annees" label="Pour quelle année le reçu doit-il être généré ?" required=true options=$user_select}} {{/if}} {{:input type="date" name="date" required=true label="Date du reçu" default=$now}} {{:input type="money" name="montant" required=true label="Montant des dons" default=$user.total}} <dt>Type de don</dt> {{:input type="radio" name="type" value="numeraire" label="Don en numéraire (en euros)" default=$user.}} {{:input type="radio" name="type" value="nature" label="Don en nature" help="Par exemple abandon de frais par les bénévoles"}} </dl> <dl class="hidden type-numeraire"> <dt>Moyens de paiement</dt> {{:input type="checkbox" name="moyens[especes]" value=1 label="Paiement en espèces"}} {{:input type="checkbox" name="moyens[cheques]" value=1 label="Paiement en chèques"}} {{:input type="checkbox" name="moyens[autres]" value=1 label="Paiement par virement, prélèvement, carte bancaire, ou autre"}} |
︙ | ︙ |
Name change from src/skel-dist/modules/recu_fiscal/previsualiser.html to src/skel-dist/modules/recus_fiscaux/previsualiser.html.
︙ | ︙ |
Name change from src/skel-dist/modules/recu_fiscal/recap.html to src/skel-dist/modules/recus_fiscaux/recap.html.
︙ | ︙ |
Name change from src/skel-dist/modules/recu_fiscal/recu.schema.json to src/skel-dist/modules/recus_fiscaux/recu.schema.json.
︙ | ︙ |
Name change from src/skel-dist/modules/recu_fiscal/snippets/transaction_details.html to src/skel-dist/modules/recus_fiscaux/snippets/transaction_details.html.
Name change from src/skel-dist/modules/recu_fiscal/voir.html to src/skel-dist/modules/recus_fiscaux/voir.html.
︙ | ︙ |
Modified src/skel-dist/modules/remise_cheques/config.html from [8f62fcf985] to [83861a729a].
︙ | ︙ | |||
19 20 21 22 23 24 25 | <form method="post" action=""> <fieldset> <legend>Configuration</legend> <dl> {{:input required=true name="accounts" type="text" label="Numéros de comptes liés aux remises de chèques" source=$module.config default="5112"}} <dd class="help"> | | | < | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <form method="post" action=""> <fieldset> <legend>Configuration</legend> <dl> {{:input required=true name="accounts" type="text" label="Numéros de comptes liés aux remises de chèques" source=$module.config default="5112"}} <dd class="help"> Pour chaque numéro de compte indiqué dans ce champ, le formulaire de remise de chèque sera proposé (en dessous de la fiche de l'écriture).<br /> Séparer les numéros de compte avec des virgules, par exemple : <tt>754, 756</tt>. </dd> </dl> </fieldset> <p class="submit"> {{:button type="submit" name="save" label="Enregistrer" shape="right" class="main"}} </p> </form> {{:admin_footer}} |
Modified src/skel-dist/modules/remise_cheques/icon.svg from [9fd2725ba3] to [79a2654d42].
|
| | | 1 | <svg width="100%" height="100%" id="img" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g><path d="m90.24 103.7h-90.24v-58.96h89.62v4h-85.62v50.96h86.24z"/><path d="m128 103.7h-25.5v-4h21.5v-50.96h-21.19v-4h25.19z"/><path d="m117.9 82.56h-6.966v-4h2.966v-7.414h-2.966v-4h6.966z"/><path d="m83.65 82.56h-73.59v-15.41h73.59v4h-69.59v7.414h69.59z"/><path d="m9.541 55.72h24.77v4h-24.77z"/><path d="m111 89.12h7.481v4h-7.481z"/><path d="m60.58 89.12h23.06v4h-23.06z"/><path d="m9.541 89.12h7.606v4h-7.606z"/><path d="m103.9 113h-4v-107h-7.256v107h-4v-111h15.26z"/><path d="m91.43 68.25h10.91v4h-10.91z"/><path d="m91.43 60.96h10.91v4h-10.91z"/><path d="m83.85 38.82h-4v-28.64h23.89v4h-19.89z"/><path d="m100.5 126.1h-8.525l-3.363-10.24v-4.713h15.26v4.713zm-5.629-4h2.734l2.262-6.951h-7.256v0.072z"/></g></svg> |
Modified src/skel-dist/modules/remise_cheques/module.ini from [a93b07bbd2] to [3f61fc17c1].
|
| < | | < > > > > > | 1 2 3 4 5 6 7 | name="Bordereau de remise de chèques" description="Permet d'imprimer un bordereau de remise de chèques à partir d'une écriture de dépôt." author="Paheko" author_url="https://paheko.cloud/" home_button=true restrict_section="accounting" restrict_level="read" |
Modified src/skel-dist/modules/remise_cheques/snippets/transaction_details.html from [7ec3e581b2] to [2b7a9269fc].
1 | {{if $module.config.accounts === null}} | | | 1 2 3 4 5 6 7 | {{if $module.config.accounts === null}} {{:assign var="module.config.accounts[]" value='5112'}} {{/if}} {{* FIXME: ne proposer le bordereau que pour les écritures de dépot *}} {{:include file="modules/recu_don/snippets/transaction_details.html"}} |
Modified src/templates/_head.tpl from [634abc5df3] to [0fa72dd81e].
︙ | ︙ | |||
33 34 35 36 37 38 39 | {/foreach} {/if} <link rel="stylesheet" type="text/css" href="{$admin_url}static/print.css?{$version_hash}" media="print" /> <link rel="stylesheet" type="text/css" href="{$admin_url}static/handheld.css?{$version_hash}" media="handheld,screen and (max-width:981px)" /> <link rel="manifest" href="{$admin_url}manifest.php" /> {if isset($config)} <link rel="icon" type="image/png" href="{$config->fileURL('favicon')}" /> | < > < | | < | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | {/foreach} {/if} <link rel="stylesheet" type="text/css" href="{$admin_url}static/print.css?{$version_hash}" media="print" /> <link rel="stylesheet" type="text/css" href="{$admin_url}static/handheld.css?{$version_hash}" media="handheld,screen and (max-width:981px)" /> <link rel="manifest" href="{$admin_url}manifest.php" /> {if isset($config)} <link rel="icon" type="image/png" href="{$config->fileURL('favicon')}" /> {/if} {custom_colors config=$config} </head> <body{if !empty($layout)} class="{$layout}"{/if}> {if !array_key_exists('_dialog', $_GET) && empty($layout)} <header class="header"> <nav class="menu"> <figure class="logo"> {if isset($config) && ($url = $config->fileURL('logo', '150px'))} <a href="{$admin_url}"><img src="{$url}" alt="" /></a> {/if} </figure> <ul> {if $is_logged} <?php $current_parent = substr($current, 0, strpos($current, '/')); ?> <li class="home{if $current == 'home'} current{elseif $current_parent == 'home'} current_parent{/if}"><h3><a href="{$admin_url}">{icon shape="home"}<b>Accueil</b></a></h3> {if !empty($plugins_menu)} |
︙ | ︙ |
Modified src/templates/acc/accounts/deposit.tpl from [db6e5c4575] to [5717951cb8].
1 2 3 4 5 6 7 8 9 | {include file="_head.tpl" title="Dépôt en banque : %s — %s"|args:$account.code,$account.label current="acc/accounts"} {form_errors} {if $missing_balance > 0} <p class="alert block"> Il existe une différence de {$missing_balance|raw|money_currency} entre la liste des écritures à déposer et le solde du compte.<br /> Cette situation est généralement dûe à des écritures de dépôt qui ont été supprimées.<br /> | | | < | | < < < < < < < < < < < | < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | {include file="_head.tpl" title="Dépôt en banque : %s — %s"|args:$account.code,$account.label current="acc/accounts"} {form_errors} {if $missing_balance > 0} <p class="alert block"> Il existe une différence de {$missing_balance|raw|money_currency} entre la liste des écritures à déposer et le solde du compte.<br /> Cette situation est généralement dûe à des écritures de dépôt qui ont été supprimées.<br /> {linkbutton shape="plus" label="Faire un virement pour régulariser" href="!acc/transactions/new.php?0=%d&l=Régularisation%%20dépôt&account=%d"|args:$missing_balance,$account.id} </p> {/if} {if !$journal->count()} <p class="alert block">Il n'y a aucune écriture qui nécessiterait un dépôt. </p> {else} <p class="help"> Cocher les cases correspondant aux montants à déposer, une nouvelle écriture sera générée. </p> <form method="post" action="{$self_url}" data-focus="1"> {include file="common/dynamic_list_head.tpl" check=true list=$journal} {foreach from=$journal->iterate() item="line"} <tr> <td class="check"> {input type="checkbox" name="deposit[%d]"|args:$line.id_line value="1" data-debit=$line.debit|abs data-credit=$line.credit default=$line.checked} </td> <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> <td>{$line.reference}</td> <td>{$line.line_reference}</td> <th>{$line.label}</th> <td class="money">{$line.debit|raw|money}</td> <td class="money">{if $line.running_sum > 0}-{/if}{$line.running_sum|abs|raw|money:false}</td> </tr> {/foreach} </tbody> </table> <fieldset> <legend>Détails de l'écriture de dépôt</legend> <dl> |
︙ | ︙ |
Modified src/templates/acc/accounts/index.tpl from [a9042469ec] to [90fb7ee1d9].
︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 18 19 20 21 22 | {if isset($_GET['chart_change'])} <p class="block error"> L'exercice sélectionné utilise un plan comptable différent, merci de sélectionner un autre compte. </p> {/if} {if !empty($grouped_accounts)} <table class="list"> <thead> <tr> <td></td> <td class="num">Numéro</td> | > > > > | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | {if isset($_GET['chart_change'])} <p class="block error"> L'exercice sélectionné utilise un plan comptable différent, merci de sélectionner un autre compte. </p> {/if} {if $pending_count} {include file="acc/transactions/_pending_message.tpl"} {/if} {if !empty($grouped_accounts)} <table class="list"> <thead> <tr> <td></td> <td class="num">Numéro</td> |
︙ | ︙ |
Modified src/templates/acc/accounts/simple.tpl from [b7a0a4a36b] to [c912366262].
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 | </aside> <ul> {foreach from=$types key="key" item="label"} <li{if $type == $key} class="current"{/if}><a href="?type={$key}">{$label}</a></li> {/foreach} </ul> </nav> {if !$list->count()} <p class="alert block"> Aucune écriture à afficher. </p> {else} <form method="post" action="{$admin_url}acc/transactions/actions.php"> | > > > > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | </aside> <ul> {foreach from=$types key="key" item="label"} <li{if $type == $key} class="current"{/if}><a href="?type={$key}">{$label}</a></li> {/foreach} </ul> </nav> {if $pending_count} {include file="acc/transactions/_pending_message.tpl"} {/if} {if !$list->count()} <p class="alert block"> Aucune écriture à afficher. </p> {else} <form method="post" action="{$admin_url}acc/transactions/actions.php"> |
︙ | ︙ |
Modified src/templates/acc/projects/index.tpl from [3427ad1e47] to [e53c25b3a2].
︙ | ︙ | |||
13 14 15 16 17 18 19 | {linkbutton href="?by_year=1" label="Grouper par exercice" shape="right"} {/if} </p> <p class="noprint print-btn"> <button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button> | > | > | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | {linkbutton href="?by_year=1" label="Grouper par exercice" shape="right"} {/if} </p> <p class="noprint print-btn"> <button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button> {if PDF_COMMAND} {linkbutton shape="download" href="%s?by_year=%d&_pdf"|args:$self_url_no_qs,$by_year label="Télécharger en PDF"} {/if} </p> {/if} </div> {if $projects_count} <table class="list projects"> <thead> <tr> <td>Projet</td> <td></td> <td class="money">Charges</td> <td class="money">Produits</td> <td class="money">Débits</td> <td class="money">Crédits</td> <td class="money">Solde</td> </tr> |
︙ | ︙ |
Modified src/templates/acc/reports/_header.tpl from [e558c75a8e] to [8250056d24].
︙ | ︙ | |||
67 68 69 70 71 72 73 74 75 76 77 78 79 | <input type="submit" value="Annuler" onclick="this.form.querySelectorAll('input:not([type=hidden]), select').forEach((a) => a.disabled = true); this.form.submit();" /> </p> </fieldset> </form> {/if} </div> {/if} <h2>{$config.org_name} — {$title}</h2> {if isset($project)} <h3>Projet : {if $project.code}{$project.code} — {/if}{$project.label}{if $project.archived} <em>(archivé)</em>{/if}</h3> {/if} {if isset($year)} | > > > > | | | | 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | <input type="submit" value="Annuler" onclick="this.form.querySelectorAll('input:not([type=hidden]), select').forEach((a) => a.disabled = true); this.form.submit();" /> </p> </fieldset> </form> {/if} </div> {/if} {if $config.files.logo} <figure class="logo print-only"><img src="{$config->fileURL('logo', '150px')}" alt="" /></figure> {/if} <h2>{$config.org_name} — {$title}</h2> {if isset($project)} <h3>Projet : {if $project.code}{$project.code} — {/if}{$project.label}{if $project.archived} <em>(archivé)</em>{/if}</h3> {/if} {if isset($year)} <p>Exercice : {$year.label} ({if $year.closed}clôturé{else}<strong>en cours</strong>{/if}) — du {$year.start_date|date_short} — au {$year.end_date|date_short}<br /> <em>Document généré le {$now|date_short}</em> </p> {/if} <p class="noprint print-btn"> <button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button> {if $current != 'graphs' && PDF_COMMAND} {linkbutton shape="download" href="%s&_pdf"|args:$self_url label="Télécharger en PDF"} {/if} </p> </div> {if !empty($allow_filter) && isset($year) && $criterias.before && $criterias.after} <p class="alert block"> Attention, seules les écritures du {$criterias.after|date_short} au {$criterias.before|date_short} sont prises en compte. </p> {/if} |
Added src/templates/acc/transactions/_pending_message.tpl version [9d2d58c5c3].
> > > > > > > > | 1 2 3 4 5 6 7 8 | <p class="alert block"> { {Il y a une dette ou créance à régler dans les exercices clos.} {Il y a %n dettes ou créances à régler dans les exercices clos.} n=$pending_count }<br /> {linkbutton href="!acc/transactions/pending.php" label="Voir les dettes et créances en attente" shape="menu"} </p> |
Modified src/templates/acc/transactions/details.tpl from [d7af5f3327] to [9f94121eb9].
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | {if !$transaction.hash && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$transaction_year->closed} {linkbutton href="edit.php?id=%d"|args:$transaction.id shape="edit" label="Modifier cette écriture"} {linkbutton href="delete.php?id=%d"|args:$transaction.id shape="delete" label="Supprimer cette écriture"} {/if} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} {linkbutton href="new.php?copy=%d"|args:$transaction.id shape="plus" label="Dupliquer cette écriture"} {/if} <aside> {if !$transaction.hash && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton href="lock.php?id=%d"|args:$transaction.id shape="lock" label="Verrouiller" target="_dialog"} {/if} {linkbutton href="?id=%d&_pdf"|args:$transaction.id shape="download" label="Télécharger en PDF"} </aside> </nav> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE) && $transaction.status & $transaction::STATUS_WAITING} <div class="block alert"> <form method="post" action="{$self_url}"> {if $transaction.type == $transaction::TYPE_DEBT} <h3>Dette en attente</h3> | > > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | {if !$transaction.hash && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$transaction_year->closed} {linkbutton href="edit.php?id=%d"|args:$transaction.id shape="edit" label="Modifier cette écriture"} {linkbutton href="delete.php?id=%d"|args:$transaction.id shape="delete" label="Supprimer cette écriture"} {/if} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} {linkbutton href="new.php?copy=%d"|args:$transaction.id shape="plus" label="Dupliquer cette écriture"} {/if} {if PDF_COMMAND} <aside> {if !$transaction.hash && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton href="lock.php?id=%d"|args:$transaction.id shape="lock" label="Verrouiller" target="_dialog"} {/if} {linkbutton href="?id=%d&_pdf"|args:$transaction.id shape="download" label="Télécharger en PDF"} </aside> {/if} </nav> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE) && $transaction.status & $transaction::STATUS_WAITING} <div class="block alert"> <form method="post" action="{$self_url}"> {if $transaction.type == $transaction::TYPE_DEBT} <h3>Dette en attente</h3> |
︙ | ︙ |
Added src/templates/acc/transactions/pending.tpl version [3033e884d5].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | {include file="admin/_head.tpl" title="Dettes et créances non réglées sur les exercices clos" current="acc/simple"} <nav class="tabs"> <aside> {exportmenu href="?export="} {linkbutton shape="search" href="!acc/search.php" label="Recherche"} </aside> </nav> {if !$list->count()} <p class="alert block"> Aucune écriture à afficher. </p> {else} {include file="common/dynamic_list_head.tpl"} {foreach from=$list->iterate() item="line"} <tr> <td>{$line.year_label}</td> <td>{$line.type_label}</td> <td class="num">{link href="!acc/transactions/details.php?id=%d"|args:$line.id label="#%d"|args:$line.id}</td> <td>{$line.date|date_short}</td> <td class="money">{$line.change|abs|raw|money}</td> <td>{$line.reference}</td> <th>{$line.label}</th> <td class="actions"> {if $line.type == Entities\Accounting\Transaction::TYPE_DEBT && ($line.status & Entities\Accounting\Transaction::STATUS_WAITING)} {linkbutton shape="check" label="Régler cette dette" href="!acc/transactions/payoff.php?for=%d"|args:$line.id} {elseif $line.type == Entities\Accounting\Transaction::TYPE_CREDIT && ($line.status & Entities\Accounting\Transaction::STATUS_WAITING)} {linkbutton shape="export" label="Régler cette créance" href="!acc/transactions/payoff.php?for=%d"|args:$line.id} {/if} {linkbutton href="!acc/transactions/details.php?id=%d"|args:$line.id label="Détails" shape="search"} </td> </tr> {/foreach} </tbody> </table> </form> {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/acc/years/balance.tpl from [3b8dda4a1a] to [52c16ded8e].
1 2 3 4 5 6 7 8 9 10 | {include file="_head.tpl" title="Balance d'ouverture" current="acc/years"} {form_errors} {if !empty($_GET.from) && empty($_POST)} <p class="block confirm"> L'exercice a bien été créé. </p> {/if} | > > > > > > > | | | | | | > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | {include file="_head.tpl" title="Balance d'ouverture" current="acc/years"} {form_errors} {if !empty($_GET.from) && empty($_POST)} <p class="block confirm"> L'exercice a bien été créé. </p> {/if} {if $year_selected} {if $has_balance} <p class="block alert"> <strong>Attention !</strong> Une balance d'ouverture existe déjà dans cet exercice.<br /> En validant ce formulaire, les écritures de balance et d'affectation du résultat qui existent <strong>seront supprimées et remplacées</strong>. </p> {elseif $year->countTransactions()} <p class="block alert"> <strong>Attention !</strong> Cet exercice a déjà des écritures, peut-être avez-vous déjà renseigné manuellement la balance d'ouverture ? </p> {/if} {/if} <form method="post" action="{$self_url}"> <fieldset> <legend>Exercice : « {$year.label} » du {$year.start_date|date_short} au {$year.end_date|date_short}</legend> {if !$year_selected} <dl> <dt><label for="f_from_year">Reporter les soldes de fermeture d'un exercice</label></dt> <dd class="help">Pour reprendre les soldes des comptes de l'exercice précédent.</dd> <dd> <select id="f_from_year" name="from_year"> <option value="">-- Aucun</option> {foreach from=$years item="year"} |
︙ | ︙ | |||
43 44 45 46 47 48 49 | let v = s.options[s.selectedIndex].dataset.closed; g.toggle('.warn-not-closed', v === '0' ? true : false); }; s.onchange = checkOpen; checkOpen(); </script> {/literal} | | | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | let v = s.options[s.selectedIndex].dataset.closed; g.toggle('.warn-not-closed', v === '0' ? true : false); }; s.onchange = checkOpen; checkOpen(); </script> {/literal} {else} <p class="help"> Renseigner ici les soldes d'ouverture (débiteur ou créditeur) des comptes. </p> {if !empty($_GET.from)} <p class="help"> Normalement il suffit de valider ce formulaire pour faire le report à nouveau des soldes de comptes. </p> |
︙ | ︙ |
Modified src/templates/acc/years/close.tpl from [6910fe6591] to [f4f3668820].
1 2 3 4 5 6 7 8 9 10 11 12 | {include file="_head.tpl" title="Clôturer un exercice" current="acc/years"} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Clôturer un exercice</legend> <h3 class="warning"> Êtes-vous sûr de vouloir clôturer l'exercice « {$year.label} » ? </h3> <p class="block alert"> | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {include file="_head.tpl" title="Clôturer un exercice" current="acc/years"} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Clôturer un exercice</legend> <h3 class="warning"> Êtes-vous sûr de vouloir clôturer l'exercice « {$year.label} » ? </h3> <p class="block alert"> <strong>Un exercice clôturé ne peut plus être modifié !</strong><br /> Il ne sera plus possible de modifier ou supprimer les écritures de l'exercice clôturé. </p> <dl> <dt>Début de l'exercice</dt> <dd>{$year.start_date|date_short}</dd> <dt>Fin de l'exercice</dt> <dd>{$year.end_date|date_short}</dd> |
︙ | ︙ |
Modified src/templates/acc/years/export.tpl from [0b6f6ce320] to [ca9b7dfdac].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | {include file="_head.tpl" title="Export d'exercice" current="acc/years"} <nav class="acc-year"> <h4>Exercice sélectionné :</h4> <h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3> </nav> <nav class="tabs"> <ul> {if !$year.closed} <li><a href="{$admin_url}acc/years/import.php?year={$year.id}">Import</a></li> {/if} <li class="current"><a href="{$admin_url}acc/years/import.php?year={$year.id}">Export</a></li> </ul> </nav> {form_errors} <form method="get" action="{$self_url}"> <fieldset> <legend>Export du journal général</legend> | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | {include file="_head.tpl" title="Export d'exercice" current="acc/years"} <nav class="acc-year"> <h4>Exercice sélectionné :</h4> <h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3> </nav> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$year.closed} <nav class="tabs"> <ul> {if !$year.closed} <li><a href="{$admin_url}acc/years/import.php?year={$year.id}">Import</a></li> {/if} <li class="current"><a href="{$admin_url}acc/years/import.php?year={$year.id}">Export</a></li> </ul> </nav> {/if} {form_errors} <form method="get" action="{$self_url}"> <fieldset> <legend>Export du journal général</legend> |
︙ | ︙ |
Modified src/templates/acc/years/index.tpl from [2990fb262c] to [ef8d0fd588].
︙ | ︙ | |||
79 80 81 82 83 84 85 | | <a href="{$admin_url}acc/reports/statement.php?year={$year.id}">Compte de résultat</a> | <a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a> </td> </tr> <tr> <td><em>{if $year.closed}Clôturé{else}En cours{/if}</em></td> <td> | < | | | | | | | < | 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | | <a href="{$admin_url}acc/reports/statement.php?year={$year.id}">Compte de résultat</a> | <a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a> </td> </tr> <tr> <td><em>{if $year.closed}Clôturé{else}En cours{/if}</em></td> <td> {linkbutton label="Export" shape="export" href="export.php?year=%d"|args:$year.id} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$year.closed} {linkbutton label="Import" shape="upload" href="import.php?year=%d"|args:$year.id} {linkbutton label="Balance d'ouverture" shape="reset" href="balance.php?id=%d"|args:$year.id} {linkbutton label="Modifier" shape="edit" href="edit.php?id=%d"|args:$year.id} {linkbutton label="Clôturer" shape="lock" href="close.php?id=%d"|args:$year.id} {linkbutton label="Supprimer" shape="delete" href="delete.php?id=%d"|args:$year.id} {/if} </td> </tr> </tbody> {/foreach} </table> {else} <p class="block alert"> Il n'y a pas d'exercice en cours. </p> {/if} {include file="_foot.tpl"} |
Modified src/templates/acc/years/new.tpl from [52a805b785] to [6309d13cdc].
1 2 3 | {include file="_head.tpl" title="Commencer un exercice" current="acc/years"} {if isset($_GET.from)} | | | 1 2 3 4 5 6 7 8 9 10 11 | {include file="_head.tpl" title="Commencer un exercice" current="acc/years"} {if isset($_GET.from)} <p class="confirm block"><strong>L'exercice a bien été clôturé.</strong><br />Vous pouvez commencer un nouvel exercice ci-dessous.</p> {/if} {form_errors} <form method="post" action="{$self_url}" data-focus="1"> <fieldset> |
︙ | ︙ | |||
22 23 24 25 26 27 28 | {input type="date" label="Début de l'exercice" name="start_date" required=true source=$year} {input type="date" label="Fin de l'exercice" name="end_date" required=true source=$year} </dl> </fieldset> <p class="submit"> {csrf_field key="acc_years_new"} | > > > | > | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | {input type="date" label="Début de l'exercice" name="start_date" required=true source=$year} {input type="date" label="Fin de l'exercice" name="end_date" required=true source=$year} </dl> </fieldset> <p class="submit"> {csrf_field key="acc_years_new"} {if isset($_GET.from)} {linkbutton shape="left" href="./" label="Ne pas créer de nouvel exercice"} {else} {linkbutton shape="left" href="./" label="Annuler"} {/if} {button type="submit" name="new" label="Créer ce nouvel exercice" shape="right" class="main"} </p> </form> {include file="_foot.tpl"} |
Modified src/templates/common/_csv_match_columns.tpl from [f3ffdaa36b] to [4bffb2b7bc].
︙ | ︙ | |||
19 20 21 22 23 24 25 | <tr> <th>{$csv_field}</th> <td class="help">{icon shape="right"}</td> <td> <select name="translation_table[{$index}]"> <option value="">-- Ne pas importer cette colonne</option> {foreach from=$csv->getColumnsWithDefaults() item="column"} | | | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <tr> <th>{$csv_field}</th> <td class="help">{icon shape="right"}</td> <td> <select name="translation_table[{$index}]"> <option value="">-- Ne pas importer cette colonne</option> {foreach from=$csv->getColumnsWithDefaults() item="column"} <option value="{$column.key}" {if $csv_field == $column.match || $csv_field == $column.label}selected="selected"{/if}>{$column.label}</option> {/foreach} </select> </td> </tr> {/foreach} </tbody> </table> </dd> </dl> </fieldset> |
Modified src/templates/common/files/edit_code.tpl from [2da57d0129] to [267be0c0b9].
1 2 3 4 5 6 7 8 9 | {include file="_head.tpl" title="Édition de fichier"} <form method="post" action="{$self_url}"> <p> {input type="textarea" name="content" cols="90" rows="50" default=$content} </p> <p class="submit"> {csrf_field key=$csrf_key} | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | {include file="_head.tpl" title="Édition de fichier"} <form method="post" action="{$self_url}"> <p> {input type="textarea" name="content" cols="90" rows="50" default=$content} </p> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer et fermer" shape="right" class="main"} </p> </form> <script type="text/javascript" src="{$admin_url}static/scripts/code_editor.js?{$version_hash}"></script> {include file="_foot.tpl"} |
Modified src/templates/common/files/edit_web.tpl from [5cdea9a946] to [cf8c700b8a].
1 2 3 4 5 6 7 8 9 | {include file="_head.tpl" title="Édition de fichier" custom_js=['wiki_editor.js']} <form method="post" action="{$self_url}"> <p class="textEditor"> {input type="textarea" name="content" cols="70" rows="30" default=$content data-preview-url="!common/files/_preview.php?f=%s"|local_url|args:$path data-fullscreen="1" data-attachments="0" data-savebtn="1" data-format=$format} </p> <p class="submit"> {csrf_field key=$csrf_key} | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | {include file="_head.tpl" title="Édition de fichier" custom_js=['wiki_editor.js']} <form method="post" action="{$self_url}"> <p class="textEditor"> {input type="textarea" name="content" cols="70" rows="30" default=$content data-preview-url="!common/files/_preview.php?f=%s"|local_url|args:$path data-fullscreen="1" data-attachments="0" data-savebtn="1" data-format=$format} </p> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer et fermer" shape="right" class="main"} </p> </form> {include file="_foot.tpl"} |
Modified src/templates/common/search/advanced.tpl from [03c3decebc] to [3f17655e98].
︙ | ︙ | |||
80 81 82 83 84 85 86 | "is not equal to": "n'est pas égal à", "is greater than": "est supérieur à", "is greater than or equal to": "est supérieur ou égal à", "is less than": "est inférieur à", "is less than or equal to": "est inférieur ou égal à", "is between": "est situé entre", "is not between": "n'est pas situé entre", | < | > | 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | "is not equal to": "n'est pas égal à", "is greater than": "est supérieur à", "is greater than or equal to": "est supérieur ou égal à", "is less than": "est inférieur à", "is less than or equal to": "est inférieur ou égal à", "is between": "est situé entre", "is not between": "n'est pas situé entre", "is null": "n'est pas renseigné", "is not null": "est renseigné", "begins with": "commence par", "doesn't begin with": "ne commence pas par", "ends with": "se termine par", "doesn't end with": "ne se termine pas par", "contains": "contient", "doesn't contain": "ne contient pas", "matches one of": "correspond à", |
︙ | ︙ | |||
105 106 107 108 109 110 111 | q.__ = function (str) { return translations[str]; }; q.loadDefaultOperators(); q.default_operator = "1"; // Add specific condition just to have the column show up in result | | | 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | q.__ = function (str) { return translations[str]; }; q.loadDefaultOperators(); q.default_operator = "1"; // Add specific condition just to have the column show up in result q.operators["1"] = "afficher cette colonne"; for (var i in q.types_operators) { q.types_operators[i]["1"] = q.operators["1"]; } q.buildInput = function (type, label, column) { if (label == '+') |
︙ | ︙ |
Modified src/templates/common/search/saved_searches.tpl from [9f71eb4c85] to [7706bc4791].
︙ | ︙ | |||
18 19 20 21 22 23 24 | <fieldset> <legend>Modifier une recherche enregistrée</legend> <dl> {input type="text" name="label" label="Intitulé" required=1 source=$search} <dt>Statut</dt> <?php $checked = (int)(bool)$search->id_membre; ?> {input type="radio" name="prive" value="1" default=$checked label="Recherche privée" help="Visible seulement par moi-même"} | > | > | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <fieldset> <legend>Modifier une recherche enregistrée</legend> <dl> {input type="text" name="label" label="Intitulé" required=1 source=$search} <dt>Statut</dt> <?php $checked = (int)(bool)$search->id_membre; ?> {input type="radio" name="prive" value="1" default=$checked label="Recherche privée" help="Visible seulement par moi-même"} {if $session->canAccess($access_section, $session::ACCESS_WRITE)} {input type="radio" name="prive" value="0" default=$checked label="Recherche publique" help="Visible et exécutable par tous les membres ayant accès à la gestion %s"|args:$target} {/if} <dt>Type</dt> <dd> {if $search.type == $search::TYPE_JSON} Avancée {elseif $search.type == $search::TYPE_SQL_UNPROTECTED} SQL non protégée {else} |
︙ | ︙ |
Modified src/templates/config/_menu.tpl from [e93a463d0d] to [48f28499f7].
1 2 3 4 5 6 7 | {if !$dialog} <nav class="tabs"> <ul> <li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Configuration</a></li> <li{if $current == 'custom'} class="current"{/if}><a href="{$admin_url}config/custom.php">Personnalisation</a></li> <li{if $current == 'users'} class="current"{/if}><a href="{$admin_url}config/users/">Membres</a></li> <li{if $current == 'backup'} class="current"{/if}><a href="{$admin_url}config/backup/">Sauvegardes</a></li> | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | {if !$dialog} <nav class="tabs"> <ul> <li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Configuration</a></li> <li{if $current == 'custom'} class="current"{/if}><a href="{$admin_url}config/custom.php">Personnalisation</a></li> <li{if $current == 'users'} class="current"{/if}><a href="{$admin_url}config/users/">Membres</a></li> <li{if $current == 'backup'} class="current"{/if}><a href="{$admin_url}config/backup/">Sauvegardes</a></li> <li{if $current == 'ext'} class="current"{/if}><a href="{$admin_url}config/ext/">Extensions</a></li> <li{if $current == 'advanced'} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li> </ul> {if $current == 'users'} <ul class="sub"> <li{if !$sub_current} class="current"{/if}><a href="{$admin_url}config/users/">Préférences</a></li> <li{if $sub_current == 'fields'} class="current"{/if}><a href="{$admin_url}config/fields/">Fiche des membres</a></li> |
︙ | ︙ |
Modified src/templates/config/advanced/errors.tpl from [f239e8b6ce] to [549263fb78].
︙ | ︙ | |||
45 46 47 48 49 50 51 | {/foreach} </table> </article> {/foreach} </section> {elseif isset($errors)} <p class="help"> | | < | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | {/foreach} </table> </article> {/foreach} </section> {elseif isset($errors)} <p class="help"> Liste des erreurs système et de code rencontrées par Paheko. </p> {if !count($errors)} <p class="block alert">Aucune erreur n'a été trouvée dans le journal error.log</p> {else} <table class="list"> <thead> |
︙ | ︙ |
Modified src/templates/config/backup/restore.tpl from [9249fbb327] to [db1206bb35].
︙ | ︙ | |||
13 14 15 16 17 18 19 | {if $ok} <p class="block confirm"> {if $ok == 'restore'}La restauration a bien été effectuée. {if $ok_code & Sauvegarde::NOT_AN_ADMIN} </p> <p class="block alert"> | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {if $ok} <p class="block confirm"> {if $ok == 'restore'}La restauration a bien été effectuée. {if $ok_code & Sauvegarde::NOT_AN_ADMIN} </p> <p class="block alert"> <strong>Vous n'êtes pas administrateur dans cette sauvegarde.</strong> Paheko a donné les droits d'administration à toutes les catégories afin d'empêcher de ne plus pouvoir se connecter. Merci de corriger les droits des catégories maintenant. {elseif $ok_code & Sauvegarde::CHANGED_USER} </p> <p class="block alert"> <strong>Votre compte membre n'existait pas dans la sauvegarde qui a été restaurée, vous êtes désormais connecté avec le premier compte administrateur.</strong> </p> {/if} |
︙ | ︙ |
Added src/templates/config/ext/delete.tpl version [af1fe35dc5].
> > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | {include file="_head.tpl" title="Désinstaller une extension" current="config"} {if $plugin} {include file="common/delete_form.tpl" legend="Supprimer une extension" confirm="Cocher cette case pour confirmer la suppression de toutes les données liées à cette extension" warning="Êtes-vous sûr de vouloir supprimer l'extension « %s » ?"|args:$plugin.label alert="Attention, cela supprimera toutes les données liées à l'extension !"} {else} {include file="common/delete_form.tpl" legend="Supprimer une extension" confirm="Cocher cette case pour confirmer la suppression de toutes les données liées à cette extension" warning="Êtes-vous sûr de vouloir supprimer l'extension « %s » ?"|args:$module.label alert="Attention, cela supprimera toutes les données liées à l'extension, y compris les modifications apportées !"} {/if} {include file="_foot.tpl"} |
Added src/templates/config/ext/index.tpl version [969da88b84].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | {include file="_head.tpl" title="Extensions" current="config"} {include file="config/_menu.tpl" current="ext"} <nav class="tabs"> <ul class="sub"> <li{if !$installable} class="current"{/if}><a href="./">Activées</a></li> <li{if $installable} class="current"{/if}><a href="./?install=1">Inactives</a></li> </ul> </nav> {if !empty($url_plugins)} <p class="actions"> {linkbutton shape="help" href=$url_plugins label="Trouver d'autres extensions à installer" target="_blank"} </p> {/if} <p class="help">Les extensions apportent des fonctionnalités supplémentaires, et peuvent être activées selon vos besoins.</p> {form_errors} <form method="post" action=""> <table class="list"> <thead> <td></td> <td>Extension</td> <td>Accès restreint</td> <td></td> <td></td> <td></td> <td></td> </thead> <tbody> {foreach from=$list item="item"} <tr {if $_GET.focus == $item.name}class="highlight"{/if}> {if $item.broken_message} <td></td> <td colspan="6"> <strong class="error">Extension cassée : {$item.name}</strong><br /> {$item.broken_message} </td> {else} <td class="icon"> {if $item.icon_url} <svg><use xlink:href='{$item.icon_url}#img' href="{$item.icon_url}#img"></use></svg> {if $item.url} </a> {/if} {/if} </td> <td> <h3>{if $item.label}{$item.label}{else}{$item.name}{/if} {if $item.module && $item.module->canDelete()} <strong class="tag">Modifiée</strong> {elseif $item.module} <span class="tag">Modifiable</span> {/if} </h3> <small>{$item.description|escape|nl2br}</small><br /> <small class="help"> {if $item.author} Par {link label=$item.author href=$item.url target="_blank"} {/if} {if $item.plugin && $item.plugin.version}— Version {$item.plugin.version}{/if} {if $item.readme_url} — {link href=$item.readme_url label="Documentation" target="_dialog"} {/if} </small> </td> {if $item.broken} <td colspan="5"> {if ENABLE_TECH_DETAILS} <strong>Le code source de l'extension est absent du répertoire <tt>…/data/plugins/</tt></strong> {else} <strong>Cette extension n'est pas installée sur ce serveur.</strong><br /> {/if} <br /> <small>Il n'est pas possible de la supprimer non plus, le code source est nécessaire pour pouvoir la supprimer.</small> </td> {else} <td> {if $item.restrict_section} <span class="permissions">{display_permissions section=$item.restrict_section level=$item.restrict_level}</span> {/if} </td> <td> {if $item.enabled && $item.url} {linkbutton shape="right" label="Ouvrir" href=$item.url} {/if} </td> <td class="actions"> {if $item.module && $item.enabled} {if $item.module->hasLocal() && $item.module->hasDist()} {linkbutton label="Remettre à zéro" href="delete.php?module=%s"|args:$item.name shape="reset" target="_dialog"} {/if} {*FIXME{linkbutton label="Modifier" href="edit.php?module=%s"|args:$item.name shape="edit" target="_dialog"}*} {elseif $item.module && !$item.enabled && $item.module->canDelete()} {linkbutton label="Supprimer" href="delete.php?module=%s"|args:$item.name shape="delete" target="_dialog"} {elseif $item.plugin && !$item.enabled && $item.installed} {linkbutton label="Supprimer" href="delete.php?plugin=%s"|args:$item.name shape="delete" target="_dialog"} {/if} </td> <td class="actions"> {if $item.config_url && $item.enabled} {linkbutton label="Configurer" href=$item.config_url shape="settings" target="_dialog"} {/if} </td> <td class="actions"> {if $item.module} {if $item.enabled} {button type="submit" label="Désactiver" shape="eye-off" name="disable_module" value=$item.name} {else} {button type="submit" label="Activer" shape="eye" name="enable" value=$item.name} {/if} {else} {if $item.enabled} {button type="submit" label="Désactiver" shape="eye-off" name="disable_plugin" value=$item.name} {else} {button type="submit" label="Activer" shape="eye" name="install" value=$item.name} {/if} {/if} </td> {/if} {/if} </tr> {/foreach} </tbody> </table> {csrf_field key=$csrf_key} </form> <p class="help"> La mention <em class="tag">Modifiable</em> indique que cette extension est un module que vous pouvez modifier. {linkbutton shape="help" label="Documentation des modules" href=$url_help_modules target="_dialog"} </p> {include file="_foot.tpl"} |
Modified src/templates/config/index.tpl from [dfd5e20e88] to [98c661b3c1].
︙ | ︙ | |||
15 16 17 18 19 20 21 | <fieldset> <legend>Informations</legend> <dl> <dt>Version installée</dt> <dd>Paheko {$garradin_version}</dd> {if CONTRIBUTOR_LICENSE === null} <dd class="help"> | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <fieldset> <legend>Informations</legend> <dl> <dt>Version installée</dt> <dd>Paheko {$garradin_version}</dd> {if CONTRIBUTOR_LICENSE === null} <dd class="help"> Le développement et le support de Paheko ne sont possibles que grâce à votre soutien !<br /> {linkbutton href="https://kd2.org/soutien.html" label="Faire un don pour soutenir le développement" target="_blank" shape="export"} :-) </dd> {/if} {if $new_version} <dd><p class="block alert"> Une nouvelle version <strong>{$new_version}</strong> est disponible !<br /> {if ENABLE_UPGRADES} |
︙ | ︙ |
Deleted src/templates/config/modules/index.tpl version [14240b61b4].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted src/templates/config/plugins.tpl version [8b8b529bed].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/templates/install.tpl from [6849a1fd7c] to [723c5234c8].
|
| | | | < | | | | | | | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | {include file="_head.tpl" title="Démarrer avec Paheko" menu=false} <p class="help"> <strong>Bienvenue dans Paheko !</strong><br /> Veuillez remplir les informations suivantes pour démarrer la gestion de votre association. </p> {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Informations sur l'association</legend> <dl> {input type="select" required=true label="Pays (pour la comptabilité)" options=$countries default="FR" help="Ce choix permet de configurer les règles comptables en fonction du pays de l'association." name="country"} {input type="text" label="Nom de l'association" required=true name="name"} </dl> </fieldset> <fieldset> <legend>Création du compte administrateur</legend> <dl> {input type="text" label="Nom et prénom" required=true name="user_name"} {input type="email" label="Adresse E-Mail" required=true name="user_email"} {include file="users/_password_form.tpl" field="password" required=true} </dl> </fieldset> {if count($installable)} <fieldset> <legend>Activer des extensions</legend> <p class="help">Les extensions apportent des fonctionnalités supplémentaires, et peuvent être activées selon vos besoins.</p> <table class="list"> <tr> <td> </td> <th>Nom</th> </tr> {foreach from=$installable key="name" item="data"} <tr> {if $data.plugin} <td class="check"> {input type="checkbox" name="plugins[%s]"|args:$name value=1} </td> <td> <label for={"f_plugins%s_1"|args:$name}><strong>{$data.plugin.label}</strong><br /> <small>{$data.plugin.description|escape|nl2br}</small></label> </td> {else} <td class="check"> {input type="checkbox" name="modules[%s]"|args:$name value=1} </td> <td> <label for={"f_modules%s_1"|args:$name}><strong>{$data.module.label}</strong><br /> <small>{$data.module.description|escape|nl2br}</small></label> </td> {/if} </tr> {/foreach} </table> <p class="help">Note : il sera ensuite possible d'activer ou désactiver les extensions dans la <strong>Configuration</strong>.</p> </fieldset> {/if} <p class="submit"> {csrf_field key="install"} {button type="submit" name="save" label="Terminer l'installation" shape="right" class="main"} </p> </form> {include file="_foot.tpl"} |
Modified src/templates/login.tpl from [c9e8fc3116] to [75add3ccf0].
︙ | ︙ | |||
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | {csrf_field key="login"} {button type="submit" name="login" label="Se connecter" shape="right" class="main"} {if !DISABLE_EMAIL && !$app_token} {linkbutton href="!password.php" label="Mot de passe perdu ?" shape="help"} {linkbutton href="!password.php?new" label="Première connexion ?" shape="user"} {/if} </p> </form> {literal} <script type="text/javascript" async="async"> if (window.navigator.userAgent.match(/MSIE|Trident\/|Edge\//)) { document.getElementById('old_browser').style.display = 'block'; } </script> {/literal} {include file="_foot.tpl"} | > > > > > | 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | {csrf_field key="login"} {button type="submit" name="login" label="Se connecter" shape="right" class="main"} {if !DISABLE_EMAIL && !$app_token} {linkbutton href="!password.php" label="Mot de passe perdu ?" shape="help"} {linkbutton href="!password.php?new" label="Première connexion ?" shape="user"} {/if} </p> <p class="help"> Suggestion : mettez cette page dans vos favoris pour la retrouver facilement :-)<br /> <small>(Sur ordinateur appuyez sur <tt>Ctrl</tt> + <tt>D</tt>. Aide : <a href="https://support.mozilla.org/fr/kb/marque-pages-firefox#w_marquer-une-page" target="_blank">Firefox</a>, <a href="https://support.google.com/chrome/answer/188842?hl=fr&co=GENIE.Platform%3DDesktop&oco=0" target="_blank">Chrome</a>)</small> </p> </form> {literal} <script type="text/javascript" async="async"> if (window.navigator.userAgent.match(/MSIE|Trident\/|Edge\//)) { document.getElementById('old_browser').style.display = 'block'; } </script> {/literal} {include file="_foot.tpl"} |
Added src/templates/me/_nav.tpl version [680aee5229].
> > > > > > > | 1 2 3 4 5 6 7 | <nav class="tabs"> <ul> <li{if $current == 'me'} class="current"{/if}><a href="{$admin_url}me/">Mes informations personnelles</a></li> <li{if $current == 'security'} class="current"{/if}><a href="{$admin_url}me/security.php">Mot de passe et options de sécurité</a></li> <li{if $current == 'preferences'} class="current"{/if}><a href="{$admin_url}me/preferences.php">Préférences</a></li> </ul> </nav> |
Modified src/templates/me/edit.tpl from [0d731c955e] to [d63c67d1c3].
1 2 | {include file="_head.tpl" title="Mes informations personnelles" current="me"} | < < < < < | | 1 2 3 4 5 6 7 8 9 10 | {include file="_head.tpl" title="Mes informations personnelles" current="me"} {include file="./_nav.tpl" current="me"} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>Informations personnelles</legend> |
︙ | ︙ |
Modified src/templates/me/index.tpl from [c8a4df60cf] to [78eaa6c657].
1 2 | {include file="_head.tpl" title="Mes informations personnelles" current="me"} | < < < < < < | | 1 2 3 4 5 6 7 8 9 10 | {include file="_head.tpl" title="Mes informations personnelles" current="me"} {include file="./_nav.tpl" current="me"} {if $ok !== null} <p class="confirm block"> Les modifications ont bien été enregistrées. </p> {/if} |
︙ | ︙ |
Modified src/templates/me/preferences.tpl from [d5439de3ce] to [b574cf9be4].
1 2 | {include file="_head.tpl" title="Mes préférences" current="me"} | < < < < < < | | 1 2 3 4 5 6 7 8 9 10 | {include file="_head.tpl" title="Mes préférences" current="me"} {include file="./_nav.tpl" current="preferences"} {if $ok !== null} <p class="confirm block"> Les modifications ont bien été enregistrées. </p> {/if} |
︙ | ︙ |
Modified src/templates/me/security.tpl from [dc16dd7875] to [1e057f4224].
1 2 | {include file="_head.tpl" title="Mes informations de connexion et sécurité" current="me"} | < < < | < < | 1 2 3 4 5 6 7 8 9 10 | {include file="_head.tpl" title="Mes informations de connexion et sécurité" current="me"} {include file="./_nav.tpl" current="security"} {if $ok} <p class="block confirm"> Changements enregistrés. </p> {/if} |
︙ | ︙ | |||
111 112 113 114 115 116 117 | <dd class="help">Permet de chiffrer les messages qui vous sont envoyés par e-mail, notamment les messages de récupération de mot de passe, pour empêcher un attaquant de prendre contrôle de votre compte si votre adresse e-mail est piratée.</dd> {/if} <dt>Déconnecter toutes mes sessions</dt> <dd>{{Vous n'avez actuellement qu'une seule session ouverte (celle-ci).}{Vous avez actuellement %n sessions ouvertes (y compris celle-ci).} n=$sessions_count}</dd> <dd>{linkbutton href="!logout.php?all" label="Me déconnecter de toutes les sessions" shape="logout"}</dd> <dt>Journal de connexion</dt> <dd>Permet de voir les tentatives de connexion, les modifications de mot de passe, etc.</dd> | | | 106 107 108 109 110 111 112 113 114 115 116 117 118 | <dd class="help">Permet de chiffrer les messages qui vous sont envoyés par e-mail, notamment les messages de récupération de mot de passe, pour empêcher un attaquant de prendre contrôle de votre compte si votre adresse e-mail est piratée.</dd> {/if} <dt>Déconnecter toutes mes sessions</dt> <dd>{{Vous n'avez actuellement qu'une seule session ouverte (celle-ci).}{Vous avez actuellement %n sessions ouvertes (y compris celle-ci).} n=$sessions_count}</dd> <dd>{linkbutton href="!logout.php?all" label="Me déconnecter de toutes les sessions" shape="logout"}</dd> <dt>Journal de connexion</dt> <dd>Permet de voir les tentatives de connexion, les modifications de mot de passe, etc.</dd> <dd>{linkbutton href="!users/log.php" label="Voir mon journal de connexion" shape="menu"}</dd> </dl> {/if} {include file="_foot.tpl"} |
Modified src/templates/users/details.tpl from [d966205540] to [2007f910f2].
︙ | ︙ | |||
50 51 52 53 54 55 56 57 58 59 60 61 62 63 | <dd>{link href="?id=%d"|args:$child.id label=$child.name}</dd> {/foreach} {/if} </dl> <aside class="describe"> <dl class="describe"> <dt>Catégorie</dt> <dd>{$category.name}</dd> <dt>Droits</dt> <dd><span class="permissions">{display_permissions permissions=$category}</span></dd> <dt>Dernière connexion</dt> <dd>{if empty($user.date_login)}Jamais{else}{$user.date_login|date_short:true}{/if}</dd> <dd> | > > > > > > > | 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | <dd>{link href="?id=%d"|args:$child.id label=$child.name}</dd> {/foreach} {/if} </dl> <aside class="describe"> <dl class="describe"> {if $user.date_updated} <dt>Fiche modifiée le</dt> <dd>{$user.date_updated|date_short:true}</dd> <dd> {linkbutton shape="menu" label="Historique" href="!users/log.php?history=%d"|args:$user.id} </dd> {/if} <dt>Catégorie</dt> <dd>{$category.name}</dd> <dt>Droits</dt> <dd><span class="permissions">{display_permissions permissions=$category}</span></dd> <dt>Dernière connexion</dt> <dd>{if empty($user.date_login)}Jamais{else}{$user.date_login|date_short:true}{/if}</dd> <dd> |
︙ | ︙ |
Modified src/templates/users/log.tpl from [b8f7ed5cb2] to [a4a181efc2].
|
| > > > | > | | > > > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | {if $params.history} {include file="_head.tpl" title="Historique des modifications"} {else} {include file="_head.tpl" title="Journal de connexion et d'actions"} {/if} {if $params.id_user} {include file="users/_nav_user.tpl" id=$params.id_user} {elseif $params.history} {include file="users/_nav_user.tpl" id=$params.history} {else} {include file="me/_nav.tpl" current="security"} {/if} {if !$params.history} <p class="help"> Cette page liste les tentatives de connexion, les modifications de mot de passe ou d'identifiant, et toutes les actions de création, suppression ou modification de contenu de ce membre. </p> {/if} {if $list->count()} {include file="common/dynamic_list_head.tpl"} {foreach from=$list->iterate() item="row"} <tr> {if !$params.id_self} <th>{if !$row.identity}*{else}{$row.identity}{/if}</th> {/if} <td>{$row.created|date_short:true}</td> <td class="help"> {if $row.type == Log::LOGIN_FAIL || $row.type == Log::LOGIN_PASSWORD_CHANGE} <span class="alert">{icon shape="alert"}</span> {/if} |
︙ | ︙ | |||
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | </tr> {/foreach} </tbody> </table> {$list->getHTMLPagination()|raw} {else} <p class="block alert"> Aucune activité trouvée. </p> {/if} </form> {include file="_foot.tpl"} | > > | 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | </tr> {/foreach} </tbody> </table> {$list->getHTMLPagination()|raw} <p class="help">Note : les heures correspondent au fuseau horaire du serveur (<?=ini_get('date.timezone')?>).</p> {else} <p class="block alert"> Aucune activité trouvée. </p> {/if} </form> {include file="_foot.tpl"} |
Modified src/templates/web/config.tpl from [99e5132839] to [d1ae25c3c3].
︙ | ︙ | |||
69 70 71 72 73 74 75 | {button type="submit" name="disable_site" label="Désactiver le site public" shape="right" class="main"} {csrf_field key="config_site"} </p> </form> </dt> <dd class="help"> En désactivant le site public, les visiteurs seront automatiquement redirigés vers la page de connexion.<br /> | | | 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | {button type="submit" name="disable_site" label="Désactiver le site public" shape="right" class="main"} {csrf_field key="config_site"} </p> </form> </dt> <dd class="help"> En désactivant le site public, les visiteurs seront automatiquement redirigés vers la page de connexion.<br /> Cette option est utile si vous avez déjà un site web et ne souhaitez pas utiliser la fonctionnalité site web de Paheko. </dd> </dl> </fieldset> {/if} <form method="post" action="{$self_url}"> |
︙ | ︙ |
Modified src/www/admin/_inc.php from [0cfa0f9f9d] to [2059b7f42e].
︙ | ︙ | |||
52 53 54 55 56 57 58 | { Utils::redirect(ADMIN_URL . 'login.php'); } } $tpl->assign('current', ''); | < < < < < < | | 52 53 54 55 56 57 58 59 60 61 62 63 | { Utils::redirect(ADMIN_URL . 'login.php'); } } $tpl->assign('current', ''); $tpl->assign('plugins_menu', Plugins::listModulesAndPluginsMenu($session)); } // Make sure we allow frames to work header('X-Frame-Options: SAMEORIGIN', true); |
Modified src/www/admin/acc/accounts/deposit.php from [b43f97e6b8] to [f448751140].
︙ | ︙ | |||
28 29 30 31 32 33 34 | $form->runIf('save', function () use ($checked, $transaction, $journal) { if (!count($checked)) { throw new UserException('Aucune ligne n\'a été cochée, impossible de créer un dépôt. Peut-être vouliez-vous saisir un virement ?'); } $transaction->importFromDepositForm(); | | | | < | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | $form->runIf('save', function () use ($checked, $transaction, $journal) { if (!count($checked)) { throw new UserException('Aucune ligne n\'a été cochée, impossible de créer un dépôt. Peut-être vouliez-vous saisir un virement ?'); } $transaction->importFromDepositForm(); Transactions::saveDeposit($transaction, $journal->iterate(), $checked); Utils::redirect(ADMIN_URL . 'acc/transactions/details.php?id=' . $transaction->id()); }, 'acc_deposit_' . $account->id()); // Uncheck everything if there was an error if ($form->hasErrors()) { $journal = $account->getDepositJournal(CURRENT_YEAR_ID); } $date = new \DateTime; if ($date > $current_year->end_date) { $date = $current_year->end_date; } $target = $account::TYPE_BANK; $missing_balance = $account->getDepositMissingBalance(CURRENT_YEAR_ID); $journal->loadFromQueryString(); $tpl->assign(compact( 'account', 'journal', 'date', 'target', 'checked', 'missing_balance', 'transaction' )); $tpl->display('acc/accounts/deposit.tpl'); |
Modified src/www/admin/acc/accounts/index.php from [f19ff030ea] to [ca40714bbd].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin; use Garradin\Accounting\Reports; require_once __DIR__ . '/../_inc.php'; if (!CURRENT_YEAR_ID) { Utils::redirect('!acc/years/?msg=OPEN'); } $tpl->assign('chart_id', $current_year->id_chart); $tpl->assign('grouped_accounts', Reports::getClosingSumsFavoriteAccounts(['year' => CURRENT_YEAR_ID])); $tpl->display('acc/accounts/index.tpl'); | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php namespace Garradin; use Garradin\Accounting\Reports; use Garradin\Accounting\Transactions; require_once __DIR__ . '/../_inc.php'; if (!CURRENT_YEAR_ID) { Utils::redirect('!acc/years/?msg=OPEN'); } $pending_count = Transactions::listPendingCreditAndDebtForClosedYears()->count(); $tpl->assign(compact('pending_count')); $tpl->assign('chart_id', $current_year->id_chart); $tpl->assign('grouped_accounts', Reports::getClosingSumsFavoriteAccounts(['year' => CURRENT_YEAR_ID])); $tpl->display('acc/accounts/index.tpl'); |
Modified src/www/admin/acc/accounts/simple.php from [83f17410b3] to [d130795fe7].
︙ | ︙ | |||
30 31 32 33 34 35 36 | $list = Transactions::listByType(CURRENT_YEAR_ID, $type == -1 ? null : $type); $list->setTitle(sprintf('Suivi - %s', $types[$type])); $list->loadFromQueryString(); $can_edit = $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$year->closed; | > > > > > > | | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | $list = Transactions::listByType(CURRENT_YEAR_ID, $type == -1 ? null : $type); $list->setTitle(sprintf('Suivi - %s', $types[$type])); $list->loadFromQueryString(); $can_edit = $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$year->closed; $pending_count = null; if ($type == Transaction::TYPE_CREDIT || $type == Transaction::TYPE_DEBT) { $pending_count = Transactions::listPendingCreditAndDebtForClosedYears()->count(); } $tpl->assign(compact('type', 'list', 'types', 'can_edit', 'year', 'pending_count')); $tpl->display('acc/accounts/simple.tpl'); |
Modified src/www/admin/acc/reports/_inc.php from [4ba354a98e] to [689b309913].
︙ | ︙ | |||
42 43 44 45 46 47 48 | if (qg('after') && ($a = Entity::filterUserDateValue(qg('after')))) { $criterias['after'] = $a; } $criterias['year'] = $year->id(); $tpl->assign('year', $year); | < | 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | if (qg('after') && ($a = Entity::filterUserDateValue(qg('after')))) { $criterias['after'] = $a; } $criterias['year'] = $year->id(); $tpl->assign('year', $year); $tpl->assign('before_default', $criterias['before'] ?? $year->end_date); $tpl->assign('after_default', $criterias['after'] ?? $year->start_date); } if (qg('projects_only')) { $criterias['projects_only'] = true; } |
︙ | ︙ | |||
73 74 75 76 77 78 79 | $c = $c->format('Y-m-d'); } } $tpl->assign('criterias_query', http_build_query($criterias_query)); unset($criterias_query['compare_year']); $tpl->assign('criterias_query_no_compare', http_build_query($criterias_query)); | > > | 72 73 74 75 76 77 78 79 80 | $c = $c->format('Y-m-d'); } } $tpl->assign('criterias_query', http_build_query($criterias_query)); unset($criterias_query['compare_year']); $tpl->assign('criterias_query_no_compare', http_build_query($criterias_query)); $tpl->assign('now', new \DateTime); |
Modified src/www/admin/acc/transactions/new.php from [611e89f981] to [53588cb2a1].
︙ | ︙ | |||
30 31 32 33 34 35 36 | $lines = [[], []]; $form->runIf(f('lines') !== null, function () use (&$lines) { $lines = Transaction::getFormLines(); }); // Quick-fill transaction from query parameters | > | | > | | > > | > > > > > > > > | > > > > > > | > > > | | > > > > > | > > > > > > > > | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | $lines = [[], []]; $form->runIf(f('lines') !== null, function () use (&$lines) { $lines = Transaction::getFormLines(); }); // Quick-fill transaction from query parameters // 0 = amount, in single currency units if (qg('0')) { $amount = Utils::moneyToInteger(qg('0')); } // 00 = Amount, in cents if (qg('00')) { $amount = (int)qg('00'); } // l = label if (qg('l')) { $transaction->label = qg('l'); } // dt = date if (qg('dt')) { $transaction->date = new Date(qg('d')); } // t = type if (qg('t')) { $transaction->type = (int) qg('t'); } // ab = Bank/cash account if (qg('ab') && ($a = $accounts->getWithCode(qg('ab'))) && in_array($a->type, [$a::TYPE_BANK, $a::TYPE_CASH, $a::TYPE_OUTSTANDING])) { $transaction->setDefaultAccount($transaction::TYPE_REVENUE, 'debit', $a->id); $transaction->setDefaultAccount($transaction::TYPE_EXPENSE, 'credit', $a->id); $transaction->setDefaultAccount($transaction::TYPE_TRANSFER, 'debit', $a->id); } // ar = Revenue account if (qg('ar') && ($a = $accounts->getWithCode(qg('ar'))) && $a->type == $a::TYPE_REVENUE) { $transaction->setDefaultAccount($transaction::TYPE_REVENUE, 'credit', $a->id); $transaction->setDefaultAccount($transaction::TYPE_CREDIT, 'credit', $a->id); } // ae = Expense account if (qg('ae') && ($a = $accounts->getWithCode(qg('ae'))) && $a->type == $a::TYPE_REVENUE) { $transaction->setDefaultAccount($transaction::TYPE_EXPENSE, 'debit', $a->id); $transaction->setDefaultAccount($transaction::TYPE_DEBT, 'debit', $a->id); } // at = Transfer account if (qg('at') && ($a = $accounts->getWithCode(qg('at'))) && $a->type == $a::TYPE_BANK) { $transaction->setDefaultAccount($transaction::TYPE_TRANSFER, 'credit', $a->id); } // a3 = Third-party account if (qg('a3') && ($a = $accounts->getWithCode(qg('a3'))) && $a->type == $a::TYPE_BANK) { $transaction->setDefaultAccount($transaction::TYPE_CREDIT, 'debit', $a->id); $transaction->setDefaultAccount($transaction::TYPE_DEBT, 'credit', $a->id); } $types_details = $transaction->getTypesDetails(); // Duplicate transaction if (qg('copy')) { $old = Transactions::get((int)qg('copy')); |
︙ | ︙ |
Added src/www/admin/acc/transactions/pending.php version [a795ae2cc4].
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php namespace Garradin; use Garradin\Accounting\Transactions; use Garradin\Entities\Accounting\Transaction; require_once __DIR__ . '/../_inc.php'; $list = Transactions::listPendingCreditAndDebtForClosedYears(); $list->loadFromQueryString(); $tpl->assign(compact('list')); $tpl->display('acc/transactions/pending.tpl'); |
Modified src/www/admin/acc/years/balance.php from [448fd1291b] to [da15f83422].
︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 35 36 37 38 | $csrf_key = 'acc_years_balance_' . $year->id(); $accounts = $year->accounts(); $form->runIf('save', function () use ($year, $session) { $db = DB::getInstance(); // Fail everything if appropriation failed $db->begin(); $transaction = new Transaction; $transaction->id_creator = Session::getUserId(); $transaction->importFromBalanceForm($year); $transaction->save(); if (f('appropriation')) { | > > | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | $csrf_key = 'acc_years_balance_' . $year->id(); $accounts = $year->accounts(); $form->runIf('save', function () use ($year, $session) { $db = DB::getInstance(); // Fail everything if appropriation failed $db->begin(); $year->deleteOpeningBalance(); $transaction = new Transaction; $transaction->id_creator = Session::getUserId(); $transaction->importFromBalanceForm($year); $transaction->save(); if (f('appropriation')) { |
︙ | ︙ | |||
137 138 139 140 141 142 143 144 | if (!empty($_POST['lines']) && is_array($_POST['lines'])) { $lines = Transaction::getFormLines(); } $appropriation_account = $accounts->getSingleAccountForType(Account::TYPE_APPROPRIATION_RESULT); $can_appropriate = $accounts->getIdForType(Account::TYPE_NEGATIVE_RESULT) && $accounts->getIdForType(Account::TYPE_POSITIVE_RESULT); | > | | 139 140 141 142 143 144 145 146 147 148 149 150 | if (!empty($_POST['lines']) && is_array($_POST['lines'])) { $lines = Transaction::getFormLines(); } $appropriation_account = $accounts->getSingleAccountForType(Account::TYPE_APPROPRIATION_RESULT); $can_appropriate = $accounts->getIdForType(Account::TYPE_NEGATIVE_RESULT) && $accounts->getIdForType(Account::TYPE_POSITIVE_RESULT); $has_balance = $year->hasOpeningBalance(); $tpl->assign(compact('lines', 'years', 'chart_change', 'previous_year', 'year_selected', 'year', 'csrf_key', 'can_appropriate', 'appropriation_account', 'has_balance')); $tpl->display('acc/years/balance.tpl'); |
Modified src/www/admin/acc/years/export.php from [f39aed5a9b] to [fe19ac4b6a].
1 2 3 4 5 6 7 8 | <?php namespace Garradin; use Garradin\Accounting\Export; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin; use Garradin\Accounting\Export; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; $year_id = (int) qg('year') ?: CURRENT_YEAR_ID; if ($year_id === CURRENT_YEAR_ID) { $year = $current_year; } else { $year = Years::get($year_id); |
︙ | ︙ |
Added src/www/admin/config/ext/delete.php version [9d0842a19f].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <?php namespace Garradin; use Garradin\UserTemplate\Modules; use Garradin\Plugins; require_once __DIR__ . '/../_inc.php'; $csrf_key = 'ext_delete'; $plugin = $module = null; if (qg('plugin')) { $plugin = Plugins::get(qg('plugin')); $form->runIf(f('delete') && f('confirm_delete'), function () use ($plugin) { $plugin->delete(); }, $csrf_key, '!config/ext/'); } else { $module = Modules::get(qg('module')); $form->runIf(f('delete') && f('confirm_delete'), function () use ($module) { $module->delete(); }, $csrf_key, '!config/ext/'); } $tpl->assign(compact('plugin', 'module', 'csrf_key')); $tpl->display('config/ext/delete.tpl'); |
Added src/www/admin/config/ext/index.php version [e286764cbd].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | <?php namespace Garradin; use Garradin\UserTemplate\Modules; use Garradin\Plugins; use Garradin\Users\Session; require_once __DIR__ . '/../_inc.php'; $csrf_key = 'ext'; $session = Session::getInstance(); $form->runIf('install', function () { Plugins::install(f('install')); }, $csrf_key, '!config/ext/?focus=' . f('install')); $form->runIf('enable', function () { $m = Modules::get(f('enable')); if (!$m) { throw new UserException('Ce module n\'existe pas'); } $m->enabled = true; $m->save(); }, $csrf_key, '!config/ext/?focus=' . f('enable')); $form->runIf('disable_module', function () { $m = Modules::get(f('disable_module')); if (!$m) { throw new UserException('Ce module n\'existe pas'); } $m->enabled = false; $m->save(); }, $csrf_key, '!config/ext/'); $form->runIf('disable_plugin', function () { $p = Plugins::get(f('disable_plugin')); if (!$p) { throw new UserException('Cette extension n\'existe pas'); } $p->set('enabled', false); $p->save(); }, $csrf_key, '!config/ext/'); Modules::refresh(); if (qg('install')) { $list = Plugins::listModulesAndPlugins(true); $tpl->assign('url_plugins', ENABLE_TECH_DETAILS ? WEBSITE . 'wiki?name=Extensions' : null); $tpl->assign('installable', true); } else { $list = Plugins::listModulesAndPlugins(false); $tpl->assign('installable', false); } $url_help_modules = sprintf(HELP_PATTERN_URL, 'modules'); $tpl->assign(compact('list', 'csrf_key', 'url_help_modules')); $tpl->display('config/ext/index.tpl'); flush(); Plugins::upgradeAllIfRequired(); |
Deleted src/www/admin/config/modules/index.php version [0741f74399].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted src/www/admin/config/plugins.php version [30e38b80b8].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/www/admin/index.php from [3fd93a9cbf] to [461052c44c].
1 2 3 4 5 6 7 8 | <?php namespace Garradin; use Garradin\Web\Web; use Garradin\Files\Files; use Garradin\Users\Session; use Garradin\Entities\Files\File; | | | | < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <?php namespace Garradin; use Garradin\Web\Web; use Garradin\Files\Files; use Garradin\Users\Session; use Garradin\Entities\Files\File; use Garradin\Plugins; require_once __DIR__ . '/_inc.php'; $banner = ''; $session = Session::getInstance(); Plugins::fireSignal('home.banner', ['user' => $session->getUser(), 'session' => $session], $banner); $homepage = Config::getInstance()->file('admin_homepage'); if ($homepage) { $homepage = $homepage->render(ADMIN_URL . 'common/files/preview.php?p=' . File::CONTEXT_DOCUMENTS . '/'); } else { $homepage = null; } $buttons = Plugins::listModulesAndPluginsHomeButtons($session); $tpl->assign(compact('homepage', 'banner', 'buttons')); $tpl->assign('custom_css', ['!web/css.php']); $tpl->display('index.tpl'); flush(); |
︙ | ︙ |
Modified src/www/admin/install.php from [9dea1c90c7] to [b9289a2728].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin; use Garradin\Users\Session; use Garradin\Entities\Accounting\Chart; const INSTALL_PROCESS = true; require_once __DIR__ . '/../../include/test_required.php'; require_once __DIR__ . '/../../include/init.php'; | > > | | > > > > > > | > > > > | | | > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | <?php namespace Garradin; use Garradin\Users\Session; use Garradin\Entities\Accounting\Chart; use Garradin\UserTemplate\Modules; use Garradin\Plugins; const INSTALL_PROCESS = true; require_once __DIR__ . '/../../include/test_required.php'; require_once __DIR__ . '/../../include/init.php'; $exists = file_exists(DB_FILE); if ($exists && !filesize(DB_FILE)) { @unlink(DB_FILE); $exists = false; } if ($exists) { throw new UserException('Garradin est déjà installé'); } Install::checkAndCreateDirectories(); Install::checkReset(); if (DISABLE_INSTALL_FORM) { throw new \RuntimeException('Install form has been disabled'); } function f($key) { return \KD2\Form::get($key); } $tpl = Template::getInstance(); $tpl->assign('admin_url', ADMIN_URL); $form = new Form; $tpl->assign_by_ref('form', $form); $form->runIf('save', function () { Install::installFromForm(); Session::getInstance()->forceLogin(1); }, 'install', ADMIN_URL); $tpl->assign('countries', Chart::COUNTRY_LIST); $modules = Modules::listLocal(); $plugins = Plugins::listInstallable(false); $installable = []; foreach (Install::DEFAULT_PLUGINS as $plugin) { if (array_key_exists($plugin, $plugins)) { $installable[$plugin] = ['plugin' => $plugins[$plugin]]; } } foreach (Install::DEFAULT_MODULES as $module) { if (array_key_exists($module, $modules)) { $installable[$module] = ['module' => $modules[$module]]; } } ksort($installable); $tpl->assign('installable', $installable); $tpl->display('install.tpl'); |
Modified src/www/admin/services/user/_form.php from [148f0505cd] to [b8dd314a46].
︙ | ︙ | |||
8 9 10 11 12 13 14 | if (!defined('\Garradin\ROOT')) { die(); } assert(isset($tpl, $form_url, $create)); | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | if (!defined('\Garradin\ROOT')) { die(); } assert(isset($tpl, $form_url, $create)); $current_only = !f('past_services'); // If there is only one user selected we can calculate the amount $single_user_id = isset($users) && count($users) == 1 ? key($users) : null; $copy_service ??= null; $copy_service_only_paid ??= null; $users ??= null; $grouped_services = Services::listGroupedWithFees($single_user_id, (int)$current_only); if (!count($grouped_services)) { $current_only = false; $grouped_services = Services::listGroupedWithFees($single_user_id, (int)$current_only); } if (!isset($count_all)) { $count_all = Services::count(); } $has_past_services = count($grouped_services) != $count_all; |
︙ | ︙ |
Modified src/www/admin/services/user/edit.php from [7234f72cb7] to [059c1bf209].
︙ | ︙ | |||
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | $form_url = sprintf('edit.php?id=%d&', $su->id()); $create = false; require __DIR__ . '/_form.php'; $form->runIf('save', function () use ($su) { $su->importForm(); $su->save(); }, $csrf_key, ADMIN_URL . 'services/user/?id=' . $su->id_user); $service_user = $su; $tpl->assign(compact('csrf_key', 'service_user')); $tpl->display('services/user/edit.tpl'); | > | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | $form_url = sprintf('edit.php?id=%d&', $su->id()); $create = false; require __DIR__ . '/_form.php'; $form->runIf('save', function () use ($su) { $su->importForm(); $su->updateExpectedAmount(); $su->save(); }, $csrf_key, ADMIN_URL . 'services/user/?id=' . $su->id_user); $service_user = $su; $tpl->assign(compact('csrf_key', 'service_user')); $tpl->display('services/user/edit.tpl'); |
Modified src/www/admin/static/print.css from [4ab93bb3b2] to [fd9783217b].
︙ | ︙ | |||
85 86 87 88 89 90 91 92 93 94 95 96 97 98 | #rapport .parent { background: #ccc; } .noprint { display: none; } td.actions *, nav.tabs, .icn-btn, .pagination, a.icn { display: none !important; } td.num a { border: none; | > > > > | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | #rapport .parent { background: #ccc; } .noprint { display: none; } .print-only { display: unset; } td.actions *, nav.tabs, .icn-btn, .pagination, a.icn { display: none !important; } td.num a { border: none; |
︙ | ︙ |
Modified src/www/admin/static/scripts/accounting.js from [93a05e165e] to [7039cc2124].
1 2 3 4 5 6 7 8 | function initTransactionForm(is_new) { // Advanced transaction: line management var lines = $('.transaction-lines tbody tr'); function initLine(row) { var removeBtn = row.querySelector('button[name="remove_line"]'); removeBtn.onclick = () => { var count = $('.transaction-lines tbody tr').length; | > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | function initTransactionForm(is_new) { var form = $('form')[0]; // Check if an account is listed twice and ask for confirmation form.addEventListener('submit', (e) => { var accounts = []; var lines = $('.transaction-lines tbody tr'); for (var i = 0; i < lines.length; i++) { var a = lines[i].querySelector('.input-list input[type="hidden"]'); if (!a) { continue; } if (accounts.includes(a.value) && !window.confirm(`Attention, cette écriture affecte deux fois le même compte (${a.value}). Confirmer ?`)) { e.preventDefault(); return false; } accounts.push(a.value); } return true; }); // Advanced transaction: line management var lines = $('.transaction-lines tbody tr'); function initLine(row) { var removeBtn = row.querySelector('button[name="remove_line"]'); removeBtn.onclick = () => { var count = $('.transaction-lines tbody tr').length; |
︙ | ︙ |
Modified src/www/admin/static/scripts/code_editor.css from [e8169a1071] to [5b73a86043].
1 2 | .codeEditor { width: 100%; | > < | > | | < < | | < | > > | | | < | | < < < < | < > | < < < < < < < < < < < < < < < < < < < > > > > > > > > > > > > > > > | | | > | > | | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | .codeEditor { min-height: 600px; width: 100%; border: 1px solid var(--gBorderColor); background: var(--gLightBackgroundColor); position: relative; display: block; } .codeEditor .sk_help { background: var(--gLightBorderColor); border-top: .2rem solid var(--gBorderColor); position: absolute; font-size: .9em; left: 0; right: 0; bottom: 0; height: 1.2rem; padding: .3rem 1rem 0; font-family: "Deja Vu Sans Mono", "Droid Sans Mono", "Courier New", Courier, monospace; } .codeEditor .sk_toolbar { border-bottom: .2em solid var(--gBorderColor); display: flex; flex-direction: row; justify-content: space-between; position: absolute; top: 0; left: 0; right: 0; height: 2.5em; padding: 0; } .codeEditor .sk_toolbar button:first-child { font-weight: bold; } .codeEditor .sk_toolbar p { display: inline; padding: .3em .5em; border-radius: .5em; font-size: .9em; margin-left: 2em; } .codeEditor .sk_toolbar button { margin: 4px .5em; } .codeEditor .lineCount, .codeEditor textarea { font-family: "Deja Vu Sans Mono", "Droid Sans Mono", "Courier New", Courier, monospace; font-size: 11pt; line-height: 11pt; } .codeEditor .editor { position: absolute; top: calc(2.7em); height: calc(100% - 4.2em); bottom: calc(1.6em + .4em); left: 0; right: 0; background: #333; } .codeEditor .container { position: relative; display: block; } .codeEditor .lineCount { position: absolute; top: 0; left: 0; bottom: 0; width: 46px; text-align: right; border-right: 2px solid #666; overflow: hidden; color: #999; } .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: #666; color: #fff; } .codeEditor .container { position: absolute; right: 4px; top: 0; bottom: 0; left: 50px; margin: 0; padding: 0; } .codeEditor textarea { height: 100%; width: 100%; padding: 0 0 0 2px; margin: 0; background: transparent; color: #fff; border-radius: none; border: none; overflow: auto; resize: none; box-shadow: none; } .codeEditor.fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; z-index: 100000; } |
Modified src/www/admin/static/scripts/code_editor.js from [8da523e07f] to [863d8d32d6].
1 2 3 4 5 6 7 8 9 10 11 12 13 | (function (){ g.style('scripts/code_editor.css'); g.script('scripts/lib/text_editor.min.js', () => { g.script('scripts/lib/code_editor.min.js', function () { var save_btn = document.querySelector('[name=save]'); 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.", | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | > | > | > | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | (function (){ g.style('scripts/code_editor.css'); g.script('scripts/lib/text_editor.min.js', () => { g.script('scripts/lib/code_editor.min.js', function () { const doc_url = 'https://fossil.kd2.org/paheko/wiki?name=Documentation/'; var save_btn = document.querySelector('[name=save]'); 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 numéro :", no_search_result: "Aucun résultat trouvé." }; code.origValue = code.textarea.value; code.saved = true; code.onlinechange = function () { if (!this.textarea.value.match(/\{\{/)) { return; } if ((p = this.parent.querySelector('nav p')) && this.origValue != code.textarea.value) { toolbar.removeChild(p); } var line = this.getLine(this.current_line); var doc = [{link: 'Brindille', title: 'Brindille'}]; if (match = line.match(/\{\{:(\w+)/)) { doc.push({link: 'Brindille/Fonctions', title: 'Fonction'}); doc.push({link: 'Brindille/Fonctions#'+match[1], title: match[1]}); } else if (match = line.match(/\{\{#(\w+)/)) { doc.push({link: 'Brindille/Sections', title: 'Section'}); doc.push({link: 'Brindille/Sections#'+match[1], title: match[1]}); } else if (match = line.match(/\{\{(select)/)) { doc.push({link: 'Brindille/Sections', title: 'Section'}); doc.push({link: 'Brindille/Sections#'+match[1], title: match[1]}); } else if (match = line.match(/\|(\w+)/)) { doc.push({link: 'Brindille/Filtres', title: 'Filtre'}); doc.push({link: 'Brindille/Filtres#'+match[1], title: match[1]}); } help.innerHTML = 'Documentation'; 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 () { const data = new URLSearchParams(); for (const pair of new FormData(this.textarea.form)) { data.append(pair[0], pair[1]); } data.append('save', 1); this.textarea.form.classList.add('progressing'); fetch(this.textarea.form.action + '&js', { method: 'post', body: data, }).then((response) => response.json()) .then(data => { this.textarea.defaultValue = this.textarea.value; // Show saved let c = document.createElement('p'); c.className = 'block confirm'; c.id = 'confirm_saved'; c.innerText = 'Enregistré'; c.style.left = '-100%'; c.style.opacity = '1'; c.onclick = () => c.remove(); document.querySelector('.codeEditor').appendChild(c); window.setTimeout(() => { c.style.left = ''; this.textarea.form.classList.remove('progressing'); }, 200); window.setTimeout(() => { c.style.opacity = 0; }, 3000); window.setTimeout(() => { c.remove(); }, 5000); }).catch(e => { console.log(e); this.textarea.form.querySelector('[type=submit]').click(); } ); 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 (icon, label, title, action) { var btn = document.createElement('button'); btn.type = 'button'; btn.innerText = label; btn.title = title; if (icon) { btn.setAttribute('data-icon', icon); } btn.onclick = () => { action.call(code); return false; }; toolbar.appendChild(btn); }; appendButton('→', 'Enregistrer', 'Enregistrer les modifications', code.saveFile); appendButton('🗘', 'Recharger', 'Recharger le fichier (effacer les modifications)', code.resetFile); appendButton('🔍', 'Chercher', 'Chercher', code.search); appendButton(null, 'Remplacer', 'Chercher et remplacer', code.searchAndReplace); appendButton(null, 'Aller à la ligne', 'Aller à la ligne', code.goToLine); code.parent.insertBefore(toolbar, code.parent.firstChild); code.shortcuts.push({ctrl: true, key: 's', callback: code.saveFile}); // Cancel Escape to close if (window.parent && window.parent.g.dialog) { |
︙ | ︙ | |||
84 85 86 87 88 89 90 | code.saveFile(); } return false; }; } else { | | | 174 175 176 177 178 179 180 181 182 183 184 185 186 | code.saveFile(); } return false; }; } else { appendButton(null, 'Plein écran', 'Plein écran', code.toggleFullscreen); } g.setParentDialogHeight('90%'); })}); }()); |
Modified src/www/admin/static/scripts/lib/code_editor.min.js from [206f11e50f] to [b93b17e625].
|
| | | 1 | !function(){function t(){}var e;String.prototype.repeat=function(t){return new Array(t+1).join(this)},window.codeEditor=function(t){if(!textEditor.call(this,t))return!1;this.onlinechange=null,this.onlinenumberchange=null,this.fullscreen=!1,this.nb_lines=0,this.current_line=0,this.search_str=null,this.search_pos=0,this.params={indent_size:4,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).init(),this.textarea.spellcheck=!1,this.shortcuts.push({shift:!0,key:"tab",callback:this.indent}),this.shortcuts.push({key:"tab",callback:this.indent}),this.shortcuts.push({ctrl:!0,key:"f",callback:this.search}),this.shortcuts.push({ctrl:!0,key:"h",callback:this.searchAndReplace}),this.shortcuts.push({ctrl:!0,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:"(",callback:this.insertBrackets}),this.shortcuts.push({key:"F11",callback:this.toggleFullscreen}),this.textarea.addEventListener("keypress",this.keyEvent.bind(this),!0),this.textarea.addEventListener("keydown",this.keyEvent.bind(this),!0)},codeEditor.prototype=(e=textEditor.prototype,t.prototype=e,new t),codeEditor.prototype.init=function(){var t=this;for(this.nb_lines=this.countLines(),this.parent=document.createElement("div"),this.parent.className="codeEditor",this.lineCounter=document.createElement("span"),this.lineCounter.className="lineCount",i=1;i<=this.nb_lines;i++)this.lineCounter.innerHTML+="<b>"+i+"</b>";this.lineCounter.innerHTML+="<i>---</i>";var e=document.createElement("div");e.className="editor",e.appendChild(this.lineCounter);var s=document.createElement("div");s.className="container",s.appendChild(this.textarea.cloneNode(!0)),e.appendChild(s),this.parent.appendChild(e);s=this.textarea.parentNode;s.appendChild(this.parent),s.removeChild(this.textarea),this.textarea=this.parent.getElementsByTagName("textarea")[0],this.textarea.wrap="off",this.textarea.style="tab-size: "+this.params.indent_size;e=(this.textarea.value.match(/^\t/gm)||[]).length,s=new RegExp("^[ ]{"+this.params.indent_size+"}","mg"),s=(this.textarea.value.match(s)||[]).length;this.indent_pattern=e<s?" ".repeat(this.params.indent_size):"\t",this.textarea.addEventListener("focus",function(){t.update()},!1),this.textarea.addEventListener("keyup",function(){t.update()},!1),this.textarea.addEventListener("click",function(){t.update()},!1),this.textarea.addEventListener("scroll",function(){t.lineCounter.scrollTop=t.textarea.scrollTop},!1)},codeEditor.prototype.update=function(){var t=this.getSelection(),e=this.getLineNumberFromPosition(t),s=this.countLines();if(this.search_pos=t.end,s!=this.nb_lines){for(var i=this.lineCounter.getElementsByTagName("b"),r=this.nb_lines;s<r;r--)this.lineCounter.removeChild(i[r-1]);for(var n=this.lineCounter.lastChild,r=i.length;r<s;r++){var a=document.createElement("b");a.innerHTML=r+1,this.lineCounter.insertBefore(a,n)}this.nb_lines=s,"function"==typeof this.onlinenumberchange&&this.onlinenumberchange.call(this)}if(e!=this.current_line){for(i=this.lineCounter.getElementsByTagName("b"),r=0;r<this.nb_lines;r++)i[r].className="";i[e].className="current",this.current_line=e,"function"==typeof this.onlinechange&&this.onlinechange.call(this)}},codeEditor.prototype.countLines=function(){var t=this.textarea.value.match(/(\r?\n)/g);return t?t.length+1:1},codeEditor.prototype.getLineNumberFromPosition=function(t){if(0==(t=t||this.getSelection()).start)return 0;t=this.textarea.value.substr(0,t.start).match(/(\r?\n)/g);return t?t.length:0},codeEditor.prototype.getLines=function(){return this.textarea.value.split("\n")},codeEditor.prototype.getLine=function(t){return this.textarea.value.split("\n",t+1)[t]},codeEditor.prototype.getLinePosition=function(t,e){var s=0;for(i=0;i<t.length;i++){if(i==e)return{start:s+i,end:s+t[i].length,length:t[i].length,text:t[i]};s+=t[i].length}return!1},codeEditor.prototype.selectLines=function(t){for(var e=t.start;0<e;e--)if("\n"==this.textarea.value.substr(e,1)){t.start=e+1;break}for(e=t.end-1;e<this.textarea.length;e++)if("\n"==this.textarea.value.substr(e,1)){t.end=e-1;break}return this.setSelection(t.start,t.end),t},codeEditor.prototype.goToLine=function(t){var e=window.prompt(that.params.lang.goto);if(e){e=this.textarea.value.split("\n",parseInt(e,10)).join("\n").length;return this.scrollToSelection(this.setSelection(e,e)),!0}},codeEditor.prototype.indent=function(t,e){var s=this.getSelection(),i=t.shiftKey,r=this.getLines(),n=this.getLineNumberFromPosition(s),a=this.getLinePosition(r,n),t=s.end>a.end;if((0==s.length||!t)&&s.start!=a.start)return this.insertAtPosition(s.start,this.indent_pattern),!0;t=new RegExp("^([ ]{"+this.params.indent_size+"}|\t)*");if(0==s.length&&s.start==a.start){var h=n-1 in r&&r[n-1].match(t);return h=h&&0==a.length?this.indent_pattern.repeat(h.length):this.indent_pattern,this.insertAtPosition(s.start,h),!0}s=this.selectLines(s);h=this.textarea.value.substr(s.start,s.end-s.start),r=h.split("\n");if(i)for(var o=new RegExp("^([ ]{"+this.params.indent_size+"}|\t)"),c=0;c<r.length;c++)r[c]=r[c].replace(o,"");else for(c=0;c<r.length;c++)r[c]=""==r[c].replace(/\s+/,"")?"":this.indent_pattern+r[c];return h=r.join("\n"),this.replaceSelection(s,h),!0},codeEditor.prototype.search=function(){if(this.search_str=window.prompt(this.params.lang.search,this.search_str||""))return this.search_pos=0,this.searchNext()},codeEditor.prototype.searchNext=function(){if(!this.search_str)return!0;var t=this.getSelection(),e=t.end>=this.search_pos?this.search_pos:t.start,s=this.textarea.value.substr(e),i=this.getSearchRegexp(this.search_str),r=s.search(i);if(-1==r)return window.alert(this.params.lang.no_search_result);i=s.match(i);return t.start=e+r,t.end=t.start+i[0].length,t.length=i[0].length,t.text=i[0],this.setSelection(t.start,t.end),this.search_pos=t.end,this.scrollToSelection(t),!0},codeEditor.prototype.getSearchRegexp=function(t,e){var s,i;return t="/"==t.substr(0,1)?(s=t.lastIndexOf("/"),i=t.substr(1,s-1),t.substr(s+1).replace(/g/,"")):(i=t.replace(/([\/$^.?()[\]{}\\])/,"\\$1"),"i"),e&&(t+="g"),new RegExp(i,t)},codeEditor.prototype.searchAndReplace=function(t){var e=this.getSelection(),i=0!=e.length?this.params.lang.search_selection:this.params.lang.search;if(!(s=window.prompt(i,this.search_str||""))||!(r=window.prompt(that.params.lang.replace)))return!0;var n,i=this.getSearchRegexp(s,!0);return 0==e.length?(n=this.textarea.value.match(i).length,this.textarea.value=this.textarea.value.replace(i,r)):(n=e.text.match(i).length,this.replaceSelection(e,e.text.replace(i,r))),window.alert(this.params.lang.replace_result.replace(/%d/g,n)),!0},codeEditor.prototype.enter=function(t){var e=this.getSelection();e.start!=e.end&&(this.replaceSelection(e,""),e=this.getSelection());var s=this.getLineNumberFromPosition(e),i="",r=!1,s=this.getLine(s);return"{"==this.textarea.value.substr(e.start-1,1)&&(i+=this.indent_pattern,r="}"==this.textarea.value.substr(e.start,1)),(match=s.match(/^(\s+)/))&&(i+=match[1]),!!i&&(this.insertAtPosition(e.start,"\n"+i),r&&(r=this.getSelection(),this.insertAtPosition(r.start,"\n"+i.substr(0,i.length-this.indent_pattern.length)),this.setSelection(r.start,r.end)),!0)},codeEditor.prototype.backspace=function(t){var e=this.getSelection();if(0<e.length)return!1;if('""'==(s=this.textarea.value.substr(e.start-1,2))||"''"==s||"{}"==s||"()"==s||"[]"==s)return--e.start,e.end+=1,this.replaceSelection(e,""),!0;var s=this.textarea.value.substr(e.start-20,20);return-1!=(pos=s.search(/([ \t]+)$/))&&(e.start-=20-pos,this.replaceSelection(e,""),!0)},codeEditor.prototype.insertBrackets=function(t,e){var s=this.getSelection(),e=e,i=e;switch(e){case"(":i=")";break;case"[":i="]";break;case"{":i="}"}return 0==s.length?this.insertAtPosition(s.start,e+i,s.start+1):this.wrapSelection(s,e,i),!0},codeEditor.prototype.toggleFullscreen=function(t){for(var e=this.parent.className.split(" "),s=0;s<e.length;s++)if("fullscreen"==e[s])return e.splice(s,1),this.parent.className=e.join(" "),!(this.fullscreen=!1);return e.push("fullscreen"),this.parent.className=e.join(" "),this.fullscreen=!0}}(); |
Deleted src/www/admin/static/scripts/loader.js version [ed5d9f8f69].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/www/admin/static/scripts/wiki_editor.css from [57a2c80c94] to [7c91ec428a].
︙ | ︙ | |||
202 203 204 205 206 207 208 | } form#insertImage .cancel input { font-size: 0.8em; margin: .5em; opacity: 0.8; } | < < < < < < < < | 202 203 204 205 206 207 208 | } form#insertImage .cancel input { font-size: 0.8em; margin: .5em; opacity: 0.8; } |
Modified src/www/admin/static/styles/01-layout.css from [ffa6515ae2] to [03092ed7ba].
︙ | ︙ | |||
9 10 11 12 13 14 15 | --gTextColor: 0, 0, 0; --gBorderColor: #666; --gLightBorderColor: #ccc; --gLightBackgroundColor: #eee; --gLinkColor: blue; --gHoverLinkColor: 127, 0, 0; | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | --gTextColor: 0, 0, 0; --gBorderColor: #666; --gLightBorderColor: #ccc; --gLightBackgroundColor: #eee; --gLinkColor: blue; --gHoverLinkColor: 127, 0, 0; --gMainColor: 32, 120, 122; --gSecondColor: 133, 185, 186; --gBgImage: url("../bg.png"); } /* Dark colors */ html.dark { --gBgColor: 30, 30, 30; --gTextColor: 225, 225, 225; |
︙ | ︙ |
Modified src/www/admin/static/styles/02-common.css from [deb85d27da] to [db410226b1].
︙ | ︙ | |||
632 633 634 635 636 637 638 | max-width: 90%; overflow: auto; margin: 1em auto; background: #eee; padding: .5em; white-space: pre-wrap; } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 | max-width: 90%; overflow: auto; margin: 1em auto; background: #eee; padding: .5em; white-space: pre-wrap; } svg.icon, .icon svg { fill: rgb(var(--gTextColor)); stroke: rgb(var(--gTextColor)); } .print-only { display: none; } .tag { font-size: .8rem; font-weight: normal; background: rgba(var(--gSecondColor), 0.3); border-radius: .5em; padding: .2em .4em; display: inline-block; margin: 0 .2em; } strong.tag { background: rgba(var(--gMainColor), 0.7); color: rgb(var(--gBgColor)); } #confirm_saved { position: absolute; top: 0; left: 0; right: 0; width: 100%; padding: .5em 0; margin: 0; text-align: center; border: 0; transition: all .5s, opacity 2s; } |
Modified src/www/admin/static/styles/03-forms.css from [b9691040d8] to [62bd0fac5d].
︙ | ︙ | |||
219 220 221 222 223 224 225 226 227 228 229 230 231 232 | content: attr(data-icon); font-weight: normal; } [data-icon]:empty:before { padding: 0; } button.main, .icn-btn.main { color: rgb(var(--gTextColor)); font-size: 1.2em; border-radius: 1em; padding: .5em 1em; } | > > > > > > > > > > > > > > > | 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | content: attr(data-icon); font-weight: normal; } [data-icon]:empty:before { padding: 0; } /* Custom SVG icon */ .icn-btn > .icon { display: inline-block; padding-right: .3em; height: 1em; width: 1em; vertical-align: middle; transition: fill .3s, stroke .3s; } .icn-btn:hover > .icon { fill: rgb(var(--gHoverLinkColor)) !important; stroke: rgb(var(--gHoverLinkColor)) !important; } button.main, .icn-btn.main { color: rgb(var(--gTextColor)); font-size: 1.2em; border-radius: 1em; padding: .5em 1em; } |
︙ | ︙ | |||
433 434 435 436 437 438 439 | font-size: .9em; } .actions-center { text-align: center; } | | | 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 | font-size: .9em; } .actions-center { text-align: center; } p.actions { float: right; margin: .5em; } /** Datepicker widget */ .datepicker-parent { position: relative; |
︙ | ︙ |
Modified src/www/admin/static/styles/05-navigation.css from [4983057d4d] to [5019cfe2e7].
︙ | ︙ | |||
130 131 132 133 134 135 136 | padding: .5em; text-align: center; position: relative; border: none; text-decoration: none; } | | < < < < < < < < | 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | padding: .5em; text-align: center; position: relative; border: none; text-decoration: none; } nav.home ul li a::before, nav.home ul li a .icon { display: flex; font-size: 48px; width: 48px; height: 48px; align-items: center; justify-content: center; border-radius: .1em; margin: 0 auto 0 auto; padding: .1em; background: rgba(var(--gSecondColor), .5); margin-bottom: 5px; text-shadow: none; } |
Modified src/www/admin/static/styles/06-tables.css from [7e93f98ea7] to [f36be1f178].
︙ | ︙ | |||
55 56 57 58 59 60 61 62 63 64 65 66 67 68 | background: inherit !important; } table.list tbody tr.checked { color: #633 !important; background: #ffc !important; } table.list .error { color: red; font-weight: bold; } table.list .alert { | > > > > | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | background: inherit !important; } table.list tbody tr.checked { color: #633 !important; background: #ffc !important; } table.list tbody tr.highlight { box-shadow: 0px 0px 5px 5px rgba(var(--gSecondColor), 1); } table.list .error { color: red; font-weight: bold; } table.list .alert { |
︙ | ︙ | |||
191 192 193 194 195 196 197 | width: 1.5em; color: rgba(var(--gMainColor), 0.5); text-shadow: none; transition: color .2s; } table.list td.icon svg { | < < | 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | width: 1.5em; color: rgba(var(--gMainColor), 0.5); text-shadow: none; transition: color .2s; } table.list td.icon svg { max-width: 48px; max-height: 48px; } table.list .folder .icon span::before { color: rgba(var(--gMainColor), 0.9); } |
︙ | ︙ |
Modified src/www/admin/static/styles/10-accounting.css from [23e876dfef] to [b27372cb3b].
︙ | ︙ | |||
86 87 88 89 90 91 92 93 94 95 96 97 98 99 | justify-content: center; align-items: center; } .year-header .forms fieldset { margin: 1em; } .year-infos .graphs { text-align: center; display: flex; flex-wrap: wrap; justify-content: center; } | > > > > > | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | justify-content: center; align-items: center; } .year-header .forms fieldset { margin: 1em; } .year-header figure.logo img { float: left; max-height: 100px; } .year-infos .graphs { text-align: center; display: flex; flex-wrap: wrap; justify-content: center; } |
︙ | ︙ |
Modified src/www/admin/users/log.php from [508c0b5404] to [2d1fa3f851].
1 2 3 4 5 6 7 8 | <?php namespace Garradin; use Garradin\Log; use Garradin\Users\Session; require_once __DIR__ . '/../_inc.php'; | > > > > > | > > > | | | < < | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <?php namespace Garradin; use Garradin\Log; use Garradin\Users\Session; require_once __DIR__ . '/../_inc.php'; $params = []; if ($id = (int)qg('history')) { $params['history'] = $id; } elseif (($id = (int)qg('id')) && $session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)) { $params['id_user'] = $id; } else { $params['id_self'] = Session::getUserId(); if (!$params['id_self']) { throw new UserException('Access forbidden'); } } $tpl->assign('current', isset($params['id_self']) ? 'me' : 'users'); $list = Log::list($params); $list->loadFromQueryString(); $tpl->assign(compact('list', 'params')); $tpl->display('users/log.tpl'); |
Modified tools/make_installer.php from [e16af43e44] to [67ca648488].
︙ | ︙ | |||
16 17 18 19 20 21 22 | namespace KD2 { ##KD2 } namespace { | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | namespace KD2 { ##KD2 } namespace { const WEBSITE = 'https://fossil.kd2.org/paheko/'; const INSTALL_DIR = __DIR__ . '/.install'; echo ' <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> |
︙ | ︙ | |||
91 92 93 94 95 96 97 98 99 | $v = \SQLite3::version(); if (!version_compare($v['versionString'], '3.16', '>=')) { throw new \Exception('SQLite3 version 3.16 ou supérieur requise. Version installée : ' . $v['versionString']); } $step = $_GET['step'] ?? null; @mkdir(INSTALL_DIR); | > | > > > > > > | > > > > > > | | | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | $v = \SQLite3::version(); if (!version_compare($v['versionString'], '3.16', '>=')) { throw new \Exception('SQLite3 version 3.16 ou supérieur requise. Version installée : ' . $v['versionString']); } $step = $_GET['step'] ?? null; $error = null; @mkdir(INSTALL_DIR); $i = new KD2\FossilInstaller(WEBSITE, __DIR__, INSTALL_DIR, '!^paheko-(.*)\.tar\.gz$!'); if ($step == 'download') { $latest = $i->latest(); if (!$latest) { die('</head><h1>Aucune version à télécharger n\'a été trouvée.</h1>'); } $i->download($latest); $next = 'install'; } elseif ($step == 'install') { $latest = $i->latest(); if (!$latest) { die('</head><h1>Aucune version à télécharger n\'a été trouvée.</h1>'); } $i->install($latest); $i->clean($latest); $next = null; } else { $next = 'download'; } echo $next ? '<meta http-equiv="refresh" content="0;url=?step='.$next.'" />' : ''; |
︙ | ︙ |
Deleted tools/make_plugin.php version [a9aa0fc1e3].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |