diff --git a/.gitignore b/.gitignore index 9a5f6d9..dc0b011 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ # Web vendors (to be installed manually) # see https://github.com/Trehinos/Thor#web-vendors-not-included /web/assets -/app/res/static/assets/vendors # Variables files (created on runtime by the application) /var/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..778659a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "app/res/static/assets/vendors"] + path = app/res/static/assets/vendors + url = ssh://git@asgard.trehinos.eu/Trehinos/thor-web-vendors diff --git a/app/res/config/config.yml b/app/res/config/config.yml index cdaa185..0480e51 100644 --- a/app/res/config/config.yml +++ b/app/res/config/config.yml @@ -1,7 +1,7 @@ app_vendor: Trehinos app_name: Thor -app_version: 0.9.6 -app_version_name: ω +app_version: 1.0.0 +app_version_name: 'Thor v1' env: DEV lang: fr timezone: Europe/Paris diff --git a/app/res/config/security.yml b/app/res/config/security.yml index 38d1429..2df0269 100644 --- a/app/res/config/security.yml +++ b/app/res/config/security.yml @@ -1,6 +1,8 @@ security: true pdo-handler: default security-factory: 'Thor\Framework\Factories\SecurityFactory:produceSecurity' +user-class: 'Thor\Framework\Security\DbUser' +username-field: 'username' firewalls: - pattern: /index.php diff --git a/app/res/config/update.yml b/app/res/config/update.yml index 4dc86c0..2a22c9a 100644 --- a/app/res/config/update.yml +++ b/app/res/config/update.yml @@ -1,6 +1,7 @@ source: 'git@github.com:/Trehinos/Thor.git' +branch: dev composer-command: ~/composer.phar composer-options: '--ignore-platform-reqs' migration-folder: migrations/ -migration-index: 0 +migration-index: 2 after-update: thor/after-update diff --git a/app/res/static/assets/thor/thor.general.js b/app/res/static/assets/thor/thor.general.js index 8d7c13d..454acae 100644 --- a/app/res/static/assets/thor/thor.general.js +++ b/app/res/static/assets/thor/thor.general.js @@ -13,4 +13,17 @@ function loadPage(url, params, callback) { callback(response); } }); -} \ No newline at end of file +} + +const Throttle = { + handler: null, + handle: (func, timeout) => { + if (this.handler !== null) { + clearTimeout(this.handler); + } + this.handler = setTimeout(() => { + func(); + this.handler = null; + }, timeout); + } +}; diff --git a/app/res/static/assets/vendors b/app/res/static/assets/vendors new file mode 160000 index 0000000..a6d71f6 --- /dev/null +++ b/app/res/static/assets/vendors @@ -0,0 +1 @@ +Subproject commit a6d71f602067b8ed66473777df941155b628cf41 diff --git a/app/res/static/commands.yml b/app/res/static/commands.yml index 9d152b4..a25a82c 100644 --- a/app/res/static/commands.yml +++ b/app/res/static/commands.yml @@ -177,4 +177,4 @@ clear/logs: route/list: class: Thor\Framework\Commands\Http\RoutesList - description: 'list all routes in app/res/static/routes.yml' \ No newline at end of file + description: 'list all routes in app/res/static/routes.yml' diff --git a/app/res/static/menu.yml b/app/res/static/menu.yml index 8371715..942aca3 100644 --- a/app/res/static/menu.yml +++ b/app/res/static/menu.yml @@ -3,68 +3,16 @@ route: index-page key: CTRL+F2 - label: Configurations - route: config-not-implemented icon: cogs group: - label: Configuration générale route: config-config icon: cog - - icon: database - route: database-config - label: Bases de données - - icon: shield-alt - route: security-config - label: Securité - - icon: sync - route: config-not-implemented - label: Mises à jour - disabled: true - - icon: image - route: config-not-implemented - label: Twig et assets - disabled: true - divider - - label: Données statiques - icon: file-alt - route: config-not-implemented - disabled: true - - icon: atom - route: config-not-implemented - label: Kernels - disabled: true - - icon: globe - route: config-not-implemented - label: Langues - disabled: true - icon: gavel route: manage-permissions label: users_permissions - - icon: cogs - route: config-not-implemented - label: Paramètres utilisateurs - divider - - icon: play - route: config-not-implemented - label: Tâches - disabled: true - - icon: terminal - route: config-not-implemented - label: Commandes - disabled: true - - divider - - icon: route - route: config-not-implemented - label: Routes - disabled: true - - icon: list - route: config-not-implemented - label: Menu - disabled: true - - divider - - label: Données - icon: database - route: config-not-implemented - disabled: true - label: users icon: users route: users @@ -73,15 +21,6 @@ route: users-table label: Tableau utilisateurs (test) - divider - - icon: cube - route: config-not-implemented - label: Classes PdoTables - disabled: true - - icon: table - route: config-not-implemented - label: Crud - disabled: true - - divider - label: documentation icon: books route: documentation diff --git a/app/res/static/migrations/1-default-create-user.sql b/app/res/static/migrations/1-default-create-user.sql new file mode 100644 index 0000000..3d60855 --- /dev/null +++ b/app/res/static/migrations/1-default-create-user.sql @@ -0,0 +1,16 @@ +CREATE TABLE `user` +( + `public_id` varchar(255) NOT NULL, + `id` int(10) NOT NULL AUTO_INCREMENT, + `username` varchar(255) NOT NULL, + `hash` varchar(255) NOT NULL, + `permissions` JSON NOT NULL, + `parameters` JSON NOT NULL, + + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_public_id` (`public_id`), + UNIQUE KEY `uniq_username` (`username`) +) DEFAULT CHARSET=utf8mb4; + + + diff --git a/app/res/static/migrations/2-default-insert-user.sql b/app/res/static/migrations/2-default-insert-user.sql new file mode 100644 index 0000000..3456db2 --- /dev/null +++ b/app/res/static/migrations/2-default-insert-user.sql @@ -0,0 +1,2 @@ +INSERT INTO `user` (public_id, username, hash, permissions, parameters) +VALUES (HEX(RANDOM_BYTES(16)), 'admin', PASSWORD('password'), '{}', '{}'); diff --git a/app/res/static/migrations/migration_1.yml b/app/res/static/migrations/migration_1.yml deleted file mode 100644 index 88ac853..0000000 --- a/app/res/static/migrations/migration_1.yml +++ /dev/null @@ -1,2 +0,0 @@ -default: - - "UPDATE user SET username='admin' WHERE username='admini'" \ No newline at end of file diff --git a/app/res/views/login.html.twig b/app/res/views/login.html.twig index 3a3befd..d888ad0 100644 --- a/app/res/views/login.html.twig +++ b/app/res/views/login.html.twig @@ -9,38 +9,57 @@ #menu { display: none; } + #footer { position: fixed; width: 100%; text-align: center; height: 60px; } + #content { margin: 0; padding-top: 64px; width: 100%; } + + body { + background: #333; + color: #fff; + } {% endblock %} {% block body_content %} -
-

