Overview
Comment:Merge trunk changes into dev
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA3-256: 58d38d4478638f07a85a63bd9f1a649122e0a0377ca657ed308a577e3379216a
User & Date: bohwaz on 2021-04-30 22:35:05
Other Links: branch diff | manifest | tags
Context
2021-04-30
22:40
Schema for 1.2.0 release check-in: 50afc211c6 user: bohwaz tags: dev
22:35
Merge trunk changes into dev check-in: 58d38d4478 user: bohwaz tags: dev
18:58
Update .htaccess to make sure that ErrorDocument is not used, should be OK now that Apache 2.4.15+ is more common check-in: 37de0d1d23 user: bohwaz tags: trunk, stable
2021-04-08
16:28
Fix change of id field check-in: 3bbe7a49e5 user: bohwaz tags: dev
Changes

Name change from src/include/data/1.0.0_schema.sql to archives/1.0.0_schema.sql.

Modified debian/makedeb.sh from [e75e803576] to [d81d35ac7e].

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
    echo "Generating ${CONTROL}..."
    cat <<EOF > ${CONTROL}
Package: ${PACKAGE_DEBNAME}
Section: web
Priority: optional
Maintainer: Garradin <garradin@kd2.org>
Architecture: ${DEB_ARCH_NAME}
Depends: dash | bash, php-cli (>=7.2), php-sqlite3
Version: ${PACKAGE_DEB_VERSION}
Suggests: www-browser, php-gd
Homepage: http://dev.kd2.org/garradin/
Description: Garradin is a tool to manage non-profit organizations.
 It's only available in french.
Description-fr: Gestionnaire d'association en interface web ou CLI.
 Garradin est un gestionnaire d'association à but non lucratif.
 Il permet de gérer les membres, leur adhésion et leurs contributions financières.
 Les membres peuvent se connecter eux-même et modifier leurs informations







|

|







123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
    echo "Generating ${CONTROL}..."
    cat <<EOF > ${CONTROL}
Package: ${PACKAGE_DEBNAME}
Section: web
Priority: optional
Maintainer: Garradin <garradin@kd2.org>
Architecture: ${DEB_ARCH_NAME}
Depends: dash | bash, php-cli (>=7.4), php-sqlite3
Version: ${PACKAGE_DEB_VERSION}
Suggests: www-browser, php-gd, php-imagick, php-intl
Homepage: http://dev.kd2.org/garradin/
Description: Garradin is a tool to manage non-profit organizations.
 It's only available in french.
Description-fr: Gestionnaire d'association en interface web ou CLI.
 Garradin est un gestionnaire d'association à but non lucratif.
 Il permet de gérer les membres, leur adhésion et leurs contributions financières.
 Les membres peuvent se connecter eux-même et modifier leurs informations

Modified src/VERSION from [c471f5d34b] to [97156291a5].

1
1.1.0-rc2
|
1
1.1.4

Modified src/include/data/1.1.0_schema.sql from [3456f2416e] to [26acbef5df].

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
...
164
165
166
167
168
169
170





171
172
173
174
175
176
177
    label TEXT NOT NULL,
    description TEXT NULL,

    amount INTEGER NULL,
    formula TEXT NULL, -- Formule de calcul du montant de la cotisation, si cotisation dynamique (exemple : membres.revenu_imposable * 0.01)

    id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE,
    id_account INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL CHECK (id_account IS NULL OR id_year IS NOT NULL), -- NULL si le type n'est pas associé automatiquement à la compta
    id_year INTEGER NULL REFERENCES acc_years (id) ON DELETE SET NULL -- NULL si le type n'est pas associé automatiquement à la compta
);

CREATE TABLE IF NOT EXISTS services_users
-- Enregistrement des cotisations et activités
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
................................................................................

    closed INTEGER NOT NULL DEFAULT 0,

    id_chart INTEGER NOT NULL REFERENCES acc_charts (id)
);

CREATE INDEX IF NOT EXISTS acc_years_closed ON acc_years (closed);






CREATE TABLE IF NOT EXISTS acc_transactions
-- Opérations comptables
(
    id INTEGER PRIMARY KEY NOT NULL,

    type INTEGER NOT NULL DEFAULT 0, -- Type d'écriture, 0 = avancée (normale)







|
|







 







>
>
>
>
>







59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
...
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
    label TEXT NOT NULL,
    description TEXT NULL,

    amount INTEGER NULL,
    formula TEXT NULL, -- Formule de calcul du montant de la cotisation, si cotisation dynamique (exemple : membres.revenu_imposable * 0.01)

    id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE,
    id_account INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL CHECK (id_account IS NULL OR id_year IS NOT NULL), -- NULL if fee is not linked to accounting, this is reset using a trigger if the year is deleted
    id_year INTEGER NULL REFERENCES acc_years (id) ON DELETE SET NULL -- NULL if fee is not linked to accounting
);

CREATE TABLE IF NOT EXISTS services_users
-- Enregistrement des cotisations et activités
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
................................................................................

    closed INTEGER NOT NULL DEFAULT 0,

    id_chart INTEGER NOT NULL REFERENCES acc_charts (id)
);

CREATE INDEX IF NOT EXISTS acc_years_closed ON acc_years (closed);

-- Make sure id_account is reset when a year is deleted
CREATE TRIGGER IF NOT EXISTS acc_years_delete BEFORE DELETE ON acc_years BEGIN
    UPDATE services_fees SET id_account = NULL, id_year = NULL WHERE id_year = OLD.id;
END;

