diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc481f75a..8f9200b6b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ This is a log of major user-visible changes in each phpMyFAQ release. - migrated from Webpack to Vite v6 (Thorsten) - migrated from Jest to vitest (Thorsten) +### phpMyFAQ v4.0.4 - unreleased + +- improved update from v3 (Thorsten) +- updated third party dependencies (Thorsten) +- fixed minor bugs (Thorsten) + ### phpMyFAQ v4.0.3 - 2025-01-03 - fixed installation bug introduced with v4.0.2 (Thorsten) diff --git a/docs/installation.md b/docs/installation.md index 3656e1da0d..472ad6a576 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -16,6 +16,7 @@ To install it, you will need a web server that meets the following requirements: - Filter support - SPL support - FileInfo support +- Sodium support ### Web server requirements @@ -28,6 +29,7 @@ You can use phpMyFAQ with the following web servers: - mod_rewrite - mod_ssl (if you wish to run phpMyFAQ under SSL) +- mod_headers You should also ensure you have `AllowOverride All` set in the `` and/or `` blocks so that the `.htaccess` file processes correctly, and rewrite rules take effect. Please check, if your path in diff --git a/phpmyfaq/admin/assets/src/user/users.js b/phpmyfaq/admin/assets/src/user/users.js index 47a1dc2259..978f3f2a92 100644 --- a/phpmyfaq/admin/assets/src/user/users.js +++ b/phpmyfaq/admin/assets/src/user/users.js @@ -57,6 +57,7 @@ const setUserData = async (userId) => { element.removeAttribute('disabled'); }); document.getElementById('checkAll').removeAttribute('disabled'); + document.getElementById('uncheckAll').setAttribute('disabled', ''); } else { const superAdmin = document.getElementById('is_superadmin'); superAdmin.removeAttribute('checked'); @@ -151,6 +152,7 @@ export const handleUsers = async () => { element.removeAttribute('disabled'); }); document.getElementById('checkAll').setAttribute('disabled', ''); + document.getElementById('uncheckAll').setAttribute('disabled', ''); } else { document.querySelectorAll('.permission').forEach((checkbox) => { checkbox.removeAttribute('disabled'); @@ -159,6 +161,7 @@ export const handleUsers = async () => { element.removeAttribute('disabled'); }); document.getElementById('checkAll').removeAttribute('disabled'); + document.getElementById('uncheckAll').removeAttribute('disabled'); } }); } diff --git a/phpmyfaq/assets/src/configuration/update.js b/phpmyfaq/assets/src/configuration/update.js index 09250bff77..9fd38e4ae4 100644 --- a/phpmyfaq/assets/src/configuration/update.js +++ b/phpmyfaq/assets/src/configuration/update.js @@ -111,51 +111,25 @@ export const handleDatabaseUpdate = async () => { body: installedVersion.value, }); + const result = await response.json(); const progressBarInstallation = document.getElementById('result-update'); - const reader = response.body.getReader(); - - async function pump() { - const { done, value } = await reader.read(); - const decodedValue = new TextDecoder().decode(value); - if (done) { - progressBarInstallation.style.width = '100%'; - progressBarInstallation.innerText = '100%'; - progressBarInstallation.classList.remove('progress-bar-animated'); - const alert = document.getElementById('phpmyfaq-update-database-success'); - alert.classList.remove('d-none'); - return; - } else { - let value; - try { - value = JSON.parse(decodedValue); - } catch (error) { - console.error('Failed to parse JSON:', error); - const alert = document.getElementById('phpmyfaq-update-database-error'); - const errorMessage = document.getElementById('error-messages'); - alert.classList.remove('d-none'); - errorMessage.innerText = `Error: ${error.message}\nFull Error: ${decodedValue}`; - return; - } - if (value.progress) { - progressBarInstallation.style.width = value.progress; - progressBarInstallation.innerText = value.progress; - } - if (value.error) { - progressBarInstallation.style.width = '100%'; - progressBarInstallation.innerText = '100%'; - progressBarInstallation.classList.remove('progress-bar-animated'); - const alert = document.getElementById('phpmyfaq-update-database-error'); - const errorMessage = document.getElementById('error-messages'); - alert.classList.remove('d-none'); - errorMessage.innerHTML = value.error; - return; - } - } - - await pump(); - } - await pump(); + if (response.ok) { + progressBarInstallation.style.width = '100%'; + progressBarInstallation.innerText = '100%'; + progressBarInstallation.classList.remove('progress-bar-animated'); + const alert = document.getElementById('phpmyfaq-update-database-success'); + alert.classList.remove('d-none'); + alert.innerText = result.success; + } else { + progressBarInstallation.style.width = '100%'; + progressBarInstallation.innerText = '100%'; + progressBarInstallation.classList.remove('progress-bar-animated'); + const alert = document.getElementById('phpmyfaq-update-database-error'); + const errorMessage = document.getElementById('error-messages'); + alert.classList.remove('d-none'); + errorMessage.innerHTML = result.error; + } } catch (error) { console.error('Error details:', error); const alert = document.getElementById('phpmyfaq-update-database-error'); diff --git a/phpmyfaq/assets/templates/default/twofactor.twig b/phpmyfaq/assets/templates/default/twofactor.twig new file mode 100644 index 0000000000..2d7e689a96 --- /dev/null +++ b/phpmyfaq/assets/templates/default/twofactor.twig @@ -0,0 +1,39 @@ +{% extends 'index.twig' %} + +{% block content %} +
+ {{ loginMessage }} +
+
+
+
+
+
+
+

