Overview
Comment:Merge trunk
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | templates
Files: files | file ages | folders
SHA3-256: bc5af9deb3c12e6bcc44970bbe047c1d7a4bf82953d2fa4c2772e0db39172734
User & Date: bohwaz on 2021-12-10 12:18:58
Other Links: branch diff | manifest | tags
Context
2021-12-10
12:24
Another try at templates check-in: e114b79178 user: bohwaz tags: templates
12:18
Merge trunk check-in: bc5af9deb3 user: bohwaz tags: templates
12:16
Generate change event from date picker check-in: 2850e9dfe9 user: bohwaz tags: trunk, stable
2021-11-25
12:58
Merge trunk check-in: b65e1fc90e user: bohwaz tags: templates
Changes

Modified src/VERSION from [37dd332b3b] to [64a3dc4a4c].

1
1.1.15
|
1
1.1.16

Modified src/include/init.php from [df903d941d] to [63c4993e30].

219
220
221
222
223
224
225

226
227
228
229
230
231
232
	}
}

if (!defined('Garradin\ADMIN_BACKGROUND_IMAGE')) {
	define('Garradin\ADMIN_BACKGROUND_IMAGE', ADMIN_URL . 'static/gdin_bg.png');
}


const WEBSITE = 'https://fossil.kd2.org/garradin/';
const PLUGINS_URL = 'https://garradin.eu/plugins/list.json';

const USER_TEMPLATES_CACHE_ROOT = CACHE_ROOT . '/utemplates';
const STATIC_CACHE_ROOT = CACHE_ROOT . '/static';
const SHARED_USER_TEMPLATES_CACHE_ROOT = SHARED_CACHE_ROOT . '/utemplates';
const SMARTYER_CACHE_ROOT = SHARED_CACHE_ROOT . '/compiled';







>







219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
	}
}

if (!defined('Garradin\ADMIN_BACKGROUND_IMAGE')) {
	define('Garradin\ADMIN_BACKGROUND_IMAGE', ADMIN_URL . 'static/gdin_bg.png');
}

const HELP_URL = 'https://garradin.eu/aide';
const WEBSITE = 'https://fossil.kd2.org/garradin/';
const PLUGINS_URL = 'https://garradin.eu/plugins/list.json';

const USER_TEMPLATES_CACHE_ROOT = CACHE_ROOT . '/utemplates';
const STATIC_CACHE_ROOT = CACHE_ROOT . '/static';
const SHARED_USER_TEMPLATES_CACHE_ROOT = SHARED_CACHE_ROOT . '/utemplates';
const SMARTYER_CACHE_ROOT = SHARED_CACHE_ROOT . '/compiled';

Modified src/include/lib/Garradin/Accounting/Graph.php from [af4ec17e7a] to [b470030203].

16
17
18
19
20
21
22



23
24
25
26
27
28
29

use KD2\Graphics\SVG\Plot;
use KD2\Graphics\SVG\Plot_Data;

use KD2\Graphics\SVG\Pie;
use KD2\Graphics\SVG\Pie_Data;




class Graph
{
	const URL_LIST = [
		ADMIN_URL . 'acc/reports/graph_plot.php?type=assets&%s' => 'Évolution banques et caisses',
		ADMIN_URL . 'acc/reports/graph_plot.php?type=result&%s' => 'Évolution dépenses et recettes',
		ADMIN_URL . 'acc/reports/graph_plot.php?type=debts&%s' => 'Évolution créances (positif) et dettes (négatif)',
		ADMIN_URL . 'acc/reports/graph_pie.php?type=revenue&%s' => 'Répartition recettes',







>
>
>







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

use KD2\Graphics\SVG\Plot;
use KD2\Graphics\SVG\Plot_Data;

use KD2\Graphics\SVG\Pie;
use KD2\Graphics\SVG\Pie_Data;

use KD2\Graphics\SVG\Bar;
use KD2\Graphics\SVG\Bar_Data_Set;

class Graph
{
	const URL_LIST = [
		ADMIN_URL . 'acc/reports/graph_plot.php?type=assets&%s' => 'Évolution banques et caisses',
		ADMIN_URL . 'acc/reports/graph_plot.php?type=result&%s' => 'Évolution dépenses et recettes',
		ADMIN_URL . 'acc/reports/graph_plot.php?type=debts&%s' => 'Évolution créances (positif) et dettes (négatif)',
		ADMIN_URL . 'acc/reports/graph_pie.php?type=revenue&%s' => 'Répartition recettes',
51
52
53
54
55
56
57

















58
59
60
61
62
63
64
		'revenue' => ['position' => Account::REVENUE, 'exclude_type' => Account::TYPE_VOLUNTEERING],
		'expense' => ['position' => Account::EXPENSE, 'exclude_type' => Account::TYPE_VOLUNTEERING],
		'assets' => ['type' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING]],
	];

	const WEEKLY_INTERVAL = 604800; // 7 days
	const MONTHLY_INTERVAL = 2635200; // 1 month


















	static public function plot(string $type, array $criterias, int $interval = self::WEEKLY_INTERVAL, int $width = 700)
	{
		if (!array_key_exists($type, self::PLOT_TYPES)) {
			throw new \InvalidArgumentException('Unknown type');
		}








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







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
		'revenue' => ['position' => Account::REVENUE, 'exclude_type' => Account::TYPE_VOLUNTEERING],
		'expense' => ['position' => Account::EXPENSE, 'exclude_type' => Account::TYPE_VOLUNTEERING],
		'assets' => ['type' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING]],
	];

	const WEEKLY_INTERVAL = 604800; // 7 days
	const MONTHLY_INTERVAL = 2635200; // 1 month

	static public function clearCache(string $type, array $criterias, int $interval = self::WEEKLY_INTERVAL, int $width = 700): void
	{
		if (!array_key_exists($type, self::PLOT_TYPES)) {
			throw new \InvalidArgumentException('Unknown type');
		}

		$cache_id = sha1('plot' . json_encode(func_get_args()));

		Static_Cache::remove($cache_id);
	}

	static public function clearCacheAllYears(): void
	{
		self::clearCache('assets', [], Graph::MONTHLY_INTERVAL, 600);
		self::clearCache('result', [], Graph::MONTHLY_INTERVAL, 600);
	}

	static public function plot(string $type, array $criterias, int $interval = self::WEEKLY_INTERVAL, int $width = 700)
	{
		if (!array_key_exists($type, self::PLOT_TYPES)) {
			throw new \InvalidArgumentException('Unknown type');
		}

175
176
177
178
179
180
181

































































182
183
184
185
186
187
188
		{
			$pie->add(new Pie_Data(abs($others) / 100, 'Autres', '#ccc'));
		}

		$pie->togglePercentage(true);

		$out = $pie->output();


































































		Static_Cache::store($cache_id, $out);

		return $out;
	}

	static protected function getColors()







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







195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
		{
			$pie->add(new Pie_Data(abs($others) / 100, 'Autres', '#ccc'));
		}

		$pie->togglePercentage(true);

		$out = $pie->output();

		Static_Cache::store($cache_id, $out);

		return $out;
	}

	static public function bar(string $type, array $criterias)
	{
		if (!array_key_exists($type, self::PLOT_TYPES)) {
			throw new \InvalidArgumentException('Unknown type');
		}

		$cache_id = sha1('bar' . json_encode(func_get_args()));

		if (!Static_Cache::expired($cache_id)) {
			return Static_Cache::get($cache_id);
		}

		$bar = new Bar(600, 300);

		$lines = self::PLOT_TYPES[$type];
		$data = [];

		$colors = self::getColors();

		foreach ($lines as $label => $line_criterias) {
			$color = current($colors);
			next($colors);

			$line_criterias = array_merge($criterias, $line_criterias);
			$years = Reports::getSumsPerYear($line_criterias);

			if (count($years) < 1) {
				continue;
			}

			// Invert sums for banks, cash, etc.
			if ('assets' === $type || 'debts' === $type || ('result' === $type && $line_criterias['position'] == Account::EXPENSE)) {
				array_walk($years, function (&$v) { $v->sum = $v->sum * -1; });
			}

			array_walk($years, function (&$v) { $v->sum = (int)$v->sum/100; });

			foreach ($years as $year) {
				$start = Utils::date_fr($year->start_date, 'Y');
				$end = Utils::date_fr($year->end_date, 'Y');
				$year_label = $start == $end ? $start : sprintf('%s-%s', $start, substr($end, -2));

				$year_id = $year_label . '-' . $year->id;

				if (!isset($data[$year_id])) {
					$data[$year_id] = new Bar_Data_Set($year_label);
				}

				$data[$year_id]->add($year->sum, $label, $color);
			}
		}

		ksort($data);

		foreach ($data as $group) {
			$bar->add($group);
		}

		$out = $bar->output();

		Static_Cache::store($cache_id, $out);

		return $out;
	}

	static protected function getColors()

Modified src/include/lib/Garradin/Accounting/Reports.php from [d37a6b0099] to [b455a0ddd1].

156
157
158
159
160
161
162















163
164
165
166
167
168
169
			return;
		}

		$current->items[] = $total($current, $by_year);

		yield $current;
	}
















	static public function getSumsByInterval(array $criterias, int $interval)
	{
		$where = self::getWhereClause($criterias);
		$where_interval = !empty($criterias['year']) ? sprintf(' WHERE id_year = %d', $criterias['year']) : '';

		$db = DB::getInstance();







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







156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
			return;
		}

		$current->items[] = $total($current, $by_year);

		yield $current;
	}

	static public function getSumsPerYear(array $criterias): array
	{
		$where = self::getWhereClause($criterias);

		$sql = sprintf('SELECT y.id, y.start_date, y.end_date, y.label, SUM(l.credit) - SUM(l.debit) AS sum
			FROM acc_transactions t
			INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
			INNER JOIN acc_accounts a ON a.id = l.id_account
			INNER JOIN acc_years y ON y.id = t.id_year
			WHERE %s
			GROUP BY t.id_year ORDER BY y.end_date;', $where, $where);

		return DB::getInstance()->getGrouped($sql);
	}

	static public function getSumsByInterval(array $criterias, int $interval)
	{
		$where = self::getWhereClause($criterias);
		$where_interval = !empty($criterias['year']) ? sprintf(' WHERE id_year = %d', $criterias['year']) : '';

		$db = DB::getInstance();
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250


251


252





















253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272


273
274
275
276
277
278
279
280
281
282
283
284


285
286
287
288


289


290

291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310

311
312

313
314
315


316
317
318
319
320
321
322
323
324
325
	{
		$where = self::getWhereClause($criterias);

		$order = $order ?: 'a.code COLLATE NOCASE';
		$reverse = $reverse ? '* -1' : '';
		$remove_zero = $remove_zero ? 'HAVING sum != 0' : '';

		// Find sums, link them to accounts
		$sql = sprintf('SELECT a.id, a.code, a.label, a.position, SUM(l.credit) AS credit, SUM(l.debit) AS debit,
			SUM(l.credit - l.debit) %s AS sum
			FROM %s l
			INNER JOIN %s t ON t.id = l.id_transaction
			INNER JOIN %s a ON a.id = l.id_account
			WHERE %s
			GROUP BY l.id_account
			%s
			ORDER BY %s;',


			$reverse, Line::TABLE, Transaction::TABLE, Account::TABLE, $where, $remove_zero, $order);


		return DB::getInstance()->getGrouped($sql);





















	}

	static public function getTrialBalance(array $criterias): array
	{
		return self::getClosingSumsWithAccounts($criterias, null, false, false);
	}

	static public function getBalanceSheet(array $criterias): array
	{
		$out = [
			Account::ASSET => [],
			Account::LIABILITY => [],
			'sums' => [
				Account::ASSET => 0,
				Account::LIABILITY => 0,
			],
		];

		$position_criteria = ['position' => [Account::ASSET, Account::LIABILITY, Account::ASSET_OR_LIABILITY]];
		$list = self::getClosingSumsWithAccounts($criterias + $position_criteria);



		foreach ($list as $row) {
			if ($row->sum == 0) {
				// Ignore empty accounts
				continue;
			}

			$position = $row->position;

			if ($position == Account::ASSET_OR_LIABILITY) {
				$position = $row->sum < 0 ? Account::ASSET : Account::LIABILITY;
				$row->sum = abs($row->sum);


			}
			elseif ($position == Account::ASSET) {
				// reverse number for assets
				$row->sum *= -1;


			}




			$out[$position][] = $row;
		}

		$result = self::getResult($criterias);

		if ($result != 0) {
			$out[Account::LIABILITY][] = (object) [
				'id' => null,
				'label' => $result > 0 ? 'Résultat de l\'exercice courant (excédent)' : 'Résultat de l\'exercice courant (perte)',
				'sum' => $result,
			];
		}

		// Calculate the total sum for assets and liabilities
		foreach ($out as $position => $rows) {
			if ($position == 'sums') {
				continue;
			}

			$sum = 0;

			foreach ($rows as $row) {
				$sum += $row->sum;

			}

			$out['sums'][$position] = $sum;


		}

		return $out;
	}

	/**
	 * Return list of favorite accounts (accounts with a type), grouped by type, with their current sum
	 * @return \Generator list of accounts grouped by type
	 */
	static public function getClosingSumsFavoriteAccounts(array $criterias): \Generator







<
|







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









<
<
<
|
|
<
<
<



>
>










|

>
>




>
>

>
>
|
>
|





|







|
<
<
<
<

>


>


|
>
>


|







249
250
251
252
253
254
255

256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300



301
302



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347




348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
	{
		$where = self::getWhereClause($criterias);

		$order = $order ?: 'a.code COLLATE NOCASE';
		$reverse = $reverse ? '* -1' : '';
		$remove_zero = $remove_zero ? 'HAVING sum != 0' : '';


		$query = 'SELECT a.code, a.id, a.label, a.position, SUM(l.credit) AS credit, SUM(l.debit) AS debit,
			SUM(l.credit - l.debit) %s AS sum
			FROM %s l
			INNER JOIN %s t ON t.id = l.id_transaction
			INNER JOIN %s a ON a.id = l.id_account
			WHERE %s
			GROUP BY l.id_account
			%s
			ORDER BY %s';

		// Find sums, link them to accounts
		$sql = sprintf($query, $reverse, Line::TABLE, Transaction::TABLE, Account::TABLE, $where, $remove_zero, $order);

		$db = DB::getInstance();
		$out = $db->getGrouped($sql);

		// SQLite does not support OUTER JOIN yet :(
		if (isset($criterias['compare_year'])) {
			$where = self::getWhereClause(array_merge($criterias, ['year' => (int)$criterias['compare_year']]));
			$sql = sprintf($query, $reverse, Line::TABLE, Transaction::TABLE, Account::TABLE, $where, $remove_zero, $order);

			foreach ($db->iterate($sql) as $row) {
				if (!isset($out[$row->code])) {
					$row->sum2 = $row->sum;
					$row->sum = 0;
					$row->change = null;
					$out[$row->code] = $row;
				}
				else {
					$out[$row->code]->sum2 = $row->sum;
					$out[$row->code]->change = ($out[$row->code]->sum - $row->sum);
				}
			}
		}

		return $out;
	}

	static public function getTrialBalance(array $criterias): array
	{
		return self::getClosingSumsWithAccounts($criterias, null, false, false);
	}

	static public function getBalanceSheet(array $criterias): array
	{



		$accounts = ['asset' => [], 'liability' => []];
		$sums = $sums2 = $change = ['asset' => 0, 'liability' => 0];




		$position_criteria = ['position' => [Account::ASSET, Account::LIABILITY, Account::ASSET_OR_LIABILITY]];
		$list = self::getClosingSumsWithAccounts($criterias + $position_criteria);

		//var_dump($list); exit;

		foreach ($list as $row) {
			if ($row->sum == 0) {
				// Ignore empty accounts
				continue;
			}

			$position = $row->position;

			if ($position == Account::ASSET_OR_LIABILITY) {
				$position = $row->sum < 0 ? 'asset' : 'liability';
				$row->sum = abs($row->sum);
				$row->sum2 = isset($row->sum2) ? abs($row->sum2) : 0;
				$row->change = isset($row->change) ? $row->change * -1 : 0;
			}
			elseif ($position == Account::ASSET) {
				// reverse number for assets
				$row->sum *= -1;
				$row->sum2 = isset($row->sum2) ? $row->sum2 * -1 : 0;
				$position = 'asset';
			}
			else {
				$position = 'liability';
			}

			$accounts[$position][] = $row;
		}

		$result = self::getResult($criterias);

		if ($result != 0) {
			$accounts['liability'][] = (object) [
				'id' => null,
				'label' => $result > 0 ? 'Résultat de l\'exercice courant (excédent)' : 'Résultat de l\'exercice courant (perte)',
				'sum' => $result,
			];
		}

		// Calculate the total sum for assets and liabilities
		foreach ($accounts as $position => $rows) {




			$sum = 0;
			$sum2 = 0;
			foreach ($rows as $row) {
				$sum += $row->sum;
				$sum2 += $row->sum2 ?? 0;
			}

			$sums[$position] = $sum;
			$sums2[$position] = $sum2;
			$change[$position] = $sum - $sum2;
		}

		return compact('sums', 'sums2', 'change', 'accounts');
	}

	/**
	 * Return list of favorite accounts (accounts with a type), grouped by type, with their current sum
	 * @return \Generator list of accounts grouped by type
	 */
	static public function getClosingSumsFavoriteAccounts(array $criterias): \Generator
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506











507

508
509
	}

	static public function getStatement(array $criterias): array
	{
		$revenue = Reports::getClosingSumsWithAccounts($criterias + ['position' => Account::REVENUE]);
		$expense = Reports::getClosingSumsWithAccounts($criterias + ['position' => Account::EXPENSE], null, true);

		$get_sum = function (array $in): int {
			$sum = 0;

			foreach ($in as $row) {
				$sum += $row->sum;
			}

			return $sum;
		};

		$revenue_sum = $get_sum($revenue);
		$expense_sum = $get_sum($expense);
		$result = $revenue_sum - $expense_sum;












		return compact('revenue', 'expense', 'revenue_sum', 'expense_sum', 'result');

	}
}







|



|









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


528
529
530
531
532
533
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
	}

	static public function getStatement(array $criterias): array
	{
		$revenue = Reports::getClosingSumsWithAccounts($criterias + ['position' => Account::REVENUE]);
		$expense = Reports::getClosingSumsWithAccounts($criterias + ['position' => Account::EXPENSE], null, true);

		$get_sum = function (array $in, string $key = 'sum'): int {
			$sum = 0;

			foreach ($in as $row) {
				$sum += $row->$key ?? 0;
			}

			return $sum;
		};

		$revenue_sum = $get_sum($revenue);
		$expense_sum = $get_sum($expense);
		$result = $revenue_sum - $expense_sum;

		$revenue_sum2 = $expense_sum2 = $result2 = $revenue_change = $expense_change = $result_change = null;

		if (isset($criterias['compare_year'])) {
			$revenue_sum2 = $get_sum($revenue, 'sum2');
			$revenue_change = $revenue_sum - $revenue_sum2;
			$expense_sum2 = $get_sum($expense, 'sum2');
			$expense_change = $expense_sum - $expense_sum2;
			$result2 = $revenue_sum2 - $expense_sum2;
			$result_change = $result < 0 ? $result2 - $result : $result - $result2;
		}

		return compact('revenue', 'expense', 'revenue_sum', 'expense_sum', 'result',
			'revenue_sum2', 'expense_sum2', 'result2', 'revenue_change', 'expense_change', 'result_change');
	}
}

Modified src/include/lib/Garradin/Accounting/Transactions.php from [7839877bed] to [31743804a6].

307
308
309
310
311
312
313