-
- {{ "users.login.connect"|_({appName}) }} -

-
-
- - -
-
- - +
+
+
+
+
+

+ {{ icon('bolt', 'fad', true, '--fa-secondary-opacity: 1; --fa-primary-color: #fe4; --fa-secondary-color: #fc4;') }}
+ {{ appName }} +

+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
- - +
{% endblock %} diff --git a/app/res/views/menu.html.twig b/app/res/views/menu.html.twig index c534f3f..98c2797 100644 --- a/app/res/views/menu.html.twig +++ b/app/res/views/menu.html.twig @@ -93,9 +93,9 @@ function menuClick($elem) { let url = $elem.data("url"); - unselect($("#menu ul li a")); loadPage(url, null, (response) => { $elem.addClass("active"); + unselect($("#menu ul li a")); if ($elem.hasClass("dropdown-item")) { $elem.parent().parent().parent().find("a.dropdown-toggle").addClass("active"); } diff --git a/app/res/views/thor/configuration/database.html.twig b/app/res/views/thor/configuration/database.html.twig deleted file mode 100644 index 6e0cdc1..0000000 --- a/app/res/views/thor/configuration/database.html.twig +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "thor/pages/base.html.twig" %} - -{% block titlebar %}{{ icon('database', 'fas', true) }} Bases de données{% endblock %} - -{% block toolbar %} - {% if writable %} - - - {% endif %} -{% endblock %} - -{% block page %} -
- {% if not writable %} -
- {{ icon('exclamation-triangle') }} Le fichier {{ filename }} n'est pas accessible - en écriture. Impossible de modifier la configuration. -
- {% else %} - - {% for name, database in databases %} -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- {% if status[name] %} - - {{ icon('check', fixed= true) }} ok - - {% else %} - - {{ icon('times', fixed= true) }} ko - - {% endif %} -
-
-
- {% endfor %} - {% endif %} -
-{% endblock %} diff --git a/app/res/views/thor/configuration/security.html.twig b/app/res/views/thor/configuration/security.html.twig deleted file mode 100644 index 3bc0e08..0000000 --- a/app/res/views/thor/configuration/security.html.twig +++ /dev/null @@ -1,77 +0,0 @@ -{% extends "thor/pages/base.html.twig" %} - -{% block titlebar %}{{ icon('shield-alt', 'fas', true) }} Sécurité{% endblock %} - -{% block toolbar %} - {% if writable %} - - {% endif %} -{% endblock %} - -{% block page %} -
- {% if not writable %} -
- {{ icon('exclamation-triangle') }} Le fichier {{ filename }} n'est pas accessible - en écriture. Impossible de modifier la configuration. -
- {% else %} -
-
-
- -
-
-
- Activer ou désactiver la sécurité web -
-
-
-
- -
-
- -
-
- Nom de la connexion BDD -
-
-
-
- -
-
- -
-
- Méthode à appeler pour construire le contexte de sécurité. Format : Class:method
- Permet à Thor d'instancier les ProviderInterface et AuthenticatorInterface. -
-
-
-

Firewalls

- {% for firewall in security.firewalls %} - - {{ dump(firewall) }} - {% else %} -

Aucun firewall défini...

- {% endfor %} - {% include "thor/not-implemented.html.twig" %} - {% endif %} -
-{% endblock %} diff --git a/app/res/views/thor/pages/changelog.html.twig b/app/res/views/thor/pages/changelog.html.twig index f77bce8..b5a26b6 100644 --- a/app/res/views/thor/pages/changelog.html.twig +++ b/app/res/views/thor/pages/changelog.html.twig @@ -42,7 +42,38 @@
- Version RC [0.9.6] current + Release [1.0.0] on-dev +
+
+
    +
  • + {{ icon('sticky-note') }} + Doing : Web AjaxController and JS +
  • +
  • + {{ icon('sticky-note') }} + Todo : Language consistency +
  • +
  • + {{ icon('sticky-note') }} + Todo : Documentation +
  • +
  • + {{ icon('palette') }} + Login page and change password modal +
  • +
  • + {{ icon('minus-square') }} + Dropped : Thor configuration from the web client +
  • +
+
+
+
+
+
+
+ Version RC [0.9.6] 2022-06-03
    @@ -91,22 +122,6 @@ {{ icon('sync') }} Cli/Console rewritten -
  • - {{ icon('sticky-note') }} - Doing : Thor configuration from the web client -
  • -
  • - {{ icon('sticky-note') }} - Doing : Web AjaxController and JS -
  • -
  • - {{ icon('sticky-note') }} - Todo : Language consistency -
  • -
  • - {{ icon('sticky-note') }} - Todo : Documentation -
@@ -159,7 +174,7 @@
  • {{ icon('bug') }} - code/update command fixed + core/update command fixed
  • {{ icon('minus-square') }} diff --git a/app/res/views/thor/pages/users.html.twig b/app/res/views/thor/pages/users.html.twig index cbf963c..ab9ffdc 100644 --- a/app/res/views/thor/pages/users.html.twig +++ b/app/res/views/thor/pages/users.html.twig @@ -19,8 +19,8 @@
    {{ icon('user', 'fa-2x fas') }} {{ user.identifier }} - #{{ user.publicId|slice(0, 8) }}
    - {{ user.publicId|slice(9) }} + {{ user.publicId|slice(0, 9) }}
    + #{{ user.publicId }}
    {% if authorized('manage-user', 'edit-user') %} - - - - {{ DICT.users.min_size }} +
    +
    + + + {{ DICT.users.min_size }} +
    +
    + + +
    -
    - - +
    + {{ icon('exclamation-triangle fa-lg') }} {{ DICT.users.password_warning|raw }}
    - {{ icon('exclamation-triangle') }} {{ DICT.users.password_warning|raw }} {% endblock %} @@ -50,7 +54,7 @@ > {{ DICT.users.generated_password }} - - diff --git a/thor/Framework/Security/HttpSecurity.php b/thor/Framework/Security/HttpSecurity.php index 8756975..5a0e229 100644 --- a/thor/Framework/Security/HttpSecurity.php +++ b/thor/Framework/Security/HttpSecurity.php @@ -20,13 +20,17 @@ class HttpSecurity extends Security /** * @param PdoRequester $requester * @param array $firewalls + * @param class-string $className + * @param string $usernameField */ public function __construct( PdoRequester $requester, - array $firewalls = [] + array $firewalls = [], + string $className = DbUser::class, + string $usernameField = 'username' ) { parent::__construct( - new DatabaseUserProvider(new CrudHelper(DbUser::class, $requester), 'username'), + new DatabaseUserProvider(new CrudHelper($className, $requester), $usernameField), new SessionAuthenticator(), $firewalls ); diff --git a/thor/Globals.php b/thor/Globals.php index ef8e02f..c3891c5 100644 --- a/thor/Globals.php +++ b/thor/Globals.php @@ -16,65 +16,65 @@ final class Globals /** * The project's folder. */ - const CODE_DIR = __DIR__ . '/../'; + public const CODE_DIR = __DIR__ . '/../'; /** * The folder where lies binary files and Cli scripts. */ - const BIN_DIR = self::CODE_DIR . 'bin/'; + public const BIN_DIR = self::CODE_DIR . 'bin/'; /** * The folder of resource files. */ - const RESOURCES_DIR = self::CODE_DIR . 'app/res/'; + public const RESOURCES_DIR = self::CODE_DIR . 'app/res/'; /** * The folder where lies the Thor's configuration. */ - const CONFIG_DIR = self::RESOURCES_DIR . 'config/'; + public const CONFIG_DIR = self::RESOURCES_DIR . 'config/'; /** * This folder contains static data of Thor. */ - const STATIC_DIR = self::RESOURCES_DIR . 'static/'; + public const STATIC_DIR = self::RESOURCES_DIR . 'static/'; /** - * The folder where lies web files. + * The folder where lies thor sources. */ - const THOR_DIR = self::CODE_DIR . 'thor/'; + public const THOR_DIR = self::CODE_DIR . 'thor/'; /** * The folder where lies web files. */ - const WEB_DIR = self::CODE_DIR . 'web/'; + public const WEB_DIR = self::CODE_DIR . 'web/'; /** * The default folder where lies vendor libraries. * * This files can be deleted without implication on project's features. */ - const VENDORS_DIR = self::CODE_DIR . 'vendors/'; + public const VENDORS_DIR = self::CODE_DIR . 'vendors/'; /** * The folder where lies var files. * * This files can be deleted without implication on Thor's or project's features. */ - const VAR_DIR = self::CODE_DIR . 'var/'; + public const VAR_DIR = self::CODE_DIR . 'var/'; /** * Substitutes `{CONST_NAME}_DIR` in **$pathString** with corresponding `Globals::CONST_NAME_DIR`. * * ### Example : * ```php - * Globals::path('{VAR}exports/export.xlsx'); // === Globals::VAR_DIR . 'exports/export.xlsx' + * Globals::path('{VAR}exports/export.xlsx'); // === realpath(Globals::VAR_DIR) . 'exports/export.xlsx' * ``` * * @param string $pathString * * @return string * - * @example Globals::path('{VAR}foo/bar.baz') === Globals::VAR_DIR . 'foo/bar.baz' + * @example Globals::path('{VAR}foo/bar.baz') === realpath(Globals::VAR_DIR) . 'foo/bar.baz' * * @see Strings::interpolate() * diff --git a/thor/Http/Client/CurlClient.php b/thor/Http/Client/CurlClient.php index 8441265..e39d0e6 100644 --- a/thor/Http/Client/CurlClient.php +++ b/thor/Http/Client/CurlClient.php @@ -8,7 +8,8 @@ Request\HttpMethod, Response\HttpStatus, Request\RequestInterface, - Response\ResponseInterface}; + Response\ResponseInterface +}; /** * Provides an implementation of ClientInterface to send requests with Curl. @@ -89,7 +90,11 @@ public function prepare(RequestInterface $request): self }, ]); if ($this->preparedRequest->getMethod() !== HttpMethod::GET) { - curl_setopt($this->curl, CURLOPT_POSTFIELDS, $this->preparedRequest->getBody()->getContents()); + curl_setopt( + $this->curl, + CURLOPT_POSTFIELDS, + json_decode($this->preparedRequest->getBody()->getContents(), true) + ); } if (!in_array($request->getMethod(), [HttpMethod::GET, HttpMethod::POST])) { @@ -111,7 +116,7 @@ public function prepare(RequestInterface $request): self public static function toHeadersLines(array $headers): array { return array_map( - fn (string $key, array|string $value) => "$key: " . (is_string($value) ? $value : implode(', ', $value)), + fn(string $key, array|string $value) => "$key: " . (is_string($value) ? $value : implode(', ', $value)), array_keys($headers), array_values($headers), ); diff --git a/thor/Http/Request/UploadedFile.php b/thor/Http/Request/UploadedFile.php index e40103f..5924ff2 100644 --- a/thor/Http/Request/UploadedFile.php +++ b/thor/Http/Request/UploadedFile.php @@ -67,6 +67,9 @@ public static function normalizeFiles(array $files): array private static function createUploadedFileFromSpec(array $value): UploadedFile|array { if (is_array($value['tmp_name'])) { + if ($value['tmp_name'][0] === '') { + return []; + } return self::normalizeNestedFileSpec($value); } return new self( diff --git a/thor/Process/Command.php b/thor/Process/Command.php index 0a4e9f7..e1e86c4 100644 --- a/thor/Process/Command.php +++ b/thor/Process/Command.php @@ -122,7 +122,8 @@ public function usage(bool $full = true): void if ($option->cumulative) { $console->echoes(Mode::BRIGHT, Color::FG_RED, '+'); } elseif ($option->hasValue) { - $console->echoes(Mode::BRIGHT, Color::FG_RED, ' ', strtoupper($option->name)); + $console->write('='); + $console->echoes(Mode::BRIGHT, Color::FG_RED, '', strtoupper($option->name)); } $console->writeln(); } @@ -147,6 +148,9 @@ public function usage(bool $full = true): void /** * @throws CommandError + * + * TODO : refactor in CommandParser + * @see CommandParser */ #[ArrayShape(['command' => "mixed|null|string", 'arguments' => "array", 'options' => "array"])] public function parse(array $commandLineArguments): array @@ -205,4 +209,4 @@ public function parse(array $commandLineArguments): array ]; } -} \ No newline at end of file +} diff --git a/thor/Stream/Stream.php b/thor/Stream/Stream.php index 7833ae3..349fdff 100644 --- a/thor/Stream/Stream.php +++ b/thor/Stream/Stream.php @@ -20,7 +20,7 @@ class Stream implements StreamInterface private const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/'; /** - * @param resource + * @param resource $stream * @param array $options * * @throws InvalidArgumentException diff --git a/thor/Tools/Email/Email.php b/thor/Tools/Email/Email.php index b414369..96005da 100644 --- a/thor/Tools/Email/Email.php +++ b/thor/Tools/Email/Email.php @@ -23,16 +23,16 @@ class Email extends Part * * @param string $subject * @param string $from - * @param array $additionnalHeaders + * @param array $additionalHeaders * @param string|null $message * * @throws Exception */ public function __construct( - private string $subject, - private string $from, - array $additionnalHeaders = [], - ?string $message = null + private readonly string $subject, + private readonly string $from, + array $additionalHeaders = [], + ?string $message = null ) { $this->boundary = Guid::base64(); $headers = new Headers( @@ -41,9 +41,9 @@ public function __construct( ); unset($headers['Content-Disposition']); $headers['MIME-Version'] = '1.0'; - $headers['X-Mailer'] = Thor::appName() . ' ' . Thor::version() . ' [' . Thor::version() . ']'; + $headers['X-Mailer'] = Thor::appName() . ' ' . Thor::versionName() . ' [' . Thor::version() . ']'; array_walk( - $additionnalHeaders, + $additionalHeaders, function (string $value, string $name) use ($headers) { $headers[$name] = $value; } @@ -99,20 +99,20 @@ public function __toString(): string public function getBody(): string { return "\r\n\r\n--$this->boundary\r\n" . - implode( - "\r\n\r\n--$this->boundary\r\n", - array_map( - fn(Part $part) => "$part", - $this->parts - ) - ) . - "\r\n\r\n--$this->boundary--\r\n"; + implode( + "\r\n\r\n--$this->boundary\r\n", + array_map( + fn(Part $part) => "$part", + $this->parts + ) + ) . + "\r\n\r\n--$this->boundary--\r\n"; } /** * Sends the email. Returns false if the mail() instruction fails. * - * @param string|array $to + * @param string|array $to * @param string|array|null $cc * @param string|array|null $bcc * diff --git a/thor/Tools/Email/Headers.php b/thor/Tools/Email/Headers.php index d57b092..a8f8439 100644 --- a/thor/Tools/Email/Headers.php +++ b/thor/Tools/Email/Headers.php @@ -21,17 +21,17 @@ final class Headers implements ArrayAccess * Construct Headers with most important headers. * * @param string $contentType - * @param string $transfertEncoding + * @param string $transferEncoding * @param string $contentDisposition */ public function __construct( string $contentType = self::TYPE_HTML, - string $transfertEncoding = '7bit', + string $transferEncoding = '7bit', string $contentDisposition = 'inline' ) { $this->headers = [ 'Content-Type' => $contentType, - 'Content-Transfer-Encoding' => $transfertEncoding, + 'Content-Transfer-Encoding' => $transferEncoding, 'Content-Disposition' => $contentDisposition, ]; }