Overview
Comment:Migrate away from documents, hello user forms
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | templates
Files: files | file ages | folders
SHA3-256: 81d19a359b2b20671b2544228b27b622b1a14b7421ed115dfc57fde8e8843a9d
User & Date: bohwaz on 2022-07-28 23:43:58
Other Links: branch diff | manifest | tags
Context
2022-07-29
03:23
Last progress on user forms, now mostly working check-in: 64eda05d62 user: bohwaz tags: templates
2022-07-28
23:43
Migrate away from documents, hello user forms check-in: 81d19a359b user: bohwaz tags: templates
23:21
Adapt example forms to new Forms check-in: 26366e6591 user: bohwaz tags: templates
Changes

Modified src/include/data/1.2.0_schema.sql from [b4745bdb46] to [c64b561387].

437
438
439
440
441
442
443
444
445
446
447










CREATE TABLE IF NOT EXISTS user_forms
-- List of user forms
(
	id INTEGER NOT NULL PRIMARY KEY,
	name TEXT NOT NULL,
	label TEXT NOT NULL,
	description TEXT NULL,
	templates TEXT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS user_forms_name ON user_forms (name);

















|



>
>
>
>
>
>
>
>
>
>
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
CREATE TABLE IF NOT EXISTS user_forms
-- List of user forms
(
	id INTEGER NOT NULL PRIMARY KEY,
	name TEXT NOT NULL,
	label TEXT NOT NULL,
	description TEXT NULL,
	config TEXT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS user_forms_name ON user_forms (name);

CREATE TABLE IF NOT EXISTS user_forms_templates
-- List of forms special templates
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_form INTEGER NOT NULL REFERENCES user_forms (id) ON DELETE CASCADE,
    name TEXT NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS user_forms_templates ON user_forms_templates (id_form, name);

Modified src/include/lib/Garradin/Entities/Files/File.php from [2d66cc0a7f] to [06a4ee4a96].

88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111

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

	const CONTEXTS_NAMES = [
		self::CONTEXT_DOCUMENTS => 'Documents',
		self::CONTEXT_USER => 'Membre',
		self::CONTEXT_TRANSACTION => 'Écriture comptable',
		self::CONTEXT_CONFIG => 'Configuration',
		self::CONTEXT_WEB => 'Site web',
		self::CONTEXT_SKELETON => 'Squelettes',
		self::CONTEXT_FORM => 'Squelettes',
	];

	const IMAGE_TYPES = [
		'image/png',
		'image/gif',
		'image/jpeg',
		'image/webp',







<








<







88
89
90
91
92
93
94

95
96
97
98
99
100
101
102

103
104
105
106
107
108
109

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


	const CONTEXTS_NAMES = [
		self::CONTEXT_DOCUMENTS => 'Documents',
		self::CONTEXT_USER => 'Membre',
		self::CONTEXT_TRANSACTION => 'Écriture comptable',
		self::CONTEXT_CONFIG => 'Configuration',
		self::CONTEXT_WEB => 'Site web',
		self::CONTEXT_SKELETON => 'Squelettes',

	];

	const IMAGE_TYPES = [
		'image/png',
		'image/gif',
		'image/jpeg',
		'image/webp',
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
		$this->assert(trim($this->name) !== '', 'Le nom de fichier ne peut rester vide');
		$this->assert(strlen($this->path), 'Le chemin ne peut rester vide');
		$this->assert(strlen($this->parent) || '' === $this->parent, 'Le chemin ne peut rester vide');
	}

	public function context(): string
	{
		return strtok($this->path, '/');
	}

	public function accessContext(): string
	{
		return strtok($this->path, '/');
	}

	public function fullpath(): string
	{
		$path = Files::callStorage('getFullPath', $this);








<
<
<
<
<







150
151
152
153
154
155
156





157
158
159
160
161
162
163
		$this->assert(trim($this->name) !== '', 'Le nom de fichier ne peut rester vide');
		$this->assert(strlen($this->path), 'Le chemin ne peut rester vide');
		$this->assert(strlen($this->parent) || '' === $this->parent, 'Le chemin ne peut rester vide');
	}

	public function context(): string
	{





		return strtok($this->path, '/');
	}

	public function fullpath(): string
	{
		$path = Files::callStorage('getFullPath', $this);

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
869
870
871
872
873
874
875
876
877









878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940

941
942
943
944
945
946
947
948
949
950
951
952
953
	public function checkReadAccess(?Session $session): bool
	{
		// Web pages and config files are always public
		if ($this->isPublic()) {
			return true;
		}

		$context = $this->accessContext();
		$ref = strtok(substr($this->path, strpos($this->path, '/')), '/');

		if (null === $session || !$session->isLogged()) {
			return false;
		}

		// All config and form files can be accessed by all logged-in users
		if ($context == self::CONTEXT_CONFIG || $context == self::CONTEXT_FORM) {
			return true;
		}
		elseif ($context == self::CONTEXT_TRANSACTION && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)) {
			return true;
		}
		// The user can access his own profile files
		else if ($context == self::CONTEXT_USER && $ref == $session->getUser()->id) {
			return true;
		}
		// Only users able to manage users can see their profile files
		else if ($context == self::CONTEXT_USER && $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)) {
			return true;
		}
		// Only users with right to access documents can read documents
		else if ($context == self::CONTEXT_DOCUMENTS && $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_READ)) {
			return true;
		}

		return false;
	}










	public function checkWriteAccess(?Session $session): bool
	{
		if (null === $session) {
			return false;
		}

		switch ($this->accessContext()) {
			case self::CONTEXT_WEB:
				return $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE);
			case self::CONTEXT_DOCUMENTS:
				// Only managers can change files
				return $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_WRITE);
			case self::CONTEXT_CONFIG:
			case self::CONTEXT_FORM:
				return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN);
			case self::CONTEXT_TRANSACTION:
				return $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE);
			case self::CONTEXT_SKELETON:
				return $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN);
			case self::CONTEXT_USER:
				return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE);
		}

		return false;
	}

	public function checkDeleteAccess(?Session $session): bool
	{
		if (null === $session) {
			return false;
		}

		switch ($this->accessContext()) {
			case self::CONTEXT_WEB:
				return $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE);
			case self::CONTEXT_SKELETON:
				return $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN);
			case self::CONTEXT_DOCUMENTS:
				// Only admins can delete files
				return $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_ADMIN);
			case self::CONTEXT_CONFIG:
			case self::CONTEXT_FORM:
				return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN);
			case self::CONTEXT_TRANSACTION:
				return $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN);
			case self::CONTEXT_USER:
				return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE);
		}

		return false;
	}

	static public function checkCreateAccess(string $path, ?Session $session): bool
	{
		if (null === $session) {
			return false;
		}

		$context = strtok($path, '/');

		switch ($context) {
			case self::CONTEXT_SKELETON:

			case self::CONTEXT_WEB:
				return $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE);
			case self::CONTEXT_DOCUMENTS:
				return $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_WRITE);
			case self::CONTEXT_CONFIG:
			case self::CONTEXT_FORM:
				return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN);
			case self::CONTEXT_TRANSACTION:
				return $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE);
			case self::CONTEXT_USER:
				return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE);
		}








|






|
|




















>
>
>
>
>
>
>
>
>







|






<




|













|



|




<




















>





<







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
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893

894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920

921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946

947
948
949
950
951
952
953
	public function checkReadAccess(?Session $session): bool
	{
		// Web pages and config files are always public
		if ($this->isPublic()) {
			return true;
		}

		$context = $this->context();
		$ref = strtok(substr($this->path, strpos($this->path, '/')), '/');

		if (null === $session || !$session->isLogged()) {
			return false;
		}

		// All config files can be accessed by all logged-in users
		if ($context == self::CONTEXT_CONFIG) {
			return true;
		}
		elseif ($context == self::CONTEXT_TRANSACTION && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)) {
			return true;
		}
		// The user can access his own profile files
		else if ($context == self::CONTEXT_USER && $ref == $session->getUser()->id) {
			return true;
		}
		// Only users able to manage users can see their profile files
		else if ($context == self::CONTEXT_USER && $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)) {
			return true;
		}
		// Only users with right to access documents can read documents
		else if ($context == self::CONTEXT_DOCUMENTS && $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_READ)) {
			return true;
		}

		return false;
	}

	public function checkSkeletonWriteAccess(?Session $session): bool
	{
		if (strpos($this->path, self::CONTEXT_SKELETON . '/web') === 0) {
			return $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN);
		}

		return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN);
	}

	public function checkWriteAccess(?Session $session): bool
	{
		if (null === $session) {
			return false;
		}

		switch ($this->context()) {
			case self::CONTEXT_WEB:
				return $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE);
			case self::CONTEXT_DOCUMENTS:
				// Only managers can change files
				return $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_WRITE);
			case self::CONTEXT_CONFIG:

				return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN);
			case self::CONTEXT_TRANSACTION:
				return $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE);
			case self::CONTEXT_SKELETON:
				return $this->checkSkeletonWriteAccess($session);
			case self::CONTEXT_USER:
				return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE);
		}

		return false;
	}

	public function checkDeleteAccess(?Session $session): bool
	{
		if (null === $session) {
			return false;
		}

		switch ($this->context()) {
			case self::CONTEXT_WEB:
				return $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE);
			case self::CONTEXT_SKELETON:
				return $this->checkSkeletonWriteAccess($session);
			case self::CONTEXT_DOCUMENTS:
				// Only admins can delete files
				return $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_ADMIN);
			case self::CONTEXT_CONFIG:

				return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN);
			case self::CONTEXT_TRANSACTION:
				return $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN);
			case self::CONTEXT_USER:
				return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE);
		}

		return false;
	}

	static public function checkCreateAccess(string $path, ?Session $session): bool
	{
		if (null === $session) {
			return false;
		}

		$context = strtok($path, '/');

		switch ($context) {
			case self::CONTEXT_SKELETON:
				return $this->checkSkeletonWriteAccess($session);
			case self::CONTEXT_WEB:
				return $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE);
			case self::CONTEXT_DOCUMENTS:
				return $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_WRITE);
			case self::CONTEXT_CONFIG:

				return $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN);
			case self::CONTEXT_TRANSACTION:
				return $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE);
			case self::CONTEXT_USER:
				return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE);
		}

Added src/include/lib/Garradin/Entities/UserForm.php version [c2ad7e5999].















































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
<?php

namespace Garradin\Entities;

use \Garradin\Entity;

class UserForm extends Entity
{
	const ROOT = File::CONTEXT_SKELETON . '/forms';
	const CONFIG_FILE = 'form.json';

	const CONFIG_TEMPLATE = 'config.html';

	const SNIPPET_TRANSACTION = 'snippets/transaction_details.html';
	const SNIPPET_USER = 'snippets/user_details.html';
	const SNIPPET_HOME_ICON = 'snippets/home_icon.html';

	const SNIPPETS = [
		self::SNIPPET_HOME_ICON => 'Icône sur la page d\'accueil',
		self::SNIPPET_USER => 'En bas de la fiche d\'un membre',
		self::SNIPPET_TRANSACTION => 'En bas de la fiche d\'une écriture',
	];

	const TABLE = 'user_forms';

	protected ?int $id;

	/**
	 * Directory name
	 */
	protected string $name;

	protected string $label;
	protected ?string $description;

	/**
	 * List of snippets/special template files
	 */
	protected array $templates;

	protected \stdClass $config;

	public function selfCheck(): void
	{
		$this->assert(preg_match('/^[a-z]+(?:_[a-z0-9])*$/', $this->name), 'Nom unique de formulaire invalide');
		$this->assert(trim($this->label) !== '', 'Le libellé ne peut rester vide');
	}

	/**
	 * Fills information from form.json file
	 */
	public function updateFromJSON(): bool
	{
		if ($file = Files::get($this->path())) {
			$json = $file->fetch();
		}
		elseif (file_exists($this->distPath())) {
			$json = file_get_contents($this->distPath());
		}
		else {
			return false;
		}

		$this->label = $json->label;
		$this->description = $json->description;

		return true;
	}

	public function updateTemplates(): void
	{
		$check = self::SNIPPETS + [self::CONFIG_TEMPLATE];
		$templates = [];
		$db = DB::getInstance();

		$db->begin();
		$db->delete('user_forms_templates', 'id_form = ' . (int)$this->id());

		foreach ($check as $file => $label) {
			if (Files::exists($this->path($file)) || file_exists($this->distPath($file))) {
				$templates[] = $file;
				$db->insert('user_forms_templates', ['id_form' => $this->id(), 'name' => $file]);
			}
		}

		$db->commit();
	}

	public function path(string $file = null): string
	{
		return self::ROOT . '/' . $this->name . ($file ? '/' . $file : '');
	}

	public function distPath(string $file = null): string
	{
		return ROOT . '/skel-dist/' . $this->name . ($file ? '/' . $file : '');
	}

	public function dir(): ?File
	{
		return Files::get(self::ROOT . $this->name);
	}

	public function hasConfig(): bool
	{
		return DB::getInstance()->test('user_forms_templates', 'id_form = ? AND name = ?', $this->id(), self::CONFIG_TEMPLATE);
	}

	public function canDelete(): bool
	{
		return $this->dir() ? true : false;
	}

	public function delete(): bool
	{
		$dir = $this->dir();

		if ($dir) {
			$dir->delete();
		}

		DB::getInstance()->exec(sprintf('DROP TABLE IF EXISTS forms_%s', $this->name));

		return parent::delete();
	}

	public function url(string $file = '', array $params = null)
	{
		if (null !== $params) {
			$params = '?' . http_build_query($params);
		}

		return sprintf('%sform/%s/%s/%s%s', WWW_URL, $this->context, $this->id, $file, $params);
	}

	public function displayWeb(string $file)
	{
		try {
			$this->template($file)->displayWeb();
		}
		catch (Brindille_Exception $e) {
			printf('<div style="border: 5px solid orange; padding: 10px; background: yellow;"><h2>Erreur dans le code du document</h2><p>%s</p></div>', nl2br(htmlspecialchars($e->getMessage())));
		}
	}

	public function fetch(string $file)
	{
		try {
			return $this->template($file)->fetch();
		}
		catch (Brindille_Exception $e) {
			return sprintf('<div style="border: 5px solid orange; padding: 10px; background: yellow;"><h2>Erreur dans le code du document</h2><p>%s</p></div>', nl2br(htmlspecialchars($e->getMessage())));
		}
	}

	public function template(string $file)
	{
		if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $file)) {
			throw new \InvalidArgumentException('Invalid skeleton name');
		}

		$ut = new UserTemplate(self::ROOT . '/' . $this->name . '/' . $file);

		return $ut;
	}

}

Deleted src/include/lib/Garradin/UserTemplate/Document.php version [6d2eaac668].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
<?php

namespace Garradin\UserTemplate;

use Garradin\Membres\Session;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Files\Files;
use Garradin\Entities\Files\File;

use KD2\Brindille_Exception;

use const Garradin\{ROOT, WWW_URL};

class Document
{
	const CONFIG_FILE = 'config.json';

	const CONTEXT_TRANSACTION = 'transaction';
	const CONTEXT_USER = 'user';
	const CONTEXT_WEB = 'web';

	const CONTEXTS = [
		self::CONTEXT_WEB => 'Site web',
		self::CONTEXT_TRANSACTION => 'Écriture',
		self::CONTEXT_USER => 'Membre',
	];


	protected bool $dist;
	protected string $context;
	protected string $id;
	protected string $path;
	public $config;
	public string $name;

	static public function fromURI(string $uri)
	{
		return new self(strtok($uri, '/'), strtok(''));
	}

	static public function serve(string $uri): void
	{
		$session = Session::getInstance();

		if (!$session->isLogged()) {
			http_response_code(403);
			throw new UserException('Merci de vous connecter pour accéder à ce document.');
		}

		$path = substr($uri, 0, strrpos($uri, '/'));
		$file = substr($uri, strrpos($uri, '/') + 1) ?: 'index.html';

		$doc = self::fromURI($path);

		try {
			if (isset($_GET['pdf'])) {
				$doc->PDF($file);
			}
			else {
				$doc->display($file);
			}
		}
		catch (\InvalidArgumentException $e) {
			http_response_code(404);
			throw new UserException('Cette page de document n\'existe pas');
		}
	}

	public function __construct(string $context, string $id)
	{
		if (!array_key_exists($context, self::CONTEXTS)) {
			throw new \InvalidArgumentException('Invalid context');
		}

		if ($context != self::CONTEXT_TRANSACTION && $context != self::CONTEXT_USER) {
			throw new \InvalidArgumentException('Invalid context');
		}

		$path = $context . '/' . $id;

		if (Files::exists(File::CONTEXT_SKELETON . '/' . $path)) {
			$f = Files::get($path . '/' . self::CONFIG_FILE);

			if (!$f) {
				throw new UserException(sprintf('Fichier "%s" manquant dans "%s"', self::CONFIG_FILE, $path));
			}

			$config = $f->fetch();
			$this->dist = false;
		}
		else {
			$config_path = ROOT . '/skel-dist/' . $path . '/' . self::CONFIG_FILE;

			$config = @file_get_contents($config_path);

			if (!$config) {
				throw new UserException(sprintf('Fichier "%s" manquant dans "skel-dist/%s"', self::CONFIG_FILE, $path));
			}

			$this->dist = true;
		}

		$this->config = json_decode($config);

		if (!isset($this->config->name)) {
			throw new UserException('Le nom du document n\'est pas défini dans ' . self::CONFIG_FILE);
		}

		$this->name = $this->config->name;
		$this->context = $context;
		$this->id = $id;
		$this->path = $path;
	}

	static public function list(string $context)
	{
		$documents = [];

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

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

			$documents[$file->getFilename()] = null;
		}

		unset($i);

		$list = Files::list(File::CONTEXT_SKELETON . '/' . $context);

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

			$documents[$file] = null;
		}

		ksort($documents);

		foreach ($documents as $key => &$doc) {
			$doc = new self($context, $key);
		}

		return $documents;
	}

	public function template(string $file)
	{
		if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $file)) {
			throw new \InvalidArgumentException('Invalid skeleton name');
		}

		$ut = new UserTemplate($this->path . '/' . $file);

		return $ut;
	}

	public function url(string $file = '', array $params = null)
	{
		if (null !== $params) {
			$params = '?' . http_build_query($params);
		}

		return sprintf('%sdoc/%s/%s/%s%s', WWW_URL, $this->context, $this->id, $file, $params);
	}

	public function display(string $file)
	{
		header('Content-Type: text/html;charset=utf-8', true);

		try {
			$this->template($file)->display();
		}
		catch (Brindille_Exception $e) {
			printf('<div style="border: 5px solid orange; padding: 10px; background: yellow;"><h2>Erreur dans le code du document</h2><p>%s</p></div>', nl2br(htmlspecialchars($e->getMessage())));
		}
	}

	public function PDF(string $file)
	{
		$this->template($file)->displayPDF();
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
























































































































































































































































































































































































Modified src/include/lib/Garradin/UserTemplate/Functions.php from [dd415d2324] to [0c1e69f4fe].

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
		'http',
		'dump',
		'error',
		'read',
		'save',
		'admin_header',
		'admin_footer',
		'signature_url',
		'mail',
	];

	static public function admin_header(array $params): string
	{
		$tpl = Template::getInstance();
		$tpl->assign($params);







|







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
		'http',
		'dump',
		'error',
		'read',
		'save',
		'admin_header',
		'admin_footer',
		'signature',
		'mail',
	];

	static public function admin_header(array $params): string
	{
		$tpl = Template::getInstance();
		$tpl->assign($params);
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210

211


212
213
214
215
216
217
218
		if (!empty($params['base64'])) {
			return base64_encode($content);
		}

		return $content;
	}

	static public function signature_url(): string
	{
		$file = Config::getInstance()->file('signature');

		if (!$file) {
			return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
		}


		return 'data:image/png;base64,' . base64_encode($file->fetch());


	}

	static public function include(array $params, UserTemplate $ut, int $line): void
	{
		$path = self::getFilePath($params, 'file', $ut, $line);

		// Avoid recursive loops







|




|


>
|
>
>







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
		if (!empty($params['base64'])) {
			return base64_encode($content);
		}

		return $content;
	}

	static public function signature(): string
	{
		$file = Config::getInstance()->file('signature');

		if (!$file) {
			return '';
		}

		// We can't just use the image URL as it would not be accessible by PDF programs
		$url = 'data:image/png;base64,' . base64_encode($file->fetch());

		return sprintf('<figure class="signature"><img src="%s" alt="Signature" /></figure>', $url);
	}

	static public function include(array $params, UserTemplate $ut, int $line): void
	{
		$path = self::getFilePath($params, 'file', $ut, $line);

		// Avoid recursive loops
308
309
310
311
312
313
314




315
316
317
318
319
320
321
322
323
				throw new Brindille_Exception('Code HTTP inconnu');
			}

			header(sprintf('HTTP/1.1 %d %s', $params['code'], $codes[$params['code']]), true);
		}

		if (isset($params['type'])) {




			header('Content-Type: ' . $params['type'], true);
			$tpl->setContentType($params['type']);
		}

		if (isset($params['download'])) {
			header(sprintf('Content-Disposition: attachment; filename="%s"', Utils::safeFileName($params['download'])), true);
		}
	}
}







>
>
>
>









311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
				throw new Brindille_Exception('Code HTTP inconnu');
			}

			header(sprintf('HTTP/1.1 %d %s', $params['code'], $codes[$params['code']]), true);
		}

		if (isset($params['type'])) {
			if ($params['type'] == 'pdf') {
				$params['type'] = 'application/pdf';
			}

			header('Content-Type: ' . $params['type'], true);
			$tpl->setContentType($params['type']);
		}

		if (isset($params['download'])) {
			header(sprintf('Content-Disposition: attachment; filename="%s"', Utils::safeFileName($params['download'])), true);
		}
	}
}

Added src/include/lib/Garradin/UserTemplate/UserForms.php version [3a7808a8ca].













































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php

namespace Garradin;

use Garradin\Entities\UserForm;

class UserForms
{
	/**
	 * Lists all forms from files and stores a cache
	 */
	static public function refresh(): void
	{
		$existing = DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s;', UserForm::TABLE));
		$list = [];

		foreach (Files::list(UserForm::ROOT) as $file) {
			if ($file->type != $file::TYPE_DIRECTORY) {
				continue;
			}

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

		foreach (glob(self::DIST_SKEL_ROOT . '/*') as $file) {
			if (!is_dir($file)) {
				continue;
			}

			$list[] = Utils::basename($file);
		}

		$list = array_unique($list);
		sort($list);

		$create = array_diff($list, $existing);
		$delete = array_diff($existing, $list);
		$existing = array_diff($list, $create);

		foreach ($create as $name) {
			self::create($name);
		}

		foreach ($delete as $name) {
			self::get($name)->delete();
		}

		foreach ($existing as $name) {
			$f = self::get($name);
			$f->updateFromJSON();
			$f->save();
			$f->updateTemplates();
		}
	}

	static public function create(string $name): ?UserForm
	{
		$uf = new UserForm;
		$uf->name = $name;

		if (!$uf->updateFromJSON()) {
			return null;
		}

		$uf->save();
		$uf->updateTemplates();
		return $uf;
	}

	static public function list(): array
	{
		return EM::getInstance(UserForm::class)->all('SELECT * FROM @TABLE ORDER BY label COLLATE NOCASE ASC;');
	}

	static public function listForSnippet(string $snippet): array
	{
		return EM::getInstance(UserForm::class)->all('SELECT * FROM @TABLE
			WHERE templates LIKE ?
			ORDER BY label COLLATE NOCASE ASC;', sprintf('%%"%s%"%', $snippet));
	}

	static public function get(string $name): ?UserForm
	{
		return EM::findOne(UserForm::class, 'SELECT * FROM @TABLE WHERE name = ?;', $name);
	}

	static public function serve(string $uri): void
	{
		$name = substr($uri, 0, strrpos($uri, '/'));
		$file = substr($uri, strrpos($uri, '/') + 1) ?: 'index.html';

		$form = self::get($name);

		if (!$form) {
			http_response_code(404);
			throw new UserException('Ce formulaire n\'existe pas');
		}

		$form->displayWeb($file);
	}

}

Modified src/include/lib/Garradin/Web/Skeleton.php from [2b1b6eaa44] to [556415611b].

13
14
15
16
17
18
19
20

21
22
23
24
25
26
27

use const Garradin\ROOT;

class Skeleton
{
	const TEMPLATE_TYPES = '!^(?:text/(?:html|plain)|\w+/(?:\w+\+)?xml)$!';

	protected $path;


	static public function isValidPath(string $path)
	{
		return (bool) preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $path);
	}

	static public function list(): array







|
>







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

use const Garradin\ROOT;

class Skeleton
{
	const TEMPLATE_TYPES = '!^(?:text/(?:html|plain)|\w+/(?:\w+\+)?xml)$!';

	protected ?string $path;
	protected ?File $file = null;

	static public function isValidPath(string $path)
	{
		return (bool) preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $path);
	}

	static public function list(): array

Modified src/include/lib/Garradin/Web/Web.php from [600473b1d9] to [56b13472d4].

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

14
15
16
17
18
19
20
<?php

namespace Garradin\Web;

use Garradin\Entities\Web\Page;
use Garradin\Entities\Files\File;
use Garradin\Web\Skeleton;
use Garradin\UserTemplate\Document;
use Garradin\Files\Files;
use Garradin\API;
use Garradin\Config;
use Garradin\DB;
use Garradin\Plugin;

use Garradin\Utils;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Membres\Session;

use KD2\DB\EntityManager as EM;








<





>







1
2
3
4
5
6
7

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

namespace Garradin\Web;

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

use Garradin\Files\Files;
use Garradin\API;
use Garradin\Config;
use Garradin\DB;
use Garradin\Plugin;
use Garradin\UserForms;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Membres\Session;

use KD2\DB\EntityManager as EM;

214
215
216
217
218
219
220





221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
		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 (substr($uri, 0, 4) === 'doc/') {
			$uri = substr($uri, 4);
			Document::serve($uri);
			return;
		}
		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)) {







>
>
>
>
>





<
<
<
<
<







214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230





231
232
233
234
235
236
237
		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, 5) === 'form/') {
			$uri = substr($uri, 5);
			UserForms::serve($uri);
			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)) {

Added src/skel-dist/forms/recu_don/snippets/transaction_details.html version [dce502db35].





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{{:assign show=false}}

{{if $form.config.accounts}}
	{{#foreach from=$transaction.lines item="line"}}
		{{if $line.account_code|regexp_match:$form.config.accounts}}
			{{:assign show=true}}
		{{/if}}
	{{/foreach}}
{{/if}}

{{if $show}}
	<h2 class="ruler">Reçu de don</h2>
	<p>
		{{:linkbutton href="%s?id=%d"|args:$form.url:$transaction.id target="_dialog" label="Prévisualiser" shape="eye"}}
		{{:linkbutton href="%s?id=%d&print=pdf"|args:$form.url:$transaction.id label="Télécharger en PDF" shape="download"}}
		{{:linkbutton href="%s?id=%d&print=yes"|args:$form.url:$transaction.id target="_blank" label="Imprimer" shape="print"}}
	</p>
{{/if}}

Modified src/templates/acc/transactions/details.tpl from [6e4d92bd4f] to [edef472c69].

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
{if $files_edit || count($files)}
<div class="attachments">
	<h3 class="ruler">Fichiers joints</h3>
	{include file="common/files/_context_list.tpl" files=$files edit=$files_edit path=$file_parent}
</div>
{/if}

{if count($documents)}
<div class="templates">
	<h3>Modèles de documents</h3>
	<table>
	{foreach from=$documents item="doc"}
		<tr>
			<th>{link href="%s?id_transaction=%d"|args:$doc->url(),$transaction.id label=$doc.name target="_blank"}</th>
			<td>
			{if $doc.config.preview}
				{linkbutton shape="eye" href="%s?id_transaction=%d"|args:$doc->url(),$transaction.id label="Prévisualiser" target="_dialog"}
			{/if}
			</td>
			<td>
			{if $doc.config.pdf}
				{linkbutton shape="download" href="%s?id_transaction=%d&pdf"|args:$doc->url(),$transaction.id label="Télécharger"}
			{/if}
			</td>
			<td>
			{if $doc.config.config}
				{linkbutton shape="settings" href="%s?id_transaction=%d"|args:$doc->url($doc.config.config),$transaction.id label="Configurer" target="_dialog"}
			{/if}
			</td>
		</tr>
	{/foreach}
</div>
{/if}

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







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

146
147
148
149
150
151
152



























153
{if $files_edit || count($files)}
<div class="attachments">
	<h3 class="ruler">Fichiers joints</h3>
	{include file="common/files/_context_list.tpl" files=$files edit=$files_edit path=$file_parent}
</div>
{/if}




























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

Modified src/templates/admin/config/_menu.tpl from [0f21a24c00] to [a027d77795].

1
2
3
4
5
6
7

8
9
10
11
12
13
14
<nav class="tabs">
	<ul>
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">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>

	{if $current == 'advanced'}
	<ul class="sub">
		<li{if !$sub_current} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li>


|




>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<nav class="tabs">
	<ul>
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Configuration</a></li>
		<li{if $current == 'custom'} class="current"{/if}><a href="{$admin_url}config/custom.php">Personnalisation</a></li>
		<li{if $current == '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/forms/">Formulaires &amp; modèles</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>

	{if $current == 'advanced'}
	<ul class="sub">
		<li{if !$sub_current} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li>

Modified src/www/admin/acc/transactions/details.php from [2a516510d2] to [48aefb0f02].

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace Garradin;

use Garradin\Accounting\Transactions;
use Garradin\UserTemplate\Document;

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

$transaction = Transactions::get((int) qg('id'));

if (!$transaction) {
	throw new UserException('Cette écriture n\'existe pas');




<







1
2
3
4

5
6
7
8
9
10
11
<?php
namespace Garradin;

use Garradin\Accounting\Transactions;


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

$transaction = Transactions::get((int) qg('id'));

if (!$transaction) {
	throw new UserException('Cette écriture n\'existe pas');
25
26
27
28
29
30
31
32
33
34
35
$tpl->assign('tr_year', $transaction->year());
$tpl->assign('creator_name', $transaction->id_creator ? (new Membres)->getNom($transaction->id_creator) : null);

$tpl->assign('files_edit', $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE));
$tpl->assign('file_parent', $transaction->getAttachementsDirectory());
$tpl->assign('related_users', $transaction->listLinkedUsers());
$tpl->assign('related_transactions', $transaction->listRelatedTransactions());
$tpl->assign('documents', Document::list(Document::CONTEXT_TRANSACTION));
$tpl->assign('doc_params', ['id_transaction' => $transaction->id()]);

$tpl->display('acc/transactions/details.tpl');







<
<


24
25
26
27
28
29
30


31
32
$tpl->assign('tr_year', $transaction->year());
$tpl->assign('creator_name', $transaction->id_creator ? (new Membres)->getNom($transaction->id_creator) : null);

$tpl->assign('files_edit', $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE));
$tpl->assign('file_parent', $transaction->getAttachementsDirectory());
$tpl->assign('related_users', $transaction->listLinkedUsers());
$tpl->assign('related_transactions', $transaction->listRelatedTransactions());



$tpl->display('acc/transactions/details.tpl');