Overview
Comment:Use INI file instead of JSON for Module metadata (easier to use), allow to add home button and menu item without any code
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA3-256: 366dbe898749b028e402153dd7cbdc5f0c01ffa53a3aea147ce2f9f2d327e0ba
User & Date: bohwaz on 2023-02-14 15:02:47
Other Links: branch diff | manifest | tags
Context
2023-02-14
22:29
Refactor plugins to have the same UI for plugins and modules, also modernize code of plugins management check-in: a8251477ac user: bohwaz tags: dev
15:02
Use INI file instead of JSON for Module metadata (easier to use), allow to add home button and menu item without any code check-in: 366dbe8987 user: bohwaz tags: dev
14:03
Make sure we don't request from database in install, and delete if database is created by mistake check-in: e2a27f4268 user: bohwaz tags: dev
Changes

Modified src/include/lib/Garradin/Entities/Module.php from [adac7a6d80] to [3f935b6b38].

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

use const Garradin\{ROOT, WWW_URL};

class Module extends Entity
{
	const ROOT = File::CONTEXT_SKELETON . '/modules';
	const DIST_ROOT = ROOT . '/skel-dist/modules';
	const META_FILE = 'module.json';

	const CONFIG_TEMPLATE = 'config.html';

	// Snippets, don't forget to create alias constant in UserTemplate\Modules class
	const SNIPPET_TRANSACTION = 'snippets/transaction_details.html';
	const SNIPPET_USER = 'snippets/user_details.html';
	const SNIPPET_HOME_BUTTON = 'snippets/home_button.html';







|







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

use const Garradin\{ROOT, WWW_URL};

class Module extends Entity
{
	const ROOT = File::CONTEXT_SKELETON . '/modules';
	const DIST_ROOT = ROOT . '/skel-dist/modules';
	const META_FILE = 'module.ini';

	const CONFIG_TEMPLATE = 'config.html';

	// Snippets, don't forget to create alias constant in UserTemplate\Modules class
	const SNIPPET_TRANSACTION = 'snippets/transaction_details.html';
	const SNIPPET_USER = 'snippets/user_details.html';
	const SNIPPET_HOME_BUTTON = 'snippets/home_button.html';
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
	/**
	 * Directory name
	 */
	protected string $name;

	protected string $label;
	protected ?string $description;






	protected ?\stdClass $config;
	protected bool $enabled;

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

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


		$json = json_decode($json);






		if (!isset($json->label)) {
			return false;
		}

		$this->set('label', $json->label);
		$this->set('description', $json->description ?? null);







		return true;
	}