314
315
316
317
318
319
320
				$e->setDetails($transaction->asDetailsArray());
			}

			throw $e;
		}

		$db->commit();


	}

	static public function importCustom(Year $year, CSV_Custom $csv, int $user_id)
	{
		if ($year->closed) {
			throw new \InvalidArgumentException('Closed year');
		}







>
>







307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
				$e->setDetails($transaction->asDetailsArray());
			}

			throw $e;
		}

		$db->commit();

		Graph::clearCacheAllYears();
	}

	static public function importCustom(Year $year, CSV_Custom $csv, int $user_id)
	{
		if ($year->closed) {
			throw new \InvalidArgumentException('Closed year');
		}
394
395
396
397
398
399
400


401
402
403
404
405
406
407
				$e->setDetails($transaction->asDetailsArray());
			}

			throw $e;
		}

		$db->commit();


	}

	static public function setAnalytical(?int $id_analytical, ?array $transactions = null, ?array $lines = null)
	{
		$db = DB::getInstance();

		if (null !== $id_analytical && !$db->test(Account::TABLE, 'type = ? AND id = ?', Account::TYPE_ANALYTICAL, $id_analytical)) {







>
>







396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
				$e->setDetails($transaction->asDetailsArray());
			}

			throw $e;
		}

		$db->commit();

		Graph::clearCacheAllYears();
	}

	static public function setAnalytical(?int $id_analytical, ?array $transactions = null, ?array $lines = null)
	{
		$db = DB::getInstance();

		if (null !== $id_analytical && !$db->test(Account::TABLE, 'type = ? AND id = ?', Account::TYPE_ANALYTICAL, $id_analytical)) {

Modified src/include/lib/Garradin/Accounting/Years.php from [9ef2231749] to [d161a36703].

42
43
44
45
46
47
48





49
50
51
52
53
54
55
		return DB::getInstance()->getAssoc('SELECT id, label FROM acc_years ORDER BY end_date;');
	}

	static public function listClosedAssoc()
	{
		return DB::getInstance()->getAssoc('SELECT id, label FROM acc_years WHERE closed = 1 ORDER BY end_date;');
	}






	static public function listClosed()
	{
		$em = EntityManager::getInstance(Year::class);
		return $em->all('SELECT * FROM @TABLE WHERE closed = 1 ORDER BY end_date;');
	}








>
>
>
>
>







42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
		return DB::getInstance()->getAssoc('SELECT id, label FROM acc_years ORDER BY end_date;');
	}

	static public function listClosedAssoc()
	{
		return DB::getInstance()->getAssoc('SELECT id, label FROM acc_years WHERE closed = 1 ORDER BY end_date;');
	}

	static public function listClosedAssocExcept(int $id)
	{
		return DB::getInstance()->getAssoc('SELECT id, label FROM acc_years WHERE closed = 1 AND id != ? ORDER BY end_date DESC;', $id);
	}

	static public function listClosed()
	{
		$em = EntityManager::getInstance(Year::class);
		return $em->all('SELECT * FROM @TABLE WHERE closed = 1 ORDER BY end_date;');
	}

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

1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16












17
18
19
20
21
22
23
<?php

namespace Garradin;

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









>



|

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







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

namespace Garradin;

use Garradin\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Membres\Champs;

use KD2\SMTP;
use KD2\Graphics\Image;

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

	const FILES_TYPES = [
		'admin_background' => 'image',
		'admin_css'        => 'code',
		'admin_homepage'   => 'web',
		'logo'             => 'image',
		'icon'             => 'image',
		'favicon'          => 'image',
	];

	protected $nom_asso;
	protected $adresse_asso;
	protected $email_asso;
	protected $telephone_asso;
	protected $site_asso;
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

	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',







|
<
<







49
50
51
52
53
54
55
56


57
58
59
60
61
62
63

	protected $last_chart_change;
	protected $last_version_check;

	protected $couleur1;
	protected $couleur2;

	protected $files = [];



	protected $site_disabled;

	protected $_types = [
		'nom_asso'              => 'string',
		'adresse_asso'          => '?string',
		'email_asso'            => 'string',
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
		'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()







|
<
|







78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
93
		'champ_identite'        => 'string',

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

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


		'files'                 => 'array',

		'site_disabled'         => 'bool',
	];

	static protected $_instance = null;

	static public function getInstance()
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170





171
172
173
174
175
176
177
	{
		if (!count($this->_modified)) {
			return true;
		}

		$this->selfCheck();

		$values = [];
		$db = DB::getInstance();

		foreach ($this->_modified as $key => $modified) {
			$value = $this->$key;
			$type = ltrim($this->_types[$key], '?');

			if ($type == Champs::class) {
				$value = $value->toString();
			}
			elseif (is_object($value)) {
				throw new \UnexpectedValueException('Unexpected object as value: ' . get_class($value));
			}

			$values[$key] = $value;
		}

		unset($value, $key, $modified);

		$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;
	}

	public function delete(): bool







|
<

<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
















>
>
>
>
>







139
140
141
142
143
144
145
146

147



148












149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
	{
		if (!count($this->_modified)) {
			return true;
		}

		$this->selfCheck();

		$values = $this->modifiedProperties(true);





		$db = DB::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();

		if (isset($values['couleur1']) || isset($values['couleur2'])) {
			// Reset graph cache
			Static_Cache::clean(0);
		}

		$this->_modified = [];

		return true;
	}

	public function delete(): bool
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
		if (!isset($source['couleur1'], $source['couleur2'])
			|| ($source['couleur1'] == ADMIN_COLOR1 && $source['couleur2'] == ADMIN_COLOR2))
		{
			$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]) {
			case 'int':







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







188
189
190
191
192
193
194


























195
196
197
198
199
200
201
		if (!isset($source['couleur1'], $source['couleur2'])
			|| ($source['couleur1'] == ADMIN_COLOR1 && $source['couleur2'] == ADMIN_COLOR2))
		{
			$source['couleur1'] = null;
			$source['couleur2'] = null;
		}



























		parent::importForm($source);
	}

	protected function _filterType(string $key, $value)
	{
		switch ($this->_types[$key]) {
			case 'int':
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

























































































		$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');
	}
}
































































































|
>
|
>
|
|
>


















|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
		$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
		$this->assert(count($this->files) == count(self::FILES));

		foreach ($this->files as $key => $value) {
			$this->assert(array_key_exists($key, self::FILES));
			$this->assert(is_int($value) || is_null($value));
		}

		$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');
	}

	public function file(string $key): ?File
	{
		if (!isset(self::FILES[$key])) {
			throw new \InvalidArgumentException('Invalid file key: ' . $key);
		}

		if (empty($this->files[$key])) {
			return null;
		}

		return Files::get(self::FILES[$key]);
	}

	public function fileURL(string $key, string $params = ''): ?string
	{
		if (empty($this->files[$key])) {

			if ($key == 'favicon') {
				return ADMIN_URL . 'static/favicon.png';
			}
			elseif ($key == 'icon') {
				return ADMIN_URL . 'static/icon.png';
			}

			return null;
		}

		$params = $params ? $params . '&' : '';

		return WWW_URL . self::FILES[$key] . '?' . $params . substr(md5($this->files[$key]), 0, 10);
	}


	public function hasFile(string $key): bool
	{
		return $this->files[$key] ? true : false;
	}

	public function setFile(string $key, ?string $value, bool $upload = false): ?File
	{
		$f = Files::get(self::FILES[$key]);
		$files = $this->files;
		$type = self::FILES_TYPES[$key];
		$path = self::FILES[$key];

		// NULL = delete file
		if (null === $value) {
			if ($f) {
				$f->delete();
			}

			$f = null;
		}
		elseif ($upload) {
			$f = File::upload(Utils::dirname($path), $value, Utils::basename($path));

			if ($type == 'image' && !$f->image) {
				$this->setFile($key, null);
				throw new UserException('Le fichier n\'est pas une image.');
			}

			// Force favicon format
			if ($key == 'favicon') {
				$format = 'png';
				$i = new Image($f->fullpath());
				$i->cropResize(32, 32);
				$f->setContent($i->output($format, true));
			}
			// Force icon format
			else if ($key == 'favicon') {
				$format = 'png';
				$i = new Image($f->fullpath());
				$i->cropResize(512, 512);
				$f->setContent($i->output($format, true));
			}
		}
		elseif ($f) {
			$f->setContent($value);
		}
		else {
			$f = File::createAndStore(Utils::dirname($path), Utils::basename($path), null, $value);
		}

		$files[$key] = $f ? $f->modified->getTimestamp() : null;
		$this->set('files', $files);

		return $f;
	}
}

Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [32bd594806] to [ab4675f214].

383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
		}

		return $sum;
	}

	public function save(): bool
	{
		if ($this->validated && empty($this->_modified['validated'])) {
			throw new ValidationException('Il n\'est pas possible de modifier une écriture qui a été validée');
		}

		$exists = $this->exists();

		$db = DB::getInstance();








|







383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
		}

		return $sum;
	}

	public function save(): bool
	{
		if ($this->validated && !(isset($this->_modified['validated']) && $this->_modified['validated'] === 0)) {
			throw new ValidationException('Il n\'est pas possible de modifier une écriture qui a été validée');
		}

		$exists = $this->exists();

		$db = DB::getInstance();

Modified src/include/lib/Garradin/Entities/Accounting/Year.php from [f33e68404f] to [478d431189].

1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
<?php

namespace Garradin\Entities\Accounting;

use KD2\DB\EntityManager;
use Garradin\DB;
use Garradin\Entity;
use Garradin\UserException;

use Garradin\Accounting\Accounts;
use Garradin\Files\Files;
use Garradin\Entities\Files\File;

class Year extends Entity
{
	const TABLE = 'acc_years';








>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace Garradin\Entities\Accounting;

use KD2\DB\EntityManager;
use Garradin\DB;
use Garradin\Entity;
use Garradin\UserException;
use Garradin\Utils;
use Garradin\Accounting\Accounts;
use Garradin\Files\Files;
use Garradin\Entities\Files\File;

class Year extends Entity
{
	const TABLE = 'acc_years';
160
161
162
163
164
165
166
167







		return EntityManager::findOneById(Chart::class, $this->id_chart);
	}

	public function accounts()
	{
		return new Accounts($this->id_chart);
	}
}














|
>
>
>
>
>
>
>
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
		return EntityManager::findOneById(Chart::class, $this->id_chart);
	}

	public function accounts()
	{
		return new Accounts($this->id_chart);
	}

	public function label_years()
	{
		$start = Utils::date_fr($this->start_date, 'Y');
		$end = Utils::date_fr($this->end_date, 'Y');
		return $start == $end ? $start : sprintf('%s-%s', $start, substr($end, -2));
	}
}

Modified src/include/lib/Garradin/Entities/Files/File.php from [0f34036f86] to [3b1c044956].

62
63
64
65
66
67
68
69





70
71
72
73
74
75
76
77
78
79
80
81
	const TYPE_DIRECTORY = 2;
	const TYPE_LINK = 3;

	/**
	 * Tailles de miniatures autorisées, pour ne pas avoir 500 fichiers générés avec 500 tailles différentes
	 * @var array
	 */
	const ALLOWED_THUMB_SIZES = [200, 500];






	const THUMB_CACHE_ID = 'file.thumb.%s.%d';

	const THUMB_SIZE_TINY = 200;
	const THUMB_SIZE_SMALL = 500;

	const CONTEXT_DOCUMENTS = 'documents';
	const CONTEXT_USER = 'user';
	const CONTEXT_TRANSACTION = 'transaction';
	const CONTEXT_CONFIG = 'config';
	const CONTEXT_WEB = 'web';
	const CONTEXT_SKELETON = 'skel';







|
>
>
>
>
>



|
|







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
	const TYPE_DIRECTORY = 2;
	const TYPE_LINK = 3;

	/**
	 * Tailles de miniatures autorisées, pour ne pas avoir 500 fichiers générés avec 500 tailles différentes
	 * @var array
	 */
	const ALLOWED_THUMB_SIZES = [
		'150px' => [['resize', 150]],
		'200px' => [['resize', 200]],
		'500px' => [['resize', 500]],
		'crop-256px' => [['cropResize', 256, 256]],
	];

	const THUMB_CACHE_ID = 'file.thumb.%s.%d';

	const THUMB_SIZE_TINY = '200px';
	const THUMB_SIZE_SMALL = '500px';

	const CONTEXT_DOCUMENTS = 'documents';
	const CONTEXT_USER = 'user';
	const CONTEXT_TRANSACTION = 'transaction';
	const CONTEXT_CONFIG = 'config';
	const CONTEXT_WEB = 'web';
	const CONTEXT_SKELETON = 'skel';
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186

		// Delete actual file content
		Files::callStorage('delete', $this);

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

		// clean up thumbnails
		foreach (self::ALLOWED_THUMB_SIZES as $size)
		{
			Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $size));
		}

		DB::getInstance()->delete('files_search', 'path = ? OR path LIKE ?', $this->path, $this->path . '/%');

		if ($this->exists()) {
			return parent::delete();
		}







|

|







175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191

		// Delete actual file content
		Files::callStorage('delete', $this);

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

		// clean up thumbnails
		foreach (self::ALLOWED_THUMB_SIZES as $key => $operations)
		{
			Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $key));
		}

		DB::getInstance()->delete('files_search', 'path = ? OR path LIKE ?', $this->path, $this->path . '/%');

		if ($this->exists()) {
			return parent::delete();
		}
225
226
227
228
229
230
231










232
233
234
235
236
237
238

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

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

		return $return;
	}











	public function setContent(string $content): self
	{
		$this->set('modified', new \DateTime);
		$this->store(null, rtrim($content));
		$this->indexForSearch(null, $content);
		return $this;







>
>
>
>
>
>
>
>
>
>







230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253

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

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

		return $return;
	}

	/**
	 * Copy the current file to a new location
	 * @param  string $target Target path
	 * @return self
	 */
	public function copy(string $target): self
	{
		return self::createAndStore(Utils::dirname($target), Utils::basename($target), Files::callStorage('getFullPath', $this), null);
	}

	public function setContent(string $content): self
	{
		$this->set('modified', new \DateTime);
		$this->store(null, rtrim($content));
		$this->indexForSearch(null, $content);
		return $this;
313
314
315
316
317
318
319






320
321
322
323
324
325
326
		}

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

		if (!$index_search) {
			$this->indexForSearch($source_path, $source_content);
		}







		return $this;
	}

	public function indexForSearch(?string $source_path, ?string $source_content, ?string $title = null): void
	{
		// Store content in search table







>
>
>
>
>
>







328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
		}

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

		if (!$index_search) {
			$this->indexForSearch($source_path, $source_content);
		}

		// clean up thumbnails
		foreach (self::ALLOWED_THUMB_SIZES as $key => $operations)
		{
			Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $key));
		}

		return $this;
	}

	public function indexForSearch(?string $source_path, ?string $source_content, ?string $title = null): void
	{
		// Store content in search table
443
444
445
446
447
448
449

450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
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
492
493
		$file->set('path', $fullpath);
		$file->set('parent', $path);
		$file->set('name', $name);

		if ($source_path && !$source_content) {
			$file->set('mime', finfo_file($finfo, $source_path));
			$file->set('size', filesize($source_path));

		}
		else {
			$file->set('mime', finfo_buffer($finfo, $source_content));
			$file->set('size', strlen($source_content));
		}

		$file->set('image', (int) in_array($file->mime, self::IMAGE_TYPES));

		// Force empty files as text/plain
		if ($file->mime == 'application/x-empty' && !$file->size) {
			$file->set('mime', 'text/plain');
		}

		return $file;
	}

	/**
	 * Create a file from an encoded base64 string
	 */
	static public function createFromBase64(string $path, string $name, string $encoded_content): self
	{
		$content = base64_decode($encoded_content);
		return self::createAndStore($path, $name, null, $content);
	}

	/**
	 * Modify a file from an encoded base64 string
	 * This is used to upload the modified background image in the admin panel
	 */
	public function storeFromBase64(string $encoded_content): self
	{
		$content = base64_decode($encoded_content);
		$this->set('modified', new \DateTime);
		$this->store(null, $content);
		return $this;
	}

	/**
	 * Upload multiple files
	 * @param  string $path Target parent directory (eg. 'documents/Logos')
	 * @param  string $key  The name of the file input in the HTML form (this MUST have a '[]' at the end of the name)
	 * @return array list of File objects created
	 */
	static public function uploadMultiple(string $path, string $key): array







>
















<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







464
465
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
492
493
494
		$file->set('path', $fullpath);
		$file->set('parent', $path);
		$file->set('name', $name);

		if ($source_path && !$source_content) {
			$file->set('mime', finfo_file($finfo, $source_path));
			$file->set('size', filesize($source_path));
			$file->set('modified', new \DateTime('@' . filemtime($source_path)));
		}
		else {
			$file->set('mime', finfo_buffer($finfo, $source_content));
			$file->set('size', strlen($source_content));
		}

		$file->set('image', (int) in_array($file->mime, self::IMAGE_TYPES));

		// Force empty files as text/plain
		if ($file->mime == 'application/x-empty' && !$file->size) {
			$file->set('mime', 'text/plain');
		}

		return $file;
	}






















	/**
	 * Upload multiple files
	 * @param  string $path Target parent directory (eg. 'documents/Logos')
	 * @param  string $key  The name of the file input in the HTML form (this MUST have a '[]' at the end of the name)
	 * @return array list of File objects created
	 */
	static public function uploadMultiple(string $path, string $key): array
528
529
530
531
532
533
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

	/**
	 * Upload a file using POST from a HTML form
	 * @param  string $path Target parent directory (eg. 'documents/Logos')
	 * @param  string $key  The name of the file input in the HTML form
	 * @return self Created file object
	 */
	static public function upload(string $path, string $key): self
	{
		if (!isset($_FILES[$key]) || !is_array($_FILES[$key])) {
			throw new UserException('Aucun fichier reçu');
		}

		$file = $_FILES[$key];

		if (!empty($file['error'])) {
			throw new UserException(self::getErrorMessage($file['error']));
		}

		if (empty($file['size']) || empty($file['name'])) {
			throw new UserException('Fichier reçu invalide : vide ou sans nom de fichier.');
		}

		if (!is_uploaded_file($file['tmp_name'])) {
			throw new \RuntimeException('Le fichier n\'a pas été envoyé de manière conventionnelle.');
		}

		$name = self::filterName($file['name']);

		return self::createAndStore($path, $name, $file['tmp_name'], null);
	}


	/**
	 * Récupération du message d'erreur







|



















|







529
530
531
532
533
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

	/**
	 * Upload a file using POST from a HTML form
	 * @param  string $path Target parent directory (eg. 'documents/Logos')
	 * @param  string $key  The name of the file input in the HTML form
	 * @return self Created file object
	 */
	static public function upload(string $path, string $key, ?string $name = null): self
	{
		if (!isset($_FILES[$key]) || !is_array($_FILES[$key])) {
			throw new UserException('Aucun fichier reçu');
		}

		$file = $_FILES[$key];

		if (!empty($file['error'])) {
			throw new UserException(self::getErrorMessage($file['error']));
		}

		if (empty($file['size']) || empty($file['name'])) {
			throw new UserException('Fichier reçu invalide : vide ou sans nom de fichier.');
		}

		if (!is_uploaded_file($file['tmp_name'])) {
			throw new \RuntimeException('Le fichier n\'a pas été envoyé de manière conventionnelle.');
		}

		$name = self::filterName($name ?? $file['name']);

		return self::createAndStore($path, $name, $file['tmp_name'], null);
	}


	/**
	 * Récupération du message d'erreur
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
		if ($download) {
			$url .= '?download';
		}

		return $url;
	}

	public function thumb_url(?int $size = null): string
	{
		$size = $size ? self::_findNearestThumbSize($size) : min(self::ALLOWED_THUMB_SIZES);
		return sprintf('%s?%dpx', $this->url(), $size);
	}

	/**
	 * Renvoie la taille de miniature la plus proche de la taille demandée
	 * @param  integer $size Taille demandée
	 * @return integer       Taille possible
	 */
	static protected function _findNearestThumbSize($size)
	{
		$size = (int) $size;

		if (in_array($size, self::ALLOWED_THUMB_SIZES))
		{
			return $size;
		}

		foreach (self::ALLOWED_THUMB_SIZES as $s)
		{
			if ($s >= $size)
			{
				return $s;
			}
		}

		return max(self::ALLOWED_THUMB_SIZES);
	}

	/**
	 * Envoie le fichier au client HTTP
	 */
	public function serve(?Session $session = null, bool $download = false): void
	{







|

<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
|


|
<
<
<
|
<
<
<
<







602
603
604
605
606
607
608
609
610



611











612
613
614
615



616




617
618
619
620
621
622
623
		if ($download) {
			$url .= '?download';
		}

		return $url;
	}

	public function thumb_url($size = null): string
	{



		if (is_int($size)) {











			$size .= 'px';
		}

		$size = isset(self::ALLOWED_THUMB_SIZES[$size]) ? $size : key(self::ALLOWED_THUMB_SIZES);



		return sprintf('%s?%dpx', $this->url(), $size);




	}

	/**
	 * Envoie le fichier au client HTTP
	 */
	public function serve(?Session $session = null, bool $download = false): void
	{
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
















701
702
703
704
705
706
707

		$this->_serve($path, $content, $download);
	}

	/**
	 * Envoie une miniature à la taille indiquée au client HTTP
	 */
	public function serveThumbnail(?Session $session = null, ?int $width = null): void
	{
		if (!$this->checkReadAccess($session)) {
			header('HTTP/1.1 403 Forbidden', true, 403);
			throw new UserException('Accès interdit');
			return;
		}

		if (!$this->image) {
			throw new UserException('Il n\'est pas possible de fournir une miniature pour un fichier qui n\'est pas une image.');
		}

		if (!$width) {
			$width = reset(self::ALLOWED_THUMB_SIZES);
		}

		if (!in_array($width, self::ALLOWED_THUMB_SIZES)) {
			throw new UserException('Cette taille de miniature n\'est pas autorisée.');
		}

		$cache_id = sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $width);
		$destination = Static_Cache::getPath($cache_id);

		// La miniature n'existe pas dans le cache statique, on la crée
		if (!Static_Cache::exists($cache_id))
		{
			try {
				if ($path = Files::callStorage('getFullPath', $this)) {
					(new Image($path))->resize($width)->save($destination);
				}
				elseif ($content = Files::callStorage('fetch', $this)) {
					Image::createFromBlob($content)->resize($width)->save($destination);
				}
				else {
					throw new \RuntimeException('Unable to fetch file');
				}
















			}
			catch (\RuntimeException $e) {
				throw new UserException('Impossible de créer la miniature');
			}
		}

		$this->_serve($destination, null);