{{ msgTwofactorEnabled }}

+
+
+
+ +
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+{% endblock %} diff --git a/phpmyfaq/assets/templates/setup/update/step1.twig b/phpmyfaq/assets/templates/setup/update/step1.twig index a22f31ad4a..f5c9ee9d23 100644 --- a/phpmyfaq/assets/templates/setup/update/step1.twig +++ b/phpmyfaq/assets/templates/setup/update/step1.twig @@ -1,4 +1,5 @@ -
+ diff --git a/phpmyfaq/assets/templates/setup/update/step2.twig b/phpmyfaq/assets/templates/setup/update/step2.twig index 5f8d03150c..fd3951e015 100644 --- a/phpmyfaq/assets/templates/setup/update/step2.twig +++ b/phpmyfaq/assets/templates/setup/update/step2.twig @@ -1,4 +1,5 @@ - + diff --git a/phpmyfaq/assets/templates/setup/update/step3.twig b/phpmyfaq/assets/templates/setup/update/step3.twig index d7cf54cf09..b80f34eaa0 100644 --- a/phpmyfaq/assets/templates/setup/update/step3.twig +++ b/phpmyfaq/assets/templates/setup/update/step3.twig @@ -1,4 +1,5 @@ - +
diff --git a/phpmyfaq/index.php b/phpmyfaq/index.php index 5d3274698f..7cb9ca8477 100755 --- a/phpmyfaq/index.php +++ b/phpmyfaq/index.php @@ -67,6 +67,8 @@ $request = Request::createFromGlobals(); $response = new Response(); $response->headers->set('Content-Type', 'text/html'); +$csrfLogoutToken = Token::getInstance()->getTokenString('logout'); + // // Service Containers @@ -638,7 +640,7 @@ 'msgBookmarks' => Translation::get('msgBookmarks'), 'msgUserRemoval' => Translation::get('ad_menu_RequestRemove'), 'msgLogoutUser' => Translation::get('ad_menu_logout'), - 'csrfLogout' => Token::getInstance($container->get('session'))->getTokenString('logout'), + 'csrfLogout' => $csrfLogoutToken, ]; } diff --git a/phpmyfaq/login.php b/phpmyfaq/login.php index 6dbb1ac142..740e2e60e4 100644 --- a/phpmyfaq/login.php +++ b/phpmyfaq/login.php @@ -36,9 +36,13 @@ $loginMessage = ''; } +$templateFile = './login.twig'; +if ($action == 'twofactor') { + $templateFile = './twofactor.twig'; +} $twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./login.twig'); +$twigTemplate = $twig->loadTemplate($templateFile); $templateVars = [ ... $templateVars, diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/UpdateController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/UpdateController.php index b606e20e8a..411e09ddac 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/UpdateController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/UpdateController.php @@ -24,8 +24,6 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Enums\PermissionType; use phpMyFAQ\Filter; -use phpMyFAQ\Setup\Update; -use phpMyFAQ\Setup\Upgrade; use phpMyFAQ\System; use phpMyFAQ\Translation; use Symfony\Component\HttpClient\HttpClient; @@ -43,7 +41,7 @@ class UpdateController extends AbstractController { /** - * @throws Exception + * @throws Exception|\Exception */ #[Route('admin/api/health-check')] public function healthCheck(): JsonResponse @@ -52,7 +50,7 @@ public function healthCheck(): JsonResponse $dateTime = new DateTime(); $dateLastChecked = $dateTime->format(DateTimeInterface::ATOM); - $upgrade = new Upgrade(new System(), $this->configuration); + $upgrade = $this->container->get('phpmyfaq.setup.upgrade'); if (!$upgrade->isMaintenanceEnabled()) { return $this->json( @@ -84,9 +82,6 @@ public function healthCheck(): JsonResponse } } - /** - * @throws Exception - */ #[Route('admin/api/versions')] public function versions(): JsonResponse { @@ -109,7 +104,7 @@ public function versions(): JsonResponse } /** - * @throws Exception + * @throws Exception|\Exception */ #[Route('admin/api/update-check')] public function updateCheck(): JsonResponse @@ -155,7 +150,7 @@ public function updateCheck(): JsonResponse * @throws RedirectionExceptionInterface * @throws ClientExceptionInterface * @throws \JsonException - * @throws Exception + * @throws Exception|\Exception */ #[Route('admin/api/download-package')] public function downloadPackage(Request $request): JsonResponse @@ -164,7 +159,7 @@ public function downloadPackage(Request $request): JsonResponse $versionNumber = Filter::filterVar($request->get('versionNumber'), FILTER_SANITIZE_SPECIAL_CHARS); - $upgrade = new Upgrade(new System(), $this->configuration); + $upgrade = $this->container->get('phpmyfaq.setup.upgrade'); $pathToPackage = $upgrade->downloadPackage($versionNumber); if ($pathToPackage === false) { @@ -188,7 +183,7 @@ public function extractPackage(): StreamedResponse { $this->userHasPermission(PermissionType::CONFIGURATION_EDIT); - $upgrade = new Upgrade(new System(), $this->configuration); + $upgrade = $this->container->get('phpmyfaq.setup.upgrade'); $pathToPackage = urldecode((string) $this->configuration->get('upgrade.lastDownloadedPackage')); return new StreamedResponse(static function () use ($upgrade, $pathToPackage) { @@ -210,7 +205,7 @@ public function createTemporaryBackup(): StreamedResponse { $this->userHasPermission(PermissionType::CONFIGURATION_EDIT); - $upgrade = new Upgrade(new System(), $this->configuration); + $upgrade = $this->container->get('phpmyfaq.setup.upgrade'); $backupHash = md5(uniqid()); return new StreamedResponse(static function () use ($upgrade, $backupHash) { @@ -232,7 +227,7 @@ public function installPackage(): StreamedResponse { $this->userHasPermission(PermissionType::CONFIGURATION_EDIT); - $upgrade = new Upgrade(new System(), $this->configuration); + $upgrade = $this->container->get('phpmyfaq.setup.upgrade'); $configurator = $this->container->get('phpmyfaq.setup.environment_configurator'); return new StreamedResponse(static function () use ($upgrade, $configurator) { $progressCallback = static function ($progress) { @@ -249,39 +244,40 @@ public function installPackage(): StreamedResponse } #[Route('admin/api/update-database')] - public function updateDatabase(): StreamedResponse + public function updateDatabase(): JsonResponse { $this->userHasPermission(PermissionType::CONFIGURATION_EDIT); - $update = new Update(new System(), $this->configuration); + $update = $this->container->get('phpmyfaq.setup.update'); $update->setVersion(System::getVersion()); - return new StreamedResponse(static function () use ($update) { - $progressCallback = static function ($progress) { - echo json_encode(['progress' => $progress]) . "\n"; - ob_flush(); - flush(); - }; - try { - if ($update->applyUpdates($progressCallback)) { - $this->configuration->set('main.maintenanceMode', 'false'); - echo json_encode(['message' => '✅ Database successfully updated.']); - } - } catch (Exception $exception) { - echo json_encode(['message' => 'Update database failed: ' . $exception->getMessage()]); + try { + if ($update->applyUpdates()) { + $this->configuration->set('main.maintenanceMode', 'false'); + return new JsonResponse( + ['success' => '✅ Database successfully updated.'], + Response::HTTP_OK + ); } - }); + + return new JsonResponse(['error' => 'Update database failed.'], Response::HTTP_BAD_GATEWAY); + } catch (Exception $exception) { + return new JsonResponse( + ['error' => 'Update database failed: ' . $exception->getMessage()], + Response::HTTP_BAD_GATEWAY + ); + } } /** - * @throws Exception + * @throws Exception|\Exception */ #[Route('admin/api/cleanup')] public function cleanUp(): JsonResponse { $this->userHasPermission(PermissionType::CONFIGURATION_EDIT); - $upgrade = new Upgrade(new System(), $this->configuration); + $upgrade = $this->container->get('phpmyfaq.setup.upgrade'); $upgrade->cleanUp(); return $this->json(['message' => '✅ Cleanup successful.'], Response::HTTP_OK); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Api/SetupController.php b/phpmyfaq/src/phpMyFAQ/Controller/Api/SetupController.php index 463ef6c665..0c5a5615e4 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Api/SetupController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Api/SetupController.php @@ -26,6 +26,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedJsonResponse; use Symfony\Component\HttpFoundation\StreamedResponse; class SetupController extends AbstractController @@ -91,7 +92,7 @@ public function backup(Request $request): JsonResponse return $this->json(['message' => '✅ Backup successful', 'backupFile' => $pathToBackup], Response::HTTP_OK); } - public function updateDatabase(Request $request): StreamedResponse|JsonResponse + public function updateDatabase(Request $request): JsonResponse { if (empty($request->getContent())) { return $this->json(['message' => 'No version given.'], Response::HTTP_BAD_REQUEST); @@ -102,23 +103,16 @@ public function updateDatabase(Request $request): StreamedResponse|JsonResponse $update = new Update(new System(), $this->configuration); $update->setVersion($installedVersion); - $response = new StreamedResponse(); - $configuration = $this->configuration; - $response->setCallback(static function () use ($update, $configuration) { - $progressCallback = static function ($progress) { - echo json_encode(['progress' => $progress]) . "\n"; - ob_flush(); - flush(); - }; - try { - if ($update->applyUpdates($progressCallback)) { - $configuration->set('main.maintenanceMode', 'false'); - echo json_encode(['success' => '✅ Database successfully updated.']); - } - } catch (Exception $exception) { - echo json_encode(['error' => 'Update database failed: ' . $exception->getMessage()]); + try { + if ($update->applyUpdates()) { + $this->configuration->set('main.maintenanceMode', 'false'); + return new JsonResponse(['success' => '✅ Database successfully updated.'], Response::HTTP_OK); } - }); - return $response; + } catch (Exception $exception) { + return new JsonResponse( + ['error' => 'Update database failed: ' . $exception->getMessage()], + Response::HTTP_BAD_GATEWAY + ); + } } } diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Update.php b/phpmyfaq/src/phpMyFAQ/Setup/Update.php index 75dec54679..19a0354d62 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Update.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Update.php @@ -145,7 +145,7 @@ public function checkInitialRewriteBasePath(Request $request): bool * @throws Exception * @throws \Exception */ - public function applyUpdates(callable $progressCallback): bool + public function applyUpdates(): bool { // 3.1 updates $this->applyUpdates310Alpha(); @@ -173,7 +173,7 @@ public function applyUpdates(callable $progressCallback): bool $this->optimizeTables(); // Execute queries - $this->executeQueries($progressCallback); + $this->executeQueries(); // Always the last step: Update version number $this->updateVersion(); @@ -210,17 +210,16 @@ public function getDryRunQueries(): array /** * @throws Exception */ - private function executeQueries(callable $progressCallback): void + private function executeQueries(): void { if ($this->dryRun) { foreach ($this->queries as $query) { - array_push($this->dryRunQueries, $query); + $this->dryRunQueries[] = $query; } } else { foreach ($this->queries as $query) { try { $this->configuration->getDb()->query($query); - $progressCallback($query); } catch (Exception $exception) { throw new Exception($exception->getMessage()); } diff --git a/phpmyfaq/src/services.php b/phpmyfaq/src/services.php index 6b2c475247..412c79d090 100644 --- a/phpmyfaq/src/services.php +++ b/phpmyfaq/src/services.php @@ -53,6 +53,8 @@ use phpMyFAQ\Services\Gravatar; use phpMyFAQ\Session\Token; use phpMyFAQ\Setup\EnvironmentConfigurator; +use phpMyFAQ\Setup\Update; +use phpMyFAQ\Setup\Upgrade; use phpMyFAQ\Sitemap; use phpMyFAQ\System; use phpMyFAQ\Tags; @@ -286,6 +288,18 @@ new Reference('phpmyfaq.configuration') ]); + $services->set('phpmyfaq.setup.update', Update::class) + ->args([ + new Reference('phpmyfaq.system'), + new Reference('phpmyfaq.configuration') + ]); + + $services->set('phpmyfaq.setup.upgrade', Upgrade::class) + ->args([ + new Reference('phpmyfaq.system'), + new Reference('phpmyfaq.configuration') + ]); + $services->set('phpmyfaq.system', System::class); $services->set('phpmyfaq.tags', Tags::class) diff --git a/tests/phpMyFAQ/Setup/UpdateTest.php b/tests/phpMyFAQ/Setup/UpdateTest.php index 962f2a683a..4f87cc3241 100644 --- a/tests/phpMyFAQ/Setup/UpdateTest.php +++ b/tests/phpMyFAQ/Setup/UpdateTest.php @@ -53,25 +53,17 @@ public function testIsConfigTableNotAvailable(): void */ public function testApplyUpdates(): void { - $progressCallback = function ($query) { - echo $query; - }; - $this->update->setVersion('4.0.0'); - $result = $this->update->applyUpdates($progressCallback); + $result = $this->update->applyUpdates(); $this->assertTrue($result); } public function testApplyUpdatesWithDryRunForAlpha3(): void { - $progressCallback = function ($query) { - echo $query; - }; - $this->update->setVersion('4.0.0-alpha.2'); $this->update->setDryRun(true); - $this->update->applyUpdates($progressCallback); + $this->update->applyUpdates(); $result = $this->update->getDryRunQueries();