CREATE TABLE IF NOT EXISTS acc_transactions
-- Opérations comptables
(
    id INTEGER PRIMARY KEY NOT NULL,

    type INTEGER NOT NULL DEFAULT 0, -- Type d'écriture, 0 = avancée (normale)

Added src/include/data/1.1.3_migration.sql version [18eac8e4eb].









>
>
>
>
1
2
3
4
-- Make sure id_account is reset when a year is deleted
CREATE TRIGGER IF NOT EXISTS acc_years_delete BEFORE DELETE ON acc_years BEGIN
    UPDATE services_fees SET id_account = NULL, id_year = NULL WHERE id_year = OLD.id;
END;

Modified src/include/init.php from [feb523c315] to [aae3e95898].

219
220
221
222
223
224
225
















































226
227
228
229
230
231
232

/*
 * Gestion des erreurs et exceptions
 */

class UserException extends \LogicException
{
















































}

class ValidationException extends UserException
{
}

class APIException extends \LogicException







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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

/*
 * Gestion des erreurs et exceptions
 */

class UserException extends \LogicException
{
	protected $details;

	public function setMessage(string $message) {
		$this->message = $message;
	}

	public function setDetails($details) {
		$this->details = $details;
	}

	public function getDetails() {
		return $this->details;
	}

	public function hasDetails(): bool {
		return $this->details !== null;
	}

	public function getDetailsHTML() {
		if (func_num_args() == 1) {
			$details = func_get_arg(0);
		}
		else {
			$details = $this->details;
		}

		if (null === $details) {
			return '<em>(nul)</em>';
		}

		if ($details instanceof \DateTimeInterface) {
			return $details->format('d/m/Y');
		}

		if (!is_array($details)) {
			return nl2br(htmlspecialchars($details));
		}

		$out = '<table>';

		foreach ($details as $key => $value) {
			$out .= sprintf('<tr><th>%s</th><td>%s</td></tr>', htmlspecialchars($key), $this->getDetailsHTML($value));
		}

		$out .= '</table>';

		return $out;
	}
}

class ValidationException extends UserException
{
}

class APIException extends \LogicException

Modified src/include/lib/Garradin/Accounting/Transactions.php from [33dd074d49] to [b8d932894e].

3
4
5
6
7
8
9

10
11
12
13
14
15
16
17
18



19
20
21
22
23
24
25
...
109
110
111
112
113
114
115
116
















117
118
119
120
121
122
123
...
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
...
269
270
271
272
273
274
275
276






277
278
279
280
281
282
283
...
349
350
351
352
353
354
355

356






357
358
359
360
361
362
363
namespace Garradin\Accounting;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Line;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Entities\Accounting\Year;
use KD2\DB\EntityManager;

use Garradin\CSV;
use Garradin\CSV_Custom;
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Utils;
use Garradin\UserException;

class Transactions
{



	const EXPECTED_CSV_COLUMNS_SELF = ['id', 'type', 'status', 'label', 'date', 'notes', 'reference',
		'line_id', 'account', 'credit', 'debit', 'line_reference', 'line_label', 'reconciled'];

	const POSSIBLE_CSV_COLUMNS = [
		'id'             => 'Numéro d\'écriture',
		'label'          => 'Libellé',
		'date'           => 'Date',
................................................................................
	{
		return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id);
	}

	/**
	 * Return all transactions from year
	 */
	static public function export(int $year_id): \Generator
















	{
		$sql = 'SELECT t.id, t.type, t.status, t.label, t.date, t.notes, t.reference,
			l.id AS line_id, a.code AS account, l.debit AS debit, l.credit AS credit,
			l.reference AS line_reference, l.label AS line_label, l.reconciled,
			a2.code AS analytical
			FROM acc_transactions t
			INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
................................................................................
			WHERE t.id_year = ? ORDER BY t.date, t.id, l.id;';

		$res = DB::getInstance()->iterate($sql, $year_id);

		$previous_id = null;

		foreach ($res as $row) {
			if ($previous_id === $row->id) {
				$row->id = $row->type = $row->status = $row->label = $row->date = $row->notes = $row->reference = null;
			}
			else {
				$row->type = Transaction::TYPES_NAMES[$row->type];

				$status = [];

................................................................................

			if (null !== $transaction) {
				$transaction->save();
			}
		}
		catch (UserException $e) {
			$db->rollback();
			throw new UserException(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage()));






		}

		$db->commit();
	}

	static public function importCustom(Year $year, CSV_Custom $csv, int $user_id)
	{
................................................................................
				]);
				$transaction->addLine($line);
				$transaction->save();
			}
		}
		catch (UserException $e) {
			$db->rollback();

			throw new UserException(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage()));






		}

		$db->commit();
	}

	static public function setAnalytical(?int $id_analytical, ?array $transactions = null, ?array $lines = null)
	{







>









>
>
>







 







|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|







 







|
>
>
>
>
>
>







 







>
|
>
>
>
>
>
>







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
...
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
...
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
...
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
...
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
namespace Garradin\Accounting;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Line;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Entities\Accounting\Year;
use KD2\DB\EntityManager;
use Garradin\Config;
use Garradin\CSV;
use Garradin\CSV_Custom;
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Utils;
use Garradin\UserException;

class Transactions
{
	const EXPORT_RAW = 'raw';
	const EXPORT_FULL = 'full';

	const EXPECTED_CSV_COLUMNS_SELF = ['id', 'type', 'status', 'label', 'date', 'notes', 'reference',
		'line_id', 'account', 'credit', 'debit', 'line_reference', 'line_label', 'reconciled'];

	const POSSIBLE_CSV_COLUMNS = [
		'id'             => 'Numéro d\'écriture',
		'label'          => 'Libellé',
		'date'           => 'Date',
................................................................................
	{
		return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id);
	}

	/**
	 * Return all transactions from year
	 */
	static public function export(Year $year, string $format, string $type = self::EXPORT_RAW): void
	{
		$header = null;

		if (self::EXPORT_FULL == $type) {
			$header = ['Numéro', 'Type', 'Statut', 'Libellé', 'Date', 'Remarques', 'Pièce comptable', 'Numéro ligne', 'Compte', 'Débit', 'Crédit', 'Référence ligne', 'Libellé ligne', 'Rapprochement', 'Compte analytique'];
		}

		CSV::export(
			$format,
			sprintf('Export comptable - %s - %s', Config::getInstance()->get('nom_asso'), $year->label),
			self::iterateExport($year->id(), $type),
			$header
		);
	}

	static protected function iterateExport(int $year_id, string $type): \Generator
	{
		$sql = 'SELECT t.id, t.type, t.status, t.label, t.date, t.notes, t.reference,
			l.id AS line_id, a.code AS account, l.debit AS debit, l.credit AS credit,
			l.reference AS line_reference, l.label AS line_label, l.reconciled,
			a2.code AS analytical
			FROM acc_transactions t
			INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
................................................................................
			WHERE t.id_year = ? ORDER BY t.date, t.id, l.id;';

		$res = DB::getInstance()->iterate($sql, $year_id);

		$previous_id = null;

		foreach ($res as $row) {
			if ($previous_id === $row->id && $type == self::EXPORT_RAW) {
				$row->id = $row->type = $row->status = $row->label = $row->date = $row->notes = $row->reference = null;
			}
			else {
				$row->type = Transaction::TYPES_NAMES[$row->type];

				$status = [];

................................................................................

			if (null !== $transaction) {
				$transaction->save();
			}
		}
		catch (UserException $e) {
			$db->rollback();
			$e->setMessage(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage()));

			if (null !== $transaction) {
				$e->setDetails($transaction->asDetailsArray());
			}

			throw $e;
		}

		$db->commit();
	}

	static public function importCustom(Year $year, CSV_Custom $csv, int $user_id)
	{
................................................................................
				]);
				$transaction->addLine($line);
				$transaction->save();
			}
		}
		catch (UserException $e) {
			$db->rollback();

			$e->setMessage(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage()));

			if (null !== $transaction) {
				$e->setDetails($transaction->asDetailsArray());
			}

			throw $e;
		}

		$db->commit();
	}

	static public function setAnalytical(?int $id_analytical, ?array $transactions = null, ?array $lines = null)
	{

Modified src/include/lib/Garradin/CSV.php from [58e95c673d] to [abd4bedd11].

153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
...
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
		if ($header) {
			fputs($fp, self::row($header));
		}

		if (!($iterator instanceof \Iterator) || $iterator->valid()) {
			foreach ($iterator as $row) {
				foreach ($row as $key => &$v) {
					if (is_object($v)&& $v instanceof \DateTimeInterface) {
						$v = $v->format('d/m/Y');
					}
				}

				$row = self::rowToArray($row, $row_map_callback);

				if (!$header)
................................................................................
			throw new UserException('Le fichier ne peut être ouvert');
		}

		// Find the delimiter
		$delim = self::findDelimiter($fp);
		self::skipBOM($fp);

		$line = 1;

		$columns = fgetcsv($fp, 4096, $delim);
		$columns = array_map('trim', $columns);

		// Check for required columns
		foreach ($expected_columns as $column) {
			if (!in_array($column, $columns, true)) {







|







 







|







153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
...
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
		if ($header) {
			fputs($fp, self::row($header));
		}

		if (!($iterator instanceof \Iterator) || $iterator->valid()) {
			foreach ($iterator as $row) {
				foreach ($row as $key => &$v) {
					if (is_object($v) && $v instanceof \DateTimeInterface) {
						$v = $v->format('d/m/Y');
					}
				}

				$row = self::rowToArray($row, $row_map_callback);

				if (!$header)
................................................................................
			throw new UserException('Le fichier ne peut être ouvert');
		}

		// Find the delimiter
		$delim = self::findDelimiter($fp);
		self::skipBOM($fp);

		$line = 0;

		$columns = fgetcsv($fp, 4096, $delim);
		$columns = array_map('trim', $columns);

		// Check for required columns
		foreach ($expected_columns as $column) {
			if (!in_array($column, $columns, true)) {

Modified src/include/lib/Garradin/Config.php from [6ac16c409f] to [977d9aeaa8].

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
..
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
...
151
152
153
154
155
156
157
158
159
160
161
162
163
164

165
166



167
168
169
170
171
172
173
...
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
...
233
234
235
236
237
238
239
240
241
242





243
244
245
246
247
248



249
250
251
252

253
254
255
256
use Garradin\Entities\Files\File;
use Garradin\Membres\Champs;

use KD2\SMTP;

class Config extends Entity
{

	const ADMIN_BACKGROUND_FILENAME = File::CONTEXT_CONFIG . '/admin_bg.png';




	protected $nom_asso;
	protected $adresse_asso;
	protected $email_asso;
	protected $telephone_asso;
	protected $site_asso;

	protected $monnaie;
	protected $pays;

	protected $champs_membres;
	protected $categorie_membres;

	protected $admin_homepage;

	protected $frequence_sauvegardes;
	protected $nombre_sauvegardes;

	protected $champ_identifiant;
	protected $champ_identite;

	protected $last_chart_change;
	protected $last_version_check;

	protected $couleur1;
	protected $couleur2;

	protected $admin_background;



	protected $site_disabled;

	protected $_types = [
		'nom_asso'              => 'string',
		'adresse_asso'          => '?string',
		'email_asso'            => 'string',
................................................................................
		'monnaie'               => 'string',
		'pays'                  => 'string',

		'champs_membres'        => Champs::class,

		'categorie_membres'     => 'int',

		'admin_homepage'        => '?string',

		'frequence_sauvegardes' => '?int',
		'nombre_sauvegardes'    => '?int',

		'champ_identifiant'     => 'string',
		'champ_identite'        => 'string',

		'last_chart_change'     => '?int',
		'last_version_check'    => '?string',

		'couleur1'              => '?string',
		'couleur2'              => '?string',
		'admin_background'      => '?string',



		'site_disabled'         => 'bool',
	];

	static protected $_instance = null;

	static public function getInstance()
................................................................................
		$db->begin();

		foreach ($values as $key => $value)
		{
			$db->preparedQuery('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?);', $key, $value);
		}

		if (!empty($values['champ_identifiant']))
		{
			// Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique
			$db->exec('UPDATE membres SET '.$this->get('champ_identifiant').' = NULL
				WHERE '.$this->get('champ_identifiant').' = "";');

			// Création de l'index unique / FIXME move to Champs

			$db->exec('DROP INDEX IF EXISTS users_id_field;');
			$db->exec('CREATE UNIQUE INDEX users_id_field ON membres ('.$this->get('champ_identifiant').');');



		}

		$db->commit();

		$this->_modified = [];

		return true;
................................................................................
		{
			$source['couleur1'] = null;
			$source['couleur2'] = null;
		}

		if (isset($source['admin_background']) && trim($source['admin_background']) == 'RESET') {
			$source['admin_background'] = null;
		}






		elseif (isset($source['admin_background']) && strlen($source['admin_background'])) {
			$file = Files::get(self::ADMIN_BACKGROUND_FILENAME);

			if ($file) {
				$file->storeFromBase64($source['admin_background']);
			}
			else {
				$file = File::createFromBase64(Utils::dirname(self::ADMIN_BACKGROUND_FILENAME), Utils::basename(self::ADMIN_BACKGROUND_FILENAME), $source['admin_background']);

			}

			$source['admin_background'] = $file->path;
		}




		parent::importForm($source);
	}

	protected function _filterType(string $key, $value)
	{
		switch ($this->_types[$key]) {
................................................................................
	public function selfCheck(): void
	{
		$this->assert(trim($this->nom_asso) != '', 'Le nom de l\'association ne peut rester vide.');
		$this->assert(trim($this->monnaie) != '', 'La monnaie ne peut rester vide.');
		$this->assert(trim($this->pays) != '' && Utils::getCountryName($this->pays), 'Le pays ne peut rester vide.');
		$this->assert(null === $this->site_asso || filter_var($this->site_asso, FILTER_VALIDATE_URL), 'L\'adresse URL du site web est invalide.');
		$this->assert(trim($this->email_asso) != '' && SMTP::checkEmailIsValid($this->email_asso, false), 'L\'adresse e-mail de l\'association est  invalide.');
		$this->assert(strlen($this->admin_homepage) > 0, 'Page d\'accueil invalide');
		$this->assert($this->champs_membres instanceof Champs, 'Objet champs membres invalide');






		$champs = $this->champs_membres;

		$this->assert(!empty($champs->get($this->champ_identite)), sprintf('Le champ spécifié pour identité, "%s" n\'existe pas', $this->champ_identite));
		$this->assert(!empty($champs->get($this->champ_identifiant)), sprintf('Le champ spécifié pour identifiant, "%s" n\'existe pas', $this->champ_identifiant));

		$db = DB::getInstance();



		$sql = sprintf('SELECT (COUNT(DISTINCT LOWER(%s)) = COUNT(*)) FROM membres WHERE %1$s IS NOT NULL AND %1$s != \'\';', $this->champ_identifiant);
		$is_unique = $db->firstColumn($sql);

		$this->assert($is_unique, sprintf('Le champ "%s" comporte des doublons et ne peut donc pas servir comme identifiant unique de connexion.', $this->champ_identifiant));


		$this->assert($db->test('users_categories', 'id = ?', $this->categorie_membres), 'Catégorie de membres inconnue');
	}
}







>
|
>
>
>













<
<













>
>







 







<
<












>
>







 







|
<
<
<
<
<
<
>

<
>
>
>







 







|
>
>
>
>
>
>

|





|
>




>
>
>







 







<


>
>
>
>
>






>
>
>
|
|

|
>




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
..
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
...
155
156
157
158
159
160
161
162






163
164

165
166
167
168
169
170
171
172
173
174
...
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
...
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
use Garradin\Entities\Files\File;
use Garradin\Membres\Champs;

use KD2\SMTP;

class Config extends Entity
{
	const DEFAULT_FILES = [
		'admin_background' => File::CONTEXT_CONFIG . '/admin_bg.png',
		'admin_homepage' => File::CONTEXT_CONFIG . '/admin_homepage.skriv',
		'admin_css' => File::CONTEXT_CONFIG . '/admin.css',
	];

	protected $nom_asso;
	protected $adresse_asso;
	protected $email_asso;
	protected $telephone_asso;
	protected $site_asso;

	protected $monnaie;
	protected $pays;

	protected $champs_membres;
	protected $categorie_membres;



	protected $frequence_sauvegardes;
	protected $nombre_sauvegardes;

	protected $champ_identifiant;
	protected $champ_identite;

	protected $last_chart_change;
	protected $last_version_check;

	protected $couleur1;
	protected $couleur2;

	protected $admin_background;
	protected $admin_homepage = 'config/admin_homepage.skriv';
	protected $admin_css = 'config/admin.css';

	protected $site_disabled;

	protected $_types = [
		'nom_asso'              => 'string',
		'adresse_asso'          => '?string',
		'email_asso'            => 'string',
................................................................................
		'monnaie'               => 'string',
		'pays'                  => 'string',

		'champs_membres'        => Champs::class,

		'categorie_membres'     => 'int',



		'frequence_sauvegardes' => '?int',
		'nombre_sauvegardes'    => '?int',

		'champ_identifiant'     => 'string',
		'champ_identite'        => 'string',

		'last_chart_change'     => '?int',
		'last_version_check'    => '?string',

		'couleur1'              => '?string',
		'couleur2'              => '?string',
		'admin_background'      => '?string',
		'admin_homepage'        => '?string',
		'admin_css'             => '?string',

		'site_disabled'         => 'bool',
	];

	static protected $_instance = null;

	static public function getInstance()
................................................................................
		$db->begin();

		foreach ($values as $key => $value)
		{
			$db->preparedQuery('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?);', $key, $value);
		}

		if (!empty($values['champ_identifiant'])) {






			// Regenerate login index
			$db->exec('DROP INDEX IF EXISTS users_id_field;');

			$config = Config::getInstance();
			$champs = $config->get('champs_membres');
			$champs->createIndexes();
		}

		$db->commit();

		$this->_modified = [];

		return true;
................................................................................
		{
			$source['couleur1'] = null;
			$source['couleur2'] = null;
		}

		if (isset($source['admin_background']) && trim($source['admin_background']) == 'RESET') {
			$source['admin_background'] = null;

			$file = Files::get(self::DEFAULT_FILES['admin_background']);

			if ($file) {
				$file->delete();
			}
		}
		elseif (isset($source['admin_background']) && strlen($source['admin_background'])) {
			$file = Files::get(self::DEFAULT_FILES['admin_background']);

			if ($file) {
				$file->storeFromBase64($source['admin_background']);
			}
			else {
				$path = self::DEFAULT_FILES['admin_background'];
				$file = File::createFromBase64(Utils::dirname($path), Utils::basename($path), $source['admin_background']);
			}

			$source['admin_background'] = $file->path;
		}
		else {
			unset($source['admin_background']);
		}

		parent::importForm($source);
	}

	protected function _filterType(string $key, $value)
	{
		switch ($this->_types[$key]) {
................................................................................
	public function selfCheck(): void
	{
		$this->assert(trim($this->nom_asso) != '', 'Le nom de l\'association ne peut rester vide.');
		$this->assert(trim($this->monnaie) != '', 'La monnaie ne peut rester vide.');
		$this->assert(trim($this->pays) != '' && Utils::getCountryName($this->pays), 'Le pays ne peut rester vide.');
		$this->assert(null === $this->site_asso || filter_var($this->site_asso, FILTER_VALIDATE_URL), 'L\'adresse URL du site web est invalide.');
		$this->assert(trim($this->email_asso) != '' && SMTP::checkEmailIsValid($this->email_asso, false), 'L\'adresse e-mail de l\'association est  invalide.');

		$this->assert($this->champs_membres instanceof Champs, 'Objet champs membres invalide');

		// Files can only have one value: their name
		$this->assert($this->admin_background === null || $this->admin_background === self::DEFAULT_FILES['admin_background']);
		$this->assert($this->admin_homepage === null || $this->admin_homepage === self::DEFAULT_FILES['admin_homepage']);
		$this->assert($this->admin_css === null || $this->admin_css === self::DEFAULT_FILES['admin_css']);

		$champs = $this->champs_membres;

		$this->assert(!empty($champs->get($this->champ_identite)), sprintf('Le champ spécifié pour identité, "%s" n\'existe pas', $this->champ_identite));
		$this->assert(!empty($champs->get($this->champ_identifiant)), sprintf('Le champ spécifié pour identifiant, "%s" n\'existe pas', $this->champ_identifiant));

		$db = DB::getInstance();

		// Check that this field is actually unique
		if (isset($this->_modified['champ_identifiant'])) {
			$sql = sprintf('SELECT (COUNT(DISTINCT %s COLLATE NOCASE) = COUNT(*)) FROM membres WHERE %1$s IS NOT NULL AND %1$s != \'\';', $this->champ_identifiant);
			$is_unique = (bool) $db->firstColumn($sql);

			$this->assert($is_unique, sprintf('Le champ "%s" comporte des doublons et ne peut donc pas servir comme identifiant unique de connexion.', $this->champ_identifiant));
		}

		$this->assert($db->test('users_categories', 'id = ?', $this->categorie_membres), 'Catégorie de membres inconnue');
	}
}

Modified src/include/lib/Garradin/Entities/Accounting/Line.php from [c9978cd52a] to [a9f6e82800].

2
3
4
5
6
7
8

9
10
11
12
13
14
15
..
65
66
67
68
69
70
71
72












namespace Garradin\Entities\Accounting;

use Garradin\DB;
use Garradin\Entity;
use Garradin\ValidationException;
use Garradin\Utils;


class Line extends Entity
{
	const TABLE = 'acc_transactions_lines';

	protected $id;
	protected $id_transaction;
................................................................................

		$db = DB::getInstance();
		$this->assert($db->firstColumn('SELECT 1 FROM acc_accounts a
			INNER JOIN acc_transactions t ON t.id = ?
			INNER JOIN acc_years y ON y.id = t.id_year
			WHERE a.id = ? AND a.id_chart = y.id_chart;', $this->id_transaction, $this->id_account), 'Le compte sélectionné ne correspond pas à l\'exercice');
	}
}


















>







 







|
>
>
>
>
>
>
>
>
>
>
>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

namespace Garradin\Entities\Accounting;

use Garradin\DB;
use Garradin\Entity;
use Garradin\ValidationException;
use Garradin\Utils;
use Garradin\Accounting\Accounts;

class Line extends Entity
{
	const TABLE = 'acc_transactions_lines';

	protected $id;
	protected $id_transaction;
................................................................................

		$db = DB::getInstance();
		$this->assert($db->firstColumn('SELECT 1 FROM acc_accounts a
			INNER JOIN acc_transactions t ON t.id = ?
			INNER JOIN acc_years y ON y.id = t.id_year
			WHERE a.id = ? AND a.id_chart = y.id_chart;', $this->id_transaction, $this->id_account), 'Le compte sélectionné ne correspond pas à l\'exercice');
	}

	public function asDetailsArray(): array
	{
		return [
			'Compte'    => $this->id_account ? Accounts::getSelectorLabel($this->id_account) : null,
			'Libellé'   => $this->label,
			'Référence' => $this->reference,
			'Crédit'    => Utils::money_format($this->credit),
			'Débit'     => Utils::money_format($this->debit),
		];
	}
}

Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [d39e3b122c] to [f45c5b736f].

198
199
200
201
202
203
204











205
206
207
208
209
210
211
...
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
...
763
764
765
766
767
768
769
770




















		foreach ($this->getLines() as $line) {
			$sum += $line->credit;
		}

		return $sum;
	}












	public function getAnalyticalId(): ?int
	{
		$lines = $this->getLines();

		if (!count($lines)) {
			return null;
................................................................................

		return parent::delete();
	}

	public function selfCheck(): void
	{
		parent::selfCheck();

		$db = DB::getInstance();

		// ID d'exercice obligatoire
		if (null === $this->id_year) {
			throw new \LogicException('Aucun exercice spécifié.');


		}

		if (!$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')))
		{
			throw new ValidationException('La date ne correspond pas à l\'exercice sélectionné : ' . $this->date->format('d/m/Y'));
		}

		$total = 0;

		$lines = $this->getLines();

		foreach ($lines as $line) {
			$total += $line->credit;
			$total -= $line->debit;
		}

		if (0 !== $total) {
			throw new ValidationException(sprintf('Écriture non équilibrée : déséquilibre (%s) entre débits et crédits', Utils::money_format($total)));
		}

		if (!array_key_exists($this->type, self::TYPES_NAMES)) {
			throw new ValidationException('Type d\'écriture inconnu : ' . $this->type);
		}
	}

	public function importFromDepositForm(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}
................................................................................
		return $out;
	}

	public function getTypeName(): string
	{
		return self::TYPES_NAMES[$this->type];
	}
}


























>
>
>
>
>
>
>
>
>
>
>







 







<


<
<
|
>
>
|
<
|
|
|
<










<
|
<
<
<
<
<







 







|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
...
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
...
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

		foreach ($this->getLines() as $line) {
			$sum += $line->credit;
		}

		return $sum;
	}

	public function getLinesDebitSum()
	{
		$sum = 0;

		foreach ($this->getLines() as $line) {
			$sum += $line->debit;
		}

		return $sum;
	}

	public function getAnalyticalId(): ?int
	{
		$lines = $this->getLines();

		if (!count($lines)) {
			return null;
................................................................................

		return parent::delete();
	}

	public function selfCheck(): void
	{
		parent::selfCheck();

		$db = DB::getInstance();



		$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('membres', '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'));

		$this->assert($is_in_year, 'La date ne correspond pas à l\'exercice sélectionné : ' . $this->date->format('d/m/Y'));


		$total = 0;

		$lines = $this->getLines();

		foreach ($lines as $line) {
			$total += $line->credit;
			$total -= $line->debit;
		}


		$this->assert(0 === $total, sprintf('Écriture non équilibrée : déséquilibre (%s) entre débits et crédits', Utils::money_format($total)));





	}

	public function importFromDepositForm(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}
................................................................................
		return $out;
	}

	public function getTypeName(): string
	{
		return self::TYPES_NAMES[$this->type];
	}

	public function asDetailsArray(): array
	{
		$lines = [];

		foreach ($this->getLines() as $line) {
			$lines[] = $line->asDetailsArray();
		}

		return [
			'Libellé'         => $this->label,
			'Date'            => $this->date,
			'Pièce comptable' => $this->reference,
			'Remarques'       => $this->notes,
			'Total crédit'    => Utils::money_format($this->getLinesCreditSum()),
			'Total débit'     => Utils::money_format($this->getLinesDebitSum()),
			'Lignes'          => $lines,
		];
	}
}

Modified src/include/lib/Garradin/Entities/Files/File.php from [1cfc3e1511] to [cfc7cdd122].

185
186
187
188
189
190
191






192
193
194
195

196
197
198
199
200
201
202
203
204
...
421
422
423
424
425
426
427

428

429
430
431
432
433
434
435
		return true;
	}

	public function move(string $target): bool
	{
		return $this->rename($target . '/' . $this->name);
	}







	public function rename(string $new_path): bool
	{
		self::validatePath($new_path);


		if ($new_path == $this->path || 0 === strpos($new_path, $this->path)) {
			throw new UserException('Impossible de renommer ou déplacer un fichier vers lui-même');
		}

		$return = Files::callStorage('move', $this, $new_path);

		Plugin::fireSignal('files.move', ['file' => $this, 'new_path' => $new_path]);

................................................................................

	/**
	 * Modify a file from an encoded base64 string
	 */
	public function storeFromBase64(string $encoded_content): self
	{
		$content = base64_decode($encoded_content);

		return $this->store(null, $content);

	}

	/**
	 * Upload du fichier par POST
	 */
	static public function upload(string $path, string $key): self
	{







>
>
>
>
>
>




>

|







 







>
|
>







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
...
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
		return true;
	}

	public function move(string $target): bool
	{
		return $this->rename($target . '/' . $this->name);
	}

	public function changeFileName(string $new_name): bool
	{
		$new_name = self::filterName($new_name);
		return $this->rename(ltrim($this->parent . '/' . $new_name, '/'));
	}

	public function rename(string $new_path): bool
	{
		self::validatePath($new_path);
		self::validateFileName(Utils::basename($new_path));

		if ($new_path == $this->path || 0 === strpos($new_path . '/', $this->path . '/')) {
			throw new UserException('Impossible de renommer ou déplacer un fichier vers lui-même');
		}

		$return = Files::callStorage('move', $this, $new_path);

		Plugin::fireSignal('files.move', ['file' => $this, 'new_path' => $new_path]);

................................................................................

	/**
	 * Modify a file from an encoded base64 string
	 */
	public function storeFromBase64(string $encoded_content): self
	{
		$content = base64_decode($encoded_content);
		$this->set('modified', new \DateTime);
		$this->store(null, $content);
		return $this;
	}

	/**
	 * Upload du fichier par POST
	 */
	static public function upload(string $path, string $key): self
	{

Modified src/include/lib/Garradin/Entities/Web/Page.php from [0aec54d411] to [c5b7261d79].

79
80
81
82
83
84
85
86
87
88
89
90
91
92


93
94
95
96
97
98
99
...
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
		$page = new self;
		$data = compact('type', 'parent', 'title', 'status');
		$data['content'] = '';

		$page->importForm($data);
		$page->published = new \DateTime;
		$page->modified = new \DateTime;
		$page->file_path = $page->filepath();
		$page->type = $type;

		$db = DB::getInstance();
		if ($db->test(self::TABLE, 'uri = ?', $page->uri)) {
			$page->importForm(['uri' => $page->uri . date('-Y-m-d-His')]);
		}



		return $page;
	}

	public function file(bool $force_reload = false)
	{
		if (null === $this->_file || $force_reload) {
................................................................................
	}

	public function path(): string
	{
		return $this->path;
	}

	public function syncFile(): void
	{
		$export = $this->export();
		$path = $this->filepath();








		$target = $this->filepath(false);

		// Move parent directory if needed
		if ($path !== $target) {
			$dir = Files::get(Utils::dirname($path));
			$dir->rename(Utils::dirname($target));
			$this->set('file_path', $target);
			$this->_file = null;
		}

		if (!$this->file()) {
			$file = $this->_file = File::createAndStore(Utils::dirname($target), Utils::basename($target), null, $export);
			$this->set('modified', new \DateTime);
		}

		elseif ($this->file()->fetch() !== $export) {
			$file = $this->file();
			$file->set('modified', new \DateTime);
			$this->set('modified', clone $file->modified);
			$file->store(null, $this->export());

		}

		$this->syncSearch();
	}

	public function syncSearch(): void
	{
		$content = $this->format == self::FORMAT_ENCRYPTED ? null : strip_tags($this->render());
		$this->file()->indexForSearch(null, $content, $this->title);
	}

	public function save(): bool
	{
		$this->syncFile();




		parent::save();


		return true;
	}

	public function delete(): bool
	{
		Files::get(Utils::dirname($this->file_path))->delete();







<






>
>







 







|


<
>
>
>
>
>
>
>
>
|

|
|
|
|
<
|
|

|
<
<
|
>
|
<
<
<
|
>













|
>
>
>
>

>







79
80
81
82
83
84
85

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
...
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
		$page = new self;
		$data = compact('type', 'parent', 'title', 'status');
		$data['content'] = '';

		$page->importForm($data);
		$page->published = new \DateTime;
		$page->modified = new \DateTime;

		$page->type = $type;

		$db = DB::getInstance();
		if ($db->test(self::TABLE, 'uri = ?', $page->uri)) {
			$page->importForm(['uri' => $page->uri . date('-Y-m-d-His')]);
		}

		$page->file_path = $page->filepath(false);

		return $page;
	}

	public function file(bool $force_reload = false)
	{
		if (null === $this->_file || $force_reload) {
................................................................................
	}

	public function path(): string
	{
		return $this->path;
	}

	public function syncFile(string $path): void
	{
		$export = $this->export();


		$exists = Files::callStorage('exists', $path);

		// Create file if required
		if (!$exists) {
			$file = $this->_file = File::createAndStore(Utils::dirname($path), Utils::basename($path), null, $export);
		}
		else {
			$target = $this->filepath(false);

			// Move parent directory if needed
			if ($path !== $target) {
				$dir = Files::get(Utils::dirname($path));
				$dir->rename(Utils::dirname($target));

				$this->_file = null;
			}

			$file = $this->file();



			// Or update file
			if ($file->fetch() !== $export) {



				$file->store(null, $export);
			}
		}

		$this->syncSearch();
	}

	public function syncSearch(): void
	{
		$content = $this->format == self::FORMAT_ENCRYPTED ? null : strip_tags($this->render());
		$this->file()->indexForSearch(null, $content, $this->title);
	}

	public function save(): bool
	{
		if (isset($this->_modified['uri']) || isset($this->_modified['path'])) {
			$this->set('file_path', $this->filepath(false));
		}

		$current_path = $this->_modified['file_path'] ?? $this->file_path;
		parent::save();
		$this->syncFile($current_path);

		return true;
	}

	public function delete(): bool
	{
		Files::get(Utils::dirname($this->file_path))->delete();

Modified src/include/lib/Garradin/Files/Files.php from [97e4ccbc5c] to [a99c4bec9a].

12
13
14
15
16
17
18





19
20
21
22
23
24
25
...
241
242
243
244
245
246
247




248
249
250
251
252
253
254











use KD2\DB\EntityManager as EM;

use const Garradin\{FILE_STORAGE_BACKEND, FILE_STORAGE_QUOTA, FILE_STORAGE_CONFIG};

class Files
{





	static public function search(string $search, string $path = null): array
	{
		if (strlen($search) > 100) {
			throw new ValidationException('Recherche trop longue : maximum 100 caractères');
		}

		$where = '';
................................................................................
		}

		return self::callStorage('getRemainingQuota');
	}

	static public function checkQuota(int $size = 0): void
	{




		$remaining = self::getRemainingQuota(true);

		if (($remaining - $size) < 0) {
			throw new ValidationException('L\'espace disque est insuffisant pour réaliser cette opération');
		}
	}
}

















>
>
>
>
>







 







>
>
>
>






|
>
>
>
>
>
>
>
>
>
>
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
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

use KD2\DB\EntityManager as EM;

use const Garradin\{FILE_STORAGE_BACKEND, FILE_STORAGE_QUOTA, FILE_STORAGE_CONFIG};

class Files
{
	/**
	 * To enable or disable quota check
	 */
	static protected $quota = true;

	static public function search(string $search, string $path = null): array
	{
		if (strlen($search) > 100) {
			throw new ValidationException('Recherche trop longue : maximum 100 caractères');
		}

		$where = '';
................................................................................
		}

		return self::callStorage('getRemainingQuota');
	}

	static public function checkQuota(int $size = 0): void
	{
		if (!self::$quota) {
			return;
		}

		$remaining = self::getRemainingQuota(true);

		if (($remaining - $size) < 0) {
			throw new ValidationException('L\'espace disque est insuffisant pour réaliser cette opération');
		}
	}

	static public function enableQuota(): void
	{
		self::$quota = true;
	}

	static public function disableQuota(): void
	{
		self::$quota = false;
	}
}

Modified src/include/lib/Garradin/Files/Storage/SQLite.php from [1733617c6f] to [0ae2ccbb40].

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
	static public function list(string $path): array
	{
		return EM::getInstance(File::class)->all('SELECT * FROM @TABLE WHERE parent = ? ORDER BY type DESC, name COLLATE NOCASE ASC;', $path);
	}

	static public function exists(string $path): bool
	{
		return DB::getInstance()->test('files', 'path = ? AND name = ?', Utils::dirname($path), Utils::basename($path));
	}

	static public function delete(File $file): bool
	{
		$db = DB::getInstance();

		$cache_id = 'files.' . $file->pathHash();







|







123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
	static public function list(string $path): array
	{
		return EM::getInstance(File::class)->all('SELECT * FROM @TABLE WHERE parent = ? ORDER BY type DESC, name COLLATE NOCASE ASC;', $path);
	}

	static public function exists(string $path): bool
	{
		return DB::getInstance()->test('files', 'path = ?', $path);
	}

	static public function delete(File $file): bool
	{
		$db = DB::getInstance();

		$cache_id = 'files.' . $file->pathHash();

Modified src/include/lib/Garradin/Form.php from [e04e8790f9] to [7d0dc0a0a9].

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

				Utils::redirect($redirect);
			}

			return true;
		}
		catch (UserException $e) {
			$this->addError($e->getMessage());
			return false;
		}
	}

	public function runIf($condition, callable $fn, ?string $csrf_key = null, ?string $redirect = null): ?bool
	{
		if (is_string($condition) && empty($_POST[$condition])) {







|







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

				Utils::redirect($redirect);
			}

			return true;
		}
		catch (UserException $e) {
			$this->addError($e);
			return false;
		}
	}

	public function runIf($condition, callable $fn, ?string $csrf_key = null, ?string $redirect = null): ?bool
	{
		if (is_string($condition) && empty($_POST[$condition])) {

Modified src/include/lib/Garradin/Install.php from [87cbe58842] to [3dd5922cef].

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
143
144
145
146
147
148
149
150
151
152


153
154
155
156
157
158
159
 * Pour procéder à l'installation de l'instance Garradin
 * Utile pour automatiser l'installation sans passer par la page d'installation
 */
class Install
{
	static public function reset(Membres\Session $session, $password, array $options = [])
	{
		$config = (object) Config::getInstance()->getConfig();
		$user = $session->getUser();

		if (!$session->checkPassword($password, $user->passe))
		{
			throw new UserException('Le mot de passe ne correspond pas.');
		}

................................................................................
			'id_category' => $cat->id(),
			'nom'         => $user_name,
			'email'       => $user_email,
			'passe'       => $user_password,
			'pays'        => 'FR',
		]);

		$welcome_text = $welcome_text ?? sprintf("Bienvenue dans l'administration de %s !\n\nUtilisez le menu à gauche pour accéder aux différentes sections.", $name);

		$file = File::createAndStore(File::CONTEXT_CONFIG, 'admin_homepage.skriv', null, $welcome_text);


		$config->set('admin_homepage', $file->path);

        // Import accounting chart
        $chart = new Chart;
        $chart->label = 'Plan comptable associatif 2020 (Règlement ANC n°2018-06)';
        $chart->country = 'FR';
        $chart->code = 'PCA2018';







|







 







|

|
>
>







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
 * Pour procéder à l'installation de l'instance Garradin
 * Utile pour automatiser l'installation sans passer par la page d'installation
 */
class Install
{
	static public function reset(Membres\Session $session, $password, array $options = [])
	{
		$config = (object) Config::getInstance()->asArray();
		$user = $session->getUser();

		if (!$session->checkPassword($password, $user->passe))
		{
			throw new UserException('Le mot de passe ne correspond pas.');
		}

................................................................................
			'id_category' => $cat->id(),
			'nom'         => $user_name,
			'email'       => $user_email,
			'passe'       => $user_password,
			'pays'        => 'FR',
		]);

		$welcome_text = $welcome_text ?? sprintf("Bienvenue dans l'administration de %s !\n\nUtilisez le menu à gauche pour accéder aux différentes sections.\n\nCe message peut être modifié dans la 'Configuration'.", $name);

		$path = Config::DEFAULT_FILES['admin_homepage'];

		$file = File::createAndStore(Utils::dirname($path), Utils::basename($path), null, $welcome_text);
		$config->set('admin_homepage', $file->path);

        // Import accounting chart
        $chart = new Chart;
        $chart->label = 'Plan comptable associatif 2020 (Règlement ANC n°2018-06)';
        $chart->country = 'FR';
        $chart->code = 'PCA2018';

Modified src/include/lib/Garradin/Membres.php from [0cee6f72d9] to [927864c538].

209
210
211
212
213
214
215




216
217
218
219
220
221
222
223

            if ($db->test('membres', 'numero = ? AND id != ?', (int)$data['numero'], $id))
            {
                throw new UserException('Ce numéro est déjà attribué à un autre membre.');
            }
        }





        if (!empty($data['passe']) && trim($data['passe']))
        {
            Session::checkPasswordValidity($data['passe']);
            $data['passe'] = Session::hashPassword($data['passe']);
        }
        else
        {
            unset($data['passe']);







>
>
>
>
|







209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227

            if ($db->test('membres', 'numero = ? AND id != ?', (int)$data['numero'], $id))
            {
                throw new UserException('Ce numéro est déjà attribué à un autre membre.');
            }
        }

        if (isset($data['delete_password'])) {
            $data['passe'] = null;
            unset($data['delete_password']);
        }
        elseif (!empty($data['passe']) && trim($data['passe']))
        {
            Session::checkPasswordValidity($data['passe']);
            $data['passe'] = Session::hashPassword($data['passe']);
        }
        else
        {
            unset($data['passe']);

Modified src/include/lib/Garradin/Membres/Champs.php from [8b03eb3d59] to [8c67451d61].

608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
...
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
            $collation = '';

            if ($this->isText($id_field)) {
                $collation = ' COLLATE NOCASE';
            }

            // Création de l'index unique
            $db->exec(sprintf('CREATE UNIQUE INDEX users_id_field ON %s (%s%s);', $table_name, $id_field, $collation));
        }

        $db->exec(sprintf('CREATE UNIQUE INDEX user_number ON %s (numero);', $table_name));
        $db->exec(sprintf('CREATE INDEX users_category ON %s (id_category);', $table_name));

        // Create index on listed columns
        // FIXME: these indexes are currently unused by SQLite in the default user list
        // when there is more than one non-hidden category, as this makes SQLite merge multiple results
        // and so the index is not useful in that case sadly.
        // EXPLAIN QUERY PLAN SELECT * FROM membres WHERE "id_category" IN (3) ORDER BY "nom" ASC LIMIT 0,100;
        // --> SEARCH TABLE membres USING INDEX users_list_nom (id_category=?)
................................................................................

            $collation = '';

            if ($this->isText($field)) {
                $collation = ' COLLATE NOCASE';
            }

            $db->exec(sprintf('CREATE INDEX users_list_%s ON %s (id_category, %1$s%s);', $field, $table_name, $collation));
        }
    }

    /**
     * Enregistre les changements de champs en base de données
     * @return boolean true
     */







|


|
|







 







|







608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
...
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
            $collation = '';

            if ($this->isText($id_field)) {
                $collation = ' COLLATE NOCASE';
            }

            // Création de l'index unique
            $db->exec(sprintf('CREATE UNIQUE INDEX IF NOT EXISTS users_id_field ON %s (%s%s);', $table_name, $id_field, $collation));
        }

        $db->exec(sprintf('CREATE UNIQUE INDEX IF NOT EXISTS user_number ON %s (numero);', $table_name));
        $db->exec(sprintf('CREATE INDEX IF NOT EXISTS users_category ON %s (id_category);', $table_name));

        // Create index on listed columns
        // FIXME: these indexes are currently unused by SQLite in the default user list
        // when there is more than one non-hidden category, as this makes SQLite merge multiple results
        // and so the index is not useful in that case sadly.
        // EXPLAIN QUERY PLAN SELECT * FROM membres WHERE "id_category" IN (3) ORDER BY "nom" ASC LIMIT 0,100;
        // --> SEARCH TABLE membres USING INDEX users_list_nom (id_category=?)
................................................................................

            $collation = '';

            if ($this->isText($field)) {
                $collation = ' COLLATE NOCASE';
            }

            $db->exec(sprintf('CREATE INDEX IF NOT EXISTS users_list_%s ON %s (id_category, %1$s%s);', $field, $table_name, $collation));
        }
    }

    /**
     * Enregistre les changements de champs en base de données
     * @return boolean true
     */

Modified src/include/lib/Garradin/Membres/Session.php from [af7b1cc2aa] to [08959104e6].

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

		return parent::isPasswordCompromised($password);
	}

	protected function getUserForLogin($login)
	{
		$champ_id = Config::getInstance()->get('champ_identifiant');
		$login = strtolower($login);

		// Ne renvoie un membre que si celui-ci a le droit de se connecter
		$query = 'SELECT m.id, m.%1$s AS login, m.passe AS password, m.secret_otp AS otp_secret
			FROM membres AS m
			INNER JOIN users_categories AS c ON c.id = m.id_category
			WHERE m.%1$s = ? AND c.perm_connect >= %2$d
			LIMIT 1;';

		$query = sprintf($query, $champ_id, self::ACCESS_READ);

		return $this->db->first($query, $login);
	}








<





|







95
96
97
98
99
100
101

102
103
104
105
106
107
108
109
110
111
112
113
114

		return parent::isPasswordCompromised($password);
	}

	protected function getUserForLogin($login)
	{
		$champ_id = Config::getInstance()->get('champ_identifiant');


		// Ne renvoie un membre que si celui-ci a le droit de se connecter
		$query = 'SELECT m.id, m.%1$s AS login, m.passe AS password, m.secret_otp AS otp_secret
			FROM membres AS m
			INNER JOIN users_categories AS c ON c.id = m.id_category
			WHERE m.%1$s = ? COLLATE NOCASE AND c.perm_connect >= %2$d
			LIMIT 1;';

		$query = sprintf($query, $champ_id, self::ACCESS_READ);

		return $this->db->first($query, $login);
	}