|











<
<
<
<
|



|


<
|
<


|


|




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







638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656




657
658
659
660
661
662
663

664

665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697

		$this->_serve($path, $content, $download);
	}

	/**
	 * Envoie une miniature à la taille indiquée au client HTTP
	 */
	public function serveThumbnail(?Session $session = null, string $size = null): void
	{
		if (!$this->checkReadAccess($session)) {
			header('HTTP/1.1 403 Forbidden', true, 403);
			throw new UserException('Accès interdit');
			return;
		}

		if (!$this->image) {
			throw new UserException('Il n\'est pas possible de fournir une miniature pour un fichier qui n\'est pas une image.');
		}





		if (!array_key_exists($size, self::ALLOWED_THUMB_SIZES)) {
			throw new UserException('Cette taille de miniature n\'est pas autorisée.');
		}

		$cache_id = sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $size);
		$destination = Static_Cache::getPath($cache_id);


		if (!Static_Cache::exists($cache_id)) {

			try {
				if ($path = Files::callStorage('getFullPath', $this)) {
					$i = new Image($path);
				}
				elseif ($content = Files::callStorage('fetch', $this)) {
					$i = Image::createFromBlob($content);
				}
				else {
					throw new \RuntimeException('Unable to fetch file');
				}

				$operations = self::ALLOWED_THUMB_SIZES[$size];
				$allowed_operations = ['resize', 'cropResize', 'flip', 'rotate', 'autoRotate', 'crop'];

				foreach ($operations as $operation) {
					$arguments = array_slice($operation, 1);
					$operation = $operation[0];

					if (!in_array($operation, $allowed_operations)) {
						throw new \InvalidArgumentException('Opération invalide: ' . $operation);
					}

					call_user_func_array([$i, $operation], $arguments);
				}

				$i->save($destination);
			}
			catch (\RuntimeException $e) {
				throw new UserException('Impossible de créer la miniature');
			}
		}

		$this->_serve($destination, null);

Modified src/include/lib/Garradin/Entities/Web/Page.php from [6b459772f9] to [d8d50d5663].

216
217
218
219
220
221
222


223

224
225
226
227
228
229
230
231
		parent::save();
		$this->syncFile($current_path);

		// Rename/move children
		if ($change_parent) {
			$db = DB::getInstance();
			$sql = sprintf('UPDATE web_pages


				SET path = %s || substr(path, %d), parent = %1$s || substr(parent, %2$d)

				WHERE parent LIKE %s;',
				$db->quote($this->path), strlen($change_parent) + 1, $db->quote($change_parent . '/%'));
			$db->exec($sql);
		}

		return true;
	}








>
>
|
>
|







216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
		parent::save();
		$this->syncFile($current_path);

		// Rename/move children
		if ($change_parent) {
			$db = DB::getInstance();
			$sql = sprintf('UPDATE web_pages
				SET
					path = %1$s || substr(path, %2$d),
					parent = %1$s || substr(parent, %2$d),
					file_path = \'web/\' || %1$s || substr(file_path, %2$d + 4)
				WHERE path LIKE %3$s;',
				$db->quote($this->path), strlen($change_parent) + 1, $db->quote($change_parent . '/%'));
			$db->exec($sql);
		}

		return true;
	}

Modified src/include/lib/Garradin/Install.php from [4757cdcf38] to [a878545951].

144
145
146
147
148
149
150


151
152
153
154
155
156
157
158
159
160
161
162
163
164
		$id_membre = $membres->add([
			'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';
        $chart->save();







>
>



<
<
<
|







144
145
146
147
148
149
150
151
152
153
154
155



156
157
158
159
160
161
162
163
		$id_membre = $membres->add([
			'id_category' => $cat->id(),
			'nom'         => $user_name,
			'email'       => $user_email,
			'passe'       => $user_password,
			'pays'        => 'FR',
		]);

		$config->set('files', array_map(fn () => null, $config::FILES));

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




		$config->setFile('admin_homepage', $welcome_text);

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

Modified src/include/lib/Garradin/Services/Fees.php from [98acb50ff8] to [f232ea6af4].

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
	 * If $user_id is specified, then it will return a column 'user_amount' containing the amount that this specific user should pay
	 */
	static public function listAllByService(?int $user_id = null)
	{
		$db = DB::getInstance();

		$sql = 'SELECT *, CASE WHEN amount THEN amount ELSE NULL END AS user_amount
			FROM services_fees ORDER BY id_service, label COLLATE NOCASE;';
		$result = $db->get($sql);

		if (!$user_id) {
			return $result;
		}

		foreach ($result as &$row) {
			if ($row->formula) {
				$sql = sprintf('SELECT %s FROM membres WHERE id = %d;', $row->formula, $user_id);
				$row->user_amount = $db->firstColumn($sql);
			}
		}

		usort($result, function ($a, $b) {
			if ($a->user_amount == $b->user_amount) {
				return 0;
			}

			return $a->user_amount > $b->user_amount ? 1 : -1;
		});

		return $result;
	}

	public function listWithStats()
	{
		$db = DB::getInstance();
		$hidden_cats = array_keys(Categories::listHidden());







|













<
<
<
<
<
<
<
<







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
	 * If $user_id is specified, then it will return a column 'user_amount' containing the amount that this specific user should pay
	 */
	static public function listAllByService(?int $user_id = null)
	{
		$db = DB::getInstance();

		$sql = 'SELECT *, CASE WHEN amount THEN amount ELSE NULL END AS user_amount
			FROM services_fees ORDER BY id_service, amount IS NULL, label COLLATE NOCASE;';
		$result = $db->get($sql);

		if (!$user_id) {
			return $result;
		}

		foreach ($result as &$row) {
			if ($row->formula) {
				$sql = sprintf('SELECT %s FROM membres WHERE id = %d;', $row->formula, $user_id);
				$row->user_amount = $db->firstColumn($sql);
			}
		}









		return $result;
	}

	public function listWithStats()
	{
		$db = DB::getInstance();
		$hidden_cats = array_keys(Categories::listHidden());

Modified src/include/lib/Garradin/Static_Cache.php from [6c89dae4c6] to [c7f77d4740].

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
		while ($file = $d->read())
		{
			if ($file[0] == '.')
			{
				continue;
			}

			if (filemtime($dir . '/' . $file) > $expire)
			{
				Utils::safe_unlink($dir . '/' . $file);
			}
		}

		$d->close();

		return true;
	}
}







|










99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
		while ($file = $d->read())
		{
			if ($file[0] == '.')
			{
				continue;
			}

			if (filemtime($dir . '/' . $file) < $expire)
			{
				Utils::safe_unlink($dir . '/' . $file);
			}
		}

		$d->close();

		return true;
	}
}

Modified src/include/lib/Garradin/Template.php from [c72c39239e] to [074688e258].

64
65
66
67
68
69
70

71
72
73
74
75
76
77
		// pour les éléments statiques (genre /admin/static/admin.css?v0.9.0)
		// car cela dévoilerait la version de Garradin utilisée, posant un souci
		// en cas de faille, on cache donc la version utilisée, chaque instance
		// aura sa propre version
		$this->assign('version_hash', substr(sha1(garradin_version() . garradin_manifest() . ROOT . SECRET_KEY), 0, 10));

		$this->assign('www_url', WWW_URL);

		$this->assign('self_url', Utils::getSelfURI());
		$this->assign('self_url_no_qs', Utils::getSelfURI(false));

		$this->assign('is_logged', false);
		$this->assign('dialog', isset($_GET['_dialog']));

		$this->assign('password_pattern', sprintf('.{%d,}', Session::MINIMUM_PASSWORD_LENGTH));







>







64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
		// pour les éléments statiques (genre /admin/static/admin.css?v0.9.0)
		// car cela dévoilerait la version de Garradin utilisée, posant un souci
		// en cas de faille, on cache donc la version utilisée, chaque instance
		// aura sa propre version
		$this->assign('version_hash', substr(sha1(garradin_version() . garradin_manifest() . ROOT . SECRET_KEY), 0, 10));

		$this->assign('www_url', WWW_URL);
		$this->assign('help_url', HELP_URL);
		$this->assign('self_url', Utils::getSelfURI());
		$this->assign('self_url_no_qs', Utils::getSelfURI(false));

		$this->assign('is_logged', false);
		$this->assign('dialog', isset($_GET['_dialog']));

		$this->assign('password_pattern', sprintf('.{%d,}', Session::MINIMUM_PASSWORD_LENGTH));
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
	{
		$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)
	{







|
|















|
|







574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
	{
		$config = Config::getInstance();

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

		if ($url = $config->fileURL('admin_background')) {
			$admin_background = $url;
		}

		// 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 ($url = $config->fileURL('admin_css')) {
			$out .= "\n" . sprintf('<link rel="stylesheet" type="text/css" href="%s" />', $url);
		}

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

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

Modified src/include/lib/Garradin/Upgrade.php from [eb62265c41] to [1ae9756519].

1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
<?php

namespace Garradin;

use Garradin\Membres\Session;
use Garradin\Membres\Champs;

use Garradin\Files\Files;


use KD2\HTTP;

use KD2\FossilInstaller;

class Upgrade
{








>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace Garradin;

use Garradin\Membres\Session;
use Garradin\Membres\Champs;

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

use KD2\HTTP;

use KD2\FossilInstaller;

class Upgrade
{
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342



























343
344
345
346
347
348
349

			if (version_compare($v, '1.1.8', '==')) {
				// Force sync to add missing pages if you had the buggy 1.1.8 version
				\Garradin\Web\Web::sync(true);
			}

			if (version_compare($v, '1.1.10', '<')) {
				\Garradin\Web\Web::sync(true); // Force sync of web pages
				Files::syncVirtualTable('', true);

				$db->begin();
				$db->exec(sprintf('DELETE FROM files_search WHERE path NOT IN (SELECT path FROM %s);', Files::getVirtualTableName()));
				$db->commit();
			}

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




























			// Vérification de la cohérence des clés étrangères
			$db->foreignKeyCheck();

			// Delete local cached files
			Utils::resetCache(USER_TEMPLATES_CACHE_ROOT);
			Utils::resetCache(STATIC_CACHE_ROOT);







|












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







324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377

			if (version_compare($v, '1.1.8', '==')) {
				// Force sync to add missing pages if you had the buggy 1.1.8 version
				\Garradin\Web\Web::sync(true);
			}

			if (version_compare($v, '1.1.10', '<')) {
				\Garradin\Web\Web::sync(); // Force sync of web pages
				Files::syncVirtualTable('', true);

				$db->begin();
				$db->exec(sprintf('DELETE FROM files_search WHERE path NOT IN (SELECT path FROM %s);', Files::getVirtualTableName()));
				$db->commit();
			}

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

			if (version_compare($v, '1.1.16', '<')) {
				$files = Config::FILES;

				foreach ($files as $key => &$set) {
					$f = Files::get($set);
					$set = $f !== null ? $f->modified->getTimestamp() : null;
				}

				unset($set);

				// Migrate files
				if ($f = Files::get(File::CONTEXT_SKELETON . '/favicon.png')) {
					$f->copy(Config::FILES['favicon']);
					$files['favicon'] = $f->modified->getTimestamp();
				}

				if ($f = Files::get(File::CONTEXT_SKELETON . '/logo.png')) {
					$f->copy(Config::FILES['icon']);
					$files['icon'] = $f->modified->getTimestamp();
				}

				$db->begin();
				$db->exec('DELETE FROM config WHERE key IN (\'admin_background\', \'admin_css\', \'admin_homepage\');');
				$db->exec(sprintf('INSERT INTO config (key, value) VALUES (\'files\', %s);', $db->quote(json_encode($files))));
				$db->commit();
			}

			// Vérification de la cohérence des clés étrangères
			$db->foreignKeyCheck();

			// Delete local cached files
			Utils::resetCache(USER_TEMPLATES_CACHE_ROOT);
			Utils::resetCache(STATIC_CACHE_ROOT);

Modified src/include/lib/Garradin/UserTemplate/CommonModifiers.php from [5a74eceef6] to [86761f9ede].

23
24
25
26
27
28
29
30
31
32
33
34
35


36
37
38
39
40
41
42
43
		'typo',
	];

	const FUNCTIONS_LIST = [
		'pagination',
	];

	static public function money($number, bool $hide_empty = true): string
	{
		if ($hide_empty && !$number) {
			return '';
		}



		return sprintf('<b class="money">%s</b>', Utils::money_format($number, ',', '&nbsp;', $hide_empty));
	}

	static public function money_currency($number, bool $hide_empty = true): string
	{
		$out = self::money($number, $hide_empty);

		if ($out !== '') {







|





>
>
|







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
		'typo',
	];

	const FUNCTIONS_LIST = [
		'pagination',
	];

	static public function money($number, bool $hide_empty = true, bool $force_sign = false): string
	{
		if ($hide_empty && !$number) {
			return '';
		}

		$sign = ($force_sign && $number > 0) ? '+' : '';

		return sprintf('<b class="money">%s</b>', $sign . Utils::money_format($number, ',', '&nbsp;', $hide_empty));
	}

	static public function money_currency($number, bool $hide_empty = true): string
	{
		$out = self::money($number, $hide_empty);

		if ($out !== '') {

Modified src/include/lib/Garradin/UserTemplate/Modifiers.php from [98d18e40cb] to [418f637151].

35
36
37
38
39
40
41


42


43
44
45
46
47
48
49
		'truncate',
		'excerpt',
		'protect_contact',
		'atom_date',
		'xml_escape',
		'replace',
		'regexp_replace',


	];



	static public function replace($str, $find, $replace): string
	{
		return str_replace($find, $replace, $str);
	}

	static public function regexp_replace($str, $pattern, $replace)







>
>

>
>







35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
		'truncate',
		'excerpt',
		'protect_contact',
		'atom_date',
		'xml_escape',
		'replace',
		'regexp_replace',
		'remove_leading_number',
		'get_leading_number',
	];

	const LEADING_NUMBER_REGEXP = '/^([\d.]+)\s*[.\)]\s*/';

	static public function replace($str, $find, $replace): string
	{
		return str_replace($find, $replace, $str);
	}

	static public function regexp_replace($str, $pattern, $replace)
119
120
121
122
123
124
125
126











		return Utils::date_fr($date, DATE_ATOM);
	}

	static public function xml_escape($str)
	{
		return htmlspecialchars($str, ENT_XML1 | ENT_QUOTES);
	}
}


















|
>
>
>
>
>
>
>
>
>
>
>
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
		return Utils::date_fr($date, DATE_ATOM);
	}

	static public function xml_escape($str)
	{
		return htmlspecialchars($str, ENT_XML1 | ENT_QUOTES);
	}

	static public function remove_leading_number($str): string
	{
		return preg_replace(self::LEADING_NUMBER_REGEXP, '', trim($str));
	}

	static public function get_leading_number($str): ?string
	{
		$match = preg_match(self::LEADING_NUMBER_REGEXP, $str);
		return $match[1] ?? null;
	}
}

