Overview
Comment:Implement module import
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA3-256: 42198abefe480c06f99376dc5b64c10c04572c3934b41f87ddf584ddf6158f0e
User & Date: bohwaz on 2023-04-05 01:19:45
Other Links: branch diff | manifest | tags
Context
2023-04-05
01:29
Pass more exception errors to user when importing a module check-in: b399e347fb user: bohwaz tags: dev
01:19
Implement module import check-in: 42198abefe user: bohwaz tags: dev
2023-04-04
22:33
Implement module export as ZIP check-in: 8105bd992f user: bohwaz tags: dev
Changes

Modified src/include/lib/Garradin/Entities/Module.php from [9a9658123a] to [60cd4b3956].

32
33
34
35
36
37
38


39
40
41
42
43
44
45

	const SNIPPETS = [
		self::SNIPPET_HOME_BUTTON => '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 = 'modules';

	protected ?int $id;

	/**
	 * Directory name
	 */







>
>







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

	const SNIPPETS = [
		self::SNIPPET_HOME_BUTTON => '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 VALID_NAME_REGEXP = '/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/';

	const TABLE = 'modules';

	protected ?int $id;

	/**
	 * Directory name
	 */
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
	/**
	 * System modules are always available, disabling them only hides the links
	 */
	protected bool $system;

	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







|







62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
	/**
	 * System modules are always available, disabling them only hides the links
	 */
	protected bool $system;

	public function selfCheck(): void
	{
		$this->assert(preg_match(self::VALID_NAME_REGEXP, $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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
	public function distPath(string $file = null): string
	{
		return self::DIST_ROOT . '/' . $this->name . ($file ? '/' . $file : '');
	}

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

	public function hasFile(string $file): bool
	{
		return $this->hasLocalFile($file) || $this->hasDistFile($file);
	}








|







152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
	public function distPath(string $file = null): string
	{
		return self::DIST_ROOT . '/' . $this->name . ($file ? '/' . $file : '');
	}

	public function dir(): ?File
	{
		return Files::get($this->path());
	}

	public function hasFile(string $file): bool
	{
		return $this->hasLocalFile($file) || $this->hasDistFile($file);
	}

Modified src/include/lib/Garradin/UserTemplate/Modules.php from [76f113eca4] to [5cc69f60ca].

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

namespace Garradin\UserTemplate;

use Garradin\Entities\Module;

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

use Garradin\Entities\Web\Page;

use const Garradin\ROOT;
use const Garradin\ADMIN_URL;

use \KD2\DB\EntityManager as EM;


class Modules
{
	// Shortcuts so that code calling snippets method don't have to use Module entity
	const SNIPPET_TRANSACTION = Module::SNIPPET_TRANSACTION;
	const SNIPPET_USER = Module::SNIPPET_USER;
	const SNIPPET_HOME_BUTTON = Module::SNIPPET_HOME_BUTTON;













>





|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

namespace Garradin\UserTemplate;

use Garradin\Entities\Module;

use Garradin\Files\Files;
use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Users\Session;
use Garradin\Web\Web;
use Garradin\Entities\Files\File;
use Garradin\Entities\Web\Page;

use const Garradin\ROOT;
use const Garradin\ADMIN_URL;

use KD2\DB\EntityManager as EM;
use KD2\ZipReader;

class Modules
{
	// Shortcuts so that code calling snippets method don't have to use Module entity
	const SNIPPET_TRANSACTION = Module::SNIPPET_TRANSACTION;
	const SNIPPET_USER = Module::SNIPPET_USER;
	const SNIPPET_HOME_BUTTON = Module::SNIPPET_HOME_BUTTON;
265
266
267
268
269
270
271
272


















































































		if (!$has_local_file && !$has_dist_file) {
			http_response_code(404);
			throw new UserException('This page is not found, sorry.');
		}

		$module->serve($path, $has_local_file, compact('uri', 'page'));
	}
}

























































































|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
		if (!$has_local_file && !$has_dist_file) {
			http_response_code(404);
			throw new UserException('This page is not found, sorry.');
		}

		$module->serve($path, $has_local_file, compact('uri', 'page'));
	}

	static public function import(string $path): ?Module
	{
		$zip = new ZipReader;

		try {
			$zip->open($path);
		}
		catch (\OutOfBoundsException $e) {
			throw new \InvalidArgumentException('Invalid ZIP file: ' . $e->getMessage(), 0, $e);
		}

		$module_name = null;
		$files = [];

		foreach ($zip->iterate() as $name => $file) {
			if ($name == 'modules' || $file['dir']) {
				continue;
			}

			if (strpos($name, 'modules/') !== 0) {
				throw new \InvalidArgumentException('Invalid ZIP file: invalid path:' . $name);
			}

			$_mod = strtok(substr($name, strlen('modules/')), '/');

			if (!$module_name) {
				if (!$_mod || !preg_match(Module::VALID_NAME_REGEXP, $_mod)) {
					throw new \InvalidArgumentException('Invalid module name (allowed: [a-z][a-z0-9]*(_[a-z0-9])*): ' . $_mod);
				}

				$module_name = $_mod;
			}
			elseif ($module_name !== $_mod) {
				throw new \InvalidArgumentException('Two different modules names found.');
			}

			$_name = strtok(false);
			$files[$_name] = $name;
		}

		if (!$module_name || !count($files)) {
			throw new \InvalidArgumentException('No module found in archive');
		}

		$base = File::CONTEXT_MODULES . '/' . $module_name;

		if (Files::exists($base)) {
			return null;
		}

		try {
			foreach ($files as $local_name => $source) {
				$f = Files::createObject($base . '/' . $local_name);
				$fp = fopen('php://temp', 'wb');
				$zip->extractToPointer($fp, $source);
				rewind($fp);
				$f->store(['pointer' => $fp]);
			}

			$module = self::get($module_name) ?? self::create($module_name);

			if (!$module) {
				throw new \InvalidArgumentException('Invalid module information');
			}

			return $module;
		}
		catch (\Exception $e) {
			$dir = Files::get($base);

			// Delete any extracted files so far
			if ($dir) {
				$dir->delete();
			}

			throw $e;
		}
		finally {
			unset($zip);
		}
	}
}

Modified src/include/lib/Garradin/Utils.php from [14fc0632f3] to [00c4d030b4].

324
325
326
327
328
329
330










331
332
333
334
335
336
337
        return $uri;
    }

    static public function getModifiedURL(string $new)
    {
        return HTTP::mergeURLs(self::getSelfURL(), $new);
    }











    static public function reloadParentFrame(?string $destination = null): void
    {
        $url = self::getLocalURL($destination ?? '!');

        echo '
            <!DOCTYPE html>







>
>
>
>
>
>
>
>
>
>







324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
        return $uri;
    }

    static public function getModifiedURL(string $new)
    {
        return HTTP::mergeURLs(self::getSelfURL(), $new);
    }

    static public function redirectDialog(?string $destination = null): void
    {
        if (isset($_GET['_dialog'])) {
            self::reloadParentFrame($destination);
        }
        else {
            self::redirect($destination);
        }
    }

    static public function reloadParentFrame(?string $destination = null): void
    {
        $url = self::getLocalURL($destination ?? '!');

        echo '
            <!DOCTYPE html>

Added src/templates/config/ext/import.tpl version [e37365cfb1].





















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="_head.tpl" title="Importer un module" current="config"}

{form_errors}

<form method="post" action="" enctype="multipart/form-data">
<fieldset>
	<legend>Importer un module d'extension</legend>
	<dl>
		{input type="file" required=true label="Fichier ZIP du module" name="zip" accept=".zip,application/zip"}
	</dl>
	<p class="alert block">
		<strong>Attention, faites-vous confiance à la personne qui vous a transmis ce module&nbsp;?</strong><br />
		Importer un module de source inconnue peut présenter des risques pour les données de votre association.<br />
		Un module écrit par une personne mal intentionnée pourrait voler les données de votre association, ou modifier ou supprimer des données.
	</p>
	<dl>
		{input type="checkbox" name="confirm" value=1 label="Je comprends les risques, importer ce module" required=true}
	</dl>
	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" shape="right" label="Importer ce module" name="import" class="main"}
	</p>
</fieldset>
</form>

{include file="_foot.tpl"}

Modified src/templates/config/ext/index.tpl from [24d2e9dc4e] to [15b765232b].

1
2
3
4
5
6
7
8
9
10
11
12
13
{include file="_head.tpl" title="Extensions" current="config"}

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

<nav class="tabs">

	<ul class="sub">
		<li{if !$installable} class="current"{/if}><a href="./">Activées</a></li>
		<li{if $installable} class="current"{/if}><a href="./?install=1">Inactives</a></li>
	</ul>
</nav>

{if !empty($url_plugins)}





<







1
2
3
4
5

6
7
8
9
10
11
12
{include file="_head.tpl" title="Extensions" current="config"}

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

<nav class="tabs">

	<ul class="sub">
		<li{if !$installable} class="current"{/if}><a href="./">Activées</a></li>
		<li{if $installable} class="current"{/if}><a href="./?install=1">Inactives</a></li>
	</ul>
</nav>

{if !empty($url_plugins)}
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
					<td>
						{if $item.enabled && $item.url && !$item.web}
							{linkbutton shape="right" label="Ouvrir" href=$item.url}
						{/if}
					</td>
					<td class="actions">
						{if $item.module && $item.enabled}
							{if $item.module->hasLocal() && $item.module->hasDist()}
								{linkbutton label="Remettre à zéro" href="delete.php?module=%s"|args:$item.name shape="reset" target="_dialog"}
							{/if}
							{linkbutton label="Modifier" href="edit.php?module=%s"|args:$item.name shape="edit"}
						{elseif $item.module && !$item.enabled && $item.module->canDelete()}
							{linkbutton label="Supprimer" href="delete.php?module=%s"|args:$item.name shape="delete" target="_dialog"}
						{elseif $item.plugin && !$item.enabled && $item.installed}
							{linkbutton label="Supprimer" href="delete.php?plugin=%s"|args:$item.name shape="delete" target="_dialog"}
						{/if}
					</td>
					<td class="actions">
						{if $item.config_url && $item.enabled}
							{linkbutton label="Configurer" href=$item.config_url shape="settings" target="_dialog"}







|
|
|


|







86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
					<td>
						{if $item.enabled && $item.url && !$item.web}
							{linkbutton shape="right" label="Ouvrir" href=$item.url}
						{/if}
					</td>
					<td class="actions">
						{if $item.module && $item.enabled}
							{*if $item.module->hasLocal() && $item.module->hasDist()}
								{linkbutton label="Supprimer mes modifications" href="delete.php?module=%s"|args:$item.name shape="delete" target="_dialog"}
							{/if*}
							{linkbutton label="Modifier" href="edit.php?module=%s"|args:$item.name shape="edit"}
						{elseif $item.module && !$item.enabled && $item.module->canDelete()}
							{linkbutton label="Supprimer données et modifications" href="delete.php?module=%s"|args:$item.name shape="delete" target="_dialog"}
						{elseif $item.plugin && !$item.enabled && $item.installed}
							{linkbutton label="Supprimer" href="delete.php?plugin=%s"|args:$item.name shape="delete" target="_dialog"}
						{/if}
					</td>
					<td class="actions">
						{if $item.config_url && $item.enabled}
							{linkbutton label="Configurer" href=$item.config_url shape="settings" target="_dialog"}
127
128
129
130
131
132
133
134
135
136







137
			{/foreach}
		</tbody>
	</table>
	{csrf_field key=$csrf_key}
</form>

<p class="help">
	La mention <em class="tag">Modifiable</em> indique que cette extension est un module que vous pouvez modifier. {linkbutton shape="help" label="Comment modifier et développer des modules" href="!static/doc/modules.html" target="_dialog"}
</p>








{include file="_foot.tpl"}







|


>
>
>
>
>
>
>

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
			{/foreach}
		</tbody>
	</table>
	{csrf_field key=$csrf_key}
</form>

<p class="help">
	La mention <em class="tag">Modifiable</em> indique que cette extension est un module que vous pouvez modifier.
</p>

<p>
	{linkbutton shape="help" label="Comment modifier et développer des modules" href="!static/doc/modules.html" target="_dialog"}
	{linkbutton shape="plus" label="Créer un module" href="new.php"}
	{linkbutton shape="import" label="Importer un module" href="import.php" target="_dialog"}
</p>


{include file="_foot.tpl"}

Modified src/www/admin/config/ext/edit.php from [111294ba6b] to [f48e197328].

10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26

if (!$module) {
	throw new UserException('Module inconnu');
}

if (null !== qg('export')) {
	$module->export(Session::getInstance());

}

$path = qg('p');
$parent_path_uri = rawurlencode($module->path($path));
$list = $module->listFiles($path);

$url_help_modules = sprintf(HELP_PATTERN_URL, 'modules');
$tpl->assign(compact('list', 'url_help_modules', 'module', 'path', 'parent_path_uri'));

$tpl->display('config/ext/edit.tpl');







>










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

if (!$module) {
	throw new UserException('Module inconnu');
}

if (null !== qg('export')) {
	$module->export(Session::getInstance());
	return;
}

$path = qg('p');
$parent_path_uri = rawurlencode($module->path($path));
$list = $module->listFiles($path);

$url_help_modules = sprintf(HELP_PATTERN_URL, 'modules');
$tpl->assign(compact('list', 'url_help_modules', 'module', 'path', 'parent_path_uri'));

$tpl->display('config/ext/edit.tpl');

Added src/www/admin/config/ext/import.php version [2d670185a8].

































































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

use Garradin\UserTemplate\Modules;
use Garradin\Users\Session;

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


$csrf_key = 'module_import';
$form->runIf('import', function () {
	if (!f('confirm')) {
		throw new UserException('Merci de cocher la case de confirmation.');
	}

	if (empty($_FILES['zip']['tmp_name'])) {
		throw new UserException('Aucun fichier reçu.');
	}

	$m = Modules::import($_FILES['zip']['tmp_name']);

	if (!$m) {
		throw new UserException('Un module avec ce nom unique existe déjà. Pour importer ce module, merci de supprimer ou remettre à zéro le module existant.');
	}

	$i = (int)!$m->enabled;
	Utils::redirectDialog(sprintf('!config/ext/?install=%d&focus=%s', $i, $m->name));
}, $csrf_key);

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

$tpl->display('config/ext/import.tpl');