Modified src/include/lib/Garradin/Sauvegarde.php from [df94c55305] to [b14c3f16f0].

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
...
461
462
463
464
465
466
467

468
469
470








471
472
473
474
475
476
477
478
479
480
481
		$this->make($backup);

		return basename($backup);
	}

	protected function make(string $dest)
	{
		// Acquire lock // FIXME use ::backup PHP 7.4.0+ is required
		// FIXME: use VACUUM INTO instead when SQLite 3.27+ is required

		$db = DB::getInstance();
		$db->exec('BEGIN IMMEDIATE TRANSACTION;');

		copy(DB_FILE, $dest);




		$db->exec('END TRANSACTION;');
		unset($db);




		$db = new \SQLite3($dest, \SQLITE3_OPEN_READWRITE);


		$db->exec('PRAGMA journal_mode = DELETE;');
		$db->exec('VACUUM;');
		$db->close();

	}

	/**
	 * Effectue une rotation des sauvegardes automatiques
	 * association.auto.1.sqlite deviendra association.auto.2.sqlite par exemple
	 */
	public function rotate(): void
................................................................................
		$appid = $db->querySingle('PRAGMA application_id;', false);

		if ($appid !== DB::APPID)
		{
			throw new UserException('Ce fichier n\'est pas une sauvegarde Garradin (application_id ne correspond pas).', self::NO_APP_ID);
		}


		if ($user_id)
		{
			// Empêchons l'admin de se tirer une balle dans le pied








			$is_still_admin = $db->querySingle('SELECT 1 FROM users_categories
				WHERE id = (SELECT id_category FROM membres WHERE id = ' . (int) $user_id . ')
				AND perm_config >= ' . Session::ACCESS_ADMIN . '
				AND perm_connect >= ' . Session::ACCESS_READ);

			if (!$is_still_admin)
			{
				$return |= self::NOT_AN_ADMIN;
			}
		}








|
|
<

<

<
>

>
>
|
<
|
>
>
>
|
>
>
|
|
|
>







 







>


<
>
>
>
>
>
>
>
>
|
<
<
<







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
...
466
467
468
469
470
471
472
473
474
475

476
477
478
479
480
481
482
483
484



485
486
487
488
489
490
491
		$this->make($backup);

		return basename($backup);
	}

	protected function make(string $dest)
	{
		// Acquire lock
		$version = \SQLite3::version();

		$db = DB::getInstance();



		Utils::safe_unlink($dest);

		if ($version['versionNumber'] >= 3027000) {
			// use VACUUM INTO instead when SQLite 3.27+ is required
			$db->exec(sprintf('VACUUM INTO %s;', $db->quote($dest)));

		}
		else {
			// use ::backup since PHP 7.4.0+
			// https://www.php.net/manual/en/sqlite3.backup.php
			$dest_db = new \SQLite3($dest);

			$db->backup($dest_db);
			$dest_db->exec('PRAGMA journal_mode = DELETE;');
			$dest_db->exec('VACUUM;');
			$db->close();
		}
	}

	/**
	 * Effectue une rotation des sauvegardes automatiques
	 * association.auto.1.sqlite deviendra association.auto.2.sqlite par exemple
	 */
	public function rotate(): void
................................................................................
		$appid = $db->querySingle('PRAGMA application_id;', false);

		if ($appid !== DB::APPID)
		{
			throw new UserException('Ce fichier n\'est pas une sauvegarde Garradin (application_id ne correspond pas).', self::NO_APP_ID);
		}

		// Empêchons l'admin de se tirer une balle dans le pied
		if ($user_id)
		{

			if (version_compare($version, '1.1', '<')) {
				$sql = 'SELECT 1 FROM membres_categories WHERE id = (SELECT id_categorie FROM membres WHERE id = %d) AND droit_connexion >= %d AND droit_config >= %d';
			}
			else {
				$sql = 'SELECT 1 FROM users_categories WHERE id = (SELECT id_category FROM membres WHERE id = %d) AND perm_connect >= %d AND perm_config >= %d';
			}

			$sql = sprintf($sql, $user_id, Session::ACCESS_READ, Session::ACCESS_ADMIN);
			$is_still_admin = $db->querySingle($sql);




			if (!$is_still_admin)
			{
				$return |= self::NOT_AN_ADMIN;
			}
		}

Modified src/include/lib/Garradin/Template.php from [3fde8a20c2] to [fbe9a25cda].

6
7
8
9
10
11
12

13
14
15
16
17
18
19
...
118
119
120
121
122
123
124
125

126













127
128
129
130
131
132
133
...
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
...
536
537
538
539
540
541
542




543
544
545
546
547
548
549
use KD2\HTTP;
use KD2\Translate;
use Garradin\Membres\Session;
use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Users\Category;
use Garradin\UserTemplate\CommonModifiers;
use Garradin\Web\Render\Skriv;


class Template extends \KD2\Smartyer
{
	static protected $_instance = null;

	static public function getInstance()
	{
................................................................................

		if (!$form->hasErrors())
		{
			return '';
		}

		$errors = $form->getErrorMessages(!empty($params['membre']) ? true : false);
		$errors = array_map([$this, 'escape'], $errors);

		$errors = array_map('nl2br', $errors);














		return '<div class="block error"><ul><li>' . implode('</li><li>', $errors) . '</li></ul></div>';
	}