Modified src/include/lib/Garradin/UserTemplate/UserTemplate.php from [008bd1b9aa] to [9c5fedaedd].

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

	static public function getRootVariables()
	{
		if (null !== self::$root_variables) {
			return self::$root_variables;
		}

		static $keys = ['adresse_asso', 'champ_identifiant', 'champ_identite', 'couleur1', 'couleur2', 'email_asso', 'monnaie', 'nom_asso', 'pays', 'site_asso', 'telephone_asso'];

		if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
		{
			if (function_exists('locale_accept_from_http'))
			{
			   $lang = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
			}







|







28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

	static public function getRootVariables()
	{
		if (null !== self::$root_variables) {
			return self::$root_variables;
		}

		static $keys = ['adresse_asso', 'champ_identifiant', 'champ_identite', 'couleur1', 'couleur2', 'email_asso', 'monnaie', 'nom_asso', 'pays', 'site_asso', 'telephone_asso', 'files'];

		if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
		{
			if (function_exists('locale_accept_from_http'))
			{
			   $lang = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
			}
50
51
52
53
54
55
56
57







58

59
60
61
62
63
64
65
		}
		else
		{
			$lang = '';
		}

		$config = Config::getInstance();








		$config = array_intersect_key($config->asArray(), array_flip($keys));


		self::$root_variables = [
			'root_url'     => WWW_URL,
			'request_url'  => Utils::getRequestURI(),
			'admin_url'    => ADMIN_URL,
			'_GET'         => &$_GET,
			'_POST'        => &$_POST,








>
>
>
>
>
>
>

>







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
		}
		else
		{
			$lang = '';
		}

		$config = Config::getInstance();

		$files = $config::FILES;

		// Put URL in files array
		array_walk($files, function (&$v, $k) use ($config) {
			$v = $config->fileURL($k);
		});

		$config = array_intersect_key($config->asArray(), array_flip($keys));
		$config['files'] = $files;

		self::$root_variables = [
			'root_url'     => WWW_URL,
			'request_url'  => Utils::getRequestURI(),
			'admin_url'    => ADMIN_URL,
			'_GET'         => &$_GET,
			'_POST'        => &$_POST,

Modified src/include/lib/Garradin/Utils.php from [920f71c5e9] to [04aa6a6cb3].

177
178
179
180
181
182
183



184
185
186
187
188
189
190
        return sprintf('%s%s%s%s', $sign, number_format($number, 0, $dec_point, $thousands_sep), $dec_point, $decimals);
    }

    static public function getLocalURL(string $url = '', ?string $default_prefix = null): string
    {
        if ($url[0] == '!') {
            return ADMIN_URL . substr($url, 1);



        }
        elseif ($url[0] == '/' && ($pos = strpos($url, WWW_URI)) === 0) {
            return WWW_URL . substr($url, strlen(WWW_URI));
        }
        elseif (substr($url, 0, 5) == 'http:' || substr($url, 0, 6) == 'https:') {
            return $url;
        }







>
>
>







177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
        return sprintf('%s%s%s%s', $sign, number_format($number, 0, $dec_point, $thousands_sep), $dec_point, $decimals);
    }

    static public function getLocalURL(string $url = '', ?string $default_prefix = null): string
    {
        if ($url[0] == '!') {
            return ADMIN_URL . substr($url, 1);
        }
        elseif (substr($url, 0, 7) == '/admin/') {
            return ADMIN_URL . substr($url, 7);
        }
        elseif ($url[0] == '/' && ($pos = strpos($url, WWW_URI)) === 0) {
            return WWW_URL . substr($url, strlen(WWW_URI));
        }
        elseif (substr($url, 0, 5) == 'http:' || substr($url, 0, 6) == 'https:') {
            return $url;
        }
832
833
834
835
836
837
838
839


840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
        $g /= 255;
        $b /= 255;
        $max = max($r, $g, $b);
        $min = min($r, $g, $b);
        $h = $s = $v = $max;

        $d = $max - $min;
        $s = ($max == 0) ? 0 : $d / $max;



        if($max == $min)
        {
            $h = 0; // achromatic
        }
        else
        {
            switch($max)
            {
                case $r: $h = ($g - $b) / $d + ($g < $b ? 6 : 0); break;
                case $g: $h = ($b - $r) / $d + 2; break;
                case $b: $h = ($r - $g) / $d + 4; break;
            }
            $h /= 6;
        }

        return array($h * 360, $s, $v);
    }

    static public function HTTPCache(?string $hash, int $last_change): bool
    {
        $etag = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : null;
        $last_modified = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : null;








|
>
>
















|







835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
        $g /= 255;
        $b /= 255;
        $max = max($r, $g, $b);
        $min = min($r, $g, $b);
        $h = $s = $v = $max;

        $d = $max - $min;
        //$s = ($max == 0) ? 0 : $d / $max;
        $l = ($max + $min) / 2;
        $s = $l > 0.5 ? $d / ((2 - $max - $min) ?: 1) : $d / (($max + $min) ?: 1);

        if($max == $min)
        {
            $h = 0; // achromatic
        }
        else
        {
            switch($max)
            {
                case $r: $h = ($g - $b) / $d + ($g < $b ? 6 : 0); break;
                case $g: $h = ($b - $r) / $d + 2; break;
                case $b: $h = ($r - $g) / $d + 4; break;
            }
            $h /= 6;
        }

        return array($h * 360, $s, $l);
    }

    static public function HTTPCache(?string $hash, int $last_change): bool
    {
        $etag = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : null;
        $last_modified = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : null;

1039
1040
1041
1042
1043
1044
1045

1046
1047
1048
1049
1050
1051
1052
        $cmd = PDF_COMMAND;

        if (!$cmd) {
            // Try to see if there's a plugin
            $in = ['source' => $source, 'target' => $target];

            if (Plugin::fireSignal('pdf.create', $in)) {

                return $target;
            }

            unset($in);

            // Try to find a local executable
            $list = ['prince', 'chromium', 'wkhtmltopdf', 'weasyprint'];







>







1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
        $cmd = PDF_COMMAND;

        if (!$cmd) {
            // Try to see if there's a plugin
            $in = ['source' => $source, 'target' => $target];

            if (Plugin::fireSignal('pdf.create', $in)) {
                Utils::safe_unlink($source);
                return $target;
            }

            unset($in);

            // Try to find a local executable
            $list = ['prince', 'chromium', 'wkhtmltopdf', 'weasyprint'];
1078
1079
1080
1081
1082
1083
1084

1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
                $cmd = 'weasyprint %1$s %2$s';
                break;
            default:
                break;
        }

        exec(sprintf($cmd, escapeshellarg($source), escapeshellarg($target)));


        if (!file_exists($target)) {
            throw new \RuntimeException('PDF command failed');
        }

        unlink($source);

        return $target;
    }

    /**
     * Integer to A-Z, AA-ZZ, AAA-ZZZ, etc.
     * @see https://www.php.net/manual/fr/function.base-convert.php#94874
     */







>





<
<







1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096


1097
1098
1099
1100
1101
1102
1103
                $cmd = 'weasyprint %1$s %2$s';
                break;
            default:
                break;
        }

        exec(sprintf($cmd, escapeshellarg($source), escapeshellarg($target)));
        Utils::safe_unlink($source);

        if (!file_exists($target)) {
            throw new \RuntimeException('PDF command failed');
        }



        return $target;
    }

    /**
     * Integer to A-Z, AA-ZZ, AAA-ZZZ, etc.
     * @see https://www.php.net/manual/fr/function.base-convert.php#94874
     */

Modified src/include/lib/Garradin/Web/Render/Skriv.php from [ce5e528272] to [02e93f8ff3].

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

		if (!$name || null === $this->current_path)
		{
			return $skriv->parseError('/!\ Tag image : aucun nom de fichier indiqué.');
		}

		$url = $this->resolveAttachment($name);
		$thumb_url = sprintf('%s?%dpx', $url, $align == 'center' ? 500 : 200);

		$out = sprintf('<a href="%s" class="internal-image" target="_image"><img src="%s" alt="%s" loading="lazy" /></a>',
			htmlspecialchars($url),
			htmlspecialchars($thumb_url),
			htmlspecialchars($caption ?? $name)
		);








|







104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

		if (!$name || null === $this->current_path)
		{
			return $skriv->parseError('/!\ Tag image : aucun nom de fichier indiqué.');
		}

		$url = $this->resolveAttachment($name);
		$thumb_url = sprintf('%s?%s', $url, $align == 'center' ? '500px' : '200px');

		$out = sprintf('<a href="%s" class="internal-image" target="_image"><img src="%s" alt="%s" loading="lazy" /></a>',
			htmlspecialchars($url),
			htmlspecialchars($thumb_url),
			htmlspecialchars($caption ?? $name)
		);

Modified src/include/lib/Garradin/Web/Web.php from [2a719367c6] to [a0e6ad5801].

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use Garradin\Utils;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Membres\Session;

use KD2\DB\EntityManager as EM;

use const Garradin\{WWW_URI, ADMIN_URL, FILE_STORAGE_BACKEND};

class Web
{
	static public function search(string $search): array
	{
		$results = Files::search($search, File::CONTEXT_WEB . '%');








|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use Garradin\Utils;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Membres\Session;

use KD2\DB\EntityManager as EM;

use const Garradin\{WWW_URI, ADMIN_URL, FILE_STORAGE_BACKEND, ROOT};

class Web
{
	static public function search(string $search): array
	{
		$results = Files::search($search, File::CONTEXT_WEB . '%');

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
		// Redirect old URLs (pre-1.1)
		if ($uri == 'feed/atom/') {
			Utils::redirect('/atom.xml');
		}
		elseif (substr($uri, 0, 4) == 'api/') {
			API::dispatchURI(substr($uri, 4));
			exit;




		}
		elseif (substr($uri, 0, 6) === 'admin/') {
			http_response_code(404);
			throw new UserException('Cette page n\'existe pas.');
		}
		elseif (($file = Files::getFromURI($uri))
			|| ($file = self::getAttachmentFromURI($uri))) {
			$size = null;


			foreach ($_GET as $key => $value) {
				if (substr($key, -2) == 'px') {
					$size = (int)substr($key, 0, -2);
					break;

				}
			}

			$session = Session::getInstance();

			if (Plugin::fireSignal('http.request.file.before', compact('file', 'uri', 'session'))) {
				// If a plugin handled the request, let's stop here







>
>
>
>









>
|
|
|
|
>







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
		// Redirect old URLs (pre-1.1)
		if ($uri == 'feed/atom/') {
			Utils::redirect('/atom.xml');
		}
		elseif (substr($uri, 0, 4) == 'api/') {
			API::dispatchURI(substr($uri, 4));
			exit;
		}
		elseif ($uri == 'favicon.ico') {
			header('Location: ' . Config::getInstance()->fileURL('favicon'), true);
			return;
		}
		elseif (substr($uri, 0, 6) === 'admin/') {
			http_response_code(404);
			throw new UserException('Cette page n\'existe pas.');
		}
		elseif (($file = Files::getFromURI($uri))
			|| ($file = self::getAttachmentFromURI($uri))) {
			$size = null;

			if ($file->image) {
				foreach ($_GET as $key => $v) {
					if (array_key_exists($key, File::ALLOWED_THUMB_SIZES)) {
						$size = $key;
						break;
					}
				}
			}

			$session = Session::getInstance();

			if (Plugin::fireSignal('http.request.file.before', compact('file', 'uri', 'session'))) {
				// If a plugin handled the request, let's stop here

Modified src/include/lib/dependencies.list from [b3a90c3add] to [e9b8ffa0d9].

9
10
11
12
13
14
15

16
17
18
19
20
21
22
23
24
25
26
KD2/Form.php
KD2/FossilInstaller.php
KD2/HTTP.php
KD2/Graphics/Image.php
KD2/Graphics/QRCode.php
KD2/Graphics/SVG/Pie.php
KD2/Graphics/SVG/Plot.php

KD2/Office/Calc/Writer.php
KD2/Security.php
KD2/Security_OTP.php
KD2/SimpleDiff.php
KD2/SkrivLite.php
KD2/Smartyer.php
KD2/SMTP.php
KD2/Translate.php
KD2/UserSession.php
KD2/ZipWriter.php
Parsedown.php







>











9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
KD2/Form.php
KD2/FossilInstaller.php
KD2/HTTP.php
KD2/Graphics/Image.php
KD2/Graphics/QRCode.php
KD2/Graphics/SVG/Pie.php
KD2/Graphics/SVG/Plot.php
KD2/Graphics/SVG/Bar.php
KD2/Office/Calc/Writer.php
KD2/Security.php
KD2/Security_OTP.php
KD2/SimpleDiff.php
KD2/SkrivLite.php
KD2/Smartyer.php
KD2/SMTP.php
KD2/Translate.php
KD2/UserSession.php
KD2/ZipWriter.php
Parsedown.php

Modified src/templates/acc/accounts/journal.tpl from [0463c7f428] to [92b1619c26].

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
				<p class="alert block">Ce tiers vous doit <strong>{$sum|abs|raw|money_currency}</strong>.</p>
			{else}
				<p class="confirm block">Vous ne devez pas d'argent à ce tiers, et il ne vous en doit pas non plus.</p>
			{/if}
		{elseif $account.type == $account::TYPE_BANK}
			{if $sum < 0}
				<p class="error block">Ce compte est à découvert de <strong>{$sum|abs|raw|money_currency}</strong> à la banque.</p>
			{elseif $sum >= 0}
				<p class="confirm block">Ce compte est créditeur de <strong>{$sum|abs|raw|money_currency}</strong> à la banque.</p>
			{/if}
		{elseif $account.type == $account::TYPE_CASH}
			{if $sum < 0}
				<p class="error block">Cette caisse est débiteur de <strong>{$sum|abs|raw|money_currency}</strong>. Est-ce normal&nbsp;? Une vérification est peut-être nécessaire&nbsp;?</p>
			{elseif $sum >= 0}
				<p class="confirm block">Cette caisse est créditrice de <strong>{$sum|abs|raw|money_currency}</strong>.</p>
			{/if}
		{elseif $account.type == $account::TYPE_OUTSTANDING}
			{if $sum < 0}
				<p class="error block">Ce compte est débiteur <strong>{$sum|abs|raw|money_currency}</strong>. Est-ce normal&nbsp;? Une vérification est peut-être nécessaire&nbsp;?</p>
			{elseif $sum >= 0}
				<p class="confirm block">Ce compte d'attente est créditeur de <strong>{$sum|abs|raw|money_currency}</strong>. {if $sum > 200}Un dépôt à la banque serait peut-être une bonne idée&nbsp;?{/if}</p>
			{/if}
		{elseif $account.type == $account::TYPE_REVENUE && $sum < 0}
			<p class="alert block">Ce compte présente un solde négatif de <strong>{$sum|raw|money_currency}</strong>. Est-ce normal&nbsp;? Cette situation ne devrait se produire que si vous avez dû procéder à des remboursements par exemple, et que ceux-ci couvrent des recettes perçues sur un exercice précédent.</p>
		{elseif $account.type == $account::TYPE_EXPENSE && $sum < 0}
			<p class="alert block">Ce compte présente un solde négatif de <strong>{$sum|raw|money_currency}</strong>. Est-ce normal&nbsp;? Cette situation ne devrait se produire que si vous avez reçu des remboursements par exemple, et que ceux-ci couvrent des dépenses réglées sur un exercice précédent.</p>
		{/if}







|





|





|







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
				<p class="alert block">Ce tiers vous doit <strong>{$sum|abs|raw|money_currency}</strong>.</p>
			{else}
				<p class="confirm block">Vous ne devez pas d'argent à ce tiers, et il ne vous en doit pas non plus.</p>
			{/if}
		{elseif $account.type == $account::TYPE_BANK}
			{if $sum < 0}
				<p class="error block">Ce compte est à découvert de <strong>{$sum|abs|raw|money_currency}</strong> à la banque.</p>
			{elseif $sum > 0}
				<p class="confirm block">Ce compte est créditeur de <strong>{$sum|abs|raw|money_currency}</strong> à la banque.</p>
			{/if}
		{elseif $account.type == $account::TYPE_CASH}
			{if $sum < 0}
				<p class="error block">Cette caisse est débiteur de <strong>{$sum|abs|raw|money_currency}</strong>. Est-ce normal&nbsp;? Une vérification est peut-être nécessaire&nbsp;?</p>
			{elseif $sum > 0}
				<p class="confirm block">Cette caisse est créditrice de <strong>{$sum|abs|raw|money_currency}</strong>.</p>
			{/if}
		{elseif $account.type == $account::TYPE_OUTSTANDING}
			{if $sum < 0}
				<p class="error block">Ce compte est débiteur <strong>{$sum|abs|raw|money_currency}</strong>. Est-ce normal&nbsp;? Une vérification est peut-être nécessaire&nbsp;?</p>
			{elseif $sum > 0}
				<p class="confirm block">Ce compte d'attente est créditeur de <strong>{$sum|abs|raw|money_currency}</strong>. {if $sum > 200}Un dépôt à la banque serait peut-être une bonne idée&nbsp;?{/if}</p>
			{/if}
		{elseif $account.type == $account::TYPE_REVENUE && $sum < 0}
			<p class="alert block">Ce compte présente un solde négatif de <strong>{$sum|raw|money_currency}</strong>. Est-ce normal&nbsp;? Cette situation ne devrait se produire que si vous avez dû procéder à des remboursements par exemple, et que ceux-ci couvrent des recettes perçues sur un exercice précédent.</p>
		{elseif $account.type == $account::TYPE_EXPENSE && $sum < 0}
			<p class="alert block">Ce compte présente un solde négatif de <strong>{$sum|raw|money_currency}</strong>. Est-ce normal&nbsp;? Cette situation ne devrait se produire que si vous avez reçu des remboursements par exemple, et que ceux-ci couvrent des dépenses réglées sur un exercice précédent.</p>
		{/if}

Modified src/templates/acc/reports/_header.tpl from [b23bfd50d9] to [2032e3497e].

22
23
24
25
26
27
28













29
30
31
32
33
34
	{if isset($analytical)}
		<h3>Projet&nbsp;: {$analytical.label}</h3>
	{/if}
	{if isset($year)}
		<p>Exercice&nbsp;: {$year.label} ({if $year.closed}clôturé{else}en cours{/if}, du
			{$year.start_date|date_short} au {$year.end_date|date_short}, généré le {$close_date|date_short})</p>
	{/if}














	<p class="noprint print-btn">
		<button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button>
		{linkbutton shape="download" href="%s&_pdf"|args:$self_url label="Télécharger en PDF"}
	</p>
</div>







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






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
	{if isset($analytical)}
		<h3>Projet&nbsp;: {$analytical.label}</h3>
	{/if}
	{if isset($year)}
		<p>Exercice&nbsp;: {$year.label} ({if $year.closed}clôturé{else}en cours{/if}, du
			{$year.start_date|date_short} au {$year.end_date|date_short}, généré le {$close_date|date_short})</p>
	{/if}

	{if !empty($allow_compare) && !empty($other_years)}
	<form method="get" action="" class="noprint">
		<fieldset>
			<legend>Comparer avec un autre exercice</legend>
			<p>
				{input type="select" name="compare_year" options=$other_years default=$criterias.compare_year}
				{button type="submit" label="Comparer" shape="right"}
			</p>
			<input type="hidden" name="year" value="{$year.id}" />
		</fieldset>
	</form>
	{/if}

	<p class="noprint print-btn">
		<button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button>
		{linkbutton shape="download" href="%s&_pdf"|args:$self_url label="Télécharger en PDF"}
	</p>
</div>

Modified src/templates/acc/reports/_statement.tpl from [674c33b3c6] to [9f0303b5cc].

16
17
18
19
20
21
22
23




24
25
26
27
28
29
30
31
32
33




34
35
36
37
38
39
40
41
42
43
44
45
46
47




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




60
61
62
63
64
65
66
67
68
	<tfoot>
		<tr>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total</th>
							<td class="money">{$statement.expense_sum|raw|money:false}</td>




						</tr>
					</tfoot>
				</table>
			</td>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total</th>
							<td class="money">{$statement.revenue_sum|raw|money:false}</td>




						</tr>
					</tfoot>
				</table>
			</td>
		</tr>
		{if $statement.result}
		<tr>
			<td>
			{if ($statement.result < 0)}
				<table>
					<tfoot>
						<tr>
							<th>Résultat (perte)</th>
							<td class="money">{$statement.result|raw|money:false}</td>




						</tr>
					</tfoot>
				</table>
			{/if}
			</td>
			<td>
			{if ($statement.result >= 0)}
				<table>
					<tfoot>
						<tr>
							<th>Résultat (excédent)</th>
							<td class="money">{$statement.result|raw|money:false}</td>




						</tr>
					</tfoot>
				</table>
			{/if}
			</td>
		</tr>
		{/if}
	</tfoot>
</table>







|
>
>
>
>









|
>
>
>
>













|
>
>
>
>











|
>
>
>
>









16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
	<tfoot>
		<tr>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total</th>
							<td class="money" width="10%">{$statement.expense_sum|raw|money:false}</td>
							{if $statement.expense_sum2}
							<td class="money" width="10%">{$statement.expense_sum2|raw|money:false}</td>
							<td class="money" width="10%">{$statement.expense_change|raw|money:true:true}</td>
							{/if}
						</tr>
					</tfoot>
				</table>
			</td>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total</th>
							<td class="money" width="10%">{$statement.revenue_sum|raw|money:false}</td>
							{if $statement.revenue_sum2}
							<td class="money" width="10%">{$statement.revenue_sum2|raw|money:false}</td>
							<td class="money" width="10%">{$statement.revenue_change|raw|money:true:true}</td>
							{/if}
						</tr>
					</tfoot>
				</table>
			</td>
		</tr>
		{if $statement.result}
		<tr>
			<td>
			{if ($statement.result < 0)}
				<table>
					<tfoot>
						<tr>
							<th>Résultat (perte)</th>
							<td class="money" width="10%">{$statement.result|raw|money:false}</td>
							{if $statement.result2}
							<td class="money" width="10%">{$statement.result2|raw|money:false}</td>
							<td class="money" width="10%">{$statement.result_change|raw|money:true:true}</td>
							{/if}
						</tr>
					</tfoot>
				</table>
			{/if}
			</td>
			<td>
			{if ($statement.result >= 0)}
				<table>
					<tfoot>
						<tr>
							<th>Résultat (excédent)</th>
							<td class="money" width="10%">{$statement.result|raw|money:false}</td>
							{if $statement.result2}
							<td class="money" width="10%">{$statement.result2|raw|money:false}</td>
							<td class="money" width="10%">{$statement.result_change|raw|money:true:true}</td>
							{/if}
						</tr>
					</tfoot>
				</table>
			{/if}
			</td>
		</tr>
		{/if}
	</tfoot>
</table>

Modified src/templates/acc/reports/_statement_table.tpl from [cb39c0e999] to [29eca66656].

1
2
3
4











5
6
7
8
9
10
11
12
13
14




15
16
17
18
<table class="list">
	{if !empty($caption)}
		<caption><h3>{$caption}</h3></caption>
	{/if}











	<tbody>
	{foreach from=$accounts item="account"}
		<tr class="compte">
			<td class="num">
				{if !empty($year)}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&amp;year={$year.id}">{$account.code}</a>
				{else}{$account.code}
				{/if}
			</td>
			<th>{$account.label}</th>
			<td class="money">{$account.sum|raw|money}</td>




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




>
>
>
>
>
>
>
>
>
>
>


|






|
>
>
>
>




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<table class="list">
	{if !empty($caption)}
		<caption><h3>{$caption}</h3></caption>
	{/if}
	{if !empty($year2)}
		<thead>
			<tr>
				<td></td>
				<th></th>
				<td class="money" width="10%">{$year->label_years()}</td>
				<td class="money" width="10%">{$year2->label_years()}</td>
				<td class="money" width="10%">Écart</td>
			</tr>
		</thead>
	{/if}
	<tbody>
	{foreach from=$accounts item="account"}
		<tr class="compte{if isset($year2) && !$account.sum} disabled{/if}">
			<td class="num">
				{if !empty($year)}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&amp;year={$year.id}">{$account.code}</a>
				{else}{$account.code}
				{/if}
			</td>
			<th>{$account.label}</th>
			<td class="money">{$account.sum|raw|money:false}</td>
			{if isset($year2)}
				<td class="money">{$account.sum2|raw|money:false}</td>
				<td class="money">{$account.change|raw|money:true:true}</td>
			{/if}
		</tr>
	{/foreach}
	</tbody>
</table>

Modified src/templates/acc/reports/balance_sheet.tpl from [dc88e4cf11] to [b458502c4d].

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="Bilan" current="acc/years"}

{include file="acc/reports/_header.tpl" current="balance_sheet" title="Bilan"}

{if $asset_sum != $liability_sum}
	<p class="alert block">
		<strong>Le bilan n'est pas équilibré&nbsp;!</strong><br />
		Vérifiez que vous n'avez pas oublié de reporter des soldes depuis le précédent exercice.
	</p>
{/if}

<table class="statement">
	<colgroup>
		<col width="50%" />
		<col width="50%" />
	</colgroup>
	<tbody>
		<tr>
			<td>
				{include file="acc/reports/_statement_table.tpl" accounts=$asset caption="Actif"}
			</td>
			<td>
				{include file="acc/reports/_statement_table.tpl" accounts=$liability caption="Passif"}
			</td>
		</tr>
	</tbody>
	<tfoot>
		<tr>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total actif</th>
							<td class="money">{$asset_sum|raw|money:false}</td>




						</tr>
					</tfoot>
				</table>
			</td>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total passif</th>
							<td class="money">{$liability_sum|raw|money:false}</td>




						</tr>
					</tfoot>
				</table>
			</td>
		</tr>
	</tfoot>
</table>

<p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p>

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


|

|














|


|










|
>
>
>
>









|
>
>
>
>











1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
{include file="admin/_head.tpl" title="Bilan" current="acc/years"}

{include file="acc/reports/_header.tpl" current="balance_sheet" title="Bilan" allow_compare=true}

{if $balance.sums.asset != $balance.sums.liability}
	<p class="alert block">
		<strong>Le bilan n'est pas équilibré&nbsp;!</strong><br />
		Vérifiez que vous n'avez pas oublié de reporter des soldes depuis le précédent exercice.
	</p>
{/if}

<table class="statement">
	<colgroup>
		<col width="50%" />
		<col width="50%" />
	</colgroup>
	<tbody>
		<tr>
			<td>
				{include file="acc/reports/_statement_table.tpl" accounts=$balance.accounts.asset caption="Actif"}
			</td>
			<td>
				{include file="acc/reports/_statement_table.tpl" accounts=$balance.accounts.liability caption="Passif"}
			</td>
		</tr>
	</tbody>
	<tfoot>
		<tr>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total actif</th>
							<td class="money" width="10%">{$balance.sums.asset|raw|money:false}</td>
							{if isset($year2)}
							<td class="money" width="10%">{$balance.sums2.asset|raw|money:false}</td>
							<td class="money" width="10%">{$balance.change.asset|raw|money:true:true}</td>
							{/if}
						</tr>
					</tfoot>
				</table>
			</td>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total passif</th>
							<td class="money" width="10%">{$balance.sums.liability|raw|money:false}</td>
							{if isset($year2)}
							<td class="money" width="10%">{$balance.sums2.liability|raw|money:false}</td>
							<td class="money" width="10%">{$balance.change.liability|raw|money:true:true}</td>
							{/if}
						</tr>
					</tfoot>
				</table>
			</td>
		</tr>
	</tfoot>
</table>

<p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p>

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

Modified src/templates/acc/reports/statement.tpl from [176cb70b6d] to [b37ad29a93].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{include file="admin/_head.tpl" title="Compte de résultat" current="acc/years"}

{include file="acc/reports/_header.tpl" current="statement" title="Compte de résultat"}

{include file="acc/reports/_statement.tpl" statement=$general caption1="Charges" caption2="Produits"}

{if !empty($volunteering.expense_sum) || !empty($volunteering.revenue_sum)}
	<h2 class="ruler">Contributions en nature</h2>
	{include file="acc/reports/_statement.tpl" statement=$volunteering header=false caption1="Emplois des contributions volontaires en nature" caption2="Contributions volontaires en nature"}
{/if}

<p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p>

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


|











1
2
3
4
5
6
7
8
9
10
11
12
13
14
{include file="admin/_head.tpl" title="Compte de résultat" current="acc/years"}

{include file="acc/reports/_header.tpl" current="statement" title="Compte de résultat" allow_compare=true}

{include file="acc/reports/_statement.tpl" statement=$general caption1="Charges" caption2="Produits"}

{if !empty($volunteering.expense_sum) || !empty($volunteering.revenue_sum)}
	<h2 class="ruler">Contributions en nature</h2>
	{include file="acc/reports/_statement.tpl" statement=$volunteering header=false caption1="Emplois des contributions volontaires en nature" caption2="Contributions volontaires en nature"}
{/if}

<p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p>

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

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

31
32
33
34
35
36
37

38
39
40

41
42
43
44
45
46
47

{if !empty($list)}
	{if count($list) > 1}
	<section class="year-infos">
		<section class="graphs">
			<figure>
				<img src="{$admin_url}acc/reports/graph_plot_all.php?type=assets" alt="" />

			</figure>
			<figure>
				<img src="{$admin_url}acc/reports/graph_plot_all.php?type=result" alt="" />

			</figure>
		</section>
	</section>
	{/if}

	<table class="list">
	{foreach from=$list item="year"}







>



>







31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

{if !empty($list)}
	{if count($list) > 1}
	<section class="year-infos">
		<section class="graphs">
			<figure>
				<img src="{$admin_url}acc/reports/graph_plot_all.php?type=assets" alt="" />
				<figcaption>Soldes des banques et caisses par exercice</figcaption>
			</figure>
			<figure>
				<img src="{$admin_url}acc/reports/graph_plot_all.php?type=result" alt="" />
				<figcaption>Recettes et dépenses par exercice</figcaption>
			</figure>
		</section>
	</section>
	{/if}

	<table class="list">
	{foreach from=$list item="year"}

Modified src/templates/admin/_head.tpl from [384cd3607b] to [0c365a5631].

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" />
    <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} data-version="{$version_hash}">
<head>
    <meta charset="utf-8" />
    <meta name="v" content="{$version_hash}" />
    <title>{$title}</title>

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <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}
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
    {if isset($plugin_js)}
        {foreach from=$plugin_js item="js"}
            <script type="text/javascript" src="{plugin_url file=$js}?{$version_hash}"></script>
        {/foreach}
    {/if}
    <link rel="stylesheet" type="text/css" href="{$admin_url}static/print.css?{$version_hash}" media="print" />
    <link rel="stylesheet" type="text/css" href="{$admin_url}static/handheld.css?{$version_hash}" media="handheld,screen and (max-width:981px)" />
    {if !empty($current) && $current == 'home'}
    <link rel="manifest" href="{$admin_url}manifest.php?{$version_hash}" />
    {/if}
    {if isset($config)}

        {custom_colors config=$config}
    {/if}
</head>

<body{if isset($transparent)} class="transparent"{/if}>

{if !array_key_exists('_dialog', $_GET) && !isset($transparent)}
<header class="header">
    <nav class="menu">







    <ul>
    {if $is_logged}
    <?php
    $current_parent = substr($current, 0, strpos($current, '/'));
    ?>
        <li class="home{if $current == 'home'} current{elseif $current_parent == 'home'} current_parent{/if}">
            <a href="{$admin_url}"><b class="icn">⌂</b><i> Accueil</i></a>







<
|
<

>









>
>
>
>
>
>
>







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
    {if isset($plugin_js)}
        {foreach from=$plugin_js item="js"}
            <script type="text/javascript" src="{plugin_url file=$js}?{$version_hash}"></script>
        {/foreach}
    {/if}
    <link rel="stylesheet" type="text/css" href="{$admin_url}static/print.css?{$version_hash}" media="print" />
    <link rel="stylesheet" type="text/css" href="{$admin_url}static/handheld.css?{$version_hash}" media="handheld,screen and (max-width:981px)" />

    <link rel="manifest" href="{$admin_url}manifest.php" />

    {if isset($config)}
        <link rel="icon" type="image/png" href="{$config->fileURL('favicon')}" />
        {custom_colors config=$config}
    {/if}
</head>

<body{if isset($transparent)} class="transparent"{/if}>

{if !array_key_exists('_dialog', $_GET) && !isset($transparent)}
<header class="header">
    <nav class="menu">
        {if isset($config)}
        <figure class="logo">
        {if $url = $config->fileURL('logo', '150px')}
            <a href="{$admin_url}"><img src="{$url}" alt="" /></a>
        {/if}
        </figure>
        {/if}
    <ul>
    {if $is_logged}
    <?php
    $current_parent = substr($current, 0, strpos($current, '/'));
    ?>
        <li class="home{if $current == 'home'} current{elseif $current_parent == 'home'} current_parent{/if}">
            <a href="{$admin_url}"><b class="icn">⌂</b><i> Accueil</i></a>
106
107
108
109
110
111
112







113
114
115
116
117
118
119
                <li{if $current == 'me/services'}  class="current"{/if}><a href="{$admin_url}me/services.php">Mes activités &amp; cotisations</a></li>
            </ul>
        </li>

        {if !defined('Garradin\LOCAL_LOGIN') || !LOCAL_LOGIN}
            <li class="logout"><a href="{$admin_url}logout.php"><b class="icn">⤝</b><i> Déconnexion</i></a></li>
        {/if}







    {elseif !defined('Garradin\INSTALL_PROCESS')}
        <li><a href="{if $config.site_asso}{$config.site_asso}{else}{$www_url}{/if}">&larr; Retour au site</a></li>
        <li><a href="{$admin_url}">Connexion</a>
            <ul>
                <li><a href="{$admin_url}password.php">Mot de passe perdu</a>
            </ul>
        </li>







>
>
>
>
>
>
>







112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
                <li{if $current == 'me/services'}  class="current"{/if}><a href="{$admin_url}me/services.php">Mes activités &amp; cotisations</a></li>
            </ul>
        </li>

        {if !defined('Garradin\LOCAL_LOGIN') || !LOCAL_LOGIN}
            <li class="logout"><a href="{$admin_url}logout.php"><b class="icn">⤝</b><i> Déconnexion</i></a></li>
        {/if}

        {if $help_url}
        <li>
            <a href="{$help_url}" target="_blank"><b class="icn">❓</b><i> Aide</i></a>
        </li>
        {/if}

    {elseif !defined('Garradin\INSTALL_PROCESS')}
        <li><a href="{if $config.site_asso}{$config.site_asso}{else}{$www_url}{/if}">&larr; Retour au site</a></li>
        <li><a href="{$admin_url}">Connexion</a>
            <ul>
                <li><a href="{$admin_url}password.php">Mot de passe perdu</a>
            </ul>
        </li>

Modified src/templates/admin/config/_menu.tpl from [91dfe799a4] to [3e2e12b62f].

1
2
3

4
5
6
7
8
9
10
<nav class="tabs">
	<ul>
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Général</a></li>

		<li{if $current == 'categories'} class="current"{/if}><a href="{$admin_url}config/categories/">Catégories de membres</a></li>
		<li{if $current == 'fiches_membres'} class="current"{/if}><a href="{$admin_url}config/membres.php">Fiche des membres</a></li>
		<li{if $current == 'backup'} class="current"{/if}><a href="{$admin_url}config/backup/">Sauvegardes</a></li>
		<li{if $current == 'plugins'} class="current"{/if}><a href="{$admin_url}config/plugins.php">Extensions</a></li>
		<li{if $current == 'advanced'} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li>
	</ul>




>







1
2
3
4
5
6
7
8
9
10
11
<nav class="tabs">
	<ul>
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Général</a></li>
		<li{if $current == 'custom'} class="current"{/if}><a href="{$admin_url}config/custom.php">Personnalisation</a></li>
		<li{if $current == 'categories'} class="current"{/if}><a href="{$admin_url}config/categories/">Catégories de membres</a></li>
		<li{if $current == 'fiches_membres'} class="current"{/if}><a href="{$admin_url}config/membres.php">Fiche des membres</a></li>
		<li{if $current == 'backup'} class="current"{/if}><a href="{$admin_url}config/backup/">Sauvegardes</a></li>
		<li{if $current == 'plugins'} class="current"{/if}><a href="{$admin_url}config/plugins.php">Extensions</a></li>
		<li{if $current == 'advanced'} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li>
	</ul>

Added src/templates/admin/config/custom.tpl version [f0931e6c67].























































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
{include file="admin/_head.tpl" title="Personnalisation" current="config" custom_css=['config.css']}

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

{if isset($_GET['ok']) && !$form->hasErrors()}
	<p class="block confirm">
		La configuration a bien été enregistrée.
	</p>
{/if}

{form_errors}

<fieldset>
	<legend>Association et site web</legend>
	<dl>
		<dt>Logo</dt>
		{if $url = $config->fileURL('logo', '150px')}
		<dd>
			<img src="{$url}" alt="" />
		</dd>
		{/if}
		<dd>
			{linkbutton href="!config/edit_file.php?k=%s"|args:'logo' label="Modifier" shape="edit" target="_dialog"}
		</dd>
		<dd class="help">
			Ce logo sera affiché en haut du menu de l'administration, sur le site web et sur les documents imprimés.
		</dd>
		<dt>Petite icône</dt>
		{if $url = $config->fileURL('favicon')}
		<dd>
			<img src="{$url}" alt="" />
		</dd>
		{/if}
		<dd>
			{linkbutton href="!config/edit_file.php?k=%s"|args:'favicon' label="Modifier" shape="edit" target="_dialog"}
		</dd>
		<dd class="help">
			Cette image sera affichée dans l'onglet du navigateur (favicon).
		</dd>
		<dt>Grande icône</dt>
		{if $url = $config->fileURL('icon', '150px')}
		<dd class="image-preview">
			<img src="{$url}" alt="" />
			<figure class="masked-icon" title="Aperçu de l'icône sur téléphone">
				<span class="icon"><img src="{$url}" alt="" /></span>
				<figcaption>{$config.nom_asso|truncate:12:'…':true}</figcaption>
			</figure>
		</dd>
		{/if}
		<dd>
			{linkbutton href="!config/edit_file.php?k=%s"|args:'icon' label="Modifier" shape="edit" target="_dialog"}
		</dd>
		<dd class="help">
			Cette image sera utilisée comme icône de l'application mobile (à installer depuis {link href="!" label="la page d'accueil"} et le bouton «&nbsp;Installer comme application sur l'écran d'accueil&nbsp;»).
		</dd>
	</dl>
</fieldset>

<form method="post" action="{$self_url}">
	<fieldset>
		<legend>Interface d'administration</legend>
		<dl>
			{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>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>
			<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}" />

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


</form>

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

Added src/templates/admin/config/edit_image.tpl version [ecffc7fa59].







































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

{form_errors}

<form method="post" action="{$self_url}" enctype="multipart/form-data" data-focus="1">
	<fieldset>
		<legend>Téléverser un fichier</legend>
		<dl>
			{input type="file" name="file" label="Fichier à envoyer" data-enhanced=1}
		</dl>
		<p class="submit">
			{csrf_field key=$csrf_key}
			{button type="submit" name="upload" label="Envoyer" shape="upload" class="main"}
			{button type="submit" name="reset" label="Supprimer" shape="delete"}
		</p>
	</fieldset>
</form>

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

Modified src/templates/admin/config/index.tpl from [b8e165fd8b] to [6e42e917da].

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
		<dl>
			{input type="select" name="categorie_membres" source=$config options=$membres_cats required=true label="Catégorie par défaut des nouveaux membres"}
			{input type="select" name="champ_identite" source=$config options=$champs required=true label="Champ utilisé pour définir l'identité des membres" help="Ce champ des fiches membres sera utilisé comme identité du membre dans les emails, les fiches, les pages, etc."}
			{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"}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








63
64
65
66
67
68
69
























70
71
72
73
74
75
76
77
		<dl>
			{input type="select" name="categorie_membres" source=$config options=$membres_cats required=true label="Catégorie par défaut des nouveaux membres"}
			{input type="select" name="champ_identite" source=$config options=$champs required=true label="Champ utilisé pour définir l'identité des membres" help="Ce champ des fiches membres sera utilisé comme identité du membre dans les emails, les fiches, les pages, etc."}
			{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>

























	<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/config/upgrade.tpl from [cbbd2e2031] to [f0a3b1ba46].

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
			{if !$can_verify}
			<br />(Cela est probablement dû au fait que votre installation ne dispose pas du module <em>GnuPG</em>.)
			{/if}
		</p>
		{/if}
		<details>
			<summary><h3>{$diff.delete|count} fichiers seront supprimés</h3></summary>
			<p>
			{foreach from=$diff.delete key="file" item="path"}
				{$file}<br />
			{/foreach}
			</p>
		</details>
		<details>
			<summary><h3>{$diff.create|count} fichiers seront rajoutés</h3></summary>
			<p>
			{foreach from=$diff.create key="file" item="path"}
				{$file}<br />
			{/foreach}
			</p>
		</details>
		<details>
			<summary><h3>{$diff.update|count} fichiers seront modifiés</h3></summary>
			<p class="alert block">
				Si vous aviez bidouillé ces fichiers, les modifications seront écrasées.
			</p>
			<p>
			{foreach from=$diff.update key="file" item="path"}
				{$file}<br />
			{/foreach}
			</p>
		</details>
		<dl class="block error">
			{input type="checkbox" name="upgrade" value=$version label="Je confirme vouloir procéder à la mise à jour" help="Cette action peut casser votre installation !"}
		</dl>
	</fieldset>

	<p class="alert block">N'oubliez pas d'aller {link href="%swiki/?name=Changelog"|args:WEBSITE target="_blank" label="lire le journal des changements"} avant d'effectuer la mise à jour&nbsp;!</p>







|

|

|



|

|

|






|

|

|







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
			{if !$can_verify}
			<br />(Cela est probablement dû au fait que votre installation ne dispose pas du module <em>GnuPG</em>.)
			{/if}
		</p>
		{/if}
		<details>
			<summary><h3>{$diff.delete|count} fichiers seront supprimés</h3></summary>
			<dl>
			{foreach from=$diff.delete key="file" item="path"}
				<dd>{$file}</dd>
			{/foreach}
			</dl>
		</details>
		<details>
			<summary><h3>{$diff.create|count} fichiers seront rajoutés</h3></summary>
			<dl>
			{foreach from=$diff.create key="file" item="path"}
				<dd>{$file}</dd>
			{/foreach}
			</dl>
		</details>
		<details>
			<summary><h3>{$diff.update|count} fichiers seront modifiés</h3></summary>
			<p class="alert block">
				Si vous aviez bidouillé ces fichiers, les modifications seront écrasées.
			</p>
			<dl>
			{foreach from=$diff.update key="file" item="path"}
				<dd>{$file}</dd>
			{/foreach}
			</dl>
		</details>
		<dl class="block error">
			{input type="checkbox" name="upgrade" value=$version label="Je confirme vouloir procéder à la mise à jour" help="Cette action peut casser votre installation !"}
		</dl>
	</fieldset>

	<p class="alert block">N'oubliez pas d'aller {link href="%swiki/?name=Changelog"|args:WEBSITE target="_blank" label="lire le journal des changements"} avant d'effectuer la mise à jour&nbsp;!</p>

Modified src/templates/admin/index.tpl from [6f63e0375d] to [03e37c3c4d].

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="Bonjour %s !"|args:$user.identite current="home"}

{$banner|raw}

<nav class="tabs">
	<ul>
		<li><a href="{$admin_url}me/">Mes informations personnelles</a></li>
		<li><a href="{$admin_url}me/services.php">Suivi de mes activités et cotisations</a></li>
	</ul>
</nav>

<nav class="home-buttons">
	{button id="homescreen-btn" label="Ajouter comme application à l'écran d'accueil" class="hidden" shape="plus"}
</nav>

<aside class="describe">
	<h3>{$config.nom_asso}</h3>
	{if !empty($config.adresse_asso)}
	<p>
		{$config.adresse_asso|escape|nl2br}












|







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="Bonjour %s !"|args:$user.identite current="home"}

{$banner|raw}

<nav class="tabs">
	<ul>
		<li><a href="{$admin_url}me/">Mes informations personnelles</a></li>
		<li><a href="{$admin_url}me/services.php">Suivi de mes activités et cotisations</a></li>
	</ul>
</nav>

<nav class="home-buttons">
	{button id="homescreen-btn" label="Installer comme application sur l'écran d'accueil" class="hidden" shape="plus"}
</nav>

<aside class="describe">
	<h3>{$config.nom_asso}</h3>
	{if !empty($config.adresse_asso)}
	<p>
		{$config.adresse_asso|escape|nl2br}

Modified src/templates/common/files/edit_code.tpl from [bf7571f93a] to [7057de99c4].

8
9
10
11
12
13
14
15
16


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

</form>

<script type="text/javascript" src="{$admin_url}static/scripts/code_editor.js"></script>



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







|

>
>

8
9
10
11
12
13
14
15
16
17
18
19
	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

<script type="text/javascript" src="{$admin_url}static/scripts/code_editor.js?{$version_hash}"></script>

<script type="text/javascript">

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

Modified src/templates/common/files/edit_web.tpl from [c721ff089b] to [bf6e4a6a9c].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="admin/_head.tpl" title="Édition de fichier" custom_js=['wiki_editor.js']}

<form method="post" action="{$self_url}">
	<p class="textEditor">
		{input type="textarea" name="content" cols="70" rows="30" default=$content data-preview-url="!common/files/_preview.php?f=%s"|local_url|args:$file.path data-fullscreen="1" data-attachments="0" data-savebtn="1" data-format=$file->renderFormat()}
	</p>

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

</form>

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




|










1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="admin/_head.tpl" title="Édition de fichier" custom_js=['wiki_editor.js']}

<form method="post" action="{$self_url}">
	<p class="textEditor">
		{input type="textarea" name="content" cols="70" rows="30" default=$content data-preview-url="!common/files/_preview.php?f=%s"|local_url|args:$path data-fullscreen="1" data-attachments="0" data-savebtn="1" data-format=$format}
	</p>

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

</form>

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

Modified src/templates/services/user/add.tpl from [f7d8e48aef] to [e38a83f3e8].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="admin/_head.tpl" title="Inscrire à une activité" current="membres/services"}

{include file="services/_nav.tpl" current="save" fee=null service=null}

{form_errors}

{if !$user_id}
<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>
		<legend>Inscrire un membre à une activité</legend>
		<dl>
			{input type="list" name="user" required=1 label="Sélectionner un membre" default=$selected_user target="membres/selector.php"}
		</dl>
	</fieldset>







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="admin/_head.tpl" title="Inscrire à une activité" current="membres/services"}

{include file="services/_nav.tpl" current="save" fee=null service=null}

{form_errors}

{if !$user_id}
<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Inscrire un membre à une activité</legend>
		<dl>
			{input type="list" name="user" required=1 label="Sélectionner un membre" default=$selected_user target="membres/selector.php"}
		</dl>
	</fieldset>

Modified src/templates/web/page.tpl from [3803091252] to [618d6c6d33].

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	</aside>
	{else}
	<aside>
		{linkbutton href="?p=%s&toggle_type"|args:$page.path label="Transformer en catégorie" shape="reset"}
	</aside>
	{/if}
	<ul>
		<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)}







|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	</aside>
	{else}
	<aside>
		{linkbutton href="?p=%s&toggle_type"|args:$page.path label="Transformer en catégorie" shape="reset"}
	</aside>
	{/if}
	<ul>
		<li><a href="{$admin_url}web/?p={if $page.type == $page::TYPE_CATEGORY}{$page.path}{else}{$page.parent}{/if}">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)}