	public function updateTemplates(): void
	{
		$check = self::SNIPPETS + [self::CONFIG_TEMPLATE => 'Config'];







>
>
>
>
>
>










|

|


|


|





>
|
>
>
|
>
>
>
|



|
|
>
>
>
>
>
>







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
	/**
	 * Directory name
	 */
	protected string $name;

	protected string $label;
	protected ?string $description;
	protected ?string $author;
	protected ?string $url;
	protected ?string $restrict_section;
	protected ?int $restrict_level;
	protected bool $home_button;
	protected bool $menu;
	protected ?\stdClass $config;
	protected bool $enabled;

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

	/**
	 * Fills information from module.ini file
	 */
	public function updateFromINI(bool $use_local = true): bool
	{
		if ($use_local && ($file = Files::get($this->path(self::META_FILE)))) {
			$ini = $file->fetch();
		}
		elseif (file_exists($this->distPath(self::META_FILE))) {
			$ini = file_get_contents($this->distPath(self::META_FILE));
		}
		else {
			return false;
		}

		$ini = @parse_ini_string($ini, false, \INI_SCANNER_TYPED);

		if (empty($ini)) {
			return false;
		}

		$ini = (object) $ini;

		if (!isset($ini->name)) {
			return false;
		}

		$this->set('label', $ini->name);
		$this->set('description', $ini->description ?? null);
		$this->set('author', $ini->author ?? null);
		$this->set('url', $ini->url ?? null);
		$this->set('home_button', !empty($ini->home_button));
		$this->set('menu', !empty($ini->menu));
		$this->set('restrict_section', $ini->restrict_section ?? null);
		$this->set('restrict_level', isset($ini->restrict_section, $ini->restrict_level, Session::ACCESS_WORDS[$ini->restrict_level]) ? Session::ACCESS_WORDS[$ini->restrict_level] : null);

		return true;
	}

	public function updateTemplates(): void
	{
		$check = self::SNIPPETS + [self::CONFIG_TEMPLATE => 'Config'];

Modified src/include/lib/Garradin/UserTemplate/Modules.php from [968f6c135b] to [f759d31bbe].

1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
<?php

namespace Garradin\UserTemplate;

use Garradin\Entities\Module;

use Garradin\Files\Files;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;


use const Garradin\ROOT;

use \KD2\DB\EntityManager as EM;

class Modules
{










>







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

namespace Garradin\UserTemplate;

use Garradin\Entities\Module;

use Garradin\Files\Files;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Users\Session;

use const Garradin\ROOT;

use \KD2\DB\EntityManager as EM;

class Modules
{
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

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

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

	/**
	 * List modules names from locally installed directories







|







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

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

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

	/**
	 * List modules names from locally installed directories
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
		$list = self::listRaw(false);
		$out = [];

		foreach ($list as $name) {
			$m = new Module;
			$m->name = $name;

			if (!$m->updateFromJSON(false)) {
				continue;
			}

			$out[$name] = $m;
		}

		return $out;
	}

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

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

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







|














|







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
		$list = self::listRaw(false);
		$out = [];

		foreach ($list as $name) {
			$m = new Module;
			$m->name = $name;

			if (!$m->updateFromINI(false)) {
				continue;
			}

			$out[$name] = $m;
		}

		return $out;
	}

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

		if (!$module->updateFromINI()) {
			return null;
		}

		$module->save();
		$module->updateTemplates();
		return $module;
	}
144
145
146
147
148
149
150

























151
152
153
154
155
156
157
158
159
160
161
	static public function listForSnippet(string $snippet): array
	{
		return EM::getInstance(Module::class)->all('SELECT f.* FROM @TABLE f
			INNER JOIN modules_templates t ON t.id_module = f.id
			WHERE t.name = ? AND f.enabled = 1
			ORDER BY f.label COLLATE NOCASE ASC;', $snippet);
	}


























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

	static public function isEnabled(string $name): bool
	{
		return (bool) EM::getInstance(Module::class)->col('SELECT 1 FROM @TABLE WHERE name = ? AND enabled = 1;', $name);
	}
}







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











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
	static public function listForSnippet(string $snippet): array
	{
		return EM::getInstance(Module::class)->all('SELECT f.* FROM @TABLE f
			INNER JOIN modules_templates t ON t.id_module = f.id
			WHERE t.name = ? AND f.enabled = 1
			ORDER BY f.label COLLATE NOCASE ASC;', $snippet);
	}

	static public function listModulesAndPluginsMenu(): array
	{
		$list = [];
		$session = Session::getInstance();

		foreach (DB::getInstance()->get('SELECT name, label, restrict_section, restrict_level FROM modules WHERE menu = 1;') as $m) {
			if (!$session->canAccess($m->restrict_section, $m->restrict_level)) {
				continue;
			}

			$list[$m->name] = sprintf('<a href="%sm/%s">%s</a>', ADMIN_URL, $m->name, $m->label);
		}

		foreach (DB::getInstance()->get('SELECT id, label, restrict_section, restrict_level FROM plugins WHERE menu = 1;') as $p) {
			if (!$session->canAccess($p->restrict_section, $p->restrict_level)) {
				continue;
			}

			$list[$m->name] = sprintf('<a href="%sp/%s">%s</a>', ADMIN_URL, $p->name, $p->label);
		}

		ksort($list);
		return $list;
	}

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

	static public function isEnabled(string $name): bool
	{
		return (bool) EM::getInstance(Module::class)->col('SELECT 1 FROM @TABLE WHERE name = ? AND enabled = 1;', $name);
	}
}

Modified src/include/lib/Garradin/Users/Session.php from [4d9aa4f9ca] to [4b67ea0f8f].

39
40
41
42
43
44
45







46
47
48
49
50
51
52
	const SECTION_CONFIG = 'config';
	const SECTION_SUBSCRIBE = 'subscribe';

	const ACCESS_NONE = 0;
	const ACCESS_READ = 1;
	const ACCESS_WRITE = 2;
	const ACCESS_ADMIN = 9;








	// Personalisation de la config de UserSession
	protected bool $non_locking = true;
	protected $cookie_name = 'pko';
	protected $remember_me_cookie_name = 'pkop';
	protected $remember_me_expiry = '+3 months';








>
>
>
>
>
>
>







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
	const SECTION_CONFIG = 'config';
	const SECTION_SUBSCRIBE = 'subscribe';

	const ACCESS_NONE = 0;
	const ACCESS_READ = 1;
	const ACCESS_WRITE = 2;
	const ACCESS_ADMIN = 9;

	const ACCESS_WORDS = [
		'none' => self::ACCESS_NONE,
		'read' => self::ACCESS_READ,
		'write' => self::ACCESS_WRITE,
		'admin' => self::ACCESS_ADMIN,
	];

	// Personalisation de la config de UserSession
	protected bool $non_locking = true;
	protected $cookie_name = 'pko';
	protected $remember_me_cookie_name = 'pkop';
	protected $remember_me_expiry = '+3 months';

Modified src/include/migrations/1.3/1.3.0.sql from [799f3d7106] to [8a8da01d86].

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
DROP TABLE services_users_old;

-- Remove old plugin as it cannot be uninstalled as it no longer exists
DELETE FROM plugins_old WHERE nom = 'ouvertures';
DELETE FROM plugins_signaux_old WHERE plugin = 'ouvertures';

-- Rename plugins table columns to English
INSERT INTO plugins SELECT id, nom, description, auteur, url, version, config FROM plugins_old;
INSERT INTO plugins_signals SELECT * FROM plugins_signaux_old;

DROP TABLE plugins_signaux_old;
DROP TABLE plugins_old;

INSERT INTO searches SELECT * FROM recherches;
UPDATE searches SET target = 'accounting' WHERE target = 'compta';







|







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
DROP TABLE services_users_old;

-- Remove old plugin as it cannot be uninstalled as it no longer exists
DELETE FROM plugins_old WHERE nom = 'ouvertures';
DELETE FROM plugins_signaux_old WHERE plugin = 'ouvertures';

-- Rename plugins table columns to English
INSERT INTO plugins (id, label, description, author, url, version, config, menu) SELECT id, nom, description, auteur, url, version, config, 0 FROM plugins_old;
INSERT INTO plugins_signals SELECT * FROM plugins_signaux_old;

DROP TABLE plugins_signaux_old;
DROP TABLE plugins_old;

INSERT INTO searches SELECT * FROM recherches;
UPDATE searches SET target = 'accounting' WHERE target = 'compta';

Modified src/include/migrations/1.3/schema.sql from [5d55bc7389] to [aef26e336a].

31
32
33
34
35
36
37




38
39
40
41
42
43
44
(
    id TEXT NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT NULL,
    author TEXT NULL,
    url TEXT NULL,
    version TEXT NOT NULL,




    config TEXT NULL
);

CREATE TABLE IF NOT EXISTS plugins_signals
-- Link between plugins and signals
(
    signal TEXT NOT NULL,







>
>
>
>







31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
(
    id TEXT NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT NULL,
    author TEXT NULL,
    url TEXT NULL,
    version TEXT NOT NULL,
    menu INT NOT NULL DEFAULT 0,
    home_button INT NOT NULL DEFAULT 0,
    restrict_section TEXT NULL,
    restrict_level INT NULL,
    config TEXT NULL
);

CREATE TABLE IF NOT EXISTS plugins_signals
-- Link between plugins and signals
(
    signal TEXT NOT NULL,
494
495
496
497
498
499
500






501
502
503
504
505
506
507
CREATE TABLE IF NOT EXISTS modules
-- List of modules
(
    id INTEGER NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    label TEXT NOT NULL,
    description TEXT NULL,






    config TEXT NULL,
    enabled INTEGER NOT NULL DEFAULT 0
);

CREATE UNIQUE INDEX IF NOT EXISTS modules_name ON modules (name);

CREATE TABLE IF NOT EXISTS modules_templates







>
>
>
>
>
>







498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
CREATE TABLE IF NOT EXISTS modules
-- List of modules
(
    id INTEGER NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    label TEXT NOT NULL,
    description TEXT NULL,
    author TEXT NULL,
    url TEXT NULL,
    menu INT NOT NULL DEFAULT 0,
    home_button INT NOT NULL DEFAULT 0,
    restrict_section TEXT NULL,
    restrict_level INT NULL,
    config TEXT NULL,
    enabled INTEGER NOT NULL DEFAULT 0
);

CREATE UNIQUE INDEX IF NOT EXISTS modules_name ON modules (name);

CREATE TABLE IF NOT EXISTS modules_templates

Added src/skel-dist/modules/bilan_pc/module.ini version [294e350d2a].









>
>
>
>
1
2
3
4
name="Bilan expert"
description="Bilan annuel selon le modèle du plan comptable des associations 2020"
author="Paheko"
url="https://paheko.cloud/"

Deleted src/skel-dist/modules/bilan_pc/module.json version [8f119bbb23].

1
2
3
4
{
	"label": "Bilan expert",
	"description": "Bilan annuel selon le modèle du plan comptable des associations 2020"
}
<
<
<
<








Added src/skel-dist/modules/carte_membre/module.ini version [e83754f068].









>
>
>
>
1
2
3
4
name="Carte de membre"
description="Carte de membre, imprimable par membre ou en planche de plusieurs membres"
author="Paheko"
url="https://paheko.cloud/"

Deleted src/skel-dist/modules/carte_membre/module.json version [3b6ce77fe3].

1
2
3
4
{
	"label": "Carte de membre",
	"description": "Carte de membre, imprimable par membre ou en planche de plusieurs membres"
}
<
<
<
<








Modified src/skel-dist/modules/invoice/module.ini from [2780326c4d] to [5bb5a68af8].

1
2
3
4


{
	"label": "Devis et factures",
	"description": "Permet de créer des devis et des factures, et de les imprimer"
}


<
|
|
<
>
>

1
2

3
4

name="Devis et factures"
description="Permet de créer des devis et des factures, et de les imprimer"

author="Paheko"
url="https://paheko.cloud/"

Modified src/skel-dist/modules/ouvertures/module.ini from [80628e2296] to [6324cdc425].

1
2
3
4


{
	"label": "Horaires d'ouverture",
	"description": "Permet d'afficher sur la page d'accueil les jours et horaires d'ouverture"
}


<
|
|
<
>
>

1
2

3
4

name="Horaires d'ouverture"
description="Permet d'afficher sur la page d'accueil les jours et horaires d'ouverture"

author="Paheko"
url="https://paheko.cloud/"

Modified src/skel-dist/modules/recu_don/module.ini from [cd920ccb31] to [ad2de6e4c1].

1
2
3
4


{
	"label": "Reçu de don",
	"description": "Reçu de don simple, sans valeur fiscale"
}


<
|
|
<
>
>

1
2

3
4

label="Reçu de don"
description="Reçu de don simple, sans valeur fiscale"

author="Paheko"
url="https://paheko.cloud/"

Modified src/skel-dist/modules/recu_paiement/module.ini from [972c67d830] to [20efb04279].

1
2
3
4


{
	"label": "Reçu de paiement",
	"description": "Reçu de paiement, pour les écritures liées à un membre"
}


<
|
|
<
>
>

1
2

3
4

name="Reçu de paiement"
description="Reçu de paiement, pour les écritures liées à un membre"

author="Paheko"
url="https://paheko.cloud/"

Modified src/skel-dist/modules/recus_fiscaux/module.ini from [4a0b870aaa] to [5d6ae17b31].

1
2
3
4


{
	"label": "Reçus fiscaux",
	"description": "Permet de générer des reçus fiscaux. Conforme aux exigences fiscales de 2022. Seuls les membres ayant accès à la comptabilité auront accès à ce module."
}


<
|
|
<
>
>

1
2

3
4

name="Reçus fiscaux"
description="Permet de générer des reçus fiscaux. Conforme aux exigences fiscales de 2022. Seuls les membres ayant accès à la comptabilité auront accès à ce module."

author="Paheko"
url="https://paheko.cloud/"

Deleted src/skel-dist/modules/recus_fiscaux/snippets/home_button.html version [99f1ef488a].

1
2
3
{{#restrict section="accounting" level="read"}}
	{{:linkbutton label="Reçus fiscaux" href=$module.url icon="%sicon.svg"|args:$module.url}}
{{/restrict}}
<
<
<






Modified src/skel-dist/modules/remise_cheques/module.ini from [a93b07bbd2] to [6dd2247e01].

1
2
3
4





{
	"label": "Bordereau de remise de chèques",
	"description": "Permet d'imprimer un bordereau de remise de chèques à partir d'une écriture de dépôt."
}





<
|
|
<
>
>
>
>
>

1
2

3
4
5
6
7

name="Bordereau de remise de chèques"
description="Permet d'imprimer un bordereau de remise de chèques à partir d'une écriture de dépôt."

author="Paheko"
url="https://paheko.cloud/"
home_button=true
restrict_section="accounting"
restrict_level="read"