+ + \ No newline at end of file diff --git a/App/Package/Core/Views/Theme/Editor/config-theme.readme.md b/App/Package/Core/Views/Theme/Editor/config-theme.readme.md new file mode 100644 index 00000000..14a212a2 --- /dev/null +++ b/App/Package/Core/Views/Theme/Editor/config-theme.readme.md @@ -0,0 +1,125 @@ +# 📄 Documentation de configuration des thèmes (EditorMenu) + +Ce fichier permet de définir dynamiquement les options de personnalisation d'un thème via l'éditeur visuel. +Chaque menu (EditorMenu) contient un groupe de valeurs (EditorValue), affichables dans le builder et modifiables en live. + +--- + +## 🔖 Structure générale + +```php +new EditorMenu( + title: 'Nom affiché dans l’UI', + key: 'identifiant_unique', + scope: 'chemin/url/optionnel', + requiredPackage: 'NomDuPackage' // ou null + values: [EditorValue(...), ...] +) +``` + +Chaque `EditorValue` définit une clé, un type, une valeur par défaut, et parfois des options (ex: select ou range). + +--- + +## 💡 Types disponibles + +| Type | Constante | Description | +|--------------|---------------------------|-----------------------------------------------------------------------------| +| Texte | `EditorType::TEXT` | Champ de texte libre | +| Zone de texte| `EditorType::TEXTAREA` | Texte multilignes | +| Nombre | `EditorType::NUMBER` | Nombre sans unité | +| Booléen | `EditorType::BOOLEAN` | Case à cocher (on/off) | +| Couleur | `EditorType::COLOR` | Picker de couleur | +| Image | `EditorType::IMAGE` | Upload et prévisualisation d’image | +| CSS libre | `EditorType::CSS` | Zone personnalisable CSS | +| Liste | `EditorType::SELECT` | Menu déroulant avec options définies | +| Curseur | `EditorType::RANGE` | Slider personnalisable (avec min, max, step, prefix/suffix) | + +--- + +## 🎨 Exemples d'utilisation dans les vues + +### 🔍 Remplacer un texte dynamiquement +```html +

+``` + +### 🌈 Appliquer une couleur dynamique +```html + +``` + +### 🖌️ Appliquer des classes dynamiques (ex: font, layout) +```html + +``` + +### 🚀 Gérer un attribut dynamique (ex: image) +```html + +``` + +### 📉 Afficher ou masquer un bloc +```html +
...
+``` + +--- + +## 🔄 Exemple complet : Menu Global +```php +new EditorMenu( + title: 'Globaux', + key: 'global', + scope: null, + requiredPackage: null, + values: [ + new EditorValue( + title: 'Police', + themeKey: 'main_font', + defaultValue: 'font-montserrat', + type: EditorType::SELECT, + selectOptions: [ new EditorSelectOptions(value: 'font-angkor', text: 'Angkor'), ... ] + ), + new EditorValue( + title: 'Image du site', + themeKey: 'site_image', + defaultValue: 'Config/Default/Images/logo.png', + type: EditorType::IMAGE + ), + new EditorValue( + title: 'Taille image', + themeKey: 'site_image_width', + defaultValue: '40', + type: EditorType::RANGE, + rangeOptions: [ new EditorRangeOptions(0, 256, 1, '', 'px') ] + ), + new EditorValue( + title: 'ALT Image', + themeKey: 'image_alt', + defaultValue: 'logo.png', + type: EditorType::TEXT + ) + ] +) +``` + +--- + +## 🌍 Utilisation dans les fichiers de thème + +- Les valeurs sont injectées automatiquement via `View::replaceThemeValues()` en public +- En mode éditeur, les attributs `data-cmw-*` permettent une mise à jour live avec JS + +--- + +## 📚 Bonnes pratiques + +- Toujours garder des `themeKey` uniques par menu +- Utiliser des `prefix/suffix` pour les classes et unités CSS dans les sliders +- Prévoir des valeurs par défaut réalistes pour chaque champ +- Garder une logique prévisible pour le builder (polices, couleurs, tailles, visibilité) + +--- + +> ✉️ Pour toute extension ou suggestion : pensez à documenter les nouveaux types ou comportements dans ce fichier. \ No newline at end of file diff --git a/App/Package/Core/Views/Theme/Editor/data-cmw.readme.md b/App/Package/Core/Views/Theme/Editor/data-cmw.readme.md new file mode 100644 index 00000000..1757b942 --- /dev/null +++ b/App/Package/Core/Views/Theme/Editor/data-cmw.readme.md @@ -0,0 +1,54 @@ +# ✨ Utilisation de `data-cmw` + +Cette documentation résume toutes les possibilités offertes par le système `data-cmw` pour créer des thèmes dynamiques, compatibles avec l'éditeur visuel du CMS. + +--- + +## 📄 Attributs disponibles + +| Attribut HTML | Syntaxe | Description | Exemple | +|---------------------------|-----------------------------------------------|-------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| `data-cmw` | `menu:key` | Remplace le contenu textuel (`textContent`) de l'élément | `` → `Mon Site` | +| `data-cmw-attr` | `attribut:menu:key [...]` | Modifie un ou plusieurs attributs HTML (`src`, `href`, `alt`, etc.) | `` | +| `data-cmw-style` | `propriété:menu:key [...]` | Applique dynamiquement un ou plusieurs styles CSS (avec gestion de `url()`) | `
` → `style="background-image: url(...)"` | +| `data-cmw-class` | `menu:key [...]` | Ajoute dynamiquement une ou plusieurs classes CSS | `` → `font-montserrat px-2` | +| `data-cmw-visible` | `menu:key` | Affiche ou masque l'élément selon une valeur booléenne | `
` | +| `__CMW:menu:key__` | `__CMW:menu:key__` | Remplace dynamiquement dans les scripts JS / JSON | `const max = __CMW:game:max_players__` | + +--- + +## 🧠 Règles intelligentes de transformation automatique + +| Type de valeur dans la config | Traitement automatique | +|-------------------------------|------------------------------------------------------------| +| `#ffffff` ou `red` | Utilisé directement pour les propriétés de couleur | +| `https://...` | Converti automatiquement en `url('...')` pour les backgrounds | +| `font-montserrat` | Utilisé tel quel comme class CSS | +| `0`, "0" ou `""` | Cache les éléments avec `data-cmw-visible` | + +--- + +## 📆 Exemple complet d'utilisation + +```html +
+