Modified src/www/.htaccess from [aeeaf470c2] to [477ff985ec].

1
2
3
4
5
6
7
8
Options -Indexes
DirectoryIndex disabled
DirectoryIndex index.php

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

# Si FallbackResource ne fonctionne pas, utiliser ceci :
|







1
2
3
4
5
6
7
8
Options -Indexes -Multiviews
DirectoryIndex disabled
DirectoryIndex index.php

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

# Si FallbackResource ne fonctionne pas, utiliser ceci :

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

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







<
<
<
<







12
13
14
15
16
17
18




19
20
21
22
23
24
25
if ('_route.php' === basename($uri)) {
	http_response_code(403);
	die('Appel interdit');
}

http_response_code(200);





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

if (file_exists(__DIR__ . $uri))
{

Modified src/www/admin/acc/charts/edit.php from [012860281d] to [1189423941].

14
15
16
17
18
19
20

21
22
23
24
25
26
27
}

if (f('save') && $form->check('acc_charts_edit_' . $chart->id()))
{
	try
	{
		$chart->importForm();

		$chart->save();

		Utils::redirect(sprintf('%sacc/charts/', ADMIN_URL));
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());







>







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

if (f('save') && $form->check('acc_charts_edit_' . $chart->id()))
{
	try
	{
		$chart->importForm();
		$chart->set('archived', (int) f('archived'));
		$chart->save();

		Utils::redirect(sprintf('%sacc/charts/', ADMIN_URL));
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());

Modified src/www/admin/acc/reports/_inc.php from [c7c07f3287] to [d05357fbdb].

40
41
42
43
44
45
46





47
48
49
	$criterias['analytical_only'] = true;
}

if (!count($criterias))
{
	throw new UserException('Critère de rapport inconnu.');
}






$tpl->assign('criterias', $criterias);
$tpl->assign('criterias_query', http_build_query($criterias));







>
>
>
>
>



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
	$criterias['analytical_only'] = true;
}

if (!count($criterias))
{
	throw new UserException('Critère de rapport inconnu.');
}

if ($y2 = Years::get((int)qg('compare_year'))) {
	$tpl->assign('year2', $y2);
	$criterias['compare_year'] = $y2->id;
}

$tpl->assign('criterias', $criterias);
$tpl->assign('criterias_query', http_build_query($criterias));

Modified src/www/admin/acc/reports/balance_sheet.php from [8ddee02813] to [f3e2995539].

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin;

use Garradin\Accounting\Reports;

use Garradin\Entities\Accounting\Account;

require_once __DIR__ . '/_inc.php';

$balance = Reports::getBalanceSheet($criterias);

$liability = $balance[Account::LIABILITY];
$asset = $balance[Account::ASSET];
$liability_sum = $balance['sums'][Account::LIABILITY];
$asset_sum = $balance['sums'][Account::ASSET];

$tpl->assign(compact('liability', 'asset', 'liability_sum', 'asset_sum'));

$tpl->display('acc/reports/balance_sheet.tpl');





>




|

|
|
|
<
|
<


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

16

17
18
<?php

namespace Garradin;

use Garradin\Accounting\Reports;
use Garradin\Accounting\Years;
use Garradin\Entities\Accounting\Account;

require_once __DIR__ . '/_inc.php';

$tpl->assign('balance', Reports::getBalanceSheet($criterias));

if (!empty($criterias['year'])) {
	$years = Years::listClosedAssocExcept($criterias['year']);
	$tpl->assign('other_years', count($years) ? [null => '-- Ne pas comparer'] + $years : $years);

}


$tpl->display('acc/reports/balance_sheet.tpl');

Modified src/www/admin/acc/reports/graph_plot_all.php from [de8f97a287] to [eb0fafc20b].

9
10
11
12
13
14
15
16
17

header('Content-Type: image/svg+xml');

$expiry = time() - 600;
$hash = sha1('plot_all');

if (!Utils::HTTPCache($hash, $expiry)) {
	echo Graph::plot(qg('type'), [], Graph::MONTHLY_INTERVAL, 600);
}







|

9
10
11
12
13
14
15
16
17

header('Content-Type: image/svg+xml');

$expiry = time() - 600;
$hash = sha1('plot_all');

if (!Utils::HTTPCache($hash, $expiry)) {
	echo Graph::bar(qg('type'), []);
}

Modified src/www/admin/acc/reports/statement.php from [3be4e9af46] to [45a0377453].

1
2
3
4
5

6
7
8
9
10
11
12





13
<?php

namespace Garradin;

use Garradin\Accounting\Reports;

use Garradin\Entities\Accounting\Account;

require_once __DIR__ . '/_inc.php';

$tpl->assign('general', Reports::getStatement($criterias + ['exclude_type' => Account::TYPE_VOLUNTEERING]));
$tpl->assign('volunteering', Reports::getStatement($criterias + ['type' => Account::TYPE_VOLUNTEERING]));






$tpl->display('acc/reports/statement.tpl');





>







>
>
>
>
>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin;

use Garradin\Accounting\Reports;
use Garradin\Accounting\Years;
use Garradin\Entities\Accounting\Account;

require_once __DIR__ . '/_inc.php';

$tpl->assign('general', Reports::getStatement($criterias + ['exclude_type' => Account::TYPE_VOLUNTEERING]));
$tpl->assign('volunteering', Reports::getStatement($criterias + ['type' => Account::TYPE_VOLUNTEERING]));

if (!empty($criterias['year'])) {
	$years = Years::listClosedAssocExcept($criterias['year']);
	$tpl->assign('other_years', count($years) ? [null => '-- Ne pas comparer'] + $years : $years);
}

$tpl->display('acc/reports/statement.tpl');

Modified src/www/admin/common/files/edit.php from [5a8ed5bb74] to [1c9c88333c].

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38


39
40
41

$form->runIf('content', function () use ($file) {
	$file->setContent(f('content'));

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

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

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

if (!$editor) {
	$tpl->assign('file', $file);
	$tpl->display('common/file_upload.tpl');
}
else {
	$content = $file->fetch();


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







<





<




>
>
|


21
22
23
24
25
26
27

28
29
30
31
32

33
34
35
36
37
38
39
40
41

$form->runIf('content', function () use ($file) {
	$file->setContent(f('content'));

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

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

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

if (!$editor) {

	$tpl->display('common/file_upload.tpl');
}
else {
	$content = $file->fetch();
	$path = $file->path;
	$format = $file->renderFormat();
	$tpl->assign(compact('csrf_key', 'content', 'path', 'format'));
	$tpl->display(sprintf('common/files/edit_%s.tpl', $editor));
}

Added src/www/admin/config/custom.php version [650b04f08c].





































































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

require_once __DIR__ . '/_inc.php';

$config = Config::getInstance();

$form->runIf('save', function () use ($config) {
	$config->importForm();

	if (f('admin_background') == 'RESET') {
		$config->setFile('admin_background', null);
	}
	elseif (f('admin_background')) {
		$config->setFile('admin_background', base64_decode(f('admin_background')));
	}

	$config->save();
}, 'config_custom', Utils::getSelfURI(['ok' => '']));

$tpl->assign([
	'color1'           => ADMIN_COLOR1,
	'color2'           => ADMIN_COLOR2,
]);

$tpl->assign('background_image_current', $config->fileURL('admin_background'));
$tpl->assign('background_image_default', ADMIN_BACKGROUND_IMAGE);

$tpl->assign('custom_js', ['color_helper.js']);
$tpl->display('admin/config/custom.tpl');

Modified src/www/admin/config/edit_file.php from [8a746033ac] to [15de1c6be2].

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->editorType();
$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));











|
<
|
|
<
|
<
|
<
<
<
|
<
|
|
|
<
|

|
|
|
>

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







|

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

$config = Config::getInstance();


if (!isset(Config::FILES[$key])) {

	throw new UserException('Fichier invalide');

}





$file = $config->file($key);

$type = Config::FILES_TYPES[$key];

$csrf_key = 'edit_file_' . $key;

$form->runIf('upload', function () use ($key, $config) {
	$config->setFile($key, 'file', true);
	$config->save();
}, $csrf_key, Utils::getSelfURI());



$form->runIf('reset', function () use ($key, $config) {
	$config->setFile($key, null);
	$config->save();
}, $csrf_key, Utils::getSelfURI());

$form->runIf('save', function () use ($key, $config) {
	$content = trim(f('content'));
	$config->setFile($key, $content === '' ? null : $content);
	$config->save();


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

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

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

if ($type == 'image') {
	$tpl->display('admin/config/edit_image.tpl');
}
else {
	$content = $file ? $file->fetch() : '';
	$path = Config::FILES[$key];
	$format = $file ? $file->renderFormat() : 'skriv';
	$tpl->assign(compact('content', 'path', 'format'));
	$tpl->display(sprintf('common/files/edit_%s.tpl', $type));
}

Modified src/www/admin/config/index.php from [00108c3e5f] to [a6385e7542].

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
	'php_version'      => phpversion(),
	'has_gpg_support'  => \KD2\Security::canUseEncryption(),
	'server_time'      => time(),
	'sqlite_version'   => \SQLite3::version()['versionString'],
	'countries'        => Utils::getCountryList(),
	'membres_cats'     => Categories::listSimple(),
	'champs'           => $config->get('champs_membres')->listAssocNames(),
	'color1'           => ADMIN_COLOR1,
	'color2'           => ADMIN_COLOR2,
	'garradin_website' => WEBSITE,
]);

$homepage = $config->admin_homepage;

if ($homepage && !Files::get($homepage)) {
	File::createAndStore(Utils::dirname($homepage), Utils::basename($homepage), null, '');
}

$admin_background = $config->get('admin_background');

$tpl->assign('background_image_current', $admin_background ? WWW_URL . $admin_background : null);
$tpl->assign('background_image_default', ADMIN_BACKGROUND_IMAGE);

$tpl->assign('custom_js', ['color_helper.js']);
$tpl->display('admin/config/index.tpl');







<
<



<
<
<
<
<
<
<
<
<
<
<
<

26
27
28
29
30
31
32


33
34
35












36
	'php_version'      => phpversion(),
	'has_gpg_support'  => \KD2\Security::canUseEncryption(),
	'server_time'      => time(),
	'sqlite_version'   => \SQLite3::version()['versionString'],
	'countries'        => Utils::getCountryList(),
	'membres_cats'     => Categories::listSimple(),
	'champs'           => $config->get('champs_membres')->listAssocNames(),


	'garradin_website' => WEBSITE,
]);













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

Modified src/www/admin/docs/new_dir.php from [d2c27619fc] to [d8847a425e].

14
15
16
17
18
19
20
21








22
23
24
25
26
}

$csrf_key = 'create_dir';

$form->runIf('create', function () use ($parent) {
	$name = trim(f('name'));
	File::validatePath($parent . '/' . $name);
	File::createDirectory($parent, $name);








}, $csrf_key, '!docs/?path=' . $parent);

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

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







|
>
>
>
>
>
>
>
>
|




14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
}

$csrf_key = 'create_dir';

$form->runIf('create', function () use ($parent) {
	$name = trim(f('name'));
	File::validatePath($parent . '/' . $name);
	$f = File::createDirectory($parent, $name);

	$url = '!docs/?path=' . $f->path;

	if (null !== qg('_dialog')) {
		Utils::reloadParentFrame($url);
	}

	Utils::redirect($url);
}, $csrf_key);

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

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

Modified src/www/admin/docs/new_file.php from [b548528de7] to [4505ff7d98].

22
23
24
25
26
27
28


29
30
31
32
33
		$name .= '.skriv';
	}

	File::validatePath($parent . '/' . $name);
	$name = File::filterName($name);

	$file = File::createAndStore($parent, $name, null, '');


}, $csrf_key, '!docs/?path=' . $parent);

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

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







>
>
|




22
23
24
25
26
27
28
29
30
31
32
33
34
35
		$name .= '.skriv';
	}

	File::validatePath($parent . '/' . $name);
	$name = File::filterName($name);

	$file = File::createAndStore($parent, $name, null, '');

	Utils::redirect('!common/files/edit.php?p=' . rawurlencode($file->path));
}, $csrf_key);

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

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

Modified src/www/admin/index.php from [025ac1c529] to [bb797c6a32].

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


16
17
18
19
20
21
22
23
24
<?php

namespace Garradin;

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

require_once __DIR__ . '/_inc.php';

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

$banner = null;
Plugin::fireSignal('accueil.banniere', ['user' => $user, 'session' => $session], $banner);



if ($homepage && ($file = Files::get($homepage))) {
	$homepage = $file->render(ADMIN_URL . 'common/files/preview.php?p=' . File::CONTEXT_DOCUMENTS . '/');
}
else {
	$homepage = null;
}

$tpl->assign(compact('homepage', 'banner'));











<
<



>
>
|
|







1
2
3
4
5
6
7
8
9
10


11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace Garradin;

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

require_once __DIR__ . '/_inc.php';



$banner = null;
Plugin::fireSignal('accueil.banniere', ['user' => $user, 'session' => $session], $banner);

$homepage = Config::getInstance()->file('admin_homepage');

if ($homepage) {
	$homepage = $homepage->render(ADMIN_URL . 'common/files/preview.php?p=' . File::CONTEXT_DOCUMENTS . '/');
}
else {
	$homepage = null;
}

$tpl->assign(compact('homepage', 'banner'));

Modified src/www/admin/manifest.php from [4bf7b7e222] to [97a146fba4].

1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28




29
30
<?php
namespace Garradin;

const LOGIN_PROCESS = true;
require_once __DIR__ . '/_inc.php';

$manifest = [
	'background_color' => 'white',

	'description'      => 'Gestion de l\'association',
	'display'          => 'fullscreen',
	'name'             => $config->nom_asso,
	'start_url'        => ADMIN_URL,
	'icons'            => [
		[
			'sizes' => '16x16',
			'src'   => WWW_URL . 'favicon.png',
			'type'  => 'image/png',
			'purpose' => 'any maskable',
		],
		[
			'sizes' => '256x256',
			'src'   => WWW_URL . 'logo.png',
			'type'  => 'image/png',
			'purpose' => 'any maskable',
		],
	],
];





header('Content-Type: text/json; charset=utf-8');
echo json_encode($manifest, JSON_PRETTY_PRINT);







|
>






|
|





|






>
>
>
>

|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
namespace Garradin;

const LOGIN_PROCESS = true;
require_once __DIR__ . '/_inc.php';

$manifest = [
	'background_color' => $config->couleur2,
	'theme_color'      => $config->couleur1,
	'description'      => 'Gestion de l\'association',
	'display'          => 'fullscreen',
	'name'             => $config->nom_asso,
	'start_url'        => ADMIN_URL,
	'icons'            => [
		[
			'sizes' => '32x32',
			'src'   => $config->fileURL('favicon'),
			'type'  => 'image/png',
			'purpose' => 'any maskable',
		],
		[
			'sizes' => '256x256',
			'src'   => $config->fileURL('icon', 'crop-256px'),
			'type'  => 'image/png',
			'purpose' => 'any maskable',
		],
	],
];

$body = json_encode($manifest, JSON_PRETTY_PRINT);

Utils::HTTPCache(md5($body), max($config->files['icon'], $config->files['favicon'], strtotime('2011-11-11')));

header('Content-Type: text/json; charset=utf-8');
echo $body;

Name change from src/www/skel-dist/favicon.png to src/www/admin/static/favicon.png.

cannot compute difference between binary files

Modified src/www/admin/static/handheld.css from [460c6b5a67] to [bb9608a6af].

45
46
47
48
49
50
51




52
53
54
55
56
57
58
	z-index: 10000;
	margin: 0;
	margin-bottom: 3em;
	width: 100%;
	padding: 0;
	display: block;
}





.header .menu *, .header .menu a {
	margin: 0;
	padding: 0;
}

.header .menu > ul {







>
>
>
>







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	z-index: 10000;
	margin: 0;
	margin-bottom: 3em;
	width: 100%;
	padding: 0;
	display: block;
}

.header .menu .logo {
	display: none;
}

.header .menu *, .header .menu a {
	margin: 0;
	padding: 0;
}

.header .menu > ul {

Modified src/www/admin/static/icon.png from [5f4be49914] to [1387768a8b].

cannot compute difference between binary files

Modified src/www/admin/static/print.css from [6937e4b226] to [cf1afc3c77].

1
2
3
4
5
6
7
8
9
10
@page {
    size: A4 landscape;
    margin: 0;
}

html {
    height: auto;
}

body {


|







1
2
3
4
5
6
7
8
9
10
@page {
    size: A4 landscape;
    margin: 1cm;
}

html {
    height: auto;
}

body {
36
37
38
39
40
41
42




43
44
45
46
47
48
49
    color: #000 !important;
    border-right: 1px solid #999;
}

table.list tfoot {
    border-top: double .3rem #000;
}





table.list tfoot tr td, table.list tfoot th {
    background: #fff;
    color: #000;
}

table.list tr {







>
>
>
>







36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    color: #000 !important;
    border-right: 1px solid #999;
}

table.list tfoot {
    border-top: double .3rem #000;
}

table.statement tfoot tr {
    color: #000;
}

table.list tfoot tr td, table.list tfoot th {
    background: #fff;
    color: #000;
}

table.list tr {

Modified src/www/admin/static/scripts/code_editor.js from [81fbe437c7] to [9902ec3c83].

85
86
87
88
89
90
91


92
93

				return false;
			};
		}
		else {
			appendButton('fullscreen', 'Plein écran', code.toggleFullscreen);
		}


	});
}());







>
>


85
86
87
88
89
90
91
92
93
94
95

				return false;
			};
		}
		else {
			appendButton('fullscreen', 'Plein écran', code.toggleFullscreen);
		}

		g.setParentDialogHeight('90%');
	});
}());

Modified src/www/admin/static/scripts/color_helper.js from [6151df33de] to [65ca233b3d].

22
23
24
25
26
27
28
29
30









31
32
33
34
35















36
37
38
39
40
41
42
		return '#' + color.split(/,/).map(function (el) {
			return ('0' + parseInt(el, 10).toString(16)).substr(-2);
		}).join('');
	}

	function changeColor(element, color)
	{
		var new_color = colorToRGB(color, element).join(',');










		// Mise à jour variable CSS
		document.documentElement.style.setProperty('--' + element, new_color);

		applyColors();
		return new_color;















	}

	function applyColors()
	{
		let input = $('#f_couleur2');
		let color = colorToRGB(input.value, 'gSecondColor');
		let color1 = $('#f_couleur1'), color2 = $('#f_couleur2');







|

>
>
>
>
>
>
>
>
>

|


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







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
		return '#' + color.split(/,/).map(function (el) {
			return ('0' + parseInt(el, 10).toString(16)).substr(-2);
		}).join('');
	}

	function changeColor(element, color)
	{
		let new_color = colorToRGB(color, element);

		let text_color = element == 'gMainColor' ? [255, 255, 255] : [0, 0, 0];
		let change = element == 'gMainColor' ? -5 : 5;

		while (!checkContrast(new_color, text_color)) {
			new_color[0] += change;
			new_color[1] += change;
			new_color[2] += change;
		}

		// Mise à jour variable CSS
		document.documentElement.style.setProperty('--' + element, new_color.join(','));

		applyColors();
		return new_color.join(',');
	}

	/**
	 * Return true if contrast is OK (W3C AA-level), false if not
	 * @see https://dev.to/alvaromontoro/building-your-own-color-contrast-checker-4j7o
	 */
	function checkContrast(color1, color2)
	{
		let l1 = 0.2126 * color1[0] + 0.7152 * color1[1] + 0.0722 * color1[2];
		let l2 = 0.2126 * color2[0] + 0.7152 * color2[1] + 0.0722 * color2[2];
		let ratio = l1 > l2
			? ((l2 + 0.05) / (l1 + 0.05))
			: ((l1 + 0.05) / (l2 + 0.05));

		return ratio < 1/3 ? true : false;
	}

	function applyColors()
	{
		let input = $('#f_couleur2');
		let color = colorToRGB(input.value, 'gSecondColor');
		let color1 = $('#f_couleur1'), color2 = $('#f_couleur2');

Modified src/www/admin/static/scripts/datepicker2.js from [6f612d2aa2] to [317a9279e3].

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
			this.button = button;
			this.input = input;
			this.date = null;

			Object.assign(this, {
				format: 0, // 0 = Y-m-d, 1 = d/m/Y
				lang: 'fr',
				class: 'datepicker',
				onchange: null
			}, config);

			var c = document.createElement('dialog');
			c.className = this.class;
			this.container = button.parentNode.insertBefore(c, button.nextSibling);

			button.onclick = () => { this.container.hasAttribute('open') ? this.close() : this.open() };







|
<







14
15
16
17
18
19
20
21

22
23
24
25
26
27
28
			this.button = button;
			this.input = input;
			this.date = null;

			Object.assign(this, {
				format: 0, // 0 = Y-m-d, 1 = d/m/Y
				lang: 'fr',
				class: 'datepicker'

			}, config);

			var c = document.createElement('dialog');
			c.className = this.class;
			this.container = button.parentNode.insertBefore(c, button.nextSibling);

			button.onclick = () => { this.container.hasAttribute('open') ? this.close() : this.open() };
183
184
185
186
187
188
189

190
191
192

193
194
195
196
197
198
199

			if (this.input) {
				this.input.value = v;
			}

			this.close();


			if (this.onchange) {
				this.onchange(v, this);
			}

		}

		focus()
		{
			this.container.querySelectorAll('tbody td').forEach((cell) => {
				var v = parseInt(cell.innerHTML, 10);








>
|
|
<
>







182
183
184
185
186
187
188
189
190
191

192
193
194
195
196
197
198
199

			if (this.input) {
				this.input.value = v;
			}

			this.close();

			event = document.createEvent('HTMLEvents');
			event.initEvent('change', true, true);
			event.eventName = 'change';

			this.input.dispatchEvent(event);
		}

		focus()
		{
			this.container.querySelectorAll('tbody td').forEach((cell) => {
				var v = parseInt(cell.innerHTML, 10);

Modified src/www/admin/static/scripts/global.js from [8f3eacf771] to [b111eaadcd].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function () {
	let s = document.head.querySelector('script');

	window.g = window.garradin = {
		url: window.location.href.replace(/\/admin\/.*?$/, ''),
		admin_url: window.location.href.replace(/\/admin\/.*?$/, '/admin/'),
		static_url: window.location.href.replace(/\/admin\/.*?$/, '/admin/static/'),
		version: s ? s.src.match(/\?(.*)$/)[1] : null,
		loaded: {}
	};

	window.$ = function(selector) {
		if (!selector.match(/^[.#]?[a-z0-9_-]+$/i))
		{
			return document.querySelectorAll(selector);

<
<




|







1


2
3
4
5
6
7
8
9
10
11
12
13
(function () {


	window.g = window.garradin = {
		url: window.location.href.replace(/\/admin\/.*?$/, ''),
		admin_url: window.location.href.replace(/\/admin\/.*?$/, '/admin/'),
		static_url: window.location.href.replace(/\/admin\/.*?$/, '/admin/static/'),
		version: document.documentElement.getAttribute('data-version'),
		loaded: {}
	};

	window.$ = function(selector) {
		if (!selector.match(/^[.#]?[a-z0-9_-]+$/i))
		{
			return document.querySelectorAll(selector);
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
		var iframe = document.createElement('iframe');
		iframe.src = url;
		iframe.name = 'dialog';
		iframe.id = 'frameDialog';
		iframe.frameborder = '0';
		iframe.scrolling = 'yes';
		iframe.width = iframe.height = 0;

		iframe.addEventListener('load', () => {
			iframe.contentWindow.onkeyup = (e) => { if (e.key == 'Escape') g.closeDialog(); };
			// We need to wait a bit for the height to be correct, not sure why
			window.setTimeout(() => {
				iframe.style.height = height == 'auto' ? iframe.contentWindow.document.body.offsetHeight + 'px' : height;
			}, 100);
		});

		g.openDialog(iframe, callback);
		return iframe;
	};

	g.reloadParentDialog = () => {
		if (!window.parent.g.dialog) {
			return;
		}

		location.href = window.parent.g.dialog.querySelector('iframe').getAttribute('src');
	};










	g.resizeParentDialog = () => {
		if (!window.parent.g.dialog) {
			return;
		}







		let height = document.body.offsetHeight;
		let parent_height = window.parent.innerHeight;

		if (height > parent_height * 0.9) {
			height = '90%';
		}
		else {
			height += 'px';

		}

		window.parent.g.dialog.childNodes[1].style.height = height;
	};

	g.closeDialog = function () {
		if (null === g.dialog) {







>




|















>
>
>
>
>
>
>
>
>
|




>
>
>
>
>
>
|
|

|
|
|
|
|
>







158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
		var iframe = document.createElement('iframe');
		iframe.src = url;
		iframe.name = 'dialog';
		iframe.id = 'frameDialog';
		iframe.frameborder = '0';
		iframe.scrolling = 'yes';
		iframe.width = iframe.height = 0;
		iframe.setAttribute('data-height', height);
		iframe.addEventListener('load', () => {
			iframe.contentWindow.onkeyup = (e) => { if (e.key == 'Escape') g.closeDialog(); };
			// We need to wait a bit for the height to be correct, not sure why
			window.setTimeout(() => {
				iframe.style.height = iframe.dataset.height == 'auto' ? iframe.contentWindow.document.body.offsetHeight + 'px' : iframe.dataset.height;
			}, 100);
		});

		g.openDialog(iframe, callback);
		return iframe;
	};

	g.reloadParentDialog = () => {
		if (!window.parent.g.dialog) {
			return;
		}

		location.href = window.parent.g.dialog.querySelector('iframe').getAttribute('src');
	};

	g.setParentDialogHeight = (height) => {
		if (!window.parent.g.dialog) {
			return;
		}

		window.parent.g.dialog.querySelector('iframe').setAttribute('data-height', height);
		g.resizeParentDialog(height);
	};

	g.resizeParentDialog = (forced_height) => {
		if (!window.parent.g.dialog) {
			return;
		}

		let height;

		if (forced_height) {
			height = forced_height;
		}
		else {
			let body_height = document.body.offsetHeight;
			let parent_height = window.parent.innerHeight;

			if (body_height > parent_height * 0.9) {
				height = '90%';
			}
			else {
				height = body_height + 'px';
			}
		}

		window.parent.g.dialog.childNodes[1].style.height = height;
	};

	g.closeDialog = function () {
		if (null === g.dialog) {

Modified src/www/admin/static/scripts/wiki_editor.js from [7a505bf40b] to [7df1c9bd9b].

248
249
250
251
252
253
254

255





256
257
258
259
260
261
262
263
			fetch(t.textarea.form.action + '&js', {
				method: 'post',
				body: data,
			}).then((response) => response.json())
			.then(data => {
				showSaved();
				t.textarea.defaultValue = t.textarea.value;

				t.textarea.form.querySelector('input[name=editing_started]').value = data.modified;





			}).catch(e => t.textarea.form.querySelector('[type=submit]').click() );
			return true;
		};

		let createToolbar = () => {
			appendButton('title', "Titre", applyHeader );
			appendButton('bold', 'Gras', applyBold );
			appendButton('italic', "Italique", applyItalic );







>
|
>
>
>
>
>
|







248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
			fetch(t.textarea.form.action + '&js', {
				method: 'post',
				body: data,
			}).then((response) => response.json())
			.then(data => {
				showSaved();
				t.textarea.defaultValue = t.textarea.value;

				let e = t.textarea.form.querySelector('input[name=editing_started]');

				if (e) {
					e.value = data.modified;
				}

			}).catch(e => { console.log(e); t.textarea.form.querySelector('[type=submit]').click(); } );
			return true;
		};

		let createToolbar = () => {
			appendButton('title', "Titre", applyHeader );
			appendButton('bold', 'Gras', applyBold );
			appendButton('italic', "Italique", applyItalic );
310
311
312
313
314
315
316


317
318
		t.shortcuts.push({ctrl: true, key: 'g', callback: applyBold });
		t.shortcuts.push({ctrl: true, key: 'i', callback: applyItalic });
		t.shortcuts.push({ctrl: true, key: 't', callback: applyHeader });
		t.shortcuts.push({ctrl: true, key: 'l', callback: insertURL});
		t.shortcuts.push({ctrl: true, key: 's', callback: save});
		t.shortcuts.push({ctrl: true, shift: true, key: 'p', callback: openPreview});
		t.shortcuts.push({key: 'F1', callback: openSyntaxHelp});


	});
}());







>
>


316
317
318
319
320
321
322
323
324
325
326
		t.shortcuts.push({ctrl: true, key: 'g', callback: applyBold });
		t.shortcuts.push({ctrl: true, key: 'i', callback: applyItalic });
		t.shortcuts.push({ctrl: true, key: 't', callback: applyHeader });
		t.shortcuts.push({ctrl: true, key: 'l', callback: insertURL});
		t.shortcuts.push({ctrl: true, key: 's', callback: save});
		t.shortcuts.push({ctrl: true, shift: true, key: 'p', callback: openPreview});
		t.shortcuts.push({key: 'F1', callback: openSyntaxHelp});

		g.setParentDialogHeight('90%');
	});
}());

Modified src/www/admin/static/styles/01-layout.css from [f34fa8a8e0] to [9046a79977].

35
36
37
38
39
40
41


















42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57


.header h1 {
    color: rgb(var(--gMainColor));
    margin-left: 180px;
    margin-bottom: 0.4em;
}



















.header .menu {
    position: fixed;
    overflow: auto;
    z-index: 1000;
    width: 170px;
    top: 0;
    bottom: 0;
    padding-top: 100px;
    background: rgb(var(--gMainColor)) var(--gBgImage) no-repeat 0px 0px;
}

.header .menu::-webkit-scrollbar {
    width: 8px;
    background: rgba(255, 255, 255, 0.25);
    box-shadow: inset 0px 0px 10px #666;







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








<







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


.header h1 {
    color: rgb(var(--gMainColor));
    margin-left: 180px;
    margin-bottom: 0.4em;
}

.header .menu .logo {
    padding: 0;
    min-height: 100px;
}

.header .menu .logo img {
    transition: opacity .2s;
}

.header .menu .logo a {
    padding: 10px;
    padding-bottom: 0;
    display: inline-block;
    text-align: center;
    color: inherit;
    width: 150px;
}

.header .menu {
    position: fixed;
    overflow: auto;
    z-index: 1000;
    width: 170px;
    top: 0;
    bottom: 0;

    background: rgb(var(--gMainColor)) var(--gBgImage) no-repeat 0px 0px;
}

.header .menu::-webkit-scrollbar {
    width: 8px;
    background: rgba(255, 255, 255, 0.25);
    box-shadow: inset 0px 0px 10px #666;

Modified src/www/admin/static/styles/02-common.css from [b08c6421a8] to [72a91c6a02].

360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393

394
395
396
397
398


399
400
401
402


403
404
405

406
407
408
409
410
411
412
    float: right;
    padding: .5em;
    border: .5em solid #000;
    background: #fff;
}

details {
    margin-left: 4em;
    margin-bottom: 1em;
}

details summary {
    list-style: none;
    padding: 0.2em 0.5em;
    transition: background-color .2s;
    position: relative;
    padding-left: 4em;
    margin-left: -4em;
}

details summary:hover {
    cursor: pointer;
    background-color: rgba(var(--gMainColor), 0.1);
}

details summary::-webkit-details-marker {
    display: none;
}

details summary::after {
    content: "↓";
    position: absolute;
    left: 0;
    top: 0;

    /* From .icn-btn */
    display: inline-block;
    color: rgb(var(--gMainColor));
    border: 1px solid rgba(var(--gSecondColor), 0.5);
    background: #fff;


    font-size: 1.5em;
    border-radius: .2em;
    padding: .2em .4em;
    margin: .2em .5em;


    transition: color .3s, background-color .3s;
    font-family: "gicon", sans-serif;
    text-shadow: 1px 1px 1px #999;

}

details[open] summary::after {
    content: "↑";
}

details summary:hover::after {







<









<
















>

|
|
|
|
>
>
|


|
>
>



>







360
361
362
363
364
365
366

367
368
369
370
371
372
373
374
375

376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
    float: right;
    padding: .5em;
    border: .5em solid #000;
    background: #fff;
}

details {

    margin-bottom: 1em;
}

details summary {
    list-style: none;
    padding: 0.2em 0.5em;
    transition: background-color .2s;
    position: relative;
    padding-left: 4em;

}

details summary:hover {
    cursor: pointer;
    background-color: rgba(var(--gMainColor), 0.1);
}

details summary::-webkit-details-marker {
    display: none;
}

details summary::after {
    content: "↓";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    /* From .icn-btn */
    cursor: pointer;
    color: #003;
    border: 1px solid rgba(var(--gMainColor), 0.5);
    background-color: rgba(var(--gSecondColor), 0.1);
    user-select: none;
    display: inline-block;
    font-size: inherit;
    border-radius: .2em;
    padding: .2em .4em;
    margin: auto .5em;
    height: 1em;
    white-space: pre;
    transition: color .3s, background-color .3s;
    font-family: "gicon", sans-serif;
    text-shadow: 1px 1px 1px #999;
    font-size: 1.2em;
}

details[open] summary::after {
    content: "↑";
}

details summary:hover::after {

Modified src/www/admin/static/styles/03-forms.css from [9d02310ab4] to [48a1aece0a].

449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
    border-radius: .5em;
}

.datepicker input {
    font-family: gicon;
}

[data-icon]:before, summary::after, .main[data-icon]:after {
    display: inline-block;
    font-family: "gicon", sans-serif;
    text-shadow: 1px 1px 1px #ccc;
    padding-right: .5em;
    font-size: 1.2em;
    line-height: .8em;
    vertical-align: middle;







|







449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
    border-radius: .5em;
}

.datepicker input {
    font-family: gicon;
}

[data-icon]:before, .main[data-icon]:after {
    display: inline-block;
    font-family: "gicon", sans-serif;
    text-shadow: 1px 1px 1px #ccc;
    padding-right: .5em;
    font-size: 1.2em;
    line-height: .8em;
    vertical-align: middle;

Modified src/www/admin/static/styles/10-accounting.css from [d4efb6cfa3] to [1b7cf590bb].

75
76
77
78
79
80
81





82
83
84
85
86
87
88
    padding-bottom: .5em;
    border-bottom: 1pt solid #999;
}

.year-header .print-btn button {
    font-size: 1.3rem;
}






.year-infos {
    text-align: center;
}

.year-infos .graphs {
    display: flex;







>
>
>
>
>







75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
    padding-bottom: .5em;
    border-bottom: 1pt solid #999;
}

.year-header .print-btn button {
    font-size: 1.3rem;
}

.year-header form {
    max-width: 30em;
    margin: 1em auto;
}

.year-infos {
    text-align: center;
}

.year-infos .graphs {
    display: flex;

Modified src/www/admin/static/styles/config.css from [bc72f7c8a4] to [33fa4a39f3].

97
98
99
100
101
102
103







































    cursor: pointer;
}

#orderFields fieldset .interactive:hover {
    cursor: pointer;
    text-decoration: underline;
}














































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
    cursor: pointer;
}

#orderFields fieldset .interactive:hover {
    cursor: pointer;
    text-decoration: underline;
}

.image-preview img, .image-preview figure {
    vertical-align: middle;
}

.image-preview img {
    max-width: 150px;
}

.masked-icon {
    background: linear-gradient(rgba(var(--gMainColor), 0.9), rgba(0, 0, 0, 0.5));
    border-radius: .5em;
    padding: 10px;
    display: inline-flex;
    flex-direction: column;
    text-align: center;
    margin-left: 2em;
}

.masked-icon .icon {
    filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.5));
    display: inline-block;
}

.masked-icon figcaption {
    font-size: .9em;
    font-family: "Droid Sans", sans-serif;
    text-shadow: 0px 0px 5px #000;
    color: #fff;
}

.masked-icon img {
    clip-path: circle(40px at center);
    background: #fff;
    width: 100px;
    height: 100px;
    box-shadow: 1px 10px 5px #000;
    display: block;
}

Modified src/www/skel-dist/_head.html from [a4ef48e61c] to [f9b2a89dba].

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

<header class="nav">
	<nav>
		<ul>
			<li class="current"><a href="{{$root_url}}">Accueil</a></li>
			<li><a href="{{$admin_url}}">Administration</a></li>
		</ul>
	</nav>
</header>

<header class="main">
	<h1><a href="{{$root_url}}">{{$config.nom_asso}}</a></h1>

{{if $config.adresse_asso || $config.telephone_asso || $config.email_asso}}
	<article class="contacts">
		{{if $config.adresse_asso}}
			<h4>{{$config.adresse_asso|escape}}</h4>
		{{/if}}
		{{if $config.telephone_asso}}





<




>














|







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
<!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" />
	<link rel="icon" type="image/png" href="{{$config.files.favicon}}" />
</head>

<body>

<header class="nav">
	<nav>
		<ul>
			<li class="current"><a href="{{$root_url}}">Accueil</a></li>
			<li><a href="{{$admin_url}}">Administration</a></li>
		</ul>
	</nav>
</header>

<header class="main">
	<h1><a href="{{$root_url}}">{{if $config.files.logo}}<img src="{{$config.files.logo}}&150px" alt="" class="logo" />{{/if}} <span>{{$config.nom_asso}}</span></a></h1>

{{if $config.adresse_asso || $config.telephone_asso || $config.email_asso}}
	<article class="contacts">
		{{if $config.adresse_asso}}
			<h4>{{$config.adresse_asso|escape}}</h4>
		{{/if}}
		{{if $config.telephone_asso}}

Modified src/www/skel-dist/content.css from [520eaea40b] to [36b56b4140].

90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
}

.web-content .toc {
    margin: 1rem 0;
    border: 1px solid rgba(0, 0, 0, 0.2);
    background: rgba(0, 0, 0, 0.1);
    padding: .3rem;
    width: max-content;
}

.web-content .toc ol {
    list-style: none;
    counter-reset: item;
    margin: .5rem 0 .5rem .5rem;
}







|







90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
}

.web-content .toc {
    margin: 1rem 0;
    border: 1px solid rgba(0, 0, 0, 0.2);
    background: rgba(0, 0, 0, 0.1);
    padding: .3rem;
    display: inline-block;
}

.web-content .toc ol {
    list-style: none;
    counter-reset: item;
    margin: .5rem 0 .5rem .5rem;
}

Modified src/www/skel-dist/default.css from [2d1b805a22] to [aa7580d3cb].

69
70
71
72
73
74
75




76
77
78




79
80
81
82
83
84
85
    padding: .2em 0 .1em 0;
    font-size: 4em;
    font-family: Georgia, "Times New Roman", Times, serif;
    font-weight: normal;
}

header.main h1 a {




    color: #9c4f15;
    text-decoration: none;
}





header.main {
    margin-bottom: 1em;
    background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXMAAADPAQMAAAA9C6NrAAAABlBMVEXx9PD+//zYDo7WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gEIER05obn78wAACb1JREFUaN7V2TuOHLkZAGBSHKgmMEyFCgRRR1CoYD2lo+gI42wAyyoKCjbUEXZvshQUONwb2BwoULhcOHDJLhT9v/io6u7ZrXAFaKa76xsWi4+fP9kq13+K/+m84o8POUcN/+XTkU1UzculIS/o3+ecwC/i3alfypWEHmzCi54/tadeLk14I4N+htdYSqC7nvrE1cG/M6tCP/6c848WvTnjMxePv8gvavxcPtY7H7EJEhW/4l3Yu5j5/d7Ts8K9lby2K9xoVTZOWC2+2PvYtRk+hit+lMfCenaemsa0vx1XepAhQjuGrsPEr6q7J16fVgTgh9Lvrvdz/xneK5P3JppS1sbXoSBeV6+Lt73HKrwsnp6EfNB43XdliccPbqQMan746XCARh4etTHY0y1d4DZYqLLkk4rw2Xziqbfc0p59ZD/DUHN4d7iP7vzcjVh6k4t/jR8D8kp1PrYHyjz6sW9lZHyRB5yaD13v4h8P85TR06OXIdd5r65q79JI9ewz+bE8U/VQeB1RfDPxOHNwCKXSwVEGMDTAxq9T9WHgz2z1cDe9wHtqE+7OdfKW66aD5v5sPknlZ/I0m9axecX9OVQfpfKJnon8wh5LCnCtznjyQZ4eO5/LMosLVvoOu3guMziWkATyZyypeEt+IW9ljBcv0816iU9QV/H4x6/VEKZZHjEqKRHu7VSJDsrOhjzPRRPqjI+KBTx8fK5UHXuzjrbFgbjzz2nyPWvezSraOhJ1rBEiUpjkWz+jR0riX9gaUlWsMx59oq5Y1CtqYhLjLP05n/ER3ccEE96UGlTPa8LrGiHY67zYCH4ofkrd5C9+EB/AJwjyL+gTarvM/j91eem9h1ceOusFFdn7VILnpv54CZpUahhoqIgfpYE27U/R+3kpwdNQFD+UyNqNT3ikvyzQ+NIjnmqKfp1SjZ7d/IUm+zarp7237MdZhn4L9uT1fVJPSg3rEmwXN/OrbXyelfmAnSVF8H3IW5zUS188+8cwAF+c96asDW29TuraBPK6eBo3boZFVW+XX/QY/3xtsZUvkjeZb/m6xqbiW4fzakEeWtOXPKblMNhgQ2uClX+Tx2vunH/cBgj6MWCrOLq3PfFQ96vWg+ihIuynVFeFc36qycUCfgS/1FWh+TYAc/Nv2a+1Ic94XSegmW/wKeCGXu39uh2w4p+Lj7XlT32b4CZZ8XOdVcUvm/km4TPB+jR5SoFkDp3xU+e1R49RT+ZozSfn7nFjCbe0glC4DiVmnHgc6r2HWkxzrjFs3NfHQTEllaieWzn286tMuEyds0iqorBjpoUrkjb5SZDWT9VDU8Jrpf6+cDOkfr7LIoLVMeIn8XcrV3xW+/xBUdwa8i/V4+S6y+wXtckHsEo/4Yc2f+RE17O/nch9Xbbre82T3Dos1BOeMmjwHt2XvF3fWx6zDLz4iY8j9ew9VVjvPA6SmbzF15BxqzBiMIBurw3UeWrSgbNDTzGbvclxWvt4XjN6m+PALc5ee4gR6Mc6Ipqf8QM/cI96ytA1rqtQb8zIfFvvyuNqTJhpL4VXqQ6O3kZXcoDOc2aKfug8/kBfEqDmObQaegxqK4507Fe19yu+XclTbbX4gN7WLU/1Cye16POXr0EZ8dBAU7S8DPd+VjS7wet1+Fw87SUnWCYlKW0+cdJvqCHeB3pDjw71Ax8lvlYfuPXIWx8oGyUf0P+S9p5yvuIhwXDFJ/T/KDOgeqqf+OeyY+Aqov+8ygzYeHg4gxe+a9tOrCcsfJ/zztMyt75l/4rbrnnzKcseuN9vmuWO/a0q66jBhgEfqIE6P1OSKuW/4b6vfjGeElbTPG0I5lfsA3uc9F+LX0/9mNhrz2OXfCIPzRU4Inf73wm8Ri/BA/1nSmwe0eZw52FePKXyH7VMy3yYeebTrk01T1UWfyVzFRf+9zNPAiebkG5/DWP9mp9XYgF6vbC3uRxNVA9RoXmp42NcDsmvew+N5a+K11vvpfyxeu6gK4qyLVaqP6mJ/bD3dcC0pTiR9zSJB9nktPMBHsCmLB4fZVWZ0ML/f9GE6rzp/Zht2QlgChTV8DH2fuHRr6Se8NrKKjRiCgT++7T3U/OY3xc/U3Y88ETdlD9XD11tv4lf2eu19xSGm8f9wDdZWrPCJP0x54UnniOZg91yKj5g0HpMa33vTechsNjEi4r4K5oB1WN0+1+qHrdkxUcJdXSc0nn7a/W4HcGTouqzHHls/H0qSx3eFr2vnob73DzO6lA9Lic2up3npUm1Mwea87TU4QCWRegmcdA98dnH4mnLFGltUe8Shyla7zsP6csbbj4eeeixgLe3HKTV8AMtZSU/gYn1hmZc4D3LGGlTJR77agi9t83TCdEke7U78lCAwV1z87C2vKR5HmhHDN7QgLjjXSuesLzqfSavOeHEksTfvpTY+AgmJyyV7D3e/Al7KztcnnDxZduF7jxH4DDR4+eZj1niE2XrPr55U/195h3xzGlHYC9Z7Vg87oY47nzNvCNeNNXCP1Xt4KJ6Jd6WaOR+WhRHefF+58fOw3PejCu3q7pW3UGE23lXZv/dzTtVTh+6g4VLXvubqSTrctOd/1tqh6hY7LuxnNxIIeGcn5p3fqqZeDszrP6ve2/9VAptGYOt55m/thyf2mI1YdoeG8eNv48UN2dXvHpdNgqupSRD9Z8i7zHKkctKlZt7P/c+cEIRZDuk2S/dVwU5/7d57XkP5PXGrxvfnd+yd7wfFT+2o7lTL9FByXmfWesaf9Ybme1Ui87Hdi668Y/owCKf+PSQ1+Jl9zKWhe2MLxvsvV/bue6pt7LOe84xRuzth7zL3J68guHqT4n2RT/tfTA5bk8UNh7bWxLPkY5aPC6aFz0EQBdNOfxGjwd/lz3EKJs4uFLcpd5etycivYeczq5jacOAqYCi0HfBTzz6Kbn5hofF5MMlr3Pzw6d/Fx8vefg8lTPMP+uUxM/DBW87/8Skl+KXSx739a68M/GaRje8tRc8/p6ap9mAbf/xvG+P1bzfnS713p7xwZz3eTNL+VsOasvhss9n/OwuerPzu3F/4t32a0IblH7QT8e83n0N6X7D250fozIP+fGY93nnp6iGB/z3e58f9j/svD49KT33/Wxum6VjHnM9d8Bb+S7p93r3z9sDHkLPs6e7g9KH/dvvrs8Ozwtex1fqiMcvX/Uhf3vERzxSMId8OD98HvD2kPdHfMLU2h304xH/6KDfZQ2/4WelLgyHy14f9OagHw74ZbN8/D7vDvrxgF8vNv9ln495fcTni81/0Q8HvTvox0PeX2rOiz4f8+aYD8NB7w768ZiP0zF/n4/5rwd9/uP4/wOHV4ghb+WqVQAAAABJRU5ErkJggg==") no-repeat top right;
}

/* LISTE DES CATÉGORIES EN DESSOUS DU NOM DE L'ASSOCIATION */







>
>
>
>



>
>
>
>







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
    padding: .2em 0 .1em 0;
    font-size: 4em;
    font-family: Georgia, "Times New Roman", Times, serif;
    font-weight: normal;
}

header.main h1 a {
    display: flex;
    flex-direction: row-reverse;
    align-items: center;
    justify-content: space-between;
    color: #9c4f15;
    text-decoration: none;
}

header.main h1 a span {
    margin-right: auto;
}

header.main {
    margin-bottom: 1em;
    background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXMAAADPAQMAAAA9C6NrAAAABlBMVEXx9PD+//zYDo7WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gEIER05obn78wAACb1JREFUaN7V2TuOHLkZAGBSHKgmMEyFCgRRR1CoYD2lo+gI42wAyyoKCjbUEXZvshQUONwb2BwoULhcOHDJLhT9v/io6u7ZrXAFaKa76xsWi4+fP9kq13+K/+m84o8POUcN/+XTkU1UzculIS/o3+ecwC/i3alfypWEHmzCi54/tadeLk14I4N+htdYSqC7nvrE1cG/M6tCP/6c848WvTnjMxePv8gvavxcPtY7H7EJEhW/4l3Yu5j5/d7Ts8K9lby2K9xoVTZOWC2+2PvYtRk+hit+lMfCenaemsa0vx1XepAhQjuGrsPEr6q7J16fVgTgh9Lvrvdz/xneK5P3JppS1sbXoSBeV6+Lt73HKrwsnp6EfNB43XdliccPbqQMan746XCARh4etTHY0y1d4DZYqLLkk4rw2Xziqbfc0p59ZD/DUHN4d7iP7vzcjVh6k4t/jR8D8kp1PrYHyjz6sW9lZHyRB5yaD13v4h8P85TR06OXIdd5r65q79JI9ewz+bE8U/VQeB1RfDPxOHNwCKXSwVEGMDTAxq9T9WHgz2z1cDe9wHtqE+7OdfKW66aD5v5sPknlZ/I0m9axecX9OVQfpfKJnon8wh5LCnCtznjyQZ4eO5/LMosLVvoOu3guMziWkATyZyypeEt+IW9ljBcv0816iU9QV/H4x6/VEKZZHjEqKRHu7VSJDsrOhjzPRRPqjI+KBTx8fK5UHXuzjrbFgbjzz2nyPWvezSraOhJ1rBEiUpjkWz+jR0riX9gaUlWsMx59oq5Y1CtqYhLjLP05n/ER3ccEE96UGlTPa8LrGiHY67zYCH4ofkrd5C9+EB/AJwjyL+gTarvM/j91eem9h1ceOusFFdn7VILnpv54CZpUahhoqIgfpYE27U/R+3kpwdNQFD+UyNqNT3ikvyzQ+NIjnmqKfp1SjZ7d/IUm+zarp7237MdZhn4L9uT1fVJPSg3rEmwXN/OrbXyelfmAnSVF8H3IW5zUS188+8cwAF+c96asDW29TuraBPK6eBo3boZFVW+XX/QY/3xtsZUvkjeZb/m6xqbiW4fzakEeWtOXPKblMNhgQ2uClX+Tx2vunH/cBgj6MWCrOLq3PfFQ96vWg+ihIuynVFeFc36qycUCfgS/1FWh+TYAc/Nv2a+1Ic94XSegmW/wKeCGXu39uh2w4p+Lj7XlT32b4CZZ8XOdVcUvm/km4TPB+jR5SoFkDp3xU+e1R49RT+ZozSfn7nFjCbe0glC4DiVmnHgc6r2HWkxzrjFs3NfHQTEllaieWzn286tMuEyds0iqorBjpoUrkjb5SZDWT9VDU8Jrpf6+cDOkfr7LIoLVMeIn8XcrV3xW+/xBUdwa8i/V4+S6y+wXtckHsEo/4Yc2f+RE17O/nch9Xbbre82T3Dos1BOeMmjwHt2XvF3fWx6zDLz4iY8j9ew9VVjvPA6SmbzF15BxqzBiMIBurw3UeWrSgbNDTzGbvclxWvt4XjN6m+PALc5ee4gR6Mc6Ipqf8QM/cI96ytA1rqtQb8zIfFvvyuNqTJhpL4VXqQ6O3kZXcoDOc2aKfug8/kBfEqDmObQaegxqK4507Fe19yu+XclTbbX4gN7WLU/1Cye16POXr0EZ8dBAU7S8DPd+VjS7wet1+Fw87SUnWCYlKW0+cdJvqCHeB3pDjw71Ax8lvlYfuPXIWx8oGyUf0P+S9p5yvuIhwXDFJ/T/KDOgeqqf+OeyY+Aqov+8ygzYeHg4gxe+a9tOrCcsfJ/zztMyt75l/4rbrnnzKcseuN9vmuWO/a0q66jBhgEfqIE6P1OSKuW/4b6vfjGeElbTPG0I5lfsA3uc9F+LX0/9mNhrz2OXfCIPzRU4Inf73wm8Ri/BA/1nSmwe0eZw52FePKXyH7VMy3yYeebTrk01T1UWfyVzFRf+9zNPAiebkG5/DWP9mp9XYgF6vbC3uRxNVA9RoXmp42NcDsmvew+N5a+K11vvpfyxeu6gK4qyLVaqP6mJ/bD3dcC0pTiR9zSJB9nktPMBHsCmLB4fZVWZ0ML/f9GE6rzp/Zht2QlgChTV8DH2fuHRr6Se8NrKKjRiCgT++7T3U/OY3xc/U3Y88ETdlD9XD11tv4lf2eu19xSGm8f9wDdZWrPCJP0x54UnniOZg91yKj5g0HpMa33vTechsNjEi4r4K5oB1WN0+1+qHrdkxUcJdXSc0nn7a/W4HcGTouqzHHls/H0qSx3eFr2vnob73DzO6lA9Lic2up3npUm1Mwea87TU4QCWRegmcdA98dnH4mnLFGltUe8Shyla7zsP6csbbj4eeeixgLe3HKTV8AMtZSU/gYn1hmZc4D3LGGlTJR77agi9t83TCdEke7U78lCAwV1z87C2vKR5HmhHDN7QgLjjXSuesLzqfSavOeHEksTfvpTY+AgmJyyV7D3e/Al7KztcnnDxZduF7jxH4DDR4+eZj1niE2XrPr55U/195h3xzGlHYC9Z7Vg87oY47nzNvCNeNNXCP1Xt4KJ6Jd6WaOR+WhRHefF+58fOw3PejCu3q7pW3UGE23lXZv/dzTtVTh+6g4VLXvubqSTrctOd/1tqh6hY7LuxnNxIIeGcn5p3fqqZeDszrP6ve2/9VAptGYOt55m/thyf2mI1YdoeG8eNv48UN2dXvHpdNgqupSRD9Z8i7zHKkctKlZt7P/c+cEIRZDuk2S/dVwU5/7d57XkP5PXGrxvfnd+yd7wfFT+2o7lTL9FByXmfWesaf9Ybme1Ui87Hdi668Y/owCKf+PSQ1+Jl9zKWhe2MLxvsvV/bue6pt7LOe84xRuzth7zL3J68guHqT4n2RT/tfTA5bk8UNh7bWxLPkY5aPC6aFz0EQBdNOfxGjwd/lz3EKJs4uFLcpd5etycivYeczq5jacOAqYCi0HfBTzz6Kbn5hofF5MMlr3Pzw6d/Fx8vefg8lTPMP+uUxM/DBW87/8Skl+KXSx739a68M/GaRje8tRc8/p6ap9mAbf/xvG+P1bzfnS713p7xwZz3eTNL+VsOasvhss9n/OwuerPzu3F/4t32a0IblH7QT8e83n0N6X7D250fozIP+fGY93nnp6iGB/z3e58f9j/svD49KT33/Wxum6VjHnM9d8Bb+S7p93r3z9sDHkLPs6e7g9KH/dvvrs8Ozwtex1fqiMcvX/Uhf3vERzxSMId8OD98HvD2kPdHfMLU2h304xH/6KDfZQ2/4WelLgyHy14f9OagHw74ZbN8/D7vDvrxgF8vNv9ln495fcTni81/0Q8HvTvox0PeX2rOiz4f8+aYD8NB7w768ZiP0zF/n4/5rwd9/uP4/wOHV4ghb+WqVQAAAABJRU5ErkJggg==") no-repeat top right;
}

/* LISTE DES CATÉGORIES EN DESSOUS DU NOM DE L'ASSOCIATION */
152
153
154
155
156
157
158




159
160
161
162
163
164
165
}

.breadcrumbs ul li::before {
    content: "»";
    color: #ccc;
    margin: .5em;
}





.breadcrumbs a {
    color: #999;
}


/* MESSAGES ALERTE ET ERREUR (par exemple : page non trouvée) */







>
>
>
>







160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
}

.breadcrumbs ul li::before {
    content: "»";
    color: #ccc;
    margin: .5em;
}

.breadcrumbs ul li:nth-child(1)::before {
    content: "";
}

.breadcrumbs a {
    color: #999;
}


/* MESSAGES ALERTE ET ERREUR (par exemple : page non trouvée) */

Added tools/fossil-verify.sh version [abbb39b794].





















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/bin/bash

REPO="$1"

if [ ! -f "$1/manifest" ]
then
	echo "Missing manifest, maybe you didn't specify a repository path,"
	echo "or you didn't enable the manifest? (fossil settings manifest on)"
	echo "Usage: $0 FOSSIL_REPOSITORY_PATH"
	exit 1
fi

gpg --verify "$1/manifest" 2> /dev/null

if [ $? != 0 ]
then
	echo "Manifest signature failed to verify"
	exit 2
fi

TMPFILE=$(mktemp)

while IFS= read -r LINE
do
	if [ "${LINE:0:2}" != "F " ]
	then
		echo "$LINE" >> $TMPFILE
		continue
	fi

	# Split string by spaces
	PARTS=($LINE)

	FILE_ENCODED="${PARTS[1]}"
	FILE="${PARTS[1]//\\s/ }"
	HASH="${PARTS[2]}"

	if [ "${#HASH}" = 40 ]
	then
		NEW_HASH=$(sha1sum "$1/$FILE" | awk '{print $1}')
	else
		NEW_HASH=$(openssl dgst -sha3-256 -binary "$1/$FILE" | xxd -p -c 100)
	fi

	if [ "$HASH" != "$NEW_HASH" ]
	then
		echo "Local file has changed"
		echo "$FILE"
		echo "Manifest hash:   $HASH"
		echo "Local file hash: $NEW_HASH"
		exit 2
	fi

	PARTS[2]="$HASH"

	# join parts in a new string
	NEW_LINE="$(printf " %s" "${PARTS[@]}")"
	NEW_LINE="${NEW_LINE:1}"

	echo "$NEW_LINE" >> $TMPFILE
done < "$1/manifest"

gpg --verify $TMPFILE 2>/dev/null

if [ $? != 0 ]
then
	echo "Something has changed between manifest and check?!"
	diff "$1/manifest" $TMPFILE
	rm -f $TMPFILE
	exit 2
fi

rm -f $TMPFILE
exit 0