	protected function showError($params)
	{
		if (!$params['if'])
................................................................................
	{
		$config = Config::getInstance();

		$couleur1 = $config->get('couleur1') ?: ADMIN_COLOR1;
		$couleur2 = $config->get('couleur2') ?: ADMIN_COLOR2;
		$admin_background = ADMIN_BACKGROUND_IMAGE;

		if ($f = $config->get('admin_background')) {
			$admin_background = WWW_URL . $f;
		}

		// Transformation Hexa vers décimal
		$couleur1 = implode(', ', sscanf($couleur1, '#%02x%02x%02x'));
		$couleur2 = implode(', ', sscanf($couleur2, '#%02x%02x%02x'));

		$out = '
................................................................................
		<style type="text/css">
		:root {
			--gMainColor: %s;
			--gSecondColor: %s;
			--gBgImage: url("%s");
		}
		</style>';





		return sprintf($out, $couleur1, $couleur2, $admin_background);
	}

	protected function displayChampMembre($v, $config = null)
	{
		if (is_string($config)) {







>







 







<
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|
|







 







>
>
>
>







6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
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
...
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
...
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
use KD2\HTTP;
use KD2\Translate;
use Garradin\Membres\Session;
use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Users\Category;
use Garradin\UserTemplate\CommonModifiers;
use Garradin\Web\Render\Skriv;
use Garradin\Files\Files;

class Template extends \KD2\Smartyer
{
	static protected $_instance = null;

	static public function getInstance()
	{
................................................................................

		if (!$form->hasErrors())
		{
			return '';
		}

		$errors = $form->getErrorMessages(!empty($params['membre']) ? true : false);


		foreach ($errors as &$error) {
			if ($error instanceof UserException) {
				$message = nl2br($this->escape($error->getMessage()));

				if ($error->hasDetails()) {
					$message = '<h3>' . $message . '</h3>' . $error->getDetailsHTML();
				}

				$error = $message;
			}
			else {
				$error = nl2br($this->escape($error));
			}
		}

		return '<div class="block error"><ul><li>' . implode('</li><li>', $errors) . '</li></ul></div>';
	}

	protected function showError($params)
	{
		if (!$params['if'])
................................................................................
	{
		$config = Config::getInstance();

		$couleur1 = $config->get('couleur1') ?: ADMIN_COLOR1;
		$couleur2 = $config->get('couleur2') ?: ADMIN_COLOR2;
		$admin_background = ADMIN_BACKGROUND_IMAGE;

		if (($f = $config->get('admin_background')) && ($file = Files::get($f))) {
			$admin_background = $file->url() . '?' . $file->modified->getTimestamp();
		}

		// Transformation Hexa vers décimal
		$couleur1 = implode(', ', sscanf($couleur1, '#%02x%02x%02x'));
		$couleur2 = implode(', ', sscanf($couleur2, '#%02x%02x%02x'));

		$out = '
................................................................................
		<style type="text/css">
		:root {
			--gMainColor: %s;
			--gSecondColor: %s;
			--gBgImage: url("%s");
		}
		</style>';

		if (($f = $config->get('admin_css')) && ($file = Files::get($f))) {
			$out .= "\n" . sprintf('<link rel="stylesheet" type="text/css" href="%s" />', $file->url() . '?' . $file->modified->getTimestamp());
		}

		return sprintf($out, $couleur1, $couleur2, $admin_background);
	}

	protected function displayChampMembre($v, $config = null)
	{
		if (is_string($config)) {

Modified src/include/lib/Garradin/Upgrade.php from [e2ad1a6938] to [5bb005e6e9].

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
...
178
179
180
181
182
183
184

































































185
186
187
188
189
190
191
				. PHP_EOL . 'Si celle-ci a échouée et que vous voulez ré-essayer, supprimez le fichier suivant:'
				. PHP_EOL . $path);
		}

		// Voir si l'utilisateur est loggé, on le fait ici pour le cas où
		// il y aurait déjà eu des entêtes envoyés au navigateur plus bas
		$session = Session::getInstance();
		$session->start();

		return true;
	}

	static public function upgrade()
	{
		$db = DB::getInstance();
		$v = $db->version();
................................................................................

		Static_Cache::store('upgrade', 'Mise à jour en cours.');

		// Créer une sauvegarde automatique
		$backup_name = (new Sauvegarde)->create(false, 'pre-upgrade-' . garradin_version());

		try {
			if (version_compare($v, '1.0.0', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0_migration.sql');
				$db->commitSchemaUpdate();

				// Import nouveau plan comptable
				$chart = new \Garradin\Entities\Accounting\Chart;
................................................................................
				$chart->label = 'Plan comptable associatif 2018';
				$chart->country = 'FR';
				$chart->code = 'PCA2018';
				$chart->save();
				$chart->accounts()->importCSV(ROOT . '/include/data/charts/fr_2018.csv');
			}

































			if (version_compare($v, '1.0.1', '<'))
			{
				// Missing trigger
				$db->begin();
				$db->import(ROOT . '/include/data/1.0.1_migration.sql');
				$db->commit();
			}
................................................................................
				foreach ($pages as $data) {
					$page = new \Garradin\Entities\Web\Page;
					$page->exists(true);
					$page->load((array) $data);
					$page->syncSearch();
				}
			}


































































			// 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);







|
>







 







|







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
...
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
				. PHP_EOL . 'Si celle-ci a échouée et que vous voulez ré-essayer, supprimez le fichier suivant:'
				. PHP_EOL . $path);
		}

		// Voir si l'utilisateur est loggé, on le fait ici pour le cas où
		// il y aurait déjà eu des entêtes envoyés au navigateur plus bas
		$session = Session::getInstance();
		$session->start(true);
		$session->isLogged(true);
		return true;
	}

	static public function upgrade()
	{
		$db = DB::getInstance();
		$v = $db->version();
................................................................................

		Static_Cache::store('upgrade', 'Mise à jour en cours.');

		// Créer une sauvegarde automatique
		$backup_name = (new Sauvegarde)->create(false, 'pre-upgrade-' . garradin_version());

		try {
			if (version_compare($v, '1.0.0-rc1', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0_migration.sql');
				$db->commitSchemaUpdate();

				// Import nouveau plan comptable
				$chart = new \Garradin\Entities\Accounting\Chart;
................................................................................
				$chart->label = 'Plan comptable associatif 2018';
				$chart->country = 'FR';
				$chart->code = 'PCA2018';
				$chart->save();
				$chart->accounts()->importCSV(ROOT . '/include/data/charts/fr_2018.csv');
			}


			if (version_compare($v, '1.0.0-rc10', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0-rc10_migration.sql');
				$db->commitSchemaUpdate();
			}

			if (version_compare($v, '1.0.0-beta1', '>=') && version_compare($v, '1.0.0-rc11', '<'))
			{
				// Missing trigger
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0_schema.sql');
				$db->commitSchemaUpdate();
			}

			if (version_compare($v, '1.0.0-rc14', '<'))
			{
				// Missing trigger
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0-rc14_migration.sql');
				$db->commitSchemaUpdate();
			}

			if (version_compare($v, '1.0.0-rc16', '<'))
			{
				// Missing trigger
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0-rc16_migration.sql');
				$db->commitSchemaUpdate();
			}

			if (version_compare($v, '1.0.1', '<'))
			{
				// Missing trigger
				$db->begin();
				$db->import(ROOT . '/include/data/1.0.1_migration.sql');
				$db->commit();
			}
................................................................................
				foreach ($pages as $data) {
					$page = new \Garradin\Entities\Web\Page;
					$page->exists(true);
					$page->load((array) $data);
					$page->syncSearch();
				}
			}

			if (version_compare($v, '1.1.1', '<')) {
				// Reset admin_background if the file does not exist
				$bg = $db->firstColumn('SELECT value FROM config WHERE key = \'admin_background\';');

				if ($bg) {
					$file = Files::get($bg);

					if (!$file) {
						$db->exec('UPDATE config SET value = NULL WHERE key = \'admin_background\';');
					}
				}

				// Fix links of admin homepage
				$homepage = $db->firstColumn('SELECT value FROM config WHERE key = \'admin_homepage\';');

				if ($homepage) {
					$file = Files::get($homepage);

					if ($file) {
						$content = $file->fetch();
						$new_content = preg_replace_callback(';\[\[((?!\]\]).*)\]\];', function ($match) {
							$link = explode('|', $match[1]);
							if (count($link) == 2) {
								list($label, $link) = $link;
							}
							else {
								$label = $link = $link[0];
							}

							if (strpos(trim($link), '/') !== false) {
								return $match[0];
							}

							$link = sprintf('!web/page.php?p=%s', trim($link));
							return sprintf('[[%s|%s]]', $label, $link);
						}, $content);

						if ($new_content != $content) {
							Files::disableQuota();
							$file->setContent($new_content);
						}
					}
				}
			}

			if (version_compare($v, '1.1.3', '<')) {
				// Missing trigger
				$db->begin();
				$db->import(ROOT . '/include/data/1.1.3_migration.sql');
				$db->commit();
			}

			if (version_compare($v, '1.1.4', '<')) {
				// Set config file names
				$config = Config::getInstance();

				$file = Files::get(Config::DEFAULT_FILES['admin_background']);
				$config->set('admin_background', $file ? Config::DEFAULT_FILES['admin_background'] : null);

				$file = Files::get(Config::DEFAULT_FILES['admin_homepage']);
				$config->set('admin_homepage', $file ? Config::DEFAULT_FILES['admin_homepage'] : null);

				$config->save();
			}

			// 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/Functions.php from [3fe8fa9c3f] to [a04e0ffc06].

2
3
4
5
6
7
8

9
10
11
12
13
14
15
...
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

namespace Garradin\UserTemplate;

use KD2\Brindille;
use KD2\Brindille_Exception;
use KD2\ErrorManager;


use Garradin\Web\Skeleton;

use const Garradin\WWW_URL;

class Functions
{
	const FUNCTIONS_LIST = [
................................................................................
			if (!isset($codes[$params['code']])) {
				throw new Brindille_Exception('Code HTTP inconnu');
			}

			header(sprintf('HTTP/1.1 %d %s', $params['code'], $codes[$params['code']]), true);
		}
		elseif (isset($params['redirect'])) {
			header('Location: ' . WWW_URL . $params['redirect'], true);
		}
		elseif (isset($params['type'])) {
			header('Content-Type: ' . $params['type'], true);
		}
		else {
			throw new Brindille_Exception('No valid parameter found for http function');
		}
	}
}







>







 







|









2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139

namespace Garradin\UserTemplate;

use KD2\Brindille;
use KD2\Brindille_Exception;
use KD2\ErrorManager;

use Garradin\Utils;
use Garradin\Web\Skeleton;

use const Garradin\WWW_URL;

class Functions
{
	const FUNCTIONS_LIST = [
................................................................................
			if (!isset($codes[$params['code']])) {
				throw new Brindille_Exception('Code HTTP inconnu');
			}

			header(sprintf('HTTP/1.1 %d %s', $params['code'], $codes[$params['code']]), true);
		}
		elseif (isset($params['redirect'])) {
			Utils::redirect($params['redirect']);
		}
		elseif (isset($params['type'])) {
			header('Content-Type: ' . $params['type'], true);
		}
		else {
			throw new Brindille_Exception('No valid parameter found for http function');
		}
	}
}

Modified src/include/lib/Garradin/UserTemplate/Modifiers.php from [f62964524d] to [e9e89801c5].

92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
	{
		$str = strip_tags($str);
		$str = self::truncate($str, $length);
		$str = preg_replace("/\n{2,}/", '</p><p>', $str);
		return '<p>' . $str . '</p>';
	}

	static public function protect_contact(string $contact): string
	{
		if (!trim($contact))
			return '';

		if (strpos($contact, '@')) {
			$reversed = strrev($contact);
			// https://unicode-table.com/en/FF20/







|







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
	{
		$str = strip_tags($str);
		$str = self::truncate($str, $length);
		$str = preg_replace("/\n{2,}/", '</p><p>', $str);
		return '<p>' . $str . '</p>';
	}

	static public function protect_contact(?string $contact): string
	{
		if (!trim($contact))
			return '';

		if (strpos($contact, '@')) {
			$reversed = strrev($contact);
			// https://unicode-table.com/en/FF20/

Modified src/include/lib/Garradin/Utils.php from [2723f401f4] to [7094cca199].

887
888
889
890
891
892
893





894
895
896
897
898
899
900
901
902





903
904

905
906
907
908
909
910
911
912
913
914
        return $str;
    }

    static public function unicodeCaseComparison($a, $b): int
    {
        if (!isset(self::$collator) && function_exists('collator_create')) {
            self::$collator = \Collator::create('fr_FR');





            // Don't use \Collator::NUMERIC_COLLATION here as it goes against what would feel logic
            // with NUMERIC_COLLATION: 1, 2, 10, 11, 101
            // without: 1, 10, 101, 11, 2
        }

        if (isset(self::$collator)) {
            return self::$collator->compare($a, $b);
        }






        $a = strtoupper(self::transliterateToAscii($a));
        $b = strtoupper(self::transliterateToAscii($b));


        return strcmp($a, $b);
    }

    static public function knatcasesort(array $array)
    {
        uksort($array, [self::class, 'unicodeCaseComparison']);
        return $array;
    }
}







>
>
>
>
>









>
>
>
>
>
|
|
>










887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
        return $str;
    }

    static public function unicodeCaseComparison($a, $b): int
    {
        if (!isset(self::$collator) && function_exists('collator_create')) {
            self::$collator = \Collator::create('fr_FR');

            // This is what makes the comparison case insensitive
            // https://www.php.net/manual/en/collator.setstrength.php
            self::$collator->setAttribute(\Collator::STRENGTH, \Collator::SECONDARY);

            // Don't use \Collator::NUMERIC_COLLATION here as it goes against what would feel logic
            // with NUMERIC_COLLATION: 1, 2, 10, 11, 101
            // without: 1, 10, 101, 11, 2
        }

        if (isset(self::$collator)) {
            return self::$collator->compare($a, $b);
        }

        if (function_exists('\mb_convert_case')) {
            $a = \mb_convert_case($a, \MB_CASE_LOWER);
            $b = \mb_convert_case($b, \MB_CASE_LOWER);
        }
        else {
            $a = strtoupper(self::transliterateToAscii($a));
            $b = strtoupper(self::transliterateToAscii($b));
        }

        return strcmp($a, $b);
    }

    static public function knatcasesort(array $array)
    {
        uksort($array, [self::class, 'unicodeCaseComparison']);
        return $array;
    }
}

Modified src/include/lib/Garradin/Web/Render/Skriv.php from [b88f52b63d] to [0996e6e8e0].

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

		// "Image.jpg"
		if ($pos === false) {
			return WWW_URL . $prefix . '/' . $uri;
		}
		// "bla/Image.jpg" outside of web context
		elseif (self::$context !== File::CONTEXT_WEB && $pos !== 0) {
			return WWW_URL . '/' . self::$context . '/' . $uri;
		}
		// "bla/Image.jpg" in web context or absolute link, eg. "/transactions/2442/42.jpg"
		else {
			return WWW_URL . '/' . ltrim($uri, '/');
		}
	}

	static public function resolveLink(string $uri) {
		if (substr($uri, 0, 1) == '/') {
			return WWW_URL . ltrim($uri, '/');

		}

		if (strpos(Utils::basename($uri), '.') === false) {
			$uri .= self::$link_suffix;
		}

		return self::$link_prefix . $uri;







|



|




|
|
>







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

		// "Image.jpg"
		if ($pos === false) {
			return WWW_URL . $prefix . '/' . $uri;
		}
		// "bla/Image.jpg" outside of web context
		elseif (self::$context !== File::CONTEXT_WEB && $pos !== 0) {
			return WWW_URL . self::$context . '/' . $uri;
		}
		// "bla/Image.jpg" in web context or absolute link, eg. "/transactions/2442/42.jpg"
		else {
			return WWW_URL . ltrim($uri, '/');
		}
	}

	static public function resolveLink(string $uri) {
		$first = substr($uri, 0, 1);
		if ($first == '/' || $first == '!') {
			return Utils::getLocalURL($uri);
		}

		if (strpos(Utils::basename($uri), '.') === false) {
			$uri .= self::$link_suffix;
		}

		return self::$link_prefix . $uri;

Modified src/include/lib/Garradin/Web/Skeleton.php from [0353cf768c] to [f4f75cf9cf].

136
137
138
139
140
141
142




143
144
145
146
147
148
149
...
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 exists()
	{
		return $this->file ? true : ($this->defaultPath() ? true : false);
	}

	public function raw(): string
	{




		return $this->file ? $this->file->fetch() : file_get_contents($this->defaultPath());
	}

	public function edit(string $content)
	{
		$file = Files::get(File::CONTEXT_SKELETON . '/' . $this->name);

................................................................................
		}
	}

	static public function list(): array
	{
		$sources = [];

		$dir = dir(ROOT . '/www/skel-dist/');


		while ($file = $dir->read())
		{

			if ($file[0] == '.')
				continue;

			$sources[$file] = null;
		}


		$dir->close();




		$list = Files::list(File::CONTEXT_SKELETON);

		foreach ($list as $file) {
			if ($file->type != $file::TYPE_FILE) {
				continue;
			}

			$sources[$file->name] = $file;
		}

		ksort($sources);

		return $sources;
	}
}







>
>
>
>







 







|
>

<
<
>
|

|
<
|
>

|
>
>
>








|







136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
...
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
	public function exists()
	{
		return $this->file ? true : ($this->defaultPath() ? true : false);
	}

	public function raw(): string
	{
		if (!$this->exists()) {
			throw new UserException('Ce fichier n\'existe pas');
		}

		return $this->file ? $this->file->fetch() : file_get_contents($this->defaultPath());
	}

	public function edit(string $content)
	{
		$file = Files::get(File::CONTEXT_SKELETON . '/' . $this->name);

................................................................................
		}
	}

	static public function list(): array
	{
		$sources = [];

		$path = ROOT . '/www/skel-dist/';
		$i = new \DirectoryIterator($path);



		foreach ($i as $file) {
			if ($file->isDot() || $file->isDir()) {
				continue;
			}


			$mime = mime_content_type($file->getRealPath());

			$sources[$file->getFilename()] = ['is_text' => substr($mime, 0, 5) == 'text/', 'changed' => null];
		}

		unset($i);

		$list = Files::list(File::CONTEXT_SKELETON);

		foreach ($list as $file) {
			if ($file->type != $file::TYPE_FILE) {
				continue;
			}

			$sources[$file->name] = ['is_text' => substr($file->mime, 0, 5) == 'text/', 'changed' => $file->modified];
		}

		ksort($sources);

		return $sources;
	}
}

Modified src/include/lib/Garradin/Web/Web.php from [17cc419c70] to [631bb6f7f6].

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
...
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
...
227
228
229
230
231
232
233




234
235
236
237
238
			$deleted = array_map(function ($page) {
				return $page->path;
			}, $deleted);

			$db->exec(sprintf('DELETE FROM web_pages WHERE %s;', $db->where('path', $deleted)));
		}

		foreach ($new as $file) {
			$f = Files::get($file . '/index.txt');

			if (!$f) {
				continue;
			}

			Page::fromFile($f)->save();
................................................................................
			else {
				$file->serve($session, isset($_GET['download']) ? true : false);
			}

			return;
		}

		if (Config::getInstance()->get('site_disabled')) {
			Utils::redirect(ADMIN_URL);
		}

		// Redirect old categories
		if (substr($uri, -1) == '/') {
			http_response_code(301);
			Utils::redirect('/' . rtrim($uri, '/'));
		}

		$page = null;

		if ($uri == '') {
			$skel = 'index.html';
		}
		elseif (($page = self::getByURI($uri)) && $page->status == Page::STATUS_ONLINE) {
			$skel = $page->template();
			$page = $page->asTemplateArray();
		}
		else {
			// Trying to see if a custom template with this name exists
			if (preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $uri)) {
				$s = new Skeleton($uri);
................................................................................
			elseif ($file = Files::getFromURI(File::CONTEXT_SKELETON . '/' . $uri)) {
				$file->serve();
				return;
			}

			$skel = '404.html';
		}





		$s = new Skeleton($skel);
		$s->serve(compact('uri', 'page', 'skel'));
	}
}







|







 







|
<
<












|







 







>
>
>
>





62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
...
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
...
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
			$deleted = array_map(function ($page) {
				return $page->path;
			}, $deleted);

			$db->exec(sprintf('DELETE FROM web_pages WHERE %s;', $db->where('path', $deleted)));
		}

		foreach (array_keys($new) as $file) {
			$f = Files::get($file . '/index.txt');

			if (!$f) {
				continue;
			}

			Page::fromFile($f)->save();
................................................................................
			else {
				$file->serve($session, isset($_GET['download']) ? true : false);
			}

			return;
		}

		$site_disabled = Config::getInstance()->get('site_disabled');



		// Redirect old categories
		if (substr($uri, -1) == '/') {
			http_response_code(301);
			Utils::redirect('/' . rtrim($uri, '/'));
		}

		$page = null;

		if ($uri == '') {
			$skel = 'index.html';
		}
		elseif (!$site_disabled && ($page = self::getByURI($uri)) && $page->status == Page::STATUS_ONLINE) {
			$skel = $page->template();
			$page = $page->asTemplateArray();
		}
		else {
			// Trying to see if a custom template with this name exists
			if (preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $uri)) {
				$s = new Skeleton($uri);
................................................................................
			elseif ($file = Files::getFromURI(File::CONTEXT_SKELETON . '/' . $uri)) {
				$file->serve();
				return;
			}

			$skel = '404.html';
		}

		if ($site_disabled && ($skel == '404.html' || $uri == '')) {
			Utils::redirect(ADMIN_URL);
		}

		$s = new Skeleton($skel);
		$s->serve(compact('uri', 'page', 'skel'));
	}
}

Added src/templates/acc/years/export.tpl version [f38c0c6bff].





















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
{include file="admin/_head.tpl" title="Importer des écritures" current="acc/years"}

<nav class="acc-year">
	<h4>Exercice sélectionné&nbsp;:</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?id={$year.id}">Import</a></li>
		{/if}
		<li class="current"><a href="{$admin_url}acc/years/import.php?id={$year.id}">Export</a></li>
	</ul>
</nav>

{form_errors}

<form method="get" action="{$self_url}">

<fieldset>
	<legend>Export du journal général</legend>
	<dl>
		<dt>Format d'export</dt>
		{input type="radio" name="format" value="ods" default="ods" label="Tableur" help="pour LibreOffice ou autre tableur"}
		{input type="radio" name="format" value="csv" label="CSV"}
		<dt>Type d'export</dt>
		{input type="radio" name="type" value="full" label="Export comptable complet" default="full" help="conseillé pour transfert vers un autre logiciel"}
		{input type="radio" name="type" value="raw" label="Export natif Garradin"}
	</dl>
</fieldset>

<p class="submit">
	<input type="hidden" name="id" value="{$year.id}" />
	{button type="submit" name="load" label="Télécharger" shape="download" class="main"}
</p>



</form>

{include file="admin/_foot.tpl"}

Modified src/templates/acc/years/import.tpl from [ba9ecedc51] to [1a8a438acd].

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
..
29
30
31
32
33
34
35
36
37

38


39
40
41
42
43
44
45
	<h4>Exercice sélectionné&nbsp;:</h4>
	<h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3>
</nav>

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}acc/years/import.php?id={$year.id}">Import</a></li>
		<li><a href="{$admin_url}acc/years/import.php?id={$year.id}&amp;export=csv">Export journal général - CSV</a></li>
		<li><a href="{$admin_url}acc/years/import.php?id={$year.id}&amp;export=ods">Export journal général - tableur</a></li>
	</ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}" enctype="multipart/form-data">

................................................................................