+
+``` + +--- + +## 💡 Bonnes pratiques + +- Toujours préfixer les valeurs dans `configValues` avec `menu_key`, exemple : `site_title` dans le menu `header` +- Prévoir des valeurs par défaut pour chaque champ configuré dans le thème + +--- + +Pour toute extension de ce système, pensez à respecter la convention `data-cmw-*` afin de garantir la compatibilité avec le builder dynamique. + diff --git a/App/Package/Core/Views/Theme/Editor/template.php b/App/Package/Core/Views/Theme/Editor/template.php new file mode 100644 index 00000000..25fea0da --- /dev/null +++ b/App/Package/Core/Views/Theme/Editor/template.php @@ -0,0 +1,98 @@ + +getValue('DIR') . 'Admin/Resources/Views/Includes/head.inc.php'); + +/* INCLUDE SCRIPTS / STYLES */ +/* @var $includes */ +/* @var $content */ +View::loadInclude($includes, 'beforeScript'); +View::loadInclude($includes, 'styles'); + +include_once (EnvManager::getInstance()->getValue('DIR') . 'App/Package/Core/Views/Theme/Editor/Includes/header.inc.php'); + +echo $content; + +include_once (EnvManager::getInstance()->getValue('DIR') . 'App/Package/Core/Views/Theme/Editor/Includes/footer.inc.php'); +/* INCLUDE SCRIPTS */ +View::loadInclude($includes, 'afterScript'); +?> + + + + \ No newline at end of file diff --git a/App/Package/Core/Views/Theme/Editor/themeManage.admin.view.php b/App/Package/Core/Views/Theme/Editor/themeManage.admin.view.php new file mode 100644 index 00000000..5f8a19d6 --- /dev/null +++ b/App/Package/Core/Views/Theme/Editor/themeManage.admin.view.php @@ -0,0 +1,634 @@ +values as $value) { + $key = $menu->key . '_' . $value->themeKey; + + $editorSettings[$key] = [ + 'default' => $value->defaultValue, + 'type' => $value->type, + ]; + + if ($value->type === EditorType::RANGE && isset($value->rangeOptions[0])) { + $range = $value->rangeOptions[0]; + $editorSettings[$key]['prefix'] = $range->getPrefix(); + $editorSettings[$key]['suffix'] = $range->getSuffix(); + $editorSettings[$key]['min'] = $range->getMin(); + $editorSettings[$key]['max'] = $range->getMax(); + $editorSettings[$key]['step'] = $range->getStep(); + } + } +} + +Website::setTitle(LangManager::translate('core.theme.manage.title', ['Theme' => ThemeManager::getInstance()->getCurrentTheme()->name()])); +Website::setDescription(LangManager::translate('core.theme.manage.description')); + +?> +
+ +
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/App/Package/Core/Views/Theme/themeManage.admin.view.php b/App/Package/Core/Views/Theme/themeManage.admin.view.php deleted file mode 100644 index e1a0c036..00000000 --- a/App/Package/Core/Views/Theme/themeManage.admin.view.php +++ /dev/null @@ -1,159 +0,0 @@ - ThemeManager::getInstance()->getCurrentTheme()->name()])); -Website::setDescription(LangManager::translate('core.theme.manage.description')); -?> - -
-

getCurrentTheme()->name() ?>

-
- -
- -
-
-
- -
-
- insertHiddenToken() ?> -
- getCurrentThemeConfigFile(); ?> -
-
-
- - - - - - \ No newline at end of file diff --git a/App/Package/Users/Controllers/Admin/Settings/UsersAdminSettingsBlacklistController.php b/App/Package/Users/Controllers/Admin/Settings/UsersAdminSettingsBlacklistController.php new file mode 100644 index 00000000..b850a845 --- /dev/null +++ b/App/Package/Users/Controllers/Admin/Settings/UsersAdminSettingsBlacklistController.php @@ -0,0 +1,146 @@ +getBlacklistedPseudos(); + + View::createAdminView('Users', 'Settings/blacklist') + ->addStyle('Admin/Resources/Assets/Css/simple-datatables.css') + ->addScriptAfter('Admin/Resources/Vendors/Simple-datatables/simple-datatables.js', + 'Admin/Resources/Vendors/Simple-datatables/config-datatables.js') + ->addVariableList(['pseudos' => $blacklistedPseudo]) + ->view(); + } + + #[NoReturn] + #[Link('/settings/blacklist/pseudo', Link::POST, [], '/cmw-admin/users')] + private function pseudoBlacklistPost(): void + { + UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings.blacklist.add'); + + if (empty($_POST['pseudo'])) { + Redirect::redirectPreviousRoute(); + } + + $pseudo = FilterManager::filterInputStringPost('pseudo'); + + if (UsersSettingsModel::getInstance()->addBlacklistedPseudo($pseudo)) { + Flash::send( + Alert::SUCCESS, + LangManager::translate('core.toaster.success'), + LangManager::translate('users.settings.blacklisted.pseudo.toasters.add.success', ['pseudo' => $pseudo]), + ); + } else { + Flash::send( + Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('users.settings.blacklisted.pseudo.toasters.add.error', ['pseudo' => $pseudo]), + ); + } + + Redirect::redirectPreviousRoute(); + } + + #[NoReturn] + #[Link('/settings/blacklist/pseudo/edit/:id', Link::POST, ['id' => '[0-9]+'], '/cmw-admin/users')] + private function editPseudoBlacklistPost(int $id): void + { + UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings.blacklist.edit'); + + if (empty($_POST['pseudo'])) { + Redirect::redirectPreviousRoute(); + } + + $pseudo = filter_input(INPUT_POST, 'pseudo'); + + if (UsersSettingsModel::getInstance()->editBlacklistedPseudo($id, $pseudo)) { + Flash::send( + Alert::SUCCESS, + LangManager::translate('core.toaster.success'), + LangManager::translate('users.settings.blacklisted.pseudo.toasters.edit.success', ['pseudo' => $pseudo]) + ); + } else { + Flash::send( + Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('users.settings.blacklisted.pseudo.toasters.edit.error', ['pseudo' => $pseudo]) + ); + } + + Redirect::redirectPreviousRoute(); + } + + #[NoReturn] + #[Link('/settings/blacklist/pseudo/delete/:id', Link::GET, ['id' => '[0-9]+'], '/cmw-admin/users')] + private function deletePseudoBlacklisted(int $id): void + { + UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings.blacklist.delete'); + + if (UsersSettingsModel::getInstance()->removeBlacklistedPseudo($id)) { + Flash::send( + Alert::SUCCESS, + LangManager::translate('core.toaster.success'), + LangManager::translate('users.settings.blacklisted.pseudo.toasters.delete.success'), + ); + } else { + Flash::send( + Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('users.settings.blacklisted.pseudo.toasters.delete.error'), + ); + } + + Redirect::redirectPreviousRoute(); + } + + #[NoReturn] + #[Link('/settings/blacklist/pseudo/delete/bulk', Link::POST, [], '/cmw-admin/users', secure: false)] + private function adminDeleteSelectedPost(): void + { + UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings.blacklist.delete'); + + $selectedIds = $_POST['selectedIds']; + + if (empty($selectedIds)) { + Flash::send(Alert::ERROR, 'Blacklist', 'Aucun pseudo sélectionné'); + Redirect::redirectPreviousRoute(); + } + + $i = 0; + foreach ($selectedIds as $selectedId) { + $selectedId = FilterManager::filterData($selectedId, 11, FILTER_SANITIZE_NUMBER_INT); + UsersSettingsModel::getInstance()->removeBlacklistedPseudo($selectedId); + $i++; + } + Flash::send(Alert::SUCCESS, 'Blacklist', "$i pseudos supprimé !"); + + Redirect::redirectPreviousRoute(); + } +} diff --git a/App/Package/Users/Controllers/Admin/Settings/UsersAdminSettingsGeneralController.php b/App/Package/Users/Controllers/Admin/Settings/UsersAdminSettingsGeneralController.php new file mode 100644 index 00000000..2db13643 --- /dev/null +++ b/App/Package/Users/Controllers/Admin/Settings/UsersAdminSettingsGeneralController.php @@ -0,0 +1,120 @@ +addVariableList(['settings' => UserSettingsEntity::getInstance()]) + ->view(); + } + + #[NoReturn] #[Link('/settings/general', Link::POST, [], '/cmw-admin/users')] + private function settingsPost(): void + { + UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings'); + + $profilePage = FilterManager::filterInputStringPost('profile_page'); + + $settingsStatus = UsersSettingsModel::getInstance()->bulkUpdateSettings( + new BulkSettingsEntity('profilePage', $profilePage), + ); + + if (!$settingsStatus) { + Flash::send( + Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('core.toaster.config.error'), + ); + } else { + Flash::send( + Alert::SUCCESS, + LangManager::translate('core.toaster.success'), + LangManager::translate('core.toaster.config.success'), + ); + } + + Redirect::redirectPreviousRoute(); + } + + #[NoReturn] #[Link('/settings/general/image/reset', Link::GET, [], '/cmw-admin/users')] + private function resetDefaultImg(): void + { + UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings'); + + if (!UsersSettingsModel::getInstance()->updateSetting('defaultImage', 'defaultImage.jpg')) { + Flash::send( + Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('core.toaster.config.error'), + ); + } else { + + Flash::send( + Alert::SUCCESS, + LangManager::translate('core.toaster.success'), + LangManager::translate('core.toaster.config.success'), + ); + } + Redirect::redirectPreviousRoute(); + } + + #[NoReturn] #[Link('/settings/general/image', Link::POST, [], '/cmw-admin/users')] + private function settingsImagePost(): void + { + UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings'); + + if (!isset($_FILES['defaultPicture']) || $_FILES['defaultPicture']['error'] !== 0) { + Flash::send( + Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('core.toaster.config.error'), + ); + Redirect::redirectPreviousRoute(); + } + + $defaultPicture = $_FILES['defaultPicture']; + + $newDefaultImage = ImagesManager::convertAndUpload($defaultPicture, 'Users/Default'); + if (!UsersSettingsModel::getInstance()->updateSetting('defaultImage', $newDefaultImage)) { + Flash::send( + Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('core.toaster.config.error'), + ); + } else { + Flash::send( + Alert::SUCCESS, + LangManager::translate('core.toaster.success'), + LangManager::translate('core.toaster.config.success'), + ); + } + + Redirect::redirectPreviousRoute(); + } +} diff --git a/App/Package/Users/Controllers/Admin/Settings/UsersAdminSettingsSecurityController.php b/App/Package/Users/Controllers/Admin/Settings/UsersAdminSettingsSecurityController.php new file mode 100644 index 00000000..71cc6dad --- /dev/null +++ b/App/Package/Users/Controllers/Admin/Settings/UsersAdminSettingsSecurityController.php @@ -0,0 +1,98 @@ +getRoles(); + $settings = UserSettingsEntity::getInstance(); + + View::createAdminView('Users', 'Settings/security') + ->addVariableList(['settings' => $settings, 'roles' => $roles]) + ->view(); + } + + #[NoReturn] #[Link('/settings/security', Link::POST, [], '/cmw-admin/users')] + private function settingsPost(): void + { + UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings'); + + $resetPasswordMethod = FilterManager::filterInputStringPost('reset_password_method'); + $securityReinforced = FilterManager::filterInputStringPost('security_reinforced'); + + $settingsStatus = UsersSettingsModel::getInstance()->bulkUpdateSettings( + new BulkSettingsEntity('resetPasswordMethod', $resetPasswordMethod), + new BulkSettingsEntity('securityReinforced', $securityReinforced) + ); + + if (!$settingsStatus) { + Flash::send( + Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('core.toaster.config.error'), + ); + Redirect::redirectPreviousRoute(); + } + + $listEnforcedToggle = FilterManager::filterInputStringPost('listEnforcedToggle'); + + if ($listEnforcedToggle === '1') { + if (empty($_POST['enforcedRoles'])) { + $listEnforcedToggle = 0; + if (!UsersSettingsModel::getInstance()->clearEnforcedRoles()) { + Flash::send(Alert::ERROR, 'Erreur', 'Impossible de mettre à jour les rôles imposés en 2fa !'); + Redirect::redirectPreviousRoute(); + } + } else { + if (UsersSettingsModel::getInstance()->clearEnforcedRoles()) { + foreach ($_POST['enforcedRoles'] as $roleId) { + UsersSettingsModel::getInstance()->updateEnforcedRoles($roleId); + } + } else { + Flash::send(Alert::ERROR, 'Erreur', 'Impossible de mettre à jour les rôles imposés en 2fa !'); + Redirect::redirectPreviousRoute(); + } + } + } else { + if (!UsersSettingsModel::getInstance()->clearEnforcedRoles()) { + Flash::send(Alert::ERROR, 'Erreur', 'Impossible de mettre à jour les rôles imposés en 2fa !'); + Redirect::redirectPreviousRoute(); + } + } + + UsersSettingsModel::getInstance()->updateSetting('listEnforcedToggle', $listEnforcedToggle); + + Flash::send( + Alert::SUCCESS, + LangManager::translate('core.toaster.success'), + LangManager::translate('core.toaster.config.success'), + ); + + Redirect::redirectPreviousRoute(); + } +} diff --git a/App/Package/Users/Controllers/UsersLoginController.php b/App/Package/Users/Controllers/UsersLoginController.php index 88ee74b3..5a5c0f9a 100644 --- a/App/Package/Users/Controllers/UsersLoginController.php +++ b/App/Package/Users/Controllers/UsersLoginController.php @@ -23,6 +23,7 @@ use CMW\Model\Users\UsersModel; use CMW\Model\Users\UsersSettingsModel; use CMW\Type\Users\LoginStatus; +use CMW\Utils\Date; use CMW\Utils\Redirect; use CMW\Utils\Utils; use CMW\Utils\Website; @@ -93,6 +94,16 @@ public function loginUser(UserEntity $user, bool $cookie): void } UsersModel::getInstance()->updateLoggedTime($user->getId()); + + if ((UsersSettingsModel::getInstance()->getSetting('securityReinforced') === '1')) { + if (MailModel::getInstance()->getConfig() !== null && MailModel::getInstance()->getConfig()->isEnable()) { + $ip = $_SERVER['REMOTE_ADDR']; + $date = date('Y-m-d H:i:s'); + $dateFormatted = Date::formatDate($date); + MailManager::getInstance()->sendMail($user->getMail(),Website::getWebsiteName() . LangManager::translate('users.security.connected.object'), LangManager::translate('users.security.connected.body', ['user_name' => $user->getPseudo(), 'website' => Website::getWebsiteName(), 'date' => $dateFormatted, 'ip' => $ip])); + } + } + try { Emitter::send(LoginEvent::class, $user->getId()); } catch (Exception) { @@ -148,7 +159,7 @@ private function loginPost(): void } $this->loginUser($user, $cookie); if ($previousRoute) { - Redirect::redirectPreviousRoute(); + Redirect::external($previousRoute); } Redirect::redirect('profile'); diff --git a/App/Package/Users/Controllers/UsersProfileController.php b/App/Package/Users/Controllers/UsersProfileController.php index ec854bfd..1bc73d87 100644 --- a/App/Package/Users/Controllers/UsersProfileController.php +++ b/App/Package/Users/Controllers/UsersProfileController.php @@ -4,20 +4,19 @@ use CMW\Entity\Users\UserSettingsEntity; use CMW\Event\Users\DeleteUserAccountEvent; +use CMW\Interface\Users\IUsersProfilePicture; use CMW\Manager\Events\Emitter; use CMW\Manager\Filter\FilterManager; use CMW\Manager\Flash\Alert; use CMW\Manager\Flash\Flash; use CMW\Manager\Lang\LangManager; +use CMW\Manager\Loader\Loader; use CMW\Manager\Package\AbstractController; use CMW\Manager\Router\Link; use CMW\Manager\Router\RouterException; use CMW\Manager\Security\EncryptManager; use CMW\Manager\Twofa\TwoFaManager; -use CMW\Manager\Uploads\ImagesException; -use CMW\Manager\Uploads\ImagesManager; use CMW\Manager\Views\View; -use CMW\Model\Users\UserPictureModel; use CMW\Model\Users\Users2FaModel; use CMW\Model\Users\UsersModel; use CMW\Model\Users\UsersSettingsModel; @@ -86,26 +85,17 @@ private function publicProfilePost(): void Redirect::redirectToHome(); } - if (!empty($_FILES['pictureProfile']['name'])) { - $image = $_FILES['pictureProfile']; - - try { - // Upload image on the server - $imageName = ImagesManager::convertAndUpload($image, 'Users'); - - if (!UserPictureModel::getInstance()->uploadImage($user->getId(), $imageName)) { - Flash::send(Alert::ERROR, LangManager::translate('core.toaster.error'), - LangManager::translate('core.errors.upload.image')); - Redirect::redirectPreviousRoute(); - } - } catch (ImagesException $e) { - Flash::send(Alert::ERROR, LangManager::translate('core.toaster.error'), - LangManager::translate('core.errors.upload.image') . " => $e"); - Redirect::redirectPreviousRoute(); - } + if (!isset($_FILES['pictureProfile']) || empty($_FILES['pictureProfile']['name'])) { + Flash::send( + Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('core.imageManager.error.emptyFile'), + ); + Redirect::redirectPreviousRoute(); } - Redirect::redirect('profile'); + $image = $_FILES['pictureProfile']; + Loader::getHighestImplementation(IUsersProfilePicture::class)->changeMethod($image, $user->getId()); } #[Link('/profile/:pseudo', Link::GET, ['pseudo' => '.*?'])] diff --git a/App/Package/Users/Controllers/UsersSettingsController.php b/App/Package/Users/Controllers/UsersSettingsController.php index 8126f21c..c9163a9a 100644 --- a/App/Package/Users/Controllers/UsersSettingsController.php +++ b/App/Package/Users/Controllers/UsersSettingsController.php @@ -2,222 +2,25 @@ namespace CMW\Controller\Users; -use CMW\Entity\Users\Settings\BulkSettingsEntity; -use CMW\Entity\Users\UserSettingsEntity; use CMW\Manager\Env\EnvManager; -use CMW\Manager\Filter\FilterManager; -use CMW\Manager\Flash\Alert; -use CMW\Manager\Flash\Flash; -use CMW\Manager\Lang\LangManager; use CMW\Manager\Package\AbstractController; -use CMW\Manager\Router\Link; -use CMW\Manager\Uploads\ImagesException; -use CMW\Manager\Uploads\ImagesManager; -use CMW\Manager\Views\View; -use CMW\Model\Users\RolesModel; use CMW\Model\Users\UsersSettingsModel; -use CMW\Utils\Redirect; -use CMW\Utils\Utils; -use JetBrains\PhpStorm\NoReturn; /** * Class: @UsersSettingsController * @package Users * @author CraftMyWebsite Team - * @version 0.0.1 + * @deprecated */ class UsersSettingsController extends AbstractController { + /** + * @return string + * @deprecated + */ public static function getDefaultImageLink(): string { $defaultImg = UsersSettingsModel::getInstance()->getSetting('defaultImage'); return EnvManager::getInstance()->getValue('PATH_SUBFOLDER') . 'Public/Uploads/Users/Default/' . $defaultImg; } - - /** - * @throws \CMW\Manager\Router\RouterException - */ - #[Link(path: '/', method: Link::GET, scope: '/cmw-admin/users')] - #[Link('/settings', Link::GET, [], '/cmw-admin/users')] - private function settings(): void - { - UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings'); - - $roles = RolesModel::getInstance()->getRoles(); - $blacklistedPseudo = UsersSettingsModel::getInstance()->getBlacklistedPseudos(); - - View::createAdminView('Users', 'settings') - ->addStyle('Admin/Resources/Assets/Css/simple-datatables.css') - ->addScriptAfter('Admin/Resources/Vendors/Simple-datatables/simple-datatables.js', - 'Admin/Resources/Vendors/Simple-datatables/config-datatables.js', - 'App/Package/Users/Views/Assets/Js/rolesWeights.js') - ->addVariableList(['settings' => new UserSettingsEntity(), 'roles' => $roles, 'pseudos' => $blacklistedPseudo]) - ->view(); - } - - #[NoReturn] #[Link('/settings/resetImg', Link::GET, [], '/cmw-admin/users')] - private function resetDefaultImg(): void - { - UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings'); - - UsersSettingsModel::getInstance()->updateSetting('defaultImage', 'defaultImage.jpg'); - - Flash::send(Alert::SUCCESS, LangManager::translate('core.toaster.success'), - LangManager::translate('core.toaster.config.success')); - - Redirect::redirectPreviousRoute(); - } - - #[Link('/settings', Link::POST, [], '/cmw-admin/users')] - private function settingsPost(): void - { - UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings'); - - if ($_FILES['defaultPicture']['name'] !== '') { - $defaultPicture = $_FILES['defaultPicture']; - - try { - $newDefaultImage = ImagesManager::convertAndUpload($defaultPicture, 'Users/Default'); - UsersSettingsModel::getInstance()->updateSetting('defaultImage', $newDefaultImage); - } catch (ImagesException $e) { - Flash::send(Alert::ERROR, LangManager::translate('core.toaster.error'), - LangManager::translate('core.errors.upload.image') . ' => ' . $e->getMessage()); - Redirect::redirectPreviousRoute(); - } - } - - [$resetPasswordMethod, $profilePage, $securityReinforced] = Utils::filterInput('reset_password_method', 'profile_page', 'security_reinforced'); - - $settingsStatus = UsersSettingsModel::getInstance()->bulkUpdateSettings( - new BulkSettingsEntity('resetPasswordMethod', $resetPasswordMethod), - new BulkSettingsEntity('profilePage', $profilePage), - new BulkSettingsEntity('securityReinforced', $securityReinforced) - ); - - if (!$settingsStatus) { - Flash::send(Alert::ERROR, LangManager::translate('core.toaster.error'), - LangManager::translate('core.toaster.config.error')); - Redirect::redirectPreviousRoute(); - } - - [$listEnforcedToggle] = Utils::filterInput('listEnforcedToggle'); - - if ($listEnforcedToggle === '1') { - if (empty($_POST['enforcedRoles'])) { - $listEnforcedToggle = 0; - if (!UsersSettingsModel::getInstance()->clearEnforcedRoles()) { - Flash::send(Alert::ERROR, 'Erreur', 'Impossible de mettre à jour les rôles imposer en 2fa !'); - Redirect::redirectPreviousRoute(); - } - } else { - if (UsersSettingsModel::getInstance()->clearEnforcedRoles()) { - foreach ($_POST['enforcedRoles'] as $roleId) { - UsersSettingsModel::getInstance()->updateEnforcedRoles($roleId); - } - } else { - Flash::send(Alert::ERROR, 'Erreur', 'Impossible de mettre à jour les rôles imposer en 2fa !'); - Redirect::redirectPreviousRoute(); - } - } - } else { - if (!UsersSettingsModel::getInstance()->clearEnforcedRoles()) { - Flash::send(Alert::ERROR, 'Erreur', 'Impossible de mettre à jour les rôles imposer en 2fa !'); - Redirect::redirectPreviousRoute(); - } - } - - UsersSettingsModel::getInstance()->updateSetting('listEnforcedToggle', $listEnforcedToggle); - - Flash::send(Alert::SUCCESS, LangManager::translate('core.toaster.success'), - LangManager::translate('core.toaster.config.success')); - - Redirect::redirectPreviousRoute(); - } - - #[NoReturn] - #[Link('/settings/blacklist/pseudo', Link::POST, [], '/cmw-admin/users')] - private function pseudoBlacklistPost(): void - { - UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings.blacklist.add'); - - if (empty($_POST['pseudo'])) { - Redirect::redirectPreviousRoute(); - } - - $pseudo = filter_input(INPUT_POST, 'pseudo'); - - if (UsersSettingsModel::getInstance()->addBlacklistedPseudo($pseudo)) { - Flash::send(Alert::SUCCESS, LangManager::translate('core.toaster.success'), - LangManager::translate('users.settings.blacklisted.pseudo.toasters.add.success', ['pseudo' => $pseudo])); - } else { - Flash::send(Alert::ERROR, LangManager::translate('core.toaster.error'), - LangManager::translate('users.settings.blacklisted.pseudo.toasters.add.error', ['pseudo' => $pseudo])); - } - - Redirect::redirectPreviousRoute(); - } - - #[NoReturn] - #[Link('/settings/blacklist/pseudo/edit/:id', Link::POST, ['id' => '[0-9]+'], '/cmw-admin/users')] - private function editPseudoBlacklistPost(int $id): void - { - UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings.blacklist.edit'); - - if (empty($_POST['pseudo'])) { - Redirect::redirectPreviousRoute(); - } - - $pseudo = filter_input(INPUT_POST, 'pseudo'); - - if (UsersSettingsModel::getInstance()->editBlacklistedPseudo($id, $pseudo)) { - Flash::send(Alert::SUCCESS, LangManager::translate('core.toaster.success'), - LangManager::translate('users.settings.blacklisted.pseudo.toasters.edit.success', ['pseudo' => $pseudo])); - } else { - Flash::send(Alert::ERROR, LangManager::translate('core.toaster.error'), - LangManager::translate('users.settings.blacklisted.pseudo.toasters.edit.error', ['pseudo' => $pseudo])); - } - - Redirect::redirectPreviousRoute(); - } - - #[NoReturn] - #[Link('/settings/blacklist/pseudo/delete/:id', Link::GET, ['id' => '[0-9]+'], '/cmw-admin/users')] - private function deletePseudoBlacklisted(int $id): void - { - UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings.blacklist.delete'); - - if (UsersSettingsModel::getInstance()->removeBlacklistedPseudo($id)) { - Flash::send(Alert::SUCCESS, LangManager::translate('core.toaster.success'), - LangManager::translate('users.settings.blacklisted.pseudo.toasters.delete.success')); - } else { - Flash::send(Alert::ERROR, LangManager::translate('core.toaster.error'), - LangManager::translate('users.settings.blacklisted.pseudo.toasters.delete.error')); - } - - Redirect::redirectPreviousRoute(); - } - - #[NoReturn] - #[Link('/settings/blacklist/pseudo/deleteSelected', Link::POST, [], '/cmw-admin/users', secure: false)] - private function adminDeleteSelectedPost(): void - { - UsersController::redirectIfNotHavePermissions('core.dashboard', 'users.settings.blacklist.delete'); - - $selectedIds = $_POST['selectedIds']; - - if (empty($selectedIds)) { - Flash::send(Alert::ERROR, 'Blacklist', 'Aucun pseudo sélectionné'); - Redirect::redirectPreviousRoute(); - } - - $i = 0; - foreach ($selectedIds as $selectedId) { - $selectedId = FilterManager::filterData($selectedId, 11, FILTER_SANITIZE_NUMBER_INT); - UsersSettingsModel::getInstance()->removeBlacklistedPseudo($selectedId); - $i++; - } - Flash::send(Alert::SUCCESS, 'Blacklist', "$i pseudos supprimé !"); - - Redirect::redirectPreviousRoute(); - } } diff --git a/App/Package/Users/Implementations/Users/UsersProfilePictureImplementation.php b/App/Package/Users/Implementations/Users/UsersProfilePictureImplementation.php index c7cca68f..e96f9d1b 100644 --- a/App/Package/Users/Implementations/Users/UsersProfilePictureImplementation.php +++ b/App/Package/Users/Implementations/Users/UsersProfilePictureImplementation.php @@ -3,11 +3,17 @@ namespace CMW\Implementation\Users\Users; use CMW\Controller\Users\UsersSessionsController; -use CMW\Controller\Users\UsersSettingsController; use CMW\Entity\Users\UserPictureEntity; use CMW\Interface\Users\IUsersProfilePicture; use CMW\Manager\Env\EnvManager; +use CMW\Manager\Flash\Alert; +use CMW\Manager\Flash\Flash; +use CMW\Manager\Lang\LangManager; +use CMW\Manager\Uploads\ImagesException; +use CMW\Manager\Uploads\ImagesManager; use CMW\Model\Users\UserPictureModel; +use CMW\Model\Users\UsersModel; +use CMW\Model\Users\UsersSettingsModel; use CMW\Utils\Redirect; use JetBrains\PhpStorm\NoReturn; @@ -21,7 +27,29 @@ public function weight(): int #[NoReturn] public function changeMethod(mixed $picture, int $userId): void { - UserPictureModel::getInstance()->uploadImage($userId, $picture); + $user = UsersModel::getInstance()->getUserById($userId); + + if ($user === null) { + Flash::send(Alert::ERROR, + LangManager::translate('core.toaster.error'), + LangManager::translate('core.errors.user.not.found'), + ); + Redirect::redirectPreviousRoute(); + } + + try { + // Upload image on the server + $imageName = ImagesManager::convertAndUpload($picture, 'Users'); + + if (!UserPictureModel::getInstance()->uploadImage($user->getId(), $imageName)) { + Flash::send(Alert::ERROR, LangManager::translate('core.toaster.error'), + LangManager::translate('core.errors.upload.image')); + Redirect::redirectPreviousRoute(); + } + } catch (ImagesException $e) { + Flash::send(Alert::ERROR, LangManager::translate('core.toaster.error'), + LangManager::translate('core.errors.upload.image') . " => $e"); + } UsersSessionsController::getInstance()->updateStoredUser($userId); @@ -63,6 +91,7 @@ public function getUserProfilePicture(int $userId): UserPictureEntity public function getDefaultProfilePicture(): string { - return UsersSettingsController::getDefaultImageLink(); + $defaultImg = UsersSettingsModel::getInstance()->getSetting('defaultImage'); + return EnvManager::getInstance()->getValue('PATH_SUBFOLDER') . 'Public/Uploads/Users/Default/' . $defaultImg; } } diff --git a/App/Package/Users/Lang/en.php b/App/Package/Users/Lang/en.php index 459f0e4a..2bde84ec 100644 --- a/App/Package/Users/Lang/en.php +++ b/App/Package/Users/Lang/en.php @@ -294,13 +294,17 @@ 'captcha' => [ 'invalid' => 'invalid Captcha', ], + 'connected' => [ + 'object' => ' - New login detected on your account', + 'body' => 'Hello %user_name%.
We have detected a new login to your account on %website%.

📍 Login details:
- Date and time: %date%
- IP address: %ip%

If you initiated this login, you can ignore this message.

⚠️ If this login was not made by you, we strongly recommend that you:
- Immediately change your password from your personal account settings.
- Check your recent account activity for any suspicious actions.
- Enable two-factor authentication (2FA) if you haven’t already.', + ], ], 'long_date' => [ 'setting' => [ 'label' => 'Strengthened user account security', 'no' => 'No (not recommended)', 'yes' => 'Yes', - 'small' => 'For users who do not have 2Fa if they have not logged in for more than 90 days they receive an identity confirmation code by email. (the email must be functional)', + 'small' => 'For users without 2FA, a confirmation code is sent by email if they haven\'t logged in for over 90 days (email delivery must be functional).
This setting also triggers an email for each login.', ], 'toaster' => [ 'title' => 'Identity verification', @@ -317,4 +321,17 @@ 'body_3' => 'If you are not the originator of this request, we advise you to change your password!', ], ], + 'pages' => [ + 'settings' => [ + 'general' => [ + 'menu' => 'General', + ], + 'security' => [ + 'menu' => 'Security', + ], + 'blacklist' => [ + 'menu' => 'Blacklist', + ], + ], + ], ]; diff --git a/App/Package/Users/Lang/fr.php b/App/Package/Users/Lang/fr.php index 2bc46256..a87b6676 100644 --- a/App/Package/Users/Lang/fr.php +++ b/App/Package/Users/Lang/fr.php @@ -248,17 +248,17 @@ 'flush' => [ 'modal' => [ 'warning' => 'Ceci va réinitialiser tous vos rôles ! (sauf Administrateur)', - 'text' => "Flusher les permissions est un outil de débogage souvent utilisé par les développeurs qui souhaitent forcer l'ajout manuel des permissions de leurs fichiers Permissions.php se trouvant dans le dossier Init." + 'text' => "Flusher les permissions est un outil de débogage souvent utilisé par les développeurs qui souhaitent forcer l'ajout manuel des permissions de leurs fichiers Permissions.php se trouvant dans le dossier Init.", ], ], 'oauth' => [ - 'manage' => [ - 'title' => 'Gestion des oAuth', - 'desc' => 'Gérez les méthodes de connexion oAuth', - 'subtitle' => 'Configuration des méthodes oAuth', - 'enabled' => 'Méthodes active', - 'disabled' => 'Méthodes inactive', - ], + 'manage' => [ + 'title' => 'Gestion des oAuth', + 'desc' => 'Gérez les méthodes de connexion oAuth', + 'subtitle' => 'Configuration des méthodes oAuth', + 'enabled' => 'Méthodes active', + 'disabled' => 'Méthodes inactive', + ], 'flash' => [ 'saveSettingFailed' => 'Une erreur s\'est produite lors de l\'enregistrement des paramètres.', 'saved' => 'Paramètres enregistrés avec succès.', @@ -295,13 +295,17 @@ 'captcha' => [ 'invalid' => 'Captcha invalide', ], + 'connected' => [ + 'object' => ' - Nouvelle connexion détectée sur votre compte', + 'body' => 'Bonjour %user_name%.
Nous avons détecté une nouvelle connexion à votre compte sur %website%.

📍 Détails de la connexion :
- Date et heure : %date%
- Adresse IP : %ip%

Si vous êtes à l\'origine de cette connexion, vous pouvez ignorer ce message.

⚠️ Si cette connexion ne vient pas de vous, nous vous recommandons fortement de :
- Changer immédiatement votre mot de passe depuis votre espace personnel.
- Vérifier l’activité récente de votre compte pour détecter d’éventuelles actions suspectes.
- Activer l’authentification à deux facteurs (2FA) si ce n’est pas encore fait.', + ], ], 'long_date' => [ 'setting' => [ 'label' => 'Renforcé la sécurité des comptes utilisateur', 'no' => 'Non (non recommandé)', 'yes' => 'Oui', - 'small' => 'Pour les utilisateurs n\'ayant pas de 2Fa s\'ils ne se sont pas connecté depuis + de 90 jours ils reçoivent un code de confirmation d\'identité par mail. (l\'envoie de mail doit être fonctionnel)', + 'small' => 'Pour les utilisateurs sans 2FA, un code de confirmation est envoyé par mail s\'ils ne se sont pas connectés depuis plus de 90 jours (l\'envoi de mail doit être fonctionnel).
Ce paramètre déclenche également un mail à chaque connexion.', ], 'toaster' => [ 'title' => 'Verification d\'identité', @@ -318,4 +322,17 @@ 'body_3' => 'Si vous n\'êtes pas à l\'origine de cette demande, nous vous conseillons de changer votre mot de passe !', ], ], + 'pages' => [ + 'settings' => [ + 'general' => [ + 'menu' => 'Généraux', + ], + 'security' => [ + 'menu' => 'Sécurité', + ], + 'blacklist' => [ + 'menu' => 'Blacklist', + ], + ], + ], ]; diff --git a/App/Package/Users/Models/UsersSettingsModel.php b/App/Package/Users/Models/UsersSettingsModel.php index 4e65c5c5..8e38c880 100644 --- a/App/Package/Users/Models/UsersSettingsModel.php +++ b/App/Package/Users/Models/UsersSettingsModel.php @@ -42,11 +42,16 @@ public function getSettings(): array return ($req->execute()) ? $req->fetchAll() : []; } - public function updateSetting(string $settingName, string $settingValue): void + /** + * @param string $settingName + * @param string $settingValue + * @return bool + */ + public function updateSetting(string $settingName, string $settingValue): bool { $db = DatabaseManager::getInstance(); $req = $db->prepare('UPDATE cmw_users_settings SET users_settings_value=:settingValue, users_settings_updated=now() WHERE users_settings_name=:settingName'); - $req->execute(['settingName' => $settingName, 'settingValue' => $settingValue]); + return $req->execute(['settingName' => $settingName, 'settingValue' => $settingValue]); } /** diff --git a/App/Package/Users/Package.php b/App/Package/Users/Package.php index 01af4d0c..bf3cff3f 100644 --- a/App/Package/Users/Package.php +++ b/App/Package/Users/Package.php @@ -46,8 +46,27 @@ public function menus(): ?array new PackageSubMenuType( title: LangManager::translate('core.menu.user.settings'), permission: 'users.settings', - url: 'users/settings', - subMenus: [] + url: null, + subMenus: [ + new PackageSubMenuType( + title: LangManager::translate('users.pages.settings.general.menu'), + permission: 'users.settings', + url: 'users/settings/general', + subMenus: [] + ), + new PackageSubMenuType( + title: LangManager::translate('users.pages.settings.security.menu'), + permission: 'users.settings', + url: 'users/settings/security', + subMenus: [] + ), + new PackageSubMenuType( + title: LangManager::translate('users.pages.settings.blacklist.menu'), + permission: 'users.settings', + url: 'users/settings/blacklist/pseudo', + subMenus: [] + ), + ] ), new PackageSubMenuType( title: LangManager::translate('core.menu.user.manage'), diff --git a/App/Package/Users/Views/Settings/blacklist.admin.view.php b/App/Package/Users/Views/Settings/blacklist.admin.view.php new file mode 100644 index 00000000..9eec82fd --- /dev/null +++ b/App/Package/Users/Views/Settings/blacklist.admin.view.php @@ -0,0 +1,121 @@ + + +

+ + - +

+ +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
getPseudo() ?>getDateBlacklistedFormatted() ?> + + +
+
+
+ +
+
+
+ insertHiddenToken() ?> +
+ + +
+ +
+
+ +
\ No newline at end of file diff --git a/App/Package/Users/Views/Settings/general.admin.view.php b/App/Package/Users/Views/Settings/general.admin.view.php new file mode 100644 index 00000000..4cdea96d --- /dev/null +++ b/App/Package/Users/Views/Settings/general.admin.view.php @@ -0,0 +1,72 @@ + + +
+

+ + - +

+
+ +
+
+
+ insertHiddenToken() ?> +
+
+ + + +
+
+ <?= LangManager::translate('users.settings.default_picture') ?> +
+
+ +
+ +
+
+
+ +
+
+ insertHiddenToken() ?> + + + + +
+ +
+
+
+
diff --git a/App/Package/Users/Views/Settings/security.admin.view.php b/App/Package/Users/Views/Settings/security.admin.view.php new file mode 100644 index 00000000..ded71fa0 --- /dev/null +++ b/App/Package/Users/Views/Settings/security.admin.view.php @@ -0,0 +1,106 @@ + + +
+

+ + - +

+ + +
+ +
+ insertHiddenToken() ?> + +
+
+ + +
+
+ + + +
+ +
+ +
+ +
+
+
:
+
+ +
+
+
+
+
+ + diff --git a/App/Package/Users/Views/manage.admin.view.php b/App/Package/Users/Views/manage.admin.view.php index 42dae053..f8f8309a 100644 --- a/App/Package/Users/Views/manage.admin.view.php +++ b/App/Package/Users/Views/manage.admin.view.php @@ -50,7 +50,7 @@
diff --git a/App/Package/Users/Views/settings.admin.view.php b/App/Package/Users/Views/settings.admin.view.php deleted file mode 100644 index 90af107e..00000000 --- a/App/Package/Users/Views/settings.admin.view.php +++ /dev/null @@ -1,217 +0,0 @@ - - -
-

- -
- -
- insertHiddenToken() ?> - -
-
-
-
- Reset -
-
- <?= LangManager::translate('users.settings.default_picture') ?> -
-
-
-
-
- - -
- -
- - -
- -
- - - -
- -
- -
- -
-
-
:
-
- -
-
-
-
-
-
- -
-
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
getPseudo() ?>getDateBlacklistedFormatted() ?> - - -
-
-
- -
-
-
- insertHiddenToken() ?> -
- - -
- -
-
- -
- - - - - - - diff --git a/Dockerfile b/Dockerfile index 23ef0970..15397ffe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,20 @@ FROM php:8.3.12-apache COPY . /var/www/html RUN apt-get update && apt-get install -y \ - libzip-dev \ + libfontconfig1 \ + libxrender1 \ + libxext6 \ + zlib1g-dev \ libpng-dev \ + libwebp-dev \ + libfreetype6-dev \ + libjpeg62-turbo-dev \ + libzip-dev \ zip \ unzip && \ apt-get clean && rm -rf /var/lib/apt/lists/* +RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp RUN docker-php-ext-install -j$(nproc) zip mysqli pdo pdo_mysql gd RUN a2enmod rewrite diff --git a/Installation/Views/Assets/Js/loader.js b/Installation/Views/Assets/Js/loader.js index cfc4e558..cf92f8ca 100644 --- a/Installation/Views/Assets/Js/loader.js +++ b/Installation/Views/Assets/Js/loader.js @@ -9,7 +9,16 @@ };*/ -const launchLoader = () => { +const launchLoader = (event) => { + const form = document.getElementById('mainForm'); + + // Vérifie si le formulaire est valide + if (!form.checkValidity()) { + event.preventDefault(); // Empêche l'envoi du formulaire si invalide + form.reportValidity(); // Affiche les messages d'erreur natifs + return; + } + let loader = document.getElementById('loader') let body = document.getElementById('body') @@ -20,11 +29,3 @@ const launchLoader = () => { const btn = document.getElementById('formBtn') btn.addEventListener('click', launchLoader) - -const customLaunchLoader = () => { - let loader = document.getElementById('loader') - let body = document.getElementById('body') - - loader.classList.remove('hidden') - body.classList.add("hidden") -} \ No newline at end of file diff --git a/Installation/Views/secondInstall.view.php b/Installation/Views/secondInstall.view.php index bd8d31cc..3aba0dfb 100644 --- a/Installation/Views/secondInstall.view.php +++ b/Installation/Views/secondInstall.view.php @@ -6,7 +6,7 @@ ?>

-
+

: