Overview
Comment: | Implement module import |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | dev |
Files: | files | file ages | folders |
SHA3-256: |
42198abefe480c06f99376dc5b64c10c |
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 | /** * System modules are always available, disabling them only hides the links */ protected bool $system; public function selfCheck(): void { | | | 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 | public function distPath(string $file = null): string { return self::DIST_ROOT . '/' . $this->name . ($file ? '/' . $file : ''); } public function dir(): ?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 | <?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; | > | > | 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 | 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 ?</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 | {include file="_head.tpl" title="Extensions" current="config"} {include file="config/_menu.tpl" current="ext"} <nav class="tabs"> | < | 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 | <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} | | | | | | 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 | {/foreach} </tbody> </table> {csrf_field key=$csrf_key} </form> <p class="help"> | | > > > > > > > | 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'); |