{else}

	<fieldset>
		<legend>Import d'écritures</legend>
		<dl>
			<dt><label for="f_type_garradin">Format de fichier</label></dt>
			{input type="radio" name="type" value="garradin" label="Journal général au format CSV Garradin" default="garradin"}
			{input type="radio" name="type" value="csv" label="Journal au format CSV libre"}

			{include file="common/_csv_help.tpl"}


			{input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1}
			<dd class="help block">
				- Les lignes comportant un numéro d'écriture existant mettront à jour les écritures correspondant à ces numéros.<br />
				- Les lignes comportant un numéro inexistant renverront une erreur.<br />
				- Les lignes sans numéro créeront de nouvelles écritures.<br />
				- Si le fichier comporte des écritures dont la date est en dehors de l'exercice courant, elles seront ignorées.
			</dd>







|
<







 







<
|
>

>
>







4
5
6
7
8
9
10
11

12
13
14
15
16
17
18
..
28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43
44
45
46
	<h4>Exercice sélectionné&nbsp;:</h4>
	<h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3>
</nav>

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}acc/years/import.php?id={$year.id}">Import</a></li>
		<li><a href="{$admin_url}acc/years/export.php?id={$year.id}">Export</a></li>

	</ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}" enctype="multipart/form-data">

................................................................................

{else}

	<fieldset>
		<legend>Import d'écritures</legend>
		<dl>
			<dt><label for="f_type_garradin">Format de fichier</label></dt>

			{input type="radio" name="type" value="csv" label="Journal au format CSV libre"  default="csv"}
			<dd class="help">Ce format ne permet d'importer que des écritures simples (un débit et un crédit par écriture) mais convient à la plupart des utilisations.</dd>
			{include file="common/_csv_help.tpl"}
			{input type="radio" name="type" value="garradin" label="Journal général au format CSV Garradin"}
			<dd class="help">Ce format permet d'importer des écritures comportant plusieurs lignes. Le format attendu est identique à l'export de journal général qui peut servir d'exemple.</dd>
			{input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1}
			<dd class="help block">
				- Les lignes comportant un numéro d'écriture existant mettront à jour les écritures correspondant à ces numéros.<br />
				- Les lignes comportant un numéro inexistant renverront une erreur.<br />
				- Les lignes sans numéro créeront de nouvelles écritures.<br />
				- Si le fichier comporte des écritures dont la date est en dehors de l'exercice courant, elles seront ignorées.
			</dd>

Modified src/templates/acc/years/index.tpl from [b7ac0ffc9e] to [fbfa64b72c].

61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
					| <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>
				{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
					{linkbutton label="Export CSV" shape="export" href="import.php?id=%d&export=csv"|args:$year.id}
					{linkbutton label="Export tableur" shape="export" href="import.php?id=%d&export=ods"|args:$year.id}
					{if !$year.closed}
						{linkbutton label="Import" shape="upload" href="import.php?id=%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}







|
<







61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
					| <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>
				{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
					{linkbutton label="Export" shape="export" href="export.php?id=%d"|args:$year.id}

					{if !$year.closed}
						{linkbutton label="Import" shape="upload" href="import.php?id=%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}

Modified src/templates/admin/_head.tpl from [c710e6cf26] to [35b3bf84ba].

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr"{if array_key_exists('_dialog', $_GET)} class="dialog"{/if}>
<head>
    <meta charset="utf-8" />
    <title>{$title}</title>
    <link rel="icon" type="image/png" href="{$admin_url}static/icon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
    <link rel="stylesheet" type="text/css" href="{$admin_url}static/admin.css?{$version_hash}" media="all" />
    <script type="text/javascript" src="{$admin_url}static/scripts/global.js?{$version_hash}"></script>
    {if isset($custom_js)}
        {foreach from=$custom_js item="js"}
            <script type="text/javascript" src="{$admin_url}static/scripts/{$js}?{$version_hash}"></script>
        {/foreach}





|







1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr"{if array_key_exists('_dialog', $_GET)} class="dialog"{/if}>
<head>
    <meta charset="utf-8" />
    <title>{$title}</title>
    <link rel="icon" type="image/png" href="{$www_url}favicon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
    <link rel="stylesheet" type="text/css" href="{$admin_url}static/admin.css?{$version_hash}" media="all" />
    <script type="text/javascript" src="{$admin_url}static/scripts/global.js?{$version_hash}"></script>
    {if isset($custom_js)}
        {foreach from=$custom_js item="js"}
            <script type="text/javascript" src="{$admin_url}static/scripts/{$js}?{$version_hash}"></script>
        {/foreach}

Deleted src/templates/admin/config/backup/automatique.tpl version [f0d729ff37].

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
{include file="admin/_head.tpl" title="Sauvegarde et restauration" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="automatique"}

{form_errors}

{if $ok == 'config'}
	<p class="block confirm">La configuration a bien été enregistrée.</p>
{/if}

<form method="post" action="{$self_url_no_qs}">

<fieldset>
	<legend>Configuration de la sauvegarde automatique</legend>
	<p class="help">
		En activant cette option une sauvegarde sera automatiquement créée à chaque intervalle donné.
		Par exemple en activant une sauvegarde hebdomadaire, une copie des données sera réalisée
		une fois par semaine, sauf si aucune modification n'a été effectuée sur les données
		ou que personne ne s'est connecté.
	</p>
	<dl>
		<dt><label for="f_frequency">Intervalle de sauvegarde</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
		<dd>
			<select name="frequence_sauvegardes" required="required" id="f_frequency">
				<option value="0"{form_field name=frequence_sauvegardes data=$config selected=0}>Aucun — les sauvegardes automatiques sont désactivées</option>
				<option value="1"{form_field name=frequence_sauvegardes data=$config selected=1}>Quotidien, tous les jours</option>
				<option value="7"{form_field name=frequence_sauvegardes data=$config selected=7}>Hebdomadaire, tous les 7 jours</option>
				<option value="15"{form_field name=frequence_sauvegardes data=$config selected=15}>Bimensuel, tous les 15 jours</option>
				<option value="30"{form_field name=frequence_sauvegardes data=$config selected=30}>Mensuel</option>
				<option value="90"{form_field name=frequence_sauvegardes data=$config selected=90}>Trimestriel</option>
				<option value="365{form_field name=frequence_sauvegardes data=$config selected=365}">Annuel</option>
			</select>
		</dd>
		<dt><label for="f_max_backups">Nombre de sauvegardes conservées</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
		<dd class="help">
			Par exemple avec l'intervalle mensuel, en indiquant de conserver 12 sauvegardes,
			vous pourrez garder un an d'historique de sauvegardes.
		</dd>
		<dd class="help">
			<strong>Attention :</strong> si vous choisissez un nombre important et un intervalle réduit,
			l'espace disque occupé par vos sauvegardes va rapidement augmenter.
		</dd>
		<dd><input type="number" name="nombre_sauvegardes" value="{form_field name=nombre_sauvegardes data=$config}" if="f_max_backups" min="1" max="90" required="required" /></dd>
	</dl>
	<p>
		{csrf_field key="backup_config"}
		<input type="submit" name="config" value="Enregistrer &rarr;" />
	</p>
</fieldset>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































Modified src/templates/admin/config/backup/index.tpl from [d594e3f550] to [e84c2fbbb2].

27
28
29
30
31
32
33
34
35
36
37
38
39
	<dl>
		<dt><strong>Membres</strong></dt>
		<dd><a href="{$admin_url}membres/import.php">Import de la liste des membres</a></dd>
		<dd><a href="{$admin_url}membres/import.php?export=ods">Export de la liste des membres au format tableur LibreOffice Calc / Excel</a></dd>
		<dd><a href="{$admin_url}membres/import.php?export=csv">Export de la liste des membres au format CSV</a></dd>
		<dt><strong>Comptabilité</strong> (pour l'exercice courant)</dt>
		<dd><a href="{$admin_url}acc/years/import.php">Import des données comptables</a></dd>
		<dd><a href="{$admin_url}acc/years/import.php?export=ods">Export des données comptables au format tableur LibreOffice Calc / Excel</a></dd>
		<dd><a href="{$admin_url}acc/years/import.php?export=csv">Export des données comptables au format CSV</a></dd>
	</dl>
</fieldset>

{include file="admin/_foot.tpl"}







|
<




27
28
29
30
31
32
33
34

35
36
37
38
	<dl>
		<dt><strong>Membres</strong></dt>
		<dd><a href="{$admin_url}membres/import.php">Import de la liste des membres</a></dd>
		<dd><a href="{$admin_url}membres/import.php?export=ods">Export de la liste des membres au format tableur LibreOffice Calc / Excel</a></dd>
		<dd><a href="{$admin_url}membres/import.php?export=csv">Export de la liste des membres au format CSV</a></dd>
		<dt><strong>Comptabilité</strong> (pour l'exercice courant)</dt>
		<dd><a href="{$admin_url}acc/years/import.php">Import des données comptables</a></dd>
		<dd><a href="{$admin_url}acc/years/export.php">Export des données comptables</a></dd>

	</dl>
</fieldset>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/backup/save.tpl from [196650304d] to [4752a2358e].

66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
81
	<p class="help">
		En activant cette option une sauvegarde sera automatiquement créée à chaque intervalle donné.
		Par exemple en activant une sauvegarde hebdomadaire, une copie des données sera réalisée
		une fois par semaine, sauf si aucune modification n'a été effectuée sur les données
		ou que personne ne s'est connecté.
	</p>
	<p class="alert block">
		Attention, la sauvegarde automatique permet uniquement de revenir à un état antérieur, mais ne prévient pas de la perte des données&nbsp;! Pour cela, il est recommandé de faire des sauvegardes manuelles en téléchargeant une copie des données sur votre ordinateur.<br /><br />

		La sauvegarde automatique ne concerne que la base de données, mais pas les documents, fichiers joints aux écritures ou aux membres, ni le contenu du site web.
	</p>
	<dl>
		<dt><label for="f_frequency">Intervalle de sauvegarde</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
		<dd>
			<select name="frequence_sauvegardes" required="required" id="f_frequency">
				<option value="0"{form_field name=frequence_sauvegardes data=$config selected=0}>Aucun — les sauvegardes automatiques sont désactivées</option>
				<option value="1"{form_field name=frequence_sauvegardes data=$config selected=1}>Quotidien, tous les jours</option>







|
>
|







66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
	<p class="help">
		En activant cette option une sauvegarde sera automatiquement créée à chaque intervalle donné.
		Par exemple en activant une sauvegarde hebdomadaire, une copie des données sera réalisée
		une fois par semaine, sauf si aucune modification n'a été effectuée sur les données
		ou que personne ne s'est connecté.
	</p>
	<p class="alert block">
		Attention, la sauvegarde automatique permet uniquement de revenir à un état antérieur, mais ne prévient pas de la perte des données&nbsp;! Pour cela, il est recommandé de faire des sauvegardes manuelles en téléchargeant une copie des données sur votre ordinateur.
		{if FILE_STORAGE_BACKEND != 'SQLite'}<br /><br />
		La sauvegarde automatique ne concerne que la base de données, mais pas les documents, fichiers joints aux écritures ou aux membres, ni le contenu du site web.{/if}
	</p>
	<dl>
		<dt><label for="f_frequency">Intervalle de sauvegarde</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
		<dd>
			<select name="frequence_sauvegardes" required="required" id="f_frequency">
				<option value="0"{form_field name=frequence_sauvegardes data=$config selected=0}>Aucun — les sauvegardes automatiques sont désactivées</option>
				<option value="1"{form_field name=frequence_sauvegardes data=$config selected=1}>Quotidien, tous les jours</option>

Modified src/templates/admin/config/index.tpl from [7910ad85ee] to [fc444db700].

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
			{input type="select" name="champ_identifiant" source=$config options=$champs required=true label="Champ utilisé comme identifiant de connexion" help="Ce champ des fiches membres sera utilisé comme identifiant pour se connecter à Garradin. Ce champ doit être unique (il ne peut pas contenir deux membres ayant la même valeur dans ce champ)."}
		</dl>
	</fieldset>

	<fieldset>
		<legend>Personnalisation</legend>
		<dl>
		{if $config.admin_homepage}
			<dt>Texte de la page d'accueil</dt>
			<dd>
				{linkbutton href="!common/files/edit.php?p=%s"|args:$config.admin_homepage label="Modifier" shape="edit" target="_dialog" data-dialog-height="90%"}
			</dd>
			<dd class="help">
				Ce contenu sera affiché à la connexion d'un membre, ou en cliquant sur l'onglet 'Accueil' du menu de gauche.
			</dd>
		{/if}
			{input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$color1 source=$config name="couleur1" label="Couleur primaire" placeholder=$color1}
			{input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$color2 source=$config name="couleur2" label="Couleur secondaire" placeholder=$color2}
			{input type="file" label="Image de fond" name="background" help="Il est conseillé d'utiliser une image en noir et blanc avec un fond blanc pour un meilleur rendu. Dimensions recommandées : 380x200" accept="image/*,*.jpeg,*.jpg,*.png,*.gif"}







		</dl>
		<input type="hidden" name="admin_background" id="f_admin_background" data-current="{$background_image_current}" data-default="{$background_image_default}" value="{$_POST.admin_background}" />
	</fieldset>

	<p class="submit">
		{csrf_field key="config"}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

{include file="admin/_foot.tpl"}







<


|




<



>
>
>
>
>
>
>












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
			{input type="select" name="champ_identifiant" source=$config options=$champs required=true label="Champ utilisé comme identifiant de connexion" help="Ce champ des fiches membres sera utilisé comme identifiant pour se connecter à Garradin. Ce champ doit être unique (il ne peut pas contenir deux membres ayant la même valeur dans ce champ)."}
		</dl>
	</fieldset>

	<fieldset>
		<legend>Personnalisation</legend>
		<dl>

			<dt>Texte de la page d'accueil</dt>
			<dd>
				{linkbutton href="!config/edit_file.php?k=%s"|args:'admin_homepage' label="Modifier" shape="edit" target="_dialog" data-dialog-height="90%"}
			</dd>
			<dd class="help">
				Ce contenu sera affiché à la connexion d'un membre, ou en cliquant sur l'onglet 'Accueil' du menu de gauche.
			</dd>

			{input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$color1 source=$config name="couleur1" label="Couleur primaire" placeholder=$color1}
			{input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$color2 source=$config name="couleur2" label="Couleur secondaire" placeholder=$color2}
			{input type="file" label="Image de fond" name="background" help="Il est conseillé d'utiliser une image en noir et blanc avec un fond blanc pour un meilleur rendu. Dimensions recommandées : 380x200" accept="image/*,*.jpeg,*.jpg,*.png,*.gif"}
			<dt>Personnalisation CSS de l'administration</dt>
			<dd>
				{linkbutton href="!config/edit_file.php?k=%s"|args:'admin_css' label="Modifier" shape="edit" target="_dialog" data-dialog-height="90%"}
			</dd>
			<dd class="help">
				Permet de rajouter des <a href="https://developer.mozilla.org/fr/docs/Learn/CSS/First_steps" target="_blank">règles CSS</a> qui modifieront l'apparence de l'interface d'administration.
			</dd>
		</dl>
		<input type="hidden" name="admin_background" id="f_admin_background" data-current="{$background_image_current}" data-default="{$background_image_default}" value="{$_POST.admin_background}" />
	</fieldset>

	<p class="submit">
		{csrf_field key="config"}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/membres/_details.tpl from [2d17bd544c] to [824eeb6d7f].

1
2
3
4
5
6
7



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
assert(isset($data, $champs, $show_message_button));
$user_files_path = (new Membres)->getAttachementsDirectory($data->id);
?>

<dl class="describe">
	{foreach from=$champs key="c" item="c_config"}



	<dt>{$c_config.title}</dt>
	<dd>
		{if $c_config.type == 'checkbox'}
			{if $data->$c}Oui{else}Non{/if}
		{elseif $c_config.type == 'file'}
			<?php
			$edit = ($c_config->editable || $mode == 'edit');
			?>
			{include file="common/files/_context_list.tpl" limit=1 path="%s/%s"|args:$user_files_path,$c}
		{elseif empty($data->$c)}
			<em>(Non renseigné)</em>
		{elseif $c == $c_config.champ_identite}
			<strong>{$data->$c}</strong>
		{elseif $c_config.type == 'email'}
			<a href="mailto:{$data->$c|escape:'url'}">{$data->$c}</a>
			{if $c == 'email' && $show_message_button}
				{linkbutton href="!membres/message.php?id=%d"|args:$data.id label="Envoyer un message" shape="mail"}
			{/if}
		{elseif $c_config.type == 'tel'}
			<a href="tel:{$data->$c}">{$data->$c|format_tel}</a>
		{elseif $c_config.type == 'country'}
			{$data->$c|get_country_name}
		{elseif $c_config.type == 'date'}
			{$data->$c|date_short}
		{elseif $c_config.type == 'datetime'}
			{$data->$c|date}
		{elseif $c_config.type == 'password'}
			*******
		{elseif $c_config.type == 'multiple'}
			<ul>
			{foreach from=$c_config.options key="b" item="name"}
				{if $data->$c & (0x01 << $b)}
					<li>{$name}</li>
				{/if}
			{/foreach}
			</ul>
		{else}
			{$data->$c|escape|rtrim|nl2br}
		{/if}
	</dd>
	{/foreach}
</dl>







>
>
>



|





|


|

|




|

|

|

|





|





|




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
<?php
assert(isset($data, $champs, $show_message_button));
$user_files_path = (new Membres)->getAttachementsDirectory($data->id);
?>

<dl class="describe">
	{foreach from=$champs key="c" item="c_config"}
	<?php
	$value = $data->$c ?? null;
	?>
	<dt>{$c_config.title}</dt>
	<dd>
		{if $c_config.type == 'checkbox'}
			{if $value}Oui{else}Non{/if}
		{elseif $c_config.type == 'file'}
			<?php
			$edit = ($c_config->editable || $mode == 'edit');
			?>
			{include file="common/files/_context_list.tpl" limit=1 path="%s/%s"|args:$user_files_path,$c}
		{elseif empty($value)}
			<em>(Non renseigné)</em>
		{elseif $c == $c_config.champ_identite}
			<strong>{$value}</strong>
		{elseif $c_config.type == 'email'}
			<a href="mailto:{$value|escape:'url'}">{$value}</a>
			{if $c == 'email' && $show_message_button}
				{linkbutton href="!membres/message.php?id=%d"|args:$data.id label="Envoyer un message" shape="mail"}
			{/if}
		{elseif $c_config.type == 'tel'}
			<a href="tel:{$value}">{$value|format_tel}</a>
		{elseif $c_config.type == 'country'}
			{$value|get_country_name}
		{elseif $c_config.type == 'date'}
			{$value|date_short}
		{elseif $c_config.type == 'datetime'}
			{$value|date}
		{elseif $c_config.type == 'password'}
			*******
		{elseif $c_config.type == 'multiple'}
			<ul>
			{foreach from=$c_config.options key="b" item="name"}
				{if $value & (0x01 << $b)}
					<li>{$name}</li>
				{/if}
			{/foreach}
			</ul>
		{else}
			{$value|escape|rtrim|nl2br}
		{/if}
	</dd>
	{/foreach}
</dl>

Modified src/templates/admin/membres/_list_actions.tpl from [158163abdd] to [9f86dc3d7a].

3
4
5
6
7
8
9

10
11

12
13
14
15
16
17
18
19
				{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td>{/if}
				<td class="actions" colspan="{$colspan}">
					<em>Pour les membres cochés :</em>
					{csrf_field key="membres_action"}
					<select name="action">
						<option value="">— Choisir une action à effectuer —</option>
						<option value="move">Changer de catégorie</option>

						<option value="csv">Exporter en tableau CSV</option>
						<option value="ods">Exporter en classeur Office</option>

						<option value="delete">Supprimer</option>
					</select>
					<noscript>
						<input type="submit" value="OK" />
					</noscript>
				</td>
			</tr>
		</tfoot>







>


>








3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
				{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td>{/if}
				<td class="actions" colspan="{$colspan}">
					<em>Pour les membres cochés :</em>
					{csrf_field key="membres_action"}
					<select name="action">
						<option value="">— Choisir une action à effectuer —</option>
						<option value="move">Changer de catégorie</option>
						{if !isset($export) || $export != false}
						<option value="csv">Exporter en tableau CSV</option>
						<option value="ods">Exporter en classeur Office</option>
						{/if}
						<option value="delete">Supprimer</option>
					</select>
					<noscript>
						<input type="submit" value="OK" />
					</noscript>
				</td>
			</tr>
		</tfoot>

Modified src/templates/admin/membres/action.tpl from [076d7f7b64] to [3bdaad34d2].

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

    {if $action == 'move'}
    <fieldset>
        <legend>Changer la catégorie des {$nb_selected} membres sélectionnés</legend>
        <dl>
            <dt><label for="f_cat">Nouvelle catégorie</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <select name="id_categorie" id="f_cat">
                {foreach from=$membres_cats key="id" item="nom"}
                    <option value="{$id}">{$nom}</option>
                {/foreach}
                </select>
            </dd>
        </dl>
    </fieldset>







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

    {if $action == 'move'}
    <fieldset>
        <legend>Changer la catégorie des {$nb_selected} membres sélectionnés</legend>
        <dl>
            <dt><label for="f_cat">Nouvelle catégorie</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <select name="id_category" id="f_cat">
                {foreach from=$membres_cats key="id" item="nom"}
                    <option value="{$id}">{$nom}</option>
                {/foreach}
                </select>
            </dd>
        </dl>
    </fieldset>

Modified src/templates/admin/membres/modifier.tpl from [4ea936740e] to [bf6707b31f].

43
44
45
46
47
48
49





50
51
52
53
54
55
56
            <dd class="help">
                Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
                <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="pw_suggest" value="{$passphrase}" autocomplete="off" />
            </dd>
            <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern="{$password_pattern}" autocomplete="off" /></dd>
            <dt><label for="f_repasse">Encore le mot de passe</label> (vérification){if $champs.passe.mandatory} <b title="(Champ obligatoire)">obligatoire</b>{/if}</dt>
            <dd><input type="password" name="passe_confirmed" id="f_repasse" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" autocomplete="off" /></dd>





        </dl>
    </fieldset>

    {if $membre.secret_otp || $membre.clef_pgp}
    <fieldset>
        <legend>Options de sécurité</legend>
        <dl>







>
>
>
>
>







43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
            <dd class="help">
                Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
                <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="pw_suggest" value="{$passphrase}" autocomplete="off" />
            </dd>
            <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern="{$password_pattern}" autocomplete="off" /></dd>
            <dt><label for="f_repasse">Encore le mot de passe</label> (vérification){if $champs.passe.mandatory} <b title="(Champ obligatoire)">obligatoire</b>{/if}</dt>
            <dd><input type="password" name="passe_confirmed" id="f_repasse" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" autocomplete="off" /></dd>
        {if $membre.passe}
            <dd>
                {input type="checkbox" name="delete_password" label="Supprimer le mot de passe de ce membre" value=1}
            </dd>
        {/if}
        </dl>
    </fieldset>

    {if $membre.secret_otp || $membre.clef_pgp}
    <fieldset>
        <legend>Options de sécurité</legend>
        <dl>

Modified src/templates/common/_csv_match_columns.tpl from [f8a953f97c] to [09427edd39].

2
3
4
5
6
7
8






9
10
11
12
13
14
15
	<legend>Importer depuis un fichier CSV générique</legend>
	<dl>
		<dd class="help">{$csv->count()} lignes trouvées dans le fichier</dd>
		<dt>{input type="checkbox" name="skip_first_line" value="1" label="Ne pas importer la première ligne" help="Décocher cette case si la première ligne ne contient pas l'intitulé des colonnes, mais des données" default=1}
		<dt><label>Correspondance des colonnes</label></dt>
		<dd>
			<table class="list auto">






				<tbody>
				{foreach from=$csv->getFirstLine() key="index" item="csv_field"}
					<tr>
						<th>{$csv_field}</th>
						<td>
							<select name="translation_table[{$index}]">
							<?php $selected = $csv->getSelectedTable(); ?>







>
>
>
>
>
>







2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	<legend>Importer depuis un fichier CSV générique</legend>
	<dl>
		<dd class="help">{$csv->count()} lignes trouvées dans le fichier</dd>
		<dt>{input type="checkbox" name="skip_first_line" value="1" label="Ne pas importer la première ligne" help="Décocher cette case si la première ligne ne contient pas l'intitulé des colonnes, mais des données" default=1}
		<dt><label>Correspondance des colonnes</label></dt>
		<dd>
			<table class="list auto">
				<thead>
					<tr>
						<th>Colonne du CSV à importer</th>
						<th>Importer cette colonne comme…</th>
					</tr>
				</thead>
				<tbody>
				{foreach from=$csv->getFirstLine() key="index" item="csv_field"}
					<tr>
						<th>{$csv_field}</th>
						<td>
							<select name="translation_table[{$index}]">
							<?php $selected = $csv->getSelectedTable(); ?>

Added src/templates/common/files/rename.tpl version [8196200baf].









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{include file="admin/_head.tpl" title="Renommer" current=null}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">
	<fieldset>
		<legend>Renommer</legend>
		<dl>
			<dt>Nom actuel</dt>
			<dd><input type="text" disabled="disabled" value="{$file.name}" /></dd>
			{input type="text" name="new_name" required="required" label="Nouveau nom" default=$file.name}
		</dl>
		<p class="submit">
			{csrf_field key=$csrf_key}
			{button type="submit" name="rename" label="Renommer" shape="right" class="main"}
		</p>
	</fieldset>
</form>

{include file="admin/_foot.tpl"}

Modified src/templates/docs/index.tpl from [34d64a62ee] to [73085f9efc].

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
..
66
67
68
69
70
71
72

73

74
75
76
77
78
79
80
..
89
90
91
92
93
94
95





96


97
98
99
100
101
102
103
...
116
117
118
119
120
121
122




123

124
125
126
127
128
129
130
	{if $can_upload}
		{linkbutton shape="plus" label="Nouveau fichier texte" target="_dialog" href="!docs/new_file.php?p=%s"|args:$path}
		{linkbutton shape="upload" label="Ajouter un fichier" target="_dialog" href="!common/files/upload.php?p=%s"|args:$path}
	{/if}
	</aside>
	<ul>
		<li{if $context == File::CONTEXT_DOCUMENTS} class="current"{/if}><a href="./">Documents</a></li>
		{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
			<li{if $context == File::CONTEXT_TRANSACTION} class="current"{/if}><a href="./?p=<?=File::CONTEXT_TRANSACTION?>">Fichiers des écritures</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}
			<li{if $context == File::CONTEXT_USER} class="current"{/if}><a href="./?p=<?=File::CONTEXT_USER?>">Fichiers des membres</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)}
			<li{if $context == File::CONTEXT_SKELETON} class="current"{/if}><a href="./?p=<?=File::CONTEXT_SKELETON?>">Squelettes du site web</a></li>
		{/if}
	</ul>

................................................................................
{/if}

{if count($files)}
<form method="post" action="{"!docs/action.php"|local_url}" target="_dialog">
<table class="list">
	<thead>
		<tr>

			<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>

			<th>Nom</th>
			<td>Modifié</td>
			<td>Type</td>
			<td>Taille</td>
			<td></td>
		</tr>
	</thead>
................................................................................
				{input type="checkbox" name="check[]" value=$file.path}
			</td>
			{/if}
			<th><a href="?p={$file.path}">{$file.name}</a></th>
			<td></td>
			<td>Répertoire</td>
			<td></td>





			<td class="actions">{linkbutton href="!common/files/delete.php?p=%s"|args:$file.path label="Supprimer" shape="delete" target="_dialog"}</td>


		</tr>
		{else}
		</tr>
			{if $can_delete}
			<td class="check">
				{input type="checkbox" name="check[]" value=$file.path}
			</td>
................................................................................
				{if $can_write && $file->getEditor()}
					{linkbutton href="!common/files/edit.php?p=%s"|args:$file.path label="Modifier" shape="edit" target="_dialog" data-dialog-height="90%"}
				{/if}
				{if $file->canPreview()}
					{linkbutton href="!common/files/preview.php?p=%s"|args:$file.path label="Voir" shape="eye" target="_dialog" data-mime=$file.mime}
				{/if}
				{linkbutton href=$file->url(true) label="Télécharger" shape="download"}




				{linkbutton href="!common/files/delete.php?p=%s"|args:$file.path label="Supprimer" shape="delete" target="_dialog"}

			</td>
		</tr>
		{/if}
	{/foreach}

	</tbody>
	{if $can_delete}







|


|







 







>
|
>







 







>
>
>
>
>
|
>
>







 







>
>
>
>
|
>







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
..
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
...
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
	{if $can_upload}
		{linkbutton shape="plus" label="Nouveau fichier texte" target="_dialog" href="!docs/new_file.php?p=%s"|args:$path}
		{linkbutton shape="upload" label="Ajouter un fichier" target="_dialog" href="!common/files/upload.php?p=%s"|args:$path}
	{/if}
	</aside>
	<ul>
		<li{if $context == File::CONTEXT_DOCUMENTS} class="current"{/if}><a href="./">Documents</a></li>
		{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)}
			<li{if $context == File::CONTEXT_TRANSACTION} class="current"{/if}><a href="./?p=<?=File::CONTEXT_TRANSACTION?>">Fichiers des écritures</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)}
			<li{if $context == File::CONTEXT_USER} class="current"{/if}><a href="./?p=<?=File::CONTEXT_USER?>">Fichiers des membres</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)}
			<li{if $context == File::CONTEXT_SKELETON} class="current"{/if}><a href="./?p=<?=File::CONTEXT_SKELETON?>">Squelettes du site web</a></li>
		{/if}
	</ul>

................................................................................
{/if}

{if count($files)}
<form method="post" action="{"!docs/action.php"|local_url}" target="_dialog">
<table class="list">
	<thead>
		<tr>
			{if $can_delete}
				<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>
			{/if}
			<th>Nom</th>
			<td>Modifié</td>
			<td>Type</td>
			<td>Taille</td>
			<td></td>
		</tr>
	</thead>
................................................................................
				{input type="checkbox" name="check[]" value=$file.path}
			</td>
			{/if}
			<th><a href="?p={$file.path}">{$file.name}</a></th>
			<td></td>
			<td>Répertoire</td>
			<td></td>
			<td class="actions">
			{if $can_write}
				{linkbutton href="!common/files/rename.php?p=%s"|args:$file.path label="Renommer" shape="minus" target="_dialog"}
			{/if}
			{if $can_delete}
				{linkbutton href="!common/files/delete.php?p=%s"|args:$file.path label="Supprimer" shape="delete" target="_dialog"}
			{/if}
			</td>
		</tr>
		{else}
		</tr>
			{if $can_delete}
			<td class="check">
				{input type="checkbox" name="check[]" value=$file.path}
			</td>
................................................................................
				{if $can_write && $file->getEditor()}
					{linkbutton href="!common/files/edit.php?p=%s"|args:$file.path label="Modifier" shape="edit" target="_dialog" data-dialog-height="90%"}
				{/if}
				{if $file->canPreview()}
					{linkbutton href="!common/files/preview.php?p=%s"|args:$file.path label="Voir" shape="eye" target="_dialog" data-mime=$file.mime}
				{/if}
				{linkbutton href=$file->url(true) label="Télécharger" shape="download"}
				{if $can_write}
					{linkbutton href="!common/files/rename.php?p=%s"|args:$file.path label="Renommer" shape="minus" target="_dialog"}
				{/if}
				{if $can_delete}
					{linkbutton href="!common/files/delete.php?p=%s"|args:$file.path label="Supprimer" shape="delete" target="_dialog"}
				{/if}
			</td>
		</tr>
		{/if}
	{/foreach}

	</tbody>
	{if $can_delete}

Modified src/templates/services/details.tpl from [d4fb23f692] to [f17fcbe21b].

10
11
12
13
14
15
16








17
18
19
20




21
22
23
24
25
26
27
..
40
41
42
43
44
45
46




47




48
49
50
51
52
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}
		{linkbutton href="%s&export=csv"|args:$self_url shape="export" label="Export CSV"}
		{linkbutton href="%s&export=ods"|args:$self_url shape="export" label="Export tableur"}
		{/if}
	</dd>
</dl>









{include file="common/dynamic_list_head.tpl"}

	{foreach from=$list->iterate() item="row"}
		<tr>




			<th><a href="../membres/fiche.php?id={$row.id_user}">{$row.identity}</a></th>
			<td>
				{if $row.status == 1 && $row.end_date}
					En cours
				{elseif $row.status == 1}
					<b class="confirm">À jour</b>
				{elseif $row.status == -1 && $row.end_date}
................................................................................
				{linkbutton shape="user" label="Toutes les activités de ce membre" href="!services/user.php?id=%d"|args:$row.id_user}
				{*FIXME TODO linkbutton shape="alert" label="Rappels envoyés" href="!services/reminders/user.php?id=%d"|args:$row.id_user*}
			</td>
		</tr>
	{/foreach}

	</tbody>




</table>





{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}


{include file="admin/_foot.tpl"}







>
>
>
>
>
>
>
>
|



>
>
>
>







 







>
>
>
>

>
>
>
>





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
..
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}
		{linkbutton href="%s&export=csv"|args:$self_url shape="export" label="Export CSV"}
		{linkbutton href="%s&export=ods"|args:$self_url shape="export" label="Export tableur"}
		{/if}
	</dd>
</dl>

<?php
$can_action = $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN);
?>

{if $can_action}
	<form method="post" action="{"!membres/action.php"|local_url}">
{/if}

{include file="common/dynamic_list_head.tpl" check=$can_action}

	{foreach from=$list->iterate() item="row"}
		<tr>
			{if $can_action}
			<td class="check">{input type="checkbox" name="selected[]" value=$row.id_user}</td>
			{/if}

			<th><a href="../membres/fiche.php?id={$row.id_user}">{$row.identity}</a></th>
			<td>
				{if $row.status == 1 && $row.end_date}
					En cours
				{elseif $row.status == 1}
					<b class="confirm">À jour</b>
				{elseif $row.status == -1 && $row.end_date}
................................................................................
				{linkbutton shape="user" label="Toutes les activités de ce membre" href="!services/user.php?id=%d"|args:$row.id_user}
				{*FIXME TODO linkbutton shape="alert" label="Rappels envoyés" href="!services/reminders/user.php?id=%d"|args:$row.id_user*}
			</td>
		</tr>
	{/foreach}

	</tbody>
	{if $can_action}
		{include file="admin/membres/_list_actions.tpl" colspan=7 export=false}
	{/if}

</table>

{if $can_action}
</form>
{/if}

{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}


{include file="admin/_foot.tpl"}

Modified src/templates/services/fees/details.tpl from [238f21a2af] to [883468d464].

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
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}
			{linkbutton href="%s&export=csv"|args:$self_url shape="export" label="Export CSV"}
			{linkbutton href="%s&export=ods"|args:$self_url shape="export" label="Export tableur"}
		{/if}
	</dd>
</dl>









{include file="common/dynamic_list_head.tpl"}

	{foreach from=$list->iterate() item="row"}
		<tr>



			<th><a href="../../membres/fiche.php?id={$row.id_user}">{$row.identity}</a></th>
			<td>{if $row.paid}<b class="confirm">Oui</b>{else}<b class="error">Non</b>{/if}</td>
			<td class="money">{$row.paid_amount|raw|money_currency}</td>
			<td>{$row.date|date_short}</td>
			<td class="actions">
				{linkbutton shape="user" label="Toutes les activités de ce membre" href="!services/user.php?id=%d"|args:$row.id_user}
				{* FIXME TODO {linkbutton shape="alert" label="Rappels envoyés" href="!services/reminders/user.php?id=%d"|args:$row.id_user} *}
			</td>
		</tr>
	{/foreach}

	</tbody>





</table>





{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}


{include file="admin/_foot.tpl"}







>
>
>
>
>
>
>
>
|



>
>
>












>
>
>
>
>

>
>
>
>





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
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}
			{linkbutton href="%s&export=csv"|args:$self_url shape="export" label="Export CSV"}
			{linkbutton href="%s&export=ods"|args:$self_url shape="export" label="Export tableur"}
		{/if}
	</dd>
</dl>

<?php
$can_action = $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN);
?>

{if $can_action}
	<form method="post" action="{"!membres/action.php"|local_url}">
{/if}

{include file="common/dynamic_list_head.tpl" check=$can_action}

	{foreach from=$list->iterate() item="row"}
		<tr>
			{if $can_action}
			<td class="check">{input type="checkbox" name="selected[]" value=$row.id_user}</td>
			{/if}
			<th><a href="../../membres/fiche.php?id={$row.id_user}">{$row.identity}</a></th>
			<td>{if $row.paid}<b class="confirm">Oui</b>{else}<b class="error">Non</b>{/if}</td>
			<td class="money">{$row.paid_amount|raw|money_currency}</td>
			<td>{$row.date|date_short}</td>
			<td class="actions">
				{linkbutton shape="user" label="Toutes les activités de ce membre" href="!services/user.php?id=%d"|args:$row.id_user}
				{* FIXME TODO {linkbutton shape="alert" label="Rappels envoyés" href="!services/reminders/user.php?id=%d"|args:$row.id_user} *}
			</td>
		</tr>
	{/foreach}

	</tbody>

	{if $can_action}
		{include file="admin/membres/_list_actions.tpl" colspan=5 export=false}
	{/if}

</table>

{if $can_action}
</form>
{/if}

{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}


{include file="admin/_foot.tpl"}

Modified src/templates/web/config.tpl from [8397b3f71c] to [cbe2f68864].

96
97
98
99
100
101
102
103
104
105
106
107
108

109

110
111
112
113
114
115
116
					<td class="check"></td>
					<th>Fichier</th>
					<td>Dernière modification</td>
					<td></td>
				</tr>
			</thead>
			<tbody>
			{foreach from=$sources key="source" item="local"}
				<tr>
					<td>{if $local.modified}<input type="checkbox" name="select[]" value="{$source}" id="f_source_{$iteration}" /><label for="f_source_{$iteration}"></label>{/if}</td>
					<th><a href="?edit={$source|escape:'url'}" title="Éditer">{$source}</a></th>
					<td>{if $local.modified}{$local.modified|date}{else}<em>(fichier non modifié)</em>{/if}</td>
					<td class="actions">

						{linkbutton shape="edit" label="Éditer" href="?edit=%s"|args:$source}

					</td>
				</tr>
			{/foreach}
			</tbody>
		</table>

		<p class="actions">







|

|

|

>

>







96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
					<td class="check"></td>
					<th>Fichier</th>
					<td>Dernière modification</td>
					<td></td>
				</tr>
			</thead>
			<tbody>
			{foreach from=$sources key="source" item="props"}
				<tr>
					<td>{if $props.changed}<input type="checkbox" name="select[]" value="{$source}" id="f_source_{$iteration}" /><label for="f_source_{$iteration}"></label>{/if}</td>
					<th><a href="?edit={$source|escape:'url'}" title="Éditer">{$source}</a></th>
					<td>{if $props.changed}{$props.changed|date}{else}<em>(fichier non modifié)</em>{/if}</td>
					<td class="actions">
						{if $props.is_text}
						{linkbutton shape="edit" label="Éditer" href="?edit=%s"|args:$source}
						{/if}
					</td>
				</tr>
			{/foreach}
			</tbody>
		</table>

		<p class="actions">

Modified src/templates/web/index.tpl from [ecc94b7c86] to [a8040f4151].

1
2
3
4
5

6
7

8
9
10
11
12
13
14
..
25
26
27
28
29
30
31

32

33
34
35
36
37
38
39
40
41
42
43
44
..
50
51
52
53
54
55
56

57
58

59
60
61
62
63
64
65
..
81
82
83
84
85
86
87

88
89

90
91
92
93
94
95
96
97
98
{include file="admin/_head.tpl" title=$title current="web"}

<nav class="tabs">
	<aside>
		{linkbutton shape="search" label="Rechercher" target="_dialog" href="search.php"}

		{linkbutton shape="plus" label="Nouvelle page" target="_dialog" href="new.php?type=%d&parent=%s"|args:$type_page,$current_path}
		{linkbutton shape="plus" label="Nouvelle catégorie" target="_dialog" href="new.php?type=%d&parent=%s"|args:$type_category,$current_path}

	</aside>
	<ul>
		<li class="current"><a href="./">Gestion du site web</a></li>
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)}
			{*<li><a href="theme.php">Thèmes</a></li>*}
			<li><a href="config.php">Configuration</a></li>
		{/if}
................................................................................
			<li><a href="?p={$id}">{$title}</a></li>
		{/foreach}
	</ul>

	{if $current_path}
		{linkbutton href="?p=%s"|args:$parent label="Retour à la catégorie parente" shape="left"}
		{linkbutton href="page.php?p=%s"|args:$current_path label="Prévisualiser cette catégorie" shape="image"}

		{linkbutton href="edit.php?p=%s"|args:$current_path label="Éditer cette catégorie" shape="edit"}

	{/if}

</nav>

{if $config.site_disabled}
	<p class="block alert">
		Le site public est désactivé. <a href="{"!web/config.php"|local_url}">Activer le site dans la configuration.</a>
	</p>
{/if}

{if count($categories)}
	<h2 class="ruler">Catégories</h2>
................................................................................
				<td>{if $p.status == $p::STATUS_ONLINE}En ligne{else}<em>Brouillon</em>{/if}</td>
				<td class="actions">
					{if $p.status == $p::STATUS_ONLINE && !$config.site_disabled}
						{linkbutton shape="eye" label="Voir sur le site" href=$p->url() target="_blank"}
					{/if}
					{linkbutton shape="menu" label="Sous-catégories et pages" href="?p=%s"|args:$p.path}
					{linkbutton shape="image" label="Prévisualiser" href="page.php?p=%s"|args:$p.path}

					{linkbutton shape="edit" label="Éditer" href="edit.php?p=%s"|args:$p.path}
					{linkbutton shape="delete" label="Supprimer" target="_dialog" href="delete.php?p=%s"|args:$p.path}

				</td>
			</tr>
			{/foreach}
		</tbody>
	</table>
{/if}

................................................................................
				<td>{$p.created|date_short}</td>
				<td>Modifié {$p.modified|relative_date:true}</td>
				<td class="actions">
					{if $p.status == $p::STATUS_ONLINE}
						{linkbutton shape="eye" label="Voir sur le site" href=$p->url() target="_blank"}
					{/if}
					{linkbutton shape="image" label="Prévisualiser" href="page.php?p=%s"|args:$p.path}

					{linkbutton shape="edit" label="Éditer" href="edit.php?p=%s"|args:$p.path}
					{linkbutton shape="delete" label="Supprimer" target="_dialog" href="delete.php?p=%s"|args:$p.path}

				</td>
			</tr>
			{/foreach}
		</tbody>
	</table>
{/if}


{include file="admin/_foot.tpl"}





>


>







 







>

>




|







 







>


>







 







>


>









1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
..
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
..
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
..
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
{include file="admin/_head.tpl" title=$title current="web"}

<nav class="tabs">
	<aside>
		{linkbutton shape="search" label="Rechercher" target="_dialog" href="search.php"}
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE)}
		{linkbutton shape="plus" label="Nouvelle page" target="_dialog" href="new.php?type=%d&parent=%s"|args:$type_page,$current_path}
		{linkbutton shape="plus" label="Nouvelle catégorie" target="_dialog" href="new.php?type=%d&parent=%s"|args:$type_category,$current_path}
		{/if}
	</aside>
	<ul>
		<li class="current"><a href="./">Gestion du site web</a></li>
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)}
			{*<li><a href="theme.php">Thèmes</a></li>*}
			<li><a href="config.php">Configuration</a></li>
		{/if}
................................................................................
			<li><a href="?p={$id}">{$title}</a></li>
		{/foreach}
	</ul>

	{if $current_path}
		{linkbutton href="?p=%s"|args:$parent label="Retour à la catégorie parente" shape="left"}
		{linkbutton href="page.php?p=%s"|args:$current_path label="Prévisualiser cette catégorie" shape="image"}
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE)}
		{linkbutton href="edit.php?p=%s"|args:$current_path label="Éditer cette catégorie" shape="edit"}
		{/if}
	{/if}

</nav>

{if $config.site_disabled && $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)}
	<p class="block alert">
		Le site public est désactivé. <a href="{"!web/config.php"|local_url}">Activer le site dans la configuration.</a>
	</p>
{/if}

{if count($categories)}
	<h2 class="ruler">Catégories</h2>
................................................................................
				<td>{if $p.status == $p::STATUS_ONLINE}En ligne{else}<em>Brouillon</em>{/if}</td>
				<td class="actions">
					{if $p.status == $p::STATUS_ONLINE && !$config.site_disabled}
						{linkbutton shape="eye" label="Voir sur le site" href=$p->url() target="_blank"}
					{/if}
					{linkbutton shape="menu" label="Sous-catégories et pages" href="?p=%s"|args:$p.path}
					{linkbutton shape="image" label="Prévisualiser" href="page.php?p=%s"|args:$p.path}
					{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE)}
					{linkbutton shape="edit" label="Éditer" href="edit.php?p=%s"|args:$p.path}
					{linkbutton shape="delete" label="Supprimer" target="_dialog" href="delete.php?p=%s"|args:$p.path}
					{/if}
				</td>
			</tr>
			{/foreach}
		</tbody>
	</table>
{/if}

................................................................................
				<td>{$p.created|date_short}</td>
				<td>Modifié {$p.modified|relative_date:true}</td>
				<td class="actions">
					{if $p.status == $p::STATUS_ONLINE}
						{linkbutton shape="eye" label="Voir sur le site" href=$p->url() target="_blank"}
					{/if}
					{linkbutton shape="image" label="Prévisualiser" href="page.php?p=%s"|args:$p.path}
					{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE)}
					{linkbutton shape="edit" label="Éditer" href="edit.php?p=%s"|args:$p.path}
					{linkbutton shape="delete" label="Supprimer" target="_dialog" href="delete.php?p=%s"|args:$p.path}
					{/if}
				</td>
			</tr>
			{/foreach}
		</tbody>
	</table>
{/if}


{include file="admin/_foot.tpl"}

Modified src/templates/web/page.tpl from [a3e822ccef] to [419cb9b89a].

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
		<li><a href="{$admin_url}web/?p={$page.parent}">Retour à la liste</a></li>
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE)}
			<li><a href="{$admin_url}web/edit.php?p={$page.path}">Modifier</a></li>
		{/if}
		{if $page.status == $page::STATUS_ONLINE && !$config.site_disabled}
			<li><a href="{$page->url()}" target="_blank">Voir sur le site</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)}
			<li><a href="{$admin_url}web/delete.php?p={$page.path}">Supprimer</a></li>
		{/if}
	</ul>
</nav>

{if !empty($breadcrumbs)}
<nav class="breadcrumbs">







|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
		<li><a href="{$admin_url}web/?p={$page.parent}">Retour à la liste</a></li>
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE)}
			<li><a href="{$admin_url}web/edit.php?p={$page.path}">Modifier</a></li>
		{/if}
		{if $page.status == $page::STATUS_ONLINE && !$config.site_disabled}
			<li><a href="{$page->url()}" target="_blank">Voir sur le site</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE)}
			<li><a href="{$admin_url}web/delete.php?p={$page.path}">Supprimer</a></li>
		{/if}
	</ul>
</nav>

{if !empty($breadcrumbs)}
<nav class="breadcrumbs">

Modified src/www/.htaccess from [5189696f0f] to [229f2663a8].

1
2
3
4






5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Options -MultiViews -Indexes
DirectoryIndex disabled
DirectoryIndex index.php







# FallbackResource n'est dispo que depuis Apache 2.2.16, soit Debian Wheezy (2013)
# Mais bugue avant Apache 2.4.15, il faut donc bien désactiver le DirectoryIndex
# cf. https://bz.apache.org/bugzilla/show_bug.cgi?id=58292
# et https://serverfault.com/questions/559067/apache-hangs-for-five-seconds-with-fallbackresource-when-accessing
<IfModule mod_version.c>
	<IfVersion >= 2.2.16>
		FallbackResource /_route.php
	</IfVersion>
</IfModule>

# Utilisation de ErrorDocument 404 à la place de FallbackResource si possible
ErrorDocument 404 /_route.php

# Un peu de sécurité
<IfModule mod_alias.c>
	RedirectMatch 404 _inc\.php
</IfModule>




>
>
>
>
>
>
|



<
<
<
<
<
<
<
<





1
2
3
4
5
6
7
8
9
10
11
12
13
14








15
16
17
18
19
Options -MultiViews -Indexes
DirectoryIndex disabled
DirectoryIndex index.php

# Rediriger les adresses dynamiques vers le routeur
FallbackResource /_route.php

# Si FallbackResource ne fonctionne pas, utiliser ceci :
#ErrorDocument 404 /_route.php

# Explication : FallbackResource n'est dispo que depuis Apache 2.2.16, soit Debian Wheezy (2013)
# Mais bugue avant Apache 2.4.15, il faut donc bien désactiver le DirectoryIndex
# cf. https://bz.apache.org/bugzilla/show_bug.cgi?id=58292
# et https://serverfault.com/questions/559067/apache-hangs-for-five-seconds-with-fallbackresource-when-accessing









# Un peu de sécurité
<IfModule mod_alias.c>
	RedirectMatch 404 _inc\.php
</IfModule>

Modified src/www/_route.php from [044eb9e8fb] to [9acabfe9b8].

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;

if (empty($_SERVER['REQUEST_URI'])) {

	die('Appel non supporté');
}

$uri = $_SERVER['REQUEST_URI'];

if ('_route.php' === basename($uri)) {

	die('Appel interdit');
}



if ('favicon.ico' === basename($uri)) {
	die('');
}

if (($pos = strpos($uri, '?')) !== false)
{
	$uri = substr($uri, 0, $pos);
}

if (file_exists(__DIR__ . $uri))
{
	if (PHP_SAPI != 'cli-server') {

		die('Erreur de configuration du serveur web: cette URL ne devrait pas être traitée par Garradin');
	}

	return false;
}
elseif (preg_match('!/p/(.+?)/(.*)!', $uri, $match))
{





>






>


>
>













>







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
<?php

namespace Garradin;

if (empty($_SERVER['REQUEST_URI'])) {
	http_response_code(500);
	die('Appel non supporté');
}

$uri = $_SERVER['REQUEST_URI'];

if ('_route.php' === basename($uri)) {
	http_response_code(403);
	die('Appel interdit');
}

http_response_code(200);

if ('favicon.ico' === basename($uri)) {
	die('');
}

if (($pos = strpos($uri, '?')) !== false)
{
	$uri = substr($uri, 0, $pos);
}

if (file_exists(__DIR__ . $uri))
{
	if (PHP_SAPI != 'cli-server') {
		http_response_code(500);
		die('Erreur de configuration du serveur web: cette URL ne devrait pas être traitée par Garradin');
	}

	return false;
}
elseif (preg_match('!/p/(.+?)/(.*)!', $uri, $match))
{

Added src/www/admin/acc/years/export.php version [5fa4e489df].





































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<?php
namespace Garradin;

use Garradin\Accounting\Transactions;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN);

$year_id = (int) qg('id') ?: CURRENT_YEAR_ID;

if ($year_id === CURRENT_YEAR_ID) {
	$year = $current_year;
}
else {
	$year = Years::get($year_id);
}

if (!$year) {
	throw new UserException("L'exercice demandé n'existe pas.");
}

$format = qg('format');
$type = qg('type');

if (null !== $format && null !== $type) {
	Transactions::export($year, $format, $type);
	exit;
}

$tpl->assign(compact('year'));

$tpl->display('acc/years/export.tpl');

Added src/www/admin/common/files/rename.php version [b9ec601421].



































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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\Entities\Files\File;
use Garradin\Files\Files;

require __DIR__ . '/../../_inc.php';

$file = Files::get(qg('p'));

if (!$file) {
	throw new UserException('Fichier inconnu');
}

if (!$file->checkWriteAccess($session)) {
    throw new UserException('Vous n\'avez pas le droit de supprimer ce fichier.');
}

$context = $file->context();

if ($context == File::CONTEXT_CONFIG || $context == File::CONTEXT_WEB) {
	throw new UserException('Vous n\'avez pas le droit de renommer ce fichier.');
}

$csrf_key = 'file_rename_' . $file->pathHash();

$form->runIf('rename', function () use ($file) {
	$file->changeFileName(f('new_name'));
}, $csrf_key, '!');

$tpl->assign(compact('file', 'csrf_key'));

$tpl->display('common/files/rename.tpl');

Modified src/www/admin/config/advanced/index.php from [68a26e2219] to [391415153c].

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Garradin;

use Garradin\Accounting\Years;
use Garradin\Files\Files;

require_once __DIR__ . '/../_inc.php';

$quota_used = Files::getUsedQuota();

$form->runIf('reset_ok', function () use ($session) {
	Install::reset($session, f('passe_verif'));
}, 'reset', Utils::getSelfURI(['msg' => 'RESET']));

$form->runIf('reopen_ok', function () use ($session) {
	$year = Years::get((int) f('year'));







|







2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Garradin;

use Garradin\Accounting\Years;
use Garradin\Files\Files;

require_once __DIR__ . '/../_inc.php';

$quota_used = Files::getUsedQuota(true);

$form->runIf('reset_ok', function () use ($session) {
	Install::reset($session, f('passe_verif'));
}, 'reset', Utils::getSelfURI(['msg' => 'RESET']));

$form->runIf('reopen_ok', function () use ($session) {
	$year = Years::get((int) f('year'));

Deleted src/www/admin/config/backup/automatique.php version [05a939d15b].

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
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

if (!ENABLE_AUTOMATIC_BACKUPS)
{
    throw new UserException('Les sauvegardes automatiques sont désactivées.');
}

if (f('config'))
{
    $form->check('backup_config', [
        'frequence_sauvegardes' => 'present|numeric|min:0|max:365',
        'nombre_sauvegardes' => 'present|numeric|min:1|max:90',
    ]);

    if (!$form->hasErrors())
    {
        try {
            $config->set('frequence_sauvegardes', f('frequence_sauvegardes'));
            $config->set('nombre_sauvegardes', f('nombre_sauvegardes'));
            $config->save();

            Utils::redirect(ADMIN_URL . 'config/donnees/automatique.php?ok=config');
        } catch (UserException $e) {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('ok', qg('ok'));

$tpl->display('admin/config/donnees/automatique.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































































Modified src/www/admin/config/backup/restore.php from [6ef87fa07a] to [c5bad54374].

1
2
3
4
5
6
7





8
9
10
11
12
13
14
..
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$s = new Sauvegarde;
$code = null; // error code






$form->runIf('restore', function () use ($s) {
	if (!f('selected')) {
		throw new UserException('Aucune sauvegarde sélectionnée');
	}

	$r = $s->restoreFromLocal(f('selected'));
................................................................................

$form->runIf('restore_file', function () use ($s, &$code, $session) {
	// Ignorer la vérification d'intégrité si autorisé et demandé
	$check = (ALLOW_MODIFIED_IMPORT && f('force_import')) ? false : true;

	try {
		$r = $s->restoreFromUpload($_FILES['file'], $session->getUser()->id, $check);
		Utils::redirect(ADMIN_URL . 'config/donnees/?ok=restore&code=' . (int)$r);
	} catch (UserException $e) {
		$code = $e->getCode();
	}
}, 'backup_restore');


$ok_code = qg('code'); // return code







>
>
>
>
>







 







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
..
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$s = new Sauvegarde;
$code = null; // error code

if (qg('download')) {
	$s->dump(qg('download'));
	exit;
}

$form->runIf('restore', function () use ($s) {
	if (!f('selected')) {
		throw new UserException('Aucune sauvegarde sélectionnée');
	}

	$r = $s->restoreFromLocal(f('selected'));
................................................................................

$form->runIf('restore_file', function () use ($s, &$code, $session) {
	// Ignorer la vérification d'intégrité si autorisé et demandé
	$check = (ALLOW_MODIFIED_IMPORT && f('force_import')) ? false : true;

	try {
		$r = $s->restoreFromUpload($_FILES['file'], $session->getUser()->id, $check);
		Utils::redirect(Utils::getSelfURI(['ok' => 'restore', 'code' => (int)$r]));
	} catch (UserException $e) {
		$code = $e->getCode();
	}
}, 'backup_restore');


$ok_code = qg('code'); // return code

Modified src/www/admin/config/categories/modifier.php from [fc55af1c22] to [4d5e12122b].

16
17
18
19
20
21
22

23
24
25
26
27
28
29
$user = $session->getUser();

$csrf_key = 'cat_edit_' . $cat->id();

$form->runIf('save', function () use ($cat, $session) {
	$user = $session->getUser();
	$cat->importForm();


	// Ne pas permettre de modifier la connexion, l'accès à la config et à la gestion des membres
	// pour la catégorie du membre qui édite les catégories, sinon il pourrait s'empêcher
	// de se connecter ou n'avoir aucune catégorie avec le droit de modifier les catégories !
	if ($cat->id() == $user->id_category) {
		$cat->set('perm_connect', Session::ACCESS_READ);
		$cat->set('perm_config', Session::ACCESS_ADMIN);







>







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$user = $session->getUser();

$csrf_key = 'cat_edit_' . $cat->id();

$form->runIf('save', function () use ($cat, $session) {
	$user = $session->getUser();
	$cat->importForm();
	$cat->hidden = (int) f('hidden');

	// Ne pas permettre de modifier la connexion, l'accès à la config et à la gestion des membres
	// pour la catégorie du membre qui édite les catégories, sinon il pourrait s'empêcher
	// de se connecter ou n'avoir aucune catégorie avec le droit de modifier les catégories !
	if ($cat->id() == $user->id_category) {
		$cat->set('perm_connect', Session::ACCESS_READ);
		$cat->set('perm_config', Session::ACCESS_ADMIN);

Added src/www/admin/config/edit_file.php version [a041a95b07].













































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<?php
namespace Garradin;

use Garradin\Entities\Files\File;
use Garradin\Files\Files;

require __DIR__ . '/_inc.php';

$key = qg('k');

if (!isset(Config::DEFAULT_FILES[$key])) {
	throw new UserException('Fichier invalide');
}

$file_path = Config::DEFAULT_FILES[$key];

$file = Files::get($file_path);

if (!$file) {
	$file = File::create(Utils::dirname($file_path), Utils::basename($file_path), null, '');
	$content = '';
}
else {
	$content = $file->fetch();
}

$editor = $file->getEditor();
$csrf_key = 'edit_file_' . $file->pathHash();

$form->runIf('save', function () use ($file, $key) {
	// For config files, make sure config value is updated
	$config = Config::getInstance();

	if (trim(f('content')) === '') {
		$file->delete();
		$config->set($key, null);
		$config->save();
	}
	else {
		$file->setContent(f('content'));
		$config->set($key, $file->path);
		$config->save();
	}

	if (qg('js') !== null) {
		die('{"success":true}');
	}

}, $csrf_key, Utils::getSelfURI());

$tpl->assign('file', $file);

$tpl->assign(compact('csrf_key', 'content'));
$tpl->display(sprintf('common/files/edit_%s.tpl', $editor));

Modified src/www/admin/docs/index.php from [c2e76c334f] to [9eee8d2a3a].

37
38
39
40
41
42
43
44
45
46
47
48
$breadcrumbs = Files::getBreadcrumbs($path);

$parent_path = Utils::dirname($path);

$quota_used = Files::getUsedQuota();
$quota_max = Files::getQuota();
$quota_left = Files::getRemainingQuota();
$quota_percent = round(($quota_used / $quota_max) * 100);

$tpl->assign(compact('path', 'files', 'can_write', 'can_delete', 'can_mkdir', 'can_upload', 'context', 'context_ref', 'breadcrumbs', 'parent_path', 'quota_used', 'quota_max', 'quota_percent', 'quota_left'));

$tpl->display('docs/index.tpl');







|




37
38
39
40
41
42
43
44
45
46
47
48
$breadcrumbs = Files::getBreadcrumbs($path);

$parent_path = Utils::dirname($path);

$quota_used = Files::getUsedQuota();
$quota_max = Files::getQuota();
$quota_left = Files::getRemainingQuota();
$quota_percent = $quota_max ? round(($quota_used / $quota_max) * 100) : 100;

$tpl->assign(compact('path', 'files', 'can_write', 'can_delete', 'can_mkdir', 'can_upload', 'context', 'context_ref', 'breadcrumbs', 'parent_path', 'quota_used', 'quota_max', 'quota_percent', 'quota_left'));

$tpl->display('docs/index.tpl');

Modified src/www/admin/membres/modifier.php from [82d245779d] to [171580f332].

46
47
48
49
50
51
52




53
54
55
56
57
58
59
        try {
            $data = [];

            foreach ($champs->getAll() as $key=>$config)
            {
                $data[$key] = f($key);
            }





            if ($session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN) && $user->id != $membre->id)
            {
                $data['id_category'] = f('id_category');
                $data['id'] = f('id');
            }








>
>
>
>







46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
        try {
            $data = [];

            foreach ($champs->getAll() as $key=>$config)
            {
                $data[$key] = f($key);
            }

            if (f('delete_password')) {
                $data['delete_password'] = true;
            }

            if ($session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN) && $user->id != $membre->id)
            {
                $data['id_category'] = f('id_category');
                $data['id'] = f('id');
            }

Modified src/www/admin/static/scripts/code_editor.js from [a3a35cf3cc] to [81fbe437c7].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
..
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
(function (){
	g.style('scripts/code_editor.css');
	g.script('scripts/lib/code_editor.min.js', function ()
	{
		var save_btn = document.querySelector('[name=save]');
		save_btn.type = 'hidden';

		var code = new codeEditor('f_content');

		code.params.lang = {
			search: "Texte à chercher ?\n(expression régulière autorisée, pour cela commencer par un slash '/')",
			replace: "Texte pour le remplacement ?\n(utiliser $1, $2... pour les captures d'expression régulière)",
			search_selection: "Texte à chercher dans la sélection ?\n(expression régulière autorisée, pour cela commencer par un slash '/')",
			replace_result: "%d occurences trouvées et remplacées.",
................................................................................
		};

		code.origValue = code.textarea.value;
		code.saved = true;

		code.saveFile = function ()
		{
			this.textarea.form.submit();
		};

		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();
................................................................................
			window.parent.g.dialog.preventClose = () => {
				if (code.textarea.value == code.textarea.defaultValue) {
					return false;
				}

				if (window.confirm('Sauvegarder avant de fermer ?')) {
					code.saveFile();
					return false;
				}

				return true;
			};
		}
		else {
			appendButton('fullscreen', 'Plein écran', code.toggleFullscreen);
		}
	});
}());





<
<







 







|







 







<


|







1
2
3
4
5


6
7
8
9
10
11
12
..
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
..
77
78
79
80
81
82
83

84
85
86
87
88
89
90
91
92
93
(function (){
	g.style('scripts/code_editor.css');
	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.",
................................................................................
		};

		code.origValue = code.textarea.value;
		code.saved = true;

		code.saveFile = function ()
		{
			save_btn.click();
		};

		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();
................................................................................
			window.parent.g.dialog.preventClose = () => {
				if (code.textarea.value == code.textarea.defaultValue) {
					return false;
				}

				if (window.confirm('Sauvegarder avant de fermer ?')) {
					code.saveFile();

				}

				return false;
			};
		}
		else {
			appendButton('fullscreen', 'Plein écran', code.toggleFullscreen);
		}
	});
}());

Modified src/www/admin/static/scripts/selector.js from [8cf8f0f8b5] to [09cbc2d747].

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

		e.querySelector('button').click();
	};
});

document.addEventListener('keydown', (evt) => {
	let current = document.querySelector('tbody tr.focused:not(.hidden)') || document.querySelector('tbody tr');



	if (evt.key == 'ArrowUp') { // Previous item
		while (current && (current = current.previousElementSibling)) {

			if (!current.classList.contains('hidden')) {
				break;

			}
		}

	}
	else if (evt.key == 'ArrowDown') {
		while (current && (current = current.nextElementSibling)) {
			if (!current.classList.contains('hidden')) {
				break;
			}
		}
	}
	else if (evt.key == 'PageUp') {
		let i = 0;
		while (current && (current = current.previousElementSibling)) {
			if (i++ < 10) {
				continue;
			}
			if (!current.classList.contains('hidden')) {
				break;
			}


		}


	}
	else if (evt.key == 'PageDown') {
		let i = 0;
		while (current && (current = current.nextElementSibling)) {
			if (i++ < 10) {
				continue;

			}
			if (!current.classList.contains('hidden')) {
				break;


			}


		}


	}
	else {
		return true;
	}








	if (current) {
		current.querySelector('button').focus();
	}

	evt.preventDefault();
	return false;
});

buttons[0].focus();

var q = document.getElementById('lookup');







>
>

<
<
>
|
<
>
|
|
>
|
<
<
|
|
|
|
|
|
|
<
<
<
|
<
<
|
>
>
|
>
>

|
<
<
<
<
>
|
<
<
>
>
|
>
>
|
>
>





>
>
>
>
>
>
>
|
|
|
<







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

		e.querySelector('button').click();
	};
});

document.addEventListener('keydown', (evt) => {
	let current = document.querySelector('tbody tr.focused:not(.hidden)') || document.querySelector('tbody tr');
	let available = [];
	let idx = 0;



	for (var i = 0; i < rows.length; i++) {
		if (rows[i].classList.contains('hidden')) {

			continue;
		}

		available.push(rows[i]);



		if (rows[i] === current) {
			idx = available.length - 1;
		}
	}

	if (!available.length) {
		return false;



	}



	if (evt.key == 'ArrowUp') { // Previous item
		idx--;
	}
	else if (evt.key == 'ArrowDown') {
		idx++;
	}
	else if (evt.key == 'PageUp') {




		idx-=10;
	}


	else if (evt.key == 'PageDown') {
		idx+=10;
	}
	else if (evt.key == 'Home') {
		idx = 0;
	}
	else if (evt.key == 'End') {
		idx = available.length;
	}
	else {
		return true;
	}

	if (idx < 0) {
		idx = 0;
	}
	else if (idx >= available.length - 1) {
		idx = available.length - 1;
	}

	current = available[idx];
	current.querySelector('button').focus();


	evt.preventDefault();
	return false;
});

buttons[0].focus();

var q = document.getElementById('lookup');

Modified src/www/admin/static/scripts/wiki_editor.js from [1882f41586] to [8d430d7dbe].

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
			window.parent.g.dialog.preventClose = () => {
				if (t.textarea.value == t.textarea.defaultValue) {
					return false;
				}

				if (window.confirm('Sauvegarder avant de fermer ?')) {
					save();
					return false;
				}

				return true;
			};
		}

		var toolbar = document.createElement('nav');
		toolbar.className = 'te';

		var toggleFullscreen = function (e)







<


|







42
43
44
45
46
47
48

49
50
51
52
53
54
55
56
57
58
			window.parent.g.dialog.preventClose = () => {
				if (t.textarea.value == t.textarea.defaultValue) {
					return false;
				}

				if (window.confirm('Sauvegarder avant de fermer ?')) {
					save();

				}

				return false;
			};
		}

		var toolbar = document.createElement('nav');
		toolbar.className = 'te';

		var toggleFullscreen = function (e)

Modified src/www/admin/static/styles/02-common.css from [96523462ef] to [5c586c401b].

15
16
17
18
19
20
21










22
23
24
25
26
27
28
span.alert, b.alert {
    color: #990;
}

.alert p, .error p, .confirm p {
    margin-bottom: .8em;
}











.alert.block, .error.block, .confirm.block, .help.block {
    border: 1px solid #ccc;
    padding: .5em;
    margin-bottom: 1em;
    border-radius: .3em;
    padding-left: 3em;







>
>
>
>
>
>
>
>
>
>







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
span.alert, b.alert {
    color: #990;
}

.alert p, .error p, .confirm p {
    margin-bottom: .8em;
}

.block table {
    margin: 1rem 0;
}

.block table th, .block table td {
    vertical-align: top;
    padding: .2rem .4rem;
    border: 1px solid #666;
}

.alert.block, .error.block, .confirm.block, .help.block {
    border: 1px solid #ccc;
    padding: .5em;
    margin-bottom: 1em;
    border-radius: .3em;
    padding-left: 3em;

Modified src/www/admin/static/styles/03-forms.css from [c5c1f73efc] to [249e20cb24].

383
384
385
386
387
388
389

390
391
392
393
394
395
396
}

dialog[open] {
    display: block;
}

dialog.datepicker {

    position: absolute;
    left: 0;
    margin: 0;
    padding: .3rem;
    border: none;
    box-shadow: 0 0 5px #000;
    border-radius: .5rem;







>







383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
}

dialog[open] {
    display: block;
}

dialog.datepicker {
    user-select: none;
    position: absolute;
    left: 0;
    margin: 0;
    padding: .3rem;
    border: none;
    box-shadow: 0 0 5px #000;
    border-radius: .5rem;

Modified src/www/admin/web/_attach.php from [a59c3eef8c] to [c602997b78].

16
17
18
19
20
21
22
23

24
25
26
27
28
29
30

31

32
33
34
35
36
37
38

if (!$page) {
	throw new UserException('Page inconnue');
}

$csrf_key = 'attach_' . $page->id();

$form->runIf('delete', function () use ($session) {

	$file = Files::get(f('delete'));

	if (!$file || !$file->checkDeleteAccess($session)) {
		throw new UserException('Vous ne pouvez pas supprimer ce fichier');
	}

	$file->delete();

}, $csrf_key, Utils::getSelfURI());



$form->runIf(f('upload') || f('uploadHelper_mode'), function () use ($page) {
	if (f('uploadHelper_status') > 0) {
		throw new UserException('Un seul fichier peut être envoyé en même temps.');
	}








|
>
|






>
|
>







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

if (!$page) {
	throw new UserException('Page inconnue');
}

$csrf_key = 'attach_' . $page->id();

$form->runIf('delete', function () use ($page, $session) {
	$path = Utils::dirname($page->file_path) . '/' . f('delete');
	$file = Files::get($path);

	if (!$file || !$file->checkDeleteAccess($session)) {
		throw new UserException('Vous ne pouvez pas supprimer ce fichier');
	}

	$file->delete();

	Utils::redirect(Utils::getSelfURI());
}, $csrf_key);


$form->runIf(f('upload') || f('uploadHelper_mode'), function () use ($page) {
	if (f('uploadHelper_status') > 0) {
		throw new UserException('Un seul fichier peut être envoyé en même temps.');
	}

Modified src/www/admin/web/config.php from [ce640ecdba] to [3dd1c46ed9].

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

use Garradin\Web\Skeleton;

require_once __DIR__ . '/_inc.php';

$config = Config::getInstance();



if (f('disable_site') && $form->check('config_site'))
{
	$config->set('site_disabled', true);
	$config->save();
	Utils::redirect(Utils::getSelfURI());
}
elseif (f('enable_site') && $form->check('config_site'))
................................................................................
{
	$config->set('site_disabled', false);
	$config->save();
	Utils::redirect(Utils::getSelfURI());
}

$form->runIf('reset', function () {




	Skeleton::resetSelected(f('select'));
}, 'squelettes', Utils::getSelfURI('reset_ok'));

if (qg('edit')) {
	$source = trim(qg('edit'));
	$csrf_key = 'edit_skel_' . md5($source);

................................................................................
	$form->runIf('save', function () use ($source) {
		$tpl = new Skeleton($source);
		$tpl->edit(f('content'));
		$fullscreen = null !== qg('fullscreen') ? '#fullscreen' : '';
		Utils::redirect(Utils::getSelfURI(sprintf('edit=%s&ok%s', rawurlencode($source), $fullscreen)));
	}, $csrf_key);








	$tpl->assign('edit', ['file' => $source, 'content' => (new Skeleton($source))->raw()]);
	$tpl->assign('csrf_key', $csrf_key);
}

$tpl->assign('sources', Skeleton::list());

$tpl->assign('reset_ok', qg('reset_ok') !== null);
$tpl->assign('ok', qg('ok') !== null);

$tpl->display('web/config.tpl');







>
>







 







>
>
>
>







 







>
>
>
>
>
>
>
|









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

use Garradin\Web\Skeleton;

require_once __DIR__ . '/_inc.php';

$config = Config::getInstance();

$session->requireAccess($session::SECTION_WEB, $session::ACCESS_ADMIN);

if (f('disable_site') && $form->check('config_site'))
{
	$config->set('site_disabled', true);
	$config->save();
	Utils::redirect(Utils::getSelfURI());
}
elseif (f('enable_site') && $form->check('config_site'))
................................................................................
{
	$config->set('site_disabled', false);
	$config->save();
	Utils::redirect(Utils::getSelfURI());
}

$form->runIf('reset', function () {
	if (!f('select')) {
		return;
	}

	Skeleton::resetSelected(f('select'));
}, 'squelettes', Utils::getSelfURI('reset_ok'));

if (qg('edit')) {
	$source = trim(qg('edit'));
	$csrf_key = 'edit_skel_' . md5($source);

................................................................................
	$form->runIf('save', function () use ($source) {
		$tpl = new Skeleton($source);
		$tpl->edit(f('content'));
		$fullscreen = null !== qg('fullscreen') ? '#fullscreen' : '';
		Utils::redirect(Utils::getSelfURI(sprintf('edit=%s&ok%s', rawurlencode($source), $fullscreen)));
	}, $csrf_key);

	try {
		$skel = new Skeleton($source);
	}
	catch (\InvalidArgumentException $e) {
		throw new UserException('Nom de squelette invalide');
	}

	$tpl->assign('edit', ['file' => $source, 'content' => $skel->raw()]);
	$tpl->assign('csrf_key', $csrf_key);
}

$tpl->assign('sources', Skeleton::list());

$tpl->assign('reset_ok', qg('reset_ok') !== null);
$tpl->assign('ok', qg('ok') !== null);

$tpl->display('web/config.tpl');

Modified src/www/admin/web/delete.php from [cee4b59d69] to [8bec9db821].

1
2
3
4
5
6
7


8
9
10
11
12
13
14
<?php
namespace Garradin;

use Garradin\Web\Web;
use Garradin\Entities\Web\Page;

require_once __DIR__ . '/_inc.php';



$page = Web::get(qg('p'));

if (!$page) {
	throw new UserException('Page inconnue');
}








>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace Garradin;

use Garradin\Web\Web;
use Garradin\Entities\Web\Page;

require_once __DIR__ . '/_inc.php';

$session->requireAccess($session::SECTION_WEB, $session::ACCESS_WRITE);

$page = Web::get(qg('p'));

if (!$page) {
	throw new UserException('Page inconnue');
}

Modified src/www/admin/web/edit.php from [93999963a2] to [735f5d3003].

4
5
6
7
8
9
10


11
12
13
14
15
16
17

use Garradin\Web\Web;
use Garradin\Entities\Web\Page;
use Garradin\Entities\Files\File;
use KD2\SimpleDiff;

require_once __DIR__ . '/_inc.php';



$page = Web::get(qg('p'));

if (!$page) {
	throw new UserException('Page inconnue');
}








>
>







4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

use Garradin\Web\Web;
use Garradin\Entities\Web\Page;
use Garradin\Entities\Files\File;
use KD2\SimpleDiff;

require_once __DIR__ . '/_inc.php';

$session->requireAccess($session::SECTION_WEB, $session::ACCESS_WRITE);

$page = Web::get(qg('p'));

if (!$page) {
	throw new UserException('Page inconnue');
}

Modified src/www/admin/web/new.php from [edfbf26682] to [dae08411a4].

4
5
6
7
8
9
10


11
12
13
14
15
16
17

use Garradin\Web\Web;
use Garradin\Entities\Web\Page;
use Garradin\Entities\Files\File;
use KD2\SimpleDiff;

require_once __DIR__ . '/_inc.php';



$csrf_key = 'web_page_new';

$parent = qg('parent') ?: null;

$form->runIf('create', function () use ($parent) {
	$page = Page::create((int) qg('type'), $parent, f('title'), Page::STATUS_DRAFT);







>
>







4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

use Garradin\Web\Web;
use Garradin\Entities\Web\Page;
use Garradin\Entities\Files\File;
use KD2\SimpleDiff;

require_once __DIR__ . '/_inc.php';

$session->requireAccess($session::SECTION_WEB, $session::ACCESS_WRITE);

$csrf_key = 'web_page_new';

$parent = qg('parent') ?: null;

$form->runIf('create', function () use ($parent) {
	$page = Page::create((int) qg('type'), $parent, f('title'), Page::STATUS_DRAFT);

Modified src/www/skel-dist/_head.html from [785b737716] to [a4ef48e61c].

1
2
3
4
5

6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="fr">
<head>
	<meta charset="utf-8" />
	<title>{{if $title}}{{$title}} — {{/if}}{{$config.nom_asso}}</title>

	<meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
	<link rel="stylesheet" type="text/css" href="{{$root_url}}default.css" media="screen,projection,handheld" />
	<link rel="stylesheet" type="text/css" href="{{$root_url}}content.css" media="all" />
	<link rel="alternate" type="application/atom+xml" title="{{$config.nom_asso}}" href="{{$root_url}}atom.xml" />
</head>

<body>





>







1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="fr">
<head>
	<meta charset="utf-8" />
	<title>{{if $title}}{{$title}} — {{/if}}{{$config.nom_asso}}</title>
	<link rel="icon" type="image/png" href="{{$root_url}}favicon.png" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
	<link rel="stylesheet" type="text/css" href="{{$root_url}}default.css" media="screen,projection,handheld" />
	<link rel="stylesheet" type="text/css" href="{{$root_url}}content.css" media="all" />
	<link rel="alternate" type="application/atom+xml" title="{{$config.nom_asso}}" href="{{$root_url}}atom.xml" />
</head>

<body>

Name change from src/www/admin/static/icon.png to src/www/skel-dist/favicon.png.

cannot compute difference between binary files

Modified src/www/skel-dist/index.html from [e346bb1e71] to [64144e4af6].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{{:include file="_head.html"}}

{{#articles order="published DESC" future=false limit=1}}
<section class="articles main">
	<article>
		{{#images parent=$path limit=1}}
		<aside>
			<a href="{{$.url}}"><img src="{{$thumb_url}}" alt="" /></a>
		</aside>
		{{/images}}

		<h1><a href="{{ $url }}">{{ $title }}</a></h1>
		<h5>Posté : {{ $published|relative_date }}</h5>
		<p>{{ $html|raw|strip_tags|truncate:600 }}</p>
	</article>
</section>
{{/articles}}

{{#articles order="published DESC" future=false begin=1 limit=9}}
<section class="articles">
	<article>





<
<
<
<
<
<


|







1
2
3
4
5






6
7
8
9
10
11
12
13
14
15
{{:include file="_head.html"}}

{{#articles order="published DESC" future=false limit=1}}
<section class="articles main">
	<article>






		<h1><a href="{{ $url }}">{{ $title }}</a></h1>
		<h5>Posté : {{ $published|relative_date }}</h5>
		<p>{{ $html|raw }}</p>
	</article>
</section>
{{/articles}}

{{#articles order="published DESC" future=false begin=1 limit=9}}
<section class="articles">
	<article>