From a4601ae5f01761dc0bb91b8c0c3901be91987ba3 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Mon, 29 Dec 2025 08:53:43 +0100 Subject: [PATCH 001/286] build: updated version strings to 4.2 --- CHANGELOG.md | 8 ++- docker-compose.yml | 4 +- docs/update.md | 58 +++++++++------- mkdocs.yml | 2 +- package.json | 2 +- phpmyfaq/src/phpMyFAQ/Setup/AbstractSetup.php | 4 +- phpmyfaq/src/phpMyFAQ/Setup/Installer.php | 2 +- phpmyfaq/src/phpMyFAQ/Setup/Update.php | 68 ------------------- phpmyfaq/src/phpMyFAQ/Setup/UpdateRunner.php | 1 - phpmyfaq/src/phpMyFAQ/System.php | 4 +- scripts/version.sh | 2 +- tests/phpMyFAQ/SystemTest.php | 2 +- 12 files changed, 52 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f888249f89..e460a4ab16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ -# phpMyFAQ 4.1 +# phpMyFAQ 4.2 -**Codename "Porus"** +**Codename "Palaimon"** ## CHANGELOG This is a log of major user-visible changes in each phpMyFAQ release. +### phpMyFAQ v4.2.0-dev - unreleased + +- n/a + ### phpMyFAQ v4.1.0-RC - 2025-12-29 - fixed security vulnerabilities (Thorsten) diff --git a/docker-compose.yml b/docker-compose.yml index b43dbb4b14..f94a49fd5b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -148,7 +148,7 @@ services: - pnpm pnpm: - image: node:22-alpine + image: node:24-alpine restart: 'no' command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm build" working_dir: /app @@ -195,7 +195,7 @@ services: elasticsearch: image: elasticsearch:8.16.5 - container_name: phpmyfaq-41_elasticsearch-v8 + container_name: phpmyfaq-42_elasticsearch-v8 restart: always environment: - cluster.name=phpmyfaq-cluster diff --git a/docs/update.md b/docs/update.md index 73d686f1c5..c66f2000d3 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,16 +1,15 @@ # 5. Updating phpMyFAQ -First, please download the latest package of phpMyFAQ. Upgrading to phpMyFAQ 4.1 is possible from the following +First, please download the latest package of phpMyFAQ. Upgrading to phpMyFAQ 4.2 is possible from the following versions: -- phpMyFAQ 3.0.x - phpMyFAQ 3.1.x - phpMyFAQ 3.2.x - phpMyFAQ 4.0.x - phpMyFAQ 4.1.x If you're running an older version of phpMyFAQ than listed above, we recommend a new and fresh installation. If you need -support for updating an old FAQ from the 1.x or 2.x series, [please email us](mailto:thorsten_AT_phpmyfaq_DOT_de). +support for updating an old FAQ from the 1.x, 2.x, or 3.0 series, [please email us](mailto:thorsten_AT_phpmyfaq_DOT_de). Please note that the requirements of phpMyFAQ have to be fulfilled. @@ -18,12 +17,13 @@ Please note that the requirements of phpMyFAQ have to be fulfilled. Please make sure that you're running at least PHP 8.3, otherwise the upgrade won't work. -## Upgrading from phpMyFAQ 3.0.x +## Upgrading from phpMyFAQ 3.1.x -Upgrading from 3.0.x is a major upgrade. -Your existing templates will not work with phpMyFAQ 4.1. +Upgrading from 3.1.x is a major upgrade. +Your existing templates will not work with phpMyFAQ 4.2. Please make a full backup before you run the upgrade! First, log in as admin into the admin section and enable the maintenance mode. +(Configuration >> Edit Configuration >> Set FAQ in maintenance mode) Second, you have to delete all files **except**: - in the directory **config/** @@ -40,10 +40,10 @@ URL in your browser: Click the button of the update script, your version will automatically be updated. -## Upgrading from phpMyFAQ 3.1.x +## Upgrading from phpMyFAQ 3.2.x -Upgrading from 3.1.x is a major upgrade. -Your existing templates will not work with phpMyFAQ 4.1. +Upgrading from 3.2.x is a major upgrade. +Your existing templates will not work with phpMyFAQ 4.2. Please make a full backup before you run the upgrade! First, log in as admin into the admin section and enable the maintenance mode. (Configuration >> Edit Configuration >> Set FAQ in maintenance mode) @@ -52,6 +52,7 @@ Second, you have to delete all files **except**: - in the directory **config/** - keep the file **database.php** - only if using LDAP/ActiveDirectory support also keep the file **ldap.php** + - only if using EntraID support also keep the file **azure.php** - the directory **attachments/** - the directory **data/** - the directory **images/** @@ -63,31 +64,25 @@ URL in your browser: Click the button of the update script, your version will automatically be updated. -## Upgrading from phpMyFAQ 3.2.x +## Upgrading from phpMyFAQ 4.0.x + +### Manual upgrade -Upgrading from 3.2.x is a major upgrade. -Your existing templates will not work with phpMyFAQ 4.1. Please make a full backup before you run the upgrade! First, log in as admin into the admin section and enable the maintenance mode. (Configuration >> Edit Configuration >> Set FAQ in maintenance mode) Second, you have to delete all files **except**: -- in the directory **config/** - - keep the file **database.php** - - only if using LDAP/ActiveDirectory support also keep the file **ldap.php** - - only if using EntraID support also keep the file **azure.php** -- the directory **attachments/** -- the directory **data/** -- the directory **images/** +- all files in the directory **content/** -Download the latest phpMyFAQ package and copy the contents into your existing FAQ directory, then open the following +Download the latest phpMyFAQ package and copy the contents into your existing FAQ directory, the open the following URL in your browser: `http://www.example.com/faq/update` Click the button of the update script, your version will automatically be updated. -## Upgrading from phpMyFAQ 4.0.x +## Upgrading from phpMyFAQ 4.1.x ### Manual upgrade @@ -105,7 +100,24 @@ URL in your browser: Click the button of the update script, your version will automatically be updated. -## Upgrading from phpMyFAQ 4.1.x +### Online update (Experimental feature) + +If you're running phpMyFAQ 4.0.0 or later, you can use the built-in online update feature. +Log in as admin into the admin section and enable the maintenance mode. +(Configuration >> Edit Configuration >> Set FAQ in maintenance mode) +Then go to the "phpMyFAQ Update" page in the configuration section and click through the update wizard: + +1. Check for System Health: this checks if your system is ready for the upgrade +2. Check for Updates: this checks if there is a new version of phpMyFAQ available +3. Download of phpMyFAQ: this downloads the latest version of phpMyFAQ in the background, this can take some seconds +4. Extracting phpMyFAQ: this extracts the downloaded archive, this can take a while +5. Install downloaded package: first, it creates a backup of your current installation, then it copies the downloaded + files into your installation, and in the end, the database is updated + +Note: +The online update feature is experimental and might not work in all environments. + +## Upgrading from phpMyFAQ 4.2.x ### Manual upgrade @@ -140,7 +152,7 @@ Then go to the "phpMyFAQ Update" page in the configuration section and click thr Note: The online update feature is experimental and might not work in all environments. -## Modifying templates for phpMyFAQ 4.1 +## Modifying templates for phpMyFAQ 4.2 We recommend you take a look at the main [Bootstrap documentation](https://getbootstrap.com/). Please remember that the style sheets are written with [SCSS](https://sass-lang.com/). diff --git a/mkdocs.yml b/mkdocs.yml index 32732b29f5..2cf7916030 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: phpMyFAQ v4.1 +site_name: phpMyFAQ v4.2 theme: name: readthedocs highlightjs: true diff --git a/package.json b/package.json index eabd8cd74d..53a912efd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@thorsten/phpmyfaq", - "version": "4.1.0-RC", + "version": "4.2.0-dev", "description": "phpMyFAQ", "repository": "git://github.com/thorsten/phpMyFAQ.git", "author": "Thorsten Rinne", diff --git a/phpmyfaq/src/phpMyFAQ/Setup/AbstractSetup.php b/phpmyfaq/src/phpMyFAQ/Setup/AbstractSetup.php index 6220a0f098..68bd8aebf5 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/AbstractSetup.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/AbstractSetup.php @@ -40,11 +40,11 @@ public function checkMinimumPhpVersion(): bool } /** - * We only support updates from 3.0.0 and later. + * We only support updates from 3.1.0 and later. */ public function checkMinimumUpdateVersion(string $version): bool { - return version_compare(version1: $version, version2: '3.0.0', operator: '>'); + return version_compare(version1: $version, version2: '3.1.0', operator: '>'); } /** diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Installer.php b/phpmyfaq/src/phpMyFAQ/Setup/Installer.php index 8a58c713bb..298f94cae1 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Installer.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Installer.php @@ -268,7 +268,7 @@ class Installer extends Setup 'main.enableUserTracking' => 'true', 'main.metaDescription' => 'phpMyFAQ should be the answer for all questions in life', 'main.metaPublisher' => '__PHPMYFAQ_PUBLISHER__', - 'main.titleFAQ' => 'phpMyFAQ Codename Porus', + 'main.titleFAQ' => 'phpMyFAQ Codename Palaimon', 'main.enableWysiwygEditor' => 'true', 'main.enableWysiwygEditorFrontend' => 'false', 'main.enableMarkdownEditor' => 'false', diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Update.php b/phpmyfaq/src/phpMyFAQ/Setup/Update.php index f391d72ecc..f6f5195be1 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Update.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Update.php @@ -158,12 +158,6 @@ public function checkInitialRewriteBasePath(Request $request): bool */ public function applyUpdates(): bool { - // 3.1 updates - $this->applyUpdates310Alpha(); - $this->applyUpdates310Alpha3(); - $this->applyUpdates310Beta(); - $this->applyUpdates310RC(); - // 3.2 updates $this->applyUpdates320Alpha(); $this->applyUpdates320Beta(); @@ -243,68 +237,6 @@ private function executeQueries(): void } } - private function applyUpdates310Alpha(): void - { - if (version_compare($this->version, '3.1.0-alpha', '<')) { - // Add is_visible flag for user data - if ('sqlite3' === Database::getType()) { - $this->queries[] = sprintf( - 'ALTER TABLE %sfaquserdata ADD COLUMN is_visible INT(1) DEFAULT 0', - Database::getTablePrefix(), - ); - } else { - $this->queries[] = sprintf( - 'ALTER TABLE %sfaquserdata ADD is_visible INTEGER DEFAULT 0', - Database::getTablePrefix(), - ); - } - - // Remove RSS support - $this->configuration->delete('main.enableRssFeeds'); - - // Add API-related configuration - $this->configuration->add('api.enableAccess', true); - $this->configuration->add('api.apiClientToken', ''); - - // Add passlist for domains - $this->configuration->add('security.domainWhiteListForRegistrations', ''); - } - } - - private function applyUpdates310Alpha3(): void - { - if (version_compare($this->version, '3.1.0-alpha.3', '<')) { - // Add "Login with email address" configuration - $this->configuration->add('main.loginWithEmailAddress', false); - } - } - - private function applyUpdates310Beta(): void - { - if (version_compare($this->version, '3.1.0-beta', '<')) { - $this->queries[] = match (Database::getType()) { - 'mysqli' => sprintf( - 'CREATE TABLE %sfaqcategory_order - (category_id int(11) NOT NULL, position int(11) NOT NULL, PRIMARY KEY (category_id))', - Database::getTablePrefix(), - ), - 'pgsql', 'sqlite3', 'sqlsrv' => sprintf( - 'CREATE TABLE %sfaqcategory_order - (category_id INTEGER NOT NULL, position INTEGER NOT NULL, PRIMARY KEY (category_id))', - Database::getTablePrefix(), - ), - }; - } - } - - private function applyUpdates310RC(): void - { - if (version_compare($this->version, '3.1.0-RC', '<')) { - $this->configuration->delete('records.autosaveActive'); - $this->configuration->delete('records.autosaveSecs'); - } - } - private function applyUpdates320Alpha(): void { if (version_compare($this->version, '3.2.0-alpha', '<')) { diff --git a/phpmyfaq/src/phpMyFAQ/Setup/UpdateRunner.php b/phpmyfaq/src/phpMyFAQ/Setup/UpdateRunner.php index 4932a048c5..d9e0e888bf 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/UpdateRunner.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/UpdateRunner.php @@ -249,7 +249,6 @@ private function withProgress(SymfonyStyle $io, callable $fn): bool }; try { - /** @var bool $result */ $result = (bool) $fn($setProgress); } finally { $progressBar->finish(); diff --git a/phpmyfaq/src/phpMyFAQ/System.php b/phpmyfaq/src/phpMyFAQ/System.php index 6808fae3f8..96900fb3dd 100644 --- a/phpmyfaq/src/phpMyFAQ/System.php +++ b/phpmyfaq/src/phpMyFAQ/System.php @@ -44,7 +44,7 @@ class System /** * Minor version. */ - private const int VERSION_MINOR = 1; + private const int VERSION_MINOR = 2; /** * Patch level. @@ -54,7 +54,7 @@ class System /** * Pre-release version. */ - private const string VERSION_PRE_RELEASE = 'RC'; + private const string VERSION_PRE_RELEASE = 'dev'; /** * API version. diff --git a/scripts/version.sh b/scripts/version.sh index 4df8b2177e..c027fd1f9f 100644 --- a/scripts/version.sh +++ b/scripts/version.sh @@ -1,5 +1,5 @@ #!/bin/sh # Check if PMF_VERSION is not set or empty if [ -z "${PMF_VERSION:-}" ]; then - PMF_VERSION="4.1.0-RC" + PMF_VERSION="4.2.0-dev" fi diff --git a/tests/phpMyFAQ/SystemTest.php b/tests/phpMyFAQ/SystemTest.php index 111ae09175..796bee6dfc 100644 --- a/tests/phpMyFAQ/SystemTest.php +++ b/tests/phpMyFAQ/SystemTest.php @@ -66,7 +66,7 @@ public function testCheckRequiredExtensions(): void public function testGetDocumentationUrl(): void { - $expectedUrl = 'https://www.phpmyfaq.de/docs/4.1'; + $expectedUrl = 'https://www.phpmyfaq.de/docs/4.2'; $actualUrl = System::getDocumentationUrl(); $this->assertEquals($expectedUrl, $actualUrl); From 103536b100b0e1fec80cadeeded6142230571b6d Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Mon, 29 Dec 2025 09:06:32 +0100 Subject: [PATCH 002/286] build: changed PHP requirement to PHP 8.4 or later --- .github/workflows/4.1-nightly.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/dev-nightly.yml | 2 +- CHANGELOG.md | 2 +- composer.json | 32 +- composer.lock | 824 +++++++++++------------------- docs/development.md | 4 +- docs/index.md | 2 +- docs/installation.md | 6 +- docs/update.md | 2 +- phpmyfaq/src/phpMyFAQ/System.php | 2 +- 11 files changed, 337 insertions(+), 543 deletions(-) diff --git a/.github/workflows/4.1-nightly.yml b/.github/workflows/4.1-nightly.yml index 839f537756..e26ae94e62 100644 --- a/.github/workflows/4.1-nightly.yml +++ b/.github/workflows/4.1-nightly.yml @@ -19,7 +19,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: - ref: 'main' #'4.1' + ref: '4.1' - name: Get current date id: date diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc112cdb22..55787b245c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.3', '8.4', '8.5', '8.6'] + php-versions: ['8.4', '8.5', '8.6'] name: phpMyFAQ ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} steps: diff --git a/.github/workflows/dev-nightly.yml b/.github/workflows/dev-nightly.yml index 23cb55fd77..8794849a5c 100644 --- a/.github/workflows/dev-nightly.yml +++ b/.github/workflows/dev-nightly.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.3'] + php-versions: ['8.4'] name: phpMyFAQ Nightly Dev Build steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index e460a4ab16..219b15df02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ This is a log of major user-visible changes in each phpMyFAQ release. ### phpMyFAQ v4.2.0-dev - unreleased -- n/a +- changed PHP requirement to PHP 8.4 or later (Thorsten) ### phpMyFAQ v4.1.0-RC - 2025-12-29 diff --git a/composer.json b/composer.json index 5dda714bfa..701d08a125 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=8.3.0", + "php": ">=8.4.0", "ext-curl": "*", "ext-fileinfo": "*", "ext-filter": "*", @@ -35,20 +35,20 @@ "opensearch-project/opensearch-php": "^2.4", "phpseclib/phpseclib": "~3.0", "robthree/twofactorauth": "^3.0.0", - "symfony/config": "^7.3", - "symfony/console": "^7.3", - "symfony/dependency-injection": "^7.3", - "symfony/dotenv": "^7.3", - "symfony/event-dispatcher": "^7.3", - "symfony/error-handler": "^7.3", - "symfony/html-sanitizer": "^7.3", - "symfony/http-client": "^7.3", - "symfony/http-foundation": "^7.3", - "symfony/http-kernel": "^7.3", - "symfony/mailer": "^7.3", + "symfony/config": "^8.0", + "symfony/console": "^8.0", + "symfony/dependency-injection": "^8.0", + "symfony/dotenv": "^8.0", + "symfony/event-dispatcher": "^8.0", + "symfony/error-handler": "^8.0", + "symfony/html-sanitizer": "^8.0", + "symfony/http-client": "^8.0", + "symfony/http-foundation": "^8.0", + "symfony/http-kernel": "^8.0", + "symfony/mailer": "^8.0", "symfony/mcp-sdk": "dev-main", - "symfony/routing": "^7.3", - "symfony/uid": "^7.3", + "symfony/routing": "^8.0", + "symfony/uid": "^8.0", "tecnickcom/tcpdf": "~6.0", "tivie/htaccess-parser": "0.4.0", "twig/intl-extra": "^3.10", @@ -61,7 +61,7 @@ "phpdocumentor/reflection-docblock": "5.*", "phpunit/phpunit": "^12.3", "rector/rector": "^2", - "symfony/yaml": "7.*", + "symfony/yaml": "8.*", "zircote/swagger-php": "^5.0" }, "suggest": { @@ -74,7 +74,7 @@ }, "config": { "platform": { - "php": "8.3.0" + "php": "8.4.0" }, "process-timeout": 0, "secure-http": true, diff --git a/composer.lock b/composer.lock index 45d1ef413b..63e566908e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d8a38630fff9570827d499e65c780b9d", + "content-hash": "6fc610567ab46bf2cc8c9fcf5fb47579", "packages": [ { "name": "2tvenom/cborencode", @@ -492,26 +492,26 @@ }, { "name": "endroid/qr-code", - "version": "6.0.9", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/endroid/qr-code.git", - "reference": "21e888e8597440b2205e2e5c484b6c8e556bcd1a" + "reference": "5a74873ba8873ddcc557d22755c914ac899563ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/endroid/qr-code/zipball/21e888e8597440b2205e2e5c484b6c8e556bcd1a", - "reference": "21e888e8597440b2205e2e5c484b6c8e556bcd1a", + "url": "https://api.github.com/repos/endroid/qr-code/zipball/5a74873ba8873ddcc557d22755c914ac899563ae", + "reference": "5a74873ba8873ddcc557d22755c914ac899563ae", "shasum": "" }, "require": { "bacon/bacon-qr-code": "^3.0", - "php": "^8.2" + "php": "^8.4" }, "require-dev": { "endroid/quality": "dev-main", "ext-gd": "*", - "khanamiryan/qrcode-detector-decoder": "^2.0.2", + "khanamiryan/qrcode-detector-decoder": "^2.0.3", "setasign/fpdf": "^1.8.2" }, "suggest": { @@ -552,7 +552,7 @@ ], "support": { "issues": "https://github.com/endroid/qr-code/issues", - "source": "https://github.com/endroid/qr-code/tree/6.0.9" + "source": "https://github.com/endroid/qr-code/tree/6.1.0" }, "funding": [ { @@ -560,7 +560,7 @@ "type": "github" } ], - "time": "2025-07-13T19:59:45+00:00" + "time": "2025-11-25T10:47:45+00:00" }, { "name": "ezimuel/guzzlestreams", @@ -1369,73 +1369,6 @@ ], "time": "2025-12-07T16:03:21+00:00" }, - { - "name": "masterminds/html5", - "version": "2.10.0", - "source": { - "type": "git", - "url": "https://github.com/Masterminds/html5-php.git", - "reference": "fcf91eb64359852f00d921887b219479b4f21251" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", - "reference": "fcf91eb64359852f00d921887b219479b4f21251", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Masterminds\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matt Butcher", - "email": "technosophos@gmail.com" - }, - { - "name": "Matt Farina", - "email": "matt@mattfarina.com" - }, - { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - } - ], - "description": "An HTML5 parser and serializer.", - "homepage": "http://masterminds.github.io/html5-php", - "keywords": [ - "HTML5", - "dom", - "html", - "parser", - "querypath", - "serializer", - "xml" - ], - "support": { - "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" - }, - "time": "2025-07-25T09:04:22+00:00" - }, { "name": "monolog/monolog", "version": "3.9.0", @@ -1884,51 +1817,49 @@ }, { "name": "opensearch-project/opensearch-php", - "version": "2.5.0", + "version": "2.4.6", "source": { "type": "git", "url": "https://github.com/opensearch-project/opensearch-php.git", - "reference": "478c5125ab62c5f01c29043b9ab5015b67156d85" + "reference": "eec5a78ade77337c6c630add63e8f90cdc2d337a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opensearch-project/opensearch-php/zipball/478c5125ab62c5f01c29043b9ab5015b67156d85", - "reference": "478c5125ab62c5f01c29043b9ab5015b67156d85", + "url": "https://api.github.com/repos/opensearch-project/opensearch-php/zipball/eec5a78ade77337c6c630add63e8f90cdc2d337a", + "reference": "eec5a78ade77337c6c630add63e8f90cdc2d337a", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": ">=1.3.7", - "ezimuel/ringphp": "^1.4.0", + "ezimuel/ringphp": "^1.2.2", "php": "^8.1", "php-http/discovery": "^1.20", - "psr/http-client": "^1.0.3", + "psr/http-client": "^1.0", "psr/http-client-implementation": "*", - "psr/http-factory": "^1.1.0", + "psr/http-factory": "^1.1", "psr/http-factory-implementation": "*", "psr/http-message": "^2.0", "psr/http-message-implementation": "*", - "psr/log": "^3.0.2", - "symfony/yaml": "^v6.4.26|^7.3.5" + "psr/log": "^2|^3", + "symfony/yaml": "*" }, "require-dev": { - "aws/aws-sdk-php": "^3.359.8", - "colinodell/psr-testlogger": "^1.3.1", + "aws/aws-sdk-php": "^3.0", + "colinodell/psr-testlogger": "^1.3", "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^v3.89.1", - "guzzlehttp/psr7": "^2.8.0", - "mockery/mockery": "^1.6.12", - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.32", - "phpstan/phpstan-deprecation-rules": "^1.2.1", - "phpstan/phpstan-mockery": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.4.2", - "phpunit/phpunit": "^10.5.58", - "react/promise": "^v3.3", - "symfony/console": "^v6.4.27|^7.3.6", - "symfony/finder": "^v6.4.27|^7.3.6", - "symfony/http-client": "^6.4.27|^7.3.6", - "symfony/http-client-contracts": "^v3.6.0" + "friendsofphp/php-cs-fixer": "^v3.64", + "guzzlehttp/psr7": "^2.7", + "mockery/mockery": "^1.6", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.4", + "phpunit/phpunit": "^9.6", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/http-client-contracts": "^3.0" }, "suggest": { "aws/aws-sdk-php": "Required (^3.0.0) in order to use the AWS Signing Client Decorator", @@ -1964,9 +1895,9 @@ ], "support": { "issues": "https://github.com/opensearch-project/opensearch-php/issues", - "source": "https://github.com/opensearch-project/opensearch-php/tree/2.5.0" + "source": "https://github.com/opensearch-project/opensearch-php/tree/2.4.6" }, - "time": "2025-11-14T21:48:40+00:00" + "time": "2025-11-10T07:56:12+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -2898,34 +2829,33 @@ }, { "name": "symfony/config", - "version": "v7.4.1", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495" + "reference": "a5a054e613da565d46183a845ae4c0c996a3fbce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/2c323304c354a43a48b61c5fa760fc4ed60ce495", - "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495", + "url": "https://api.github.com/repos/symfony/config/zipball/a5a054e613da565d46183a845ae4c0c996a3fbce", + "reference": "a5a054e613da565d46183a845ae4c0c996a3fbce", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^7.1|^8.0", - "symfony/polyfill-ctype": "~1.8" + "symfony/filesystem": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/finder": "<6.4", "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/finder": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -2953,7 +2883,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.1" + "source": "https://github.com/symfony/config/tree/v8.0.1" }, "funding": [ { @@ -2973,51 +2903,43 @@ "type": "tidelift" } ], - "time": "2025-12-05T07:52:08+00:00" + "time": "2025-12-05T14:08:45+00:00" }, { "name": "symfony/console", - "version": "v7.4.1", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" + "reference": "fcb73f69d655b48fcb894a262f074218df08bd58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", - "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "url": "https://api.github.com/repos/symfony/console/zipball/fcb73f69d655b48fcb894a262f074218df08bd58", + "reference": "fcb73f69d655b48fcb894a262f074218df08bd58", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2|^8.0" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" + "symfony/string": "^7.4|^8.0" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/lock": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3051,7 +2973,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.1" + "source": "https://github.com/symfony/console/tree/v8.0.1" }, "funding": [ { @@ -3071,43 +2993,40 @@ "type": "tidelift" } ], - "time": "2025-12-05T15:23:39+00:00" + "time": "2025-12-05T15:25:33+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.2", + "version": "v8.0.2", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b" + "reference": "90f6c3364b8f444f85bdb6939664c80af9e0d576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", - "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/90f6c3364b8f444f85bdb6939664c80af9e0d576", + "reference": "90f6c3364b8f444f85bdb6939664c80af9e0d576", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^3.6", - "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" + "symfony/var-exporter": "^7.4|^8.0" }, "conflict": { - "ext-psr": "<1.1|>=2", - "symfony/config": "<6.4", - "symfony/finder": "<6.4", - "symfony/yaml": "<6.4" + "ext-psr": "<1.1|>=2" }, "provide": { "psr/container-implementation": "1.1|2.0", "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/config": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3135,7 +3054,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.2" + "source": "https://github.com/symfony/dependency-injection/tree/v8.0.2" }, "funding": [ { @@ -3155,7 +3074,7 @@ "type": "tidelift" } ], - "time": "2025-12-08T06:57:04+00:00" + "time": "2025-12-08T06:57:48+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3226,28 +3145,24 @@ }, { "name": "symfony/dotenv", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "1658a4d34df028f3d93bcdd8e81f04423925a364" + "reference": "460b4067a85288c59a59ce8c1bfb3942e71fd85c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/1658a4d34df028f3d93bcdd8e81f04423925a364", - "reference": "1658a4d34df028f3d93bcdd8e81f04423925a364", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/460b4067a85288c59a59ce8c1bfb3942e71fd85c", + "reference": "460b4067a85288c59a59ce8c1bfb3942e71fd85c", "shasum": "" }, "require": { - "php": ">=8.2" - }, - "conflict": { - "symfony/console": "<6.4", - "symfony/process": "<6.4" + "php": ">=8.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0" + "symfony/console": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3280,7 +3195,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v7.4.0" + "source": "https://github.com/symfony/dotenv/tree/v8.0.0" }, "funding": [ { @@ -3300,37 +3215,36 @@ "type": "tidelift" } ], - "time": "2025-11-16T10:14:42+00:00" + "time": "2025-11-16T10:17:21+00:00" }, { "name": "symfony/error-handler", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" + "reference": "d77ec7dda0c274178745d152e82baf7ea827fd73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", - "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/d77ec7dda0c274178745d152e82baf7ea827fd73", + "reference": "d77ec7dda0c274178745d152e82baf7ea827fd73", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "psr/log": "^1|^2|^3", "symfony/polyfill-php85": "^1.32", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/var-dumper": "^7.4|^8.0" }, "conflict": { - "symfony/deprecation-contracts": "<2.5", - "symfony/http-kernel": "<6.4" + "symfony/deprecation-contracts": "<2.5" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", + "symfony/console": "^7.4|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -3362,7 +3276,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.4.0" + "source": "https://github.com/symfony/error-handler/tree/v8.0.0" }, "funding": [ { @@ -3382,28 +3296,28 @@ "type": "tidelift" } ], - "time": "2025-11-05T14:29:59+00:00" + "time": "2025-11-05T14:36:47+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" + "reference": "573f95783a2ec6e38752979db139f09fec033f03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d", - "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", + "reference": "573f95783a2ec6e38752979db139f09fec033f03", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -3412,14 +3326,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/error-handler": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/framework-bundle": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0|^8.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3447,7 +3361,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" }, "funding": [ { @@ -3467,7 +3381,7 @@ "type": "tidelift" } ], - "time": "2025-10-28T09:38:46+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -3547,25 +3461,25 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.0", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + "reference": "d937d400b980523dc9ee946bb69972b5e619058d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0|^8.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3593,7 +3507,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + "source": "https://github.com/symfony/filesystem/tree/v8.0.1" }, "funding": [ { @@ -3613,28 +3527,26 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/html-sanitizer", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "5b0bbcc3600030b535dd0b17a0e8c56243f96d7f" + "reference": "b091fe14296544172b1ec810cbd0af42e8d8ce89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/5b0bbcc3600030b535dd0b17a0e8c56243f96d7f", - "reference": "5b0bbcc3600030b535dd0b17a0e8c56243f96d7f", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/b091fe14296544172b1ec810cbd0af42e8d8ce89", + "reference": "b091fe14296544172b1ec810cbd0af42e8d8ce89", "shasum": "" }, "require": { "ext-dom": "*", "league/uri": "^6.5|^7.0", - "masterminds/html5": "^2.7.2", - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -3667,7 +3579,7 @@ "sanitizer" ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v7.4.0" + "source": "https://github.com/symfony/html-sanitizer/tree/v8.0.0" }, "funding": [ { @@ -3687,35 +3599,31 @@ "type": "tidelift" } ], - "time": "2025-10-30T13:39:42+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/http-client", - "version": "v7.4.1", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "26cc224ea7103dda90e9694d9e139a389092d007" + "reference": "727fda60d0aebfdfcc4c8bc4661f0cb8f44153c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/26cc224ea7103dda90e9694d9e139a389092d007", - "reference": "26cc224ea7103dda90e9694d9e139a389092d007", + "url": "https://api.github.com/repos/symfony/http-client/zipball/727fda60d0aebfdfcc4c8bc4661f0cb8f44153c0", + "reference": "727fda60d0aebfdfcc4c8bc4661f0cb8f44153c0", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", - "symfony/polyfill-php83": "^1.29", "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "amphp/amp": "<2.5", - "amphp/socket": "<1.1", - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.4" + "amphp/amp": "<3", + "php-http/discovery": "<1.15" }, "provide": { "php-http/async-client-implementation": "*", @@ -3724,20 +3632,19 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/http-client": "^4.2.1|^5.0", - "amphp/http-tunnel": "^1.0|^2.0", + "amphp/http-client": "^5.3.2", + "amphp/http-tunnel": "^2.0", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0" + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3768,7 +3675,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.1" + "source": "https://github.com/symfony/http-client/tree/v8.0.1" }, "funding": [ { @@ -3788,7 +3695,7 @@ "type": "tidelift" } ], - "time": "2025-12-04T21:12:57+00:00" + "time": "2025-12-05T14:08:45+00:00" }, { "name": "symfony/http-client-contracts", @@ -3870,37 +3777,35 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.1", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27" + "reference": "3690740e2e8b19d877f20d4f10b7a489cddf0fe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27", - "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3690740e2e8b19d877f20d4f10b7a489cddf0fe2", + "reference": "3690740e2e8b19d877f20d4f10b7a489cddf0fe2", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.4", "symfony/polyfill-mbstring": "^1.1" }, "conflict": { - "doctrine/dbal": "<3.6", - "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + "doctrine/dbal": "<4.3" }, "require-dev": { - "doctrine/dbal": "^3.6|^4", + "doctrine/dbal": "^4.3", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5|^8.0", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/mime": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^6.4|^7.0|^8.0" + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3928,7 +3833,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.1" + "source": "https://github.com/symfony/http-foundation/tree/v8.0.1" }, "funding": [ { @@ -3948,78 +3853,63 @@ "type": "tidelift" } ], - "time": "2025-12-07T11:13:10+00:00" + "time": "2025-12-07T11:23:24+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.2", + "version": "v8.0.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f" + "reference": "bcef77a3c8ae8934ce7067172e2a1a6491a62a7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6e6f0a5fa8763f75a504b930163785fb6dd055f", - "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/bcef77a3c8ae8934ce7067172e2a1a6491a62a7d", + "reference": "bcef77a3c8ae8934ce7067172e2a1a6491a62a7d", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0|^8.0", - "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/browser-kit": "<6.4", - "symfony/cache": "<6.4", - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/doctrine-bridge": "<6.4", "symfony/flex": "<2.10", - "symfony/form": "<6.4", - "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/mailer": "<6.4", - "symfony/messenger": "<6.4", - "symfony/translation": "<6.4", "symfony/translation-contracts": "<2.5", - "symfony/twig-bridge": "<6.4", - "symfony/validator": "<6.4", - "symfony/var-dumper": "<6.4", - "twig/twig": "<3.12" + "twig/twig": "<3.21" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0|^8.0", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/css-selector": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/dom-crawler": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/property-access": "^7.1|^8.0", - "symfony/routing": "^6.4|^7.0|^8.0", - "symfony/serializer": "^7.1|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0|^8.0", - "symfony/validator": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0", - "twig/twig": "^3.12" + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" }, "type": "library", "autoload": { @@ -4047,7 +3937,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.2" + "source": "https://github.com/symfony/http-kernel/tree/v8.0.2" }, "funding": [ { @@ -4067,32 +3957,31 @@ "type": "tidelift" } ], - "time": "2025-12-08T07:43:37+00:00" + "time": "2025-12-08T07:59:34+00:00" }, { "name": "symfony/intl", - "version": "v7.4.0", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "2fa074de6c7faa6b54f2891fc22708f42245ed5c" + "reference": "f9eca217ae8f2be0b3ad80723d6a3b518b90cd66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/2fa074de6c7faa6b54f2891fc22708f42245ed5c", - "reference": "2fa074de6c7faa6b54f2891fc22708f42245ed5c", + "url": "https://api.github.com/repos/symfony/intl/zipball/f9eca217ae8f2be0b3ad80723d6a3b518b90cd66", + "reference": "f9eca217ae8f2be0b3ad80723d6a3b518b90cd66", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.4" }, "conflict": { - "symfony/string": "<7.1" + "symfony/string": "<7.4" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0|^8.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0" + "symfony/filesystem": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4137,7 +4026,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v7.4.0" + "source": "https://github.com/symfony/intl/tree/v8.0.1" }, "funding": [ { @@ -4157,43 +4046,39 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd" + "reference": "f9b546f0e28cbd08fd5d03f2472aad913a9398f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", - "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f9b546f0e28cbd08fd5d03f2472aad913a9398f9", + "reference": "f9b546f0e28cbd08fd5d03f2472aad913a9398f9", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.2", + "php": ">=8.4", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/mime": "^7.2|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/messenger": "<6.4", - "symfony/mime": "<6.4", - "symfony/twig-bridge": "<6.4" + "symfony/http-client-contracts": "<2.5" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/twig-bridge": "^6.4|^7.0|^8.0" + "symfony/console": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4221,7 +4106,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.0" + "source": "https://github.com/symfony/mailer/tree/v8.0.0" }, "funding": [ { @@ -4241,7 +4126,7 @@ "type": "tidelift" } ], - "time": "2025-11-21T15:26:00+00:00" + "time": "2025-11-27T08:09:45+00:00" }, { "name": "symfony/mcp-sdk", @@ -4319,40 +4204,37 @@ }, { "name": "symfony/mime", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" + "reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", - "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", + "url": "https://api.github.com/repos/symfony/mime/zipball/7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40", + "reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.4", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<6.4", - "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + "phpdocumentor/type-resolver": "<1.4.0" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/property-info": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4384,7 +4266,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.0" + "source": "https://github.com/symfony/mime/tree/v8.0.0" }, "funding": [ { @@ -4404,7 +4286,7 @@ "type": "tidelift" } ], - "time": "2025-11-16T10:14:42+00:00" + "time": "2025-11-16T10:17:21+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4992,86 +4874,6 @@ ], "time": "2024-09-09T11:45:10+00:00" }, - { - "name": "symfony/polyfill-php83", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php83\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-07-08T02:45:35+00:00" - }, { "name": "symfony/polyfill-php85", "version": "v1.33.0", @@ -5237,34 +5039,29 @@ }, { "name": "symfony/routing", - "version": "v7.4.0", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "4720254cb2644a0b876233d258a32bf017330db7" + "reference": "bc8fa314a61fb7c4190e964b18a5bd000d3b45ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/4720254cb2644a0b876233d258a32bf017330db7", - "reference": "4720254cb2644a0b876233d258a32bf017330db7", + "url": "https://api.github.com/repos/symfony/routing/zipball/bc8fa314a61fb7c4190e964b18a5bd000d3b45ce", + "reference": "bc8fa314a61fb7c4190e964b18a5bd000d3b45ce", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, - "conflict": { - "symfony/config": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/yaml": "<6.4" - }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5298,7 +5095,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.0" + "source": "https://github.com/symfony/routing/tree/v8.0.1" }, "funding": [ { @@ -5318,7 +5115,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/service-contracts", @@ -5409,35 +5206,34 @@ }, { "name": "symfony/string", - "version": "v7.4.0", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", - "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", + "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.33", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5476,7 +5272,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.0" + "source": "https://github.com/symfony/string/tree/v8.0.1" }, "funding": [ { @@ -5496,28 +5292,28 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/uid", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2498e9f81b7baa206f44de583f2f48350b90142c" + "reference": "8395a2cc2ed49aa68f602c5c489f60ab853893df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c", - "reference": "2498e9f81b7baa206f44de583f2f48350b90142c", + "url": "https://api.github.com/repos/symfony/uid/zipball/8395a2cc2ed49aa68f602c5c489f60ab853893df", + "reference": "8395a2cc2ed49aa68f602c5c489f60ab853893df", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0" + "symfony/console": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5554,7 +5350,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.0" + "source": "https://github.com/symfony/uid/tree/v8.0.0" }, "funding": [ { @@ -5574,35 +5370,35 @@ "type": "tidelift" } ], - "time": "2025-09-25T11:02:55+00:00" + "time": "2025-09-26T07:52:19+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" + "reference": "d2a2476c93b58ac5292145e9fac1ff76a21d1ce2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", - "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/d2a2476c93b58ac5292145e9fac1ff76a21d1ce2", + "reference": "d2a2476c93b58ac5292145e9fac1ff76a21d1ce2", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { - "symfony/console": "<6.4" + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -5641,7 +5437,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" + "source": "https://github.com/symfony/var-dumper/tree/v8.0.0" }, "funding": [ { @@ -5661,30 +5457,29 @@ "type": "tidelift" } ], - "time": "2025-10-27T20:36:44+00:00" + "time": "2025-10-28T09:34:19+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.4" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5722,7 +5517,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" }, "funding": [ { @@ -5742,32 +5537,31 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "time": "2025-11-05T18:53:00+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.1", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" + "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", - "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", + "url": "https://api.github.com/repos/symfony/yaml/zipball/7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14", + "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.4", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<6.4" + "symfony/console": "<7.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0" + "symfony/console": "^7.4|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -5798,7 +5592,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.1" + "source": "https://github.com/symfony/yaml/tree/v8.0.1" }, "funding": [ { @@ -5818,7 +5612,7 @@ "type": "tidelift" } ], - "time": "2025-12-04T18:11:45+00:00" + "time": "2025-12-04T18:17:06+00:00" }, { "name": "tecnickcom/tcpdf", @@ -8214,23 +8008,23 @@ }, { "name": "symfony/finder", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", + "url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291", + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0|^8.0" + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -8258,7 +8052,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.0" + "source": "https://github.com/symfony/finder/tree/v8.0.0" }, "funding": [ { @@ -8278,7 +8072,7 @@ "type": "tidelift" } ], - "time": "2025-11-05T05:42:40+00:00" + "time": "2025-11-05T14:36:47+00:00" }, { "name": "theseer/tokenizer", @@ -8489,7 +8283,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.3.0", + "php": ">=8.4.0", "ext-curl": "*", "ext-fileinfo": "*", "ext-filter": "*", @@ -8502,7 +8296,7 @@ }, "platform-dev": {}, "platform-overrides": { - "php": "8.3.0" + "php": "8.4.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/docs/development.md b/docs/development.md index 698630fbd0..96f8232c42 100644 --- a/docs/development.md +++ b/docs/development.md @@ -213,10 +213,10 @@ work on your copy and send pull requests. Before working on phpMyFAQ, set up a local environment with the following software: - Git -- PHP 8.3 up to 8.6 +- PHP 8.4 up to 8.6 - PHPUnit 12.x - Composer -- Node.js 22+ +- Node.js 24+ - TypeScript 5.x - PNPM - Docker diff --git a/docs/index.md b/docs/index.md index 7fc2a75387..a2253814f6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ # phpMyFAQ Manual phpMyFAQ is a comprehensive, multilingual FAQ system that is entirely database-driven. -It is compatible with a variety of databases for data storage and requires PHP 8.3+ for data access. +It is compatible with a variety of databases for data storage and requires PHP 8.4+ for data access. The system features a multi-language Content Management System equipped with a WYSIWYG editor and an Image Manager. It also provides real-time search capabilities with Elasticsearch or OpenSearch. diff --git a/docs/installation.md b/docs/installation.md index 38dba8c9a4..537a09d1aa 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -7,7 +7,7 @@ To install it, you will need a web server that meets the following requirements: ### PHP requirements -- version 8.3 or later +- version 8.4 or later - memory_limit = 128M (the more the better) - PDO support - cURL support @@ -77,7 +77,7 @@ content: ` Date: Mon, 29 Dec 2025 09:29:44 +0100 Subject: [PATCH 003/286] build: updated Dockerfiles for 4.2 --- .docker/frankenphp/Dockerfile | 1 - docker-compose.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.docker/frankenphp/Dockerfile b/.docker/frankenphp/Dockerfile index 21a6b47aa2..4600d1661c 100644 --- a/.docker/frankenphp/Dockerfile +++ b/.docker/frankenphp/Dockerfile @@ -34,7 +34,6 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ pdo_pgsql \ pgsql \ zip \ - opcache \ bz2 \ sodium diff --git a/docker-compose.yml b/docker-compose.yml index f94a49fd5b..aa1e1a8e73 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -226,7 +226,7 @@ services: opensearch: image: opensearchproject/opensearch:2.19.2 - container_name: opensearch + container_name: phpmyfaq-42_opensearch environment: - cluster.name=phpmyfaq-cluster - node.name=phpmyfaq From 28514f56102fef7e7c05dfb740bc48c2ab97c392 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 04:05:49 +0000 Subject: [PATCH 004/286] build(deps-dev): bump typescript-eslint from 8.50.1 to 8.51.0 Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.50.1 to 8.51.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.51.0/packages/typescript-eslint) --- updated-dependencies: - dependency-name: typescript-eslint dependency-version: 8.51.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 116 ++++++++++++++++++++++++------------------------- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 53a912efd0..4cfe3f3b67 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "sass": "^1.97.1", "sigmund": "^1.0.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.50.1", + "typescript-eslint": "^8.51.0", "vite": "^7.3.0", "vite-plugin-compression": "^0.5.1", "vite-plugin-html": "^3.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85d18aae67..dc6a77aaec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,8 +119,8 @@ devDependencies: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.50.1 - version: 8.50.1(eslint@9.39.2)(typescript@5.9.3) + specifier: ^8.51.0 + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) vite: specifier: ^7.3.0 version: 7.3.0(@types/node@24.10.4)(sass@1.97.1) @@ -2267,40 +2267,40 @@ packages: resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} dev: true - /@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1)(eslint@9.39.2)(typescript@5.9.3): - resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==} + /@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0)(eslint@9.39.2)(typescript@5.9.3): + resolution: {integrity: sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.50.1 + '@typescript-eslint/parser': ^8.51.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.50.1(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.50.1 - '@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.1 + '@typescript-eslint/parser': 8.51.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.51.0 + '@typescript-eslint/type-utils': 8.51.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.51.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.51.0 eslint: 9.39.2 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.3.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3): - resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==} + /@typescript-eslint/parser@8.51.0(eslint@9.39.2)(typescript@5.9.3): + resolution: {integrity: sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' dependencies: - '@typescript-eslint/scope-manager': 8.50.1 - '@typescript-eslint/types': 8.50.1 - '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.1 + '@typescript-eslint/scope-manager': 8.51.0 + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.51.0 debug: 4.4.3 eslint: 9.39.2 typescript: 5.9.3 @@ -2308,30 +2308,30 @@ packages: - supports-color dev: true - /@typescript-eslint/project-service@8.50.1(typescript@5.9.3): - resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} + /@typescript-eslint/project-service@8.51.0(typescript@5.9.3): + resolution: {integrity: sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' dependencies: - '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) - '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/tsconfig-utils': 8.51.0(typescript@5.9.3) + '@typescript-eslint/types': 8.51.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@8.50.1: - resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} + /@typescript-eslint/scope-manager@8.51.0: + resolution: {integrity: sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@typescript-eslint/types': 8.50.1 - '@typescript-eslint/visitor-keys': 8.50.1 + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/visitor-keys': 8.51.0 dev: true - /@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3): - resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} + /@typescript-eslint/tsconfig-utils@8.51.0(typescript@5.9.3): + resolution: {integrity: sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -2339,71 +2339,71 @@ packages: typescript: 5.9.3 dev: true - /@typescript-eslint/type-utils@8.50.1(eslint@9.39.2)(typescript@5.9.3): - resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==} + /@typescript-eslint/type-utils@8.51.0(eslint@9.39.2)(typescript@5.9.3): + resolution: {integrity: sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' dependencies: - '@typescript-eslint/types': 8.50.1 - '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.51.0(eslint@9.39.2)(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.2 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.3.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types@8.50.1: - resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} + /@typescript-eslint/types@8.51.0: + resolution: {integrity: sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: true - /@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3): - resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} + /@typescript-eslint/typescript-estree@8.51.0(typescript@5.9.3): + resolution: {integrity: sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' dependencies: - '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) - '@typescript-eslint/types': 8.50.1 - '@typescript-eslint/visitor-keys': 8.50.1 + '@typescript-eslint/project-service': 8.51.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.51.0(typescript@5.9.3) + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/visitor-keys': 8.51.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.3.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@8.50.1(eslint@9.39.2)(typescript@5.9.3): - resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} + /@typescript-eslint/utils@8.51.0(eslint@9.39.2)(typescript@5.9.3): + resolution: {integrity: sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) - '@typescript-eslint/scope-manager': 8.50.1 - '@typescript-eslint/types': 8.50.1 - '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.51.0 + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/visitor-keys@8.50.1: - resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} + /@typescript-eslint/visitor-keys@8.51.0: + resolution: {integrity: sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/types': 8.51.0 eslint-visitor-keys: 4.2.1 dev: true @@ -4572,8 +4572,8 @@ packages: punycode: 2.3.1 dev: true - /ts-api-utils@2.1.0(typescript@5.9.3): - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + /ts-api-utils@2.3.0(typescript@5.9.3): + resolution: {integrity: sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4592,17 +4592,17 @@ packages: prelude-ls: 1.2.1 dev: true - /typescript-eslint@8.50.1(eslint@9.39.2)(typescript@5.9.3): - resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==} + /typescript-eslint@8.51.0(eslint@9.39.2)(typescript@5.9.3): + resolution: {integrity: sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' dependencies: - '@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1)(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 8.50.1(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.51.0(@typescript-eslint/parser@8.51.0)(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.51.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.51.0(eslint@9.39.2)(typescript@5.9.3) eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: From e584b6d2f249dd293a97e263b4506ac5581dae80 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Tue, 30 Dec 2025 13:58:26 +0100 Subject: [PATCH 005/286] refactor: migrated contact page (#3834) --- .docker/frankenphp/Caddyfile | 2 - .docker/nginx/default.conf | 4 +- CLAUDE.md | 44 +++++ nginx.conf | 2 +- phpmyfaq/.htaccess | 2 +- phpmyfaq/contact.php | 64 ------- phpmyfaq/index.php | 40 ++++- .../Controller/AbstractFrontController.php | 165 ++++++++++++++++++ .../phpMyFAQ/Controller/ContactController.php | 66 +++++++ .../phpMyFAQ/Controller/FrontController.php | 33 ---- phpmyfaq/src/public-routes.php | 19 +- .../Controller/FrontControllerTest.php | 28 --- 12 files changed, 331 insertions(+), 138 deletions(-) delete mode 100644 phpmyfaq/contact.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/AbstractFrontController.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/ContactController.php delete mode 100644 phpmyfaq/src/phpMyFAQ/Controller/FrontController.php delete mode 100644 tests/phpMyFAQ/Controller/FrontControllerTest.php diff --git a/.docker/frankenphp/Caddyfile b/.docker/frankenphp/Caddyfile index c587dc4743..03f745b927 100644 --- a/.docker/frankenphp/Caddyfile +++ b/.docker/frankenphp/Caddyfile @@ -34,7 +34,6 @@ rewrite /show-categories.html /index.php?action=show rewrite /search.html /index.php?action=search rewrite /open-questions.html /index.php?action=open-questions - rewrite /contact.html /index.php?action=contact rewrite /glossary.html /index.php?action=glossary rewrite /overview.html /index.php?action=overview rewrite /login.html /index.php?action=login @@ -164,7 +163,6 @@ rewrite /show-categories.html /index.php?action=show rewrite /search.html /index.php?action=search rewrite /open-questions.html /index.php?action=open-questions - rewrite /contact.html /index.php?action=contact rewrite /glossary.html /index.php?action=glossary rewrite /overview.html /index.php?action=overview rewrite /login.html /index.php?action=login diff --git a/.docker/nginx/default.conf b/.docker/nginx/default.conf index 0cf138f27b..e9ecdb1b67 100644 --- a/.docker/nginx/default.conf +++ b/.docker/nginx/default.conf @@ -78,7 +78,7 @@ server { rewrite ^/add-question\.html$ /index.php?action=ask last; rewrite ^/show-categories\.html$ /index.php?action=show last; rewrite ^/forgot-password/?$ /index.php?action=password last; - rewrite ^/(search|open-questions|help|contact|glossary|overview|login|privacy|index)\.html$ /index.php?action=$1 last; + rewrite ^/(search|open-questions|help|glossary|overview|login|privacy|index)\.html$ /index.php?action=$1 last; rewrite ^/(login)$ /index.php?action=login last; # a solution id page @@ -259,7 +259,7 @@ server { rewrite ^/add-question\.html$ /index.php?action=ask last; rewrite ^/show-categories\.html$ /index.php?action=show last; rewrite ^/forgot-password/?$ /index.php?action=password last; - rewrite ^/(search|open-questions|help|contact|glossary|overview|login|privacy|index)\.html$ /index.php?action=$1 last; + rewrite ^/(search|open-questions|help|glossary|overview|login|privacy|index)\.html$ /index.php?action=$1 last; rewrite ^/(login)$ /index.php?action=login last; # a solution id page diff --git a/CLAUDE.md b/CLAUDE.md index a4ab35294c..6bc76a8364 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,6 +70,50 @@ It is built using HTML5, CSS, TypeScript, and PHP and supports various databases - Use single quotes for strings. - Use arrow functions for callbacks. +## Routing System + +The application uses Symfony Router for modern, controller-based routing while maintaining backward compatibility with legacy code. + +### Architecture + +1. **index.php**: Entry point that tries Symfony Router first, falls back to legacy logic +2. **public-routes.php**: Route definitions using Symfony RouteCollection +3. **Controllers**: Modern Controller classes extending AbstractController + +### Adding New Routes + +To add a new route: + +1. Create a Controller in `phpmyfaq/src/phpMyFAQ/Controller/` +2. Add the route to `phpmyfaq/src/public-routes.php` +3. The Controller should extend `AbstractController` + +Example: + +```php +// MyController.php +final class MyController extends AbstractController +{ + public function index(Request $request): Response + { + return $this->render('template.twig', ['data' => 'value']); + } +} + +// public-routes.php +'public.my_route' => [ + 'path' => '/my-page.html', + 'controller' => [MyController::class, 'index'], + 'methods' => 'GET' +] +``` + +### Migration Strategy + +- New features should use Controllers +- Legacy code continues to work via a fallback mechanism +- Gradual migration from `?action=xyz` to route-based URLs + ## UI guidelines - Application should have a modern and clean design. diff --git a/nginx.conf b/nginx.conf index 43f2459b86..8831165f9c 100644 --- a/nginx.conf +++ b/nginx.conf @@ -100,7 +100,7 @@ server { rewrite ^/add-question\.html$ /index.php?action=ask last; rewrite ^/show-categories\.html$ /index.php?action=show last; rewrite ^/forgot-password/?$ /index.php?action=password last; - rewrite ^/(search|open-questions|help|contact|glossary|overview|login|privacy|index)\.html$ /index.php?action=$1 last; + rewrite ^/(search|open-questions|help|glossary|overview|login|privacy|index)\.html$ /index.php?action=$1 last; rewrite ^/(login)$ /index.php?action=login last; # a solution id page diff --git a/phpmyfaq/.htaccess b/phpmyfaq/.htaccess index cfe0d9b9ab..094ca1414c 100644 --- a/phpmyfaq/.htaccess +++ b/phpmyfaq/.htaccess @@ -109,7 +109,7 @@ Header set Access-Control-Allow-Headers "Content-Type, Authorization" RewriteRule ^add-question.html$ index.php?action=ask [L,QSA] RewriteRule ^show-categories.html$ index.php?action=show [L,QSA] RewriteRule ^forgot-password$ index.php?action=password [L,QSA] - RewriteRule ^(search|open-questions|contact|glossary|overview|login|privacy)\.html$ index.php?action=$1 [L,QSA] + RewriteRule ^(search|open-questions|glossary|overview|login|privacy)\.html$ index.php?action=$1 [L,QSA] RewriteRule ^(login) index.php?action=login [L,QSA] # start page RewriteRule ^index.html$ index.php [L,QSA] diff --git a/phpmyfaq/contact.php b/phpmyfaq/contact.php deleted file mode 100644 index 0c26265ed2..0000000000 --- a/phpmyfaq/contact.php +++ /dev/null @@ -1,64 +0,0 @@ - - * @copyright 2002-2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2002-09-16 - */ - -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); -$faqSession->userTracking('contact', 0); - -$captcha = $container->get('phpmyfaq.captcha'); - -$captchaHelper = $container->get('phpmyfaq.captcha.helper.captcha_helper'); - -if ($faqConfig->get('layout.contactInformationHTML')) { - $contactText = html_entity_decode((string) $faqConfig->get('main.contactInformation')); -} else { - $contactText = nl2br((string) $faqConfig->get('main.contactInformation')); -} - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./contact.twig'); - -// Twig template variables -$templateVars = [ - ...$templateVars, - 'title' => sprintf('%s - %s', Translation::get(key: 'msgContact'), $faqConfig->getTitle()), - 'msgContactOwnText' => $contactText, - 'privacyURL' => $faqConfig->get('main.privacyURL'), - 'lang' => $Language->getLanguage(), - 'defaultContentMail' => $user->getUserId() > 0 ? $user->getUserData('email') : '', - 'defaultContentName' => $user->getUserId() > 0 ? $user->getUserData('display_name') : '', - 'version' => $faqConfig->getVersion(), - 'captchaFieldset' => $captchaHelper->renderCaptcha( - $captcha, - 'contact', - Translation::get(key: 'msgCaptcha'), - $user->isLoggedIn(), - ), -]; - -return $templateVars; // diff --git a/phpmyfaq/index.php b/phpmyfaq/index.php index d418766e9f..940c4777e7 100755 --- a/phpmyfaq/index.php +++ b/phpmyfaq/index.php @@ -51,6 +51,9 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\Matcher\UrlMatcher; +use Symfony\Component\Routing\RequestContext; // // Define the named constant used as a check by any included PHP file @@ -114,6 +117,41 @@ // Strings::init($faqLangCode); +// +// Try Symfony Router first +// +try { + // Load routes + $routes = require __DIR__ . '/src/public-routes.php'; + + // Create URL matcher + $context = new RequestContext(); + $context->fromRequest($request); + $matcher = new UrlMatcher($routes, $context); + + // Try to match the current route + $parameters = $matcher->match($request->getPathInfo()); + + // Extract controller and method + $controllerCallable = $parameters['_controller']; + unset($parameters['_controller'], $parameters['_route'], $parameters['_methods']); + + // Instantiate controller and call method + if (is_array($controllerCallable)) { + [$controllerClass, $method] = $controllerCallable; + $controller = new $controllerClass(); + $routeResponse = $controller->$method($request, ...$parameters); + } else { + $routeResponse = $controllerCallable($request, ...$parameters); + } + + // Send response and exit + $routeResponse->send(); + exit; +} catch (ResourceNotFoundException $e) { + // No route matched - continue with legacy logic below +} + // // Set actual template set name // @@ -618,7 +656,7 @@ ]; // -// Show login box or logged-in user information +// Show the login box or logged-in user information // if ($user->isLoggedIn() && $user->getUserId() > 0) { if ( diff --git a/phpmyfaq/src/phpMyFAQ/Controller/AbstractFrontController.php b/phpmyfaq/src/phpMyFAQ/Controller/AbstractFrontController.php new file mode 100644 index 0000000000..699e7567ed --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/AbstractFrontController.php @@ -0,0 +1,165 @@ + + * @copyright 2024-2025 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2024-06-16 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller; + +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Enums\PermissionType; +use phpMyFAQ\Environment; +use phpMyFAQ\Helper\LanguageHelper; +use phpMyFAQ\System; +use phpMyFAQ\Translation; +use phpMyFAQ\Twig\TwigWrapper; +use Symfony\Component\HttpFoundation\Request; + +abstract class AbstractFrontController extends AbstractController +{ + /** + * @return string[] + * @throws Exception + * @throws \Exception + */ + protected function getHeader(Request $request): array + { + $faqSystem = $this->container->get(id: 'phpmyfaq.system'); + $seo = $this->container->get(id: 'phpmyfaq.seo'); + $action = $request->query->get(key: 'action', default: 'index'); + + $isUserHasAdminRights = $this->currentUser->perm->hasPermission( + $this->currentUser->getUserId(), + PermissionType::VIEW_ADMIN_LINK->value, + ); + + return [ + 'isMaintenanceMode' => $this->configuration->get('main.maintenanceMode'), + 'isCompletelySecured' => $this->configuration->get('security.enableLoginOnly'), + 'isDebugEnabled' => Environment::isDebugMode(), + 'richSnippetsEnabled' => $this->configuration->get('seo.enableRichSnippets'), + 'tplSetName' => TwigWrapper::getTemplateSetName(), + 'msgLoginUser' => $this->currentUser->isLoggedIn() + ? $this->currentUser->getUserData('display_name') + : Translation::get(key: 'msgLoginUser'), + 'isUserLoggedIn' => $this->currentUser->isLoggedIn(), + 'isUserHasAdminRights' => $isUserHasAdminRights || $this->currentUser->isSuperAdmin(), + 'baseHref' => $faqSystem->getSystemUri($this->configuration), + 'customCss' => $this->configuration->getCustomCss(), + 'version' => $this->configuration->getVersion(), + 'header' => str_replace('"', '', $this->configuration->getTitle()), + 'metaDescription' => $metaDescription ?? $this->configuration->get('seo.description'), + 'metaPublisher' => $this->configuration->get('main.metaPublisher'), + 'metaLanguage' => Translation::get(key: 'metaLanguage'), + 'metaRobots' => $seo->getMetaRobots($action), + 'phpmyfaqVersion' => $this->configuration->getVersion(), + 'stylesheet' => Translation::get(key: 'direction') == 'rtl' ? 'style.rtl' : 'style', + 'currentPageUrl' => $request->getSchemeAndHttpHost() . $request->getRequestUri(), + 'action' => $action, + 'dir' => Translation::get(key: 'direction'), + 'formActionUrl' => './search', + 'searchBox' => Translation::get(key: 'msgSearch'), + 'languageBox' => Translation::get(key: 'msgLanguageSubmit'), + 'switchLanguages' => LanguageHelper::renderSelectLanguage( + $this->configuration->getLanguage()->getLanguage(), + true, + ), + 'copyright' => System::getPoweredByString(), + 'isUserRegistrationEnabled' => $this->configuration->get('security.enableRegistration'), + 'pluginStylesheets' => $this->configuration->getPluginManager()->getAllPluginStylesheets(), + 'pluginScripts' => $this->configuration->getPluginManager()->getAllPluginScripts(), + 'msgRegisterUser' => Translation::get(key: 'msgRegisterUser'), + 'sendPassword' => + '' + . Translation::get(key: 'lostPassword') + . '', + 'msgFullName' => Translation::get(key: 'ad_user_loggedin') . $this->currentUser->getLogin(), + 'msgLoginName' => $this->currentUser->getUserData('display_name'), + 'loginHeader' => Translation::get(key: 'msgLoginUser'), + 'msgAdvancedSearch' => Translation::get(key: 'msgAdvancedSearch'), + 'currentYear' => date(format: 'Y', timestamp: time()), + 'cookieConsentEnabled' => $this->configuration->get('layout.enableCookieConsent'), + 'faqHome' => $this->configuration->getDefaultUrl(), + 'topNavigation' => $this->getTopNavigation($request), + 'isAskQuestionsEnabled' => $this->configuration->get('main.enableAskQuestions'), + 'isOpenQuestionsEnabled' => $this->configuration->get('main.enableAskQuestions'), + 'footerNavigation' => $this->getFooterNavigation($request), + 'isPrivacyLinkEnabled' => $this->configuration->get('layout.enablePrivacyLink'), + 'urlPrivacyLink' => $this->configuration->get('main.privacyURL'), + 'msgPrivacyNote' => Translation::get(key: 'msgPrivacyNote'), + 'isCookieConsentEnabled' => $this->configuration->get('layout.enableCookieConsent'), + 'cookiePreferences' => Translation::get(key: 'cookiePreferences'), + ]; + } + + private function getTopNavigation(Request $request): array + { + $action = $request->query->get(key: 'action', default: 'index'); + + return [ + [ + 'name' => Translation::get(key: 'msgShowAllCategories'), + 'link' => './show-categories.html', + 'active' => 'show' === $action ? 'active' : '', + ], + [ + 'name' => Translation::get(key: 'msgAddContent'), + 'link' => './add-faq.html', + 'active' => 'add' === $action ? 'active' : '', + ], + [ + 'name' => Translation::get(key: 'msgQuestion'), + 'link' => './add-question.html', + 'active' => 'ask' == $action ? 'active' : '', + ], + [ + 'name' => Translation::get(key: 'msgOpenQuestions'), + 'link' => './open-questions.html', + 'active' => 'open-questions' == $action ? 'active' : '', + ], + ]; + } + + private function getFooterNavigation(Request $request): array + { + $action = $request->query->get(key: 'action', default: 'index'); + + return [ + [ + 'name' => Translation::get(key: 'faqOverview'), + 'link' => './overview.html', + 'active' => 'faq-overview' == $action ? 'active' : '', + ], + [ + 'name' => Translation::get(key: 'msgSitemap'), + 'link' => './sitemap/A/' . $this->configuration->getLanguage()->getLanguage() . '.html', + 'active' => 'sitemap' == $action ? 'active' : '', + ], + [ + 'name' => Translation::get(key: 'ad_menu_glossary'), + 'link' => './glossary.html', + 'active' => 'glossary' == $action ? 'active' : '', + ], + [ + 'name' => Translation::get(key: 'msgContact'), + 'link' => './contact.html', + 'active' => 'contact' == $action ? 'active' : '', + ], + ]; + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php b/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php new file mode 100644 index 0000000000..cb791d171d --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php @@ -0,0 +1,66 @@ + + * @copyright 2002-2025 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2002-09-16 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller; + +use phpMyFAQ\Translation; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +final class ContactController extends AbstractFrontController +{ + /** + * Handles both GET and POST requests for the contact form + *@throws \Exception + */ + public function index(Request $request): Response + { + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('contact', 0); + + $captcha = $this->container->get('phpmyfaq.captcha'); + $captchaHelper = $this->container->get('phpmyfaq.captcha.helper.captcha_helper'); + + if ($this->configuration->get('layout.contactInformationHTML')) { + $contactText = html_entity_decode((string) $this->configuration->get('main.contactInformation')); + } else { + $contactText = nl2br((string) $this->configuration->get('main.contactInformation')); + } + + return $this->render('contact.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'msgContact'), $this->configuration->getTitle()), + 'msgContactOwnText' => $contactText, + 'privacyURL' => $this->configuration->get('main.privacyURL'), + 'lang' => $this->configuration->getLanguage()->getLanguage(), + 'defaultContentMail' => $this->currentUser->getUserId() > 0 ? $this->currentUser->getUserData('email') : '', + 'defaultContentName' => $this->currentUser->getUserId() > 0 + ? $this->currentUser->getUserData('display_name') + : '', + 'version' => $this->configuration->getVersion(), + 'captchaFieldset' => $captchaHelper->renderCaptcha( + $captcha, + 'contact', + Translation::get(key: 'msgCaptcha'), + $this->currentUser->isLoggedIn(), + ), + ]); + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/FrontController.php b/phpmyfaq/src/phpMyFAQ/Controller/FrontController.php deleted file mode 100644 index 2266a1c4c3..0000000000 --- a/phpmyfaq/src/phpMyFAQ/Controller/FrontController.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @copyright 2024-2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2024-06-16 - */ - -declare(strict_types=1); - -namespace phpMyFAQ\Controller; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Attribute\Route; - -final class FrontController extends AbstractController -{ - #[Route(path: '/{path}', name: 'front_controller', requirements: ['path' => '.+'])] - public function handle(Request $request, string $path): Response - { - return new Response(content: 'Handled by FrontController: ' . $path); - } -} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 1d655e9ca4..99fa4811b6 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -17,19 +17,26 @@ declare(strict_types=1); +use phpMyFAQ\Controller\ContactController; use phpMyFAQ\Controller\FrontController; -use phpMyFAQ\Controller\WebAuthnController; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; $routes = new RouteCollection(); $routesConfig = [ - 'public.index' => [ - 'path' => '/', - 'controller' => [FrontController::class, 'handle'], - 'methods' => 'GET' - ] + // Specific routes should come first + 'public.contact' => [ + 'path' => '/contact.html', + 'controller' => [ContactController::class, 'index'], + 'methods' => 'GET|POST' + ], + // Fallback route should be last + // 'public.index' => [ + // 'path' => '/', + // 'controller' => [FrontController::class, 'handle'], + // 'methods' => 'GET' + // ], ]; foreach ($routesConfig as $name => $config) { diff --git a/tests/phpMyFAQ/Controller/FrontControllerTest.php b/tests/phpMyFAQ/Controller/FrontControllerTest.php deleted file mode 100644 index 85e70851fe..0000000000 --- a/tests/phpMyFAQ/Controller/FrontControllerTest.php +++ /dev/null @@ -1,28 +0,0 @@ -createStub(Request::class); - $path = 'some/test/path'; - - $controller = new FrontController(); - $response = $controller->handle($request, $path); - - $this->assertInstanceOf(Response::class, $response); - $this->assertEquals('Handled by FrontController: ' . $path, $response->getContent()); - } -} From 46e7f90fa8dadc752e97bab2b1ea6b99923f8f4a Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Wed, 31 Dec 2025 11:17:49 +0100 Subject: [PATCH 006/286] refactor: migrated 404 error page (#3834) --- .docker/nginx/default.conf | 4 +- nginx.conf | 2 +- phpmyfaq/.htaccess | 2 +- phpmyfaq/404.php | 34 ------------- phpmyfaq/index.php | 13 ++++- .../Controller/PageNotFoundController.php | 48 +++++++++++++++++++ phpmyfaq/src/public-routes.php | 7 ++- 7 files changed, 70 insertions(+), 40 deletions(-) delete mode 100644 phpmyfaq/404.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php diff --git a/.docker/nginx/default.conf b/.docker/nginx/default.conf index e9ecdb1b67..fbe80ff7f9 100644 --- a/.docker/nginx/default.conf +++ b/.docker/nginx/default.conf @@ -64,7 +64,7 @@ server { } location @error404 { - rewrite ^ /index.php?action=404 last; + rewrite ^ /404.html last; } location / { @@ -245,7 +245,7 @@ server { } location @error404 { - rewrite ^ /index.php?action=404 last; + rewrite ^ /404.html last; } location / { diff --git a/nginx.conf b/nginx.conf index 8831165f9c..10c57dca1f 100644 --- a/nginx.conf +++ b/nginx.conf @@ -86,7 +86,7 @@ server { } location @error404 { - rewrite ^ /index.php?action=404 last; + rewrite ^ /404.html last; } location / { diff --git a/phpmyfaq/.htaccess b/phpmyfaq/.htaccess index 094ca1414c..718c10002a 100644 --- a/phpmyfaq/.htaccess +++ b/phpmyfaq/.htaccess @@ -103,7 +103,7 @@ Header set Access-Control-Allow-Headers "Content-Type, Authorization" # Exclude assets from being handled by Symfony Router RewriteRule ^(admin/assets)($|/) - [L] # Error pages - ErrorDocument 404 /index.php?action=404 + ErrorDocument 404 /404.html # General pages RewriteRule ^add-faq.html$ index.php?action=add [L,QSA] RewriteRule ^add-question.html$ index.php?action=ask [L,QSA] diff --git a/phpmyfaq/404.php b/phpmyfaq/404.php deleted file mode 100644 index fd9ab6f0b4..0000000000 --- a/phpmyfaq/404.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @copyright 2019-2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2019-01-25 - */ - -use phpMyFAQ\Enums\SessionActionType; -use phpMyFAQ\Twig\TwigWrapper; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); -$faqSession->userTracking(SessionActionType::NOT_FOUND->value, 0); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./404.twig'); diff --git a/phpmyfaq/index.php b/phpmyfaq/index.php index 940c4777e7..039c3e3416 100755 --- a/phpmyfaq/index.php +++ b/phpmyfaq/index.php @@ -23,6 +23,7 @@ use phpMyFAQ\Attachment\AttachmentFactory; use phpMyFAQ\Category; use phpMyFAQ\Category\Relation; +use phpMyFAQ\Controller\PageNotFoundController; use phpMyFAQ\Core\Exception; use phpMyFAQ\Entity\SeoEntity; use phpMyFAQ\Enums\PermissionType; @@ -683,6 +684,16 @@ $includePhp = 'login.php'; } +// +// Handle 404 action with PageNotFoundController +// +if ('404' === $action) { + $pageNotFoundController = new \phpMyFAQ\Controller\PageNotFoundController(); + $notFoundResponse = $pageNotFoundController->index($request); + $notFoundResponse->send(); + exit; +} + // // Include requested PHP file // @@ -696,7 +707,7 @@ // // Check for 404 HTTP status code // -if ($response->getStatusCode() === Response::HTTP_NOT_FOUND || $action === '404') { +if ($response->getStatusCode() === Response::HTTP_NOT_FOUND) { $response->setStatusCode(Response::HTTP_NOT_FOUND); } diff --git a/phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php b/phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php new file mode 100644 index 0000000000..8f83435bad --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php @@ -0,0 +1,48 @@ + + * @copyright 2019-2025 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2019-01-25 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller; + +use phpMyFAQ\Enums\SessionActionType; +use phpMyFAQ\Translation; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +final class PageNotFoundController extends AbstractFrontController +{ + /** + * Handles the 404 Not Found page + * @throws \Exception + */ + public function index(Request $request): Response + { + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking(SessionActionType::NOT_FOUND->value, 0); + + $response = $this->render('404.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'msgError404'), $this->configuration->getTitle()), + ]); + + $response->setStatusCode(Response::HTTP_NOT_FOUND); + + return $response; + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 99fa4811b6..0d9013f107 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -18,7 +18,7 @@ declare(strict_types=1); use phpMyFAQ\Controller\ContactController; -use phpMyFAQ\Controller\FrontController; +use phpMyFAQ\Controller\PageNotFoundController; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -31,6 +31,11 @@ 'controller' => [ContactController::class, 'index'], 'methods' => 'GET|POST' ], + 'public.404' => [ + 'path' => '/404.html', + 'controller' => [PageNotFoundController::class, 'index'], + 'methods' => 'GET' + ], // Fallback route should be last // 'public.index' => [ // 'path' => '/', From 47b76368873f240a3b816021fa3a23ff790fd70d Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Wed, 31 Dec 2025 11:24:11 +0100 Subject: [PATCH 007/286] fix: corrected environment configurator (#3834) --- phpmyfaq/src/phpMyFAQ/Setup/EnvironmentConfigurator.php | 4 ++-- tests/phpMyFAQ/Setup/EnvironmentConfiguratorTest.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/phpmyfaq/src/phpMyFAQ/Setup/EnvironmentConfigurator.php b/phpmyfaq/src/phpMyFAQ/Setup/EnvironmentConfigurator.php index 08f018cb1f..dc39a6b3bf 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/EnvironmentConfigurator.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/EnvironmentConfigurator.php @@ -73,7 +73,7 @@ public function getRewriteBase(): string * This method ensures that URL routing works correctly and 404 errors are properly handled. * * - RewriteBase is set to the application's installation path (e.g., /faq/) - * - ErrorDocument 404 is configured to route errors to the application's error handler (e.g., /faq/index.php?action=404) + * - ErrorDocument 404 is configured to route errors to the application's error handler (e.g., /404.html) * * @return bool Returns true if the .htaccess file was successfully modified, false otherwise. * @throws Exception If the .htaccess file does not exist or contains syntax errors during parsing. @@ -110,7 +110,7 @@ public function adjustRewriteBaseHtaccess(): bool $errorDocument404->removeArgument($arg); } // Set new arguments: error code and path - $new404Path = rtrim($this->getServerPath(), '/') . '/index.php?action=404'; + $new404Path = rtrim($this->getServerPath(), '/') . '/404.html'; $errorDocument404->setArguments(['404', $new404Path]); } diff --git a/tests/phpMyFAQ/Setup/EnvironmentConfiguratorTest.php b/tests/phpMyFAQ/Setup/EnvironmentConfiguratorTest.php index a3d584cfb0..6faf4fe11d 100644 --- a/tests/phpMyFAQ/Setup/EnvironmentConfiguratorTest.php +++ b/tests/phpMyFAQ/Setup/EnvironmentConfiguratorTest.php @@ -102,7 +102,7 @@ public function testAdjustRewriteBaseHtaccessUpdatesErrorDocumentWithRootPath(): RewriteEngine On RewriteBase /phpmyfaq-test/ - ErrorDocument 404 /phpmyfaq-test/index.php?action=404 + ErrorDocument 404 /404.html HTACCESS; file_put_contents($htaccessPath, $htaccessContent); @@ -115,7 +115,7 @@ public function testAdjustRewriteBaseHtaccessUpdatesErrorDocumentWithRootPath(): // Read the .htaccess file and verify ErrorDocument 404 is set correctly $htaccessContent = file_get_contents($htaccessPath); - $this->assertStringContainsString('ErrorDocument 404 /index.php?action=404', $htaccessContent); + $this->assertStringContainsString('ErrorDocument 404 /404.html', $htaccessContent); } /** @@ -130,7 +130,7 @@ public function testAdjustRewriteBaseHtaccessUpdatesErrorDocumentWithSubdirector RewriteEngine On RewriteBase / - ErrorDocument 404 /index.php?action=404 + ErrorDocument 404 /404.html HTACCESS; file_put_contents($htaccessPath, $htaccessContent); @@ -143,6 +143,6 @@ public function testAdjustRewriteBaseHtaccessUpdatesErrorDocumentWithSubdirector // Read the .htaccess file and verify ErrorDocument 404 is set correctly $htaccessContent = file_get_contents($htaccessPath); - $this->assertStringContainsString('ErrorDocument 404 /faq/index.php?action=404', $htaccessContent); + $this->assertStringContainsString('ErrorDocument 404 /faq/404.html', $htaccessContent); } } From 976a1c3ed82bcefe438b97d6c76cb1973a5c5d89 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Wed, 31 Dec 2025 12:16:00 +0100 Subject: [PATCH 008/286] refactor: migrated sitemap.xml, robots.txt, llms.txt, and webauthn pages (#3834) --- phpmyfaq/llms.txt.php | 50 ------------------- phpmyfaq/robots.txt.php | 50 ------------------- phpmyfaq/sitemap.xml.php | 50 ------------------- .../phpMyFAQ/Controller/ContactController.php | 4 +- .../phpMyFAQ/Controller/LlmsController.php | 5 +- .../Controller/PageNotFoundController.php | 5 +- .../phpMyFAQ/Controller/RobotsController.php | 5 +- .../phpMyFAQ/Controller/SitemapController.php | 2 + .../Controller/WebAuthnController.php | 4 +- phpmyfaq/src/phpMyFAQ/Link.php | 1 - phpmyfaq/src/public-routes.php | 31 +++++++++--- 11 files changed, 43 insertions(+), 164 deletions(-) delete mode 100644 phpmyfaq/llms.txt.php delete mode 100644 phpmyfaq/robots.txt.php delete mode 100644 phpmyfaq/sitemap.xml.php diff --git a/phpmyfaq/llms.txt.php b/phpmyfaq/llms.txt.php deleted file mode 100644 index c1f3cf4076..0000000000 --- a/phpmyfaq/llms.txt.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @copyright 2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2025-07-07 - */ - -use phpMyFAQ\Application; -use phpMyFAQ\Controller\LlmsController; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; - -require __DIR__ . '/src/Bootstrap.php'; - -// -// Service Containers -// -$container = new ContainerBuilder(); -$loader = new PhpFileLoader($container, new FileLocator(__DIR__)); -try { - $loader->load('src/services.php'); -} catch (Exception $exception) { - echo $exception->getMessage(); -} - -$routes = new RouteCollection(); -$routes->add('public.llms.txt', new Route('/llms.txt', ['_controller' => [LlmsController::class, 'index']])); - -$app = new Application($container); -try { - $app->run($routes); -} catch (Exception $exception) { - echo $exception->getMessage(); -} \ No newline at end of file diff --git a/phpmyfaq/robots.txt.php b/phpmyfaq/robots.txt.php deleted file mode 100644 index ee00941209..0000000000 --- a/phpmyfaq/robots.txt.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @copyright 2024-2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2024-11-17 - */ - -use phpMyFAQ\Application; -use phpMyFAQ\Controller\RobotsController; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; - -require __DIR__ . '/src/Bootstrap.php'; - -// -// Service Containers -// -$container = new ContainerBuilder(); -$loader = new PhpFileLoader($container, new FileLocator(__DIR__)); -try { - $loader->load('src/services.php'); -} catch (Exception $exception) { - echo $exception->getMessage(); -} - -$routes = new RouteCollection(); -$routes->add('public.robots.txt', new Route('/robots.txt', ['_controller' => [RobotsController::class, 'index']])); - -$app = new Application($container); -try { - $app->run($routes); -} catch (Exception $exception) { - echo $exception->getMessage(); -} diff --git a/phpmyfaq/sitemap.xml.php b/phpmyfaq/sitemap.xml.php deleted file mode 100644 index 3aea09b99f..0000000000 --- a/phpmyfaq/sitemap.xml.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @copyright 2006-2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2006-06-26 - */ - -use phpMyFAQ\Application; -use phpMyFAQ\Controller\SitemapController; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; - -require __DIR__ . '/src/Bootstrap.php'; - -// -// Service Containers -// -$container = new ContainerBuilder(); -$loader = new PhpFileLoader($container, new FileLocator(__DIR__)); -try { - $loader->load('src/services.php'); -} catch (\Exception $exception) { - echo $exception->getMessage(); -} - -$routes = new RouteCollection(); -$routes->add('public.sitemap.xml', new Route('/sitemap.xml', ['_controller' => [SitemapController::class, 'index']])); - -$app = new Application($container); -try { - $app->run($routes); -} catch (Exception $exception) { - echo $exception->getMessage(); -} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php b/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php index cb791d171d..5b24ada887 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php @@ -19,6 +19,7 @@ namespace phpMyFAQ\Controller; +use Exception; use phpMyFAQ\Translation; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -27,8 +28,9 @@ final class ContactController extends AbstractFrontController { /** * Handles both GET and POST requests for the contact form - *@throws \Exception + * @throws Exception */ + #[Route(path: '/contact.html', name: 'public.contact')] public function index(Request $request): Response { $faqSession = $this->container->get('phpmyfaq.user.session'); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/LlmsController.php b/phpmyfaq/src/phpMyFAQ/Controller/LlmsController.php index e977c50e76..5acf5cc54b 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/LlmsController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/LlmsController.php @@ -19,13 +19,16 @@ namespace phpMyFAQ\Controller; +use Exception; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; final class LlmsController extends AbstractController { /** - * @throws \Exception + * @throws Exception */ + #[Route(path: '/llms.txt', name: 'public.llms.index')] public function index(): Response { $response = new Response(); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php b/phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php index 8f83435bad..6870b76147 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php @@ -19,17 +19,20 @@ namespace phpMyFAQ\Controller; +use Exception; use phpMyFAQ\Enums\SessionActionType; use phpMyFAQ\Translation; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; final class PageNotFoundController extends AbstractFrontController { /** * Handles the 404 Not Found page - * @throws \Exception + * @throws Exception */ + #[Route(path: '/404.html', name: 'public.404')] public function index(Request $request): Response { $faqSession = $this->container->get('phpmyfaq.user.session'); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/RobotsController.php b/phpmyfaq/src/phpMyFAQ/Controller/RobotsController.php index 17cb0226cb..370f5086f0 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/RobotsController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/RobotsController.php @@ -19,13 +19,16 @@ namespace phpMyFAQ\Controller; +use Exception; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; final class RobotsController extends AbstractController { /** - * @throws \Exception + * @throws Exception */ + #[Route(path: '/robots.txt', name: 'public.robots.index')] public function index(): Response { $response = new Response(); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/SitemapController.php b/phpmyfaq/src/phpMyFAQ/Controller/SitemapController.php index 1e5777a12f..2c585f2e7f 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/SitemapController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/SitemapController.php @@ -22,6 +22,7 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Twig\TemplateException; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; final class SitemapController extends AbstractController { @@ -30,6 +31,7 @@ final class SitemapController extends AbstractController /** * @throws TemplateException|Exception|\Exception */ + #[Route(path: '/sitemap.xml', name: 'public.sitemap.xml')] public function index(): Response { $response = new Response(); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php b/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php index d4e1d8aac5..5f7c39fca2 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php @@ -27,7 +27,7 @@ use phpMyFAQ\Twig\TwigWrapper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Twig\Error\LoaderError; final class WebAuthnController extends AbstractController @@ -35,7 +35,7 @@ final class WebAuthnController extends AbstractController /** * @throws Exception|LoaderError */ - #[Route(path: '/', name: 'public.webauthn.index')] + #[Route(path: '/services/webauthn', name: 'public.webauthn.index')] public function index(Request $request): Response { $system = new System(); diff --git a/phpmyfaq/src/phpMyFAQ/Link.php b/phpmyfaq/src/phpMyFAQ/Link.php index 1d5405cba9..f0b07aff05 100755 --- a/phpmyfaq/src/phpMyFAQ/Link.php +++ b/phpmyfaq/src/phpMyFAQ/Link.php @@ -183,7 +183,6 @@ class Link 'thankyou' => 1, 'twofactor' => 1, 'ucp' => 1, - '404' => 1, 'bookmarks' => 1, ]; diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 0d9013f107..704b6fc8c6 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -18,14 +18,17 @@ declare(strict_types=1); use phpMyFAQ\Controller\ContactController; +use phpMyFAQ\Controller\LlmsController; use phpMyFAQ\Controller\PageNotFoundController; +use phpMyFAQ\Controller\RobotsController; +use phpMyFAQ\Controller\SitemapController; +use phpMyFAQ\Controller\WebAuthnController; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; $routes = new RouteCollection(); $routesConfig = [ - // Specific routes should come first 'public.contact' => [ 'path' => '/contact.html', 'controller' => [ContactController::class, 'index'], @@ -36,12 +39,26 @@ 'controller' => [PageNotFoundController::class, 'index'], 'methods' => 'GET' ], - // Fallback route should be last - // 'public.index' => [ - // 'path' => '/', - // 'controller' => [FrontController::class, 'handle'], - // 'methods' => 'GET' - // ], + 'public.llms.txt' => [ + 'path' => '/llms.txt', + 'controller' => [LlmsController::class, 'index'], + 'methods' => 'GET' + ], + 'public.robots.txt' => [ + 'path' => '/robots.txt', + 'controller' => [RobotsController::class, 'index'], + 'methods' => 'GET' + ], + 'public.sitemap.xml' => [ + 'path' => '/sitemap.xml', + 'controller' => [SitemapController::class, 'index'], + 'methods' => 'GET' + ], + 'public.webauthn.index' => [ + 'path' => '/services/webauthn', + 'controller' => [WebAuthnController::class, 'index'], + 'methods' => 'GET' + ], ]; foreach ($routesConfig as $name => $config) { From f1dc4cdf4bd686825e339ac6c9b6515cae72b28b Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Wed, 31 Dec 2025 12:33:53 +0100 Subject: [PATCH 009/286] fix: corrected .htaccess and nginx configuration for new routes (#3834) --- .docker/nginx/default.conf | 28 +++--- nginx.conf | 34 ++++---- phpmyfaq/.htaccess | 12 +-- phpmyfaq/services/webauthn/index.php | 50 ----------- .../Controller/WebAuthnController.php | 85 +------------------ phpmyfaq/src/public-routes.php | 2 +- 6 files changed, 40 insertions(+), 171 deletions(-) delete mode 100644 phpmyfaq/services/webauthn/index.php diff --git a/.docker/nginx/default.conf b/.docker/nginx/default.conf index fbe80ff7f9..f1d8fbce9c 100644 --- a/.docker/nginx/default.conf +++ b/.docker/nginx/default.conf @@ -103,24 +103,24 @@ server { rewrite ^/sitemap/([^\/]+)/([a-z\-_]+)\.htm(l?)$ /index.php?action=sitemap&letter=$1&lang=$2 last; # PMF Google sitemap - rewrite ^/sitemap\.xml$ /sitemap.xml.php last; - rewrite ^/sitemap\.gz$ /sitemap.xml.php?gz=1 last; - rewrite ^/sitemap\.xml\.gz$ /sitemap.xml.php?gz=1 last; + rewrite ^/sitemap\.xml$ /index.php last; + rewrite ^/sitemap\.gz$ /index.php last; + rewrite ^/sitemap\.xml\.gz$ /index.php last; # robots.txt - rewrite ^/robots\.txt$ /robots.txt.php last; + rewrite ^/robots\.txt$ /index.php last; # llms.txt - rewrite ^/llms\.txt$ /llms.txt.php last; + rewrite ^/llms\.txt$ /index.php last; # PMF tags page with page count - rewrite ^/tags/([0-9]+)/([0-9]+)/(.+)\.htm(l?)$ /index.php?action=search&tagging_id=$1&seite=$2 last; + rewrite ^/tags/([0-9]+)/([0-9]+)/(.+)\.htm(l?)$ /index.php?action=search&tagging_id=$1&seite=$2 last; # PMF tags page rewrite ^/tags/([0-9]+)/([^\/]+)\.htm(l?)$ /index.php?action=search&tagging_id=$1 last; # Authentication services - rewrite ^/services/webauthn/(.*)$ /services/webauthn/index.php last; + rewrite ^/services/webauthn/(.*)$ /index.php last; # User pages rewrite ^/user/(ucp|bookmarks|request-removal|logout|register)$ /index.php?action=$1 last; @@ -284,24 +284,24 @@ server { rewrite ^/sitemap/([^\/]+)/([a-z\-_]+)\.htm(l?)$ /index.php?action=sitemap&letter=$1&lang=$2 last; # PMF Google sitemap - rewrite ^/sitemap\.xml$ /sitemap.xml.php last; - rewrite ^/sitemap\.gz$ /sitemap.xml.php?gz=1 last; - rewrite ^/sitemap\.xml\.gz$ /sitemap.xml.php?gz=1 last; + rewrite ^/sitemap\.xml$ /index.php last; + rewrite ^/sitemap\.gz$ /index.php last; + rewrite ^/sitemap\.xml\.gz$ /index.php last; # robots.txt - rewrite ^/robots\.txt$ /robots.txt.php last; + rewrite ^/robots\.txt$ /index.php last; # llms.txt - rewrite ^/llms\.txt$ /llms.txt.php last; + rewrite ^/llms\.txt$ /index.php last; # PMF tags page with page count - rewrite ^/tags/([0-9]+)/([0-9]+)/(.+)\.htm(l?)$ /index.php?action=search&tagging_id=$1&seite=$2 last; + rewrite ^/tags/([0-9]+)/([0-9]+)/(.+)\.htm(l?)$ /index.php?action=search&tagging_id=$1&seite=$2 last; # PMF tags page rewrite ^/tags/([0-9]+)/([^\/]+)\.htm(l?)$ /index.php?action=search&tagging_id=$1 last; # Authentication services - rewrite ^/services/webauthn(.*)$ /services/webauthn/index.php last; + rewrite ^/services/webauthn(.*)$ /index.php last; # User pages rewrite ^/user/(ucp|bookmarks|request-removal|logout|register)$ /index.php?action=$1 last; diff --git a/nginx.conf b/nginx.conf index 10c57dca1f..1ed890fcae 100644 --- a/nginx.conf +++ b/nginx.conf @@ -96,53 +96,53 @@ server { location @rewriteapp { # General pages - rewrite ^/add-faq\.html$ /index.php?action=add last; - rewrite ^/add-question\.html$ /index.php?action=ask last; - rewrite ^/show-categories\.html$ /index.php?action=show last; + rewrite ^/add-faq\.html$ /index.php?action=add last; + rewrite ^/add-question\.html$ /index.php?action=ask last; + rewrite ^/show-categories\.html$ /index.php?action=show last; rewrite ^/forgot-password/?$ /index.php?action=password last; rewrite ^/(search|open-questions|help|glossary|overview|login|privacy|index)\.html$ /index.php?action=$1 last; - rewrite ^/(login)$ /index.php?action=login last; + rewrite ^/(login)$ /index.php?action=login last; # a solution id page - rewrite ^/solution_id_([0-9]+)\.html$ /index.php?solution_id=$1 last; + rewrite ^/solution_id_([0-9]+)\.html$ /index.php?solution_id=$1 last; # the bookmarks page - rewrite ^/bookmarks\.html$ /index.php?action=bookmarks last; + rewrite ^/bookmarks\.html$ /index.php?action=bookmarks last; # PMF faq record page rewrite ^/content/([0-9]+)/([0-9]+)/([a-z\-_]+)/(.+)\.htm(l?)$ /index.php?action=faq&cat=$1&id=$2&artlang=$3 last; # PMF category page with page count - rewrite ^/category/([0-9]+)/([0-9]+)/(.+)\.html$ /index.php?action=show&cat=$1&seite=$2 last; + rewrite ^/category/([0-9]+)/([0-9]+)/(.+)\.html$ /index.php?action=show&cat=$1&seite=$2 last; # PMF category page - rewrite ^/category/([0-9]+)/(.+)\.html$ /index.php?action=show&cat=$1 last; + rewrite ^/category/([0-9]+)/(.+)\.html$ /index.php?action=show&cat=$1 last; # PMF news page - rewrite ^/news/([0-9]+)/([a-z\-_]+)/(.+)\.html$ /index.php?action=news&newsid=$1&newslang=$2 last; + rewrite ^/news/([0-9]+)/([a-z\-_]+)/(.+)\.html$ /index.php?action=news&newsid=$1&newslang=$2 last; # PMF sitemap rewrite ^/sitemap/([^\/]+)/([a-z\-_]+)\.htm(l?)$ /index.php?action=sitemap&letter=$1&lang=$2 last; # PMF Google sitemap - rewrite ^/sitemap\.xml$ /sitemap.xml.php last; - rewrite ^/sitemap\.gz$ /sitemap.xml.php?gz=1 last; - rewrite ^/sitemap\.xml\.gz$ /sitemap.xml.php?gz=1 last; + rewrite ^/sitemap\.xml$ /index.php last; + rewrite ^/sitemap\.gz$ /index.php last; + rewrite ^/sitemap\.xml\.gz$ /index.php last; # robots.txt - rewrite ^/robots\.txt$ /robots.txt.php last; + rewrite ^/robots\.txt$ /index.php last; # llms.txt - rewrite ^/llms\.txt$ /llms.txt.php last; + rewrite ^/llms\.txt$ /index.php last; # PMF tags page with page count - rewrite ^/tags/([0-9]+)/([0-9]+)/(.+)\.htm(l?)$ /index.php?action=search&tagging_id=$1&seite=$2 last; + rewrite ^/tags/([0-9]+)/([0-9]+)/(.+)\.htm(l?)$ /index.php?action=search&tagging_id=$1&seite=$2 last; # PMF tags page rewrite ^/tags/([0-9]+)/([^\/]+)\.htm(l?)$ /index.php?action=search&tagging_id=$1 last; # Authentication services - rewrite ^/services/webauthn/(.*)$ /services/webauthn/index.php last; + rewrite ^/services/webauthn/(.*)$ /index.php last; # User pages rewrite ^/user/(ucp|bookmarks|request-removal|logout|register)$ /index.php?action=$1 last; @@ -154,7 +154,7 @@ server { rewrite ^/admin/(.*)$ /admin/index.php last; # REST API v3.0 and v3.1 - rewrite ^/api/v3\.[01]/(.*)$ /api/index.php last; + rewrite ^/api/v3\.[01]/(.*)$ /api/index.php last; # Private APIs rewrite ^/api/(.*)$ /api/index.php last; diff --git a/phpmyfaq/.htaccess b/phpmyfaq/.htaccess index 718c10002a..a65a4319ef 100644 --- a/phpmyfaq/.htaccess +++ b/phpmyfaq/.htaccess @@ -129,19 +129,19 @@ Header set Access-Control-Allow-Headers "Content-Type, Authorization" # phpMyFAQ sitemap RewriteRule ^sitemap/([^/]+)/([a-z\-_]+)\.htm(l?)$ index.php?action=sitemap&letter=$1&lang=$2 [L,QSA] # phpMyFAQ Google sitemap - RewriteRule ^sitemap.xml$ sitemap.xml.php [L,QSA] - RewriteRule ^sitemap.gz$ sitemap.xml.php?gz=1 [L,QSA] - RewriteRule ^sitemap.xml.gz$ sitemap.xml.php?gz=1 [L,QSA] + RewriteRule ^sitemap.xml$ index.php [L,QSA] + RewriteRule ^sitemap.gz$ index.php [L,QSA] + RewriteRule ^sitemap.xml.gz$ index.php [L,QSA] # robots.txt - RewriteRule ^robots.txt$ robots.txt.php [L,QSA] + RewriteRule ^robots.txt$ index.php [L,QSA] # llms.txt - RewriteRule ^llms.txt$ llms.txt.php [L,QSA] + RewriteRule ^llms.txt$ index.php [L,QSA] # phpMyFAQ tags page with page count RewriteRule ^tags/([0-9]+)/([0-9]+)/(.+)\.htm(l?)$ index.php?action=search&tagging_id=$1&seite=$2 [L,QSA] # phpMyFAQ tags page RewriteRule ^tags/([0-9]+)/([^/]+)\.htm(l?)$ index.php?action=search&tagging_id=$1 [L,QSA] # Authentication services - RewriteRule ^services/webauthn(.*) services/webauthn/index.php [L,QSA] + RewriteRule ^services/webauthn(.*) index.php [L,QSA] # User pages RewriteRule ^user/(ucp|bookmarks|request-removal|logout|register) index.php?action=$1 [L,QSA] # Setup and update pages diff --git a/phpmyfaq/services/webauthn/index.php b/phpmyfaq/services/webauthn/index.php deleted file mode 100644 index f143bbef4b..0000000000 --- a/phpmyfaq/services/webauthn/index.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @copyright 2024-2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2024-09-11 - */ - -use phpMyFAQ\Application; -use phpMyFAQ\Controller\WebAuthnController; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; - -require '../../src/Bootstrap.php'; - -// -// Service Containers -// -$container = new ContainerBuilder(); -$loader = new PhpFileLoader($container, new FileLocator(__DIR__)); -try { - $loader->load('../../src/services.php'); -} catch (\Exception $e) { - echo $e->getMessage(); -} - -$routes = new RouteCollection(); -$routes->add( - 'public.webauthn.index', - new Route('/services/webauthn', ['_controller' => [WebAuthnController::class, 'index']]) -); - -$app = new Application($container); -try { - $app->run($routes); -} catch (Exception $exception) { - echo $exception->getMessage(); -} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php b/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php index 5f7c39fca2..eb65944166 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php @@ -20,17 +20,13 @@ namespace phpMyFAQ\Controller; use phpMyFAQ\Core\Exception; -use phpMyFAQ\Environment; -use phpMyFAQ\Helper\LanguageHelper; -use phpMyFAQ\System; use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Twig\Error\LoaderError; -final class WebAuthnController extends AbstractController +final class WebAuthnController extends AbstractFrontController { /** * @throws Exception|LoaderError @@ -38,90 +34,13 @@ final class WebAuthnController extends AbstractController #[Route(path: '/services/webauthn', name: 'public.webauthn.index')] public function index(Request $request): Response { - $system = new System(); - - $topNavigation = [ - [ - 'name' => Translation::get(key: 'msgShowAllCategories'), - 'link' => './show-categories.html', - 'active' => '', - ], - [ - 'name' => Translation::get(key: 'msgAddContent'), - 'link' => './add-faq.html', - 'active' => '', - ], - [ - 'name' => Translation::get(key: 'msgQuestion'), - 'link' => './add-question.html', - 'active' => '', - ], - [ - 'name' => Translation::get(key: 'msgOpenQuestions'), - 'link' => './open-questions.html', - 'active' => '', - ], - ]; - - $footerNavigation = [ - [ - 'name' => Translation::get(key: 'faqOverview'), - 'link' => './overview.html', - 'active' => '', - ], - [ - 'name' => Translation::get(key: 'msgSitemap'), - 'link' => './sitemap/A/' . $this->configuration->getDefaultLanguage() . '.html', - 'active' => '', - ], - [ - 'name' => Translation::get(key: 'ad_menu_glossary'), - 'link' => './glossary.html', - 'active' => '', - ], - [ - 'name' => Translation::get(key: 'msgContact'), - 'link' => './contact.html', - 'active' => '', - ], - ]; - return $this->render(file: '/webauthn.twig', context: [ - 'isMaintenanceMode' => $this->configuration->get(item: 'main.maintenanceMode'), - 'isCompletelySecured' => $this->configuration->get(item: 'security.enableLoginOnly'), - 'isDebugEnabled' => Environment::isDebugMode(), - 'richSnippetsEnabled' => $this->configuration->get(item: 'seo.enableRichSnippets'), - 'tplSetName' => TwigWrapper::getTemplateSetName(), + ...$this->getHeader($request), 'msgLoginUser' => Translation::get(key: 'msgLoginUser'), - 'isUserLoggedIn' => $this->currentUser->isLoggedIn(), 'title' => Translation::get(key: 'msgLoginUser'), - 'baseHref' => $system->getSystemUri($this->configuration), - 'customCss' => $this->configuration->getCustomCss(), - 'version' => $this->configuration->getVersion(), - 'header' => str_replace(search: '"', replace: '', subject: $this->configuration->getTitle()), - 'metaPublisher' => $this->configuration->get(item: 'main.metaPublisher'), - 'metaLanguage' => Translation::get(key: 'metaLanguage'), - 'phpmyfaqVersion' => $this->configuration->getVersion(), - 'stylesheet' => Translation::get(key: 'direction') === 'rtl' ? 'style.rtl' : 'style', - 'currentPageUrl' => $request->getSchemeAndHttpHost() . $request->getRequestUri(), - 'dir' => Translation::get(key: 'direction'), - 'searchBox' => Translation::get(key: 'msgSearch'), 'faqHome' => $this->configuration->getDefaultUrl(), - 'topNavigation' => $topNavigation, - 'footerNavigation' => $footerNavigation, - 'languageBox' => Translation::get(key: 'msgLanguageSubmit'), - 'switchLanguages' => LanguageHelper::renderSelectLanguage( - $this->configuration->getDefaultLanguage(), - submitOnChange: true, - ), - 'copyright' => System::getPoweredByString(), 'isUserRegistrationEnabled' => $this->configuration->get(item: 'security.enableRegistration'), 'msgRegisterUser' => Translation::get(key: 'msgRegisterUser'), - 'isPrivacyLinkEnabled' => $this->configuration->get(item: 'layout.enablePrivacyLink'), - 'urlPrivacyLink' => $this->configuration->get(item: 'main.privacyURL'), - 'msgPrivacyNote' => Translation::get(key: 'msgPrivacyNote'), - 'isCookieConsentEnabled' => $this->configuration->get(item: 'layout.enableCookieConsent'), - 'cookiePreferences' => Translation::get(key: 'cookiePreferences'), ]); } } diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 704b6fc8c6..d3adafe4b3 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -55,7 +55,7 @@ 'methods' => 'GET' ], 'public.webauthn.index' => [ - 'path' => '/services/webauthn', + 'path' => '/services/webauthn/', 'controller' => [WebAuthnController::class, 'index'], 'methods' => 'GET' ], From f70acd7fa54b9a8d06a02884bd8ecac588b906d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:01:57 +0000 Subject: [PATCH 010/286] build(deps): bump symfony/routing from 8.0.1 to 8.0.3 Bumps [symfony/routing](https://github.com/symfony/routing) from 8.0.1 to 8.0.3. - [Release notes](https://github.com/symfony/routing/releases) - [Changelog](https://github.com/symfony/routing/blob/7.3/CHANGELOG.md) - [Commits](https://github.com/symfony/routing/compare/v8.0.1...v8.0.3) --- updated-dependencies: - dependency-name: symfony/routing dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index 63e566908e..60dca9f64a 100644 --- a/composer.lock +++ b/composer.lock @@ -5039,16 +5039,16 @@ }, { "name": "symfony/routing", - "version": "v8.0.1", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "bc8fa314a61fb7c4190e964b18a5bd000d3b45ce" + "reference": "3827ac6e03dcd86e430fb6ae6056acf5b51aece3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/bc8fa314a61fb7c4190e964b18a5bd000d3b45ce", - "reference": "bc8fa314a61fb7c4190e964b18a5bd000d3b45ce", + "url": "https://api.github.com/repos/symfony/routing/zipball/3827ac6e03dcd86e430fb6ae6056acf5b51aece3", + "reference": "3827ac6e03dcd86e430fb6ae6056acf5b51aece3", "shasum": "" }, "require": { @@ -5095,7 +5095,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v8.0.1" + "source": "https://github.com/symfony/routing/tree/v8.0.3" }, "funding": [ { @@ -5115,7 +5115,7 @@ "type": "tidelift" } ], - "time": "2025-12-01T09:13:36+00:00" + "time": "2025-12-19T10:01:18+00:00" }, { "name": "symfony/service-contracts", @@ -8298,5 +8298,5 @@ "platform-overrides": { "php": "8.4.0" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } From cd290621f426cfc2209bee752d9f96460b8b7208 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:02:02 +0000 Subject: [PATCH 011/286] build(deps): bump symfony/http-kernel from 8.0.2 to 8.0.3 Bumps [symfony/http-kernel](https://github.com/symfony/http-kernel) from 8.0.2 to 8.0.3. - [Release notes](https://github.com/symfony/http-kernel/releases) - [Changelog](https://github.com/symfony/http-kernel/blob/7.3/CHANGELOG.md) - [Commits](https://github.com/symfony/http-kernel/compare/v8.0.2...v8.0.3) --- updated-dependencies: - dependency-name: symfony/http-kernel dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/composer.lock b/composer.lock index 60dca9f64a..0c94b2cfd0 100644 --- a/composer.lock +++ b/composer.lock @@ -3857,16 +3857,16 @@ }, { "name": "symfony/http-kernel", - "version": "v8.0.2", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "bcef77a3c8ae8934ce7067172e2a1a6491a62a7d" + "reference": "e6dfb348eb1dd4df14c39e6dc7e283bab4199fd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/bcef77a3c8ae8934ce7067172e2a1a6491a62a7d", - "reference": "bcef77a3c8ae8934ce7067172e2a1a6491a62a7d", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/e6dfb348eb1dd4df14c39e6dc7e283bab4199fd9", + "reference": "e6dfb348eb1dd4df14c39e6dc7e283bab4199fd9", "shasum": "" }, "require": { @@ -3937,7 +3937,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v8.0.2" + "source": "https://github.com/symfony/http-kernel/tree/v8.0.3" }, "funding": [ { @@ -3957,7 +3957,7 @@ "type": "tidelift" } ], - "time": "2025-12-08T07:59:34+00:00" + "time": "2025-12-31T09:29:34+00:00" }, { "name": "symfony/intl", @@ -5374,16 +5374,16 @@ }, { "name": "symfony/var-dumper", - "version": "v8.0.0", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "d2a2476c93b58ac5292145e9fac1ff76a21d1ce2" + "reference": "3bc368228532ad538cc216768caa8968be95a8d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/d2a2476c93b58ac5292145e9fac1ff76a21d1ce2", - "reference": "d2a2476c93b58ac5292145e9fac1ff76a21d1ce2", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/3bc368228532ad538cc216768caa8968be95a8d6", + "reference": "3bc368228532ad538cc216768caa8968be95a8d6", "shasum": "" }, "require": { @@ -5437,7 +5437,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.0.0" + "source": "https://github.com/symfony/var-dumper/tree/v8.0.3" }, "funding": [ { @@ -5457,7 +5457,7 @@ "type": "tidelift" } ], - "time": "2025-10-28T09:34:19+00:00" + "time": "2025-12-18T11:23:51+00:00" }, { "name": "symfony/var-exporter", From c64ae7fa4f5aca05959b81f26303f7b1dbb07cbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:02:07 +0000 Subject: [PATCH 012/286] build(deps): bump symfony/http-client from 8.0.1 to 8.0.3 Bumps [symfony/http-client](https://github.com/symfony/http-client) from 8.0.1 to 8.0.3. - [Release notes](https://github.com/symfony/http-client/releases) - [Changelog](https://github.com/symfony/http-client/blob/7.3/CHANGELOG.md) - [Commits](https://github.com/symfony/http-client/compare/v8.0.1...v8.0.3) --- updated-dependencies: - dependency-name: symfony/http-client dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 0c94b2cfd0..d36817be79 100644 --- a/composer.lock +++ b/composer.lock @@ -3603,16 +3603,16 @@ }, { "name": "symfony/http-client", - "version": "v8.0.1", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "727fda60d0aebfdfcc4c8bc4661f0cb8f44153c0" + "reference": "ea062691009cc2b7bb87734fef20e02671cbd50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/727fda60d0aebfdfcc4c8bc4661f0cb8f44153c0", - "reference": "727fda60d0aebfdfcc4c8bc4661f0cb8f44153c0", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ea062691009cc2b7bb87734fef20e02671cbd50b", + "reference": "ea062691009cc2b7bb87734fef20e02671cbd50b", "shasum": "" }, "require": { @@ -3675,7 +3675,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v8.0.1" + "source": "https://github.com/symfony/http-client/tree/v8.0.3" }, "funding": [ { @@ -3695,7 +3695,7 @@ "type": "tidelift" } ], - "time": "2025-12-05T14:08:45+00:00" + "time": "2025-12-23T14:52:06+00:00" }, { "name": "symfony/http-client-contracts", From c18fd0e147e7f1fa6c338c20ed90dfd9484cfe61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:02:12 +0000 Subject: [PATCH 013/286] build(deps): bump symfony/console from 8.0.1 to 8.0.3 Bumps [symfony/console](https://github.com/symfony/console) from 8.0.1 to 8.0.3. - [Release notes](https://github.com/symfony/console/releases) - [Changelog](https://github.com/symfony/console/blob/7.3/CHANGELOG.md) - [Commits](https://github.com/symfony/console/compare/v8.0.1...v8.0.3) --- updated-dependencies: - dependency-name: symfony/console dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index d36817be79..23a2db17c3 100644 --- a/composer.lock +++ b/composer.lock @@ -2907,16 +2907,16 @@ }, { "name": "symfony/console", - "version": "v8.0.1", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fcb73f69d655b48fcb894a262f074218df08bd58" + "reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fcb73f69d655b48fcb894a262f074218df08bd58", - "reference": "fcb73f69d655b48fcb894a262f074218df08bd58", + "url": "https://api.github.com/repos/symfony/console/zipball/6145b304a5c1ea0bdbd0b04d297a5864f9a7d587", + "reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587", "shasum": "" }, "require": { @@ -2973,7 +2973,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.1" + "source": "https://github.com/symfony/console/tree/v8.0.3" }, "funding": [ { @@ -2993,7 +2993,7 @@ "type": "tidelift" } ], - "time": "2025-12-05T15:25:33+00:00" + "time": "2025-12-23T14:52:06+00:00" }, { "name": "symfony/dependency-injection", From 99c98b3996fed6331f2ef915d927e5c6f26e4018 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:02:18 +0000 Subject: [PATCH 014/286] build(deps): bump symfony/config from 8.0.1 to 8.0.3 Bumps [symfony/config](https://github.com/symfony/config) from 8.0.1 to 8.0.3. - [Release notes](https://github.com/symfony/config/releases) - [Changelog](https://github.com/symfony/config/blob/7.3/CHANGELOG.md) - [Commits](https://github.com/symfony/config/compare/v8.0.1...v8.0.3) --- updated-dependencies: - dependency-name: symfony/config dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 23a2db17c3..336e3185c4 100644 --- a/composer.lock +++ b/composer.lock @@ -2829,16 +2829,16 @@ }, { "name": "symfony/config", - "version": "v8.0.1", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "a5a054e613da565d46183a845ae4c0c996a3fbce" + "reference": "58063686fd7b8e676f14b5a4808cb85265c5216e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/a5a054e613da565d46183a845ae4c0c996a3fbce", - "reference": "a5a054e613da565d46183a845ae4c0c996a3fbce", + "url": "https://api.github.com/repos/symfony/config/zipball/58063686fd7b8e676f14b5a4808cb85265c5216e", + "reference": "58063686fd7b8e676f14b5a4808cb85265c5216e", "shasum": "" }, "require": { @@ -2883,7 +2883,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v8.0.1" + "source": "https://github.com/symfony/config/tree/v8.0.3" }, "funding": [ { @@ -2903,7 +2903,7 @@ "type": "tidelift" } ], - "time": "2025-12-05T14:08:45+00:00" + "time": "2025-12-23T14:52:06+00:00" }, { "name": "symfony/console", From fcf9914e7f2f156a623289d14c2c17c112649f56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:02:31 +0000 Subject: [PATCH 015/286] build(deps): bump symfony/mailer from 8.0.0 to 8.0.3 Bumps [symfony/mailer](https://github.com/symfony/mailer) from 8.0.0 to 8.0.3. - [Release notes](https://github.com/symfony/mailer/releases) - [Changelog](https://github.com/symfony/mailer/blob/7.3/CHANGELOG.md) - [Commits](https://github.com/symfony/mailer/compare/v8.0.0...v8.0.3) --- updated-dependencies: - dependency-name: symfony/mailer dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 336e3185c4..4c95090668 100644 --- a/composer.lock +++ b/composer.lock @@ -4050,16 +4050,16 @@ }, { "name": "symfony/mailer", - "version": "v8.0.0", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "f9b546f0e28cbd08fd5d03f2472aad913a9398f9" + "reference": "02e033db6e00a42c66b8b8992e4e565ea7464a28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/f9b546f0e28cbd08fd5d03f2472aad913a9398f9", - "reference": "f9b546f0e28cbd08fd5d03f2472aad913a9398f9", + "url": "https://api.github.com/repos/symfony/mailer/zipball/02e033db6e00a42c66b8b8992e4e565ea7464a28", + "reference": "02e033db6e00a42c66b8b8992e4e565ea7464a28", "shasum": "" }, "require": { @@ -4106,7 +4106,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v8.0.0" + "source": "https://github.com/symfony/mailer/tree/v8.0.3" }, "funding": [ { @@ -4126,7 +4126,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T08:09:45+00:00" + "time": "2025-12-16T08:10:18+00:00" }, { "name": "symfony/mcp-sdk", From 6fd456ab3119f1ad725e690e22688a6cc4eff59a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 11:09:17 +0000 Subject: [PATCH 016/286] build(deps): bump symfony/dependency-injection from 8.0.2 to 8.0.3 Bumps [symfony/dependency-injection](https://github.com/symfony/dependency-injection) from 8.0.2 to 8.0.3. - [Release notes](https://github.com/symfony/dependency-injection/releases) - [Changelog](https://github.com/symfony/dependency-injection/blob/7.3/CHANGELOG.md) - [Commits](https://github.com/symfony/dependency-injection/compare/v8.0.2...v8.0.3) --- updated-dependencies: - dependency-name: symfony/dependency-injection dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 4c95090668..48630b70ac 100644 --- a/composer.lock +++ b/composer.lock @@ -2997,16 +2997,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v8.0.2", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "90f6c3364b8f444f85bdb6939664c80af9e0d576" + "reference": "8db0d4c1dd4c533a29210c68074999ba45ad6d3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/90f6c3364b8f444f85bdb6939664c80af9e0d576", - "reference": "90f6c3364b8f444f85bdb6939664c80af9e0d576", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8db0d4c1dd4c533a29210c68074999ba45ad6d3e", + "reference": "8db0d4c1dd4c533a29210c68074999ba45ad6d3e", "shasum": "" }, "require": { @@ -3054,7 +3054,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v8.0.2" + "source": "https://github.com/symfony/dependency-injection/tree/v8.0.3" }, "funding": [ { @@ -3074,7 +3074,7 @@ "type": "tidelift" } ], - "time": "2025-12-08T06:57:48+00:00" + "time": "2025-12-23T14:52:06+00:00" }, { "name": "symfony/deprecation-contracts", From 428134fb3bc28598895cca05317639be83d62925 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 12:13:19 +0100 Subject: [PATCH 017/286] docs: phpMyFAQ 4.2 won't support updates from 3.0 --- phpmyfaq/assets/templates/setup/update/step1.twig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/phpmyfaq/assets/templates/setup/update/step1.twig b/phpmyfaq/assets/templates/setup/update/step1.twig index f5c9ee9d23..98af69b4e7 100644 --- a/phpmyfaq/assets/templates/setup/update/step1.twig +++ b/phpmyfaq/assets/templates/setup/update/step1.twig @@ -19,11 +19,10 @@

This update script will work only for the following versions:

    -
  • phpMyFAQ 3.0.x
  • -
  • phpMyFAQ 3.1.x
  • phpMyFAQ 3.2.x
  • phpMyFAQ 4.0.x
  • phpMyFAQ 4.1.x
  • +
  • phpMyFAQ 4.2.x
@@ -32,6 +31,8 @@
  • phpMyFAQ 0.x
  • phpMyFAQ 1.x
  • phpMyFAQ 2.x
  • +
  • phpMyFAQ 3.0.x
  • +
  • phpMyFAQ 3.1.x
  • From 966c26e6a35e860e7c33a8bc5e5adbc88066b1fe Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 12:41:33 +0100 Subject: [PATCH 018/286] fix: corrected Apache redirect loop --- phpmyfaq/.htaccess | 12 ++++++--- phpmyfaq/src/Bootstrap.php | 11 ++++++-- phpmyfaq/src/phpMyFAQ/Application.php | 36 +++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/phpmyfaq/.htaccess b/phpmyfaq/.htaccess index a65a4319ef..085a0efdf5 100644 --- a/phpmyfaq/.htaccess +++ b/phpmyfaq/.htaccess @@ -144,19 +144,23 @@ Header set Access-Control-Allow-Headers "Content-Type, Authorization" RewriteRule ^services/webauthn(.*) index.php [L,QSA] # User pages RewriteRule ^user/(ucp|bookmarks|request-removal|logout|register) index.php?action=$1 [L,QSA] - # Setup and update pages - RewriteCond %{REQUEST_URI} ^(.*/)setup/ - RewriteRule ^(.*/)?setup/(.*) $1setup/index.php [L,QSA] - RewriteRule ^update/(.*) update/index.php [L,QSA] # Administration API RewriteRule ^admin/api/(.*) admin/api/index.php [L,QSA] # Administration pages RewriteRule ^admin/(.*) admin/index.php [L,QSA] # Private APIs + RewriteCond %{REQUEST_URI} !index\.php$ RewriteRule ^api/(autocomplete|bookmark/delete|bookmark/create|user/data/update|user/password/update|user/request-removal|user/remove-twofactor|contact|voting|register|captcha|share|comment/create|faq/create|question/create|webauthn/prepare|webauthn/register|webauthn/prepare-login|webauthn/login|translations) api/index.php [L,QSA] # Setup APIs + RewriteCond %{REQUEST_URI} !index\.php$ RewriteRule ^api/setup/(check|backup|update-database) api/index.php [L,QSA] # REST API v3.0 and v3.1 # * http://[...]/api/v3.x/ + RewriteCond %{REQUEST_URI} !index\.php$ RewriteRule ^api/v3\.[01]/(.*) api/index.php [L,QSA] + # Setup and update pages + RewriteCond %{REQUEST_URI} ^(.*/)setup/ + RewriteCond %{REQUEST_URI} !index\.php$ + RewriteRule ^(.*/)?setup/(.*) $1setup/index.php [L,QSA] + RewriteRule ^update/(.*) update/index.php [L,QSA] diff --git a/phpmyfaq/src/Bootstrap.php b/phpmyfaq/src/Bootstrap.php index 481627f4ca..b0dd87f57f 100644 --- a/phpmyfaq/src/Bootstrap.php +++ b/phpmyfaq/src/Bootstrap.php @@ -91,10 +91,17 @@ // // Check if config/database.php exist -> if not, redirect to the installer +// Skip redirect if we're already in setup or API setup context // +$requestUri = $_SERVER['REQUEST_URI'] ?? ''; +$isSetupContext = str_contains($requestUri, '/setup/') || str_contains($requestUri, '/api/setup/'); + if (!file_exists(PMF_CONFIG_DIR . '/database.php') && !file_exists(PMF_LEGACY_CONFIG_DIR . '/database.php')) { - $response = new RedirectResponse('./setup/'); - $response->send(); + if (!$isSetupContext) { + $response = new RedirectResponse('/setup/'); + $response->send(); + exit; + } } else { if (file_exists(PMF_CONFIG_DIR . '/database.php')) { $databaseFile = PMF_CONFIG_DIR . '/database.php'; diff --git a/phpmyfaq/src/phpMyFAQ/Application.php b/phpmyfaq/src/phpMyFAQ/Application.php index dace38d513..00c104ad44 100644 --- a/phpmyfaq/src/phpMyFAQ/Application.php +++ b/phpmyfaq/src/phpMyFAQ/Application.php @@ -165,6 +165,42 @@ private function handleRequest( ) : 'Bad Request'; $response = new Response(content: $message, status: Response::HTTP_BAD_REQUEST); + } catch (Throwable $exception) { + // Log the error for debugging + error_log(sprintf( + 'Unhandled exception in Application: %s at %s:%d', + $exception->getMessage(), + $exception->getFile(), + $exception->getLine(), + )); + + $message = Environment::isDebugMode() + ? $this->formatExceptionMessage( + template: 'Internal Server Error: :message at line :line at :file', + exception: $exception, + ) + : 'Internal Server Error'; + + // Return JSON response for API requests + if (str_contains(haystack: $urlMatcher->getContext()->getBaseUrl(), needle: '/api')) { + $content = Environment::isDebugMode() + ? json_encode(value: [ + 'error' => 'Internal Server Error', + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]) + : json_encode(value: ['error' => 'Internal Server Error']); + + $response = new Response(content: $content, status: Response::HTTP_INTERNAL_SERVER_ERROR, headers: [ + 'Content-Type' => 'application/json', + ]); + + $response->send(); + return; + } + + $response = new Response(content: $message, status: Response::HTTP_INTERNAL_SERVER_ERROR); } $response->send(); From d19971dba02665cc10f88ee907a9ade3a1adc774 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 13:02:42 +0100 Subject: [PATCH 019/286] build: moved private API controller in another folder, fixed code formatting in tests --- phpmyfaq/.htaccess | 5 +- phpmyfaq/index.php | 20 +- phpmyfaq/setup/index.php | 2 +- phpmyfaq/src/Bootstrap.php | 12 +- phpmyfaq/src/admin-api-routes.php | 222 +++++++++--------- phpmyfaq/src/admin-routes.php | 151 ++++++------ phpmyfaq/src/api-routes.php | 144 ++++++------ .../phpMyFAQ/Controller/ContactController.php | 68 ------ .../AbstractFrontController.php | 3 +- .../{ => Api}/AutoCompleteController.php | 4 +- .../Frontend/{ => Api}/BookmarkController.php | 4 +- .../Frontend/{ => Api}/CaptchaController.php | 2 +- .../Frontend/{ => Api}/CommentController.php | 2 +- .../Frontend/Api/ContactController.php | 94 ++++++++ .../Frontend/{ => Api}/FaqController.php | 2 +- .../Frontend/{ => Api}/QuestionController.php | 2 +- .../{ => Api}/RegistrationController.php | 2 +- .../Frontend/{ => Api}/SetupController.php | 4 +- .../{ => Api}/TranslationController.php | 2 +- .../{ => Api}/UnauthorizedUserController.php | 2 +- .../Frontend/{ => Api}/UserController.php | 2 +- .../Frontend/{ => Api}/VotingController.php | 2 +- .../Frontend/Api/WebAuthnController.php | 174 ++++++++++++++ .../Controller/Frontend/ContactController.php | 97 +++----- .../{ => Frontend}/PageNotFoundController.php | 2 +- .../Frontend/WebAuthnController.php | 156 ++---------- .../Controller/WebAuthnController.php | 46 ---- phpmyfaq/src/public-routes.php | 38 +-- phpmyfaq/update/index.php | 54 ----- tests/bootstrap.php | 2 + .../phpMyFAQ/Administration/AdminLogTest.php | 20 +- tests/phpMyFAQ/Administration/ApiTest.php | 60 ++--- .../Backup/BackupExecuteResultTest.php | 35 +-- .../Backup/BackupExportResultTest.php | 19 +- .../Backup/BackupParseResultTest.php | 30 +-- .../Backup/BackupRepositoryTest.php | 76 ++---- tests/phpMyFAQ/Administration/BackupTest.php | 87 +++---- .../phpMyFAQ/Administration/CategoryTest.php | 17 +- .../phpMyFAQ/Administration/ChangelogTest.php | 10 +- tests/phpMyFAQ/Administration/HelperTest.php | 2 +- .../Administration/HttpStreamerTest.php | 4 +- .../Administration/LatestUsersTest.php | 8 +- .../Administration/RatingDataTest.php | 29 +-- tests/phpMyFAQ/Administration/ReportTest.php | 27 +-- .../phpMyFAQ/Administration/RevisionTest.php | 39 +-- .../Administration/SessionRepositoryTest.php | 21 +- tests/phpMyFAQ/Administration/SessionTest.php | 10 +- .../TranslationStatisticsTest.php | 2 +- tests/phpMyFAQ/ApplicationTest.php | 42 ++-- .../Attachment/AbstractAttachmentTest.php | 2 +- .../Attachment/AbstractMimeTypeTest.php | 2 +- .../Attachment/AttachmentCollectionTest.php | 13 +- .../Attachment/AttachmentFactoryTest.php | 12 +- tests/phpMyFAQ/Attachment/FileTest.php | 44 ++-- .../Filesystem/File/VanillaFileTest.php | 17 +- tests/phpMyFAQ/Auth/AuthDatabaseTest.php | 4 +- tests/phpMyFAQ/Auth/AuthEntraIdTest.php | 37 +-- tests/phpMyFAQ/Auth/AuthHttpTest.php | 2 +- tests/phpMyFAQ/Auth/AuthLdapTest.php | 76 +++--- tests/phpMyFAQ/Auth/AuthSsoTest.php | 8 +- tests/phpMyFAQ/Auth/AuthWebAuthnTest.php | 16 +- tests/phpMyFAQ/Auth/EntraId/OAuthTest.php | 70 +++--- tests/phpMyFAQ/Auth/EntraId/SessionTest.php | 29 ++- .../Auth/WebAuthn/WebAuthnUserTest.php | 3 +- tests/phpMyFAQ/AuthTest.php | 2 +- .../Bookmark/BookmarkFormatterTest.php | 2 +- .../Bookmark/BookmarkRepositoryTest.php | 2 +- tests/phpMyFAQ/BookmarkTest.php | 2 +- tests/phpMyFAQ/Captcha/BuiltinCaptchaTest.php | 13 +- tests/phpMyFAQ/Captcha/CaptchaTest.php | 3 +- .../phpMyFAQ/Captcha/GoogleRecaptchaTest.php | 10 +- .../BuiltinCaptchaAbstractHelperTest.php | 48 +--- .../Captcha/Helper/CaptchaHelperTest.php | 2 +- .../GoogleRecaptchaAbstractHelperTest.php | 83 ++----- tests/phpMyFAQ/Category/CategoryCacheTest.php | 3 +- .../CategoryPermissionContextTest.php | 3 +- .../phpMyFAQ/Category/CategoryServiceTest.php | 65 +++-- .../Category/CategoryTreeFacadeTest.php | 29 ++- tests/phpMyFAQ/Category/ImageTest.php | 120 +++------- .../Language/CategoryLanguageServiceTest.php | 21 +- .../Navigation/BreadcrumbsBuilderTest.php | 4 +- .../Navigation/CategoryTreeNavigatorTest.php | 42 +++- tests/phpMyFAQ/Category/OrderTest.php | 59 +++-- .../CategoryPermissionServiceTest.php | 12 +- tests/phpMyFAQ/Category/PermissionTest.php | 86 ++++--- tests/phpMyFAQ/Category/RelationTest.php | 146 ++++++++---- tests/phpMyFAQ/Category/StartpageTest.php | 156 ++++++++---- .../Category/Tree/TreeBuilderTest.php | 16 +- tests/phpMyFAQ/CategoryTest.php | 46 ++-- .../phpMyFAQ/Command/McpServerCommandTest.php | 26 +- tests/phpMyFAQ/Command/UpdateCommandTest.php | 4 +- .../Comment/CommentsRepositoryTest.php | 23 +- tests/phpMyFAQ/CommentsTest.php | 4 +- .../DatabaseConfigurationTest.php | 2 +- .../ElasticsearchConfigurationTest.php | 51 ++-- .../Configuration/LdapConfigurationTest.php | 2 +- .../MultisiteConfigurationLocatorTest.php | 2 +- .../OpenSearchConfigurationTest.php | 63 ++--- tests/phpMyFAQ/ConfigurationTest.php | 89 +++---- .../Controller/AbstractControllerTest.php | 2 +- .../Api/AttachmentControllerTest.php | 28 +-- .../Controller/Api/LanguageControllerTest.php | 2 +- .../Controller/Api/TitleControllerTest.php | 2 +- .../Controller/Api/VersionControllerTest.php | 2 +- .../Controller/BackupControllerTest.php | 2 +- .../Frontend/TranslationControllerTest.php | 3 +- .../Controller/LlmsControllerTest.php | 2 +- .../Controller/RobotsControllerTest.php | 2 +- .../Controller/SitemapControllerTest.php | 2 +- .../Controller/WebAuthnControllerTest.php | 3 +- tests/phpMyFAQ/Core/ErrorTest.php | 50 ++-- tests/phpMyFAQ/Core/ExceptionTest.php | 2 +- .../phpMyFAQ/Database/DatabaseHelperTest.php | 6 +- tests/phpMyFAQ/Database/MysqliTest.php | 42 +--- tests/phpMyFAQ/Database/PdoMysqlTest.php | 63 ++--- tests/phpMyFAQ/Database/PdoPgsqlTest.php | 57 ++--- tests/phpMyFAQ/Database/PdoSqliteTest.php | 57 ++--- tests/phpMyFAQ/Database/PdoSqlsrvTest.php | 72 ++---- tests/phpMyFAQ/Database/PgsqlTest.php | 24 +- tests/phpMyFAQ/Database/Sqlite3Test.php | 24 +- tests/phpMyFAQ/Database/SqlsrvTest.php | 24 +- tests/phpMyFAQ/DatabaseTest.php | 26 +- tests/phpMyFAQ/DateTest.php | 13 +- tests/phpMyFAQ/EncryptionTest.php | 4 +- tests/phpMyFAQ/Entity/CommentTest.php | 10 +- tests/phpMyFAQ/Entity/CommentTypeTest.php | 4 +- tests/phpMyFAQ/Entity/VoteTest.php | 4 +- tests/phpMyFAQ/EnvironmentTest.php | 2 +- tests/phpMyFAQ/Export/JsonTest.php | 63 ++--- tests/phpMyFAQ/Export/Pdf/WrapperTest.php | 2 +- tests/phpMyFAQ/ExportTest.php | 2 +- tests/phpMyFAQ/Faq/ImportTest.php | 2 +- tests/phpMyFAQ/Faq/PermissionTest.php | 2 +- tests/phpMyFAQ/Faq/QueryHelperTest.php | 2 +- tests/phpMyFAQ/Faq/StatisticsTest.php | 2 +- tests/phpMyFAQ/FaqTest.php | 2 +- tests/phpMyFAQ/Filesystem/FilesystemTest.php | 14 +- tests/phpMyFAQ/FilterTest.php | 17 +- tests/phpMyFAQ/Form/FormsRepositoryTest.php | 30 ++- tests/phpMyFAQ/FormsTest.php | 2 +- .../phpMyFAQ/Glossary/GlossaryHelperTest.php | 2 +- tests/phpMyFAQ/GlossaryTest.php | 15 +- .../phpMyFAQ/Helper/AttachmentHelperTest.php | 2 +- tests/phpMyFAQ/Helper/CategoryHelperTest.php | 11 +- tests/phpMyFAQ/Helper/FaqHelperTest.php | 2 +- .../phpMyFAQ/Helper/PermissionHelperTest.php | 4 +- tests/phpMyFAQ/Helper/QuestionHelperTest.php | 2 +- .../Helper/RegistrationHelperTest.php | 54 +++-- tests/phpMyFAQ/Helper/SearchHelperTest.php | 60 ++--- .../phpMyFAQ/Helper/StatisticsHelperTest.php | 76 +++--- tests/phpMyFAQ/Helper/TagsHelperTest.php | 24 +- tests/phpMyFAQ/Helper/UserHelperTest.php | 12 +- tests/phpMyFAQ/Instance/ClientTest.php | 2 +- tests/phpMyFAQ/Instance/DatabaseTest.php | 2 +- tests/phpMyFAQ/Instance/MainTest.php | 2 +- tests/phpMyFAQ/Instance/SetupTest.php | 4 +- tests/phpMyFAQ/InstanceTest.php | 38 +-- tests/phpMyFAQ/Language/LanguageCodesTest.php | 2 +- tests/phpMyFAQ/Language/PluralsTest.php | 20 +- tests/phpMyFAQ/LanguageTest.php | 6 +- tests/phpMyFAQ/LdapTest.php | 74 +++--- .../Link/LinkStrategyRegistryDiTest.php | 3 +- .../Link/Strategy/FaqStrategyTest.php | 7 +- .../Link/Strategy/GenericPathStrategyTest.php | 6 +- .../Link/Strategy/NewsStrategyTest.php | 7 +- .../Link/Strategy/SearchStrategyTest.php | 6 +- .../Link/Strategy/ShowStrategyTest.php | 6 +- .../Link/Strategy/SitemapStrategyTest.php | 6 +- .../Link/Util/LinkQueryParserTest.php | 3 +- .../phpMyFAQ/Link/Util/TitleSlugifierTest.php | 5 +- tests/phpMyFAQ/LinkTest.php | 80 ++----- tests/phpMyFAQ/Mail/SmtpTest.php | 54 +++-- tests/phpMyFAQ/MailTest.php | 6 +- tests/phpMyFAQ/NetworkTest.php | 48 ++-- tests/phpMyFAQ/NewsTest.php | 65 +++-- tests/phpMyFAQ/NotificationTest.php | 2 +- tests/phpMyFAQ/PaginationTest.php | 28 +-- .../BasicPermissionRepositoryTest.php | 4 +- .../Permission/BasicPermissionTest.php | 4 +- .../MediumPermissionRepositoryTest.php | 4 +- .../Permission/MediumPermissionTest.php | 28 +-- tests/phpMyFAQ/PermissionTest.php | 4 +- tests/phpMyFAQ/Plugin/MockPlugin.php | 3 +- tests/phpMyFAQ/Plugin/PluginEventTest.php | 2 +- .../phpMyFAQ/Plugin/PluginIntegrationTest.php | 146 ++++++------ tests/phpMyFAQ/Plugin/PluginManagerTest.php | 7 +- tests/phpMyFAQ/QuestionTest.php | 2 +- .../Questions/QuestionRepositoryTest.php | 2 +- .../phpMyFAQ/Rating/RatingRepositoryTest.php | 12 +- tests/phpMyFAQ/RatingTest.php | 2 +- tests/phpMyFAQ/RelationTest.php | 15 +- .../Repository/NewsRepositoryTest.php | 3 +- tests/phpMyFAQ/Search/SearchDatabaseTest.php | 49 ++-- tests/phpMyFAQ/Search/SearchFactoryTest.php | 6 +- tests/phpMyFAQ/Search/SearchResultSetTest.php | 2 +- tests/phpMyFAQ/SearchTest.php | 17 +- tests/phpMyFAQ/Seo/SeoRepositoryTest.php | 9 +- tests/phpMyFAQ/SeoTest.php | 17 +- tests/phpMyFAQ/Service/GravatarTest.php | 6 +- .../McpServer/FaqSearchToolExecutorTest.php | 18 +- .../McpServer/FaqSearchToolMetadataTest.php | 2 +- .../McpServer/PhpMyFaqMcpServerTest.php | 42 +--- tests/phpMyFAQ/ServicesConfigurationTest.php | 15 +- tests/phpMyFAQ/Session/SessionWrapperTest.php | 7 +- tests/phpMyFAQ/Session/TokenTest.php | 18 +- .../Setup/EnvironmentConfiguratorTest.php | 28 +-- tests/phpMyFAQ/Setup/HtaccessUpdaterTest.php | 122 +++++----- tests/phpMyFAQ/Setup/InstallerTest.php | 3 +- tests/phpMyFAQ/Setup/UpdateRunnerTest.php | 29 +-- tests/phpMyFAQ/Setup/UpdateTest.php | 5 +- tests/phpMyFAQ/Setup/UpgradeTest.php | 4 +- tests/phpMyFAQ/SitemapTest.php | 13 +- tests/phpMyFAQ/StopWordsTest.php | 2 +- tests/phpMyFAQ/Strings/MbstringTest.php | 8 +- tests/phpMyFAQ/Strings/StringBasicTest.php | 8 +- tests/phpMyFAQ/StringsTest.php | 12 +- tests/phpMyFAQ/SystemTest.php | 16 +- tests/phpMyFAQ/TagsTest.php | 2 +- tests/phpMyFAQ/TranslationTest.php | 2 +- .../CategoryNameTwigExtensionTest.php | 10 +- .../CreateLinkTwigExtensionTest.php | 11 +- .../Twig/Extensions/FaqTwigExtensionTest.php | 5 +- .../FormatBytesTwigExtensionTest.php | 2 +- .../FormatDateTwigExtensionTest.php | 4 +- .../Extensions/IsoDateTwigExtensionTest.php | 2 +- .../LanguageCodeTwigExtensionTest.php | 2 +- ...PermissionTranslationTwigExtensionTest.php | 4 +- .../Extensions/PluginTwigExtensionTest.php | 4 +- .../Extensions/TagNameTwigExtensionTest.php | 4 +- .../Extensions/TranslateTwigExtensionTest.php | 5 +- .../Extensions/UserNameTwigExtensionTest.php | 8 +- tests/phpMyFAQ/Twig/TwigWrapperTest.php | 2 +- tests/phpMyFAQ/User/CurrentUserTest.php | 2 +- tests/phpMyFAQ/User/TrackingTest.php | 15 +- tests/phpMyFAQ/User/TwoFactorTest.php | 40 ++-- .../phpMyFAQ/User/UserAuthenticationTest.php | 2 +- tests/phpMyFAQ/User/UserDataTest.php | 16 +- tests/phpMyFAQ/UserTest.php | 26 +- tests/phpMyFAQ/UtilsTest.php | 2 +- .../phpMyFAQ/Visits/VisitsRepositoryTest.php | 2 +- 240 files changed, 2818 insertions(+), 2904 deletions(-) delete mode 100644 phpmyfaq/src/phpMyFAQ/Controller/ContactController.php rename phpmyfaq/src/phpMyFAQ/Controller/{ => Frontend}/AbstractFrontController.php (98%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/AutoCompleteController.php (96%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/BookmarkController.php (98%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/CaptchaController.php (96%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/CommentController.php (99%) create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/ContactController.php rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/FaqController.php (99%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/QuestionController.php (99%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/RegistrationController.php (98%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/SetupController.php (98%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/TranslationController.php (97%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/UnauthorizedUserController.php (98%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/UserController.php (99%) rename phpmyfaq/src/phpMyFAQ/Controller/Frontend/{ => Api}/VotingController.php (98%) create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/WebAuthnController.php rename phpmyfaq/src/phpMyFAQ/Controller/{ => Frontend}/PageNotFoundController.php (97%) delete mode 100644 phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php delete mode 100644 phpmyfaq/update/index.php diff --git a/phpmyfaq/.htaccess b/phpmyfaq/.htaccess index 085a0efdf5..079c3fcd02 100644 --- a/phpmyfaq/.htaccess +++ b/phpmyfaq/.htaccess @@ -109,7 +109,7 @@ Header set Access-Control-Allow-Headers "Content-Type, Authorization" RewriteRule ^add-question.html$ index.php?action=ask [L,QSA] RewriteRule ^show-categories.html$ index.php?action=show [L,QSA] RewriteRule ^forgot-password$ index.php?action=password [L,QSA] - RewriteRule ^(search|open-questions|glossary|overview|login|privacy)\.html$ index.php?action=$1 [L,QSA] + RewriteRule ^(search|open-questions|glossary|overview|login|privacy|contact)\.html$ index.php?action=$1 [L,QSA] RewriteRule ^(login) index.php?action=login [L,QSA] # start page RewriteRule ^index.html$ index.php [L,QSA] @@ -162,5 +162,6 @@ Header set Access-Control-Allow-Headers "Content-Type, Authorization" RewriteCond %{REQUEST_URI} ^(.*/)setup/ RewriteCond %{REQUEST_URI} !index\.php$ RewriteRule ^(.*/)?setup/(.*) $1setup/index.php [L,QSA] - RewriteRule ^update/(.*) update/index.php [L,QSA] + RewriteRule ^update$ update/ [R=301,L] + RewriteRule ^update/(.*) index.php [L,QSA] diff --git a/phpmyfaq/index.php b/phpmyfaq/index.php index b2a4bd521a..fda896fb82 100755 --- a/phpmyfaq/index.php +++ b/phpmyfaq/index.php @@ -23,7 +23,6 @@ use phpMyFAQ\Attachment\AttachmentFactory; use phpMyFAQ\Category; use phpMyFAQ\Category\Relation; -use phpMyFAQ\Controller\PageNotFoundController; use phpMyFAQ\Core\Exception; use phpMyFAQ\Entity\SeoEntity; use phpMyFAQ\Enums\PermissionType; @@ -148,7 +147,7 @@ // Send response and exit $routeResponse->send(); - exit; + exit(); } catch (ResourceNotFoundException $e) { // No route matched - continue with legacy logic below } @@ -585,10 +584,7 @@ 'msgSubmit' => Translation::get(key: 'msgNewContentSubmit'), 'loginPageMessage' => Translation::get(key: 'loginPageMessage'), 'msgAdvancedSearch' => Translation::get(key: 'msgAdvancedSearch'), - 'currentYear' => date( - format: 'Y', - timestamp: time(), - ), + 'currentYear' => date(format: 'Y', timestamp: time()), 'cookieConsentEnabled' => $faqConfig->get('layout.enableCookieConsent'), ]; @@ -650,10 +646,7 @@ 'msgPrivacyNote' => Translation::get(key: 'msgPrivacyNote'), 'isCookieConsentEnabled' => $faqConfig->get('layout.enableCookieConsent'), 'cookiePreferences' => Translation::get(key: 'cookiePreferences'), - 'currentYear' => date( - format: 'Y', - timestamp: time(), - ), + 'currentYear' => date(format: 'Y', timestamp: time()), ]; // @@ -661,8 +654,7 @@ // if ($user->isLoggedIn() && $user->getUserId() > 0) { if ( - $user->perm->hasPermission($user->getUserId(), PermissionType::VIEW_ADMIN_LINK->value) - || $user->isSuperAdmin() + $user->perm->hasPermission($user->getUserId(), PermissionType::VIEW_ADMIN_LINK->value) || $user->isSuperAdmin() ) { $templateVars = [ ...$templateVars, @@ -688,10 +680,10 @@ // Handle 404 action with PageNotFoundController // if ('404' === $action) { - $pageNotFoundController = new \phpMyFAQ\Controller\PageNotFoundController(); + $pageNotFoundController = new \phpMyFAQ\Controller\Frontend\PageNotFoundController(); $notFoundResponse = $pageNotFoundController->index($request); $notFoundResponse->send(); - exit; + exit(); } // diff --git a/phpmyfaq/setup/index.php b/phpmyfaq/setup/index.php index db764794d5..b2b4ef442f 100644 --- a/phpmyfaq/setup/index.php +++ b/phpmyfaq/setup/index.php @@ -25,7 +25,7 @@ use Composer\Autoload\ClassLoader; use phpMyFAQ\Application; -use phpMyFAQ\Controller\Frontend\SetupController; +use phpMyFAQ\Controller\Frontend\Api\SetupController; use phpMyFAQ\Environment; use phpMyFAQ\Strings; use phpMyFAQ\Translation; diff --git a/phpmyfaq/src/Bootstrap.php b/phpmyfaq/src/Bootstrap.php index b0dd87f57f..18f3c12dfe 100644 --- a/phpmyfaq/src/Bootstrap.php +++ b/phpmyfaq/src/Bootstrap.php @@ -25,8 +25,8 @@ use phpMyFAQ\Configuration\ElasticsearchConfiguration; use phpMyFAQ\Configuration\LdapConfiguration; use phpMyFAQ\Configuration\OpenSearchConfiguration; -use phpMyFAQ\Database; use phpMyFAQ\Core\Exception; +use phpMyFAQ\Database; use phpMyFAQ\Environment; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -40,7 +40,7 @@ $foundCurrPath = false; $includePaths = explode(PATH_SEPARATOR, ini_get('include_path')); $i = 0; -while ((!$foundCurrPath) && ($i < count($includePaths))) { +while (!$foundCurrPath && $i < count($includePaths)) { if ('.' == $includePaths[$i]) { $foundCurrPath = true; } @@ -100,7 +100,7 @@ if (!$isSetupContext) { $response = new RedirectResponse('/setup/'); $response->send(); - exit; + exit(); } } else { if (file_exists(PMF_CONFIG_DIR . '/database.php')) { @@ -152,7 +152,7 @@ $dbConfig->getUser(), $dbConfig->getPassword(), $dbConfig->getDatabase(), - $dbConfig->getPort() + $dbConfig->getPort(), ); } catch (Exception $exception) { Database::errorPage($exception->getMessage()); @@ -210,7 +210,7 @@ // Optional: wait for Elasticsearch to be ready (HTTP health) try { $http = HttpClient::create(['verify_peer' => false]); - $deadline = time() + (int)($_ENV['SEARCH_WAIT_TIMEOUT'] ?? 15); + $deadline = time() + (int) ($_ENV['SEARCH_WAIT_TIMEOUT'] ?? 15); do { try { $res = $http->request('GET', rtrim($esBaseUri, '/') . '/_cluster/health'); @@ -247,7 +247,7 @@ // Optional: wait for OpenSearch to be ready (HTTP health) try { $http = HttpClient::create(['verify_peer' => false]); - $deadline = time() + (int)($_ENV['SEARCH_WAIT_TIMEOUT'] ?? 15); + $deadline = time() + (int) ($_ENV['SEARCH_WAIT_TIMEOUT'] ?? 15); do { try { $res = $http->request('GET', rtrim($baseUri, '/') . '/_cluster/health'); diff --git a/phpmyfaq/src/admin-api-routes.php b/phpmyfaq/src/admin-api-routes.php index 048ec86e63..832bdd1178 100644 --- a/phpmyfaq/src/admin-api-routes.php +++ b/phpmyfaq/src/admin-api-routes.php @@ -53,560 +53,554 @@ 'admin.api.content.attachments' => [ 'path' => '/content/attachments', 'controller' => [AttachmentController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.content.attachments.refresh' => [ 'path' => '/content/attachments/refresh', 'controller' => [AttachmentController::class, 'refresh'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.content.attachments.upload' => [ 'path' => '/content/attachments/upload', 'controller' => [AttachmentController::class, 'upload'], - 'methods' => 'POST' + 'methods' => 'POST', ], // Category API 'admin.api.category.delete' => [ 'path' => '/category/delete', 'controller' => [CategoryController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.category.permissions' => [ 'path' => '/category/permissions/{categories}', 'controller' => [CategoryController::class, 'permissions'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.category.update-order' => [ 'path' => '/category/update-order', 'controller' => [CategoryController::class, 'updateOrder'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.category.translations' => [ 'path' => '/category/translations/{categoryId}', 'controller' => [CategoryController::class, 'translations'], - 'methods' => 'GET' + 'methods' => 'GET', ], // Comment API 'admin.api.content.comments' => [ 'path' => '/content/comments', 'controller' => [CommentController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], // Configuration API 'admin.api.configuration.activate-maintenance-mode' => [ 'path' => '/configuration/activate-maintenance-mode', 'controller' => [ConfigurationController::class, 'activateMaintenanceMode'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.configuration.faqs-sorting-key' => [ 'path' => '/configuration/faqs-sorting-key/{current}', 'controller' => [ConfigurationTabController::class, 'faqsSortingKey'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.configuration.faqs-sorting-order' => [ 'path' => '/configuration/faqs-sorting-order/{current}', 'controller' => [ConfigurationTabController::class, 'faqsSortingOrder'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.configuration.faqs-sorting-popular' => [ 'path' => '/configuration/faqs-sorting-popular/{current}', 'controller' => [ConfigurationTabController::class, 'faqsSortingPopular'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.configuration.list' => [ 'path' => '/configuration/list/{mode}', 'controller' => [ConfigurationTabController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.configuration.permLevel' => [ 'path' => '/configuration/perm-level/{current}', 'controller' => [ConfigurationTabController::class, 'permLevel'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.configuration.release-environment' => [ 'path' => '/configuration/release-environment/{current}', 'controller' => [ConfigurationTabController::class, 'releaseEnvironment'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.configuration.save' => [ 'path' => '/configuration', 'controller' => [ConfigurationTabController::class, 'save'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.configuration.search-relevance' => [ 'path' => '/configuration/search-relevance/{current}', 'controller' => [ConfigurationTabController::class, 'searchRelevance'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.configuration.seo-metatags' => [ 'path' => '/configuration/seo-metatags/{current}', 'controller' => [ConfigurationTabController::class, 'seoMetaTags'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.configuration.send-test-mail' => [ 'path' => '/configuration/send-test-mail', 'controller' => [ConfigurationController::class, 'sendTestMail'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.configuration.translations' => [ 'path' => '/configuration/translations', 'controller' => [ConfigurationTabController::class, 'translations'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.configuration.templates' => [ 'path' => '/configuration/templates', 'controller' => [ConfigurationTabController::class, 'templates'], - 'methods' => 'GET' + 'methods' => 'GET', ], // Glossary API 'admin.api.glossary.create' => [ 'path' => '/glossary/create', 'controller' => [GlossaryController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.glossary.delete' => [ 'path' => '/glossary/delete', 'controller' => [GlossaryController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.glossary.update' => [ 'path' => '/glossary/update', 'controller' => [GlossaryController::class, 'update'], - 'methods' => 'PUT' + 'methods' => 'PUT', ], 'admin.api.glossary' => [ 'path' => '/glossary/{glossaryId}/{glossaryLanguage}', 'controller' => [GlossaryController::class, 'fetch'], - 'methods' => 'GET' + 'methods' => 'GET', ], // Image API 'admin.api.content.images' => [ 'path' => '/content/images', 'controller' => [ImageController::class, 'upload'], - 'methods' => 'POST' + 'methods' => 'POST', ], // Instance API 'admin.api.instance.add' => [ 'path' => '/instance/add', 'controller' => [InstanceController::class, 'add'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.instance.delete' => [ 'path' => '/instance/delete', 'controller' => [InstanceController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], // Markdown API 'admin.api.content.markdown' => [ 'path' => '/content/markdown', 'controller' => [MarkdownController::class, 'renderMarkdown'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.media.browser' => [ 'path' => '/media-browser', 'controller' => [MediaBrowserController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], // Dashboard API 'admin.api.dashboard.topten' => [ 'path' => '/dashboard/topten', 'controller' => [DashboardController::class, 'topTen'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.dashboard.verify' => [ 'path' => '/dashboard/verify', 'controller' => [DashboardController::class, 'verify'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.dashboard.versions' => [ 'path' => '/dashboard/versions', 'controller' => [DashboardController::class, 'versions'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.dashboard.visits' => [ 'path' => '/dashboard/visits', 'controller' => [DashboardController::class, 'visits'], - 'methods' => 'GET' + 'methods' => 'GET', ], // Elasticsearch API 'admin.api.elasticsearch.create' => [ 'path' => '/elasticsearch/create', 'controller' => [ElasticsearchController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.elasticsearch.drop' => [ 'path' => '/elasticsearch/drop', 'controller' => [ElasticsearchController::class, 'drop'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.elasticsearch.import' => [ 'path' => '/elasticsearch/import', 'controller' => [ElasticsearchController::class, 'import'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.elasticsearch.statistics' => [ 'path' => '/elasticsearch/statistics', 'controller' => [ElasticsearchController::class, 'statistics'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.elasticsearch.healthcheck' => [ 'path' => '/elasticsearch/healthcheck', 'controller' => [ElasticsearchController::class, 'healthcheck'], - 'methods' => 'GET' + 'methods' => 'GET', ], // OpenSearch API 'admin.api.opensearch.create' => [ 'path' => '/opensearch/create', 'controller' => [OpenSearchController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.opensearch.drop' => [ 'path' => '/opensearch/drop', 'controller' => [OpenSearchController::class, 'drop'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.opensearch.import' => [ 'path' => '/opensearch/import', 'controller' => [OpenSearchController::class, 'import'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.opensearch.statistics' => [ 'path' => '/opensearch/statistics', 'controller' => [OpenSearchController::class, 'statistics'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.opensearch.healthcheck' => [ 'path' => '/opensearch/healthcheck', 'controller' => [OpenSearchController::class, 'healthcheck'], - 'methods' => 'GET' + 'methods' => 'GET', ], // Export API 'admin.api.export.file' => [ 'path' => '/export/file', 'controller' => [ExportController::class, 'exportFile'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.export.report' => [ 'path' => '/export/report', 'controller' => [ExportController::class, 'exportReport'], - 'methods' => 'POST' + 'methods' => 'POST', ], // FAQ API 'admin.api.faq.activate' => [ 'path' => '/faq/activate', 'controller' => [FaqController::class, 'activate'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.faq.create' => [ 'path' => '/faq/create', 'controller' => [FaqController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.faq.delete' => [ 'path' => '/faq/delete', 'controller' => [FaqController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.faq.update' => [ 'path' => '/faq/update', 'controller' => [FaqController::class, 'update'], - 'methods' => 'PUT' + 'methods' => 'PUT', ], 'admin.api.faq.permissions' => [ 'path' => '/faq/permissions/{faqId}', 'controller' => [FaqController::class, 'listPermissions'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.faq.search' => [ 'path' => '/faq/search', 'controller' => [FaqController::class, 'search'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.faq.sticky' => [ 'path' => '/faq/sticky', 'controller' => [FaqController::class, 'sticky'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.faqs.sticky.order' => [ 'path' => '/faqs/sticky/order', 'controller' => [FaqController::class, 'saveOrderOfStickyFaqs'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.faqs' => [ 'path' => '/faqs/{categoryId}/{language}', 'controller' => [FaqController::class, 'listByCategory'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.faq.import' => [ 'path' => '/faq/import', 'controller' => [FaqController::class, 'import'], - 'methods' => 'POST' + 'methods' => 'POST', ], // Group API 'admin.api.group.groups' => [ 'path' => '/group/groups', 'controller' => [GroupController::class, 'listGroups'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.group.members' => [ 'path' => '/group/members/{groupId}', 'controller' => [GroupController::class, 'listMembers'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.group.permissions' => [ 'path' => '/group/permissions/{groupId}', 'controller' => [GroupController::class, 'listPermissions'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.group.users' => [ 'path' => '/group/users', 'controller' => [GroupController::class, 'listUsers'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.group.data' => [ 'path' => '/group/data/{groupId}', 'controller' => [GroupController::class, 'groupData'], - 'methods' => 'GET' + 'methods' => 'GET', ], // Question API 'admin.api.question.delete' => [ 'path' => '/question/delete', 'controller' => [QuestionController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.question.toggle' => [ 'path' => '/question/visibility/toggle', 'controller' => [QuestionController::class, 'toggle'], - 'methods' => 'PUT' + 'methods' => 'PUT', ], // Search API 'admin.api.search.term' => [ 'path' => '/search/term', 'controller' => [SearchController::class, 'deleteTerm'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], // Stop word API 'admin.api.stopwords' => [ 'path' => '/stopwords', 'controller' => [StopWordController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.stopword.delete' => [ 'path' => '/stopword/delete', 'controller' => [StopWordController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.stopword.save' => [ 'path' => '/stopword/save', 'controller' => [StopWordController::class, 'save'], - 'methods' => 'POST' + 'methods' => 'POST', ], // Tag API 'admin.api.content.tag' => [ 'path' => '/content/tag', 'controller' => [TagController::class, 'update'], - 'methods' => 'PUT' + 'methods' => 'PUT', ], 'admin.api.content.tags' => [ 'path' => '/content/tags', 'controller' => [TagController::class, 'search'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.content.tags.id' => [ 'path' => '/content/tags/{tagId}', 'controller' => [TagController::class, 'delete'], - 'methods' => 'GET' + 'methods' => 'GET', ], // Update API 'admin.api.health-check' => [ 'path' => '/health-check', 'controller' => [UpdateController::class, 'healthCheck'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.versions' => [ 'path' => '/versions', 'controller' => [UpdateController::class, 'versions'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.update-check' => [ 'path' => '/update-check', 'controller' => [UpdateController::class, 'updateCheck'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.download-package' => [ 'path' => '/download-package/{versionNumber}', 'controller' => [UpdateController::class, 'downloadPackage'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.extract-package' => [ 'path' => '/extract-package', 'controller' => [UpdateController::class, 'extractPackage'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.create-temporary-backup' => [ 'path' => '/create-temporary-backup', 'controller' => [UpdateController::class, 'createTemporaryBackup'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.install-package' => [ 'path' => '/install-package', 'controller' => [UpdateController::class, 'installPackage'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.update-database' => [ 'path' => '/update-database', 'controller' => [UpdateController::class, 'updateDatabase'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.cleanup' => [ 'path' => '/cleanup', 'controller' => [UpdateController::class, 'cleanUp'], - 'methods' => 'POST' + 'methods' => 'POST', ], // User API 'admin.api.user.users' => [ 'path' => '/user/users', 'controller' => [UserController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.user.users.csv' => [ 'path' => '/user/users/csv', 'controller' => [UserController::class, 'csvExport'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.user.add' => [ 'path' => '/user/add', 'controller' => [UserController::class, 'addUser'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.user.data' => [ 'path' => '/user/data/{userId}', 'controller' => [UserController::class, 'userData'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.user.delete' => [ 'path' => '/user/delete', 'controller' => [UserController::class, 'deleteUser'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.user.edit' => [ 'path' => '/user/edit', 'controller' => [UserController::class, 'editUser'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.user.update-rights' => [ 'path' => '/user/update-rights', 'controller' => [UserController::class, 'updateUserRights'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.user.permissions' => [ 'path' => '/user/permissions/{userId}', 'controller' => [UserController::class, 'userPermissions'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.api.user.activate' => [ 'path' => '/user/activate', 'controller' => [UserController::class, 'activate'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.user.overwrite-password' => [ 'path' => '/user/overwrite-password', 'controller' => [UserController::class, 'overwritePassword'], - 'methods' => 'POST' + 'methods' => 'POST', ], // Session API 'admin.api.session.export' => [ 'path' => '/session/export', 'controller' => [SessionController::class, 'export'], - 'methods' => 'POST' + 'methods' => 'POST', ], // Statistics API 'admin.api.statistics.adminlog.delete' => [ 'path' => '/statistics/admin-log', 'controller' => [StatisticsController::class, 'deleteAdminLog'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.statistics.ratings.clear' => [ 'path' => '/statistics/ratings/clear', 'controller' => [StatisticsController::class, 'clearRatings'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.statistics.search-terms.truncate' => [ 'path' => '/statistics/search-terms', 'controller' => [StatisticsController::class, 'truncateSearchTerms'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.statistics.sessions.truncate' => [ 'path' => '/statistics/sessions', 'controller' => [StatisticsController::class, 'truncateSessions'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.statistics.visits.clear' => [ 'path' => '/statistics/visits/clear', 'controller' => [StatisticsController::class, 'clearVisits'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], // Forms API 'admin.api.forms.activate' => [ 'path' => '/forms/activate', 'controller' => [FormController::class, 'activateInput'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.forms.required' => [ 'path' => '/forms/required', 'controller' => [FormController::class, 'setInputAsRequired'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.forms.translation-edit' => [ 'path' => '/forms/translation-edit', 'controller' => [FormController::class, 'editTranslation'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.forms.translation-delete' => [ 'path' => '/forms/translation-delete', 'controller' => [FormController::class, 'deleteTranslation'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.forms.translation-add' => [ 'path' => '/forms/translation-add', 'controller' => [FormController::class, 'addTranslation'], - 'methods' => 'POST' + 'methods' => 'POST', ], // News API 'admin.api.news.create' => [ 'path' => '/news/create', 'controller' => [NewsController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.api.news.delete' => [ 'path' => '/news/delete', 'controller' => [NewsController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'admin.api.news.update' => [ 'path' => '/news/update', 'controller' => [NewsController::class, 'update'], - 'methods' => 'PUT' + 'methods' => 'PUT', ], 'admin.api.news.activate' => [ 'path' => '/news/activate', 'controller' => [NewsController::class, 'activate'], - 'methods' => 'POST' + 'methods' => 'POST', ], ]; foreach ($routesConfig as $name => $config) { - $routes->add( - $name, - new Route( - $config['path'], - [ - '_controller' => $config['controller'], - '_methods' => $config['methods'] - ] - ) - ); + $routes->add($name, new Route($config['path'], [ + '_controller' => $config['controller'], + '_methods' => $config['methods'], + ])); } return $routes; diff --git a/phpmyfaq/src/admin-routes.php b/phpmyfaq/src/admin-routes.php index cd052c89f3..9d2c613058 100644 --- a/phpmyfaq/src/admin-routes.php +++ b/phpmyfaq/src/admin-routes.php @@ -59,362 +59,355 @@ 'admin.attachments' => [ 'path' => '/attachments', 'controller' => [AttachmentsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.auth.authenticate' => [ 'path' => '/authenticate', 'controller' => [AuthenticationController::class, 'authenticate'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.auth.check' => [ 'path' => '/check', 'controller' => [AuthenticationController::class, 'check'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.auth.login' => [ 'path' => '/login', 'controller' => [AuthenticationController::class, 'login'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.auth.logout' => [ 'path' => '/logout', 'controller' => [AuthenticationController::class, 'logout'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.auth.token' => [ 'path' => '/token', 'controller' => [AuthenticationController::class, 'token'], 'methods' => 'GET', - ], 'admin.backup' => [ 'path' => '/backup', 'controller' => [BackupController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.backup.export' => [ 'path' => '/backup/export/{type}', 'controller' => [BackupController::class, 'export'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.backup.restore' => [ 'path' => '/backup/restore', 'controller' => [BackupController::class, 'restore'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.category' => [ 'path' => '/category', 'controller' => [CategoryController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.category.add' => [ 'path' => '/category/add', 'controller' => [CategoryController::class, 'add'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.category.add.child' => [ 'path' => '/category/add/{parentId}/{language}', 'controller' => [CategoryController::class, 'addChild'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.category.create' => [ 'path' => '/category/create', 'controller' => [CategoryController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.category.edit' => [ 'path' => '/category/edit/{categoryId}', 'controller' => [CategoryController::class, 'edit'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.category.hierarchy' => [ 'path' => '/category/hierarchy', 'controller' => [CategoryController::class, 'hierarchy'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.category.translate' => [ 'path' => '/category/translate/{categoryId}', 'controller' => [CategoryController::class, 'translate'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.category.update' => [ 'path' => '/category/update', 'controller' => [CategoryController::class, 'update'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.content.orphaned-faqs' => [ 'path' => '/orphaned-faqs', 'controller' => [OrphanedFaqsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.content.sticky-faqs' => [ 'path' => '/sticky-faqs', 'controller' => [StickyFaqsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.comments' => [ 'path' => '/comments', 'controller' => [CommentsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.configuration' => [ 'path' => '/configuration', 'controller' => [ConfigurationController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.configuration.plugins' => [ 'path' => '/plugins', 'controller' => [PluginController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.dashboard' => [ 'path' => '/', 'controller' => [DashboardController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.faq.add' => [ 'path' => '/faq/add', 'controller' => [FaqController::class, 'add'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.faq.add.category' => [ 'path' => '/faq/add/{categoryId}/{categoryLanguage}', 'controller' => [FaqController::class, 'addInCategory'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.faq.answer' => [ 'path' => '/faq/answer/{questionId}/{faqLanguage}', 'controller' => [FaqController::class, 'answer'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.faq.copy' => [ 'path' => '/faq/copy/{faqId}/{faqLanguage}', 'controller' => [FaqController::class, 'copy'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.faq.edit' => [ 'path' => '/faq/edit/{faqId}/{faqLanguage}', 'controller' => [FaqController::class, 'edit'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.faq.translate' => [ 'path' => '/faq/translate/{faqId}/{faqLanguage}', 'controller' => [FaqController::class, 'translate'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.faqs' => [ 'path' => '/faqs', 'controller' => [FaqController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.forms' => [ 'path' => '/forms', 'controller' => [FormsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.forms.translate' => [ 'path' => '/forms/translate/{formId}/{inputId}', 'controller' => [FormsController::class, 'translate'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.elasticsearch' => [ 'path' => '/elasticsearch', 'controller' => [ElasticsearchController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.opensearch' => [ 'path' => '/opensearch', 'controller' => [OpenSearchController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.export' => [ 'path' => '/export', 'controller' => [ExportController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.glossary' => [ 'path' => '/glossary', 'controller' => [GlossaryController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.group' => [ 'path' => '/group', 'controller' => [GroupController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.group.add' => [ 'path' => '/group/add', 'controller' => [GroupController::class, 'add'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.group.create' => [ 'path' => '/group/create', 'controller' => [GroupController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.group.confirm' => [ 'path' => '/group/confirm', 'controller' => [GroupController::class, 'confirm'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.group.delete' => [ 'path' => '/group/delete', 'controller' => [GroupController::class, 'delete'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.group.update' => [ 'path' => '/group/update', 'controller' => [GroupController::class, 'update'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.group.update.members' => [ 'path' => '/group/update/members', 'controller' => [GroupController::class, 'updateMembers'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.group.update.permissions' => [ 'path' => '/group/update/permissions', 'controller' => [GroupController::class, 'updatePermissions'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.import' => [ 'path' => '/import', 'controller' => [ImportController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.instance.edit' => [ 'path' => '/instance/edit/{id}', 'controller' => [InstanceController::class, 'edit'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.instance.update' => [ 'path' => '/instance/update', 'controller' => [InstanceController::class, 'update'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.instances' => [ 'path' => '/instances', 'controller' => [InstanceController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.news' => [ 'path' => '/news', 'controller' => [NewsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.news.add' => [ 'path' => '/news/add', 'controller' => [NewsController::class, 'add'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.news.edit' => [ 'path' => '/news/edit/{newsId}', 'controller' => [NewsController::class, 'edit'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.password.change' => [ 'path' => '/password/change', 'controller' => [PasswordChangeController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.password.update' => [ 'path' => '/password/update', 'controller' => [PasswordChangeController::class, 'update'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'admin.questions' => [ 'path' => '/questions', 'controller' => [OpenQuestionsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.session.keepalive' => [ 'path' => '/session-keep-alive', 'controller' => [SessionKeepAliveController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.statistics.admin-log' => [ 'path' => '/statistics/admin-log', 'controller' => [AdminLogController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.statistics.ratings' => [ 'path' => '/statistics/ratings', 'controller' => [RatingController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.statistics.report' => [ 'path' => '/statistics/report', 'controller' => [ReportController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.statistics.search' => [ 'path' => '/statistics/search', 'controller' => [StatisticsSearchController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.statistics.sessions' => [ 'path' => '/statistics/sessions', 'controller' => [StatisticsSessionsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.statistics.sessions.day' => [ 'path' => '/statistics/sessions/{date}', 'controller' => [StatisticsSessionsController::class, 'viewDay'], - 'methods' => 'POST, GET' + 'methods' => 'POST, GET', ], 'admin.statistics.session.id' => [ 'path' => '/statistics/session/{sessionId}', 'controller' => [StatisticsSessionsController::class, 'viewSession'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.stopwords' => [ 'path' => '/stopwords', 'controller' => [StopwordsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.system' => [ 'path' => '/system', 'controller' => [SystemInformationController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.tags' => [ 'path' => '/tags', 'controller' => [TagController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.update' => [ 'path' => '/update', 'controller' => [UpdateController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.user' => [ 'path' => '/user', 'controller' => [UserController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.user.edit' => [ 'path' => '/user/edit/{userId}', 'controller' => [UserController::class, 'edit'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'admin.user.list' => [ 'path' => '/user/list', 'controller' => [UserController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], ]; foreach ($routesConfig as $name => $config) { - $routes->add( - $name, - new Route( - $config['path'], - [ - '_controller' => $config['controller'], - '_methods' => $config['methods'] - ] - ) - ); + $routes->add($name, new Route($config['path'], [ + '_controller' => $config['controller'], + '_methods' => $config['methods'], + ])); } return $routes; diff --git a/phpmyfaq/src/api-routes.php b/phpmyfaq/src/api-routes.php index 435f2983f2..a1407f48c7 100644 --- a/phpmyfaq/src/api-routes.php +++ b/phpmyfaq/src/api-routes.php @@ -35,19 +35,19 @@ use phpMyFAQ\Controller\Api\TagController; use phpMyFAQ\Controller\Api\TitleController; use phpMyFAQ\Controller\Api\VersionController; -use phpMyFAQ\Controller\Frontend\AutoCompleteController; -use phpMyFAQ\Controller\Frontend\BookmarkController; -use phpMyFAQ\Controller\Frontend\CaptchaController; -use phpMyFAQ\Controller\Frontend\CommentController as CommentFrontendController; -use phpMyFAQ\Controller\Frontend\ContactController; -use phpMyFAQ\Controller\Frontend\FaqController as FaqFrontendController; -use phpMyFAQ\Controller\Frontend\QuestionController as QuestionFrontendController; -use phpMyFAQ\Controller\Frontend\RegistrationController as RegistrationFrontendController; -use phpMyFAQ\Controller\Frontend\TranslationController; -use phpMyFAQ\Controller\Frontend\UnauthorizedUserController; -use phpMyFAQ\Controller\Frontend\UserController; -use phpMyFAQ\Controller\Frontend\VotingController; -use phpMyFAQ\Controller\Frontend\WebAuthnController; +use phpMyFAQ\Controller\Frontend\Api\AutoCompleteController; +use phpMyFAQ\Controller\Frontend\Api\BookmarkController; +use phpMyFAQ\Controller\Frontend\Api\CaptchaController; +use phpMyFAQ\Controller\Frontend\Api\CommentController as CommentFrontendController; +use phpMyFAQ\Controller\Frontend\Api\ContactController; +use phpMyFAQ\Controller\Frontend\Api\FaqController as FaqFrontendController; +use phpMyFAQ\Controller\Frontend\Api\QuestionController as QuestionFrontendController; +use phpMyFAQ\Controller\Frontend\Api\RegistrationController as RegistrationFrontendController; +use phpMyFAQ\Controller\Frontend\Api\TranslationController; +use phpMyFAQ\Controller\Frontend\Api\UnauthorizedUserController; +use phpMyFAQ\Controller\Frontend\Api\UserController; +use phpMyFAQ\Controller\Frontend\Api\VotingController; +use phpMyFAQ\Controller\Frontend\Api\WebAuthnController; use phpMyFAQ\System; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -61,279 +61,273 @@ 'api.attachments' => [ 'path' => "v{$apiVersion}/attachments/{recordId}", 'controller' => [AttachmentController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.backup' => [ 'path' => "v{$apiVersion}/backup/{type}", 'controller' => [BackupController::class, 'download'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.categories' => [ 'path' => "v{$apiVersion}/categories", 'controller' => [CategoryController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.category' => [ 'path' => "v{$apiVersion}/category", 'controller' => [CategoryController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.faq.create' => [ 'path' => "v{$apiVersion}/faq/create", 'controller' => [FaqController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.faq.update' => [ 'path' => "v{$apiVersion}/faq/update", 'controller' => [FaqController::class, 'update'], - 'methods' => 'PUT' + 'methods' => 'PUT', ], 'api.faq-by-id' => [ 'path' => "v{$apiVersion}/faq/{categoryId}/{faqId}", 'controller' => [FaqController::class, 'getById'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.comments' => [ 'path' => "v{$apiVersion}/comments/{recordId}", 'controller' => [CommentController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.faqs.by-tag-id' => [ 'path' => "v{$apiVersion}/faqs/tags/{tagId}", 'controller' => [FaqController::class, 'getByTagId'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.faqs.latest' => [ 'path' => "v{$apiVersion}/faqs/latest", 'controller' => [FaqController::class, 'getLatest'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.faqs.popular' => [ 'path' => "v{$apiVersion}/faqs/popular", 'controller' => [FaqController::class, 'getPopular'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.faqs.trending' => [ 'path' => "v{$apiVersion}/faqs/trending", 'controller' => [FaqController::class, 'getTrending'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.faqs.sticky' => [ 'path' => "v{$apiVersion}/faqs/sticky", 'controller' => [FaqController::class, 'getSticky'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.faqs.by-category-id' => [ 'path' => "v{$apiVersion}/faqs/{categoryId}", 'controller' => [FaqController::class, 'getByCategoryId'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.faqs' => [ 'path' => "v{$apiVersion}/faqs", 'controller' => [FaqController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.groups' => [ 'path' => "v{$apiVersion}/groups", 'controller' => [GroupController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.language' => [ 'path' => "v{$apiVersion}/language", 'controller' => [LanguageController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.login' => [ 'path' => "v{$apiVersion}/login", 'controller' => [LoginController::class, 'login'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.news' => [ 'path' => "v{$apiVersion}/news", 'controller' => [NewsController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.open-questions' => [ 'path' => "v{$apiVersion}/open-questions", 'controller' => [OpenQuestionController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.pdf-by-id' => [ 'path' => "v{$apiVersion}/pdf/{categoryId}/{faqId}", 'controller' => [PdfController::class, 'getById'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.question' => [ 'path' => "v{$apiVersion}/question", 'controller' => [QuestionController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.register' => [ 'path' => "v{$apiVersion}/register", 'controller' => [RegistrationController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.search' => [ 'path' => "v{$apiVersion}/search", 'controller' => [SearchController::class, 'search'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.search.popular' => [ 'path' => "v{$apiVersion}/searches/popular", 'controller' => [SearchController::class, 'popular'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.tags' => [ 'path' => "v{$apiVersion}/tags", 'controller' => [TagController::class, 'list'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.title' => [ 'path' => "v{$apiVersion}/title", 'controller' => [TitleController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.version' => [ 'path' => "v{$apiVersion}/version", 'controller' => [VersionController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], // Private REST API 'api.private.autocomplete' => [ 'path' => 'autocomplete', 'controller' => [AutoCompleteController::class, 'search'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.private.bookmark.create' => [ 'path' => 'bookmark/create', 'controller' => [BookmarkController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.bookmark.delete' => [ 'path' => 'bookmark/delete', 'controller' => [BookmarkController::class, 'delete'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'api.private.bookmark.delete-all' => [ 'path' => 'bookmark/delete-all', 'controller' => [BookmarkController::class, 'deleteAll'], - 'methods' => 'DELETE' + 'methods' => 'DELETE', ], 'api.private.captcha' => [ 'path' => 'captcha', 'controller' => [CaptchaController::class, 'renderImage'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'api.private.contact' => [ 'path' => 'contact', 'controller' => [ContactController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.comment' => [ 'path' => 'comment/create', 'controller' => [CommentFrontendController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.faq.create' => [ 'path' => 'faq/create', 'controller' => [FaqFrontendController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.question.create' => [ 'path' => 'question/create', 'controller' => [QuestionFrontendController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.register' => [ 'path' => 'register', 'controller' => [RegistrationFrontendController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.translations' => [ 'path' => 'translations/{language}', 'controller' => [TranslationController::class, 'translations'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.user.data.export' => [ 'path' => 'user/data/export', 'controller' => [UserController::class, 'exportUserData'], - 'methods' => 'PUT' + 'methods' => 'PUT', ], 'api.private.user.password' => [ 'path' => 'user/password/update', 'controller' => [UnauthorizedUserController::class, 'updatePassword'], - 'methods' => 'PUT' + 'methods' => 'PUT', ], 'api.private.user.request-removal' => [ 'path' => 'user/request-removal', 'controller' => [UserController::class, 'requestUserRemoval'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.user.remove-twofactor' => [ 'path' => 'user/remove-twofactor', 'controller' => [UserController::class, 'removeTwofactorConfig'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.user.update' => [ 'path' => 'user/data/update', 'controller' => [UserController::class, 'updateData'], - 'methods' => 'PUT' + 'methods' => 'PUT', ], 'api.private.voting' => [ 'path' => 'voting', 'controller' => [VotingController::class, 'create'], - 'methods' => 'POST' + 'methods' => 'POST', ], // Setup REST API 'api.private.setup.check' => [ 'path' => 'setup/check', 'controller' => [SetupController::class, 'check'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.setup.backup' => [ 'path' => 'setup/backup', 'controller' => [SetupController::class, 'backup'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.setup.update-database' => [ 'path' => 'setup/update-database', 'controller' => [SetupController::class, 'updateDatabase'], - 'methods' => 'POST' + 'methods' => 'POST', ], // WebAuthn REST API 'api.private.webauthn.login' => [ 'path' => 'webauthn/login', 'controller' => [WebAuthnController::class, 'login'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.webauthn.prepare' => [ 'path' => 'webauthn/prepare', 'controller' => [WebAuthnController::class, 'prepare'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.webauthn.prepare-login' => [ 'path' => 'webauthn/prepare-login', 'controller' => [WebAuthnController::class, 'prepareLogin'], - 'methods' => 'POST' + 'methods' => 'POST', ], 'api.private.webauthn.register' => [ 'path' => 'webauthn/register', 'controller' => [WebAuthnController::class, 'register'], - 'methods' => 'POST' + 'methods' => 'POST', ], ]; foreach ($routesConfig as $name => $config) { - $routes->add( - $name, - new Route( - $config['path'], - [ - '_controller' => $config['controller'], - '_methods' => $config['methods'] - ] - ) - ); + $routes->add($name, new Route($config['path'], [ + '_controller' => $config['controller'], + '_methods' => $config['methods'], + ])); } return $routes; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php b/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php deleted file mode 100644 index 5b24ada887..0000000000 --- a/phpmyfaq/src/phpMyFAQ/Controller/ContactController.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @copyright 2002-2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2002-09-16 - */ - -declare(strict_types=1); - -namespace phpMyFAQ\Controller; - -use Exception; -use phpMyFAQ\Translation; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -final class ContactController extends AbstractFrontController -{ - /** - * Handles both GET and POST requests for the contact form - * @throws Exception - */ - #[Route(path: '/contact.html', name: 'public.contact')] - public function index(Request $request): Response - { - $faqSession = $this->container->get('phpmyfaq.user.session'); - $faqSession->setCurrentUser($this->currentUser); - $faqSession->userTracking('contact', 0); - - $captcha = $this->container->get('phpmyfaq.captcha'); - $captchaHelper = $this->container->get('phpmyfaq.captcha.helper.captcha_helper'); - - if ($this->configuration->get('layout.contactInformationHTML')) { - $contactText = html_entity_decode((string) $this->configuration->get('main.contactInformation')); - } else { - $contactText = nl2br((string) $this->configuration->get('main.contactInformation')); - } - - return $this->render('contact.twig', [ - ...$this->getHeader($request), - 'title' => sprintf('%s - %s', Translation::get(key: 'msgContact'), $this->configuration->getTitle()), - 'msgContactOwnText' => $contactText, - 'privacyURL' => $this->configuration->get('main.privacyURL'), - 'lang' => $this->configuration->getLanguage()->getLanguage(), - 'defaultContentMail' => $this->currentUser->getUserId() > 0 ? $this->currentUser->getUserData('email') : '', - 'defaultContentName' => $this->currentUser->getUserId() > 0 - ? $this->currentUser->getUserData('display_name') - : '', - 'version' => $this->configuration->getVersion(), - 'captchaFieldset' => $captchaHelper->renderCaptcha( - $captcha, - 'contact', - Translation::get(key: 'msgCaptcha'), - $this->currentUser->isLoggedIn(), - ), - ]); - } -} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/AbstractFrontController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php similarity index 98% rename from phpmyfaq/src/phpMyFAQ/Controller/AbstractFrontController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php index 699e7567ed..3157c10d24 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/AbstractFrontController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php @@ -17,8 +17,9 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller; +namespace phpMyFAQ\Controller\Frontend; +use phpMyFAQ\Controller\AbstractController; use phpMyFAQ\Core\Exception; use phpMyFAQ\Enums\PermissionType; use phpMyFAQ\Environment; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AutoCompleteController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/AutoCompleteController.php similarity index 96% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/AutoCompleteController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/AutoCompleteController.php index 7f808313ed..56bd5f6041 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AutoCompleteController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/AutoCompleteController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use phpMyFAQ\Category; use phpMyFAQ\Controller\AbstractController; @@ -27,7 +27,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; final class AutoCompleteController extends AbstractController { diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/BookmarkController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/BookmarkController.php similarity index 98% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/BookmarkController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/BookmarkController.php index c9f9546710..4e8cc39f49 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/BookmarkController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/BookmarkController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use Exception; use JsonException; @@ -29,7 +29,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; final class BookmarkController extends AbstractController { diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/CaptchaController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/CaptchaController.php similarity index 96% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/CaptchaController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/CaptchaController.php index 3cffd63c48..c326f62e28 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/CaptchaController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/CaptchaController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use phpMyFAQ\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/CommentController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/CommentController.php similarity index 99% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/CommentController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/CommentController.php index 0891c4400c..e1af313d90 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/CommentController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/CommentController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use phpMyFAQ\Controller\AbstractController; use phpMyFAQ\Core\Exception; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/ContactController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/ContactController.php new file mode 100644 index 0000000000..e8741dba47 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/ContactController.php @@ -0,0 +1,94 @@ + + * @copyright 2024-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2024-03-09 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller\Frontend\Api; + +use phpMyFAQ\Controller\AbstractController; +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Filter; +use phpMyFAQ\Translation; +use phpMyFAQ\Utils; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Routing\Attribute\Route; + +final class ContactController extends AbstractController +{ + /** + * @throws Exception + * @throws \JsonException + * @throws \Exception + * + */ + #[Route(path: 'api/contact', methods: ['POST'])] + public function create(Request $request): JsonResponse + { + $data = json_decode($request->getContent()); + + $author = trim((string) Filter::filterVar($data->name, FILTER_SANITIZE_SPECIAL_CHARS)); + $email = Filter::filterVar($data->email, FILTER_VALIDATE_EMAIL); + $question = trim((string) Filter::filterVar($data->question, FILTER_SANITIZE_SPECIAL_CHARS)); + + if (!$this->captchaCodeIsValid($request)) { + return $this->json(['error' => Translation::get(key: 'msgCaptcha')], Response::HTTP_BAD_REQUEST); + } + + $stopWords = $this->container->get(id: 'phpmyfaq.stop-words'); + + if ( + $author !== '' + && $author !== '0' + && $email !== '' + && $question !== '' + && $question !== '0' + && $stopWords->checkBannedWord($question) + ) { + $question = sprintf( + '%s: %s
    %s: %s

    %s', + Translation::get(key: 'msgNewContentName'), + $author, + Translation::get(key: 'msgNewContentMail'), + $email, + $question, + ); + + $mailer = $this->container->get(id: 'phpmyfaq.mail'); + try { + $mailer->setReplyTo($email, $author); + $mailer->addTo($this->configuration->getAdminEmail()); + $mailer->setReplyTo($this->configuration->getNoReplyEmail()); + $mailer->subject = Utils::resolveMarkers( + text: 'Feedback: %sitename%', + configuration: $this->configuration, + ); + $mailer->message = $question; + $mailer->send(); + unset($mailer); + + return $this->json(['success' => Translation::get(key: 'msgMailContact')], Response::HTTP_OK); + } catch (Exception|TransportExceptionInterface $e) { + return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST); + } + } + + return $this->json(['error' => Translation::get(key: 'err_sendMail')], Response::HTTP_BAD_REQUEST); + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/FaqController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/FaqController.php similarity index 99% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/FaqController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/FaqController.php index a6f72df50c..baf2a3dd9f 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/FaqController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/FaqController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use phpMyFAQ\Category; use phpMyFAQ\Category\Permission as CategoryPermission; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/QuestionController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/QuestionController.php similarity index 99% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/QuestionController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/QuestionController.php index b06cc9bf98..4e451c1233 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/QuestionController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/QuestionController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use phpMyFAQ\Category; use phpMyFAQ\Controller\AbstractController; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/RegistrationController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/RegistrationController.php similarity index 98% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/RegistrationController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/RegistrationController.php index a9acc9a0aa..b1e8e669f2 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/RegistrationController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/RegistrationController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use phpMyFAQ\Controller\AbstractController; use phpMyFAQ\Core\Exception; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/SetupController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/SetupController.php similarity index 98% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/SetupController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/SetupController.php index c992e83c24..13a7a6ea9f 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/SetupController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/SetupController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use Elastic\Elasticsearch\Exception\AuthenticationException; use phpMyFAQ\Configuration; @@ -31,7 +31,7 @@ use phpMyFAQ\Twig\TwigWrapper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Twig\Error\LoaderError; final class SetupController diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/TranslationController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/TranslationController.php similarity index 97% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/TranslationController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/TranslationController.php index acb94a758e..f8754513fa 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/TranslationController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/TranslationController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use phpMyFAQ\Controller\AbstractController; use phpMyFAQ\Core\Exception; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UnauthorizedUserController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/UnauthorizedUserController.php similarity index 98% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/UnauthorizedUserController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/UnauthorizedUserController.php index 0565efc2c3..e3e5f74f87 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UnauthorizedUserController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/UnauthorizedUserController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use phpMyFAQ\Configuration; use phpMyFAQ\Core\Exception; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/UserController.php similarity index 99% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/UserController.php index 0696891b40..0bc4d043a8 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/UserController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use phpMyFAQ\Controller\AbstractController; use phpMyFAQ\Core\Exception; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/VotingController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/VotingController.php similarity index 98% rename from phpmyfaq/src/phpMyFAQ/Controller/Frontend/VotingController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/VotingController.php index 09b2a4cf1d..a2139bcda4 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/VotingController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/VotingController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller\Frontend; +namespace phpMyFAQ\Controller\Frontend\Api; use Exception; use phpMyFAQ\Controller\AbstractController; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/WebAuthnController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/WebAuthnController.php new file mode 100644 index 0000000000..5fda4843a6 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/WebAuthnController.php @@ -0,0 +1,174 @@ + + * @copyright 2024-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2024-09-11 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller\Frontend\Api; + +use phpMyFAQ\Auth\AuthWebAuthn; +use phpMyFAQ\Auth\WebAuthn\WebAuthnUser; +use phpMyFAQ\Controller\AbstractController; +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Enums\AuthenticationSourceType; +use phpMyFAQ\Filter; +use phpMyFAQ\Translation; +use phpMyFAQ\User; +use phpMyFAQ\User\CurrentUser; +use Random\RandomException; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class WebAuthnController extends AbstractController +{ + private readonly AuthWebAuthn $authWebAuthn; + + private readonly User $user; + + public function __construct() + { + parent::__construct(); + + $this->authWebAuthn = new AuthWebAuthn($this->configuration); + $this->user = new User($this->configuration); + } + + /** + * @throws RandomException|\JsonException + * @throws \Exception + */ + #[Route(path: 'api/webauthn/prepare', name: 'api.private.webauthn.prepare', methods: ['POST'])] + public function prepare(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR); + $username = Filter::filterVar($data->username, FILTER_SANITIZE_SPECIAL_CHARS); + + if (!$this->user->getUserByLogin($username, raiseError: false)) { + try { + $this->user->createUser($username); + $this->user->setStatus(status: 'active'); + $this->user->setAuthSource(AuthenticationSourceType::AUTH_WEB_AUTHN->value); + $this->user->setUserData([ + 'display_name' => $username, + 'email' => $username, + ]); + } catch (\Exception $e) { + return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST); + } + } + + $webAuthnUser = new WebAuthnUser(); + $webAuthnUser->setName($username)->setId((string) $this->user->getUserId())->setWebAuthnKeys(webAuthnKeys: ''); + + $this->authWebAuthn->storeUserInSession($webAuthnUser); + + return $this->json([ + 'challenge' => $this->authWebAuthn->prepareChallengeForRegistration( + $username, + (string) $this->user->getUserId(), + ), + ], Response::HTTP_OK); + } + + /** + * @throws Exception + * @throws \JsonException + */ + #[Route(path: 'api/webauthn/register', name: 'api.private.webauthn.register', methods: ['POST'])] + public function register(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR); + $register = Filter::filterVar($data->register, FILTER_SANITIZE_SPECIAL_CHARS); + + $webAuthnUser = $this->authWebAuthn->getUserFromSession(); + $webAuthnUser->setWebAuthnKeys($this->authWebAuthn->register($register, $webAuthnUser->getWebAuthnKeys())); + + try { + $this->user->getUserByLogin($webAuthnUser->getName()); + } catch (Exception) { + return $this->json(['error' => Translation::get(key: 'ad_auth_fail')], Response::HTTP_BAD_REQUEST); + } + + if ($this->user->setWebAuthnKeys($webAuthnUser->getWebAuthnKeys())) { + return $this->json([ + 'success' => 'ok', + 'message' => Translation::get(key: 'msgPasskeyRegistrationSuccess'), + ], Response::HTTP_OK); + } + + return $this->json(['error' => 'Cannot set WebAuthn keys'], Response::HTTP_BAD_REQUEST); + } + + /** + * @throws \JsonException + * @throws RandomException + */ + #[Route(path: 'api/webauthn/prepare-login', name: 'api.private.webauthn.prepare-login', methods: ['POST'])] + public function prepareLogin(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR); + $login = Filter::filterVar($data->username, FILTER_SANITIZE_SPECIAL_CHARS); + + try { + $this->user->getUserByLogin($login); + } catch (Exception) { + return $this->json(['error' => Translation::get(key: 'ad_auth_fail')], Response::HTTP_BAD_REQUEST); + } + + $webAuthnKeys = $this->user->getWebAuthnKeys(); + + return $this->json($this->authWebAuthn->prepareForLogin($webAuthnKeys), Response::HTTP_OK); + } + + /** + * @throws Exception + * @throws \JsonException + * @throws \Exception + */ + #[Route(path: 'api/webauthn/login', name: 'api.private.webauthn.login', methods: ['POST'])] + public function login(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR); + $login = Filter::filterVar($data->username, FILTER_SANITIZE_SPECIAL_CHARS); + $loginData = $data->login; + + $this->user->getUserByLogin($login); + + $webAuthnKeys = $this->user->getWebAuthnKeys(); + + if ($this->authWebAuthn->authenticate($loginData, $webAuthnKeys)) { + $currentUser = new CurrentUser($this->configuration); + $currentUser->getUserByLogin($login); + + if ($currentUser->isBlocked()) { + return $this->json(['error' => Translation::get(key: 'ad_auth_fail')], Response::HTTP_UNAUTHORIZED); + } + + $currentUser->setLoggedIn(loggedIn: true); + $currentUser->setSuccess(success: true); + $currentUser->updateSessionId(updateLastLogin: true); + $currentUser->saveToSession(); + return $this->json([ + 'success' => 'ok', + 'redirect' => $this->configuration->getDefaultUrl(), + ], Response::HTTP_OK); + } + + return $this->json(['error' => Translation::get(key: 'ad_auth_fail')], Response::HTTP_UNAUTHORIZED); + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/ContactController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/ContactController.php index 1456b4d815..9c6b26fd77 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/ContactController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/ContactController.php @@ -1,7 +1,7 @@ - * @copyright 2024-2026 phpMyFAQ Team + * @copyright 2002-2025 phpMyFAQ Team * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 * @link https://www.phpmyfaq.de - * @since 2024-03-09 + * @since 2002-09-16 */ declare(strict_types=1); namespace phpMyFAQ\Controller\Frontend; -use phpMyFAQ\Controller\AbstractController; -use phpMyFAQ\Core\Exception; -use phpMyFAQ\Filter; +use Exception; +use phpMyFAQ\Controller\Route; use phpMyFAQ\Translation; -use phpMyFAQ\Utils; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Mailer\Exception\TransportExceptionInterface; -use Symfony\Component\Routing\Attribute\Route; -final class ContactController extends AbstractController +final class ContactController extends AbstractFrontController { /** + * Handles both GET and POST requests for the contact form * @throws Exception - * @throws \JsonException - * @throws \Exception - * */ - #[Route(path: 'api/contact', methods: ['POST'])] - public function create(Request $request): JsonResponse + #[Route(path: '/contact.html', name: 'public.contact')] + public function index(Request $request): Response { - $data = json_decode($request->getContent()); + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('contact', 0); - $author = trim((string) Filter::filterVar($data->name, FILTER_SANITIZE_SPECIAL_CHARS)); - $email = Filter::filterVar($data->email, FILTER_VALIDATE_EMAIL); - $question = trim((string) Filter::filterVar($data->question, FILTER_SANITIZE_SPECIAL_CHARS)); + $captcha = $this->container->get('phpmyfaq.captcha'); + $captchaHelper = $this->container->get('phpmyfaq.captcha.helper.captcha_helper'); - if (!$this->captchaCodeIsValid($request)) { - return $this->json(['error' => Translation::get(key: 'msgCaptcha')], Response::HTTP_BAD_REQUEST); + if ($this->configuration->get('layout.contactInformationHTML')) { + $contactText = html_entity_decode((string) $this->configuration->get('main.contactInformation')); + } else { + $contactText = nl2br((string) $this->configuration->get('main.contactInformation')); } - $stopWords = $this->container->get(id: 'phpmyfaq.stop-words'); - - if ( - $author !== '' - && $author !== '0' - && $email !== '' - && $question !== '' - && $question !== '0' - && $stopWords->checkBannedWord($question) - ) { - $question = sprintf( - '%s: %s
    %s: %s

    %s', - Translation::get(key: 'msgNewContentName'), - $author, - Translation::get(key: 'msgNewContentMail'), - $email, - $question, - ); - - $mailer = $this->container->get(id: 'phpmyfaq.mail'); - try { - $mailer->setReplyTo($email, $author); - $mailer->addTo($this->configuration->getAdminEmail()); - $mailer->setReplyTo($this->configuration->getNoReplyEmail()); - $mailer->subject = Utils::resolveMarkers( - text: 'Feedback: %sitename%', - configuration: $this->configuration, - ); - $mailer->message = $question; - $mailer->send(); - unset($mailer); - - return $this->json(['success' => Translation::get(key: 'msgMailContact')], Response::HTTP_OK); - } catch (Exception|TransportExceptionInterface $e) { - return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST); - } - } - - return $this->json(['error' => Translation::get(key: 'err_sendMail')], Response::HTTP_BAD_REQUEST); + return $this->render('contact.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'msgContact'), $this->configuration->getTitle()), + 'msgContactOwnText' => $contactText, + 'privacyURL' => $this->configuration->get('main.privacyURL'), + 'lang' => $this->configuration->getLanguage()->getLanguage(), + 'defaultContentMail' => $this->currentUser->getUserId() > 0 ? $this->currentUser->getUserData('email') : '', + 'defaultContentName' => $this->currentUser->getUserId() > 0 + ? $this->currentUser->getUserData('display_name') + : '', + 'version' => $this->configuration->getVersion(), + 'captchaFieldset' => $captchaHelper->renderCaptcha( + $captcha, + 'contact', + Translation::get(key: 'msgCaptcha'), + $this->currentUser->isLoggedIn(), + ), + ]); } } diff --git a/phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PageNotFoundController.php similarity index 97% rename from phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php rename to phpmyfaq/src/phpMyFAQ/Controller/Frontend/PageNotFoundController.php index 6870b76147..d72ee3d900 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/PageNotFoundController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PageNotFoundController.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Controller; +namespace phpMyFAQ\Controller\Frontend; use Exception; use phpMyFAQ\Enums\SessionActionType; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/WebAuthnController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/WebAuthnController.php index 8d2ede9b27..9d1b1c21ff 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/WebAuthnController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/WebAuthnController.php @@ -19,156 +19,28 @@ namespace phpMyFAQ\Controller\Frontend; -use phpMyFAQ\Auth\AuthWebAuthn; -use phpMyFAQ\Auth\WebAuthn\WebAuthnUser; -use phpMyFAQ\Controller\AbstractController; use phpMyFAQ\Core\Exception; -use phpMyFAQ\Enums\AuthenticationSourceType; -use phpMyFAQ\Filter; use phpMyFAQ\Translation; -use phpMyFAQ\User; -use phpMyFAQ\User\CurrentUser; -use Random\RandomException; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; +use Twig\Error\LoaderError; -final class WebAuthnController extends AbstractController +final class WebAuthnController extends AbstractFrontController { - private readonly AuthWebAuthn $authWebAuthn; - - private readonly User $user; - - public function __construct() - { - parent::__construct(); - - $this->authWebAuthn = new AuthWebAuthn($this->configuration); - $this->user = new User($this->configuration); - } - - /** - * @throws RandomException|\JsonException - * @throws \Exception - */ - #[Route(path: 'api/webauthn/prepare', name: 'api.private.webauthn.prepare', methods: ['POST'])] - public function prepare(Request $request): JsonResponse - { - $data = json_decode($request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR); - $username = Filter::filterVar($data->username, FILTER_SANITIZE_SPECIAL_CHARS); - - if (!$this->user->getUserByLogin($username, raiseError: false)) { - try { - $this->user->createUser($username); - $this->user->setStatus(status: 'active'); - $this->user->setAuthSource(AuthenticationSourceType::AUTH_WEB_AUTHN->value); - $this->user->setUserData([ - 'display_name' => $username, - 'email' => $username, - ]); - } catch (\Exception $e) { - return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST); - } - } - - $webAuthnUser = new WebAuthnUser(); - $webAuthnUser->setName($username)->setId((string) $this->user->getUserId())->setWebAuthnKeys(webAuthnKeys: ''); - - $this->authWebAuthn->storeUserInSession($webAuthnUser); - - return $this->json([ - 'challenge' => $this->authWebAuthn->prepareChallengeForRegistration( - $username, - (string) $this->user->getUserId(), - ), - ], Response::HTTP_OK); - } - - /** - * @throws Exception - * @throws \JsonException - */ - #[Route(path: 'api/webauthn/register', name: 'api.private.webauthn.register', methods: ['POST'])] - public function register(Request $request): JsonResponse - { - $data = json_decode($request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR); - $register = Filter::filterVar($data->register, FILTER_SANITIZE_SPECIAL_CHARS); - - $webAuthnUser = $this->authWebAuthn->getUserFromSession(); - $webAuthnUser->setWebAuthnKeys($this->authWebAuthn->register($register, $webAuthnUser->getWebAuthnKeys())); - - try { - $this->user->getUserByLogin($webAuthnUser->getName()); - } catch (Exception) { - return $this->json(['error' => Translation::get(key: 'ad_auth_fail')], Response::HTTP_BAD_REQUEST); - } - - if ($this->user->setWebAuthnKeys($webAuthnUser->getWebAuthnKeys())) { - return $this->json([ - 'success' => 'ok', - 'message' => Translation::get(key: 'msgPasskeyRegistrationSuccess'), - ], Response::HTTP_OK); - } - - return $this->json(['error' => 'Cannot set WebAuthn keys'], Response::HTTP_BAD_REQUEST); - } - /** - * @throws \JsonException - * @throws RandomException + * @throws Exception|LoaderError */ - #[Route(path: 'api/webauthn/prepare-login', name: 'api.private.webauthn.prepare-login', methods: ['POST'])] - public function prepareLogin(Request $request): JsonResponse + #[Route(path: '/services/webauthn', name: 'public.webauthn.index')] + public function index(Request $request): Response { - $data = json_decode($request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR); - $login = Filter::filterVar($data->username, FILTER_SANITIZE_SPECIAL_CHARS); - - try { - $this->user->getUserByLogin($login); - } catch (Exception) { - return $this->json(['error' => Translation::get(key: 'ad_auth_fail')], Response::HTTP_BAD_REQUEST); - } - - $webAuthnKeys = $this->user->getWebAuthnKeys(); - - return $this->json($this->authWebAuthn->prepareForLogin($webAuthnKeys), Response::HTTP_OK); - } - - /** - * @throws Exception - * @throws \JsonException - * @throws \Exception - */ - #[Route(path: 'api/webauthn/login', name: 'api.private.webauthn.login', methods: ['POST'])] - public function login(Request $request): JsonResponse - { - $data = json_decode($request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR); - $login = Filter::filterVar($data->username, FILTER_SANITIZE_SPECIAL_CHARS); - $loginData = $data->login; - - $this->user->getUserByLogin($login); - - $webAuthnKeys = $this->user->getWebAuthnKeys(); - - if ($this->authWebAuthn->authenticate($loginData, $webAuthnKeys)) { - $currentUser = new CurrentUser($this->configuration); - $currentUser->getUserByLogin($login); - - if ($currentUser->isBlocked()) { - return $this->json(['error' => Translation::get(key: 'ad_auth_fail')], Response::HTTP_UNAUTHORIZED); - } - - $currentUser->setLoggedIn(loggedIn: true); - $currentUser->setSuccess(success: true); - $currentUser->updateSessionId(updateLastLogin: true); - $currentUser->saveToSession(); - return $this->json([ - 'success' => 'ok', - 'redirect' => $this->configuration->getDefaultUrl(), - ], Response::HTTP_OK); - } - - return $this->json(['error' => Translation::get(key: 'ad_auth_fail')], Response::HTTP_UNAUTHORIZED); + return $this->render(file: '/webauthn.twig', context: [ + ...$this->getHeader($request), + 'msgLoginUser' => Translation::get(key: 'msgLoginUser'), + 'title' => Translation::get(key: 'msgLoginUser'), + 'faqHome' => $this->configuration->getDefaultUrl(), + 'isUserRegistrationEnabled' => $this->configuration->get(item: 'security.enableRegistration'), + 'msgRegisterUser' => Translation::get(key: 'msgRegisterUser'), + ]); } } diff --git a/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php b/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php deleted file mode 100644 index ad9d46c761..0000000000 --- a/phpmyfaq/src/phpMyFAQ/Controller/WebAuthnController.php +++ /dev/null @@ -1,46 +0,0 @@ - - * @copyright 2024-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2024-09-11 - */ - -declare(strict_types=1); - -namespace phpMyFAQ\Controller; - -use phpMyFAQ\Core\Exception; -use phpMyFAQ\Translation; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Attribute\Route; -use Twig\Error\LoaderError; - -final class WebAuthnController extends AbstractFrontController -{ - /** - * @throws Exception|LoaderError - */ - #[Route(path: '/services/webauthn', name: 'public.webauthn.index')] - public function index(Request $request): Response - { - return $this->render(file: '/webauthn.twig', context: [ - ...$this->getHeader($request), - 'msgLoginUser' => Translation::get(key: 'msgLoginUser'), - 'title' => Translation::get(key: 'msgLoginUser'), - 'faqHome' => $this->configuration->getDefaultUrl(), - 'isUserRegistrationEnabled' => $this->configuration->get(item: 'security.enableRegistration'), - 'msgRegisterUser' => Translation::get(key: 'msgRegisterUser'), - ]); - } -} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index c5edb8e05b..60fa8c3f7a 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -17,12 +17,13 @@ declare(strict_types=1); -use phpMyFAQ\Controller\ContactController; +use phpMyFAQ\Controller\Frontend\Api\SetupController; +use phpMyFAQ\Controller\Frontend\ContactController; +use phpMyFAQ\Controller\Frontend\PageNotFoundController; +use phpMyFAQ\Controller\Frontend\WebAuthnController; use phpMyFAQ\Controller\LlmsController; -use phpMyFAQ\Controller\PageNotFoundController; use phpMyFAQ\Controller\RobotsController; use phpMyFAQ\Controller\SitemapController; -use phpMyFAQ\Controller\WebAuthnController; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -32,46 +33,45 @@ 'public.contact' => [ 'path' => '/contact.html', 'controller' => [ContactController::class, 'index'], - 'methods' => 'GET|POST' + 'methods' => 'GET|POST', ], 'public.404' => [ 'path' => '/404.html', 'controller' => [PageNotFoundController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'public.llms.txt' => [ 'path' => '/llms.txt', 'controller' => [LlmsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'public.robots.txt' => [ 'path' => '/robots.txt', 'controller' => [RobotsController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'public.sitemap.xml' => [ 'path' => '/sitemap.xml', 'controller' => [SitemapController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', ], 'public.webauthn.index' => [ 'path' => '/services/webauthn/', 'controller' => [WebAuthnController::class, 'index'], - 'methods' => 'GET' + 'methods' => 'GET', + ], + 'public.update.index' => [ + 'path' => '/update/', + 'controller' => [SetupController::class, 'update'], + 'methods' => 'POST', ], ]; foreach ($routesConfig as $name => $config) { - $routes->add( - $name, - new Route( - $config['path'], - [ - '_controller' => $config['controller'], - '_methods' => $config['methods'] - ] - ) - ); + $routes->add($name, new Route($config['path'], [ + '_controller' => $config['controller'], + '_methods' => $config['methods'], + ])); } return $routes; diff --git a/phpmyfaq/update/index.php b/phpmyfaq/update/index.php deleted file mode 100644 index 56ec201382..0000000000 --- a/phpmyfaq/update/index.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @copyright 2024-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2024-05-31 - */ - -use phpMyFAQ\Application; -use phpMyFAQ\Controller\Frontend\SetupController; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; - -require '../src/Bootstrap.php'; - -// -// Service Containers -// -$container = new ContainerBuilder(); -$loader = new PhpFileLoader($container, new FileLocator(__DIR__)); -try { - $loader->load('../src/services.php'); -} catch (Exception $e) { - echo $e->getMessage(); -} - -$routes = new RouteCollection(); -$routes->add('public.update.index', new Route('/', ['_controller' => [SetupController::class, 'update']])); - -$app = new Application($container); -try { - $app->run($routes); -} catch (Exception $exception) { - echo sprintf( - 'An error occurred: %s at line %d at %s', - $exception->getMessage(), - $exception->getLine(), - $exception->getFile() - ); -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 088028010e..5432890b29 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -35,7 +35,9 @@ define('PMF_CONTENT_DIR', dirname(__DIR__) . '/tests/content'); const PMF_LOG_DIR = __DIR__ . '/logs/phpmyfaq.log'; + const PMF_TEST_DIR = __DIR__; + const IS_VALID_PHPMYFAQ = true; $_SERVER['HTTP_HOST'] = 'localhost'; diff --git a/tests/phpMyFAQ/Administration/AdminLogTest.php b/tests/phpMyFAQ/Administration/AdminLogTest.php index 97e58a295f..6a31e5aaaa 100644 --- a/tests/phpMyFAQ/Administration/AdminLogTest.php +++ b/tests/phpMyFAQ/Administration/AdminLogTest.php @@ -8,8 +8,8 @@ use phpMyFAQ\Entity\AdminLog as AdminLogEntity; use phpMyFAQ\System; use phpMyFAQ\User; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class AdminLogTest extends TestCase @@ -47,7 +47,7 @@ protected function tearDown(): void public function testDelete(): void { - $_SERVER['REQUEST_TIME'] = $this->now - 31 * 86400; + $_SERVER['REQUEST_TIME'] = $this->now - (31 * 86400); $this->adminLog->log(new User($this->configuration), 'foo'); $this->adminLog->log(new User($this->configuration), 'bar'); $this->assertEquals(2, $this->adminLog->getNumberOfEntries()); @@ -78,24 +78,16 @@ public function testGetAll(): void $result = $this->adminLog->getAll(); $adminLogEntity = new AdminLogEntity(); - $one = $adminLogEntity->setId(1) - ->setTime($this->now) - ->setUserId(-1) - ->setText('foo') - ->setIp('127.0.0.1'); + $one = $adminLogEntity->setId(1)->setTime($this->now)->setUserId(-1)->setText('foo')->setIp('127.0.0.1'); $adminLogEntity = new AdminLogEntity(); - $two = $adminLogEntity->setId(2) - ->setTime($this->now) - ->setUserId(-1) - ->setText('bar') - ->setIp('127.0.0.1'); + $two = $adminLogEntity->setId(2)->setTime($this->now)->setUserId(-1)->setText('bar')->setIp('127.0.0.1'); $this->assertEquals( [ 2 => $two, - 1 => $one + 1 => $one, ], - $result + $result, ); } } diff --git a/tests/phpMyFAQ/Administration/ApiTest.php b/tests/phpMyFAQ/Administration/ApiTest.php index 6ae4fe5476..d52e0aa941 100644 --- a/tests/phpMyFAQ/Administration/ApiTest.php +++ b/tests/phpMyFAQ/Administration/ApiTest.php @@ -4,18 +4,18 @@ use phpMyFAQ\Configuration; use phpMyFAQ\System; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use ReflectionClass; use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpFoundation\Response; -use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class ApiTest extends TestCase @@ -56,7 +56,8 @@ public function testGetVersionsSuccess(): void $response->method('getStatusCode')->willReturn(Response::HTTP_OK); $response->method('toArray')->willReturn($versions); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->with('GET', 'https://api.phpmyfaq.de/versions') ->willReturn($response); @@ -67,7 +68,7 @@ public function testGetVersionsSuccess(): void 'installed' => '5.0.0', 'stable' => '5.0.0', 'development' => '5.1.0', - 'nightly' => '5.2.0' + 'nightly' => '5.2.0', ]; $this->assertEquals($expected, $this->api->getVersions()); @@ -84,7 +85,8 @@ public function testIsVerifiedSuccess(): void $response->method('getContent')->willReturn('{"hash1": "abc", "hash2": "def"}'); $response->method('getStatusCode')->willReturn(Response::HTTP_OK); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->willReturn($response); @@ -100,7 +102,8 @@ public function testIsVerifiedFailure(): void $response = $this->createStub(ResponseInterface::class); $response->method('getStatusCode')->willReturn(Response::HTTP_INTERNAL_SERVER_ERROR); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->willReturn($response); @@ -114,7 +117,8 @@ public function testIsVerifiedFailure(): void */ public function testIsVerifiedTransportException(): void { - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->willThrowException(new TransportException()); @@ -128,23 +132,22 @@ public function testIsVerifiedTransportException(): void public function testGetVerificationIssues(): void { $this->configuration = $this->createStub(Configuration::class); - $mockSystem = $this->getMockBuilder(System::class) - ->onlyMethods(['createHashes']) - ->getMock(); + $mockSystem = $this->getMockBuilder(System::class)->onlyMethods(['createHashes'])->getMock(); - $mockSystem->expects($this->once()) + $mockSystem + ->expects($this->once()) ->method('createHashes') ->willReturn(json_encode([ 'hash1' => 'abc123', 'hash2' => 'def456', - 'hash3' => 'ghi789' + 'hash3' => 'ghi789', ])); $api = new Api($this->configuration, $mockSystem); $api->setRemoteHashes(json_encode([ 'hash1' => 'abc123', - 'hash3' => 'ghi789' + 'hash3' => 'ghi789', ])); $result = $api->getVerificationIssues(); @@ -163,7 +166,8 @@ public function testGetVersionsWithHttpError(): void $response = $this->createStub(ResponseInterface::class); $response->method('getStatusCode')->willReturn(Response::HTTP_NOT_FOUND); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->with('GET', 'https://api.phpmyfaq.de/versions') ->willReturn($response); @@ -174,7 +178,7 @@ public function testGetVersionsWithHttpError(): void 'installed' => '4.0.0', 'stable' => 'n/a', 'development' => 'n/a', - 'nightly' => 'n/a' + 'nightly' => 'n/a', ]; $this->assertEquals($expected, $this->api->getVersions()); @@ -188,7 +192,8 @@ public function testGetVersionsWithTransportException(): void { $transportException = new TransportException('Network error'); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->willThrowException($transportException); @@ -207,7 +212,8 @@ public function testIsVerifiedWithInvalidJson(): void $response = $this->createStub(ResponseInterface::class); $response->method('getContent')->willReturn('invalid json content'); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->willReturn($response); @@ -224,7 +230,8 @@ public function testIsVerifiedWithEmptyResponse(): void $response = $this->createStub(ResponseInterface::class); $response->method('getContent')->willReturn(''); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->willReturn($response); @@ -237,7 +244,8 @@ public function testIsVerifiedWithNonArrayJson(): void $response = $this->createStub(ResponseInterface::class); $response->method('getContent')->willReturn('"just a string"'); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->willReturn($response); @@ -249,11 +257,10 @@ public function testIsVerifiedWithNonArrayJson(): void public function testIsVerifiedWithServerException(): void { $response = $this->createStub(ResponseInterface::class); - $response->method('getContent')->willThrowException( - $this->createMock(ServerExceptionInterface::class) - ); + $response->method('getContent')->willThrowException($this->createMock(ServerExceptionInterface::class)); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->willReturn($response); @@ -326,9 +333,7 @@ public function testSetHttpClient(): void $response->method('getStatusCode')->willReturn(Response::HTTP_OK); $response->method('toArray')->willReturn(['stable' => '1.0.0', 'development' => '1.1.0', 'nightly' => '1.2.0']); - $newHttpClient->expects($this->once()) - ->method('request') - ->willReturn($response); + $newHttpClient->expects($this->once())->method('request')->willReturn($response); $this->configuration->method('getVersion')->willReturn('1.0.0'); @@ -385,7 +390,8 @@ public function testIsVerifiedSetsRemoteHashes(): void $response = $this->createStub(ResponseInterface::class); $response->method('getContent')->willReturn($expectedHashes); - $this->httpClient->expects($this->once()) + $this->httpClient + ->expects($this->once()) ->method('request') ->willReturn($response); diff --git a/tests/phpMyFAQ/Administration/Backup/BackupExecuteResultTest.php b/tests/phpMyFAQ/Administration/Backup/BackupExecuteResultTest.php index e592993a4e..60e9da03d0 100644 --- a/tests/phpMyFAQ/Administration/Backup/BackupExecuteResultTest.php +++ b/tests/phpMyFAQ/Administration/Backup/BackupExecuteResultTest.php @@ -19,7 +19,7 @@ public function testConstructorWithAllParameters(): void queriesOk: 10, queriesFailed: 2, lastErrorQuery: 'SELECT * FROM invalid_table', - lastErrorReason: 'Table does not exist' + lastErrorReason: 'Table does not exist', ); $this->assertEquals(10, $result->queriesOk); @@ -30,10 +30,7 @@ public function testConstructorWithAllParameters(): void public function testConstructorWithOnlyRequiredParameters(): void { - $result = new BackupExecuteResult( - queriesOk: 15, - queriesFailed: 0 - ); + $result = new BackupExecuteResult(queriesOk: 15, queriesFailed: 0); $this->assertEquals(15, $result->queriesOk); $this->assertEquals(0, $result->queriesFailed); @@ -43,10 +40,7 @@ public function testConstructorWithOnlyRequiredParameters(): void public function testConstructorWithZeroQueries(): void { - $result = new BackupExecuteResult( - queriesOk: 0, - queriesFailed: 0 - ); + $result = new BackupExecuteResult(queriesOk: 0, queriesFailed: 0); $this->assertEquals(0, $result->queriesOk); $this->assertEquals(0, $result->queriesFailed); @@ -58,7 +52,7 @@ public function testConstructorWithOnlyFailedQueries(): void queriesOk: 0, queriesFailed: 5, lastErrorQuery: 'INSERT INTO locked_table VALUES (1)', - lastErrorReason: 'Table is locked' + lastErrorReason: 'Table is locked', ); $this->assertEquals(0, $result->queriesOk); @@ -73,7 +67,7 @@ public function testConstructorWithOnlySuccessfulQueries(): void queriesOk: 100, queriesFailed: 0, lastErrorQuery: null, - lastErrorReason: null + lastErrorReason: null, ); $this->assertEquals(100, $result->queriesOk); @@ -84,10 +78,7 @@ public function testConstructorWithOnlySuccessfulQueries(): void public function testReadonlyProperties(): void { - $result = new BackupExecuteResult( - queriesOk: 5, - queriesFailed: 1 - ); + $result = new BackupExecuteResult(queriesOk: 5, queriesFailed: 1); // Verify properties are accessible $this->assertIsInt($result->queriesOk); @@ -96,10 +87,7 @@ public function testReadonlyProperties(): void public function testConstructorWithLargeNumbers(): void { - $result = new BackupExecuteResult( - queriesOk: 99999, - queriesFailed: 1 - ); + $result = new BackupExecuteResult(queriesOk: 99999, queriesFailed: 1); $this->assertEquals(99999, $result->queriesOk); $this->assertEquals(1, $result->queriesFailed); @@ -107,12 +95,7 @@ public function testConstructorWithLargeNumbers(): void public function testConstructorWithErrorDetailsButNoErrors(): void { - $result = new BackupExecuteResult( - queriesOk: 10, - queriesFailed: 0, - lastErrorQuery: null, - lastErrorReason: null - ); + $result = new BackupExecuteResult(queriesOk: 10, queriesFailed: 0, lastErrorQuery: null, lastErrorReason: null); $this->assertEquals(10, $result->queriesOk); $this->assertEquals(0, $result->queriesFailed); @@ -128,7 +111,7 @@ public function testConstructorWithComplexErrorMessage(): void queriesOk: 5, queriesFailed: 1, lastErrorQuery: 'SELECT * FROM missing_table', - lastErrorReason: $complexError + lastErrorReason: $complexError, ); $this->assertEquals($complexError, $result->lastErrorReason); diff --git a/tests/phpMyFAQ/Administration/Backup/BackupExportResultTest.php b/tests/phpMyFAQ/Administration/Backup/BackupExportResultTest.php index 172d16575f..0446e8fd08 100644 --- a/tests/phpMyFAQ/Administration/Backup/BackupExportResultTest.php +++ b/tests/phpMyFAQ/Administration/Backup/BackupExportResultTest.php @@ -50,9 +50,10 @@ public function testConstructorWithLargeContent(): void public function testConstructorWithMultilineContent(): void { $fileName = 'phpmyfaq-data.2025-12-22-10-30-45.sql'; - $content = "-- pmf4.0: faqconfig\n" . - "-- DO NOT REMOVE THE FIRST LINE!\n" . - "INSERT INTO faqconfig VALUES (1, 'test');"; + $content = + "-- pmf4.0: faqconfig\n" + . "-- DO NOT REMOVE THE FIRST LINE!\n" + . "INSERT INTO faqconfig VALUES (1, 'test');"; $result = new BackupExportResult($fileName, $content); @@ -73,15 +74,9 @@ public function testConstructorWithSpecialCharactersInContent(): void public function testConstructorWithDifferentBackupTypes(): void { - $dataBackup = new BackupExportResult( - 'phpmyfaq-data.2025-12-22-10-30-45.sql', - 'data content' - ); - - $logsBackup = new BackupExportResult( - 'phpmyfaq-logs.2025-12-22-10-30-46.sql', - 'logs content' - ); + $dataBackup = new BackupExportResult('phpmyfaq-data.2025-12-22-10-30-45.sql', 'data content'); + + $logsBackup = new BackupExportResult('phpmyfaq-logs.2025-12-22-10-30-46.sql', 'logs content'); $this->assertStringContainsString('data', $dataBackup->fileName); $this->assertStringContainsString('logs', $logsBackup->fileName); diff --git a/tests/phpMyFAQ/Administration/Backup/BackupParseResultTest.php b/tests/phpMyFAQ/Administration/Backup/BackupParseResultTest.php index 0fd56fa574..dc0ab5fb08 100644 --- a/tests/phpMyFAQ/Administration/Backup/BackupParseResultTest.php +++ b/tests/phpMyFAQ/Administration/Backup/BackupParseResultTest.php @@ -21,7 +21,7 @@ public function testConstructorWithMatchingVersions(): void versionFound: '-- pmf4.0', versionExpected: '-- pmf4.0', queries: $queries, - tablePrefix: 'pmf_' + tablePrefix: 'pmf_', ); $this->assertTrue($result->versionMatches); @@ -39,7 +39,7 @@ public function testConstructorWithNonMatchingVersions(): void versionFound: '-- pmf3.2', versionExpected: '-- pmf4.0', queries: $queries, - tablePrefix: '' + tablePrefix: '', ); $this->assertFalse($result->versionMatches); @@ -56,7 +56,7 @@ public function testConstructorWithEmptyQueries(): void versionFound: '-- pmf4.0', versionExpected: '-- pmf4.0', queries: [], - tablePrefix: 'pmf_' + tablePrefix: 'pmf_', ); $this->assertTrue($result->versionMatches); @@ -72,7 +72,7 @@ public function testConstructorWithEmptyTablePrefix(): void versionFound: '-- pmf4.0', versionExpected: '-- pmf4.0', queries: $queries, - tablePrefix: '' + tablePrefix: '', ); $this->assertEquals('', $result->tablePrefix); @@ -85,7 +85,7 @@ public function testConstructorWithMultipleQueries(): void 'DELETE FROM faqconfig', 'DELETE FROM faqdata', 'INSERT INTO faqconfig VALUES (1, "test")', - 'INSERT INTO faqdata VALUES (2, "data")' + 'INSERT INTO faqdata VALUES (2, "data")', ]; $result = new BackupParseResult( @@ -93,7 +93,7 @@ public function testConstructorWithMultipleQueries(): void versionFound: '-- pmf4.0', versionExpected: '-- pmf4.0', queries: $queries, - tablePrefix: 'pmf_' + tablePrefix: 'pmf_', ); $this->assertCount(4, $result->queries); @@ -108,7 +108,7 @@ public function testConstructorWithComplexTablePrefix(): void versionFound: '-- pmf4.0', versionExpected: '-- pmf4.0', queries: $queries, - tablePrefix: 'my_custom_prefix_' + tablePrefix: 'my_custom_prefix_', ); $this->assertEquals('my_custom_prefix_', $result->tablePrefix); @@ -122,7 +122,7 @@ public function testReadonlyProperties(): void versionFound: '-- pmf4.0', versionExpected: '-- pmf4.0', queries: $queries, - tablePrefix: 'test_' + tablePrefix: 'test_', ); // Verify all properties are accessible @@ -140,7 +140,7 @@ public function testConstructorWithDifferentVersionFormats(): void versionFound: '-- pmf4.1', versionExpected: '-- pmf4.0', queries: [], - tablePrefix: '' + tablePrefix: '', ); $this->assertFalse($result->versionMatches); @@ -152,7 +152,7 @@ public function testConstructorWithDeleteQueries(): void $queries = [ 'DELETE FROM faqconfig', 'DELETE FROM faqdata', - 'DELETE FROM faqcategories' + 'DELETE FROM faqcategories', ]; $result = new BackupParseResult( @@ -160,7 +160,7 @@ public function testConstructorWithDeleteQueries(): void versionFound: '-- pmf4.0', versionExpected: '-- pmf4.0', queries: $queries, - tablePrefix: '' + tablePrefix: '', ); foreach ($result->queries as $query) { @@ -172,7 +172,7 @@ public function testConstructorWithInsertQueries(): void { $queries = [ 'INSERT INTO faqconfig (id, meta_key, meta_value) VALUES (1, "key", "value")', - 'INSERT INTO faqdata (id, lang, thema) VALUES (1, "en", "Test")' + 'INSERT INTO faqdata (id, lang, thema) VALUES (1, "en", "Test")', ]; $result = new BackupParseResult( @@ -180,7 +180,7 @@ public function testConstructorWithInsertQueries(): void versionFound: '-- pmf4.0', versionExpected: '-- pmf4.0', queries: $queries, - tablePrefix: 'pmf_' + tablePrefix: 'pmf_', ); foreach ($result->queries as $query) { @@ -193,7 +193,7 @@ public function testConstructorWithMixedQueries(): void $queries = [ 'DELETE FROM faqconfig', 'INSERT INTO faqconfig VALUES (1)', - 'UPDATE faqconfig SET meta_value = "test" WHERE id = 1' + 'UPDATE faqconfig SET meta_value = "test" WHERE id = 1', ]; $result = new BackupParseResult( @@ -201,7 +201,7 @@ public function testConstructorWithMixedQueries(): void versionFound: '-- pmf4.0', versionExpected: '-- pmf4.0', queries: $queries, - tablePrefix: '' + tablePrefix: '', ); $this->assertCount(3, $result->queries); diff --git a/tests/phpMyFAQ/Administration/Backup/BackupRepositoryTest.php b/tests/phpMyFAQ/Administration/Backup/BackupRepositoryTest.php index 751f3dc8a8..3ea426d7b3 100644 --- a/tests/phpMyFAQ/Administration/Backup/BackupRepositoryTest.php +++ b/tests/phpMyFAQ/Administration/Backup/BackupRepositoryTest.php @@ -64,15 +64,15 @@ public function testGetAll(): void 'filename' => 'backup1.sql', 'authkey' => 'key1', 'authcode' => 'code1', - 'created' => '2025-12-22 10:00:00' + 'created' => '2025-12-22 10:00:00', ], (object) [ 'id' => 2, 'filename' => 'backup2.sql', 'authkey' => 'key2', 'authcode' => 'code2', - 'created' => '2025-12-22 11:00:00' - ] + 'created' => '2025-12-22 11:00:00', + ], ]; $this->mockDb->method('query')->willReturn(true); @@ -114,7 +114,7 @@ public function testFindByFilename(): void 'filename' => 'test-backup.sql', 'authkey' => 'test-key', 'authcode' => 'test-code', - 'created' => '2025-12-22 10:00:00' + 'created' => '2025-12-22 10:00:00', ]; $this->mockDb->method('escape')->willReturnArgument(0); @@ -149,7 +149,8 @@ public function testFindByFilenameWithEmptyString(): void public function testFindByFilenameEscapesInput(): void { - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('escape') ->with("test'; DROP TABLE faqbackup; --") ->willReturn("test\\'; DROP TABLE faqbackup; --"); @@ -166,60 +167,35 @@ public function testAdd(): void $this->mockDb->method('escape')->willReturnArgument(0); $this->mockDb->method('query')->willReturn(true); - $result = $this->repository->add( - 'backup.sql', - 'authkey123', - 'authcode456', - '2025-12-22 10:00:00' - ); + $result = $this->repository->add('backup.sql', 'authkey123', 'authcode456', '2025-12-22 10:00:00'); $this->assertTrue($result); } public function testAddWithEmptyFilename(): void { - $result = $this->repository->add( - '', - 'authkey123', - 'authcode456', - '2025-12-22 10:00:00' - ); + $result = $this->repository->add('', 'authkey123', 'authcode456', '2025-12-22 10:00:00'); $this->assertFalse($result); } public function testAddWithEmptyAuthKey(): void { - $result = $this->repository->add( - 'backup.sql', - '', - 'authcode456', - '2025-12-22 10:00:00' - ); + $result = $this->repository->add('backup.sql', '', 'authcode456', '2025-12-22 10:00:00'); $this->assertFalse($result); } public function testAddWithEmptyAuthCode(): void { - $result = $this->repository->add( - 'backup.sql', - 'authkey123', - '', - '2025-12-22 10:00:00' - ); + $result = $this->repository->add('backup.sql', 'authkey123', '', '2025-12-22 10:00:00'); $this->assertFalse($result); } public function testAddWithEmptyCreated(): void { - $result = $this->repository->add( - 'backup.sql', - 'authkey123', - 'authcode456', - '' - ); + $result = $this->repository->add('backup.sql', 'authkey123', 'authcode456', ''); $this->assertFalse($result); } @@ -227,17 +203,13 @@ public function testAddWithEmptyCreated(): void public function testAddEscapesAllInputs(): void { $this->mockDb->method('nextId')->willReturn(1); - $this->mockDb->expects($this->exactly(4)) + $this->mockDb + ->expects($this->exactly(4)) ->method('escape') ->willReturnArgument(0); $this->mockDb->method('query')->willReturn(true); - $this->repository->add( - 'backup.sql', - 'authkey123', - 'authcode456', - '2025-12-22 10:00:00' - ); + $this->repository->add('backup.sql', 'authkey123', 'authcode456', '2025-12-22 10:00:00'); } public function testDeleteById(): void @@ -291,7 +263,8 @@ public function testDeleteByFilenameWithEmptyString(): void public function testDeleteByFilenameEscapesInput(): void { - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('escape') ->with("backup'; DROP TABLE faqbackup; --") ->willReturn("backup\\'; DROP TABLE faqbackup; --"); @@ -334,7 +307,7 @@ public function testGetAllOrdersByIdDesc(): void $mockBackups = [ (object) ['id' => 3, 'filename' => 'backup3.sql'], (object) ['id' => 2, 'filename' => 'backup2.sql'], - (object) ['id' => 1, 'filename' => 'backup1.sql'] + (object) ['id' => 1, 'filename' => 'backup1.sql'], ]; $this->mockDb->method('query')->willReturn(true); @@ -356,21 +329,18 @@ public function testAddWithLongAuthKeyAndCode(): void $this->mockDb->method('escape')->willReturnArgument(0); $this->mockDb->method('query')->willReturn(true); - $result = $this->repository->add( - 'backup.sql', - $longAuthKey, - $longAuthCode, - '2025-12-22 10:00:00' - ); + $result = $this->repository->add('backup.sql', $longAuthKey, $longAuthCode, '2025-12-22 10:00:00'); $this->assertTrue($result); } public function testFindByFilenameWithSpecialCharacters(): void { - $this->mockDb->method('escape')->willReturnCallback(function ($input) { - return str_replace("'", "\\'", $input); - }); + $this->mockDb + ->method('escape') + ->willReturnCallback(function ($input) { + return str_replace("'", "\\'", $input); + }); $this->mockDb->method('query')->willReturn(true); $this->mockDb->method('numRows')->willReturn(0); diff --git a/tests/phpMyFAQ/Administration/BackupTest.php b/tests/phpMyFAQ/Administration/BackupTest.php index d3efaf546b..1b577a56be 100644 --- a/tests/phpMyFAQ/Administration/BackupTest.php +++ b/tests/phpMyFAQ/Administration/BackupTest.php @@ -4,16 +4,16 @@ use phpMyFAQ\Administration\Backup\BackupExportResult; use phpMyFAQ\Configuration; -use phpMyFAQ\Database\DatabaseHelper; use phpMyFAQ\Database\DatabaseDriver; +use phpMyFAQ\Database\DatabaseHelper; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Enums\BackupType; use phpMyFAQ\System; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use SodiumException; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class BackupTest @@ -89,7 +89,9 @@ public function testVerifyBackup(): void $tableNames = 'faqconfig faqinstances'; // Mock the generateBackupQueries method - $this->mockDatabaseHelper->method('buildInsertQueries')->willReturn(['INSERT INTO faqconfig VALUES (1, "test");']); + $this->mockDatabaseHelper + ->method('buildInsertQueries') + ->willReturn(['INSERT INTO faqconfig VALUES (1, "test");']); $backupQueries = $this->backup->generateBackupQueries($tableNames); // Mock createBackup method @@ -128,7 +130,7 @@ public function testGenerateBackupQueries(): void /** * @throws SodiumException - */public function testCreateBackupWithTablePrefix(): void + */ public function testCreateBackupWithTablePrefix(): void { $this->mockDb->method('nextId')->willReturn(1); $this->mockDb->method('escape')->willReturnArgument(0); @@ -157,10 +159,11 @@ public function testCreateBackupWithoutTablePrefix(): void /** * @throws SodiumException - */public function testCreateBackupEscapesInput(): void + */ public function testCreateBackupEscapesInput(): void { $this->mockDb->method('nextId')->willReturn(1); - $this->mockDb->method('escape') + $this->mockDb + ->method('escape') ->willReturnCallback(function ($input) { return str_replace("'", "\\'", $input); }); @@ -174,7 +177,7 @@ public function testCreateBackupWithoutTablePrefix(): void /** * @throws SodiumException - */public function testVerifyBackupWithNonExistentFile(): void + */ public function testVerifyBackupWithNonExistentFile(): void { $this->mockDb->method('escape')->willReturnArgument(0); $this->mockDb->method('query')->willReturn(true); @@ -187,7 +190,7 @@ public function testCreateBackupWithoutTablePrefix(): void /** * @throws SodiumException - */public function testVerifyBackupWithValidData(): void + */ public function testVerifyBackupWithValidData(): void { // Create mock database result $mockResult = new stdClass(); @@ -222,7 +225,8 @@ public function testGenerateBackupQueriesWithEmptyTableNames(): void public function testGenerateBackupQueriesWithMultipleTables(): void { - $this->mockDatabaseHelper->method('buildInsertQueries') + $this->mockDatabaseHelper + ->method('buildInsertQueries') ->willReturn(['INSERT INTO table1 VALUES (1);', 'INSERT INTO table2 VALUES (2);']); $result = $this->backup->generateBackupQueries('table1 table2'); @@ -233,7 +237,8 @@ public function testGenerateBackupQueriesWithMultipleTables(): void public function testGenerateBackupQueriesWithSingleTable(): void { - $this->mockDatabaseHelper->method('buildInsertQueries') + $this->mockDatabaseHelper + ->method('buildInsertQueries') ->willReturn(['INSERT INTO faqconfig VALUES (1, "test");']); $result = $this->backup->generateBackupQueries('faqconfig'); @@ -244,14 +249,14 @@ public function testGenerateBackupQueriesWithSingleTable(): void /** * @throws \Exception - */public function testGetBackupTableNamesForDataBackup(): void + */ public function testGetBackupTableNamesForDataBackup(): void { $mockTables = [ 'faqconfig', 'faqdata', 'faqadminlog', // Should be excluded for DATA backup 'faqsessions', // Should be excluded for DATA backup - 'faqcategories' + 'faqcategories', ]; $this->mockDb->method('getTableNames')->willReturn($mockTables); @@ -267,14 +272,14 @@ public function testGenerateBackupQueriesWithSingleTable(): void /** * @throws \Exception - */public function testGetBackupTableNamesForLogsBackup(): void + */ public function testGetBackupTableNamesForLogsBackup(): void { $mockTables = [ 'faqconfig', 'faqdata', 'faqadminlog', // Should be included for LOGS backup 'faqsessions', // Should be included for LOGS backup - 'faqcategories' + 'faqcategories', ]; $this->mockDb->method('getTableNames')->willReturn($mockTables); @@ -290,7 +295,7 @@ public function testGenerateBackupQueriesWithSingleTable(): void /** * @throws \Exception - */public function testGetBackupTableNamesWithEmptyTableList(): void + */ public function testGetBackupTableNamesWithEmptyTableList(): void { $this->mockDb->method('getTableNames')->willReturn([]); @@ -301,12 +306,12 @@ public function testGenerateBackupQueriesWithSingleTable(): void /** * @throws \Exception - */public function testGetBackupTableNamesWithWhitespaceInTableNames(): void + */ public function testGetBackupTableNamesWithWhitespaceInTableNames(): void { $mockTables = [ ' faqconfig ', // With whitespace 'faqdata', - ' faqcategories ' + ' faqcategories ', ]; $this->mockDb->method('getTableNames')->willReturn($mockTables); @@ -330,7 +335,7 @@ public function testBackupHeaderGeneration(): void 'DO NOT REMOVE THE FIRST LINE!', 'pmftableprefix:', 'DO NOT REMOVE THE LINES ABOVE!', - 'Otherwise this backup will be broken.' + 'Otherwise this backup will be broken.', ]; foreach ($expectedHeaderElements as $element) { @@ -340,7 +345,7 @@ public function testBackupHeaderGeneration(): void /** * @throws SodiumException - */public function testCreateBackupDateFormat(): void + */ public function testCreateBackupDateFormat(): void { $this->mockDb->method('nextId')->willReturn(1); $this->mockDb->method('escape')->willReturnArgument(0); @@ -349,10 +354,7 @@ public function testBackupHeaderGeneration(): void $result = $this->backup->createBackup('data', 'test content'); // Check date format: YYYY-MM-DD-HH-MM-SS - $this->assertMatchesRegularExpression( - '/phpmyfaq-data\.\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.sql/', - $result - ); + $this->assertMatchesRegularExpression('/phpmyfaq-data\.\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.sql/', $result); } public function testGenerateBackupQueriesIntegration(): void @@ -360,10 +362,11 @@ public function testGenerateBackupQueriesIntegration(): void // Test full integration with DatabaseHelper $mockQueries = [ 'INSERT INTO faqconfig (meta_key, meta_value) VALUES ("main.language", "en");', - 'INSERT INTO faqconfig (meta_key, meta_value) VALUES ("main.currentVersion", "4.0.0");' + 'INSERT INTO faqconfig (meta_key, meta_value) VALUES ("main.currentVersion", "4.0.0");', ]; - $this->mockDatabaseHelper->expects($this->exactly(2)) + $this->mockDatabaseHelper + ->expects($this->exactly(2)) ->method('buildInsertQueries') ->willReturn($mockQueries); @@ -392,7 +395,7 @@ public function testGetLastBackupInfoWithRecentBackup(): void $mockBackup = (object) [ 'id' => 1, 'filename' => 'backup.sql', - 'created' => $recentDate + 'created' => $recentDate, ]; $this->mockDb->method('query')->willReturn(true); @@ -411,7 +414,7 @@ public function testGetLastBackupInfoWithOldBackup(): void $mockBackup = (object) [ 'id' => 1, 'filename' => 'backup.sql', - 'created' => $oldDate + 'created' => $oldDate, ]; $this->mockDb->method('query')->willReturn(true); @@ -429,7 +432,7 @@ public function testGetLastBackupInfoWithInvalidDate(): void $mockBackup = (object) [ 'id' => 1, 'filename' => 'backup.sql', - 'created' => 'invalid-date' + 'created' => 'invalid-date', ]; $this->mockDb->method('query')->willReturn(true); @@ -445,12 +448,13 @@ public function testGetLastBackupInfoWithInvalidDate(): void public function testParseBackupFile(): void { $backupFile = PMF_TEST_DIR . '/test-backup.sql'; - $content = "-- pmf4.0: faqconfig faqdata\n" . - "-- DO NOT REMOVE THE FIRST LINE!\n" . - "-- pmftableprefix: pmf_\n" . - "-- DO NOT REMOVE THE LINES ABOVE!\n" . - "INSERT INTO faqconfig VALUES (1, 'test');\n" . - "INSERT INTO faqdata VALUES (2, 'data');"; + $content = + "-- pmf4.0: faqconfig faqdata\n" + . "-- DO NOT REMOVE THE FIRST LINE!\n" + . "-- pmftableprefix: pmf_\n" + . "-- DO NOT REMOVE THE LINES ABOVE!\n" + . "INSERT INTO faqconfig VALUES (1, 'test');\n" + . "INSERT INTO faqdata VALUES (2, 'data');"; file_put_contents($backupFile, $content); @@ -468,10 +472,11 @@ public function testParseBackupFile(): void public function testParseBackupFileWithVersionMismatch(): void { $backupFile = PMF_TEST_DIR . '/test-backup-old.sql'; - $content = "-- pmf3.2: faqconfig\n" . - "-- DO NOT REMOVE THE FIRST LINE!\n" . - "-- pmftableprefix: \n" . - "INSERT INTO faqconfig VALUES (1, 'test');"; + $content = + "-- pmf3.2: faqconfig\n" + . "-- DO NOT REMOVE THE FIRST LINE!\n" + . "-- pmftableprefix: \n" + . "INSERT INTO faqconfig VALUES (1, 'test');"; file_put_contents($backupFile, $content); @@ -571,7 +576,7 @@ public function testExecuteBackupQueriesWithDifferentTablePrefix(): void /** * @throws SodiumException - */public function testExportForDataBackup(): void + */ public function testExportForDataBackup(): void { $mockTables = ['faqconfig', 'faqdata']; $mockQueries = ['INSERT INTO faqconfig VALUES (1);']; @@ -593,7 +598,7 @@ public function testExecuteBackupQueriesWithDifferentTablePrefix(): void /** * @throws SodiumException - */public function testExportForLogsBackup(): void + */ public function testExportForLogsBackup(): void { $mockTables = ['faqadminlog', 'faqsessions']; $mockQueries = ['INSERT INTO faqadminlog VALUES (1);']; @@ -613,7 +618,7 @@ public function testExecuteBackupQueriesWithDifferentTablePrefix(): void /** * @throws SodiumException - */public function testExportForContentBackupThrowsException(): void + */ public function testExportForContentBackupThrowsException(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('To be implemented'); diff --git a/tests/phpMyFAQ/Administration/CategoryTest.php b/tests/phpMyFAQ/Administration/CategoryTest.php index 7833dc6ea4..804ef12cbd 100644 --- a/tests/phpMyFAQ/Administration/CategoryTest.php +++ b/tests/phpMyFAQ/Administration/CategoryTest.php @@ -2,11 +2,11 @@ namespace phpMyFAQ\Administration; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; use phpMyFAQ\Database\DatabaseDriver; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * Class CategoryTest @@ -27,9 +27,7 @@ protected function setUp(): void $this->databaseMock = $this->createMock(DatabaseDriver::class); $this->configurationMock = $this->createStub(Configuration::class); - $this->configurationMock - ->method('getDb') - ->willReturn($this->databaseMock); + $this->configurationMock->method('getDb')->willReturn($this->databaseMock); $this->category = new Category($this->configurationMock); } @@ -84,7 +82,6 @@ public function testGetOwnerWithoutCategories(): void public function testLoadCategoriesWithoutLanguage(): void { - $this->databaseMock ->expects($this->once()) ->method('query') @@ -136,7 +133,7 @@ public function testLoadCategoriesWithSampleData(): void 'group_id' => 1, 'active' => 1, 'show_home' => 1, - 'image' => 'test.png' + 'image' => 'test.png', ]; $this->databaseMock @@ -188,7 +185,7 @@ public function testBuildAdminCategoryTreeWithData(): void $categories = [ ['id' => 1, 'parent_id' => 0, 'name' => 'Root Category'], ['id' => 2, 'parent_id' => 1, 'name' => 'Child Category'], - ['id' => 3, 'parent_id' => 0, 'name' => 'Another Root'] + ['id' => 3, 'parent_id' => 0, 'name' => 'Another Root'], ]; $result = $this->category->buildAdminCategoryTree($categories); @@ -204,7 +201,7 @@ public function testBuildAdminCategoryTreeWithSpecificParent(): void $categories = [ ['id' => 1, 'parent_id' => 0, 'name' => 'Root Category'], ['id' => 2, 'parent_id' => 1, 'name' => 'Child Category'], - ['id' => 3, 'parent_id' => 1, 'name' => 'Another Child'] + ['id' => 3, 'parent_id' => 1, 'name' => 'Another Child'], ]; $result = $this->category->buildAdminCategoryTree($categories, 1); @@ -240,7 +237,7 @@ public function testGetOwnerAfterLoadingCategories(): void 'group_id' => 1, 'active' => 1, 'show_home' => 1, - 'image' => 'test.png' + 'image' => 'test.png', ]; $this->databaseMock diff --git a/tests/phpMyFAQ/Administration/ChangelogTest.php b/tests/phpMyFAQ/Administration/ChangelogTest.php index a749302786..c68b51bba4 100644 --- a/tests/phpMyFAQ/Administration/ChangelogTest.php +++ b/tests/phpMyFAQ/Administration/ChangelogTest.php @@ -6,8 +6,8 @@ use phpMyFAQ\Database; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\System; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class ChangelogTest extends TestCase @@ -42,9 +42,9 @@ public function testAdd(): void $this->assertTrue($result); $query = sprintf( - "SELECT COUNT(*) AS count FROM %sfaqchanges WHERE beitrag = %d", + 'SELECT COUNT(*) AS count FROM %sfaqchanges WHERE beitrag = %d', Database::getTablePrefix(), - $id + $id, ); $result = $this->configuration->getDb()->query($query); @@ -72,9 +72,9 @@ public function testGetByFaqId(): void 'user' => $userId, 'date' => $_SERVER['REQUEST_TIME'], 'changelog' => $text, - ] + ], ], - $result + $result, ); } } diff --git a/tests/phpMyFAQ/Administration/HelperTest.php b/tests/phpMyFAQ/Administration/HelperTest.php index 8f0f55b9ac..f1508f1584 100644 --- a/tests/phpMyFAQ/Administration/HelperTest.php +++ b/tests/phpMyFAQ/Administration/HelperTest.php @@ -9,8 +9,8 @@ use phpMyFAQ\Strings; use phpMyFAQ\Translation; use phpMyFAQ\User\CurrentUser; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Class HelperTest diff --git a/tests/phpMyFAQ/Administration/HttpStreamerTest.php b/tests/phpMyFAQ/Administration/HttpStreamerTest.php index 535f950695..d67d2e05c9 100644 --- a/tests/phpMyFAQ/Administration/HttpStreamerTest.php +++ b/tests/phpMyFAQ/Administration/HttpStreamerTest.php @@ -3,10 +3,10 @@ namespace phpMyFAQ\Administration; use phpMyFAQ\Core\Exception; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\Response; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class HttpStreamerTest extends TestCase @@ -29,7 +29,7 @@ public function testSend(): void } catch (Exception $e) { $this->assertEquals( 'Error: unable to send my headers: someone already sent other headers!', - $e->getMessage() + $e->getMessage(), ); } diff --git a/tests/phpMyFAQ/Administration/LatestUsersTest.php b/tests/phpMyFAQ/Administration/LatestUsersTest.php index a4bcfad04f..2498661d65 100644 --- a/tests/phpMyFAQ/Administration/LatestUsersTest.php +++ b/tests/phpMyFAQ/Administration/LatestUsersTest.php @@ -1,14 +1,13 @@ databaseMock = $this->createMock(DatabaseDriver::class); $this->configurationMock = $this->createStub(Configuration::class); - $this->configurationMock - ->method('getDb') - ->willReturn($this->databaseMock); + $this->configurationMock->method('getDb')->willReturn($this->databaseMock); $this->latestUsers = new LatestUsers($this->configurationMock); } @@ -227,4 +224,3 @@ public function testGetListSetsEmptyMemberSinceIsoWhenSourceIsEmpty(): void $this->assertSame('', $result[0]['member_since_iso']); } } - diff --git a/tests/phpMyFAQ/Administration/RatingDataTest.php b/tests/phpMyFAQ/Administration/RatingDataTest.php index c0c48d9b2e..3459721358 100644 --- a/tests/phpMyFAQ/Administration/RatingDataTest.php +++ b/tests/phpMyFAQ/Administration/RatingDataTest.php @@ -6,9 +6,9 @@ use phpMyFAQ\Database; use phpMyFAQ\Database\DatabaseDriver; use phpMyFAQ\Link; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class RatingDataTest extends TestCase @@ -23,9 +23,7 @@ protected function setUp(): void $this->mockConfiguration = $this->createStub(Configuration::class); // Mock Database class - $this->mockDb = $this->getMockBuilder(DatabaseDriver::class) - ->disableOriginalConstructor() - ->getMock(); + $this->mockDb = $this->getMockBuilder(DatabaseDriver::class)->disableOriginalConstructor()->getMock(); // Stub the getDb method of Configuration to return the mockDb object $this->mockConfiguration->method('getDb')->willReturn($this->mockDb); @@ -59,8 +57,7 @@ public function testGetAllReturnsSingleRating(): void $mockResult->usr = 10; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult, false); $result = $this->ratingData->getAll(); @@ -97,8 +94,7 @@ public function testGetAllReturnsMultipleRatings(): void $mockResult2->usr = 5; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult1, $mockResult2, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult1, $mockResult2, false); $result = $this->ratingData->getAll(); @@ -128,8 +124,7 @@ public function testGetAllEscapesHtmlInQuestions(): void $mockResult->usr = 10; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult, false); $result = $this->ratingData->getAll(); @@ -153,8 +148,7 @@ public function testGetAllHandlesWhitespaceInQuestions(): void $mockResult->usr = 10; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult, false); $result = $this->ratingData->getAll(); @@ -177,8 +171,7 @@ public function testGetAllGeneratesCorrectUrl(): void $mockResult->usr = 7; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult, false); $result = $this->ratingData->getAll(); @@ -193,9 +186,10 @@ public function testGetAllVerifiesQueryStructure(): void $this->mockConfiguration->method('getDefaultUrl')->willReturn('http://example.com/'); // Verify that query method is called with a SQL string containing expected elements - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') - ->willReturnCallback(function($query) { + ->willReturnCallback(function ($query) { // Verify the query contains expected SQL components $this->assertStringContainsString('SELECT', $query); $this->assertStringContainsString('faqvoting', $query); @@ -227,8 +221,7 @@ public function testGetAllHandlesNumericTypes(): void $mockResult->usr = 12; // integer user count $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult, false); $result = $this->ratingData->getAll(); diff --git a/tests/phpMyFAQ/Administration/ReportTest.php b/tests/phpMyFAQ/Administration/ReportTest.php index 52bddae893..445de5b1b0 100644 --- a/tests/phpMyFAQ/Administration/ReportTest.php +++ b/tests/phpMyFAQ/Administration/ReportTest.php @@ -4,9 +4,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\DatabaseDriver; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class ReportTest extends TestCase @@ -21,9 +21,7 @@ protected function setUp(): void $this->mockConfiguration = $this->createStub(Configuration::class); // Mock Database class - $this->mockDb = $this->getMockBuilder(DatabaseDriver::class) - ->disableOriginalConstructor() - ->getMock(); + $this->mockDb = $this->getMockBuilder(DatabaseDriver::class)->disableOriginalConstructor()->getMock(); // Stub the getDb method of Configuration to return the mockDb object $this->mockConfiguration->method('getDb')->willReturn($this->mockDb); @@ -35,7 +33,7 @@ protected function setUp(): void public function testSanitize(): void { $data = [ - ['John Doe', 'john.doe@example.com', '12345'], + ['John Doe', 'john.doe@example.com', '12345'], ['Jane Smith', 'jane.smith@example.com', '=SUM(A1:A10)'], ]; @@ -43,7 +41,7 @@ public function testSanitize(): void $expected = [ 'John Doe,"john.doe@example.com",12345', - 'Jane Smith,"jane.smith@example.com","=SUM(A1:A10)"' + 'Jane Smith,"jane.smith@example.com","=SUM(A1:A10)"', ]; foreach ($data as $row) { @@ -152,8 +150,7 @@ public function testGetReportingDataReturnsSingleFaq(): void $mockResult->last_author = 'Jane Smith'; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult, false); $result = $this->report->getReportingData(); @@ -203,8 +200,7 @@ public function testGetReportingDataHandlesMultipleFaqs(): void $mockResult2->last_author = 'Editor Two'; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult1, $mockResult2, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult1, $mockResult2, false); $result = $this->report->getReportingData(); @@ -251,8 +247,7 @@ public function testGetReportingDataCountsTranslations(): void $mockResult2->last_author = 'Hans Mueller'; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult1, $mockResult2, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult1, $mockResult2, false); $result = $this->report->getReportingData(); @@ -279,8 +274,7 @@ public function testGetReportingDataHandlesNullValues(): void $mockResult->last_author = null; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturn($mockResult, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult, false); $result = $this->report->getReportingData(); @@ -295,9 +289,10 @@ public function testGetReportingDataHandlesNullValues(): void public function testGetReportingDataVerifiesQueryStructure(): void { // Verify that query method is called with a SQL string containing expected elements - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') - ->willReturnCallback(function($query) { + ->willReturnCallback(function ($query) { // Verify the query contains expected SQL components $this->assertStringContainsString('SELECT', $query); $this->assertStringContainsString('faqdata', $query); diff --git a/tests/phpMyFAQ/Administration/RevisionTest.php b/tests/phpMyFAQ/Administration/RevisionTest.php index bc28ba6711..ef166e5721 100644 --- a/tests/phpMyFAQ/Administration/RevisionTest.php +++ b/tests/phpMyFAQ/Administration/RevisionTest.php @@ -4,9 +4,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\DatabaseDriver; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class RevisionTest extends TestCase @@ -21,9 +21,7 @@ protected function setUp(): void $this->mockConfiguration = $this->createStub(Configuration::class); // Mock Database class - $this->mockDb = $this->getMockBuilder(DatabaseDriver::class) - ->disableOriginalConstructor() - ->getMock(); + $this->mockDb = $this->getMockBuilder(DatabaseDriver::class)->disableOriginalConstructor()->getMock(); // Stub the getDb method of Configuration to return the mockDb object $this->mockConfiguration->method('getDb')->willReturn($this->mockDb); @@ -47,9 +45,10 @@ public function testCreateGeneratesCorrectQuery(): void $this->mockDb->method('escape')->willReturn('de'); // Verify the SQL query structure - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') - ->willReturnCallback(function($query) { + ->willReturnCallback(function ($query) { // Verify the query contains expected SQL components $this->assertStringContainsString('INSERT INTO', $query); $this->assertStringContainsString('faqdata_revisions', $query); @@ -76,7 +75,8 @@ public function testCreateEscapesFaqLanguage(): void $this->mockDb->method('escape')->willReturn($escapedInput); $this->mockDb->method('query')->willReturn(true); - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('escape') ->with($dangerousInput) ->willReturn($escapedInput); @@ -109,8 +109,7 @@ public function testGetReturnsSingleRevision(): void $mockResult->updated = '20250804120000'; $mockResult->author = 'Jane Smith'; - $this->mockDb->method('fetchObject') - ->willReturn($mockResult, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult, false); $result = $this->revision->get(123, 'en', 'John Doe'); @@ -137,8 +136,7 @@ public function testGetReturnsMultipleRevisions(): void $mockResult2->updated = '20250804140000'; $mockResult2->author = 'Author Two'; - $this->mockDb->method('fetchObject') - ->willReturn($mockResult1, $mockResult2, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult1, $mockResult2, false); $result = $this->revision->get(456, 'de', 'Fallback Author'); @@ -167,8 +165,7 @@ public function testGetHandlesFaqIdZeroWithCurrentTimestamp(): void $mockResult->updated = '20250804120000'; // This should be ignored for FAQ ID 0 $mockResult->author = 'Database Author'; // This should be ignored for FAQ ID 0 - $this->mockDb->method('fetchObject') - ->willReturn($mockResult, false); + $this->mockDb->method('fetchObject')->willReturn($mockResult, false); $result = $this->revision->get(0, 'en', 'john doe'); @@ -188,9 +185,10 @@ public function testGetGeneratesCorrectQuery(): void $this->mockDb->method('numRows')->willReturn(0); // Verify the SQL query structure - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') - ->willReturnCallback(function($query) { + ->willReturnCallback(function ($query) { // Verify the query contains expected SQL components $this->assertStringContainsString('SELECT', $query); $this->assertStringContainsString('revision_id, updated, author', $query); @@ -217,7 +215,8 @@ public function testGetEscapesFaqLanguage(): void $this->mockDb->method('query')->willReturn(true); $this->mockDb->method('numRows')->willReturn(0); - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('escape') ->with($dangerousInput) ->willReturn($escapedInput); @@ -252,9 +251,10 @@ public function testDeleteGeneratesCorrectQuery(): void $this->mockDb->method('escape')->willReturn('it'); // Verify the SQL query structure - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') - ->willReturnCallback(function($query) { + ->willReturnCallback(function ($query) { // Verify the query contains expected SQL components $this->assertStringContainsString('DELETE FROM', $query); $this->assertStringContainsString('faqdata_revisions', $query); @@ -277,7 +277,8 @@ public function testDeleteEscapesFaqLanguage(): void $this->mockDb->method('escape')->willReturn($escapedInput); $this->mockDb->method('query')->willReturn(true); - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('escape') ->with($dangerousInput) ->willReturn($escapedInput); diff --git a/tests/phpMyFAQ/Administration/SessionRepositoryTest.php b/tests/phpMyFAQ/Administration/SessionRepositoryTest.php index 2c69ab8db3..b68a7718b1 100644 --- a/tests/phpMyFAQ/Administration/SessionRepositoryTest.php +++ b/tests/phpMyFAQ/Administration/SessionRepositoryTest.php @@ -4,9 +4,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\DatabaseDriver; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SessionRepositoryTest extends TestCase @@ -157,7 +157,8 @@ public function testDeleteSessionsByTimeRangeReturnsTrue(): void $first = 1609459200; $last = 1609545600; - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') ->with($this->stringContains('DELETE FROM')) ->willReturn(true); @@ -181,7 +182,8 @@ public function testDeleteSessionsByTimeRangeReturnsFalse(): void public function testDeleteAllSessionsReturnsTrue(): void { - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') ->with($this->stringContains('DELETE FROM')) ->willReturn(true); @@ -212,8 +214,7 @@ public function testGetSessionTimestampsReturnsArray(): void $resultMock2->time = 1609476000; $this->mockDb->method('query')->willReturn(true); - $this->mockDb->method('fetchObject') - ->willReturnOnConsecutiveCalls($resultMock1, $resultMock2, false); + $this->mockDb->method('fetchObject')->willReturnOnConsecutiveCalls($resultMock1, $resultMock2, false); $timestamps = $this->repository->getSessionTimestamps($startDate, $endDate); @@ -239,9 +240,10 @@ public function testCountOnlineUsersFromSessionsVerifiesQuery(): void { $minTimestamp = 1609459200; - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') - ->willReturnCallback(function($query) { + ->willReturnCallback(function ($query) { $this->assertStringContainsString('SELECT COUNT(DISTINCT user_id)', $query); $this->assertStringContainsString('faqsessions', $query); $this->assertStringContainsString('WHERE time >=', $query); @@ -260,9 +262,10 @@ public function testCountOnlineUsersFromFaqUserVerifiesQuery(): void { $minTimestamp = 1609459200; - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') - ->willReturnCallback(function($query) { + ->willReturnCallback(function ($query) { $this->assertStringContainsString('SELECT COUNT(*)', $query); $this->assertStringContainsString('faquser', $query); $this->assertStringContainsString('session_id IS NOT NULL', $query); diff --git a/tests/phpMyFAQ/Administration/SessionTest.php b/tests/phpMyFAQ/Administration/SessionTest.php index fe8a6e1244..9f649d0309 100644 --- a/tests/phpMyFAQ/Administration/SessionTest.php +++ b/tests/phpMyFAQ/Administration/SessionTest.php @@ -4,10 +4,10 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\DatabaseDriver; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SessionTest extends TestCase @@ -98,7 +98,8 @@ public function testDeleteSessions(): void $first = 1609459200; $last = 1609545600; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('DELETE FROM')) ->willReturn(true); @@ -110,7 +111,8 @@ public function testDeleteSessions(): void public function testDeleteAllSessions(): void { - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('DELETE FROM')) ->willReturn(true); @@ -140,7 +142,7 @@ public function testGetLast30DaysVisits() for ($date = $startDate; $date <= $endDate; $date += 86400) { $visit = new stdClass(); $visit->date = date(format: 'Y-m-d', timestamp: $date); - $visit->number = ($date == $startDate + 86400) ? 1 : 0; + $visit->number = $date == ($startDate + 86400) ? 1 : 0; $expectedVisits[] = $visit; } diff --git a/tests/phpMyFAQ/Administration/TranslationStatisticsTest.php b/tests/phpMyFAQ/Administration/TranslationStatisticsTest.php index 2e75f8c215..32a604ac00 100644 --- a/tests/phpMyFAQ/Administration/TranslationStatisticsTest.php +++ b/tests/phpMyFAQ/Administration/TranslationStatisticsTest.php @@ -6,8 +6,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class TranslationStatisticsTest extends TestCase diff --git a/tests/phpMyFAQ/ApplicationTest.php b/tests/phpMyFAQ/ApplicationTest.php index 9bf2a65cf5..c692e7e8ba 100644 --- a/tests/phpMyFAQ/ApplicationTest.php +++ b/tests/phpMyFAQ/ApplicationTest.php @@ -2,23 +2,23 @@ namespace phpMyFAQ; +use phpMyFAQ\Core\Exception as PMFException; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; -use phpMyFAQ\Core\Exception as PMFException; use ReflectionClass; use ReflectionException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\Controller\ControllerResolver; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class ApplicationTest extends TestCase @@ -79,25 +79,25 @@ public function testSetLanguageWithContainer(): void $session = $this->createStub(Session::class); $language = new Language($configuration, $session); - $configuration->expects($this->exactly(2)) + $configuration + ->expects($this->exactly(2)) ->method('get') ->willReturnMap([ ['main.languageDetection', true], - ['main.language', 'en'], + ['main.language', 'en'], ]); // Keine Mock-Erwartung auf Language::setLanguage() – echte Instanz wird verwendet // Konfiguration speichert die Language-Instanz über setLanguage() - $configuration->expects($this->once()) - ->method('setLanguage') - ->with($language); + $configuration->expects($this->once())->method('setLanguage')->with($language); - $this->container->expects($this->exactly(2)) + $this->container + ->expects($this->exactly(2)) ->method('get') ->willReturnMap([ ['phpmyfaq.configuration', $configuration], - ['phpmyfaq.language', $language], + ['phpmyfaq.language', $language], ]); $reflection = new ReflectionClass(Application::class); @@ -143,7 +143,7 @@ public function testHandleRequestSuccess(): void $routeCollection->add('test_route', new Route('/test', [ '_controller' => function () { return new Response('Test Response'); - } + }, ])); $request = Request::create('/test'); @@ -186,7 +186,7 @@ public function testHandleRequestUnauthorizedHttpExceptionForApi(): void $routeCollection->add('api_route', new Route('/api/test', [ '_controller' => function () { throw new UnauthorizedHttpException('Bearer', 'Unauthorized'); - } + }, ])); $request = Request::create('/api/test'); @@ -213,7 +213,7 @@ public function testHandleRequestUnauthorizedHttpExceptionForNonApi(): void $routeCollection->add('web_route', new Route('/test', [ '_controller' => function () { throw new UnauthorizedHttpException('Bearer', 'Unauthorized'); - } + }, ])); $request = Request::create('/test'); @@ -240,7 +240,7 @@ public function testHandleRequestBadRequestException(): void $routeCollection->add('bad_route', new Route('/bad', [ '_controller' => function () { throw new BadRequestException('Bad request'); - } + }, ])); $request = Request::create('/bad'); @@ -266,24 +266,24 @@ public function testRunMethodWithContainer(): void $session = $this->createStub(Session::class); $language = new Language($configuration, $session); - $configuration->expects($this->exactly(2)) + $configuration + ->expects($this->exactly(2)) ->method('get') ->willReturnMap([ ['main.languageDetection', true], - ['main.language', 'en'], + ['main.language', 'en'], ]); // Keine Mock-Erwartung auf Language::setLanguage() – echte Instanz wird verwendet - $configuration->expects($this->once()) - ->method('setLanguage') - ->with($language); + $configuration->expects($this->once())->method('setLanguage')->with($language); - $this->container->expects($this->exactly(2)) + $this->container + ->expects($this->exactly(2)) ->method('get') ->willReturnMap([ ['phpmyfaq.configuration', $configuration], - ['phpmyfaq.language', $language], + ['phpmyfaq.language', $language], ]); $routeCollection = new RouteCollection(); diff --git a/tests/phpMyFAQ/Attachment/AbstractAttachmentTest.php b/tests/phpMyFAQ/Attachment/AbstractAttachmentTest.php index 96fbec557b..3ab5d9dd71 100644 --- a/tests/phpMyFAQ/Attachment/AbstractAttachmentTest.php +++ b/tests/phpMyFAQ/Attachment/AbstractAttachmentTest.php @@ -3,10 +3,10 @@ namespace phpMyFAQ\Attachment; use phpMyFAQ\Database\DatabaseDriver; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class AbstractAttachmentTest diff --git a/tests/phpMyFAQ/Attachment/AbstractMimeTypeTest.php b/tests/phpMyFAQ/Attachment/AbstractMimeTypeTest.php index da529afe63..a3fd01afa2 100644 --- a/tests/phpMyFAQ/Attachment/AbstractMimeTypeTest.php +++ b/tests/phpMyFAQ/Attachment/AbstractMimeTypeTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Attachment; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class AbstractMimeTypeTest extends TestCase diff --git a/tests/phpMyFAQ/Attachment/AttachmentCollectionTest.php b/tests/phpMyFAQ/Attachment/AttachmentCollectionTest.php index c4bb4f414b..690148d390 100644 --- a/tests/phpMyFAQ/Attachment/AttachmentCollectionTest.php +++ b/tests/phpMyFAQ/Attachment/AttachmentCollectionTest.php @@ -4,9 +4,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\DatabaseDriver; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class AttachmentCollectionTest extends TestCase @@ -52,7 +52,7 @@ public function testGetBreadcrumbsReturnsDataWhenQuerySucceeds(): void 'filename' => 'file1.pdf', 'filesize' => 123456, 'mime_type' => 'application/pdf', - 'thema' => 'General' + 'thema' => 'General', ], [ 'id' => 2, @@ -61,15 +61,18 @@ public function testGetBreadcrumbsReturnsDataWhenQuerySucceeds(): void 'filename' => 'file2.jpg', 'filesize' => 78910, 'mime_type' => 'image/jpeg', - 'thema' => 'Media' - ] + 'thema' => 'Media', + ], ]; // Mock the query method to return a non-false result $this->mockDatabase->method('query')->willReturn('mock_result'); // Mock fetchAll to return the expected result - $this->mockDatabase->method('fetchAll')->with('mock_result')->willReturn($expectedResult); + $this->mockDatabase + ->method('fetchAll') + ->with('mock_result') + ->willReturn($expectedResult); // Call the method being tested $result = $this->attachmentCollection->getBreadcrumbs(); diff --git a/tests/phpMyFAQ/Attachment/AttachmentFactoryTest.php b/tests/phpMyFAQ/Attachment/AttachmentFactoryTest.php index 75fa1dec67..f7fd80fd5f 100644 --- a/tests/phpMyFAQ/Attachment/AttachmentFactoryTest.php +++ b/tests/phpMyFAQ/Attachment/AttachmentFactoryTest.php @@ -6,10 +6,10 @@ use phpMyFAQ\Database\DatabaseDriver; use phpMyFAQ\Enums\AttachmentStorageType; use phpMyFAQ\Language; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class AttachmentFactoryTest @@ -182,9 +182,10 @@ public function testFetchByRecordIdQueryStructure(): void $expectedQueryPattern = "SELECT id FROM %sfaqattachment WHERE record_id = 456 AND record_lang = 'de'"; - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') - ->willReturnCallback(function($query) use ($expectedQueryPattern) { + ->willReturnCallback(function ($query) use ($expectedQueryPattern) { $this->assertStringContainsString('SELECT id FROM', $query); $this->assertStringContainsString('faqattachment', $query); $this->assertStringContainsString('record_id = 456', $query); @@ -249,9 +250,10 @@ public function testFetchByRecordIdWithDifferentLanguages(): void // Test with French language Language::$language = 'fr'; - $this->mockDb->expects($this->once()) + $this->mockDb + ->expects($this->once()) ->method('query') - ->willReturnCallback(function($query) { + ->willReturnCallback(function ($query) { $this->assertStringContainsString("record_lang = 'fr'", $query); return true; }); diff --git a/tests/phpMyFAQ/Attachment/FileTest.php b/tests/phpMyFAQ/Attachment/FileTest.php index 6569a6c6d3..448d61febd 100644 --- a/tests/phpMyFAQ/Attachment/FileTest.php +++ b/tests/phpMyFAQ/Attachment/FileTest.php @@ -2,15 +2,15 @@ namespace phpMyFAQ\Attachment; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; use phpMyFAQ\Attachment\Filesystem\AbstractFile as FilesystemFile; use phpMyFAQ\Attachment\Filesystem\File\EncryptedFile; use phpMyFAQ\Attachment\Filesystem\File\VanillaFile; use phpMyFAQ\Database\DatabaseDriver; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; -use org\bovigo\vfs\vfsStream; -use org\bovigo\vfs\vfsStreamDirectory; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class FileTest @@ -77,7 +77,7 @@ public function testBuildFilePathWithValidHash(): void $properties = [ 'encrypted' => false, - 'realHash' => 'abcdefghijklmnopqrstuvwxyz123456' + 'realHash' => 'abcdefghijklmnopqrstuvwxyz123456', ]; foreach ($properties as $prop => $value) { @@ -93,9 +93,16 @@ public function testBuildFilePathWithValidHash(): void $filePath = $this->file->testBuildFilePath(); // Should create path with 3 subdirectories of 5 characters each - $expectedPattern = '/tmp/attachments' . DIRECTORY_SEPARATOR . 'abcde' . - DIRECTORY_SEPARATOR . 'fghij' . DIRECTORY_SEPARATOR . 'klmno' . - DIRECTORY_SEPARATOR . 'pqrstuvwxyz123456'; + $expectedPattern = + '/tmp/attachments' + . DIRECTORY_SEPARATOR + . 'abcde' + . DIRECTORY_SEPARATOR + . 'fghij' + . DIRECTORY_SEPARATOR + . 'klmno' + . DIRECTORY_SEPARATOR + . 'pqrstuvwxyz123456'; $this->assertEquals($expectedPattern, $filePath); } @@ -115,8 +122,8 @@ public function testCreateSubDirsAlreadyExists(): void // Create directory structure first vfsStream::create([ 'existing' => [ - 'directory' => [] - ] + 'directory' => [], + ], ], $this->vfsRoot); $testPath = vfsStream::url('attachments/existing/directory/file.txt'); @@ -142,7 +149,7 @@ public function testDeleteWithLinkedRecords(): void $properties = [ 'encrypted' => false, 'realHash' => 'testhash12345', - 'id' => 1 + 'id' => 1, ]; foreach ($properties as $prop => $value) { @@ -165,7 +172,7 @@ public function testBuildFilePathWithLongHash(): void $reflection = new ReflectionClass($this->file); $properties = [ 'encrypted' => false, - 'realHash' => 'verylonghashstringwithmorethan30charactersinthehashvalue123456789' + 'realHash' => 'verylonghashstringwithmorethan30charactersinthehashvalue123456789', ]; foreach ($properties as $prop => $value) { @@ -181,9 +188,16 @@ public function testBuildFilePathWithLongHash(): void $filePath = $this->file->testBuildFilePath(); // Should create proper subdirectories even with long hash - $expectedPattern = '/tmp/attachments' . DIRECTORY_SEPARATOR . 'veryl' . - DIRECTORY_SEPARATOR . 'ongha' . DIRECTORY_SEPARATOR . 'shstr' . - DIRECTORY_SEPARATOR . 'ingwithmorethan30charactersinthehashvalue123456789'; + $expectedPattern = + '/tmp/attachments' + . DIRECTORY_SEPARATOR + . 'veryl' + . DIRECTORY_SEPARATOR + . 'ongha' + . DIRECTORY_SEPARATOR + . 'shstr' + . DIRECTORY_SEPARATOR + . 'ingwithmorethan30charactersinthehashvalue123456789'; $this->assertEquals($expectedPattern, $filePath); } @@ -194,7 +208,7 @@ public function testMkVirtualHashMethod(): void $reflection = new ReflectionClass($this->file); $properties = [ 'encrypted' => false, - 'realHash' => 'test123456789' + 'realHash' => 'test123456789', ]; foreach ($properties as $prop => $value) { diff --git a/tests/phpMyFAQ/Attachment/Filesystem/File/VanillaFileTest.php b/tests/phpMyFAQ/Attachment/Filesystem/File/VanillaFileTest.php index 1ebc0316b5..5452b8603f 100644 --- a/tests/phpMyFAQ/Attachment/Filesystem/File/VanillaFileTest.php +++ b/tests/phpMyFAQ/Attachment/Filesystem/File/VanillaFileTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Attachment\Filesystem\File; -use org\bovigo\vfs\vfsStreamDirectory; -use PHPUnit\Framework\TestCase; use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class VanillaFileTest extends TestCase @@ -17,14 +17,15 @@ protected function setUp(): void { // Setup the virtual file system $this->root = vfsStream::setup('root', null, [ - 'file.txt' => 'test file content' + 'file.txt' => 'test file content', ]); // Get the virtual file path $filePath = vfsStream::url('root/file.txt'); // Mock the VanillaFile class and inject the virtual file - $this->mockFile = $this->getMockBuilder(VanillaFile::class) + $this->mockFile = $this + ->getMockBuilder(VanillaFile::class) ->setConstructorArgs([$filePath]) ->onlyMethods(['getChunk', 'putChunk', 'eof']) ->getMock(); @@ -32,10 +33,11 @@ protected function setUp(): void public function testPutChunkWritesData(): void { - $data = "test chunk data"; + $data = 'test chunk data'; // Write data to the virtual file - $this->mockFile->expects($this->once()) + $this->mockFile + ->expects($this->once()) ->method('putChunk') ->with($data) ->willReturn(true); @@ -47,7 +49,8 @@ public function testPutChunkWritesData(): void public function testGetChunkReadsData(): void { // Mocking the getChunk behavior to return content from the virtual file - $this->mockFile->expects($this->once()) + $this->mockFile + ->expects($this->once()) ->method('getChunk') ->willReturn('test file content'); diff --git a/tests/phpMyFAQ/Auth/AuthDatabaseTest.php b/tests/phpMyFAQ/Auth/AuthDatabaseTest.php index d64c551ecc..e09a9c98c4 100644 --- a/tests/phpMyFAQ/Auth/AuthDatabaseTest.php +++ b/tests/phpMyFAQ/Auth/AuthDatabaseTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Auth; -use phpMyFAQ\Core\Exception; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; +use phpMyFAQ\Core\Exception; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class AuthDatabaseTest extends TestCase diff --git a/tests/phpMyFAQ/Auth/AuthEntraIdTest.php b/tests/phpMyFAQ/Auth/AuthEntraIdTest.php index cda6cfbb42..6c9f4d20ac 100644 --- a/tests/phpMyFAQ/Auth/AuthEntraIdTest.php +++ b/tests/phpMyFAQ/Auth/AuthEntraIdTest.php @@ -2,19 +2,21 @@ namespace phpMyFAQ\Auth; -use phpMyFAQ\Auth\EntraId\OAuth; +use Monolog\Logger; use phpMyFAQ\Auth\EntraId\EntraIdSession; +use phpMyFAQ\Auth\EntraId\OAuth; use phpMyFAQ\Configuration; use phpMyFAQ\Core\Exception; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Monolog\Logger; use ReflectionClass; use TypeError; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; const AAD_OAUTH_TENANTID = 'test-tenant-id'; + const AAD_OAUTH_CLIENTID = 'test-client-id'; + const AAD_OAUTH_SCOPE = 'test-scope'; #[AllowMockObjectsWithoutExpectations] @@ -108,7 +110,8 @@ public function testIsValidLoginSuccess(): void { $login = 'test@example.com'; - $this->oAuthMock->expects($this->once()) + $this->oAuthMock + ->expects($this->once()) ->method('getMail') ->willReturn('test@example.com'); @@ -120,7 +123,8 @@ public function testIsValidLoginFailure(): void { $login = 'test@example.com'; - $this->oAuthMock->expects($this->once()) + $this->oAuthMock + ->expects($this->once()) ->method('getMail') ->willReturn('different@example.com'); @@ -132,25 +136,22 @@ public function testAuthorize(): void { $defaultUrl = 'https://example.com/'; - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('getDefaultUrl') ->willReturn($defaultUrl); - $this->sessionMock->expects($this->once()) - ->method('setCurrentSessionKey'); + $this->sessionMock->expects($this->once())->method('setCurrentSessionKey'); - $this->sessionMock->expects($this->once()) + $this->sessionMock + ->expects($this->once()) ->method('set') ->with(EntraIdSession::ENTRA_ID_OAUTH_VERIFIER, $this->isString()); - $this->sessionMock->expects($this->once()) + $this->sessionMock + ->expects($this->once()) ->method('setCookie') - ->with( - EntraIdSession::ENTRA_ID_OAUTH_VERIFIER, - $this->isString(), - 7200, - false - ); + ->with(EntraIdSession::ENTRA_ID_OAUTH_VERIFIER, $this->isString(), 7200, false); // Capture output to prevent HTML output in test results ob_start(); @@ -210,7 +211,7 @@ public function testCreateOAuthChallengeGeneration(): void $expectedChallenge = str_replace( '=', '', - strtr(base64_encode(pack('H*', hash('sha256', $verifier))), '+/', '-_') + strtr(base64_encode(pack('H*', hash('sha256', $verifier))), '+/', '-_'), ); $this->assertEquals($expectedChallenge, $challenge); } @@ -274,7 +275,7 @@ public function testConstants(): void $this->assertEquals('S256', $reflection->getConstant('ENTRAID_CHALLENGE_METHOD')); $this->assertEquals( 'https://login.microsoftonline.com/common/wsfederation?wa=wsignout1.0', - $reflection->getConstant('ENTRAID_LOGOUT_URL') + $reflection->getConstant('ENTRAID_LOGOUT_URL'), ); } } diff --git a/tests/phpMyFAQ/Auth/AuthHttpTest.php b/tests/phpMyFAQ/Auth/AuthHttpTest.php index f85a959dbc..2cdb8dbf2c 100644 --- a/tests/phpMyFAQ/Auth/AuthHttpTest.php +++ b/tests/phpMyFAQ/Auth/AuthHttpTest.php @@ -4,8 +4,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Core\Exception; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class AuthHttpTest extends TestCase diff --git a/tests/phpMyFAQ/Auth/AuthLdapTest.php b/tests/phpMyFAQ/Auth/AuthLdapTest.php index 5906efeff2..cb934a3a4b 100644 --- a/tests/phpMyFAQ/Auth/AuthLdapTest.php +++ b/tests/phpMyFAQ/Auth/AuthLdapTest.php @@ -2,13 +2,13 @@ namespace phpMyFAQ\Auth; +use Monolog\Logger; use phpMyFAQ\Configuration; use phpMyFAQ\Enums\AuthenticationSourceType; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Monolog\Logger; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class AuthLdapTest extends TestCase @@ -27,18 +27,22 @@ protected function setUp(): void public function testConstructWithValidConfiguration(): void { - $this->configurationMock->method('getLdapServer')->willReturn([ - 0 => [ - 'ldap_server' => 'ldap.example.com', - 'ldap_port' => 389, - 'ldap_base' => 'dc=example,dc=com', - 'ldap_user' => 'cn=admin,dc=example,dc=com', - 'ldap_password' => 'password' - ] - ]); - $this->configurationMock->method('get')->willReturnMap([ - ['ldap.ldap_use_multiple_servers', false] - ]); + $this->configurationMock + ->method('getLdapServer') + ->willReturn([ + 0 => [ + 'ldap_server' => 'ldap.example.com', + 'ldap_port' => 389, + 'ldap_base' => 'dc=example,dc=com', + 'ldap_user' => 'cn=admin,dc=example,dc=com', + 'ldap_password' => 'password', + ], + ]); + $this->configurationMock + ->method('get') + ->willReturnMap([ + ['ldap.ldap_use_multiple_servers', false], + ]); // Test configuration validation without actually creating the AuthLdap instance // This avoids the LDAP connection attempt that causes warnings @@ -63,13 +67,29 @@ public function testConstructWithEmptyLdapServerThrowsException(): void public function testConstructWithMultipleServersConfiguration(): void { - $this->configurationMock->method('get')->willReturnMap([ - ['ldap.ldap_use_multiple_servers', true] - ]); - $this->configurationMock->method('getLdapServer')->willReturn([ - 0 => ['ldap_server' => 'ldap1.example.com', 'ldap_port' => 389, 'ldap_base' => 'dc=example,dc=com', 'ldap_user' => 'admin1', 'ldap_password' => 'pass1'], - 1 => ['ldap_server' => 'ldap2.example.com', 'ldap_port' => 389, 'ldap_base' => 'dc=example,dc=com', 'ldap_user' => 'admin2', 'ldap_password' => 'pass2'] - ]); + $this->configurationMock + ->method('get') + ->willReturnMap([ + ['ldap.ldap_use_multiple_servers', true], + ]); + $this->configurationMock + ->method('getLdapServer') + ->willReturn([ + 0 => [ + 'ldap_server' => 'ldap1.example.com', + 'ldap_port' => 389, + 'ldap_base' => 'dc=example,dc=com', + 'ldap_user' => 'admin1', + 'ldap_password' => 'pass1', + ], + 1 => [ + 'ldap_server' => 'ldap2.example.com', + 'ldap_port' => 389, + 'ldap_base' => 'dc=example,dc=com', + 'ldap_user' => 'admin2', + 'ldap_password' => 'pass2', + ], + ]); // Test multiple server configuration validation without creating AuthLdap instance // This avoids LDAP connection warnings @@ -130,8 +150,8 @@ public function testLdapServerConfigurationStructure(): void 'ldap_port' => 389, 'ldap_base' => 'dc=example,dc=com', 'ldap_user' => 'cn=admin,dc=example,dc=com', - 'ldap_password' => 'password' - ] + 'ldap_password' => 'password', + ], ]; $this->configurationMock->method('getLdapServer')->willReturn($serverConfig); @@ -153,15 +173,15 @@ public function testMultipleServersConfigValidation(): void 'ldap_port' => 389, 'ldap_base' => 'dc=example,dc=com', 'ldap_user' => 'admin1', - 'ldap_password' => 'pass1' + 'ldap_password' => 'pass1', ], 1 => [ 'ldap_server' => 'ldap2.example.com', 'ldap_port' => 389, 'ldap_base' => 'dc=example,dc=com', 'ldap_user' => 'admin2', - 'ldap_password' => 'pass2' - ] + 'ldap_password' => 'pass2', + ], ]; $this->assertCount(2, $multiServerConfig); @@ -183,8 +203,8 @@ public function testGroupMappingConfigStructure(): void 'auto_assign' => true, 'group_mapping' => [ 'AdminGroup' => 'Administrators', - 'UserGroup' => 'Users' - ] + 'UserGroup' => 'Users', + ], ]; $this->assertIsArray($groupConfig); diff --git a/tests/phpMyFAQ/Auth/AuthSsoTest.php b/tests/phpMyFAQ/Auth/AuthSsoTest.php index 559ccf5b6f..dfe06da9f3 100644 --- a/tests/phpMyFAQ/Auth/AuthSsoTest.php +++ b/tests/phpMyFAQ/Auth/AuthSsoTest.php @@ -4,9 +4,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Enums\AuthenticationSourceType; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class AuthSsoTest extends TestCase @@ -32,7 +32,8 @@ public function testCreateWithLdapActive(): void $password = 'password'; $domain = 'example.com'; - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('isLdapActive') ->willReturn(true); @@ -50,7 +51,8 @@ public function testCreateWithoutLdap(): void $password = 'password'; $domain = 'example.com'; - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('isLdapActive') ->willReturn(false); diff --git a/tests/phpMyFAQ/Auth/AuthWebAuthnTest.php b/tests/phpMyFAQ/Auth/AuthWebAuthnTest.php index 9f900cbb4b..ab04826886 100644 --- a/tests/phpMyFAQ/Auth/AuthWebAuthnTest.php +++ b/tests/phpMyFAQ/Auth/AuthWebAuthnTest.php @@ -7,9 +7,9 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Plugin\PluginException; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class AuthWebAuthnTest extends TestCase @@ -22,7 +22,6 @@ class AuthWebAuthnTest extends TestCase */ protected function setUp(): void { - $dbHandle = new Sqlite3(); $dbHandle->connect(PMF_TEST_DIR . '/test.db', '', ''); $this->configuration = new Configuration($dbHandle); @@ -217,10 +216,10 @@ public function testPrepareForLogin(): void // Create a WebAuthn data structure that matches the expected format // The AuthWebAuthn::prepareForLogin() expects objects with 'id' property $userWebAuthn = json_encode([ - (object)[ + (object) [ 'id' => base64_encode('test-credential-id'), - 'publicKey' => 'test-public-key' - ] + 'publicKey' => 'test-public-key', + ], ]); $result = $this->authWebAuthn->prepareForLogin($userWebAuthn); @@ -254,10 +253,10 @@ public function testAuthenticateWithInvalidJson(): void // Create proper WebAuthn structure with 'id' property for authenticate method $userWebAuthn = json_encode([ - (object)[ + (object) [ 'id' => base64_encode('test-credential-id'), - 'publicKey' => 'test-public-key' - ] + 'publicKey' => 'test-public-key', + ], ]); // Expect TypeError from invalid JSON processing @@ -285,4 +284,3 @@ public function testConstructorWithDifferentUrls(): void $this->assertEquals('example.com', $result['publicKey']['rp']['id']); } } - diff --git a/tests/phpMyFAQ/Auth/EntraId/OAuthTest.php b/tests/phpMyFAQ/Auth/EntraId/OAuthTest.php index cff5aabe7f..7f9f5cc920 100644 --- a/tests/phpMyFAQ/Auth/EntraId/OAuthTest.php +++ b/tests/phpMyFAQ/Auth/EntraId/OAuthTest.php @@ -3,6 +3,7 @@ namespace phpMyFAQ\Auth\EntraId; use phpMyFAQ\Configuration; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use ReflectionClass; @@ -10,11 +11,13 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; const AAD_OAUTH_TENANTID = 'fake_tenant_id'; + const AAD_OAUTH_CLIENTID = 'fake_client_id'; + const AAD_OAUTH_SECRET = 'fake_secret'; + const AAD_OAUTH_SCOPE = 'fake_scope'; #[AllowMockObjectsWithoutExpectations] @@ -44,17 +47,21 @@ protected function setUp(): void public function testGetOAuthTokenSuccess(): void { $mockResponse = $this->createStub(ResponseInterface::class); - $mockResponse->method('getContent')->willReturn(json_encode([ - 'access_token' => 'fake_access_token', - 'id_token' => 'fake_id_token' - ])); - - $this->mockSession->expects($this->exactly(1)) + $mockResponse + ->method('getContent') + ->willReturn(json_encode([ + 'access_token' => 'fake_access_token', + 'id_token' => 'fake_id_token', + ])); + + $this->mockSession + ->expects($this->exactly(1)) ->method('get') ->with(EntraIdSession::ENTRA_ID_OAUTH_VERIFIER) ->willReturnOnConsecutiveCalls('', 'code_verifier'); - $this->mockClient->expects($this->once()) + $this->mockClient + ->expects($this->once()) ->method('request') ->with('POST', $this->stringContains('microsoftonline.com')) ->willReturn($mockResponse); @@ -77,15 +84,18 @@ public function testGetOAuthTokenSuccess(): void public function testRefreshTokenSuccess(): void { $mockResponse = $this->createStub(ResponseInterface::class); - $mockResponse->method('getContent')->willReturn(json_encode([ - 'access_token' => 'new_access_token', - 'refresh_token' => 'new_refresh_token', - 'id_token' => 'new_id_token' - ])); + $mockResponse + ->method('getContent') + ->willReturn(json_encode([ + 'access_token' => 'new_access_token', + 'refresh_token' => 'new_refresh_token', + 'id_token' => 'new_id_token', + ])); $this->oAuth->setRefreshToken('fake_refresh_token'); - $this->mockClient->expects($this->once()) + $this->mockClient + ->expects($this->once()) ->method('request') ->with('POST', $this->stringContains('microsoftonline.com')) ->willReturn($mockResponse); @@ -101,7 +111,6 @@ public function testRefreshTokenSuccess(): void $this->assertEquals('new_refresh_token', $result->refresh_token); } - public function testSetToken(): void { $header = base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); @@ -112,7 +121,8 @@ public function testSetToken(): void $token = new stdClass(); $token->id_token = $idToken; - $this->mockSession->expects($this->once()) + $this->mockSession + ->expects($this->once()) ->method('set') ->with(EntraIdSession::ENTRA_ID_JWT, $this->stringContains('John Doe')); @@ -144,7 +154,8 @@ public function testGetName(): void $token = new stdClass(); $token->id_token = $idToken; - $this->mockSession->expects($this->once()) + $this->mockSession + ->expects($this->once()) ->method('set') ->with(EntraIdSession::ENTRA_ID_JWT, $this->anything()); @@ -162,19 +173,22 @@ public function testGetMail(): void $token = new stdClass(); $token->id_token = $idToken; - $this->mockSession->expects($this->once()) + $this->mockSession + ->expects($this->once()) ->method('set') ->with(EntraIdSession::ENTRA_ID_JWT, $this->anything()); $this->oAuth->setToken($token); $this->assertEquals('test@company.com', $this->oAuth->getMail()); } + public function testSetTokenWithMalformedJWT(): void { $token = new stdClass(); $token->id_token = 'invalid-jwt'; - $this->mockSession->expects($this->once()) + $this->mockSession + ->expects($this->once()) ->method('set') ->with(EntraIdSession::ENTRA_ID_JWT, '{}'); @@ -188,7 +202,8 @@ public function testSetTokenWithIncompleteJWTParts(): void $token = new stdClass(); $token->id_token = 'header.payload'; // Missing signature part - $this->mockSession->expects($this->once()) + $this->mockSession + ->expects($this->once()) ->method('set') ->with(EntraIdSession::ENTRA_ID_JWT, '{}'); @@ -202,7 +217,8 @@ public function testSetTokenWithInvalidBase64(): void $token = new stdClass(); $token->id_token = 'header.invalid-base64!!!.signature'; - $this->mockSession->expects($this->once()) + $this->mockSession + ->expects($this->once()) ->method('set') ->with(EntraIdSession::ENTRA_ID_JWT, '{}'); @@ -219,7 +235,8 @@ public function testSetTokenWithMalformedJSON(): void $token = new stdClass(); $token->id_token = $header . '.' . $payload . '.' . $signature; - $this->mockSession->expects($this->once()) + $this->mockSession + ->expects($this->once()) ->method('set') ->with(EntraIdSession::ENTRA_ID_JWT, '{}'); @@ -243,8 +260,7 @@ public function testGetNameWithEmptyToken(): void $token = new stdClass(); $token->id_token = 'invalid'; - $this->mockSession->expects($this->once()) - ->method('set'); + $this->mockSession->expects($this->once())->method('set'); $this->oAuth->setToken($token); $this->assertEquals('', $this->oAuth->getName()); @@ -255,8 +271,7 @@ public function testGetMailWithEmptyToken(): void $token = new stdClass(); $token->id_token = 'invalid'; - $this->mockSession->expects($this->once()) - ->method('set'); + $this->mockSession->expects($this->once())->method('set'); $this->oAuth->setToken($token); $this->assertEquals('', $this->oAuth->getMail()); @@ -272,7 +287,8 @@ public function testSetTokenWithValidJWTButMissingFields(): void $token = new stdClass(); $token->id_token = $idToken; - $this->mockSession->expects($this->once()) + $this->mockSession + ->expects($this->once()) ->method('set') ->with(EntraIdSession::ENTRA_ID_JWT, $this->anything()); diff --git a/tests/phpMyFAQ/Auth/EntraId/SessionTest.php b/tests/phpMyFAQ/Auth/EntraId/SessionTest.php index 2bf29e525f..e530de3c97 100644 --- a/tests/phpMyFAQ/Auth/EntraId/SessionTest.php +++ b/tests/phpMyFAQ/Auth/EntraId/SessionTest.php @@ -3,9 +3,9 @@ namespace phpMyFAQ\Auth\EntraId; use phpMyFAQ\Configuration; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session as SymfonySession; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SessionTest extends TestCase @@ -30,7 +30,7 @@ public function testCreateCurrentSessionKey(): void // UUID v4 format check $this->assertMatchesRegularExpression( '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', - $key + $key, ); } @@ -48,12 +48,10 @@ public function testGetCurrentSessionKey(): void */ public function testSetCurrentSessionKey(): void { - $this->sessionMock->expects($this->once()) + $this->sessionMock + ->expects($this->once()) ->method('set') - ->with( - EntraIdSession::ENTRA_ID_SESSION_KEY, - $this->isString() - ); + ->with(EntraIdSession::ENTRA_ID_SESSION_KEY, $this->isString()); $result = $this->session->setCurrentSessionKey(); $this->assertInstanceOf(EntraIdSession::class, $result); @@ -65,12 +63,10 @@ public function testSetCurrentSessionKey(): void */ public function testSetCurrentSessionKeyIsChainable(): void { - $this->sessionMock->expects($this->once()) + $this->sessionMock + ->expects($this->once()) ->method('set') - ->with( - EntraIdSession::ENTRA_ID_SESSION_KEY, - $this->isString() - ); + ->with(EntraIdSession::ENTRA_ID_SESSION_KEY, $this->isString()); $result = $this->session->setCurrentSessionKey(); $this->assertSame($this->session, $result); @@ -85,7 +81,8 @@ public function testConstants(): void public function testSetCookieWithDefaults(): void { - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('getDefaultUrl') ->willReturn('https://example.com'); @@ -95,7 +92,8 @@ public function testSetCookieWithDefaults(): void public function testSetCookieWithCustomTimeout(): void { - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('getDefaultUrl') ->willReturn('https://example.com'); @@ -105,7 +103,8 @@ public function testSetCookieWithCustomTimeout(): void public function testSetCookieWithNullSessionId(): void { - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('getDefaultUrl') ->willReturn('https://example.com'); diff --git a/tests/phpMyFAQ/Auth/WebAuthn/WebAuthnUserTest.php b/tests/phpMyFAQ/Auth/WebAuthn/WebAuthnUserTest.php index fbfd7d8b73..1ec85bec7d 100644 --- a/tests/phpMyFAQ/Auth/WebAuthn/WebAuthnUserTest.php +++ b/tests/phpMyFAQ/Auth/WebAuthn/WebAuthnUserTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Auth\WebAuthn; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class WebAuthnUserTest extends TestCase @@ -45,5 +45,4 @@ public function testFluentInterface(): void $this->assertInstanceOf(WebAuthnUser::class, $result); } - } diff --git a/tests/phpMyFAQ/AuthTest.php b/tests/phpMyFAQ/AuthTest.php index ec28475cff..cd2ed6eb4e 100644 --- a/tests/phpMyFAQ/AuthTest.php +++ b/tests/phpMyFAQ/AuthTest.php @@ -93,7 +93,7 @@ public function testEnableDisableReadOnly(): void /** * @throws Exception - */public function testEncrypt(): void + */ public function testEncrypt(): void { $this->auth->getEncryptionContainer('bcrypt'); $hash = $this->auth->encrypt('foobar'); diff --git a/tests/phpMyFAQ/Bookmark/BookmarkFormatterTest.php b/tests/phpMyFAQ/Bookmark/BookmarkFormatterTest.php index 015c3e2b45..ab23c1c0ff 100644 --- a/tests/phpMyFAQ/Bookmark/BookmarkFormatterTest.php +++ b/tests/phpMyFAQ/Bookmark/BookmarkFormatterTest.php @@ -8,10 +8,10 @@ use phpMyFAQ\Strings; use phpMyFAQ\Translation; use phpMyFAQ\User\CurrentUser; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception as MockException; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class BookmarkFormatterTest extends TestCase diff --git a/tests/phpMyFAQ/Bookmark/BookmarkRepositoryTest.php b/tests/phpMyFAQ/Bookmark/BookmarkRepositoryTest.php index d91daac6b4..7099630d55 100644 --- a/tests/phpMyFAQ/Bookmark/BookmarkRepositoryTest.php +++ b/tests/phpMyFAQ/Bookmark/BookmarkRepositoryTest.php @@ -8,10 +8,10 @@ use phpMyFAQ\Strings; use phpMyFAQ\Translation; use phpMyFAQ\User\CurrentUser; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception as MockException; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class BookmarkRepositoryTest extends TestCase diff --git a/tests/phpMyFAQ/BookmarkTest.php b/tests/phpMyFAQ/BookmarkTest.php index 0dee791ff2..97d3062ec2 100644 --- a/tests/phpMyFAQ/BookmarkTest.php +++ b/tests/phpMyFAQ/BookmarkTest.php @@ -4,12 +4,12 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\User\CurrentUser; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use ReflectionClass; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class BookmarkTest extends TestCase diff --git a/tests/phpMyFAQ/Captcha/BuiltinCaptchaTest.php b/tests/phpMyFAQ/Captcha/BuiltinCaptchaTest.php index 3c8d19ec2a..684fddede4 100644 --- a/tests/phpMyFAQ/Captcha/BuiltinCaptchaTest.php +++ b/tests/phpMyFAQ/Captcha/BuiltinCaptchaTest.php @@ -6,9 +6,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Strings; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class CaptchaTest @@ -51,8 +51,9 @@ public function testValidateCaptchaCode(): void public function testRenderCaptchaImage(): void { - $expected = 'Chuck Norris has counted to infinity. Twice.'; + $expected = + 'Chuck Norris has counted to infinity. Twice.'; $this->assertEquals($expected, $this->captcha->renderCaptchaImage()); } @@ -178,9 +179,7 @@ public function testUserLoginStatusMethods(): void */ public function testFluentInterface(): void { - $result = $this->captcha - ->setUserIsLoggedIn(true) - ->setUserIsLoggedIn(false); + $result = $this->captcha->setUserIsLoggedIn(true)->setUserIsLoggedIn(false); $this->assertInstanceOf(BuiltinCaptcha::class, $result); $this->assertFalse($this->captcha->isUserIsLoggedIn()); @@ -225,7 +224,7 @@ public function testValidateCaptchaCodeWithSpecialCharacters(): void 'SELECT * FROM users', '../../etc/passwd', 'javascript:alert(1)', - '' + '', ]; foreach ($specialCodes as $code) { diff --git a/tests/phpMyFAQ/Captcha/CaptchaTest.php b/tests/phpMyFAQ/Captcha/CaptchaTest.php index 2e8d003e1a..140db6e73e 100644 --- a/tests/phpMyFAQ/Captcha/CaptchaTest.php +++ b/tests/phpMyFAQ/Captcha/CaptchaTest.php @@ -5,8 +5,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Strings; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class CaptchaTest extends TestCase @@ -25,6 +25,7 @@ protected function setUp(): void $dbHandle->connect(PMF_TEST_DIR . '/test.db', '', ''); $this->configuration = new Configuration($dbHandle); } + public function testGetInstanceWithGoogleRecaptchaEnabled(): void { $this->configuration->set('security.enableGoogleReCaptchaV2', true); diff --git a/tests/phpMyFAQ/Captcha/GoogleRecaptchaTest.php b/tests/phpMyFAQ/Captcha/GoogleRecaptchaTest.php index 0a7255dee8..5c00043393 100644 --- a/tests/phpMyFAQ/Captcha/GoogleRecaptchaTest.php +++ b/tests/phpMyFAQ/Captcha/GoogleRecaptchaTest.php @@ -16,11 +16,11 @@ namespace phpMyFAQ\Tests\Captcha; -use PHPUnit\Framework\TestCase; -use phpMyFAQ\Captcha\GoogleRecaptcha; use phpMyFAQ\Captcha\CaptchaInterface; +use phpMyFAQ\Captcha\GoogleRecaptcha; use phpMyFAQ\Configuration; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Class GoogleRecaptchaTest @@ -108,9 +108,7 @@ public function testCheckCaptchaCodeWhenNotLoggedIn(): void */ public function testFluentInterface(): void { - $result = $this->googleRecaptcha - ->setUserIsLoggedIn(true) - ->setUserIsLoggedIn(false); + $result = $this->googleRecaptcha->setUserIsLoggedIn(true)->setUserIsLoggedIn(false); $this->assertInstanceOf(GoogleRecaptcha::class, $result); $this->assertFalse($this->googleRecaptcha->isUserIsLoggedIn()); @@ -199,7 +197,7 @@ public function testAuthenticationBypassSecurity(): void 'javascript:alert(1)', '../../etc/passwd', 'SELECT * FROM users', - str_repeat('A', 10000) // Very long string + str_repeat('A', 10000), // Very long string ]; foreach ($maliciousInputs as $input) { diff --git a/tests/phpMyFAQ/Captcha/Helper/BuiltinCaptchaAbstractHelperTest.php b/tests/phpMyFAQ/Captcha/Helper/BuiltinCaptchaAbstractHelperTest.php index 0c7834e541..f30c72120e 100644 --- a/tests/phpMyFAQ/Captcha/Helper/BuiltinCaptchaAbstractHelperTest.php +++ b/tests/phpMyFAQ/Captcha/Helper/BuiltinCaptchaAbstractHelperTest.php @@ -16,10 +16,10 @@ namespace phpMyFAQ\Captcha\Helper; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Captcha\BuiltinCaptcha; use phpMyFAQ\Configuration; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Class BuiltinCaptchaAbstractHelperTest @@ -60,16 +60,9 @@ public function testRenderCaptchaWhenEnabledAndNotAuthenticated(): void // Mock captcha behavior $this->captcha->captchaLength = 5; - $this->captcha - ->method('renderCaptchaImage') - ->willReturn('Captcha'); + $this->captcha->method('renderCaptchaImage')->willReturn('Captcha'); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'refresh-captcha', - 'Captcha Label', - false - ); + $result = $this->helper->renderCaptcha($this->captcha, 'refresh-captcha', 'Captcha Label', false); // Assertions $this->assertStringContainsString('Captcha Label', $result); @@ -94,12 +87,7 @@ public function testRenderCaptchaWhenDisabled(): void ->with('spam.enableCaptchaCode') ->willReturn(false); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'refresh-captcha', - 'Captcha Label', - false - ); + $result = $this->helper->renderCaptcha($this->captcha, 'refresh-captcha', 'Captcha Label', false); $this->assertEmpty($result); } @@ -114,12 +102,7 @@ public function testRenderCaptchaWhenAuthenticated(): void ->with('spam.enableCaptchaCode') ->willReturn(true); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'refresh-captcha', - 'Captcha Label', - true // User is authenticated - ); + $result = $this->helper->renderCaptcha($this->captcha, 'refresh-captcha', 'Captcha Label', true); // User is authenticated $this->assertEmpty($result); } @@ -135,9 +118,7 @@ public function testRenderCaptchaWithEmptyParameters(): void ->willReturn(true); $this->captcha->captchaLength = 6; - $this->captcha - ->method('renderCaptchaImage') - ->willReturn(''); + $this->captcha->method('renderCaptchaImage')->willReturn(''); $result = $this->helper->renderCaptcha($this->captcha); @@ -157,16 +138,9 @@ public function testRenderCaptchaHtmlStructure(): void ->willReturn(true); $this->captcha->captchaLength = 4; - $this->captcha - ->method('renderCaptchaImage') - ->willReturn(''); + $this->captcha->method('renderCaptchaImage')->willReturn(''); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'test-action', - 'Test Label', - false - ); + $result = $this->helper->renderCaptcha($this->captcha, 'test-action', 'Test Label', false); // Test Bootstrap grid classes $this->assertStringContainsString('col-md-3 col-sm-12 col-form-label', $result); @@ -196,15 +170,13 @@ public function testRenderCaptchaWithSpecialCharacters(): void ->willReturn(true); $this->captcha->captchaLength = 5; - $this->captcha - ->method('renderCaptchaImage') - ->willReturn(''); + $this->captcha->method('renderCaptchaImage')->willReturn(''); $result = $this->helper->renderCaptcha( $this->captcha, 'test-action&special=true', 'Label with ', - false + false, ); $this->assertStringContainsString('data-action="test-action&special=true"', $result); diff --git a/tests/phpMyFAQ/Captcha/Helper/CaptchaHelperTest.php b/tests/phpMyFAQ/Captcha/Helper/CaptchaHelperTest.php index 571f6edc5b..da1ebf00fc 100644 --- a/tests/phpMyFAQ/Captcha/Helper/CaptchaHelperTest.php +++ b/tests/phpMyFAQ/Captcha/Helper/CaptchaHelperTest.php @@ -6,8 +6,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Strings; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class CaptchaHelperTest extends TestCase diff --git a/tests/phpMyFAQ/Captcha/Helper/GoogleRecaptchaAbstractHelperTest.php b/tests/phpMyFAQ/Captcha/Helper/GoogleRecaptchaAbstractHelperTest.php index aea1d157de..321d71b29b 100644 --- a/tests/phpMyFAQ/Captcha/Helper/GoogleRecaptchaAbstractHelperTest.php +++ b/tests/phpMyFAQ/Captcha/Helper/GoogleRecaptchaAbstractHelperTest.php @@ -16,11 +16,11 @@ namespace phpMyFAQ\Tests\Captcha\Helper; -use PHPUnit\Framework\TestCase; -use phpMyFAQ\Captcha\Helper\GoogleRecaptchaAbstractHelper; use phpMyFAQ\Captcha\CaptchaInterface; +use phpMyFAQ\Captcha\Helper\GoogleRecaptchaAbstractHelper; use phpMyFAQ\Configuration; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Class GoogleRecaptchaAbstractHelperTest @@ -57,16 +57,11 @@ public function testRenderCaptchaWhenEnabledAndNotAuthenticated(): void $this->configuration ->method('get') ->willReturnMap([ - ['spam.enableCaptchaCode', true], - ['security.googleReCaptchaV2SiteKey', 'test-site-key-123'] + ['spam.enableCaptchaCode', true], + ['security.googleReCaptchaV2SiteKey', 'test-site-key-123'], ]); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'refresh-captcha', - 'Google reCAPTCHA', - false - ); + $result = $this->helper->renderCaptcha($this->captcha, 'refresh-captcha', 'Google reCAPTCHA', false); // Assertions for HTML structure $this->assertStringContainsString('Google reCAPTCHA', $result); @@ -91,12 +86,7 @@ public function testRenderCaptchaWhenDisabled(): void ->with('spam.enableCaptchaCode') ->willReturn(false); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'refresh-captcha', - 'Google reCAPTCHA', - false - ); + $result = $this->helper->renderCaptcha($this->captcha, 'refresh-captcha', 'Google reCAPTCHA', false); $this->assertEmpty($result); } @@ -111,12 +101,7 @@ public function testRenderCaptchaWhenAuthenticated(): void ->with('spam.enableCaptchaCode') ->willReturn(true); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'refresh-captcha', - 'Google reCAPTCHA', - true // User is authenticated - ); + $result = $this->helper->renderCaptcha($this->captcha, 'refresh-captcha', 'Google reCAPTCHA', true); // User is authenticated $this->assertEmpty($result); } @@ -129,8 +114,8 @@ public function testRenderCaptchaWithEmptyParameters(): void $this->configuration ->method('get') ->willReturnMap([ - ['spam.enableCaptchaCode', true], - ['security.googleReCaptchaV2SiteKey', ''] + ['spam.enableCaptchaCode', true], + ['security.googleReCaptchaV2SiteKey', ''], ]); $result = $this->helper->renderCaptcha($this->captcha); @@ -149,16 +134,11 @@ public function testRenderCaptchaWithSpecialCharactersSiteKey(): void $this->configuration ->method('get') ->willReturnMap([ - ['spam.enableCaptchaCode', true], - ['security.googleReCaptchaV2SiteKey', $specialSiteKey] + ['spam.enableCaptchaCode', true], + ['security.googleReCaptchaV2SiteKey', $specialSiteKey], ]); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'test-action', - 'Test Label', - false - ); + $result = $this->helper->renderCaptcha($this->captcha, 'test-action', 'Test Label', false); $this->assertStringContainsString($specialSiteKey, $result); $this->assertStringContainsString('data-sitekey="' . $specialSiteKey . '"', $result); @@ -172,16 +152,11 @@ public function testRenderCaptchaHtmlStructure(): void $this->configuration ->method('get') ->willReturnMap([ - ['spam.enableCaptchaCode', true], - ['security.googleReCaptchaV2SiteKey', 'valid-site-key'] + ['spam.enableCaptchaCode', true], + ['security.googleReCaptchaV2SiteKey', 'valid-site-key'], ]); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'test-action', - 'Test Label', - false - ); + $result = $this->helper->renderCaptcha($this->captcha, 'test-action', 'Test Label', false); // Test Bootstrap grid classes $this->assertStringContainsString('row mb-2', $result); @@ -207,16 +182,11 @@ public function testRenderCaptchaWithLongSiteKey(): void $this->configuration ->method('get') ->willReturnMap([ - ['spam.enableCaptchaCode', true], - ['security.googleReCaptchaV2SiteKey', $longSiteKey] + ['spam.enableCaptchaCode', true], + ['security.googleReCaptchaV2SiteKey', $longSiteKey], ]); - $result = $this->helper->renderCaptcha( - $this->captcha, - 'test-action', - 'Test Label', - false - ); + $result = $this->helper->renderCaptcha($this->captcha, 'test-action', 'Test Label', false); $this->assertStringContainsString($longSiteKey, $result); $this->assertStringContainsString('g-recaptcha', $result); @@ -230,8 +200,8 @@ public function testRenderCaptchaWithMultilingualLabels(): void $this->configuration ->method('get') ->willReturnMap([ - ['spam.enableCaptchaCode', true], - ['security.googleReCaptchaV2SiteKey', 'test-key'] + ['spam.enableCaptchaCode', true], + ['security.googleReCaptchaV2SiteKey', 'test-key'], ]); $multilingualLabels = [ @@ -240,16 +210,11 @@ public function testRenderCaptchaWithMultilingualLabels(): void 'Étiquette française', 'Etiqueta española', '中文标签', - 'Русская метка' + 'Русская метка', ]; foreach ($multilingualLabels as $label) { - $result = $this->helper->renderCaptcha( - $this->captcha, - 'test-action', - $label, - false - ); + $result = $this->helper->renderCaptcha($this->captcha, 'test-action', $label, false); $this->assertStringContainsString($label, $result); $this->assertStringContainsString('col-form-label', $result); @@ -264,8 +229,8 @@ public function testRenderCaptchaParametersIndependence(): void $this->configuration ->method('get') ->willReturnMap([ - ['spam.enableCaptchaCode', true], - ['security.googleReCaptchaV2SiteKey', 'test-key'] + ['spam.enableCaptchaCode', true], + ['security.googleReCaptchaV2SiteKey', 'test-key'], ]); // Test that action parameter doesn't affect output for Google reCAPTCHA diff --git a/tests/phpMyFAQ/Category/CategoryCacheTest.php b/tests/phpMyFAQ/Category/CategoryCacheTest.php index 7716ccd17d..866b506183 100644 --- a/tests/phpMyFAQ/Category/CategoryCacheTest.php +++ b/tests/phpMyFAQ/Category/CategoryCacheTest.php @@ -4,8 +4,8 @@ namespace phpMyFAQ\Category; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class CategoryCacheTest extends TestCase @@ -221,4 +221,3 @@ public function testAddChildWithReference(): void $this->assertSame('Modified', $children[1]['name']); } } - diff --git a/tests/phpMyFAQ/Category/CategoryPermissionContextTest.php b/tests/phpMyFAQ/Category/CategoryPermissionContextTest.php index 758abf6a08..512bceb7ab 100644 --- a/tests/phpMyFAQ/Category/CategoryPermissionContextTest.php +++ b/tests/phpMyFAQ/Category/CategoryPermissionContextTest.php @@ -4,8 +4,8 @@ namespace phpMyFAQ\Category; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class CategoryPermissionContextTest extends TestCase @@ -168,4 +168,3 @@ public function testOverwriteModerator(): void $this->assertSame(200, $context->getModeratorGroupId(1)); } } - diff --git a/tests/phpMyFAQ/Category/CategoryServiceTest.php b/tests/phpMyFAQ/Category/CategoryServiceTest.php index 53028a8817..af231fb9b0 100644 --- a/tests/phpMyFAQ/Category/CategoryServiceTest.php +++ b/tests/phpMyFAQ/Category/CategoryServiceTest.php @@ -4,10 +4,10 @@ namespace phpMyFAQ\Category; -use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\MockObject\MockObject; use phpMyFAQ\Entity\CategoryEntity; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class CategoryServiceTest extends TestCase @@ -28,7 +28,8 @@ public function testGetAllCategories(): void 2 => ['id' => 2, 'name' => 'Category 2'], ]; - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findAllCategories') ->with('en') ->willReturn($expected); @@ -41,7 +42,8 @@ public function testGetAllCategoryIds(): void { $expected = [1, 2, 3]; - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findAllCategoryIds') ->with('en') ->willReturn($expected); @@ -56,7 +58,8 @@ public function testGetCategoriesFromFaq(): void 1 => ['id' => 1, 'name' => 'Category 1'], ]; - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findCategoriesFromFaq') ->with(42, 'en') ->willReturn($expected); @@ -67,7 +70,8 @@ public function testGetCategoriesFromFaq(): void public function testGetCategoryIdFromFaq(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findCategoriesFromFaq') ->with(42, 'en') ->willReturn([ @@ -80,7 +84,8 @@ public function testGetCategoryIdFromFaq(): void public function testGetCategoryIdFromFaqReturnsZeroWhenEmpty(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findCategoriesFromFaq') ->with(42, 'en') ->willReturn([]); @@ -91,7 +96,8 @@ public function testGetCategoryIdFromFaqReturnsZeroWhenEmpty(): void public function testGetCategoryIdsFromFaq(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findCategoriesFromFaq') ->with(42, 'en') ->willReturn([ @@ -106,7 +112,8 @@ public function testGetCategoryIdsFromFaq(): void public function testGetCategoryIdFromName(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findCategoryIdByName') ->with('Test Category') ->willReturn(42); @@ -117,7 +124,8 @@ public function testGetCategoryIdFromName(): void public function testGetCategoryIdFromNameReturnsFalseWhenNotFound(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findCategoryIdByName') ->with('Non Existent') ->willReturn(null); @@ -131,7 +139,8 @@ public function testGetCategoryData(): void $entity = new CategoryEntity(); $entity->setId(42); - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findByIdAndLanguage') ->with(42, 'en') ->willReturn($entity); @@ -142,7 +151,8 @@ public function testGetCategoryData(): void public function testGetCategoryDataReturnsNewEntityWhenNotFound(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findByIdAndLanguage') ->with(42, 'en') ->willReturn(null); @@ -156,7 +166,8 @@ public function testCreate(): void { $entity = new CategoryEntity(); - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('create') ->with($entity) ->willReturn(42); @@ -169,7 +180,8 @@ public function testUpdate(): void { $entity = new CategoryEntity(); - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('update') ->with($entity) ->willReturn(true); @@ -180,7 +192,8 @@ public function testUpdate(): void public function testDelete(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('delete') ->with(42, 'en') ->willReturn(true); @@ -191,7 +204,8 @@ public function testDelete(): void public function testMoveOwnership(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('moveOwnership') ->with(10, 20) ->willReturn(true); @@ -202,7 +216,8 @@ public function testMoveOwnership(): void public function testHasLanguage(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('hasLanguage') ->with(42, 'de') ->willReturn(true); @@ -213,7 +228,8 @@ public function testHasLanguage(): void public function testUpdateParentCategory(): void { - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('updateParentCategory') ->with(42, 10) ->willReturn(true); @@ -224,8 +240,7 @@ public function testUpdateParentCategory(): void public function testUpdateParentCategoryReturnsFalseWhenSameId(): void { - $this->repository->expects($this->never()) - ->method('updateParentCategory'); + $this->repository->expects($this->never())->method('updateParentCategory'); $result = $this->service->updateParentCategory(42, 42); $this->assertFalse($result); @@ -238,7 +253,8 @@ public function testCheckIfCategoryExists(): void $entity->setLang('en'); $entity->setParentId(0); - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('countByNameLangParent') ->with('Test', 'en', 0) ->willReturn(1); @@ -251,7 +267,8 @@ public function testGetCategoryLanguagesTranslated(): void { $expected = ['en' => 'English Category', 'de' => 'Deutsche Kategorie']; - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('getCategoryLanguagesTranslated') ->with(42) ->willReturn($expected); @@ -267,7 +284,8 @@ public function testGetMissingCategories(): void ['id' => 2, 'lang' => 'fr'], ]; - $this->repository->expects($this->once()) + $this->repository + ->expects($this->once()) ->method('findMissingCategories') ->with('en') ->willReturn($expected); @@ -276,4 +294,3 @@ public function testGetMissingCategories(): void $this->assertSame($expected, $result); } } - diff --git a/tests/phpMyFAQ/Category/CategoryTreeFacadeTest.php b/tests/phpMyFAQ/Category/CategoryTreeFacadeTest.php index e2cbdd366d..6873d2c004 100644 --- a/tests/phpMyFAQ/Category/CategoryTreeFacadeTest.php +++ b/tests/phpMyFAQ/Category/CategoryTreeFacadeTest.php @@ -4,10 +4,10 @@ namespace phpMyFAQ\Category; -use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\MockObject\MockObject; use phpMyFAQ\Category\Tree\TreeBuilder; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class CategoryTreeFacadeTest extends TestCase @@ -32,7 +32,8 @@ public function testBuildLinearTree(): void ['id' => 2, 'indent' => 1], ]; - $this->treeBuilder->expects($this->once()) + $this->treeBuilder + ->expects($this->once()) ->method('buildLinearTree') ->with($categories, 0, 0) ->willReturn($expected); @@ -46,7 +47,8 @@ public function testBuildLinearTreeWithCustomParams(): void $categories = [1 => ['id' => 1]]; $expected = [['id' => 1, 'indent' => 2]]; - $this->treeBuilder->expects($this->once()) + $this->treeBuilder + ->expects($this->once()) ->method('buildLinearTree') ->with($categories, 5, 2) ->willReturn($expected); @@ -63,7 +65,8 @@ public function testBuildAdminCategoryTree(): void ]; $expected = [1 => [2 => []]]; - $this->treeBuilder->expects($this->once()) + $this->treeBuilder + ->expects($this->once()) ->method('buildAdminCategoryTree') ->with($categories, 0) ->willReturn($expected); @@ -77,7 +80,8 @@ public function testBuildAdminCategoryTreeWithParentId(): void $categories = [2 => ['id' => 2, 'parent_id' => 1]]; $expected = [2 => []]; - $this->treeBuilder->expects($this->once()) + $this->treeBuilder + ->expects($this->once()) ->method('buildAdminCategoryTree') ->with($categories, 1) ->willReturn($expected); @@ -93,7 +97,8 @@ public function testGetChildren(): void ]; $expected = [1, 2]; - $this->treeBuilder->expects($this->once()) + $this->treeBuilder + ->expects($this->once()) ->method('getChildren') ->with($childrenMap, 0) ->willReturn($expected); @@ -110,7 +115,8 @@ public function testGetChildNodes(): void ]; $expected = [1, 2, 3]; - $this->treeBuilder->expects($this->once()) + $this->treeBuilder + ->expects($this->once()) ->method('getChildNodes') ->with($childrenMap, 0) ->willReturn($expected); @@ -127,7 +133,8 @@ public function testGetLevelOf(): void 3 => ['id' => 3, 'parent_id' => 2], ]; - $this->treeBuilder->expects($this->once()) + $this->treeBuilder + ->expects($this->once()) ->method('computeLevel') ->with($categoryNames, 3) ->willReturn(2); @@ -145,7 +152,8 @@ public function testGetNodes(): void ]; $expected = [1, 2, 3]; - $this->treeBuilder->expects($this->once()) + $this->treeBuilder + ->expects($this->once()) ->method('getNodes') ->with($categoryNames, 3) ->willReturn($expected); @@ -165,4 +173,3 @@ public function testConstructorWithoutTreeBuilder(): void $this->assertIsInt($result); } } - diff --git a/tests/phpMyFAQ/Category/ImageTest.php b/tests/phpMyFAQ/Category/ImageTest.php index 067b504f2c..b6243a3654 100644 --- a/tests/phpMyFAQ/Category/ImageTest.php +++ b/tests/phpMyFAQ/Category/ImageTest.php @@ -2,11 +2,11 @@ namespace phpMyFAQ\Category; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; use phpMyFAQ\Core\Exception; -use Symfony\Component\HttpFoundation\File\UploadedFile; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\File\UploadedFile; /** * Class ImageTest @@ -45,9 +45,7 @@ public function testConstructor(): void public function testSetUploadedFileWithValidFile(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->once()) - ->method('isValid') - ->willReturn(true); + $uploadedFileMock->expects($this->once())->method('isValid')->willReturn(true); $result = $this->image->setUploadedFile($uploadedFileMock); @@ -58,9 +56,7 @@ public function testSetUploadedFileWithValidFile(): void public function testSetUploadedFileWithInvalidFile(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->once()) - ->method('isValid') - ->willReturn(false); + $uploadedFileMock->expects($this->once())->method('isValid')->willReturn(false); $result = $this->image->setUploadedFile($uploadedFileMock); @@ -79,12 +75,8 @@ public function testSetFileName(): void public function testGetFileNameWithUploadJpeg(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->once()) - ->method('isValid') - ->willReturn(true); - $uploadedFileMock->expects($this->once()) - ->method('getMimeType') - ->willReturn('image/jpeg'); + $uploadedFileMock->expects($this->once())->method('isValid')->willReturn(true); + $uploadedFileMock->expects($this->once())->method('getMimeType')->willReturn('image/jpeg'); $this->image->setUploadedFile($uploadedFileMock); $fileName = $this->image->getFileName(123, 'test-category'); @@ -95,12 +87,8 @@ public function testGetFileNameWithUploadJpeg(): void public function testGetFileNameWithUploadPng(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->once()) - ->method('isValid') - ->willReturn(true); - $uploadedFileMock->expects($this->once()) - ->method('getMimeType') - ->willReturn('image/png'); + $uploadedFileMock->expects($this->once())->method('isValid')->willReturn(true); + $uploadedFileMock->expects($this->once())->method('getMimeType')->willReturn('image/png'); $this->image->setUploadedFile($uploadedFileMock); $fileName = $this->image->getFileName(123, 'test-category'); @@ -111,12 +99,8 @@ public function testGetFileNameWithUploadPng(): void public function testGetFileNameWithUploadGif(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->once()) - ->method('isValid') - ->willReturn(true); - $uploadedFileMock->expects($this->once()) - ->method('getMimeType') - ->willReturn('image/gif'); + $uploadedFileMock->expects($this->once())->method('isValid')->willReturn(true); + $uploadedFileMock->expects($this->once())->method('getMimeType')->willReturn('image/gif'); $this->image->setUploadedFile($uploadedFileMock); $fileName = $this->image->getFileName(123, 'test-category'); @@ -127,12 +111,8 @@ public function testGetFileNameWithUploadGif(): void public function testGetFileNameWithUploadWebp(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->once()) - ->method('isValid') - ->willReturn(true); - $uploadedFileMock->expects($this->once()) - ->method('getMimeType') - ->willReturn('image/webp'); + $uploadedFileMock->expects($this->once())->method('isValid')->willReturn(true); + $uploadedFileMock->expects($this->once())->method('getMimeType')->willReturn('image/webp'); $this->image->setUploadedFile($uploadedFileMock); $fileName = $this->image->getFileName(123, 'test-category'); @@ -143,12 +123,8 @@ public function testGetFileNameWithUploadWebp(): void public function testGetFileNameWithUploadUnknownMimeType(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->once()) - ->method('isValid') - ->willReturn(true); - $uploadedFileMock->expects($this->once()) - ->method('getMimeType') - ->willReturn('unknown/mime'); + $uploadedFileMock->expects($this->once())->method('isValid')->willReturn(true); + $uploadedFileMock->expects($this->once())->method('getMimeType')->willReturn('unknown/mime'); $this->image->setUploadedFile($uploadedFileMock); $fileName = $this->image->getFileName(123, 'test-category'); @@ -169,20 +145,13 @@ public function testUploadValidImage(): void file_put_contents($tempFile, 'fake image content'); $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->atLeastOnce()) - ->method('isValid') - ->willReturn(true); - $uploadedFileMock->expects($this->atLeastOnce()) - ->method('getSize') - ->willReturn(1024); // 1 KB - $uploadedFileMock->expects($this->once()) - ->method('getClientMimeType') - ->willReturn('image/jpeg'); - $uploadedFileMock->expects($this->once()) - ->method('move') - ->willReturnSelf(); // Return UploadedFile instance instead of bool - - $this->configurationMock->expects($this->once()) + $uploadedFileMock->expects($this->atLeastOnce())->method('isValid')->willReturn(true); + $uploadedFileMock->expects($this->atLeastOnce())->method('getSize')->willReturn(1024); // 1 KB + $uploadedFileMock->expects($this->once())->method('getClientMimeType')->willReturn('image/jpeg'); + $uploadedFileMock->expects($this->once())->method('move')->willReturnSelf(); // Return UploadedFile instance instead of bool + + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('records.maxAttachmentSize') ->willReturn(2048); // 2KB limit @@ -203,9 +172,7 @@ public function testUploadValidImage(): void public function testUploadWithInvalidFile(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->atLeastOnce()) - ->method('isValid') - ->willReturn(false); + $uploadedFileMock->expects($this->atLeastOnce())->method('isValid')->willReturn(false); $this->image->setUploadedFile($uploadedFileMock); @@ -218,14 +185,11 @@ public function testUploadWithInvalidFile(): void public function testUploadWithFileTooLarge(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->atLeastOnce()) - ->method('isValid') - ->willReturn(true); - $uploadedFileMock->expects($this->once()) - ->method('getSize') - ->willReturn(3072); // 3KB - - $this->configurationMock->expects($this->once()) + $uploadedFileMock->expects($this->atLeastOnce())->method('isValid')->willReturn(true); + $uploadedFileMock->expects($this->once())->method('getSize')->willReturn(3072); // 3KB + + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('records.maxAttachmentSize') ->willReturn(2048); // 2KB limit @@ -241,14 +205,11 @@ public function testUploadWithFileTooLarge(): void public function testUploadWithUndetectableSize(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->atLeastOnce()) - ->method('isValid') - ->willReturn(true); - $uploadedFileMock->expects($this->atLeastOnce()) - ->method('getSize') - ->willReturn(false); - - $this->configurationMock->expects($this->once()) + $uploadedFileMock->expects($this->atLeastOnce())->method('isValid')->willReturn(true); + $uploadedFileMock->expects($this->atLeastOnce())->method('getSize')->willReturn(false); + + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('records.maxAttachmentSize') ->willReturn(2048); @@ -264,17 +225,12 @@ public function testUploadWithUndetectableSize(): void public function testUploadWithInvalidMimeType(): void { $uploadedFileMock = $this->createMock(UploadedFile::class); - $uploadedFileMock->expects($this->atLeastOnce()) - ->method('isValid') - ->willReturn(true); - $uploadedFileMock->expects($this->atLeastOnce()) - ->method('getSize') - ->willReturn(1024); - $uploadedFileMock->expects($this->once()) - ->method('getClientMimeType') - ->willReturn('text/plain'); - - $this->configurationMock->expects($this->once()) + $uploadedFileMock->expects($this->atLeastOnce())->method('isValid')->willReturn(true); + $uploadedFileMock->expects($this->atLeastOnce())->method('getSize')->willReturn(1024); + $uploadedFileMock->expects($this->once())->method('getClientMimeType')->willReturn('text/plain'); + + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('records.maxAttachmentSize') ->willReturn(2048); diff --git a/tests/phpMyFAQ/Category/Language/CategoryLanguageServiceTest.php b/tests/phpMyFAQ/Category/Language/CategoryLanguageServiceTest.php index 1b0227c4f0..959d8cc1b7 100644 --- a/tests/phpMyFAQ/Category/Language/CategoryLanguageServiceTest.php +++ b/tests/phpMyFAQ/Category/Language/CategoryLanguageServiceTest.php @@ -6,9 +6,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Language as PmfLanguage; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] final class CategoryLanguageServiceTest extends TestCase @@ -22,7 +22,8 @@ protected function setUp(): void { parent::setUp(); $this->configuration = $this->createStub(Configuration::class); - $this->language = $this->getMockBuilder(PmfLanguage::class) + $this->language = $this + ->getMockBuilder(PmfLanguage::class) ->disableOriginalConstructor() ->onlyMethods(['isLanguageAvailable']) ->getMock(); @@ -31,7 +32,10 @@ protected function setUp(): void public function testGetLanguagesInUseReturnsCodes(): void { - $this->language->method('isLanguageAvailable')->with(0, 'faqcategories')->willReturn(['en', 'de']); + $this->language + ->method('isLanguageAvailable') + ->with(0, 'faqcategories') + ->willReturn(['en', 'de']); $service = new CategoryLanguageService(); $result = $service->getLanguagesInUse($this->configuration); @@ -42,7 +46,10 @@ public function testGetLanguagesInUseReturnsCodes(): void public function testGetExistingTranslationsKeysMatchExisting(): void { - $this->language->method('isLanguageAvailable')->with(123, 'faqcategories')->willReturn(['en', 'de']); + $this->language + ->method('isLanguageAvailable') + ->with(123, 'faqcategories') + ->willReturn(['en', 'de']); $service = new CategoryLanguageService(); $result = $service->getExistingTranslations($this->configuration, 123); @@ -53,7 +60,10 @@ public function testGetExistingTranslationsKeysMatchExisting(): void public function testGetLanguagesToTranslateExcludesExisting(): void { - $this->language->method('isLanguageAvailable')->with(456, 'faqcategories')->willReturn(['en']); + $this->language + ->method('isLanguageAvailable') + ->with(456, 'faqcategories') + ->willReturn(['en']); $service = new CategoryLanguageService(); $result = $service->getLanguagesToTranslate($this->configuration, 456); @@ -62,4 +72,3 @@ public function testGetLanguagesToTranslateExcludesExisting(): void $this->assertIsArray($result); } } - diff --git a/tests/phpMyFAQ/Category/Navigation/BreadcrumbsBuilderTest.php b/tests/phpMyFAQ/Category/Navigation/BreadcrumbsBuilderTest.php index 1635db8d3b..93dd0b4e8a 100644 --- a/tests/phpMyFAQ/Category/Navigation/BreadcrumbsBuilderTest.php +++ b/tests/phpMyFAQ/Category/Navigation/BreadcrumbsBuilderTest.php @@ -4,8 +4,8 @@ namespace phpMyFAQ\Category\Navigation; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class BreadcrumbsBuilderTest extends TestCase @@ -208,7 +208,7 @@ public function testBuildFromIdsWithStartpageWithCustomDescription(): void $ids, 'Home', 'Welcome to FAQ', - 'All categories' + 'All categories', ); $this->assertCount(2, $result); // startpage + all categories diff --git a/tests/phpMyFAQ/Category/Navigation/CategoryTreeNavigatorTest.php b/tests/phpMyFAQ/Category/Navigation/CategoryTreeNavigatorTest.php index 94c9b4aab8..3700e19ed5 100644 --- a/tests/phpMyFAQ/Category/Navigation/CategoryTreeNavigatorTest.php +++ b/tests/phpMyFAQ/Category/Navigation/CategoryTreeNavigatorTest.php @@ -4,9 +4,9 @@ namespace phpMyFAQ\Category\Navigation; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Category\CategoryCache; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class CategoryTreeNavigatorTest extends TestCase @@ -98,8 +98,20 @@ public function testExpandSetsCorrectSymbol(): void public function testCollapseAllChangesAllMinusToPlus(): void { // Arrange - $this->cache->addTreeTabEntry(['id' => 1, 'symbol' => 'minus', 'name' => 'A', 'level' => 0, 'numChildren' => 1]); - $this->cache->addTreeTabEntry(['id' => 2, 'symbol' => 'minus', 'name' => 'B', 'level' => 1, 'numChildren' => 0]); + $this->cache->addTreeTabEntry([ + 'id' => 1, + 'symbol' => 'minus', + 'name' => 'A', + 'level' => 0, + 'numChildren' => 1, + ]); + $this->cache->addTreeTabEntry([ + 'id' => 2, + 'symbol' => 'minus', + 'name' => 'B', + 'level' => 1, + 'numChildren' => 0, + ]); $this->cache->addTreeTabEntry(['id' => 3, 'symbol' => 'plus', 'name' => 'C', 'level' => 0, 'numChildren' => 1]); // Act @@ -134,9 +146,27 @@ public function testExpandToExpandsPathToNode(): void ]); // Add tree tab entries - $this->cache->addTreeTabEntry(['id' => 1, 'symbol' => 'plus', 'name' => 'Root', 'level' => 0, 'numChildren' => 1]); - $this->cache->addTreeTabEntry(['id' => 2, 'symbol' => 'plus', 'name' => 'Middle', 'level' => 1, 'numChildren' => 1]); - $this->cache->addTreeTabEntry(['id' => 3, 'symbol' => 'angle', 'name' => 'Leaf', 'level' => 2, 'numChildren' => 0]); + $this->cache->addTreeTabEntry([ + 'id' => 1, + 'symbol' => 'plus', + 'name' => 'Root', + 'level' => 0, + 'numChildren' => 1, + ]); + $this->cache->addTreeTabEntry([ + 'id' => 2, + 'symbol' => 'plus', + 'name' => 'Middle', + 'level' => 1, + 'numChildren' => 1, + ]); + $this->cache->addTreeTabEntry([ + 'id' => 3, + 'symbol' => 'angle', + 'name' => 'Leaf', + 'level' => 2, + 'numChildren' => 0, + ]); // Act $this->navigator->expandTo($this->cache, 3); diff --git a/tests/phpMyFAQ/Category/OrderTest.php b/tests/phpMyFAQ/Category/OrderTest.php index 6527223bc4..b9edca6af4 100644 --- a/tests/phpMyFAQ/Category/OrderTest.php +++ b/tests/phpMyFAQ/Category/OrderTest.php @@ -2,11 +2,11 @@ namespace phpMyFAQ\Category; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; use phpMyFAQ\Database\DatabaseDriver; -use stdClass; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; +use stdClass; /** * Class OrderTest @@ -23,7 +23,8 @@ protected function setUp(): void $this->databaseMock = $this->createMock(DatabaseDriver::class); $this->configurationMock = $this->createMock(Configuration::class); - $this->configurationMock->expects($this->any()) + $this->configurationMock + ->expects($this->any()) ->method('getDb') ->willReturn($this->databaseMock); @@ -41,12 +42,14 @@ public function testAddSuccess(): void $parentId = 456; $nextId = 10; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('nextId') ->with('faqcategory_order', 'position') ->willReturn($nextId); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('INSERT INTO faqcategory_order')) ->willReturn(true); @@ -61,12 +64,14 @@ public function testAddFailure(): void $parentId = 456; $nextId = 10; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('nextId') ->with('faqcategory_order', 'position') ->willReturn($nextId); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn(false); @@ -78,7 +83,8 @@ public function testRemoveSuccess(): void { $categoryId = 123; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('DELETE FROM faqcategory_order WHERE category_id = 123')) ->willReturn(true); @@ -91,7 +97,8 @@ public function testRemoveFailure(): void { $categoryId = 123; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn(false); @@ -101,7 +108,8 @@ public function testRemoveFailure(): void public function testSetCategoryTreeWithEmptyTree(): void { - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('DELETE FROM faqcategory_order')); @@ -121,8 +129,7 @@ public function testSetCategoryTreeWithSimpleTree(): void $categoryTree = [$category1, $category2]; // Expect DELETE query first, then INSERT queries - $this->databaseMock->expects($this->exactly(3)) - ->method('query'); + $this->databaseMock->expects($this->exactly(3))->method('query'); $this->order->setCategoryTree($categoryTree); } @@ -140,8 +147,7 @@ public function testSetCategoryTreeWithNestedTree(): void $categoryTree = [$parentCategory]; // Expect DELETE query first, then INSERT queries for parent and child - $this->databaseMock->expects($this->exactly(3)) - ->method('query'); + $this->databaseMock->expects($this->exactly(3))->method('query'); $this->order->setCategoryTree($categoryTree); } @@ -155,7 +161,8 @@ public function testSetCategoryTreeWithInvalidId(): void $categoryTree = [$category]; // Only expect DELETE query, no INSERT because ID is invalid - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('DELETE FROM faqcategory_order')); @@ -322,12 +329,16 @@ public function testGetParentIdDeepNesting(): void public function testGetAllCategoriesEmpty(): void { - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') - ->with($this->stringContains('SELECT category_id, parent_id, position FROM faqcategory_order ORDER BY position')) + ->with($this->stringContains( + 'SELECT category_id, parent_id, position FROM faqcategory_order ORDER BY position', + )) ->willReturn('mock_result'); - $this->databaseMock->expects($this->exactly(1)) + $this->databaseMock + ->expects($this->exactly(1)) ->method('fetchArray') ->with('mock_result') ->willReturn(false); @@ -343,18 +354,16 @@ public function testGetAllCategoriesWithData(): void ['category_id' => '2', 'parent_id' => '1', 'position' => '2'], ]; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn('mock_result'); - $this->databaseMock->expects($this->exactly(3)) + $this->databaseMock + ->expects($this->exactly(3)) ->method('fetchArray') ->with('mock_result') - ->willReturnOnConsecutiveCalls( - $mockData[0], - $mockData[1], - false - ); + ->willReturnOnConsecutiveCalls($mockData[0], $mockData[1], false); $result = $this->order->getAllCategories(); $this->assertEquals($mockData, $result); diff --git a/tests/phpMyFAQ/Category/Permission/CategoryPermissionServiceTest.php b/tests/phpMyFAQ/Category/Permission/CategoryPermissionServiceTest.php index fb7cf42494..8276bd8694 100644 --- a/tests/phpMyFAQ/Category/Permission/CategoryPermissionServiceTest.php +++ b/tests/phpMyFAQ/Category/Permission/CategoryPermissionServiceTest.php @@ -4,8 +4,8 @@ namespace phpMyFAQ\Category\Permission; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class CategoryPermissionServiceTest extends TestCase @@ -15,7 +15,10 @@ public function testBuildWhereClauseWithInactiveFalseAddsActiveFilter(): void $service = new CategoryPermissionService(); $sql = $service->buildWhereClause([1, 2], 42); - $this->assertStringStartsWith('WHERE ( fg.group_id IN (1, 2) OR (fu.user_id = 42 AND fg.group_id IN (1, 2)))', $sql); + $this->assertStringStartsWith( + 'WHERE ( fg.group_id IN (1, 2) OR (fu.user_id = 42 AND fg.group_id IN (1, 2)))', + $sql, + ); $this->assertStringContainsString('AND fc.active = 1', $sql); } @@ -24,9 +27,6 @@ public function testBuildWhereClauseWithInactiveTrueOmitsActiveFilter(): void $service = new CategoryPermissionService(); $sql = $service->buildWhereClauseWithInactive([], 0); - $this->assertSame( - "WHERE ( fg.group_id IN (-1) OR (fu.user_id = 0 AND fg.group_id IN (-1))) ", - $sql, - ); + $this->assertSame('WHERE ( fg.group_id IN (-1) OR (fu.user_id = 0 AND fg.group_id IN (-1))) ', $sql); } } diff --git a/tests/phpMyFAQ/Category/PermissionTest.php b/tests/phpMyFAQ/Category/PermissionTest.php index 794884e686..acad3fa065 100644 --- a/tests/phpMyFAQ/Category/PermissionTest.php +++ b/tests/phpMyFAQ/Category/PermissionTest.php @@ -2,12 +2,12 @@ namespace phpMyFAQ\Category; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; use phpMyFAQ\Database; use phpMyFAQ\Database\DatabaseDriver; -use stdClass; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; +use stdClass; /** * Class PermissionTest @@ -24,7 +24,8 @@ protected function setUp(): void $this->databaseMock = $this->createMock(DatabaseDriver::class); $this->configurationMock = $this->createMock(Configuration::class); - $this->configurationMock->expects($this->any()) + $this->configurationMock + ->expects($this->any()) ->method('getDb') ->willReturn($this->databaseMock); @@ -50,12 +51,14 @@ public function testAddWithUserMode(): void $userIds = [10, 20]; // Mock existance check (returns 0 rows = not exists) - $this->databaseMock->expects($this->exactly(4)) + $this->databaseMock + ->expects($this->exactly(4)) ->method('numRows') ->willReturn(0); // Mock queries for existence checks and inserts - $this->databaseMock->expects($this->exactly(8)) + $this->databaseMock + ->expects($this->exactly(8)) ->method('query') ->willReturn(true); @@ -70,11 +73,13 @@ public function testAddWithGroupMode(): void $categories = [1]; $groupIds = [5]; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(0); - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('query') ->willReturn(true); @@ -96,12 +101,14 @@ public function testAddWithExistingPermission(): void $userIds = [10]; // Mock existence check (returns 1 row = already exists) - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(1); // Only expect one query for the existence check, no insert - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn(true); @@ -115,7 +122,8 @@ public function testDeleteWithUserMode(): void $categories = [1, 2]; - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('query') ->with($this->stringContains('faqcategory_user')) ->willReturn(true); @@ -130,7 +138,8 @@ public function testDeleteWithGroupMode(): void $categories = [1]; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('DELETE FROM')) ->willReturn(true); @@ -153,7 +162,8 @@ public function testIsRestrictedWithUserPermissions(): void // Mock for user permissions query $userResult = 'user_result'; - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('query') ->willReturnOnConsecutiveCalls($userResult, 'group_result'); @@ -161,7 +171,8 @@ public function testIsRestrictedWithUserPermissions(): void $userRow = new stdClass(); $userRow->permission = 10; - $this->databaseMock->expects($this->any()) + $this->databaseMock + ->expects($this->any()) ->method('fetchObject') ->willReturnOnConsecutiveCalls($userRow, false, false); @@ -176,7 +187,8 @@ public function testIsRestrictedWithGroupPermissions(): void $categoryId = 1; // Mock for both queries - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('query') ->willReturnOnConsecutiveCalls('user_result', 'group_result'); @@ -184,7 +196,8 @@ public function testIsRestrictedWithGroupPermissions(): void $groupRow = new stdClass(); $groupRow->permission = 5; - $this->databaseMock->expects($this->any()) + $this->databaseMock + ->expects($this->any()) ->method('fetchObject') ->willReturnOnConsecutiveCalls(false, $groupRow, false); @@ -198,12 +211,14 @@ public function testIsRestrictedWithNoPermissions(): void $categoryId = 1; - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('query') ->willReturnOnConsecutiveCalls('user_result', 'group_result'); // Mock no permissions for both user and group - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('fetchObject') ->willReturn(false); @@ -218,7 +233,8 @@ public function testGetWithUserMode(): void $categories = [1, 2]; $result_mock = 'query_result'; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('SELECT user_id AS permission FROM test_faqcategory_user')) ->willReturn($result_mock); @@ -228,7 +244,8 @@ public function testGetWithUserMode(): void $row2 = new stdClass(); $row2->permission = 20; - $this->databaseMock->expects($this->exactly(3)) + $this->databaseMock + ->expects($this->exactly(3)) ->method('fetchObject') ->with($result_mock) ->willReturnOnConsecutiveCalls($row1, $row2, false); @@ -244,7 +261,8 @@ public function testGetWithGroupMode(): void $categories = [1]; $result_mock = 'query_result'; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('SELECT group_id AS permission FROM test_faqcategory_group')) ->willReturn($result_mock); @@ -252,7 +270,8 @@ public function testGetWithGroupMode(): void $row = new stdClass(); $row->permission = 5; - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('fetchObject') ->willReturnOnConsecutiveCalls($row, false); @@ -272,11 +291,13 @@ public function testGetWithEmptyResult(): void $categories = [1]; - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('fetchObject') ->willReturn(false); @@ -291,7 +312,8 @@ public function testGetAll(): void $categories = [1, 2]; // Mock two queries (user and group) - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('query') ->willReturnOnConsecutiveCalls('user_result', 'group_result'); @@ -309,11 +331,15 @@ public function testGetAll(): void $groupRow->category_id = 1; $groupRow->permission = 5; - $this->databaseMock->expects($this->exactly(5)) + $this->databaseMock + ->expects($this->exactly(5)) ->method('fetchObject') ->willReturnOnConsecutiveCalls( - $userRow1, $userRow2, false, // User query results - $groupRow, false // Group query results + $userRow1, + $userRow2, + false, // User query results + $groupRow, + false, // Group query results ); $result = $this->permission->getAll($categories); @@ -344,12 +370,14 @@ public function testGetAllWithNoPermissions(): void $categories = [1]; - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('query') ->willReturnOnConsecutiveCalls('user_result', 'group_result'); // Mock no results for both queries - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('fetchObject') ->willReturn(false); diff --git a/tests/phpMyFAQ/Category/RelationTest.php b/tests/phpMyFAQ/Category/RelationTest.php index b266256aee..e0fb06a777 100644 --- a/tests/phpMyFAQ/Category/RelationTest.php +++ b/tests/phpMyFAQ/Category/RelationTest.php @@ -2,14 +2,14 @@ namespace phpMyFAQ\Category; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Category; use phpMyFAQ\Configuration; use phpMyFAQ\Database; use phpMyFAQ\Database\DatabaseDriver; use phpMyFAQ\Language; -use stdClass; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; +use stdClass; /** * Class RelationTest @@ -30,11 +30,13 @@ protected function setUp(): void $this->categoryMock = $this->createMock(Category::class); $this->languageMock = $this->createMock(Language::class); - $this->configurationMock->expects($this->any()) + $this->configurationMock + ->expects($this->any()) ->method('getDb') ->willReturn($this->databaseMock); - $this->configurationMock->expects($this->any()) + $this->configurationMock + ->expects($this->any()) ->method('getLanguage') ->willReturn($this->languageMock); @@ -68,12 +70,14 @@ public function testGetCategoryFaqsMatrixEmpty(): void { Database::setTablePrefix('test_'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('SELECT')) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->with('query_result') ->willReturn(0); @@ -86,11 +90,13 @@ public function testGetCategoryFaqsMatrixWithData(): void { Database::setTablePrefix('test_'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(2); @@ -102,7 +108,8 @@ public function testGetCategoryFaqsMatrixWithData(): void $row2->id_cat = 2; $row2->id = 20; - $this->databaseMock->expects($this->exactly(3)) + $this->databaseMock + ->expects($this->exactly(3)) ->method('fetchObject') ->willReturnOnConsecutiveCalls($row1, $row2, false); @@ -110,7 +117,7 @@ public function testGetCategoryFaqsMatrixWithData(): void $expected = [ 1 => [10 => true], - 2 => [20 => true] + 2 => [20 => true], ]; $this->assertEquals($expected, $result); @@ -120,11 +127,13 @@ public function testGetCategoryFaqsMatrixMultipleFaqsPerCategory(): void { Database::setTablePrefix('test_'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(3); @@ -140,7 +149,8 @@ public function testGetCategoryFaqsMatrixMultipleFaqsPerCategory(): void $row3->id_cat = 2; $row3->id = 20; - $this->databaseMock->expects($this->exactly(4)) + $this->databaseMock + ->expects($this->exactly(4)) ->method('fetchObject') ->willReturnOnConsecutiveCalls($row1, $row2, $row3, false); @@ -148,7 +158,7 @@ public function testGetCategoryFaqsMatrixMultipleFaqsPerCategory(): void $expected = [ 1 => [10 => true, 11 => true], - 2 => [20 => true] + 2 => [20 => true], ]; $this->assertEquals($expected, $result); @@ -158,21 +168,25 @@ public function testGetCategoryWithFaqsBasicPermissions(): void { Database::setTablePrefix('test_'); - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.permLevel') ->willReturn('basic'); - $this->languageMock->expects($this->any()) + $this->languageMock + ->expects($this->any()) ->method('getLanguage') ->willReturn('en'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains("fd.lang = 'en'")) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(1); @@ -183,7 +197,8 @@ public function testGetCategoryWithFaqsBasicPermissions(): void $categoryRow->description = 'Test Description'; $categoryRow->number = 5; - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('fetchObject') ->willReturnOnConsecutiveCalls($categoryRow, false); @@ -195,8 +210,8 @@ public function testGetCategoryWithFaqsBasicPermissions(): void 'parent_id' => 0, 'name' => 'Test Category', 'description' => 'Test Description', - 'faqs' => 5 - ] + 'faqs' => 5, + ], ]; $this->assertEquals($expected, $result); @@ -206,29 +221,35 @@ public function testGetCategoryWithFaqsAdvancedPermissionsWithUser(): void { Database::setTablePrefix('test_'); - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.permLevel') ->willReturn('medium'); - $this->categoryMock->expects($this->any()) + $this->categoryMock + ->expects($this->any()) ->method('getUser') ->willReturn(42); - $this->categoryMock->expects($this->any()) + $this->categoryMock + ->expects($this->any()) ->method('getGroups') ->willReturn([1, 2, 3]); - $this->languageMock->expects($this->any()) + $this->languageMock + ->expects($this->any()) ->method('getLanguage') ->willReturn('de'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('fdu.user_id = 42')) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(0); @@ -240,29 +261,35 @@ public function testGetCategoryWithFaqsAdvancedPermissionsGuestUser(): void { Database::setTablePrefix('test_'); - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.permLevel') ->willReturn('medium'); - $this->categoryMock->expects($this->any()) + $this->categoryMock + ->expects($this->any()) ->method('getUser') ->willReturn(-1); - $this->categoryMock->expects($this->any()) + $this->categoryMock + ->expects($this->any()) ->method('getGroups') ->willReturn([1, 2]); - $this->languageMock->expects($this->any()) + $this->languageMock + ->expects($this->any()) ->method('getLanguage') ->willReturn(''); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('fdg.group_id IN (1, 2)')) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(0); @@ -274,16 +301,19 @@ public function testGetNumberOfFaqsPerCategoryWithoutRestriction(): void { Database::setTablePrefix('test_'); - $this->languageMock->expects($this->any()) + $this->languageMock + ->expects($this->any()) ->method('getLanguage') ->willReturn('en'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('COUNT(fcr.record_id) AS number')) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(1); @@ -291,7 +321,8 @@ public function testGetNumberOfFaqsPerCategoryWithoutRestriction(): void $row->category_id = 1; $row->number = 10; - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('fetchObject') ->willReturnOnConsecutiveCalls($row, false); @@ -308,16 +339,19 @@ public function testGetNumberOfFaqsPerCategoryWithRestriction(): void // Set groups first $this->relation->setGroups([5]); - $this->languageMock->expects($this->any()) + $this->languageMock + ->expects($this->any()) ->method('getLanguage') ->willReturn('de'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('fdg.group_id = 5')) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(2); @@ -329,7 +363,8 @@ public function testGetNumberOfFaqsPerCategoryWithRestriction(): void $row2->category_id = 2; $row2->number = 8; - $this->databaseMock->expects($this->exactly(3)) + $this->databaseMock + ->expects($this->exactly(3)) ->method('fetchObject') ->willReturnOnConsecutiveCalls($row1, $row2, false); @@ -343,16 +378,19 @@ public function testGetNumberOfFaqsPerCategoryOnlyActive(): void { Database::setTablePrefix('test_'); - $this->languageMock->expects($this->once()) + $this->languageMock + ->expects($this->once()) ->method('getLanguage') ->willReturn(''); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains("AND fd.active = 'yes'")) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(0); @@ -367,19 +405,22 @@ public function testGetNumberOfFaqsPerCategoryWithRestrictionAndOnlyActive(): vo // Set groups first $this->relation->setGroups([3]); - $this->languageMock->expects($this->any()) + $this->languageMock + ->expects($this->any()) ->method('getLanguage') ->willReturn('fr'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->logicalAnd( $this->stringContains('fdg.group_id = 3'), - $this->stringContains("fd.active = 'yes'") + $this->stringContains("fd.active = 'yes'"), )) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(0); @@ -391,16 +432,19 @@ public function testGetNumberOfFaqsPerCategoryEmptyLanguage(): void { Database::setTablePrefix('test_'); - $this->languageMock->expects($this->any()) + $this->languageMock + ->expects($this->any()) ->method('getLanguage') ->willReturn(''); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('COUNT(fcr.record_id) AS number')) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('numRows') ->willReturn(0); diff --git a/tests/phpMyFAQ/Category/StartpageTest.php b/tests/phpMyFAQ/Category/StartpageTest.php index 1de0fca16a..1db6d23f83 100644 --- a/tests/phpMyFAQ/Category/StartpageTest.php +++ b/tests/phpMyFAQ/Category/StartpageTest.php @@ -2,11 +2,11 @@ namespace phpMyFAQ\Category; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; use phpMyFAQ\Database; use phpMyFAQ\Database\DatabaseDriver; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Class StartpageTest @@ -23,7 +23,8 @@ protected function setUp(): void $this->databaseMock = $this->createMock(DatabaseDriver::class); $this->configurationMock = $this->createMock(Configuration::class); - $this->configurationMock->expects($this->any()) + $this->configurationMock + ->expects($this->any()) ->method('getDb') ->willReturn($this->databaseMock); @@ -78,19 +79,25 @@ public function testGetCategoriesEmpty(): void Database::setTablePrefix('test_'); // Setup required properties - $this->startpage->setUser(1)->setGroups([1])->setLanguage('en'); + $this->startpage + ->setUser(1) + ->setGroups([1]) + ->setLanguage('en'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('escape') ->with('en') ->willReturn('en'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains('SELECT')) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('fetchArray') ->with('query_result') ->willReturn(false); @@ -103,23 +110,29 @@ public function testGetCategoriesWithValidLanguage(): void { Database::setTablePrefix('test_'); - $this->startpage->setUser(42)->setGroups([1, 2])->setLanguage('de'); + $this->startpage + ->setUser(42) + ->setGroups([1, 2]) + ->setLanguage('de'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('escape') ->with('de') ->willReturn('de'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->logicalAnd( $this->stringContains("fc.lang = 'de'"), $this->stringContains('fu.user_id = 42'), - $this->stringContains('fg.group_id IN (1, 2)') + $this->stringContains('fg.group_id IN (1, 2)'), )) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('fetchArray') ->willReturn(false); @@ -131,18 +144,22 @@ public function testGetCategoriesWithInvalidLanguage(): void { Database::setTablePrefix('test_'); - $this->startpage->setUser(1)->setGroups([1])->setLanguage('invalid123'); + $this->startpage + ->setUser(1) + ->setGroups([1]) + ->setLanguage('invalid123'); // escape should not be called for invalid language - $this->databaseMock->expects($this->never()) - ->method('escape'); + $this->databaseMock->expects($this->never())->method('escape'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') - ->with($this->logicalNot($this->stringContains("fc.lang ="))) + ->with($this->logicalNot($this->stringContains('fc.lang ='))) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('fetchArray') ->willReturn(false); @@ -154,18 +171,24 @@ public function testGetCategoriesWithData(): void { Database::setTablePrefix('test_'); - $this->startpage->setUser(10)->setGroups([5])->setLanguage('en'); + $this->startpage + ->setUser(10) + ->setGroups([5]) + ->setLanguage('en'); - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('getDefaultUrl') ->willReturn('https://example.com/'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('escape') ->with('en') ->willReturn('en'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn('query_result'); @@ -180,10 +203,11 @@ public function testGetCategoriesWithData(): void 'active' => 1, 'image' => 'test-image.jpg', 'show_home' => 1, - 'position' => 1 + 'position' => 1, ]; - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('fetchArray') ->willReturnOnConsecutiveCalls($categoryData, false); @@ -200,18 +224,24 @@ public function testGetCategoriesWithEmptyImage(): void { Database::setTablePrefix('test_'); - $this->startpage->setUser(1)->setGroups([1])->setLanguage('fr'); + $this->startpage + ->setUser(1) + ->setGroups([1]) + ->setLanguage('fr'); - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('getDefaultUrl') ->willReturn('https://test.com/'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('escape') ->with('fr') ->willReturn('fr'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn('query_result'); @@ -226,10 +256,11 @@ public function testGetCategoriesWithEmptyImage(): void 'active' => 1, 'image' => '', // Empty image 'show_home' => 1, - 'position' => 2 + 'position' => 2, ]; - $this->databaseMock->expects($this->exactly(2)) + $this->databaseMock + ->expects($this->exactly(2)) ->method('fetchArray') ->willReturnOnConsecutiveCalls($categoryData, false); @@ -244,17 +275,23 @@ public function testGetCategoriesMultipleCategories(): void { Database::setTablePrefix('test_'); - $this->startpage->setUser(5)->setGroups([2, 3])->setLanguage('es'); + $this->startpage + ->setUser(5) + ->setGroups([2, 3]) + ->setLanguage('es'); - $this->configurationMock->expects($this->any()) + $this->configurationMock + ->expects($this->any()) ->method('getDefaultUrl') ->willReturn('https://demo.com/'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('escape') ->willReturn('es'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->willReturn('query_result'); @@ -269,7 +306,7 @@ public function testGetCategoriesMultipleCategories(): void 'active' => 1, 'image' => 'cat1.jpg', 'show_home' => 1, - 'position' => 1 + 'position' => 1, ]; $category2 = [ @@ -283,10 +320,11 @@ public function testGetCategoriesMultipleCategories(): void 'active' => 1, 'image' => '', 'show_home' => 1, - 'position' => 2 + 'position' => 2, ]; - $this->databaseMock->expects($this->exactly(3)) + $this->databaseMock + ->expects($this->exactly(3)) ->method('fetchArray') ->willReturnOnConsecutiveCalls($category1, $category2, false); @@ -304,19 +342,25 @@ public function testGetCategoriesLanguageEdgeCases(): void Database::setTablePrefix('test_'); // Test with language that has hyphen - $this->startpage->setUser(1)->setGroups([1])->setLanguage('zh-cn'); + $this->startpage + ->setUser(1) + ->setGroups([1]) + ->setLanguage('zh-cn'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('escape') ->with('zh-cn') ->willReturn('zh-cn'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->stringContains("fc.lang = 'zh-cn'")) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('fetchArray') ->willReturn(false); @@ -329,17 +373,21 @@ public function testGetCategoriesWithSingleCharacterLanguage(): void Database::setTablePrefix('test_'); // Single character language should not match regex - $this->startpage->setUser(1)->setGroups([1])->setLanguage('a'); + $this->startpage + ->setUser(1) + ->setGroups([1]) + ->setLanguage('a'); - $this->databaseMock->expects($this->never()) - ->method('escape'); + $this->databaseMock->expects($this->never())->method('escape'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') - ->with($this->logicalNot($this->stringContains("fc.lang ="))) + ->with($this->logicalNot($this->stringContains('fc.lang ='))) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('fetchArray') ->willReturn(false); @@ -351,24 +399,30 @@ public function testGetCategoriesQueryStructure(): void { Database::setTablePrefix('test_'); - $this->startpage->setUser(123)->setGroups([10, 20])->setLanguage('en'); + $this->startpage + ->setUser(123) + ->setGroups([10, 20]) + ->setLanguage('en'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('escape') ->willReturn('en'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('query') ->with($this->logicalAnd( $this->stringContains('fc.active = 1'), $this->stringContains('fc.show_home = 1'), $this->stringContains('ORDER BY'), $this->stringContains('fco.position'), - $this->stringContains('GROUP BY') + $this->stringContains('GROUP BY'), )) ->willReturn('query_result'); - $this->databaseMock->expects($this->once()) + $this->databaseMock + ->expects($this->once()) ->method('fetchArray') ->willReturn(false); diff --git a/tests/phpMyFAQ/Category/Tree/TreeBuilderTest.php b/tests/phpMyFAQ/Category/Tree/TreeBuilderTest.php index 3cb5b14840..354750e095 100644 --- a/tests/phpMyFAQ/Category/Tree/TreeBuilderTest.php +++ b/tests/phpMyFAQ/Category/Tree/TreeBuilderTest.php @@ -4,8 +4,8 @@ namespace phpMyFAQ\Category\Tree; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class TreeBuilderTest extends TestCase @@ -21,11 +21,14 @@ public function testBuildAdminCategoryTreeFlat(): void $tree = $builder->buildAdminCategoryTree($categories); - $this->assertSame([ - 1 => [], - 2 => [], - 3 => [], - ], $tree); + $this->assertSame( + [ + 1 => [], + 2 => [], + 3 => [], + ], + $tree, + ); } public function testBuildLinearTreeNested(): void @@ -47,4 +50,3 @@ public function testBuildLinearTreeNested(): void $this->assertSame(1, $result[2]['parent_id']); } } - diff --git a/tests/phpMyFAQ/CategoryTest.php b/tests/phpMyFAQ/CategoryTest.php index 8bec8b9792..79a5719ff0 100644 --- a/tests/phpMyFAQ/CategoryTest.php +++ b/tests/phpMyFAQ/CategoryTest.php @@ -4,10 +4,10 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Entity\CategoryEntity; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class CategoryTest extends TestCase @@ -31,6 +31,7 @@ protected function setUp(): void $this->category = new Category($configuration); } + public function testGetGroups(): void { $groups = [1, 2, 3]; @@ -94,10 +95,10 @@ public function testGetOrderedCategories(): void 'active' => 1, 'show_home' => 1, 'image' => 'image.png', - 'level' => 0 - ] + 'level' => 0, + ], ], - $this->category->getOrderedCategories(false) + $this->category->getOrderedCategories(false), ); // Cleanup @@ -133,10 +134,10 @@ public function testGetAllCategories(): void 'active' => 1, 'show_home' => 1, 'image' => 'image.png', - 'level' => 0 - ] + 'level' => 0, + ], ], - $this->category->getAllCategories() + $this->category->getAllCategories(), ); // Cleanup @@ -168,10 +169,7 @@ public function testAdminCategoryTree(): void $categories = $this->category->getOrderedCategories(false); - $this->assertEquals( - [ 1 => [], 2 => [], 3 => []], - $this->category->buildAdminCategoryTree($categories) - ); + $this->assertEquals([1 => [], 2 => [], 3 => []], $this->category->buildAdminCategoryTree($categories)); // Cleanup $this->category->delete(1, 'en'); @@ -179,7 +177,6 @@ public function testAdminCategoryTree(): void $this->category->delete(3, 'en'); } - private function createCategory(int $id = 1): CategoryEntity { $category = new CategoryEntity(); @@ -203,10 +200,7 @@ public function testGetCategoryData(): void $category = $this->createCategory(); $this->category->create($category); - $this->assertEquals( - $category, - $this->category->getCategoryData(1) - ); + $this->assertEquals($category, $this->category->getCategoryData(1)); // Cleanup $this->category->delete(1, 'en'); @@ -217,10 +211,7 @@ public function testGetCategoryIdFromName(): void $category = $this->createCategory(); $this->category->create($category); - $this->assertEquals( - 1, - $this->category->getCategoryIdFromName('Category 1') - ); + $this->assertEquals(1, $this->category->getCategoryIdFromName('Category 1')); // Cleanup $this->category->delete(1, 'en'); @@ -252,10 +243,7 @@ public function testUpdate(): void $this->category->update($category); - $this->assertEquals( - $category, - $this->category->getCategoryData(1) - ); + $this->assertEquals($category, $this->category->getCategoryData(1)); // Cleanup $this->category->delete(1, 'en'); @@ -270,10 +258,7 @@ public function testMoveOwnership(): void $category->setUserId(2); - $this->assertEquals( - $category, - $this->category->getCategoryData(1) - ); + $this->assertEquals($category, $this->category->getCategoryData(1)); // Cleanup $this->category->delete(1, 'en'); @@ -299,10 +284,7 @@ public function testUpdateParentCategory(): void $category->setParentId(2); - $this->assertEquals( - $category, - $this->category->getCategoryData(1) - ); + $this->assertEquals($category, $this->category->getCategoryData(1)); // Cleanup $this->category->delete(1, 'en'); diff --git a/tests/phpMyFAQ/Command/McpServerCommandTest.php b/tests/phpMyFAQ/Command/McpServerCommandTest.php index 38a5e19c3d..7ecd3dc563 100644 --- a/tests/phpMyFAQ/Command/McpServerCommandTest.php +++ b/tests/phpMyFAQ/Command/McpServerCommandTest.php @@ -2,12 +2,12 @@ namespace phpMyFAQ\Command; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Service\McpServer\PhpMyFaqMcpServer; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; use ReflectionClass; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class McpServerCommandTest extends TestCase @@ -35,13 +35,16 @@ public function testExecuteWithInfoOptionShowsServerInfo(): void $input->method('getOption')->with('info')->willReturn(true); - $this->serverMock->expects($this->once())->method('getServerInfo')->willReturn([ - 'name' => 'phpMyFAQ MCP Server', - 'version' => '0.1.0-dev', - 'description' => 'Test server', - 'capabilities' => ['tools' => true], - 'tools' => [['name' => 'faq_search', 'description' => 'Search tool']] - ]); + $this->serverMock + ->expects($this->once()) + ->method('getServerInfo') + ->willReturn([ + 'name' => 'phpMyFAQ MCP Server', + 'version' => '0.1.0-dev', + 'description' => 'Test server', + 'capabilities' => ['tools' => true], + 'tools' => [['name' => 'faq_search', 'description' => 'Search tool']], + ]); $result = $this->callExecute($input, $output); $this->assertSame(0, $result); @@ -54,7 +57,10 @@ public function testExecuteRunsServerSuccessfully(): void $input->method('getOption')->with('info')->willReturn(false); - $this->serverMock->expects($this->once())->method('runConsole')->with($input, $output); + $this->serverMock + ->expects($this->once()) + ->method('runConsole') + ->with($input, $output); $result = $this->callExecute($input, $output); $this->assertSame(0, $result); diff --git a/tests/phpMyFAQ/Command/UpdateCommandTest.php b/tests/phpMyFAQ/Command/UpdateCommandTest.php index 8f4649b2b4..7ff4d233bd 100644 --- a/tests/phpMyFAQ/Command/UpdateCommandTest.php +++ b/tests/phpMyFAQ/Command/UpdateCommandTest.php @@ -3,16 +3,16 @@ namespace phpMyFAQ\Command; use DateTime; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; use phpMyFAQ\System; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; use ReflectionClass; use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\CommandTester; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for UpdateCommand diff --git a/tests/phpMyFAQ/Comment/CommentsRepositoryTest.php b/tests/phpMyFAQ/Comment/CommentsRepositoryTest.php index 8352f6d67b..8869cd8e5c 100644 --- a/tests/phpMyFAQ/Comment/CommentsRepositoryTest.php +++ b/tests/phpMyFAQ/Comment/CommentsRepositoryTest.php @@ -3,15 +3,16 @@ namespace phpMyFAQ\Comment; use phpMyFAQ\Category; -use phpMyFAQ\Category\Relation;use phpMyFAQ\Configuration; +use phpMyFAQ\Category\Relation; +use phpMyFAQ\Configuration; use phpMyFAQ\Database; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Entity\Comment as CommentEntity; use phpMyFAQ\Entity\CommentType; -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Session\Session; use phpMyFAQ\Language; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Session; #[AllowMockObjectsWithoutExpectations] class CommentsRepositoryTest extends TestCase @@ -134,16 +135,16 @@ public function testIsCommentAllowed(): void { $prefix = Database::getTablePrefix(); // Ensure a faqdata row for id=1, lang='en' exists - $this->configuration->getDb()->query( - "INSERT OR IGNORE INTO {$prefix}faqdata (id, lang, solution_id, revision_id, active, sticky, thema, author, email, comment, updated, date_start, date_end) VALUES (1, 'en', 1, 0, 'yes', 0, 'Test', 'Admin', 'admin@example.org', 'n', '20200101000000', '00000000000000', '99991231235959')" - ); + $this->configuration + ->getDb() + ->query( + "INSERT OR IGNORE INTO {$prefix}faqdata (id, lang, solution_id, revision_id, active, sticky, thema, author, email, comment, updated, date_start, date_end) VALUES (1, 'en', 1, 0, 'yes', 0, 'Test', 'Admin', 'admin@example.org', 'n', '20200101000000', '00000000000000', '99991231235959')", + ); // Set comment flag to 'y' - $this->configuration->getDb()->query(sprintf( - "UPDATE %sfaqdata SET comment = 'y' WHERE id = 1 AND lang = 'en'", - $prefix - )); + $this->configuration + ->getDb() + ->query(sprintf("UPDATE %sfaqdata SET comment = 'y' WHERE id = 1 AND lang = 'en'", $prefix)); $this->assertTrue($this->repository->isCommentAllowed(1, 'en', CommentType::FAQ)); } } - diff --git a/tests/phpMyFAQ/CommentsTest.php b/tests/phpMyFAQ/CommentsTest.php index edf74440a7..2e62a6cd49 100644 --- a/tests/phpMyFAQ/CommentsTest.php +++ b/tests/phpMyFAQ/CommentsTest.php @@ -2,12 +2,12 @@ namespace phpMyFAQ; +use phpMyFAQ\Category\Relation; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Entity\Comment; use phpMyFAQ\Entity\CommentType; -use phpMyFAQ\Category\Relation; -use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; diff --git a/tests/phpMyFAQ/Configuration/DatabaseConfigurationTest.php b/tests/phpMyFAQ/Configuration/DatabaseConfigurationTest.php index a2289a7dd8..13b1d40464 100644 --- a/tests/phpMyFAQ/Configuration/DatabaseConfigurationTest.php +++ b/tests/phpMyFAQ/Configuration/DatabaseConfigurationTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Configuration; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class DatabaseConfigurationTest extends TestCase diff --git a/tests/phpMyFAQ/Configuration/ElasticsearchConfigurationTest.php b/tests/phpMyFAQ/Configuration/ElasticsearchConfigurationTest.php index c82104bdee..a201b83bc4 100644 --- a/tests/phpMyFAQ/Configuration/ElasticsearchConfigurationTest.php +++ b/tests/phpMyFAQ/Configuration/ElasticsearchConfigurationTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Configuration; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Test class for ElasticsearchConfiguration @@ -54,7 +54,7 @@ public function testConstructorWithValidConfiguration(): void { $config = [ 'hosts' => ['localhost:9200', 'elastic.example.com:9200'], - 'index' => 'phpmyfaq_test' + 'index' => 'phpmyfaq_test', ]; $configFile = $this->createConfigFile('valid_config.php', $config); @@ -69,7 +69,7 @@ public function testConstructorWithMinimalConfiguration(): void { $config = [ 'hosts' => [], - 'index' => '' + 'index' => '', ]; $configFile = $this->createConfigFile('minimal_config.php', $config); @@ -83,7 +83,7 @@ public function testConstructorWithSingleHostConfiguration(): void { $config = [ 'hosts' => ['http://localhost:9200'], - 'index' => 'single_index' + 'index' => 'single_index', ]; $configFile = $this->createConfigFile('single_host_config.php', $config); @@ -100,9 +100,9 @@ public function testConstructorWithMultipleHostsConfiguration(): void 'hosts' => [ 'https://node1.elastic.example.com:9200', 'https://node2.elastic.example.com:9200', - 'https://node3.elastic.example.com:9200' + 'https://node3.elastic.example.com:9200', ], - 'index' => 'production_faq_index' + 'index' => 'production_faq_index', ]; $configFile = $this->createConfigFile('multi_host_config.php', $config); @@ -117,12 +117,12 @@ public function testGetHostsReturnsCorrectArray(): void { $hosts = [ 'cluster-node-1.elasticsearch.local:9200', - 'cluster-node-2.elasticsearch.local:9200' + 'cluster-node-2.elasticsearch.local:9200', ]; $config = [ 'hosts' => $hosts, - 'index' => 'test_index' + 'index' => 'test_index', ]; $configFile = $this->createConfigFile('hosts_test.php', $config); @@ -139,7 +139,7 @@ public function testGetIndexReturnsCorrectString(): void $config = [ 'hosts' => ['localhost:9200'], - 'index' => $indexName + 'index' => $indexName, ]; $configFile = $this->createConfigFile('index_test.php', $config); @@ -155,9 +155,9 @@ public function testConstructorWithComplexHostUrls(): void 'hosts' => [ 'https://user:pass@secure-elastic.com:9200', 'http://192.168.1.100:9200', - 'https://elastic-cluster.internal:443/elasticsearch' + 'https://elastic-cluster.internal:443/elasticsearch', ], - 'index' => 'complex_index_name_with_underscores' + 'index' => 'complex_index_name_with_underscores', ]; $configFile = $this->createConfigFile('complex_config.php', $config); @@ -171,7 +171,7 @@ public function testReadonlyPropertiesCannotBeModified(): void { $config = [ 'hosts' => ['localhost:9200'], - 'index' => 'readonly_test' + 'index' => 'readonly_test', ]; $configFile = $this->createConfigFile('readonly_config.php', $config); @@ -191,7 +191,7 @@ public function testConstructorWithEmptyHostsArray(): void { $config = [ 'hosts' => [], - 'index' => 'empty_hosts_index' + 'index' => 'empty_hosts_index', ]; $configFile = $this->createConfigFile('empty_hosts_config.php', $config); @@ -208,9 +208,9 @@ public function testConstructorWithNumericIndexKeys(): void 'hosts' => [ 0 => 'first-host:9200', 1 => 'second-host:9200', - 2 => 'third-host:9200' + 2 => 'third-host:9200', ], - 'index' => 'numeric_keys_index' + 'index' => 'numeric_keys_index', ]; $configFile = $this->createConfigFile('numeric_keys_config.php', $config); @@ -226,7 +226,7 @@ public function testConstructorWithSpecialCharactersInIndex(): void { $config = [ 'hosts' => ['localhost:9200'], - 'index' => 'test-index_with.special-chars_2025' + 'index' => 'test-index_with.special-chars_2025', ]; $configFile = $this->createConfigFile('special_chars_config.php', $config); @@ -240,12 +240,12 @@ public function testMultipleInstancesWithDifferentConfigurations(): void // Test dass verschiedene Instanzen unabhängig voneinander funktionieren $config1 = [ 'hosts' => ['host1:9200'], - 'index' => 'index1' + 'index' => 'index1', ]; $config2 = [ 'hosts' => ['host2:9200', 'host3:9200'], - 'index' => 'index2' + 'index' => 'index2', ]; $configFile1 = $this->createConfigFile('config1.php', $config1); @@ -272,11 +272,16 @@ public function testClassIsReadonly(): void public function testConstructorWithDefaultValues(): void { // Test mit expliziter Standard-Konfiguration - $configContent = ' [],' . PHP_EOL . - ' "index" => "",' . PHP_EOL . - '];'; + $configContent = + ' [],' + . PHP_EOL + . ' "index" => "",' + . PHP_EOL + . '];'; $configFile = $this->testConfigDir . '/default_config.php'; file_put_contents($configFile, $configContent); diff --git a/tests/phpMyFAQ/Configuration/LdapConfigurationTest.php b/tests/phpMyFAQ/Configuration/LdapConfigurationTest.php index 380cd936ec..a585abcb07 100644 --- a/tests/phpMyFAQ/Configuration/LdapConfigurationTest.php +++ b/tests/phpMyFAQ/Configuration/LdapConfigurationTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Configuration; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Test class for LdapConfiguration diff --git a/tests/phpMyFAQ/Configuration/MultisiteConfigurationLocatorTest.php b/tests/phpMyFAQ/Configuration/MultisiteConfigurationLocatorTest.php index b9812ee25d..21c3e74ff1 100644 --- a/tests/phpMyFAQ/Configuration/MultisiteConfigurationLocatorTest.php +++ b/tests/phpMyFAQ/Configuration/MultisiteConfigurationLocatorTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Configuration; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class MultisiteConfigurationLocatorTest extends TestCase diff --git a/tests/phpMyFAQ/Configuration/OpenSearchConfigurationTest.php b/tests/phpMyFAQ/Configuration/OpenSearchConfigurationTest.php index 22c5c99d76..6be5978c03 100644 --- a/tests/phpMyFAQ/Configuration/OpenSearchConfigurationTest.php +++ b/tests/phpMyFAQ/Configuration/OpenSearchConfigurationTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Configuration; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Test class for OpenSearchConfiguration @@ -54,7 +54,7 @@ public function testConstructorWithValidConfiguration(): void { $config = [ 'hosts' => ['localhost:9200', 'opensearch.example.com:9200'], - 'index' => 'phpmyfaq_opensearch' + 'index' => 'phpmyfaq_opensearch', ]; $configFile = $this->createConfigFile('valid_config.php', $config); @@ -69,7 +69,7 @@ public function testConstructorWithMinimalConfiguration(): void { $config = [ 'hosts' => [], - 'index' => '' + 'index' => '', ]; $configFile = $this->createConfigFile('minimal_config.php', $config); @@ -83,7 +83,7 @@ public function testConstructorWithSingleNodeConfiguration(): void { $config = [ 'hosts' => ['https://localhost:9200'], - 'index' => 'single_node_index' + 'index' => 'single_node_index', ]; $configFile = $this->createConfigFile('single_node_config.php', $config); @@ -100,9 +100,9 @@ public function testConstructorWithMultiNodeClusterConfiguration(): void 'hosts' => [ 'https://node1.opensearch.cluster.local:9200', 'https://node2.opensearch.cluster.local:9200', - 'https://node3.opensearch.cluster.local:9200' + 'https://node3.opensearch.cluster.local:9200', ], - 'index' => 'production_cluster_index' + 'index' => 'production_cluster_index', ]; $configFile = $this->createConfigFile('multi_node_config.php', $config); @@ -117,9 +117,9 @@ public function testConstructorWithAwsOpenSearchService(): void { $config = [ 'hosts' => [ - 'https://search-my-domain.us-east-1.es.amazonaws.com' + 'https://search-my-domain.us-east-1.es.amazonaws.com', ], - 'index' => 'aws_opensearch_index' + 'index' => 'aws_opensearch_index', ]; $configFile = $this->createConfigFile('aws_opensearch_config.php', $config); @@ -134,9 +134,9 @@ public function testConstructorWithSecurityEnabledCluster(): void $config = [ 'hosts' => [ 'https://admin:password@secure-opensearch.example.com:9200', - 'https://admin:password@secure-opensearch-2.example.com:9200' + 'https://admin:password@secure-opensearch-2.example.com:9200', ], - 'index' => 'secure_index_with_auth' + 'index' => 'secure_index_with_auth', ]; $configFile = $this->createConfigFile('secure_config.php', $config); @@ -150,12 +150,12 @@ public function testGetHostsReturnsCorrectArray(): void { $hosts = [ 'opensearch-cluster-1.internal:9200', - 'opensearch-cluster-2.internal:9200' + 'opensearch-cluster-2.internal:9200', ]; $config = [ 'hosts' => $hosts, - 'index' => 'test_index' + 'index' => 'test_index', ]; $configFile = $this->createConfigFile('hosts_test.php', $config); @@ -172,7 +172,7 @@ public function testGetIndexReturnsCorrectString(): void $config = [ 'hosts' => ['localhost:9200'], - 'index' => $indexName + 'index' => $indexName, ]; $configFile = $this->createConfigFile('index_test.php', $config); @@ -186,7 +186,7 @@ public function testConstructorWithDevelopmentConfiguration(): void { $config = [ 'hosts' => ['http://localhost:9200'], - 'index' => 'dev_local_index' + 'index' => 'dev_local_index', ]; $configFile = $this->createConfigFile('dev_config.php', $config); @@ -202,9 +202,9 @@ public function testConstructorWithProductionConfiguration(): void 'hosts' => [ 'https://prod-opensearch-1.company.com:443', 'https://prod-opensearch-2.company.com:443', - 'https://prod-opensearch-3.company.com:443' + 'https://prod-opensearch-3.company.com:443', ], - 'index' => 'prod_faq_knowledge_base' + 'index' => 'prod_faq_knowledge_base', ]; $configFile = $this->createConfigFile('prod_config.php', $config); @@ -219,7 +219,7 @@ public function testReadonlyPropertiesCannotBeModified(): void { $config = [ 'hosts' => ['localhost:9200'], - 'index' => 'readonly_test' + 'index' => 'readonly_test', ]; $configFile = $this->createConfigFile('readonly_config.php', $config); @@ -239,7 +239,7 @@ public function testConstructorWithEmptyHostsArray(): void { $config = [ 'hosts' => [], - 'index' => 'empty_hosts_index' + 'index' => 'empty_hosts_index', ]; $configFile = $this->createConfigFile('empty_hosts_config.php', $config); @@ -253,7 +253,7 @@ public function testConstructorWithSpecialCharactersInIndex(): void { $config = [ 'hosts' => ['localhost:9200'], - 'index' => 'test-index_with.special-chars_2025' + 'index' => 'test-index_with.special-chars_2025', ]; $configFile = $this->createConfigFile('special_chars_config.php', $config); @@ -267,9 +267,9 @@ public function testConstructorWithDockerComposeConfiguration(): void $config = [ 'hosts' => [ 'http://opensearch-node1:9200', - 'http://opensearch-node2:9200' + 'http://opensearch-node2:9200', ], - 'index' => 'docker_compose_index' + 'index' => 'docker_compose_index', ]; $configFile = $this->createConfigFile('docker_config.php', $config); @@ -283,9 +283,9 @@ public function testConstructorWithKubernetesConfiguration(): void { $config = [ 'hosts' => [ - 'https://opensearch-cluster.opensearch-system.svc.cluster.local:9200' + 'https://opensearch-cluster.opensearch-system.svc.cluster.local:9200', ], - 'index' => 'k8s_cluster_index' + 'index' => 'k8s_cluster_index', ]; $configFile = $this->createConfigFile('k8s_config.php', $config); @@ -300,12 +300,12 @@ public function testMultipleInstancesWithDifferentConfigurations(): void // Test dass verschiedene Instanzen unabhängig voneinander funktionieren $config1 = [ 'hosts' => ['http://dev-opensearch:9200'], - 'index' => 'dev_index' + 'index' => 'dev_index', ]; $config2 = [ 'hosts' => ['https://prod-opensearch-1:9200', 'https://prod-opensearch-2:9200'], - 'index' => 'prod_index' + 'index' => 'prod_index', ]; $configFile1 = $this->createConfigFile('config1.php', $config1); @@ -318,7 +318,10 @@ public function testMultipleInstancesWithDifferentConfigurations(): void $this->assertNotEquals($osConfig1->getIndex(), $osConfig2->getIndex()); $this->assertEquals(['http://dev-opensearch:9200'], $osConfig1->getHosts()); - $this->assertEquals(['https://prod-opensearch-1:9200', 'https://prod-opensearch-2:9200'], $osConfig2->getHosts()); + $this->assertEquals( + ['https://prod-opensearch-1:9200', 'https://prod-opensearch-2:9200'], + $osConfig2->getHosts(), + ); $this->assertEquals('dev_index', $osConfig1->getIndex()); $this->assertEquals('prod_index', $osConfig2->getIndex()); } @@ -335,9 +338,9 @@ public function testConstructorWithCustomPortConfiguration(): void 'hosts' => [ 'https://opensearch-master:9200', 'https://opensearch-data-1:9201', - 'https://opensearch-data-2:9202' + 'https://opensearch-data-2:9202', ], - 'index' => 'custom_port_index' + 'index' => 'custom_port_index', ]; $configFile = $this->createConfigFile('custom_port_config.php', $config); @@ -352,9 +355,9 @@ public function testConstructorWithPathBasedUrls(): void $config = [ 'hosts' => [ 'https://example.com/opensearch', - 'https://backup.example.com/search-service' + 'https://backup.example.com/search-service', ], - 'index' => 'path_based_index' + 'index' => 'path_based_index', ]; $configFile = $this->createConfigFile('path_based_config.php', $config); diff --git a/tests/phpMyFAQ/ConfigurationTest.php b/tests/phpMyFAQ/ConfigurationTest.php index ecdf6ff0ba..100a9d3396 100644 --- a/tests/phpMyFAQ/ConfigurationTest.php +++ b/tests/phpMyFAQ/ConfigurationTest.php @@ -6,8 +6,8 @@ use phpMyFAQ\Database\DatabaseDriver; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Plugin\PluginManager; -use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; @@ -45,12 +45,12 @@ private function setupTestDatabase(): void $db = $this->configuration->getDb(); // Create faqconfig table if it doesn't exist - $createTable = " + $createTable = ' CREATE TABLE IF NOT EXISTS faqconfig ( config_name VARCHAR(255) NOT NULL PRIMARY KEY, config_value TEXT ) - "; + '; $db->query($createTable); @@ -58,14 +58,14 @@ private function setupTestDatabase(): void $defaultConfigs = [ 'main.currentVersion' => System::getVersion(), 'main.language' => 'en', - 'security.permLevel' => 'basic' + 'security.permLevel' => 'basic', ]; foreach ($defaultConfigs as $key => $value) { $insertQuery = sprintf( "INSERT OR REPLACE INTO faqconfig (config_name, config_value) VALUES ('%s', '%s')", $key, - $value + $value, ); $db->query($insertQuery); } @@ -99,6 +99,7 @@ public function testSetDatabase(): void $this->assertSame($database, $config->get('core.database')); } + public function testSet(): void { $key = 'upgrade.releaseEnvironment'; @@ -132,13 +133,13 @@ public function testSetLdapConfigWithSingleServer(): void // Demo data from /content/core/config/ldap.php file_put_contents( PMF_TEST_DIR . '/content/core/config/ldap.php', - "configuration->set('ldap.ldap_use_multiple_servers', 'false'); @@ -149,8 +150,8 @@ public function testSetLdapConfigWithSingleServer(): void 'ldap_port' => 389, 'ldap_user' => 'admin', 'ldap_password' => 'foobar', - 'ldap_base' => 'DC=foo,DC=bar,DC=baz' - ] + 'ldap_base' => 'DC=foo,DC=bar,DC=baz', + ], ]; $ldapConfig = new LdapConfiguration(PMF_TEST_DIR . '/content/core/config/ldap.php'); @@ -165,18 +166,18 @@ public function testSetLdapConfigWithMultipleServers(): void // Demo data from /content/core/config/ldap.php file_put_contents( PMF_TEST_DIR . '/content/core/config/ldap.php', - "configuration->set('ldap.ldap_use_multiple_servers', 'true'); @@ -187,15 +188,15 @@ public function testSetLdapConfigWithMultipleServers(): void 'ldap_port' => '389', 'ldap_user' => 'admin', 'ldap_password' => 'foobar', - 'ldap_base' => 'DC=foo,DC=bar,DC=baz' - ], + 'ldap_base' => 'DC=foo,DC=bar,DC=baz', + ], 1 => [ 'server' => '::1', 'port' => '389', 'user' => 'root', 'password' => '42', - 'base' => 'DC=foo,DC=bar,DC=baz' - ] + 'base' => 'DC=foo,DC=bar,DC=baz', + ], ]; $ldapConfig = new LdapConfiguration(PMF_TEST_DIR . '/content/core/config/ldap.php'); @@ -209,18 +210,18 @@ public function testSetLdapConfigWithMultipleServersButDisabled(): void // Demo data from /content/core/config/ldap.php file_put_contents( PMF_TEST_DIR . '/content/core/config/ldap.php', - "configuration->set('ldap.ldap_use_multiple_servers', 'false'); @@ -231,8 +232,8 @@ public function testSetLdapConfigWithMultipleServersButDisabled(): void 'ldap_port' => '389', 'ldap_user' => 'admin', 'ldap_password' => 'foobar', - 'ldap_base' => 'DC=foo,DC=bar,DC=baz' - ] + 'ldap_base' => 'DC=foo,DC=bar,DC=baz', + ], ]; $ldapConfig = new LdapConfiguration(PMF_TEST_DIR . '/content/core/config/ldap.php'); diff --git a/tests/phpMyFAQ/Controller/AbstractControllerTest.php b/tests/phpMyFAQ/Controller/AbstractControllerTest.php index 6e973e6f5b..90ecd29e27 100644 --- a/tests/phpMyFAQ/Controller/AbstractControllerTest.php +++ b/tests/phpMyFAQ/Controller/AbstractControllerTest.php @@ -7,6 +7,7 @@ use phpMyFAQ\Enums\PermissionType; use phpMyFAQ\Permission\BasicPermission; use phpMyFAQ\User\CurrentUser; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -15,7 +16,6 @@ use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Twig\Extension\ExtensionInterface; use Twig\TwigFilter; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class AbstractControllerTest extends TestCase diff --git a/tests/phpMyFAQ/Controller/Api/AttachmentControllerTest.php b/tests/phpMyFAQ/Controller/Api/AttachmentControllerTest.php index ee62108d5d..965413233e 100644 --- a/tests/phpMyFAQ/Controller/Api/AttachmentControllerTest.php +++ b/tests/phpMyFAQ/Controller/Api/AttachmentControllerTest.php @@ -5,6 +5,7 @@ use phpMyFAQ\Attachment\AttachmentException; use phpMyFAQ\Attachment\File; use phpMyFAQ\Configuration; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use ReflectionClass; @@ -13,7 +14,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test-specific subclass of AttachmentController that allows us to control the behavior @@ -26,6 +26,7 @@ class TestableAttachmentController extends AttachmentController public function __construct(mixed $returnValueOrException) { $this->returnValueOrException = $returnValueOrException; + // Don't call parent constructor to avoid the API check } @@ -81,19 +82,16 @@ class AttachmentControllerTest extends TestCase public function testConstructorWithApiEnabled(): void { $configuration = $this->createStub(Configuration::class); - $configuration->method('get') - ->with('api.enableAccess') - ->willReturn(true); + $configuration->method('get')->with('api.enableAccess')->willReturn(true); - $attachmentController = $this->getMockBuilder(AttachmentController::class) + $attachmentController = $this + ->getMockBuilder(AttachmentController::class) ->disableOriginalConstructor() ->onlyMethods(['isApiEnabled']) ->getMock(); // Expect isApiEnabled to be called exactly once during constructor - $attachmentController->expects($this->once()) - ->method('isApiEnabled') - ->willReturn(true); + $attachmentController->expects($this->once())->method('isApiEnabled')->willReturn(true); $reflection = new ReflectionClass(AttachmentController::class); $constructor = $reflection->getConstructor(); @@ -109,19 +107,16 @@ public function testConstructorWithApiEnabled(): void public function testConstructorWithApiDisabled(): void { $configuration = $this->createStub(Configuration::class); - $configuration->method('get') - ->with('api.enableAccess') - ->willReturn(false); + $configuration->method('get')->with('api.enableAccess')->willReturn(false); - $attachmentController = $this->getMockBuilder(AttachmentController::class) + $attachmentController = $this + ->getMockBuilder(AttachmentController::class) ->disableOriginalConstructor() ->onlyMethods(['isApiEnabled']) ->getMock(); // Expect isApiEnabled to be called exactly once during constructor - $attachmentController->expects($this->once()) - ->method('isApiEnabled') - ->willReturn(false); + $attachmentController->expects($this->once())->method('isApiEnabled')->willReturn(false); $this->expectException(UnauthorizedHttpException::class); @@ -208,8 +203,7 @@ private function createAttachmentControllerTestDouble(mixed $returnValueOrExcept $attachmentController = new TestableAttachmentController($returnValueOrException); $configuration = $this->createStub(Configuration::class); - $configuration->method('getDefaultUrl') - ->willReturn('https://www.example.org/'); + $configuration->method('getDefaultUrl')->willReturn('https://www.example.org/'); $reflection = new ReflectionClass(AttachmentController::class); diff --git a/tests/phpMyFAQ/Controller/Api/LanguageControllerTest.php b/tests/phpMyFAQ/Controller/Api/LanguageControllerTest.php index c5092ec7b4..60e283a6b3 100644 --- a/tests/phpMyFAQ/Controller/Api/LanguageControllerTest.php +++ b/tests/phpMyFAQ/Controller/Api/LanguageControllerTest.php @@ -4,11 +4,11 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Language; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class LanguageControllerTest extends TestCase diff --git a/tests/phpMyFAQ/Controller/Api/TitleControllerTest.php b/tests/phpMyFAQ/Controller/Api/TitleControllerTest.php index cd16361b8c..2993a96a60 100644 --- a/tests/phpMyFAQ/Controller/Api/TitleControllerTest.php +++ b/tests/phpMyFAQ/Controller/Api/TitleControllerTest.php @@ -3,9 +3,9 @@ namespace phpMyFAQ\Controller\Api; use phpMyFAQ\Configuration; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\JsonResponse; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class TitleControllerTest extends TestCase diff --git a/tests/phpMyFAQ/Controller/Api/VersionControllerTest.php b/tests/phpMyFAQ/Controller/Api/VersionControllerTest.php index 4953426273..c57278a2af 100644 --- a/tests/phpMyFAQ/Controller/Api/VersionControllerTest.php +++ b/tests/phpMyFAQ/Controller/Api/VersionControllerTest.php @@ -3,9 +3,9 @@ namespace phpMyFAQ\Controller\Api; use phpMyFAQ\System; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\JsonResponse; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class VersionControllerTest extends TestCase diff --git a/tests/phpMyFAQ/Controller/BackupControllerTest.php b/tests/phpMyFAQ/Controller/BackupControllerTest.php index b500b3b55e..fa8a41601e 100644 --- a/tests/phpMyFAQ/Controller/BackupControllerTest.php +++ b/tests/phpMyFAQ/Controller/BackupControllerTest.php @@ -11,6 +11,7 @@ use phpMyFAQ\Enums\BackupType; use phpMyFAQ\Permission\BasicPermission; use phpMyFAQ\User\CurrentUser; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -19,7 +20,6 @@ use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class BackupControllerTest extends TestCase diff --git a/tests/phpMyFAQ/Controller/Frontend/TranslationControllerTest.php b/tests/phpMyFAQ/Controller/Frontend/TranslationControllerTest.php index e8a6566b34..80bedc6cc6 100644 --- a/tests/phpMyFAQ/Controller/Frontend/TranslationControllerTest.php +++ b/tests/phpMyFAQ/Controller/Frontend/TranslationControllerTest.php @@ -2,14 +2,15 @@ namespace phpMyFAQ\Controller\Frontend; +use phpMyFAQ\Controller\Frontend\Api\TranslationController; use phpMyFAQ\Strings; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class TranslationControllerTest extends TestCase diff --git a/tests/phpMyFAQ/Controller/LlmsControllerTest.php b/tests/phpMyFAQ/Controller/LlmsControllerTest.php index 7c5b1cb0db..d14ab87291 100644 --- a/tests/phpMyFAQ/Controller/LlmsControllerTest.php +++ b/tests/phpMyFAQ/Controller/LlmsControllerTest.php @@ -4,11 +4,11 @@ use phpMyFAQ\Strings; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Response; use Twig\Environment; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class LlmsControllerTest extends TestCase diff --git a/tests/phpMyFAQ/Controller/RobotsControllerTest.php b/tests/phpMyFAQ/Controller/RobotsControllerTest.php index 87dfd2e46e..60ca4ce78f 100644 --- a/tests/phpMyFAQ/Controller/RobotsControllerTest.php +++ b/tests/phpMyFAQ/Controller/RobotsControllerTest.php @@ -4,11 +4,11 @@ use phpMyFAQ\Strings; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Response; use Twig\Environment; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class RobotsControllerTest extends TestCase diff --git a/tests/phpMyFAQ/Controller/SitemapControllerTest.php b/tests/phpMyFAQ/Controller/SitemapControllerTest.php index 1a6a1304e6..11c64f6a97 100644 --- a/tests/phpMyFAQ/Controller/SitemapControllerTest.php +++ b/tests/phpMyFAQ/Controller/SitemapControllerTest.php @@ -5,11 +5,11 @@ use phpMyFAQ\Strings; use phpMyFAQ\Translation; use phpMyFAQ\Twig\TemplateException; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Response; use Twig\Environment; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SitemapControllerTest extends TestCase diff --git a/tests/phpMyFAQ/Controller/WebAuthnControllerTest.php b/tests/phpMyFAQ/Controller/WebAuthnControllerTest.php index 3241384857..5a3304d140 100644 --- a/tests/phpMyFAQ/Controller/WebAuthnControllerTest.php +++ b/tests/phpMyFAQ/Controller/WebAuthnControllerTest.php @@ -17,14 +17,15 @@ namespace phpMyFAQ\Controller; +use phpMyFAQ\Controller\Frontend\WebAuthnController; use phpMyFAQ\Core\Exception; use phpMyFAQ\Strings; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Twig\Error\LoaderError; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class WebAuthnControllerTest diff --git a/tests/phpMyFAQ/Core/ErrorTest.php b/tests/phpMyFAQ/Core/ErrorTest.php index c07478001b..7dbd929167 100644 --- a/tests/phpMyFAQ/Core/ErrorTest.php +++ b/tests/phpMyFAQ/Core/ErrorTest.php @@ -5,10 +5,10 @@ use ErrorException; use Exception; use phpMyFAQ\Environment; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class ErrorTest extends TestCase @@ -36,8 +36,8 @@ protected function tearDown(): void public function testErrorHandlerThrowsExceptionWhenErrorReportingIsEnabled(): void { $level = E_NOTICE; - $message = "Undefined variable: x"; - $filename = "test.php"; + $message = 'Undefined variable: x'; + $filename = 'test.php'; $line = 10; error_reporting(E_ALL); @@ -54,8 +54,8 @@ public function testErrorHandlerThrowsExceptionWhenErrorReportingIsEnabled(): vo public function testErrorHandlerDoesNotThrowExceptionWhenErrorReportingIsDisabled(): void { $level = E_NOTICE; - $message = "Undefined variable: x"; - $filename = "test.php"; + $message = 'Undefined variable: x'; + $filename = 'test.php'; $line = 10; error_reporting(0); @@ -72,8 +72,8 @@ public function testErrorHandlerUsesCorrectFilenameInDebugMode(): void } $level = E_WARNING; - $message = "Test warning"; - $filename = "/path/to/test.php"; + $message = 'Test warning'; + $filename = '/path/to/test.php'; $line = 15; try { @@ -104,7 +104,7 @@ public function testErrorHandlerWithDifferentErrorLevels(): void foreach ($errorLevels as $level) { try { - Error::errorHandler($level, "Test message", "test.php", 1); + Error::errorHandler($level, 'Test message', 'test.php', 1); $this->fail("Expected ErrorException was not thrown for level: $level"); } catch (ErrorException $exception) { $this->assertSame($level, $exception->getSeverity()); @@ -116,7 +116,7 @@ public function testErrorHandlerWithDifferentErrorLevels(): void #[RunInSeparateProcess] public function testExceptionHandlerSets404ResponseCode(): void { - $exception = new Exception("Not found", 404); + $exception = new Exception('Not found', 404); $this->expectOutputRegex('/

    phpMyFAQ Fatal error<\/h1>/'); @@ -129,7 +129,7 @@ public function testExceptionHandlerSets404ResponseCode(): void #[RunInSeparateProcess] public function testExceptionHandlerSets500ResponseCodeForNon404Errors(): void { - $exception = new Exception("Test error", 200); + $exception = new Exception('Test error', 200); $this->expectOutputRegex('/phpMyFAQ Fatal error/'); @@ -150,22 +150,22 @@ public function testExceptionHandlerOutputContainsExpectedElements(): void Error::exceptionHandler($exception); $output = ob_get_clean(); - $this->assertStringContainsString("

    phpMyFAQ Fatal error

    ", $output); + $this->assertStringContainsString('

    phpMyFAQ Fatal error

    ', $output); $this->assertStringContainsString("Uncaught exception: 'Exception'", $output); $this->assertStringContainsString("Message: '", $output); - $this->assertStringContainsString("Stack trace:", $output); + $this->assertStringContainsString('Stack trace:', $output); $this->assertStringContainsString("Thrown in '", $output); - $this->assertStringContainsString("on line ", $output); + $this->assertStringContainsString('on line ', $output); - $this->assertStringNotContainsString(" and & symbols', 'author_name' => 'Author', - 'lastmodified' => '2025-01-01 00:00:00' - ] + 'lastmodified' => '2025-01-01 00:00:00', + ], ]; $this->categoryMock->method('transform'); @@ -191,11 +194,13 @@ public function testGenerateWithCustomParameters(): void $downwards = false; $language = 'fr'; - $this->categoryMock->expects($this->once()) + $this->categoryMock + ->expects($this->once()) ->method('transform') ->with($categoryId); - $this->faqMock->expects($this->once()) + $this->faqMock + ->expects($this->once()) ->method('get') ->with('faq_export_json', $categoryId, $downwards, $language) ->willReturn([]); @@ -229,7 +234,7 @@ public function testGenerateWithMultipleFaqEntries(): void 'topic' => 'First Question', 'content' => 'First Answer', 'author_name' => 'Author 1', - 'lastmodified' => '2025-01-01 10:00:00' + 'lastmodified' => '2025-01-01 10:00:00', ], [ 'id' => 2, @@ -239,16 +244,17 @@ public function testGenerateWithMultipleFaqEntries(): void 'topic' => 'Second Question', 'content' => 'Second Answer', 'author_name' => 'Author 2', - 'lastmodified' => '2025-01-02 11:00:00' - ] + 'lastmodified' => '2025-01-02 11:00:00', + ], ]; $this->categoryMock->method('transform'); $this->faqMock->method('get')->willReturn($mockFaqData); - $this->categoryMock->method('getPath') + $this->categoryMock + ->method('getPath') ->willReturnMap([ [1, ' >> ', 'Category 1'], - [2, ' >> ', 'Category 2'] + [2, ' >> ', 'Category 2'], ]); $result = $this->jsonExport->generate(); @@ -272,8 +278,8 @@ public function testGenerateCreatesValidJsonWithJsonThrowOnError(): void 'topic' => 'Question', 'content' => 'Answer', 'author_name' => 'Author', - 'lastmodified' => '2025-01-01 00:00:00' - ] + 'lastmodified' => '2025-01-01 00:00:00', + ], ]; $this->categoryMock->method('transform'); @@ -302,8 +308,8 @@ public function testGenerateFormatsDateCorrectly(): void 'topic' => 'Question', 'content' => 'Answer', 'author_name' => 'Author', - 'lastmodified' => '2025-08-10 14:30:45' - ] + 'lastmodified' => '2025-08-10 14:30:45', + ], ]; $this->categoryMock->method('transform'); @@ -334,14 +340,15 @@ public function testGenerateHandlesCategoryPathWithSpecialSeparator(): void 'topic' => 'Question', 'content' => 'Answer', 'author_name' => 'Author', - 'lastmodified' => '2025-01-01 00:00:00' - ] + 'lastmodified' => '2025-01-01 00:00:00', + ], ]; $this->categoryMock->method('transform'); $this->faqMock->method('get')->willReturn($mockFaqData); - $this->categoryMock->expects($this->once()) + $this->categoryMock + ->expects($this->once()) ->method('getPath') ->with(3, ' >> ') ->willReturn('Root >> Subcategory >> Target'); @@ -363,8 +370,8 @@ public function testGenerateHandlesNullValues(): void 'topic' => null, 'content' => null, 'author_name' => null, - 'lastmodified' => '2025-01-01 00:00:00' - ] + 'lastmodified' => '2025-01-01 00:00:00', + ], ]; $this->categoryMock->method('transform'); diff --git a/tests/phpMyFAQ/Export/Pdf/WrapperTest.php b/tests/phpMyFAQ/Export/Pdf/WrapperTest.php index 56a07cb866..32d53a26fb 100644 --- a/tests/phpMyFAQ/Export/Pdf/WrapperTest.php +++ b/tests/phpMyFAQ/Export/Pdf/WrapperTest.php @@ -5,9 +5,9 @@ use Exception; use phpMyFAQ\Configuration; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class WrapperTest extends TestCase diff --git a/tests/phpMyFAQ/ExportTest.php b/tests/phpMyFAQ/ExportTest.php index fb46a8a6ec..7726e18fce 100644 --- a/tests/phpMyFAQ/ExportTest.php +++ b/tests/phpMyFAQ/ExportTest.php @@ -6,9 +6,9 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Export\Json; use phpMyFAQ\Export\Pdf; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class ExportTest extends TestCase diff --git a/tests/phpMyFAQ/Faq/ImportTest.php b/tests/phpMyFAQ/Faq/ImportTest.php index a243dc7147..0e44b93bfd 100644 --- a/tests/phpMyFAQ/Faq/ImportTest.php +++ b/tests/phpMyFAQ/Faq/ImportTest.php @@ -7,10 +7,10 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Language; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class ImportTest extends TestCase diff --git a/tests/phpMyFAQ/Faq/PermissionTest.php b/tests/phpMyFAQ/Faq/PermissionTest.php index a09752513f..448d08919b 100644 --- a/tests/phpMyFAQ/Faq/PermissionTest.php +++ b/tests/phpMyFAQ/Faq/PermissionTest.php @@ -5,8 +5,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Translation; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** diff --git a/tests/phpMyFAQ/Faq/QueryHelperTest.php b/tests/phpMyFAQ/Faq/QueryHelperTest.php index 99e78d37e8..ddd4209097 100644 --- a/tests/phpMyFAQ/Faq/QueryHelperTest.php +++ b/tests/phpMyFAQ/Faq/QueryHelperTest.php @@ -8,9 +8,9 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Language; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class QueryHelperTest extends TestCase diff --git a/tests/phpMyFAQ/Faq/StatisticsTest.php b/tests/phpMyFAQ/Faq/StatisticsTest.php index 13c6217823..ae3eab0517 100644 --- a/tests/phpMyFAQ/Faq/StatisticsTest.php +++ b/tests/phpMyFAQ/Faq/StatisticsTest.php @@ -6,10 +6,10 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Language\Plurals; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class StatisticsTest diff --git a/tests/phpMyFAQ/FaqTest.php b/tests/phpMyFAQ/FaqTest.php index 7fcf0ff703..d954e12b1f 100644 --- a/tests/phpMyFAQ/FaqTest.php +++ b/tests/phpMyFAQ/FaqTest.php @@ -8,9 +8,9 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Entity\FaqEntity; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class FaqTest extends TestCase diff --git a/tests/phpMyFAQ/Filesystem/FilesystemTest.php b/tests/phpMyFAQ/Filesystem/FilesystemTest.php index 02e49bfa2e..b2ee1d4a66 100644 --- a/tests/phpMyFAQ/Filesystem/FilesystemTest.php +++ b/tests/phpMyFAQ/Filesystem/FilesystemTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Filesystem; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class FilesystemTest extends TestCase @@ -47,10 +47,7 @@ public function testCreateDirectoryDuplicateDirectory(): void public function testCopy(): void { $this->filesystem->createDirectory(PMF_CONTENT_DIR . '/copy-test'); - $actual = $this->filesystem->copy( - PMF_TEST_DIR . '/path/foo.bar', - PMF_CONTENT_DIR . '/copy-test/foo.bar' - ); + $actual = $this->filesystem->copy(PMF_TEST_DIR . '/path/foo.bar', PMF_CONTENT_DIR . '/copy-test/foo.bar'); $this->assertTrue($actual); $actual = $this->filesystem->deleteDirectory(PMF_CONTENT_DIR . '/copy-test'); @@ -68,7 +65,7 @@ public function testMoveDirectory(): void $this->filesystem->createDirectory(PMF_CONTENT_DIR . '/move-directory-test'); $actual = $this->filesystem->moveDirectory( PMF_CONTENT_DIR . '/move-directory-test', - PMF_CONTENT_DIR . '/move-directory-test-moved' + PMF_CONTENT_DIR . '/move-directory-test-moved', ); $this->assertTrue($actual); $actual = $this->filesystem->deleteDirectory(PMF_CONTENT_DIR . '/move-directory-test-moved'); @@ -91,9 +88,6 @@ public function testRecursiveCopy(): void public function testGetRootPath(): void { - $this->assertEquals( - PMF_TEST_DIR, - $this->filesystem->getRootPath() - ); + $this->assertEquals(PMF_TEST_DIR, $this->filesystem->getRootPath()); } } diff --git a/tests/phpMyFAQ/FilterTest.php b/tests/phpMyFAQ/FilterTest.php index c7971df0e2..24821c8156 100644 --- a/tests/phpMyFAQ/FilterTest.php +++ b/tests/phpMyFAQ/FilterTest.php @@ -38,7 +38,7 @@ public function testFilterInputWithSpecialChars(): void INPUT_GET, 'special_test', FILTER_SANITIZE_SPECIAL_CHARS, - 'safe_default' + 'safe_default', ); $this->assertEquals('safe_default', $resultWithDefault); } else { @@ -73,7 +73,7 @@ public function testFilterArray(): void $testArray = [ 'name' => 'John Doe', 'email' => 'john@example.com', - 'age' => '25' + 'age' => '25', ]; $result = Filter::filterArray($testArray, FILTER_SANITIZE_SPECIAL_CHARS); @@ -86,7 +86,7 @@ public function testFilterArrayWithMaliciousInput(): void { $testArray = [ 'name' => 'John', - 'comment' => 'Good product' + 'comment' => 'Good product', ]; $result = Filter::filterArray($testArray, FILTER_SANITIZE_SPECIAL_CHARS); @@ -202,19 +202,12 @@ public function testFilterInputArrayMethod(): void $definition = [ 'name' => FILTER_FLAG_NO_ENCODE_QUOTES, - 'email' => FILTER_VALIDATE_EMAIL + 'email' => FILTER_VALIDATE_EMAIL, ]; $result = Filter::filterInputArray(INPUT_POST, $definition); - $this->assertThat( - $result, - $this->logicalOr( - $this->isArray(), - $this->isBool(), - $this->isNull() - ) - ); + $this->assertThat($result, $this->logicalOr($this->isArray(), $this->isBool(), $this->isNull())); } public function testFilterWithDifferentTypes(): void diff --git a/tests/phpMyFAQ/Form/FormsRepositoryTest.php b/tests/phpMyFAQ/Form/FormsRepositoryTest.php index 30e83666d9..559d884496 100644 --- a/tests/phpMyFAQ/Form/FormsRepositoryTest.php +++ b/tests/phpMyFAQ/Form/FormsRepositoryTest.php @@ -20,10 +20,10 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database; use phpMyFAQ\Database\Sqlite3; +use phpMyFAQ\Language; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use phpMyFAQ\Language; #[AllowMockObjectsWithoutExpectations] class FormsRepositoryTest extends TestCase @@ -46,16 +46,20 @@ protected function setUp(): void // Seed minimal form row in default language if not exists (portable SQL) $prefix = Database::getTablePrefix(); - $check = $this->configuration->getDb()->query( - "SELECT COUNT(*) AS cnt FROM {$prefix}faqforms WHERE form_id = 1 AND input_id = 1 AND input_lang = 'default'" - ); + $check = $this->configuration + ->getDb() + ->query( + "SELECT COUNT(*) AS cnt FROM {$prefix}faqforms WHERE form_id = 1 AND input_id = 1 AND input_lang = 'default'", + ); $countObj = $this->configuration->getDb()->fetchObject($check); $count = $countObj ? (int) $countObj->cnt : 0; if ($count === 0) { - $this->configuration->getDb()->query( - "INSERT INTO {$prefix}faqforms (form_id, input_id, input_type, input_label, input_lang, input_active, input_required)" - . " VALUES (1, 1, 'text', 'msgContactName', 'default', 1, 1)" - ); + $this->configuration + ->getDb() + ->query( + "INSERT INTO {$prefix}faqforms (form_id, input_id, input_type, input_label, input_lang, input_active, input_required)" + . " VALUES (1, 1, 'text', 'msgContactName', 'default', 1, 1)", + ); } } @@ -68,7 +72,15 @@ public function testFetchFormDataAndTranslationsAndUpdates(): void // Add a translation for 'en' $default = $this->repository->fetchDefaultInputData(1, 1); $this->assertNotNull($default); - $this->assertTrue($this->repository->insertTranslationRow(1, 1, $default->input_type, 'Name', (int) $default->input_active, (int) $default->input_required, 'en')); + $this->assertTrue($this->repository->insertTranslationRow( + 1, + 1, + $default->input_type, + 'Name', + (int) $default->input_active, + (int) $default->input_required, + 'en', + )); $translations = $this->repository->fetchTranslationsByFormAndInput(1, 1); $this->assertNotEmpty($translations); diff --git a/tests/phpMyFAQ/FormsTest.php b/tests/phpMyFAQ/FormsTest.php index bc7224191f..b0ee320873 100644 --- a/tests/phpMyFAQ/FormsTest.php +++ b/tests/phpMyFAQ/FormsTest.php @@ -4,9 +4,9 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Database\Sqlite3; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class FormsTest extends TestCase diff --git a/tests/phpMyFAQ/Glossary/GlossaryHelperTest.php b/tests/phpMyFAQ/Glossary/GlossaryHelperTest.php index 0eee1b3162..0f3bdf4cc2 100644 --- a/tests/phpMyFAQ/Glossary/GlossaryHelperTest.php +++ b/tests/phpMyFAQ/Glossary/GlossaryHelperTest.php @@ -19,8 +19,8 @@ namespace phpMyFAQ\Glossary; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class GlossaryHelperTest extends TestCase diff --git a/tests/phpMyFAQ/GlossaryTest.php b/tests/phpMyFAQ/GlossaryTest.php index 1904822ae2..89b016b63d 100644 --- a/tests/phpMyFAQ/GlossaryTest.php +++ b/tests/phpMyFAQ/GlossaryTest.php @@ -3,10 +3,11 @@ namespace phpMyFAQ; use phpMyFAQ\Database\Sqlite3; -use phpMyFAQ\Glossary\GlossaryRepository;use PHPUnit\Framework\MockObject\Exception; +use phpMyFAQ\Glossary\GlossaryRepository; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class GlossaryTest extends TestCase @@ -89,7 +90,8 @@ public function testFetchAll(): void public function testInsertItemsIntoContent(): void { - $glossary = $this->getMockBuilder(Glossary::class) + $glossary = $this + ->getMockBuilder(Glossary::class) ->disableOriginalConstructor() ->onlyMethods(['fetchAll']) ->getMock(); @@ -151,7 +153,8 @@ public function testCacheInvalidationOnCreateUpdateDelete(): void public function testRepositoryErrorHandling(): void { - $repoMock = $this->getMockBuilder(GlossaryRepository::class) + $repoMock = $this + ->getMockBuilder(GlossaryRepository::class) ->disableOriginalConstructor() ->onlyMethods(['create', 'update', 'delete', 'fetchAll', 'fetch']) ->getMock(); @@ -163,8 +166,8 @@ public function testRepositoryErrorHandling(): void $glossary = new Glossary($this->configuration, $repoMock); $glossary->setLanguage('en'); - $this->assertFalse($glossary->create('x','y')); - $this->assertFalse($glossary->update(1,'x','y')); + $this->assertFalse($glossary->create('x', 'y')); + $this->assertFalse($glossary->update(1, 'x', 'y')); $this->assertFalse($glossary->delete(1)); $this->assertEmpty($glossary->fetchAll()); $this->assertEmpty($glossary->fetch(1)); diff --git a/tests/phpMyFAQ/Helper/AttachmentHelperTest.php b/tests/phpMyFAQ/Helper/AttachmentHelperTest.php index c1c33a2fcb..8efcae5fea 100644 --- a/tests/phpMyFAQ/Helper/AttachmentHelperTest.php +++ b/tests/phpMyFAQ/Helper/AttachmentHelperTest.php @@ -4,8 +4,8 @@ use phpMyFAQ\Attachment\AbstractAttachment; use phpMyFAQ\Translation; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class AttachmentHelperTest extends TestCase diff --git a/tests/phpMyFAQ/Helper/CategoryHelperTest.php b/tests/phpMyFAQ/Helper/CategoryHelperTest.php index b4841b80ad..d5b07e2bea 100644 --- a/tests/phpMyFAQ/Helper/CategoryHelperTest.php +++ b/tests/phpMyFAQ/Helper/CategoryHelperTest.php @@ -8,10 +8,10 @@ use phpMyFAQ\Database\DatabaseDriver; use phpMyFAQ\Language; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class CategoryHelperTest extends TestCase @@ -36,7 +36,8 @@ protected function setUp(): void $this->mockConfiguration = $this->createStub(Configuration::class); $this->mockCategory = $this->createStub(Category::class); - $this->categoryHelper = $this->getMockBuilder(CategoryHelper::class) + $this->categoryHelper = $this + ->getMockBuilder(CategoryHelper::class) ->onlyMethods(['getCategory', 'getConfiguration']) ->getMock(); @@ -150,7 +151,8 @@ public function testRenderCategoryTreeWithCategories(): void ]); $mockRelation->method('getAggregatedFaqNumbers')->willReturn([1 => 8, 2 => 3]); - $categoryHelper = $this->getMockBuilder(CategoryHelper::class) + $categoryHelper = $this + ->getMockBuilder(CategoryHelper::class) ->onlyMethods(['getCategory', 'getConfiguration', 'normalizeCategoryTree', 'buildCategoryList']) ->getMock(); @@ -195,7 +197,8 @@ public function testRenderCategoryTreeWithoutCategories(): void 'French' => 'Catégorie française', ]); - $categoryHelper = $this->getMockBuilder(CategoryHelper::class) + $categoryHelper = $this + ->getMockBuilder(CategoryHelper::class) ->onlyMethods(['getCategory', 'getConfiguration', 'buildAvailableCategoryTranslationsList']) ->getMock(); diff --git a/tests/phpMyFAQ/Helper/FaqHelperTest.php b/tests/phpMyFAQ/Helper/FaqHelperTest.php index f4e14b04ce..d120bba7bf 100644 --- a/tests/phpMyFAQ/Helper/FaqHelperTest.php +++ b/tests/phpMyFAQ/Helper/FaqHelperTest.php @@ -10,9 +10,9 @@ use phpMyFAQ\Strings; use phpMyFAQ\System; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class FaqHelperTest extends TestCase diff --git a/tests/phpMyFAQ/Helper/PermissionHelperTest.php b/tests/phpMyFAQ/Helper/PermissionHelperTest.php index 5e1690e538..261f540597 100644 --- a/tests/phpMyFAQ/Helper/PermissionHelperTest.php +++ b/tests/phpMyFAQ/Helper/PermissionHelperTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Helper; -use PHPUnit\Framework\MockObject\Exception; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class PermissionHelperTest extends TestCase diff --git a/tests/phpMyFAQ/Helper/QuestionHelperTest.php b/tests/phpMyFAQ/Helper/QuestionHelperTest.php index f302745dd6..036c7a51f7 100644 --- a/tests/phpMyFAQ/Helper/QuestionHelperTest.php +++ b/tests/phpMyFAQ/Helper/QuestionHelperTest.php @@ -12,9 +12,9 @@ use phpMyFAQ\Helper\QuestionHelper; use phpMyFAQ\Language; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class QuestionHelperTest extends TestCase diff --git a/tests/phpMyFAQ/Helper/RegistrationHelperTest.php b/tests/phpMyFAQ/Helper/RegistrationHelperTest.php index 7ddf2a6693..5d8955c8ee 100644 --- a/tests/phpMyFAQ/Helper/RegistrationHelperTest.php +++ b/tests/phpMyFAQ/Helper/RegistrationHelperTest.php @@ -6,10 +6,10 @@ use phpMyFAQ\Mail; use phpMyFAQ\User; use phpMyFAQ\User\UserData; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class RegistrationHelperTest extends TestCase @@ -47,7 +47,8 @@ public function testIsDomainAllowedWithEmptyWhitelist(): void { $email = 'test@example.com'; - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn(''); @@ -63,7 +64,8 @@ public function testIsDomainAllowedWithAllowedDomain(): void $email = 'test@example.com'; $whitelist = 'example.com,allowed.org'; - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn($whitelist); @@ -78,7 +80,8 @@ public function testIsDomainAllowedWithNotAllowedDomain(): void $email = 'test@notallowed.com'; $whitelist = 'example.com,allowed.org'; - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn($whitelist); @@ -99,7 +102,8 @@ public function testIsDomainAllowedWithMultipleDomainsInWhitelist(): void $whitelist = 'example.com, allowed.org, trusted.edu'; - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn($whitelist); @@ -115,7 +119,8 @@ public function testIsDomainAllowedWithWhitespaceInWhitelist(): void $email = 'test@spaced.com'; $whitelist = ' spaced.com , another.com '; // With extra spaces - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn($whitelist); @@ -129,7 +134,8 @@ public function testIsDomainAllowedWithNullWhitelist(): void { $email = 'test@example.com'; - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn(''); @@ -144,7 +150,8 @@ public function testIsDomainAllowedWithComplexEmail(): void $email = 'user.name+tag@sub.example.com'; $whitelist = 'sub.example.com'; - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn($whitelist); @@ -159,7 +166,8 @@ public function testIsDomainAllowedCaseSensitive(): void $email = 'test@Example.COM'; $whitelist = 'example.com'; - $this->configurationMock->expects($this->once()) + $this->configurationMock + ->expects($this->once()) ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn($whitelist); @@ -174,20 +182,18 @@ public function testAllPublicMethodsExist(): void $expectedMethods = [ '__construct', 'createUser', - 'isDomainAllowed' + 'isDomainAllowed', ]; foreach ($expectedMethods as $methodName) { - $this->assertTrue( - method_exists($this->registrationHelper, $methodName), - "Method $methodName should exist" - ); + $this->assertTrue(method_exists($this->registrationHelper, $methodName), "Method $methodName should exist"); } } public function testMethodReturnTypes(): void { - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn('example.com'); @@ -197,7 +203,8 @@ public function testMethodReturnTypes(): void public function testIsDomainAllowedEdgeCases(): void { - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn('example.com'); @@ -219,7 +226,8 @@ public function testDomainExtractionFromEmail(): void 'test+tag@example.co.uk' => 'example.co.uk', ]; - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn('domain.com,sub.domain.org,example.co.uk'); @@ -259,7 +267,8 @@ public function testConfigurationDependency(): void public function testIsDomainAllowedBoundaryConditions(): void { // Test single character domain - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn('a.b'); @@ -272,7 +281,8 @@ public function testIsDomainAllowedBoundaryConditions(): void public function testIsDomainAllowedWithoutAtSignReturnsFalse(): void { - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn('example.com'); @@ -281,7 +291,8 @@ public function testIsDomainAllowedWithoutAtSignReturnsFalse(): void public function testIsDomainAllowedWithMultipleAtSignsReturnsFalse(): void { - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn('example.com'); @@ -290,7 +301,8 @@ public function testIsDomainAllowedWithMultipleAtSignsReturnsFalse(): void public function testIsDomainAllowedWithEmptyEmailReturnsFalse(): void { - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('security.domainWhiteListForRegistrations') ->willReturn('example.com'); diff --git a/tests/phpMyFAQ/Helper/SearchHelperTest.php b/tests/phpMyFAQ/Helper/SearchHelperTest.php index b8dfd2f235..8c4aa8d099 100644 --- a/tests/phpMyFAQ/Helper/SearchHelperTest.php +++ b/tests/phpMyFAQ/Helper/SearchHelperTest.php @@ -5,11 +5,11 @@ use phpMyFAQ\Category; use phpMyFAQ\Configuration; use phpMyFAQ\Search\SearchResultSet; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SearchHelperTest extends TestCase @@ -68,12 +68,12 @@ public function testCreateAutoCompleteResultWithResults(): void { $this->searchHelper->setSearchTerm('php test'); - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('records.numberOfRecordsPerPage') ->willReturn(10); - $this->configurationMock->method('getDefaultUrl') - ->willReturn('https://example.com/'); + $this->configurationMock->method('getDefaultUrl')->willReturn('https://example.com/'); $mockResult = new stdClass(); $mockResult->category_id = 1; @@ -84,7 +84,10 @@ public function testCreateAutoCompleteResultWithResults(): void $this->searchResultSetMock->method('getNumberOfResults')->willReturn(1); $this->searchResultSetMock->method('getResultSet')->willReturn([$mockResult]); - $this->categoryMock->method('getPath')->with(1)->willReturn('Programming/PHP'); + $this->categoryMock + ->method('getPath') + ->with(1) + ->willReturn('Programming/PHP'); $result = $this->searchHelper->createAutoCompleteResult($this->searchResultSetMock); @@ -99,12 +102,12 @@ public function testCreateAutoCompleteResultWithResults(): void public function testCreateAutoCompleteResultLimitsResults(): void { - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('records.numberOfRecordsPerPage') ->willReturn(2); - $this->configurationMock->method('getDefaultUrl') - ->willReturn('https://example.com/'); + $this->configurationMock->method('getDefaultUrl')->willReturn('https://example.com/'); $mockResults = []; for ($i = 1; $i <= 5; $i++) { @@ -139,12 +142,12 @@ public function testRenderAdminSuggestionResultWithNoResults(): void public function testRenderAdminSuggestionResultWithResults(): void { - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->with('records.numberOfRecordsPerPage') ->willReturn(10); - $this->configurationMock->method('getDefaultUrl') - ->willReturn('https://example.com/'); + $this->configurationMock->method('getDefaultUrl')->willReturn('https://example.com/'); $mockResult = new stdClass(); $mockResult->id = 123; @@ -184,14 +187,14 @@ public function testGetSearchResultWithResults(): void { $this->searchHelper->setSearchTerm('php programming'); - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->willReturnMap([ ['records.numberOfRecordsPerPage', 10], - ['search.enableHighlighting', true] + ['search.enableHighlighting', true], ]); - $this->configurationMock->method('getDefaultUrl') - ->willReturn('https://example.com/'); + $this->configurationMock->method('getDefaultUrl')->willReturn('https://example.com/'); $mockResult = new stdClass(); $mockResult->id = 123; @@ -205,10 +208,12 @@ public function testGetSearchResultWithResults(): void $this->searchResultSetMock->method('getResultSet')->willReturn([$mockResult]); $this->categoryMock->method('setLanguage')->with('en'); - $this->categoryMock->method('getCategoriesFromFaq') + $this->categoryMock + ->method('getCategoriesFromFaq') ->with(123) ->willReturn([1 => ['id' => 1, 'name' => 'Programming']]); - $this->categoryMock->method('getPath') + $this->categoryMock + ->method('getPath') ->with(1) ->willReturn('Programming/PHP'); @@ -230,14 +235,14 @@ public function testGetSearchResultWithResults(): void public function testGetSearchResultWithPagination(): void { - $this->configurationMock->method('get') + $this->configurationMock + ->method('get') ->willReturnMap([ ['records.numberOfRecordsPerPage', 2], - ['search.enableHighlighting', false] + ['search.enableHighlighting', false], ]); - $this->configurationMock->method('getDefaultUrl') - ->willReturn('https://example.com/'); + $this->configurationMock->method('getDefaultUrl')->willReturn('https://example.com/'); $mockResults = []; for ($i = 1; $i <= 5; $i++) { @@ -332,8 +337,7 @@ public function testRenderRelatedFaqsWithNoResults(): void public function testRenderRelatedFaqsWithResults(): void { - $this->configurationMock->method('getDefaultUrl') - ->willReturn('https://example.com/'); + $this->configurationMock->method('getDefaultUrl')->willReturn('https://example.com/'); $mockResults = []; @@ -373,8 +377,7 @@ public function testRenderRelatedFaqsWithResults(): void public function testRenderRelatedFaqsLimitsToFiveResults(): void { - $this->configurationMock->method('getDefaultUrl') - ->willReturn('https://example.com/'); + $this->configurationMock->method('getDefaultUrl')->willReturn('https://example.com/'); $mockResults = []; for ($i = 1; $i <= 8; $i++) { @@ -417,14 +420,11 @@ public function testAllPublicMethodsExist(): void 'createAutoCompleteResult', 'renderAdminSuggestionResult', 'getSearchResult', - 'renderRelatedFaqs' + 'renderRelatedFaqs', ]; foreach ($expectedMethods as $methodName) { - $this->assertTrue( - method_exists($this->searchHelper, $methodName), - "Method $methodName should exist" - ); + $this->assertTrue(method_exists($this->searchHelper, $methodName), "Method $methodName should exist"); } } diff --git a/tests/phpMyFAQ/Helper/StatisticsHelperTest.php b/tests/phpMyFAQ/Helper/StatisticsHelperTest.php index c236769c7a..b96419be45 100644 --- a/tests/phpMyFAQ/Helper/StatisticsHelperTest.php +++ b/tests/phpMyFAQ/Helper/StatisticsHelperTest.php @@ -6,11 +6,11 @@ use phpMyFAQ\Date; use phpMyFAQ\Translation; use phpMyFAQ\Visits; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class StatisticsHelperTest extends TestCase @@ -29,11 +29,7 @@ protected function setUp(): void // ensure Translation is initialized for calls to Translation::get in helper Translation::create(); - $this->statisticsHelper = new StatisticsHelper( - $this->sessionMock, - $this->visitsMock, - $this->dateMock - ); + $this->statisticsHelper = new StatisticsHelper($this->sessionMock, $this->visitsMock, $this->dateMock); $_SERVER = []; } @@ -45,18 +41,15 @@ protected function tearDown(): void public function testConstructor(): void { - $helper = new StatisticsHelper( - $this->sessionMock, - $this->visitsMock, - $this->dateMock - ); + $helper = new StatisticsHelper($this->sessionMock, $this->visitsMock, $this->dateMock); $this->assertInstanceOf(StatisticsHelper::class, $helper); } public function testGetTrackingFilesStatisticsStructure(): void { - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('getTrackingFileDateStart') ->willReturn(0); @@ -75,7 +68,8 @@ public function testGetTrackingFilesStatisticsStructure(): void public function testGetTrackingFilesStatisticsWithMockedValidDates(): void { $callCount = 0; - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('getTrackingFileDateStart') ->willReturnCallback(function () use (&$callCount) { $callCount++; @@ -94,7 +88,8 @@ public function testGetTrackingFilesStatisticsWithMockedValidDates(): void public function testGetAllTrackingDatesStructure(): void { - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('getTrackingFileDateStart') ->willReturnCallback(function ($filename) { if (strlen($filename) === 16 && str_starts_with($filename, 'tracking')) { @@ -117,7 +112,8 @@ public function testDeleteTrackingFilesBasicBehavior(): void { $month = '012024'; - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('getTrackingFileDateStart') ->willReturnCallback(function ($filename) use ($month) { if (strpos($filename, 'tracking') === 0 && strpos($filename, $month) !== false) { @@ -126,7 +122,8 @@ public function testDeleteTrackingFilesBasicBehavior(): void return 0; }); - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('getTrackingFileDateEnd') ->willReturnCallback(function ($filename) use ($month) { if (strpos($filename, 'tracking') === 0 && strpos($filename, $month) !== false) { @@ -135,7 +132,8 @@ public function testDeleteTrackingFilesBasicBehavior(): void return 0; }); - $this->sessionMock->expects($this->once()) + $this->sessionMock + ->expects($this->once()) ->method('deleteSessions') ->willReturn(true); @@ -146,10 +144,10 @@ public function testDeleteTrackingFilesBasicBehavior(): void public function testClearAllVisitsBasicBehavior(): void { - $this->visitsMock->expects($this->once()) - ->method('resetAll'); + $this->visitsMock->expects($this->once())->method('resetAll'); - $this->sessionMock->expects($this->once()) + $this->sessionMock + ->expects($this->once()) ->method('deleteAllSessions') ->willReturn(true); @@ -161,7 +159,8 @@ public function testClearAllVisitsBasicBehavior(): void public function testRenderMonthSelectorStructure(): void { // This test remains unchanged as it relies on getAllTrackingDates - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('getTrackingFileDateStart') ->willReturn(1704067200); @@ -171,7 +170,8 @@ public function testRenderMonthSelectorStructure(): void public function testRenderDaySelectorStructure(): void { - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('getTrackingFileDateStart') ->willReturn(1704067200); @@ -195,20 +195,14 @@ public function testAllPublicMethodsExist(): void 'deleteTrackingFiles', 'clearAllVisits', 'renderMonthSelector', - 'renderDaySelector' + 'renderDaySelector', ]; foreach ($expectedMethods as $methodName) { - $this->assertTrue( - $reflection->hasMethod($methodName), - "Method $methodName should exist" - ); + $this->assertTrue($reflection->hasMethod($methodName), "Method $methodName should exist"); $method = $reflection->getMethod($methodName); - $this->assertTrue( - $method->isPublic(), - "Method $methodName should be public" - ); + $this->assertTrue($method->isPublic(), "Method $methodName should be public"); } } @@ -217,19 +211,23 @@ public function testAllPublicMethodsExist(): void */ public function testMethodReturnTypes(): void { - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('getTrackingFileDateStart') ->willReturn(1704067200); - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('format') ->willReturn('2024-01-01 12:00'); - $this->sessionMock->expects($this->any()) + $this->sessionMock + ->expects($this->any()) ->method('deleteSessions') ->willReturn(true); - $this->sessionMock->expects($this->any()) + $this->sessionMock + ->expects($this->any()) ->method('deleteAllSessions') ->willReturn(true); @@ -256,7 +254,8 @@ public function testMethodReturnTypes(): void */ public function testEdgeCaseHandling(): void { - $this->dateMock->expects($this->any()) + $this->dateMock + ->expects($this->any()) ->method('getTrackingFileDateStart') ->willReturn(0); @@ -275,10 +274,7 @@ public function testReadonlyClassBehavior(): void $reflection = new ReflectionClass(StatisticsHelper::class); if (method_exists($reflection, 'isReadOnly')) { - $this->assertTrue( - $reflection->isReadOnly(), - 'StatisticsHelper should be a readonly class' - ); + $this->assertTrue($reflection->isReadOnly(), 'StatisticsHelper should be a readonly class'); } $constructor = $reflection->getConstructor(); diff --git a/tests/phpMyFAQ/Helper/TagsHelperTest.php b/tests/phpMyFAQ/Helper/TagsHelperTest.php index baa2ccaf1d..a32b0e3da0 100644 --- a/tests/phpMyFAQ/Helper/TagsHelperTest.php +++ b/tests/phpMyFAQ/Helper/TagsHelperTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Helper; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class TagsHelperTest extends TestCase @@ -24,10 +24,11 @@ public function testRenderTagList() $this->tagsHelper->setTaggingIds([1, 2]); - $expectedOutput = 'tag1 ' . - ' ' . - 'tag2 ' . - ' '; + $expectedOutput = + 'tag1 ' + . ' ' + . 'tag2 ' + . ' '; $result = $this->tagsHelper->renderTagList($tags); $this->assertEquals($expectedOutput, $result); @@ -40,8 +41,9 @@ public function testRenderSearchTag() $this->tagsHelper->setTaggingIds([1, 2]); - $expectedOutput = 'tag1 ' . - ' '; + $expectedOutput = + 'tag1 ' + . ' '; $result = $this->tagsHelper->renderSearchTag($tagId, $tagName); $this->assertEquals($expectedOutput, $result); @@ -64,12 +66,12 @@ public function testRenderRelatedTag() $this->tagsHelper->setTaggingIds([2, 3]); - $expectedOutput = '' . - ' tag1 ' . - '10'; + $expectedOutput = + '' + . ' tag1 ' + . '10'; $result = $this->tagsHelper->renderRelatedTag($tagId, $tagName, $relevance); $this->assertEquals($expectedOutput, $result); } } - diff --git a/tests/phpMyFAQ/Helper/UserHelperTest.php b/tests/phpMyFAQ/Helper/UserHelperTest.php index 4fa51faa17..3cee670f24 100644 --- a/tests/phpMyFAQ/Helper/UserHelperTest.php +++ b/tests/phpMyFAQ/Helper/UserHelperTest.php @@ -3,9 +3,9 @@ namespace phpMyFAQ\Helper; use phpMyFAQ\User; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test case for UserHelper class @@ -57,7 +57,7 @@ public function testGetAllUsersForTemplateWithDefaultParameters(): void $expected = [ ['id' => 1, 'selected' => true, 'displayName' => 'User One', 'login' => 'user1'], ['id' => 2, 'selected' => false, 'displayName' => 'User Two', 'login' => 'user2'], - ['id' => 3, 'selected' => false, 'displayName' => 'User Three', 'login' => 'user3'] + ['id' => 3, 'selected' => false, 'displayName' => 'User Three', 'login' => 'user3'], ]; $this->assertEquals($expected, $result); @@ -74,9 +74,7 @@ public function testGetAllUsersForTemplateWithSelectedUser(): void ->with(true, false) ->willReturn($userIds); - $this->userMock - ->expects($this->exactly(3)) - ->method('getUserById'); + $this->userMock->expects($this->exactly(3))->method('getUserById'); $this->userMock ->expects($this->exactly(3)) @@ -105,9 +103,7 @@ public function testGetAllUsersForTemplateWithAllowBlockedUsers(): void ->with(true, true) ->willReturn($userIds); - $this->userMock - ->expects($this->exactly(2)) - ->method('getUserById'); + $this->userMock->expects($this->exactly(2))->method('getUserById'); $this->userMock ->expects($this->exactly(2)) diff --git a/tests/phpMyFAQ/Instance/ClientTest.php b/tests/phpMyFAQ/Instance/ClientTest.php index 49dfad6a52..ce3f1dbbfa 100644 --- a/tests/phpMyFAQ/Instance/ClientTest.php +++ b/tests/phpMyFAQ/Instance/ClientTest.php @@ -5,8 +5,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\DatabaseDriver; use phpMyFAQ\Filesystem\Filesystem; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Class ClientTest diff --git a/tests/phpMyFAQ/Instance/DatabaseTest.php b/tests/phpMyFAQ/Instance/DatabaseTest.php index ebaf9c4a39..c7a802b0ae 100644 --- a/tests/phpMyFAQ/Instance/DatabaseTest.php +++ b/tests/phpMyFAQ/Instance/DatabaseTest.php @@ -5,8 +5,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Core\Exception; use phpMyFAQ\Database\DatabaseDriver; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class DatabaseTest extends TestCase diff --git a/tests/phpMyFAQ/Instance/MainTest.php b/tests/phpMyFAQ/Instance/MainTest.php index 7a016b1cd9..a83bea1cfa 100644 --- a/tests/phpMyFAQ/Instance/MainTest.php +++ b/tests/phpMyFAQ/Instance/MainTest.php @@ -4,9 +4,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Instance; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class MainTest extends TestCase diff --git a/tests/phpMyFAQ/Instance/SetupTest.php b/tests/phpMyFAQ/Instance/SetupTest.php index f53c1d9117..f7901f3c8a 100644 --- a/tests/phpMyFAQ/Instance/SetupTest.php +++ b/tests/phpMyFAQ/Instance/SetupTest.php @@ -5,9 +5,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Core\Exception; use phpMyFAQ\User; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SetupTest extends TestCase @@ -38,7 +38,7 @@ public function testSetRootDir(): void $this->assertSame($rootDir, $property->getValue($this->setup)); } - + public function testCreateDatabaseFile(): void { $data = [ diff --git a/tests/phpMyFAQ/InstanceTest.php b/tests/phpMyFAQ/InstanceTest.php index a8cff681d1..c88c677c35 100644 --- a/tests/phpMyFAQ/InstanceTest.php +++ b/tests/phpMyFAQ/InstanceTest.php @@ -4,13 +4,14 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Entity\InstanceEntity; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class InstanceTest extends TestCase { private Instance $instance; + protected function setUp(): void { parent::setUp(); @@ -25,10 +26,7 @@ protected function setUp(): void public function testCreate(): void { $instance = new InstanceEntity(); - $instance - ->setUrl('http://two.localhost') - ->setInstance('Second localhost') - ->setComment('Test instance'); + $instance->setUrl('http://two.localhost')->setInstance('Second localhost')->setComment('Test instance'); $this->assertEquals(2, $this->instance->create($instance)); $this->instance->delete(2); @@ -40,10 +38,7 @@ public function testGetAll(): void $this->assertCount(1, $instances); // Only one instance is created by default $instance = new InstanceEntity(); - $instance - ->setUrl('http://two.localhost') - ->setInstance('Second localhost') - ->setComment('Test instance'); + $instance->setUrl('http://two.localhost')->setInstance('Second localhost')->setComment('Test instance'); $this->instance->create($instance); $this->assertCount(2, $this->instance->getAll()); @@ -53,10 +48,7 @@ public function testGetAll(): void public function testGetById(): void { $instance = new InstanceEntity(); - $instance - ->setUrl('http://two.localhost') - ->setInstance('Second localhost') - ->setComment('Test instance'); + $instance->setUrl('http://two.localhost')->setInstance('Second localhost')->setComment('Test instance'); $id = $this->instance->create($instance); $instance = $this->instance->getById($id); @@ -70,16 +62,10 @@ public function testGetById(): void public function testUpdate(): void { $instance = new InstanceEntity(); - $instance - ->setUrl('http://two.localhost') - ->setInstance('Second localhost') - ->setComment('Test instance'); + $instance->setUrl('http://two.localhost')->setInstance('Second localhost')->setComment('Test instance'); $id = $this->instance->create($instance); - $instance - ->setUrl('http://three.localhost') - ->setInstance('Third localhost') - ->setComment('Test instance'); + $instance->setUrl('http://three.localhost')->setInstance('Third localhost')->setComment('Test instance'); $this->assertTrue($this->instance->update($id, $instance)); $instance = $this->instance->getById($id); @@ -93,10 +79,7 @@ public function testUpdate(): void public function testAddConfig(): void { $instance = new InstanceEntity(); - $instance - ->setUrl('http://two.localhost') - ->setInstance('Second localhost') - ->setComment('Test instance'); + $instance->setUrl('http://two.localhost')->setInstance('Second localhost')->setComment('Test instance'); $id = $this->instance->create($instance); $this->instance->addConfig('foo', 'bar'); @@ -108,10 +91,7 @@ public function testAddConfig(): void public function testGetInstanceConfig(): void { $instance = new InstanceEntity(); - $instance - ->setUrl('http://two.localhost') - ->setInstance('Second localhost') - ->setComment('Test instance'); + $instance->setUrl('http://two.localhost')->setInstance('Second localhost')->setComment('Test instance'); $id = $this->instance->create($instance); $this->instance->addConfig('foo', 'bar'); diff --git a/tests/phpMyFAQ/Language/LanguageCodesTest.php b/tests/phpMyFAQ/Language/LanguageCodesTest.php index 192d8dca17..eaa6da70e0 100644 --- a/tests/phpMyFAQ/Language/LanguageCodesTest.php +++ b/tests/phpMyFAQ/Language/LanguageCodesTest.php @@ -17,8 +17,8 @@ namespace phpMyFAQ\Language; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class LanguageCodesTest extends TestCase diff --git a/tests/phpMyFAQ/Language/PluralsTest.php b/tests/phpMyFAQ/Language/PluralsTest.php index 7544315b77..22e072ba48 100644 --- a/tests/phpMyFAQ/Language/PluralsTest.php +++ b/tests/phpMyFAQ/Language/PluralsTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Language; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use ReflectionMethod; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class PluralsTest extends TestCase @@ -326,9 +326,21 @@ public function testMultipleGermanicLanguages(): void $germanicLanguages = ['da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'it', 'nb', 'nl', 'hu', 'pt', 'sv']; foreach ($germanicLanguages as $lang) { - $this->assertEquals(1, $this->pluralMethod->invoke($this->plurals, $lang, 0), "Language $lang failed for 0"); - $this->assertEquals(0, $this->pluralMethod->invoke($this->plurals, $lang, 1), "Language $lang failed for 1"); - $this->assertEquals(1, $this->pluralMethod->invoke($this->plurals, $lang, 2), "Language $lang failed for 2"); + $this->assertEquals( + 1, + $this->pluralMethod->invoke($this->plurals, $lang, 0), + "Language $lang failed for 0", + ); + $this->assertEquals( + 0, + $this->pluralMethod->invoke($this->plurals, $lang, 1), + "Language $lang failed for 1", + ); + $this->assertEquals( + 1, + $this->pluralMethod->invoke($this->plurals, $lang, 2), + "Language $lang failed for 2", + ); } } diff --git a/tests/phpMyFAQ/LanguageTest.php b/tests/phpMyFAQ/LanguageTest.php index 36555b87f2..4db3820d07 100644 --- a/tests/phpMyFAQ/LanguageTest.php +++ b/tests/phpMyFAQ/LanguageTest.php @@ -3,10 +3,10 @@ namespace phpMyFAQ; use phpMyFAQ\Database\Sqlite3; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class LanguageTest extends TestCase @@ -47,8 +47,8 @@ protected function tearDown(): void public function testIsLanguageAvailableWithId(): void { $this->dbHandle->query( - 'INSERT INTO faqdata (id, lang, solution_id, active, sticky, thema, author, email, updated) VALUES' . - '(999, "en", 1001, 1, 1, "Test", "Author", "test@example.org", DATETIME("now", "localtime"))' + 'INSERT INTO faqdata (id, lang, solution_id, active, sticky, thema, author, email, updated) VALUES' + . '(999, "en", 1001, 1, 1, "Test", "Author", "test@example.org", DATETIME("now", "localtime"))', ); $result = $this->language->isLanguageAvailable(999); diff --git a/tests/phpMyFAQ/LdapTest.php b/tests/phpMyFAQ/LdapTest.php index 76b51a3997..28043fe08e 100644 --- a/tests/phpMyFAQ/LdapTest.php +++ b/tests/phpMyFAQ/LdapTest.php @@ -30,14 +30,16 @@ protected function setUp(): void $this->configuration = $this->createStub(Configuration::class); // Mock der LDAP-Konfiguration - $this->configuration->method('getLdapConfig')->willReturn([ - 'ldap_mapping' => [ - 'username' => 'uid', - 'mail' => 'mail', - 'name' => 'cn', - 'memberOf' => 'CN=TestGroup,DC=example,DC=com' - ] - ]); + $this->configuration + ->method('getLdapConfig') + ->willReturn([ + 'ldap_mapping' => [ + 'username' => 'uid', + 'mail' => 'mail', + 'name' => 'cn', + 'memberOf' => 'CN=TestGroup,DC=example,DC=com', + ], + ]); $this->configuration->method('getLdapOptions')->willReturn([]); @@ -66,12 +68,12 @@ public function testConnectWithEmptyBase(): void public function testQuoteMethod(): void { $testCases = [ - ['simple', 'simple'], - ['test*', 'test\\2a'], - ['test()', 'test\\28\\29'], - ['test space', 'test\\20space'], + ['simple', 'simple'], + ['test*', 'test\\2a'], + ['test()', 'test\\28\\29'], + ['test space', 'test\\20space'], ['test\\slash', 'test\\5cslash'], - ['complex*()', 'complex\\2a\\28\\29'] + ['complex*()', 'complex\\2a\\28\\29'], ]; foreach ($testCases as [$input, $expected]) { @@ -139,7 +141,8 @@ public function testBindWithoutConnection(): void public function testConnectionStateValidation(): void { // Create a partial mock that allows us to test the validation logic - $ldapMock = $this->getMockBuilder(Ldap::class) + $ldapMock = $this + ->getMockBuilder(Ldap::class) ->setConstructorArgs([$this->configuration]) ->onlyMethods(['error']) ->getMock(); @@ -208,10 +211,12 @@ public function testConnectWithLdapExtension(): void // Test connection failure scenarios safely without triggering warnings $this->configuration->method('getLdapOptions')->willReturn([]); - $this->configuration->method('get')->willReturnMap([ - ['ldap.ldap_use_dynamic_login', false], - ['ldap.ldap_use_anonymous_login', false] - ]); + $this->configuration + ->method('get') + ->willReturnMap([ + ['ldap.ldap_use_dynamic_login', false], + ['ldap.ldap_use_anonymous_login', false], + ]); // Test with obviously invalid parameters to ensure graceful failure // Use empty strings which are validated before ldap_connect @@ -228,7 +233,8 @@ public function testLdapMethodsWithProperErrorHandling(): void // These tests expect the methods to check connection state before calling LDAP functions // Create a spy/partial mock that can track method calls - $ldapSpy = $this->getMockBuilder(Ldap::class) + $ldapSpy = $this + ->getMockBuilder(Ldap::class) ->setConstructorArgs([$this->configuration]) ->onlyMethods([]) // Don't mock any methods, use real implementation ->getMock(); @@ -248,10 +254,10 @@ public function testLdapMethodsWithProperErrorHandling(): void public function testQuoteMethodEdgeCases(): void { $testCases = [ - ['', ''], - ['normal_text', 'normal_text'], - ['\\()* ', '\\5c\\28\\29\\2a\\20'], - ['a\\b(c)d*e f', 'a\\5cb\\28c\\29d\\2ae\\20f'] + ['', ''], + ['normal_text', 'normal_text'], + ['\\()* ', '\\5c\\28\\29\\2a\\20'], + ['a\\b(c)d*e f', 'a\\5cb\\28c\\29d\\2ae\\20f'], ]; foreach ($testCases as [$input, $expected]) { @@ -306,13 +312,15 @@ public function testConnectionParameterValidation(): void public function testConfigurationInjection(): void { $mockConfig = $this->createStub(Configuration::class); - $mockConfig->method('getLdapConfig')->willReturn([ - 'ldap_mapping' => [ - 'username' => 'sAMAccountName', - 'mail' => 'mail', - 'name' => 'displayName' - ] - ]); + $mockConfig + ->method('getLdapConfig') + ->willReturn([ + 'ldap_mapping' => [ + 'username' => 'sAMAccountName', + 'mail' => 'mail', + 'name' => 'displayName', + ], + ]); $ldap = new Ldap($mockConfig); $this->assertInstanceOf(Ldap::class, $ldap); @@ -339,11 +347,7 @@ public function testSearchFilterConstruction(): void // Test memberOf filter construction $memberOfDN = 'CN=Admins,OU=Groups,DC=example,DC=com'; - $complexFilter = sprintf( - '(&%s(memberOf:1.2.840.113556.1.4.1941:=%s))', - $basicFilter, - $memberOfDN - ); + $complexFilter = sprintf('(&%s(memberOf:1.2.840.113556.1.4.1941:=%s))', $basicFilter, $memberOfDN); $this->assertStringContainsString('(&', $complexFilter); $this->assertStringContainsString('memberOf:', $complexFilter); diff --git a/tests/phpMyFAQ/Link/LinkStrategyRegistryDiTest.php b/tests/phpMyFAQ/Link/LinkStrategyRegistryDiTest.php index fc378c219b..f9951add97 100644 --- a/tests/phpMyFAQ/Link/LinkStrategyRegistryDiTest.php +++ b/tests/phpMyFAQ/Link/LinkStrategyRegistryDiTest.php @@ -10,8 +10,8 @@ use phpMyFAQ\Link\Strategy\StrategyInterface; use phpMyFAQ\Link\Strategy\StrategyRegistry; use phpMyFAQ\Strings; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class LinkStrategyRegistryDiTest extends TestCase @@ -65,4 +65,3 @@ public function build(array $params, Link $link): string $this->assertTrue($link->getStrategyRegistry()->has('custom')); } } - diff --git a/tests/phpMyFAQ/Link/Strategy/FaqStrategyTest.php b/tests/phpMyFAQ/Link/Strategy/FaqStrategyTest.php index 522f15ee52..d4cd36cf82 100644 --- a/tests/phpMyFAQ/Link/Strategy/FaqStrategyTest.php +++ b/tests/phpMyFAQ/Link/Strategy/FaqStrategyTest.php @@ -4,12 +4,13 @@ namespace phpMyFAQ\Link\Strategy; -use InvalidArgumentException;use phpMyFAQ\Link; +use InvalidArgumentException; use phpMyFAQ\Configuration; -use phpMyFAQ\Strings; use phpMyFAQ\Database\Sqlite3; -use PHPUnit\Framework\TestCase; +use phpMyFAQ\Link; +use phpMyFAQ\Strings; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class FaqStrategyTest extends TestCase diff --git a/tests/phpMyFAQ/Link/Strategy/GenericPathStrategyTest.php b/tests/phpMyFAQ/Link/Strategy/GenericPathStrategyTest.php index 093ce70ecc..f827f7a0e0 100644 --- a/tests/phpMyFAQ/Link/Strategy/GenericPathStrategyTest.php +++ b/tests/phpMyFAQ/Link/Strategy/GenericPathStrategyTest.php @@ -4,12 +4,12 @@ namespace phpMyFAQ\Link\Strategy; -use phpMyFAQ\Link; use phpMyFAQ\Configuration; -use phpMyFAQ\Strings; use phpMyFAQ\Database\Sqlite3; -use PHPUnit\Framework\TestCase; +use phpMyFAQ\Link; +use phpMyFAQ\Strings; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class GenericPathStrategyTest extends TestCase diff --git a/tests/phpMyFAQ/Link/Strategy/NewsStrategyTest.php b/tests/phpMyFAQ/Link/Strategy/NewsStrategyTest.php index 50bd38851a..fc89c44e8d 100644 --- a/tests/phpMyFAQ/Link/Strategy/NewsStrategyTest.php +++ b/tests/phpMyFAQ/Link/Strategy/NewsStrategyTest.php @@ -4,12 +4,13 @@ namespace phpMyFAQ\Link\Strategy; -use InvalidArgumentException;use phpMyFAQ\Link; +use InvalidArgumentException; use phpMyFAQ\Configuration; -use phpMyFAQ\Strings; use phpMyFAQ\Database\Sqlite3; -use PHPUnit\Framework\TestCase; +use phpMyFAQ\Link; +use phpMyFAQ\Strings; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class NewsStrategyTest extends TestCase diff --git a/tests/phpMyFAQ/Link/Strategy/SearchStrategyTest.php b/tests/phpMyFAQ/Link/Strategy/SearchStrategyTest.php index f45ea1d599..29f2f7700b 100644 --- a/tests/phpMyFAQ/Link/Strategy/SearchStrategyTest.php +++ b/tests/phpMyFAQ/Link/Strategy/SearchStrategyTest.php @@ -4,12 +4,12 @@ namespace phpMyFAQ\Link\Strategy; -use phpMyFAQ\Link; use phpMyFAQ\Configuration; -use phpMyFAQ\Strings; use phpMyFAQ\Database\Sqlite3; -use PHPUnit\Framework\TestCase; +use phpMyFAQ\Link; +use phpMyFAQ\Strings; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class SearchStrategyTest extends TestCase diff --git a/tests/phpMyFAQ/Link/Strategy/ShowStrategyTest.php b/tests/phpMyFAQ/Link/Strategy/ShowStrategyTest.php index c69de9a86a..6a017a8d79 100644 --- a/tests/phpMyFAQ/Link/Strategy/ShowStrategyTest.php +++ b/tests/phpMyFAQ/Link/Strategy/ShowStrategyTest.php @@ -4,12 +4,12 @@ namespace phpMyFAQ\Link\Strategy; -use phpMyFAQ\Link; use phpMyFAQ\Configuration; -use phpMyFAQ\Strings; use phpMyFAQ\Database\Sqlite3; -use PHPUnit\Framework\TestCase; +use phpMyFAQ\Link; +use phpMyFAQ\Strings; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class ShowStrategyTest extends TestCase diff --git a/tests/phpMyFAQ/Link/Strategy/SitemapStrategyTest.php b/tests/phpMyFAQ/Link/Strategy/SitemapStrategyTest.php index 5792416e2a..84e2433237 100644 --- a/tests/phpMyFAQ/Link/Strategy/SitemapStrategyTest.php +++ b/tests/phpMyFAQ/Link/Strategy/SitemapStrategyTest.php @@ -4,12 +4,12 @@ namespace phpMyFAQ\Link\Strategy; -use phpMyFAQ\Link; use phpMyFAQ\Configuration; -use phpMyFAQ\Strings; use phpMyFAQ\Database\Sqlite3; -use PHPUnit\Framework\TestCase; +use phpMyFAQ\Link; +use phpMyFAQ\Strings; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class SitemapStrategyTest extends TestCase diff --git a/tests/phpMyFAQ/Link/Util/LinkQueryParserTest.php b/tests/phpMyFAQ/Link/Util/LinkQueryParserTest.php index 1e985458dc..0c0ecc1700 100644 --- a/tests/phpMyFAQ/Link/Util/LinkQueryParserTest.php +++ b/tests/phpMyFAQ/Link/Util/LinkQueryParserTest.php @@ -4,8 +4,8 @@ namespace phpMyFAQ\Link\Util; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class LinkQueryParserTest extends TestCase @@ -34,4 +34,3 @@ public function testEmptyUrl(): void $this->assertSame([], $params); } } - diff --git a/tests/phpMyFAQ/Link/Util/TitleSlugifierTest.php b/tests/phpMyFAQ/Link/Util/TitleSlugifierTest.php index 6206936c02..60eecc1b70 100644 --- a/tests/phpMyFAQ/Link/Util/TitleSlugifierTest.php +++ b/tests/phpMyFAQ/Link/Util/TitleSlugifierTest.php @@ -5,8 +5,8 @@ namespace phpMyFAQ\Link\Util; use phpMyFAQ\Strings; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class TitleSlugifierTest extends TestCase @@ -28,7 +28,7 @@ public function testUmlauts(): void public function testMultipleSpacesAndPunctuation(): void { - $this->assertSame('foo-bar', TitleSlugifier::slug(" Foo , bar !! ")); + $this->assertSame('foo-bar', TitleSlugifier::slug(' Foo , bar !! ')); } public function testKeepsSingleDash(): void @@ -36,4 +36,3 @@ public function testKeepsSingleDash(): void $this->assertSame('foo-bar-baz', TitleSlugifier::slug('foo bar---baz')); } } - diff --git a/tests/phpMyFAQ/LinkTest.php b/tests/phpMyFAQ/LinkTest.php index 9414e0e04b..3a3848a414 100644 --- a/tests/phpMyFAQ/LinkTest.php +++ b/tests/phpMyFAQ/LinkTest.php @@ -3,9 +3,9 @@ namespace phpMyFAQ; use phpMyFAQ\Database\Sqlite3; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class LinkTest @@ -90,30 +90,12 @@ public function testGetSEOTitle(): void { $this->link = new Link('https://example.com/my-test-faq/', $this->configuration); - $this->assertEquals( - 'hd-ready', - $this->link->getSEOTitle('HD Ready') - ); - $this->assertEquals( - 'hd-ready', - $this->link->getSEOTitle('HD Ready ') - ); - $this->assertEquals( - 'hd_ready', - $this->link->getSEOTitle('HD-Ready') - ); - $this->assertEquals( - 'hd-ready', - $this->link->getSEOTitle("HD\r\nReady") - ); - $this->assertEquals( - 'hd-ready', - $this->link->getSEOTitle('{HD + Ready}') - ); - $this->assertEquals( - 'hd-raedy', - $this->link->getSEOTitle('HD Rädy') - ); + $this->assertEquals('hd-ready', $this->link->getSEOTitle('HD Ready')); + $this->assertEquals('hd-ready', $this->link->getSEOTitle('HD Ready ')); + $this->assertEquals('hd_ready', $this->link->getSEOTitle('HD-Ready')); + $this->assertEquals('hd-ready', $this->link->getSEOTitle("HD\r\nReady")); + $this->assertEquals('hd-ready', $this->link->getSEOTitle('{HD + Ready}')); + $this->assertEquals('hd-raedy', $this->link->getSEOTitle('HD Rädy')); } /** @@ -127,10 +109,7 @@ public function testGetHttpGetParameters(): void $this->link = new Link('https://example.com/my-test-faq/?foo=bar', $this->configuration); $this->assertEquals(array('foo' => 'bar'), $method->invokeArgs($this->link, array())); - $this->link = new Link( - 'https://example.com/my-test-faq/?foo=bar&action=noaction', - $this->configuration - ); + $this->link = new Link('https://example.com/my-test-faq/?foo=bar&action=noaction', $this->configuration); $this->assertEquals(array('foo' => 'bar', 'action' => 'noaction'), $method->invokeArgs($this->link, array())); $this->link = new Link('https://example.com/my-test-faq/?foo=bar&action=noaction', $this->configuration); @@ -210,29 +189,18 @@ public function testToHtmlAnchor(): void $this->link = new Link($url, $this->configuration); $this->link->class = 'pmf-foo'; - $this->assertEquals( - sprintf( - '%s', - $url, - $url - ), - $this->link->toHtmlAnchor() - ); + $this->assertEquals(sprintf('%s', $url, $url), $this->link->toHtmlAnchor()); $this->link->id = 'pmf-id'; $this->assertEquals( - sprintf( - '%s', - $url, - $url - ), - $this->link->toHtmlAnchor() + sprintf('%s', $url, $url), + $this->link->toHtmlAnchor(), ); $this->link->text = 'Foo FAQ'; $this->assertEquals( sprintf('Foo FAQ', $url), - $this->link->toHtmlAnchor() + $this->link->toHtmlAnchor(), ); } @@ -258,32 +226,20 @@ public function testAppendSids(): void public function testToStringWithEnabledRewriteRules(): void { $this->link = new Link('http://example.com/my-test-faq/', $this->configuration); - $this->assertEquals( - 'http://example.com/my-test-faq/', - $this->link->toString() - ); + $this->assertEquals('http://example.com/my-test-faq/', $this->link->toString()); $this->link = new Link('http://example.com/my-test-faq/index.php?action=add', $this->configuration); - $this->assertEquals( - 'http://example.com/my-test-faq/add-faq.html', - $this->link->toString() - ); + $this->assertEquals('http://example.com/my-test-faq/add-faq.html', $this->link->toString()); $this->link = new Link('http://example.com/my-test-faq/index.php?action=bookmarks', $this->configuration); - $this->assertEquals( - 'http://example.com/my-test-faq/user/bookmarks', - $this->link->toString() - ); + $this->assertEquals('http://example.com/my-test-faq/user/bookmarks', $this->link->toString()); $this->link = new Link( 'http://example.com/my-test-faq/index.php?action=faq&cat=1&id=36&artlang=de', - $this->configuration + $this->configuration, ); $this->link->setTitle('HD Ready'); - $this->assertEquals( - 'http://example.com/my-test-faq/content/1/36/de/hd-ready.html', - $this->link->toString() - ); + $this->assertEquals('http://example.com/my-test-faq/content/1/36/de/hd-ready.html', $this->link->toString()); } /** @@ -305,7 +261,7 @@ public function testToStringWithDisabledRewriteRules(): void $this->link->setTitle('Foobar'); $this->assertEquals( 'https://example.com/my-test-faq/content/1/36/de/foobar.html', - $this->link->toStringWithoutSession() + $this->link->toStringWithoutSession(), ); } } diff --git a/tests/phpMyFAQ/Mail/SmtpTest.php b/tests/phpMyFAQ/Mail/SmtpTest.php index 61f87d781a..7ec63eceb3 100644 --- a/tests/phpMyFAQ/Mail/SmtpTest.php +++ b/tests/phpMyFAQ/Mail/SmtpTest.php @@ -2,11 +2,11 @@ namespace phpMyFAQ\Mail; +use phpMyFAQ\Mail\Smtp; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; -use phpMyFAQ\Mail\Smtp; use ReflectionClass; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\MailerInterface; @@ -66,10 +66,12 @@ public function testSendBasicEmail(): void ->expects($this->once()) ->method('send') ->with($this->callback(function (Email $email): bool { - return $email->getSubject() === 'Test Subject' && - count($email->getTo()) === 1 && - $email->getTextBody() === 'This is a test email body.' && - $email->getHtmlBody() === 'This is a test email body.'; + return ( + $email->getSubject() === 'Test Subject' + && count($email->getTo()) === 1 + && $email->getTextBody() === 'This is a test email body.' + && $email->getHtmlBody() === 'This is a test email body.' + ); })); $result = $this->smtp->send($recipients, $headers, $body); @@ -115,11 +117,15 @@ public function testSendEmailWithCcAndBcc(): void ->with($this->callback(function (Email $email): bool { $cc = $email->getCc(); $bcc = $email->getBcc(); - return $email->getSubject() === 'Test Subject' && - $email->getTextBody() === 'Test body' && - $email->getHtmlBody() === 'Test body' && - count($cc) === 1 && $cc[0]->getAddress() === 'cc@example.com' && - count($bcc) === 1 && $bcc[0]->getAddress() === 'bcc@example.com'; + return ( + $email->getSubject() === 'Test Subject' + && $email->getTextBody() === 'Test body' + && $email->getHtmlBody() === 'Test body' + && count($cc) === 1 + && $cc[0]->getAddress() === 'cc@example.com' + && count($bcc) === 1 + && $bcc[0]->getAddress() === 'bcc@example.com' + ); })); $result = $this->smtp->send($recipients, $headers, $body); @@ -170,13 +176,19 @@ public function testSendEmailWithAllHeaders(): void $bcc = $email->getBcc(); $replyTo = $email->getReplyTo(); - return $email->getSubject() === 'Complete Test' && - $email->getTextBody() === 'Complete test body' && - $email->getHtmlBody() === 'Complete test body' && - count($from) === 1 && $from[0]->getAddress() === 'bounce@example.com' && - count($cc) === 1 && $cc[0]->getAddress() === 'cc@example.com' && - count($bcc) === 1 && $bcc[0]->getAddress() === 'bcc@example.com' && - count($replyTo) === 1 && $replyTo[0]->getAddress() === 'reply@example.com'; + return ( + $email->getSubject() === 'Complete Test' + && $email->getTextBody() === 'Complete test body' + && $email->getHtmlBody() === 'Complete test body' + && count($from) === 1 + && $from[0]->getAddress() === 'bounce@example.com' + && count($cc) === 1 + && $cc[0]->getAddress() === 'cc@example.com' + && count($bcc) === 1 + && $bcc[0]->getAddress() === 'bcc@example.com' + && count($replyTo) === 1 + && $replyTo[0]->getAddress() === 'reply@example.com' + ); })); $result = $this->smtp->send($recipients, $headers, $body); @@ -276,8 +288,10 @@ public function testSendEmailWithSpecialCharacters(): void ->expects($this->once()) ->method('send') ->with($this->callback(function (Email $email): bool { - return $email->getSubject() === 'Special Characters: äöü ßÄÖÜ €' && - $email->getTextBody() === 'Body with special characters: 中文 русский'; + return ( + $email->getSubject() === 'Special Characters: äöü ßÄÖÜ €' + && $email->getTextBody() === 'Body with special characters: 中文 русский' + ); })); $result = $this->smtp->send($recipients, $headers, $body); diff --git a/tests/phpMyFAQ/MailTest.php b/tests/phpMyFAQ/MailTest.php index 3d906a7448..4c145f3b01 100644 --- a/tests/phpMyFAQ/MailTest.php +++ b/tests/phpMyFAQ/MailTest.php @@ -6,9 +6,9 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Mail\Builtin; use phpMyFAQ\Mail\Smtp; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class MailTest extends TestCase @@ -150,7 +150,7 @@ public function testGetTimeWithNoRequestTime(): void public function testWrapLinesWithDefaultWidth(): void { - $message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum acnunc quis neque tempor varius."; + $message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum acnunc quis neque tempor varius.'; $result = $this->mail->wrapLines($message); $expectedResult = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum\r\nacnunc quis neque tempor varius."; @@ -159,7 +159,7 @@ public function testWrapLinesWithDefaultWidth(): void public function testWrapLinesWithCustomWidth(): void { - $message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ac nunc quis neque tempor varius."; + $message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ac nunc quis neque tempor varius.'; $result = $this->mail->wrapLines($message, 30); $expectedResult = "Lorem ipsum dolor sit amet,\r\nconsectetur adipiscing elit.\r\nVestibulum ac nunc quis neque\r\ntempor varius."; diff --git a/tests/phpMyFAQ/NetworkTest.php b/tests/phpMyFAQ/NetworkTest.php index df68c50244..83adb01bd1 100644 --- a/tests/phpMyFAQ/NetworkTest.php +++ b/tests/phpMyFAQ/NetworkTest.php @@ -17,9 +17,9 @@ namespace phpMyFAQ; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class NetworkTest @@ -83,7 +83,7 @@ public function testIsBannedWithIpv6Addresses(): void // Test banned IPv6 $this->assertTrue($this->network->isBanned('2001:db8::1')); - + // Test allowed IPv6 $this->assertFalse($this->network->isBanned('2001:db8::2')); } @@ -102,7 +102,7 @@ public function testIsBannedWithIpv4CidrNotation(): void $this->assertTrue($this->network->isBanned('192.168.1.50')); $this->assertTrue($this->network->isBanned('192.168.1.255')); $this->assertTrue($this->network->isBanned('10.1.2.3')); - + // Test IP outside banned CIDR range $this->assertFalse($this->network->isBanned('192.168.2.1')); $this->assertFalse($this->network->isBanned('172.16.0.1')); @@ -122,7 +122,7 @@ public function testIsBannedWithIpv6CidrNotation(): void $this->assertTrue($this->network->isBanned('2001:db8::1')); $this->assertTrue($this->network->isBanned('2001:db8:1234::5678')); $this->assertTrue($this->network->isBanned('fe80::1')); - + // Test IPv6 outside banned CIDR range $this->assertFalse($this->network->isBanned('2001:db9::1')); $this->assertFalse($this->network->isBanned('2002::1')); @@ -167,10 +167,10 @@ public function testIsBannedWithLocalhostAddresses(): void // Test IPv4 localhost $this->assertTrue($this->network->isBanned('127.0.0.1')); - + // Test IPv6 localhost $this->assertTrue($this->network->isBanned('::1')); - + // Test other loopback addresses $this->assertFalse($this->network->isBanned('127.0.0.2')); } @@ -187,14 +187,14 @@ public function testIsBannedWithPrivateIpRanges(): void // Test Class A private range $this->assertTrue($this->network->isBanned('10.1.2.3')); - + // Test Class B private range $this->assertTrue($this->network->isBanned('172.16.0.1')); $this->assertTrue($this->network->isBanned('172.31.255.255')); - + // Test Class C private range $this->assertTrue($this->network->isBanned('192.168.1.1')); - + // Test public IPs $this->assertFalse($this->network->isBanned('8.8.8.8')); $this->assertFalse($this->network->isBanned('1.1.1.1')); @@ -214,7 +214,7 @@ public function testIsBannedWithMalformedIpAddresses(): void $this->assertFalse($this->network->isBanned('192.168.1.256')); $this->assertFalse($this->network->isBanned('not.an.ip.address')); $this->assertFalse($this->network->isBanned('192.168')); - + // Test invalid IPv6 $this->assertFalse($this->network->isBanned('gggg::1')); $this->assertFalse($this->network->isBanned('2001:db8:::1')); @@ -234,7 +234,7 @@ public function testIsBannedWithMixedIpVersions(): void $this->assertTrue($this->network->isBanned('192.168.1.1')); $this->assertTrue($this->network->isBanned('10.0.0.50')); $this->assertFalse($this->network->isBanned('172.16.0.1')); - + // Test IPv6 addresses $this->assertTrue($this->network->isBanned('2001:db8::1')); $this->assertTrue($this->network->isBanned('fe80::1234')); @@ -254,7 +254,7 @@ public function testIsBannedWithEdgeCaseIpAddresses(): void // Test edge IPv4 addresses $this->assertTrue($this->network->isBanned('0.0.0.0')); $this->assertTrue($this->network->isBanned('255.255.255.255')); - + // Test edge IPv6 addresses $this->assertTrue($this->network->isBanned('::')); $this->assertTrue($this->network->isBanned('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff')); @@ -266,15 +266,15 @@ public function testIsBannedWithEdgeCaseIpAddresses(): void public function testConstructorReadonlyBehavior(): void { $network = new Network($this->config); - + $this->assertInstanceOf(Network::class, $network); - + // Verify that the network instance works correctly $this->config ->method('get') ->with('security.bannedIPs') ->willReturn('127.0.0.1'); - + $result = $network->isBanned('127.0.0.1'); $this->assertTrue($result); } @@ -289,7 +289,7 @@ public function testIsBannedWithLongBannedList(): void for ($i = 1; $i <= 100; $i++) { $bannedIps[] = "192.168.1.$i"; } - + $this->config ->method('get') ->with('security.bannedIPs') @@ -299,7 +299,7 @@ public function testIsBannedWithLongBannedList(): void $this->assertTrue($this->network->isBanned('192.168.1.50')); $this->assertTrue($this->network->isBanned('192.168.1.1')); $this->assertTrue($this->network->isBanned('192.168.1.100')); - + // Test IP not in the list $this->assertFalse($this->network->isBanned('192.168.2.1')); } @@ -316,14 +316,14 @@ public function testIsBannedWithComplexCidrRanges(): void // Test multiple IPs efficiently $testCases = [ - ['192.168.100.1', true], + ['192.168.100.1', true], ['10.255.255.255', true], - ['172.31.0.1', true], - ['169.254.1.1', true], - ['8.8.8.8', false], - ['1.1.1.1', false], - ['172.15.0.1', false], - ['172.32.0.1', false] + ['172.31.0.1', true], + ['169.254.1.1', true], + ['8.8.8.8', false], + ['1.1.1.1', false], + ['172.15.0.1', false], + ['172.32.0.1', false], ]; foreach ($testCases as [$ip, $expected]) { diff --git a/tests/phpMyFAQ/NewsTest.php b/tests/phpMyFAQ/NewsTest.php index b1642b40d9..f46ea72388 100644 --- a/tests/phpMyFAQ/NewsTest.php +++ b/tests/phpMyFAQ/NewsTest.php @@ -6,11 +6,11 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Entity\NewsMessage; use phpMyFAQ\News\NewsRepositoryInterface; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use stdClass; +use Symfony\Component\HttpFoundation\Session\Session; #[AllowMockObjectsWithoutExpectations] class NewsTest extends TestCase @@ -61,7 +61,8 @@ public function testCreate(): void ->setActive(true) ->setComment(true); - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('insert') ->with($newsMessage) ->willReturn(true); @@ -83,7 +84,8 @@ public function testUpdate(): void ->setActive(true) ->setComment(true); - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('update') ->with($newsMessage) ->willReturn(true); @@ -93,7 +95,8 @@ public function testUpdate(): void public function testDelete(): void { - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('delete') ->with(1, 'en') ->willReturn(true); @@ -103,7 +106,8 @@ public function testDelete(): void public function testDeleteFails(): void { - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('delete') ->with(999, 'en') ->willReturn(false); @@ -113,7 +117,8 @@ public function testDeleteFails(): void public function testActivate(): void { - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('activate') ->with(1, true) ->willReturn(true); @@ -123,7 +128,8 @@ public function testActivate(): void public function testDeactivate(): void { - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('activate') ->with(1, false) ->willReturn(true); @@ -133,7 +139,7 @@ public function testDeactivate(): void /** * @throws \Exception - */public function testGetAll(): void + */ public function testGetAll(): void { $mockRow = new stdClass(); $mockRow->id = 1; @@ -142,7 +148,8 @@ public function testDeactivate(): void $mockRow->artikel = '

    Test content

    '; $mockRow->datum = '20251222100000'; - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getLatest') ->with('en', true, 5) ->willReturn([$mockRow]); @@ -159,7 +166,7 @@ public function testDeactivate(): void /** * @throws \Exception - */public function testGetAllShowArchive(): void + */ public function testGetAllShowArchive(): void { $mockRow = new stdClass(); $mockRow->id = 1; @@ -168,7 +175,8 @@ public function testDeactivate(): void $mockRow->artikel = 'Archived content'; $mockRow->datum = '20200101120000'; - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getLatest') ->with('en', true, null) ->willReturn([$mockRow]); @@ -181,7 +189,8 @@ public function testDeactivate(): void public function testGetAllInactive(): void { - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getLatest') ->with('en', false, null) ->willReturn([]); @@ -207,7 +216,8 @@ public function testGetLatestData(): void $mockRow->linktitel = 'Example'; $mockRow->target = '_blank'; - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getLatest') ->with('en', true, 5) ->willReturn([$mockRow]); @@ -231,7 +241,8 @@ public function testGetLatestData(): void public function testGetLatestDataWithArchive(): void { - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getLatest') ->with('en', true, null) ->willReturn([]); @@ -245,7 +256,8 @@ public function testGetLatestDataWithArchive(): void public function testGetLatestDataForceConfLimit(): void { - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getLatest') ->with('en', true, 10) ->willReturn([]); @@ -266,7 +278,8 @@ public function testGetHeader(): void $mockRow->datum = '20251222100000'; $mockRow->active = 'y'; - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getHeaders') ->with('en') ->willReturn([$mockRow]); @@ -282,7 +295,8 @@ public function testGetHeader(): void public function testGetHeaderEmpty(): void { - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getHeaders') ->with('en') ->willReturn([]); @@ -308,7 +322,8 @@ public function testGet(): void $mockRow->linktitel = 'Example'; $mockRow->target = '_self'; - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getById') ->with(1, 'en') ->willReturn($mockRow); @@ -330,7 +345,8 @@ public function testGet(): void public function testGetNonExistent(): void { - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getById') ->with(999, 'en') ->willReturn(null); @@ -356,7 +372,8 @@ public function testGetInactiveAsNonAdmin(): void $mockRow->linktitel = ''; $mockRow->target = ''; - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getById') ->with(1, 'en') ->willReturn($mockRow); @@ -383,7 +400,8 @@ public function testGetInactiveAsAdmin(): void $mockRow->linktitel = ''; $mockRow->target = ''; - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getById') ->with(1, 'en') ->willReturn($mockRow); @@ -410,7 +428,8 @@ public function testGetWithCommentsDisabled(): void $mockRow->linktitel = ''; $mockRow->target = ''; - $this->mockRepository->expects($this->once()) + $this->mockRepository + ->expects($this->once()) ->method('getById') ->with(1, 'en') ->willReturn($mockRow); diff --git a/tests/phpMyFAQ/NotificationTest.php b/tests/phpMyFAQ/NotificationTest.php index ebebc4607e..76abf56579 100644 --- a/tests/phpMyFAQ/NotificationTest.php +++ b/tests/phpMyFAQ/NotificationTest.php @@ -18,9 +18,9 @@ namespace phpMyFAQ; use phpMyFAQ\Core\Exception; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Class NotificationTest diff --git a/tests/phpMyFAQ/PaginationTest.php b/tests/phpMyFAQ/PaginationTest.php index 4da03b9740..85c664a7f8 100644 --- a/tests/phpMyFAQ/PaginationTest.php +++ b/tests/phpMyFAQ/PaginationTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class PaginationTest extends TestCase @@ -13,22 +13,22 @@ public function testRender(): void $pagination = new Pagination([ 'total' => 30, 'perPage' => 10, - 'baseUrl' => 'http://example.com?action=foo' + 'baseUrl' => 'http://example.com?action=foo', ]); $expectedOutput = - '
      ' . - '
    • ' . - '1' . - '
    •   
    • ' . - '2' . - '
    •   
    • ' . - '3' . - '
    •   
    • ' . - '' . - '
    •   
    • ' . - '' . - '
    '; + '
      ' + . '
    • ' + . '1' + . '
    •   
    • ' + . '2' + . '
    •   
    • ' + . '3' + . '
    •   
    • ' + . '' + . '
    •   
    • ' + . '' + . '
    '; $this->assertEquals($expectedOutput, $pagination->render()); } diff --git a/tests/phpMyFAQ/Permission/BasicPermissionRepositoryTest.php b/tests/phpMyFAQ/Permission/BasicPermissionRepositoryTest.php index 0666100211..47a5fd532d 100644 --- a/tests/phpMyFAQ/Permission/BasicPermissionRepositoryTest.php +++ b/tests/phpMyFAQ/Permission/BasicPermissionRepositoryTest.php @@ -4,8 +4,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class BasicPermissionRepositoryTest extends TestCase @@ -147,4 +147,4 @@ public function testNextRightId(): void $this->assertIsInt($nextId); $this->assertGreaterThan(0, $nextId); } -} \ No newline at end of file +} diff --git a/tests/phpMyFAQ/Permission/BasicPermissionTest.php b/tests/phpMyFAQ/Permission/BasicPermissionTest.php index c442b6cef4..3641b703b8 100644 --- a/tests/phpMyFAQ/Permission/BasicPermissionTest.php +++ b/tests/phpMyFAQ/Permission/BasicPermissionTest.php @@ -7,8 +7,8 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Enums\PermissionType; use phpMyFAQ\User\CurrentUser; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class BasicPermissionTest extends TestCase @@ -152,7 +152,7 @@ public function testCheckRightData(): void 'for_groups' => 1, 'for_sections' => 1, ], - $this->basicPermission->checkRightData([]) + $this->basicPermission->checkRightData([]), ); } diff --git a/tests/phpMyFAQ/Permission/MediumPermissionRepositoryTest.php b/tests/phpMyFAQ/Permission/MediumPermissionRepositoryTest.php index 968bc6f1f1..3b1bb21502 100644 --- a/tests/phpMyFAQ/Permission/MediumPermissionRepositoryTest.php +++ b/tests/phpMyFAQ/Permission/MediumPermissionRepositoryTest.php @@ -4,8 +4,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class MediumPermissionRepositoryTest extends TestCase @@ -278,4 +278,4 @@ public function testDeleteGroupRights(): void // Cleanup $this->dbHandle->query('DELETE FROM faqgroup WHERE group_id = ' . $nextId); } -} \ No newline at end of file +} diff --git a/tests/phpMyFAQ/Permission/MediumPermissionTest.php b/tests/phpMyFAQ/Permission/MediumPermissionTest.php index 0848947437..93bb05561b 100644 --- a/tests/phpMyFAQ/Permission/MediumPermissionTest.php +++ b/tests/phpMyFAQ/Permission/MediumPermissionTest.php @@ -7,8 +7,8 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Enums\PermissionType; use phpMyFAQ\User\CurrentUser; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class MediumPermissionTest extends TestCase @@ -244,14 +244,14 @@ public function testGetAllGroupsOptions(): void $user = new CurrentUser($this->configuration); $user->getUserById(1); - $this->assertEquals( - '', - $this->mediumPermission->getAllGroupsOptions([], $user) - ); - $this->assertEquals( - '', - $this->mediumPermission->getAllGroupsOptions([1], $user) - ); + $this->assertEquals('', $this->mediumPermission->getAllGroupsOptions( + [], + $user, + )); + $this->assertEquals('', $this->mediumPermission->getAllGroupsOptions( + [1], + $user, + )); // Cleanup $this->mediumPermission->deleteGroup(1); @@ -373,7 +373,7 @@ public function testGetGroupData(): void 'auto_join' => 1, 'group_id' => 1, ], - $this->mediumPermission->getGroupData(1) + $this->mediumPermission->getGroupData(1), ); // Cleanup @@ -439,21 +439,21 @@ public function testFindOrCreateGroupByName(): void { $groupName = 'TestADGroup'; $description = 'Test AD Group Description'; - + // Test creating a new group $groupId = $this->mediumPermission->findOrCreateGroupByName($groupName, $description); $this->assertGreaterThan(0, $groupId); - + // Test finding an existing group $existingGroupId = $this->mediumPermission->findOrCreateGroupByName($groupName, $description); $this->assertEquals($groupId, $existingGroupId); - + // Test creating without description $groupName2 = 'TestADGroup2'; $groupId2 = $this->mediumPermission->findOrCreateGroupByName($groupName2); $this->assertGreaterThan(0, $groupId2); $this->assertNotEquals($groupId, $groupId2); - + // Cleanup $this->mediumPermission->deleteGroup($groupId); $this->mediumPermission->deleteGroup($groupId2); diff --git a/tests/phpMyFAQ/PermissionTest.php b/tests/phpMyFAQ/PermissionTest.php index 680c470fde..42aea35184 100644 --- a/tests/phpMyFAQ/PermissionTest.php +++ b/tests/phpMyFAQ/PermissionTest.php @@ -3,11 +3,11 @@ namespace phpMyFAQ; use InvalidArgumentException; -use PHPUnit\Framework\MockObject\Exception; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Permission\BasicPermission; use phpMyFAQ\Permission\MediumPermission; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class PermissionTest extends TestCase diff --git a/tests/phpMyFAQ/Plugin/MockPlugin.php b/tests/phpMyFAQ/Plugin/MockPlugin.php index 74dc27f9da..878ceb13e8 100644 --- a/tests/phpMyFAQ/Plugin/MockPlugin.php +++ b/tests/phpMyFAQ/Plugin/MockPlugin.php @@ -6,7 +6,6 @@ class MockPlugin implements PluginInterface { - public function getName(): string { return 'mockPlugin'; @@ -59,6 +58,6 @@ public function registerEvents(EventDispatcherInterface $eventDispatcher): void public function onMockEvent($event): void { - $event->setOutput("MockPlugin: Event triggered."); + $event->setOutput('MockPlugin: Event triggered.'); } } diff --git a/tests/phpMyFAQ/Plugin/PluginEventTest.php b/tests/phpMyFAQ/Plugin/PluginEventTest.php index 39a49d0133..4511b157c3 100644 --- a/tests/phpMyFAQ/Plugin/PluginEventTest.php +++ b/tests/phpMyFAQ/Plugin/PluginEventTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Plugin; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class PluginEventTest extends TestCase diff --git a/tests/phpMyFAQ/Plugin/PluginIntegrationTest.php b/tests/phpMyFAQ/Plugin/PluginIntegrationTest.php index 5c5acbb412..4a7ce59d0b 100644 --- a/tests/phpMyFAQ/Plugin/PluginIntegrationTest.php +++ b/tests/phpMyFAQ/Plugin/PluginIntegrationTest.php @@ -30,103 +30,103 @@ protected function setUp(): void // Create CSS files file_put_contents( $this->testPluginDir . '/IntegrationTestPlugin/assets/style.css', - '.test-plugin { color: blue; }' + '.test-plugin { color: blue; }', ); file_put_contents( $this->testPluginDir . '/IntegrationTestPlugin/assets/admin-style.css', - '.test-plugin-admin { color: red; }' + '.test-plugin-admin { color: red; }', ); // Create JavaScript files file_put_contents( $this->testPluginDir . '/IntegrationTestPlugin/assets/script.js', - 'console.log("Test plugin loaded");' + 'console.log("Test plugin loaded");', ); file_put_contents( $this->testPluginDir . '/IntegrationTestPlugin/assets/admin-script.js', - 'console.log("Test plugin admin loaded");' + 'console.log("Test plugin admin loaded");', ); // Create translation files file_put_contents( $this->testPluginDir . '/IntegrationTestPlugin/translations/language_en.php', - "testPluginDir . '/IntegrationTestPlugin/translations/language_de.php', - "testPluginDir . '/IntegrationTestPlugin/IntegrationTestPluginPlugin.php', - $pluginClass + $pluginClass, ); // Setup Translation @@ -135,15 +135,9 @@ public function registerEvents(EventDispatcherInterface $eventDispatcher): void mkdir($translationsDir, 0777, true); } - file_put_contents( - $translationsDir . '/language_en.php', - " 'Core value'];\n" - ); + file_put_contents($translationsDir . '/language_en.php', " 'Core value'];\n"); - file_put_contents( - $translationsDir . '/language_de.php', - " 'Kernwert'];\n" - ); + file_put_contents($translationsDir . '/language_de.php', " 'Kernwert'];\n"); Translation::resetInstance(); Translation::create() diff --git a/tests/phpMyFAQ/Plugin/PluginManagerTest.php b/tests/phpMyFAQ/Plugin/PluginManagerTest.php index 8baf1019b3..b8f014c19e 100644 --- a/tests/phpMyFAQ/Plugin/PluginManagerTest.php +++ b/tests/phpMyFAQ/Plugin/PluginManagerTest.php @@ -2,9 +2,9 @@ namespace phpMyFAQ\Plugin; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use ReflectionException; require 'MockPlugin.php'; @@ -28,7 +28,7 @@ public function testRegisterPlugin(): void /** * @throws ReflectionException - */public function testLoadPlugins(): void + */ public function testLoadPlugins(): void { $mockPluginPath = __DIR__ . '/MockPlugin.php'; file_put_contents($mockPluginPath, file_get_contents(__DIR__ . '/MockPlugin.php')); @@ -40,7 +40,6 @@ public function testRegisterPlugin(): void $this->assertEquals('phpMyFAQ\Plugin', $namespace); } - /** * @throws ReflectionException */ @@ -246,7 +245,7 @@ public function testGetIncompatiblePluginsReturnsEmpty(): void public function testIncompatiblePluginIsTracked(): void { // Create a mock plugin class that is incompatible - $incompatiblePluginClass = new class () implements PluginInterface { + $incompatiblePluginClass = new class() implements PluginInterface { public function getName(): string { return 'IncompatiblePlugin'; diff --git a/tests/phpMyFAQ/QuestionTest.php b/tests/phpMyFAQ/QuestionTest.php index 4163bd6344..450fe116de 100644 --- a/tests/phpMyFAQ/QuestionTest.php +++ b/tests/phpMyFAQ/QuestionTest.php @@ -3,10 +3,10 @@ namespace phpMyFAQ; use phpMyFAQ\Database\Sqlite3; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class QuestionTest extends TestCase diff --git a/tests/phpMyFAQ/Questions/QuestionRepositoryTest.php b/tests/phpMyFAQ/Questions/QuestionRepositoryTest.php index 778911222e..f39c7139b6 100644 --- a/tests/phpMyFAQ/Questions/QuestionRepositoryTest.php +++ b/tests/phpMyFAQ/Questions/QuestionRepositoryTest.php @@ -7,10 +7,10 @@ use phpMyFAQ\Entity\QuestionEntity; use phpMyFAQ\Language; use phpMyFAQ\Question\QuestionRepository; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class QuestionRepositoryTest extends TestCase diff --git a/tests/phpMyFAQ/Rating/RatingRepositoryTest.php b/tests/phpMyFAQ/Rating/RatingRepositoryTest.php index c4f91f0290..134f148f20 100644 --- a/tests/phpMyFAQ/Rating/RatingRepositoryTest.php +++ b/tests/phpMyFAQ/Rating/RatingRepositoryTest.php @@ -17,7 +17,17 @@ namespace phpMyFAQ\Rating; -use phpMyFAQ\Configuration;use phpMyFAQ\Database\Sqlite3;use phpMyFAQ\Entity\Vote;use phpMyFAQ\Language;use phpMyFAQ\Search\Rating\RatingRepository;use phpMyFAQ\Strings;use phpMyFAQ\Translation;use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;use PHPUnit\Framework\MockObject\Exception as MockException;use PHPUnit\Framework\TestCase;use Symfony\Component\HttpFoundation\Session\Session; +use phpMyFAQ\Configuration; +use phpMyFAQ\Database\Sqlite3; +use phpMyFAQ\Entity\Vote; +use phpMyFAQ\Language; +use phpMyFAQ\Search\Rating\RatingRepository; +use phpMyFAQ\Strings; +use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\MockObject\Exception as MockException; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Session; #[AllowMockObjectsWithoutExpectations] class RatingRepositoryTest extends TestCase diff --git a/tests/phpMyFAQ/RatingTest.php b/tests/phpMyFAQ/RatingTest.php index a1b664078d..d270766217 100644 --- a/tests/phpMyFAQ/RatingTest.php +++ b/tests/phpMyFAQ/RatingTest.php @@ -5,9 +5,9 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Entity\Vote; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class RatingTest extends TestCase diff --git a/tests/phpMyFAQ/RelationTest.php b/tests/phpMyFAQ/RelationTest.php index 8afb1aab8a..a08e62d332 100644 --- a/tests/phpMyFAQ/RelationTest.php +++ b/tests/phpMyFAQ/RelationTest.php @@ -3,12 +3,13 @@ namespace phpMyFAQ; use phpMyFAQ\Configuration\DatabaseConfiguration; -use phpMyFAQ\Database\PdoSqlite;use phpMyFAQ\Database\Sqlite3; +use phpMyFAQ\Database\PdoSqlite; +use phpMyFAQ\Database\Sqlite3; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use stdClass; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class RelationTest extends TestCase @@ -37,7 +38,7 @@ public function testGetAllRelatedByQuestion(): void $dbConfig->getUser(), $dbConfig->getPassword(), $dbConfig->getDatabase(), - $dbConfig->getPort() + $dbConfig->getPort(), ); $configuration = new Configuration($this->db); $configuration->set('search.enableRelevance', false); @@ -47,12 +48,12 @@ public function testGetAllRelatedByQuestion(): void $configuration->setLanguage($language); $this->db->query( - 'INSERT INTO faqdata ' . - '(id, lang, solution_id, sticky, thema, content, keywords, active, author, email, updated) VALUES ' . - '(1, \'en\', 1000, \'yes\', \'sample question\', \'sample answer\', \'sample keywords\', \'yes\', \'Author\', \'test@example.org\', \'date\')' + 'INSERT INTO faqdata ' + . '(id, lang, solution_id, sticky, thema, content, keywords, active, author, email, updated) VALUES ' + . '(1, \'en\', 1000, \'yes\', \'sample question\', \'sample answer\', \'sample keywords\', \'yes\', \'Author\', \'test@example.org\', \'date\')', ); $this->db->query( - 'INSERT INTO faqcategoryrelations (category_id, category_lang, record_id, record_lang) VALUES (1, \'en\', 1, \'en\')' + 'INSERT INTO faqcategoryrelations (category_id, category_lang, record_id, record_lang) VALUES (1, \'en\', 1, \'en\')', ); $relation = new Relation($configuration); diff --git a/tests/phpMyFAQ/Repository/NewsRepositoryTest.php b/tests/phpMyFAQ/Repository/NewsRepositoryTest.php index 3475570b01..95a4bf1055 100644 --- a/tests/phpMyFAQ/Repository/NewsRepositoryTest.php +++ b/tests/phpMyFAQ/Repository/NewsRepositoryTest.php @@ -10,8 +10,8 @@ use phpMyFAQ\Entity\NewsMessage; use phpMyFAQ\News\NewsRepository; use phpMyFAQ\Strings; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class NewsRepositoryTest extends TestCase @@ -74,4 +74,3 @@ public function testActivateAndDelete(): void $this->assertNull($repo->getById($id, 'en')); } } - diff --git a/tests/phpMyFAQ/Search/SearchDatabaseTest.php b/tests/phpMyFAQ/Search/SearchDatabaseTest.php index cc2b459b14..ed96515b78 100644 --- a/tests/phpMyFAQ/Search/SearchDatabaseTest.php +++ b/tests/phpMyFAQ/Search/SearchDatabaseTest.php @@ -5,8 +5,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Strings; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Class SearchDatabaseTest @@ -64,12 +64,14 @@ public function testSetAndGetResultColumns() 'faqdata.id AS id', 'faqdata.lang AS lang', 'faqdata.thema AS question', - 'faqdata.content AS answer' + 'faqdata.content AS answer', ]; $this->searchDatabase->setResultColumns($resultColumns); - $this->assertEquals('faqdata.id AS id, faqdata.lang AS lang, faqdata.thema AS question, faqdata.content AS answer', - $this->searchDatabase->getResultColumns()); + $this->assertEquals( + 'faqdata.id AS id, faqdata.lang AS lang, faqdata.thema AS question, faqdata.content AS answer', + $this->searchDatabase->getResultColumns(), + ); $this->assertIsString($this->searchDatabase->getResultColumns()); } @@ -83,12 +85,14 @@ public function testSetAndGetJoinedColumns() { $joinedColumns = [ 'faqdata.id = faqcategoryrelations.record_id', - 'faqdata.lang = faqcategoryrelations.record_lang' + 'faqdata.lang = faqcategoryrelations.record_lang', ]; $this->searchDatabase->setJoinedColumns($joinedColumns); - $this->assertEquals('faqdata.id = faqcategoryrelations.record_id AND faqdata.lang = faqcategoryrelations.record_lang ', - $this->searchDatabase->getJoinedColumns()); + $this->assertEquals( + 'faqdata.id = faqcategoryrelations.record_id AND faqdata.lang = faqcategoryrelations.record_lang ', + $this->searchDatabase->getJoinedColumns(), + ); $this->assertIsString($this->searchDatabase->getJoinedColumns()); } @@ -103,12 +107,14 @@ public function testSetAndGetMatchingColumns() $matchingColumns = [ 'faqdata.thema', 'faqdata.content', - 'faqdata.keywords' + 'faqdata.keywords', ]; $this->searchDatabase->setMatchingColumns($matchingColumns); - $this->assertEquals('faqdata.thema, faqdata.content, faqdata.keywords', - $this->searchDatabase->getMatchingColumns()); + $this->assertEquals( + 'faqdata.thema, faqdata.content, faqdata.keywords', + $this->searchDatabase->getMatchingColumns(), + ); $this->assertIsString($this->searchDatabase->getMatchingColumns()); } @@ -122,12 +128,14 @@ public function testSetAndGetConditions() { $conditions = [ 'faqdata.active' => "'yes'", - 'faqcategoryrelations.category_id' => 1 + 'faqcategoryrelations.category_id' => 1, ]; $this->searchDatabase->setConditions($conditions); - $this->assertEquals(" AND faqdata.active = 'yes' AND faqcategoryrelations.category_id = 1", - $this->searchDatabase->getConditions()); + $this->assertEquals( + " AND faqdata.active = 'yes' AND faqcategoryrelations.category_id = 1", + $this->searchDatabase->getConditions(), + ); $this->assertIsString($this->searchDatabase->getConditions()); } @@ -140,24 +148,27 @@ public function testSetAndGetConditionsWithoutConditions() public function testGetMatchClause() { $this->searchDatabase->setMatchingColumns(['faqdata.author']); - $this->assertEquals(" (faqdata.author LIKE '%Thorsten%')", - $this->searchDatabase->getMatchClause('Thorsten')); + $this->assertEquals(" (faqdata.author LIKE '%Thorsten%')", $this->searchDatabase->getMatchClause('Thorsten')); $this->assertIsString($this->searchDatabase->getMatchClause('Thorsten')); } public function testGetMatchClauseWithTwoSearchTerms() { $this->searchDatabase->setMatchingColumns(['faqdata.author']); - $this->assertEquals(" (faqdata.author LIKE '%Thorsten%') OR (faqdata.author LIKE '%Rinne%')", - $this->searchDatabase->getMatchClause('Thorsten Rinne')); + $this->assertEquals( + " (faqdata.author LIKE '%Thorsten%') OR (faqdata.author LIKE '%Rinne%')", + $this->searchDatabase->getMatchClause('Thorsten Rinne'), + ); $this->assertIsString($this->searchDatabase->getMatchClause('Thorsten')); } public function testGetMatchClauseWithTwoColumns() { $this->searchDatabase->setMatchingColumns(['faqdata.author', 'faqdata.thema']); - $this->assertEquals(" (faqdata.author LIKE '%Thorsten%' OR faqdata.thema LIKE '%Thorsten%')", - $this->searchDatabase->getMatchClause('Thorsten')); + $this->assertEquals( + " (faqdata.author LIKE '%Thorsten%' OR faqdata.thema LIKE '%Thorsten%')", + $this->searchDatabase->getMatchClause('Thorsten'), + ); $this->assertIsString($this->searchDatabase->getMatchClause('Thorsten')); } } diff --git a/tests/phpMyFAQ/Search/SearchFactoryTest.php b/tests/phpMyFAQ/Search/SearchFactoryTest.php index adfad673d3..02ddc2c499 100644 --- a/tests/phpMyFAQ/Search/SearchFactoryTest.php +++ b/tests/phpMyFAQ/Search/SearchFactoryTest.php @@ -8,8 +8,8 @@ use phpMyFAQ\Database; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Strings; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Class SearchFactoryTest @@ -24,7 +24,7 @@ class SearchFactoryTest extends TestCase * Prepares the environment before running a test. * * @throws Exception -*/ + */ protected function setUp(): void { parent::setUp(); @@ -39,7 +39,7 @@ protected function setUp(): void $dbConfig->getUser(), $dbConfig->getPassword(), $dbConfig->getDatabase(), - $dbConfig->getPort() + $dbConfig->getPort(), ); $this->configuration = new Configuration($db); } diff --git a/tests/phpMyFAQ/Search/SearchResultSetTest.php b/tests/phpMyFAQ/Search/SearchResultSetTest.php index 3c72b754b6..315b7ef8f6 100644 --- a/tests/phpMyFAQ/Search/SearchResultSetTest.php +++ b/tests/phpMyFAQ/Search/SearchResultSetTest.php @@ -5,8 +5,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Strings; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; /** * Class ResultsetTest diff --git a/tests/phpMyFAQ/SearchTest.php b/tests/phpMyFAQ/SearchTest.php index 9ff051df1e..e71b9cef10 100644 --- a/tests/phpMyFAQ/SearchTest.php +++ b/tests/phpMyFAQ/SearchTest.php @@ -5,9 +5,9 @@ use Exception; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Plugin\PluginException; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SearchTest extends TestCase @@ -71,7 +71,8 @@ public function testSearchWithNumericTermWhenSolutionIdSearchEnabled(): void { $this->setConfigValue('search.searchForSolutionId', 'true'); - $this->search = $this->getMockBuilder(Search::class) + $this->search = $this + ->getMockBuilder(Search::class) ->setConstructorArgs([$this->configuration]) ->onlyMethods(['searchDatabase']) ->getMock(); @@ -100,7 +101,8 @@ public function testSearchWithNumericTermWhenSolutionIdSearchDisabled(): void $this->setConfigValue('search.enableElasticsearch', 'false'); $this->setConfigValue('search.enableOpenSearch', 'false'); - $this->search = $this->getMockBuilder(Search::class) + $this->search = $this + ->getMockBuilder(Search::class) ->setConstructorArgs([$this->configuration]) ->onlyMethods(['searchDatabase']) ->getMock(); @@ -122,7 +124,8 @@ public function testSearchWithNumericTermWhenElasticsearchEnabledAndSolutionIdSe $this->setConfigValue('search.searchForSolutionId', 'false'); $this->setConfigValue('search.enableElasticsearch', 'true'); - $this->search = $this->getMockBuilder(Search::class) + $this->search = $this + ->getMockBuilder(Search::class) ->setConstructorArgs([$this->configuration]) ->onlyMethods(['searchElasticsearch']) ->getMock(); @@ -145,7 +148,8 @@ public function testSearchWithNumericTermWhenOpenSearchEnabledAndSolutionIdSearc $this->setConfigValue('search.enableElasticsearch', 'false'); $this->setConfigValue('search.enableOpenSearch', 'true'); - $this->search = $this->getMockBuilder(Search::class) + $this->search = $this + ->getMockBuilder(Search::class) ->setConstructorArgs([$this->configuration]) ->onlyMethods(['searchOpenSearch']) ->getMock(); @@ -164,7 +168,8 @@ public function testSearchWithNumericTermWhenOpenSearchEnabledAndSolutionIdSearc */ public function testSearchWithNonNumericTerm(): void { - $this->search = $this->getMockBuilder(Search::class) + $this->search = $this + ->getMockBuilder(Search::class) ->setConstructorArgs([$this->configuration]) ->onlyMethods(['searchDatabase']) ->getMock(); diff --git a/tests/phpMyFAQ/Seo/SeoRepositoryTest.php b/tests/phpMyFAQ/Seo/SeoRepositoryTest.php index db06e68dd8..c4b6461cee 100644 --- a/tests/phpMyFAQ/Seo/SeoRepositoryTest.php +++ b/tests/phpMyFAQ/Seo/SeoRepositoryTest.php @@ -9,9 +9,9 @@ use phpMyFAQ\Database\DatabaseDriver; use phpMyFAQ\Entity\SeoEntity; use phpMyFAQ\Enums\SeoType; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SeoRepositoryTest extends TestCase @@ -27,9 +27,7 @@ protected function setUp(): void $this->configuration = $this->createStub(Configuration::class); $this->database = $this->createMock(DatabaseDriver::class); - $this->configuration - ->method('getDb') - ->willReturn($this->database); + $this->configuration->method('getDb')->willReturn($this->database); $this->repository = new SeoRepository($this->configuration); } @@ -116,8 +114,7 @@ public function testGetPopulatesEntityWhenRowExists(): void ->setReferenceId(42) ->setReferenceLanguage('en'); - $result = new class { - }; + $result = new class {}; $this->database ->expects($this->atLeastOnce()) diff --git a/tests/phpMyFAQ/SeoTest.php b/tests/phpMyFAQ/SeoTest.php index bf992a0d93..0bb43b2bc3 100644 --- a/tests/phpMyFAQ/SeoTest.php +++ b/tests/phpMyFAQ/SeoTest.php @@ -12,7 +12,7 @@ class SeoTest extends TestCase { private Seo $seo; - + protected function setUp(): void { parent::setUp(); @@ -32,7 +32,8 @@ protected function setUp(): void public function testCreate(): void { $seo = new SeoEntity(); - $seo->setSeoType(SeoType::FAQ) + $seo + ->setSeoType(SeoType::FAQ) ->setReferenceId(1) ->setReferenceLanguage('en') ->setTitle('Test Title') @@ -46,7 +47,8 @@ public function testCreate(): void public function testGet(): void { $seo = new SeoEntity(); - $seo->setSeoType(SeoType::FAQ) + $seo + ->setSeoType(SeoType::FAQ) ->setReferenceId(1) ->setReferenceLanguage('en') ->setTitle('Test Title') @@ -62,14 +64,16 @@ public function testGet(): void public function testUpdate(): void { $seo = new SeoEntity(); - $seo->setSeoType(SeoType::FAQ) + $seo + ->setSeoType(SeoType::FAQ) ->setReferenceId(1) ->setReferenceLanguage('en') ->setTitle('Test Title') ->setDescription('Test Description'); $this->seo->create($seo); - $seo->setSeoType(SeoType::FAQ) + $seo + ->setSeoType(SeoType::FAQ) ->setReferenceId(1) ->setReferenceLanguage('en') ->setTitle('Updated Title') @@ -84,7 +88,8 @@ public function testUpdate(): void public function testDelete(): void { $seo = new SeoEntity(); - $seo->setSeoType(SeoType::FAQ) + $seo + ->setSeoType(SeoType::FAQ) ->setReferenceId(1) ->setReferenceLanguage('en') ->setTitle('Test Title') diff --git a/tests/phpMyFAQ/Service/GravatarTest.php b/tests/phpMyFAQ/Service/GravatarTest.php index 86c8f9bc13..5dbf4aed46 100644 --- a/tests/phpMyFAQ/Service/GravatarTest.php +++ b/tests/phpMyFAQ/Service/GravatarTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Service; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class GravatarTest extends TestCase @@ -25,7 +25,7 @@ public function testGetImage(): void 'default' => 'identicon', 'rating' => 'pg', 'force_default' => true, - 'class' => 'avatar-img' + 'class' => 'avatar-img', ]; $expectedResult2 = 'Gravatar'; $result2 = $gravatar->getImage($email, $params); @@ -33,7 +33,7 @@ public function testGetImage(): void // Test case 3: Test with only class parameter $params = [ - 'class' => 'rounded-img' + 'class' => 'rounded-img', ]; $expectedResult3 = 'Gravatar'; $result3 = $gravatar->getImage($email, $params); diff --git a/tests/phpMyFAQ/Service/McpServer/FaqSearchToolExecutorTest.php b/tests/phpMyFAQ/Service/McpServer/FaqSearchToolExecutorTest.php index 986adfc818..52875c5802 100644 --- a/tests/phpMyFAQ/Service/McpServer/FaqSearchToolExecutorTest.php +++ b/tests/phpMyFAQ/Service/McpServer/FaqSearchToolExecutorTest.php @@ -3,13 +3,13 @@ namespace phpMyFAQ\Service\McpServer; use Exception; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; -use phpMyFAQ\Search; use phpMyFAQ\Faq; +use phpMyFAQ\Search; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; use Symfony\AI\McpSdk\Capability\Tool\ToolCall; use Symfony\AI\McpSdk\Capability\Tool\ToolCallResult; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class FaqSearchToolExecutorTest extends TestCase @@ -26,11 +26,7 @@ protected function setUp(): void $configMock->method('getDefaultUrl')->willReturn('https://example.com/'); - $this->executor = new FaqSearchToolExecutor( - $configMock, - $this->searchMock, - $this->faqMock - ); + $this->executor = new FaqSearchToolExecutor($configMock, $this->searchMock, $this->faqMock); } public function testGetName(): void @@ -74,13 +70,13 @@ public function testCallWithNoResults(): void public function testCallWithResults(): void { $toolCall = new ToolCall('test-id', 'faq_search', ['query' => 'test', 'limit' => 1]); - $searchResult = (object)[ + $searchResult = (object) [ 'id' => 42, 'lang' => 'en', 'question' => 'What is phpMyFAQ?', 'answer' => 'phpMyFAQ is an open source FAQ system.', 'category_id' => 1, - 'score' => 0.95 + 'score' => 0.95, ]; $this->searchMock->method('search')->willReturn([$searchResult]); $this->faqMock->method('getFaqResult')->willReturn(['id' => 42]); @@ -97,7 +93,7 @@ public function testCallWithResults(): void $this->assertSame('What is phpMyFAQ?', $jsonData['results'][0]['question']); $this->assertStringContainsString( 'https://example.com/index.php?action=faq&cat=0&id=42&artlang=en', - $jsonData['results'][0]['url'] + $jsonData['results'][0]['url'], ); } diff --git a/tests/phpMyFAQ/Service/McpServer/FaqSearchToolMetadataTest.php b/tests/phpMyFAQ/Service/McpServer/FaqSearchToolMetadataTest.php index 2dc478efab..1b599bb148 100644 --- a/tests/phpMyFAQ/Service/McpServer/FaqSearchToolMetadataTest.php +++ b/tests/phpMyFAQ/Service/McpServer/FaqSearchToolMetadataTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ\Service\McpServer; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class FaqSearchToolMetadataTest extends TestCase diff --git a/tests/phpMyFAQ/Service/McpServer/PhpMyFaqMcpServerTest.php b/tests/phpMyFAQ/Service/McpServer/PhpMyFaqMcpServerTest.php index f5ff63f240..873960fc80 100644 --- a/tests/phpMyFAQ/Service/McpServer/PhpMyFaqMcpServerTest.php +++ b/tests/phpMyFAQ/Service/McpServer/PhpMyFaqMcpServerTest.php @@ -4,14 +4,14 @@ use Exception; use Monolog\Logger; -use phpMyFAQ\Language; -use PHPUnit\Framework\TestCase; use phpMyFAQ\Configuration; use phpMyFAQ\Faq; +use phpMyFAQ\Language; use phpMyFAQ\Search; -use Symfony\AI\McpSdk\Server\JsonRpcHandler; -use Symfony\AI\McpSdk\Capability\Tool\ToolCall; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; +use Symfony\AI\McpSdk\Capability\Tool\ToolCall; +use Symfony\AI\McpSdk\Server\JsonRpcHandler; #[AllowMockObjectsWithoutExpectations] class PhpMyFaqMcpServerTest extends TestCase @@ -34,18 +34,14 @@ protected function setUp(): void $this->configMock->method('getDefaultUrl')->willReturn('https://example.com'); // Mock the configuration values needed by Language::setLanguage() - $this->configMock->method('get') + $this->configMock + ->method('get') ->willReturnMap([ ['main.languageDetection', true], - ['main.language', 'en'] + ['main.language', 'en'], ]); - $this->server = new PhpMyFaqMcpServer( - $this->configMock, - $languageMock, - $this->searchMock, - $this->faqMock - ); + $this->server = new PhpMyFaqMcpServer($this->configMock, $languageMock, $this->searchMock, $this->faqMock); } public function testJsonRpcHandlerIsInitialized(): void @@ -71,11 +67,7 @@ public function testGetServerInfoReturnsExpectedArray(): void */ public function testFaqSearchToolExecutorReturnsValidJsonFormat(): void { - $executor = new FaqSearchToolExecutor( - $this->configMock, - $this->searchMock, - $this->faqMock - ); + $executor = new FaqSearchToolExecutor($this->configMock, $this->searchMock, $this->faqMock); // Mock search results $searchResults = [ @@ -85,8 +77,8 @@ public function testFaqSearchToolExecutorReturnsValidJsonFormat(): void 'question' => 'Test question?', 'answer' => 'Test answer', 'category_id' => 1, - 'score' => 0.95 - ] + 'score' => 0.95, + ], ]; $this->searchMock->method('search')->willReturn($searchResults); @@ -107,11 +99,7 @@ public function testFaqSearchToolExecutorReturnsValidJsonFormat(): void public function testFaqSearchToolExecutorHandlesEmptyQuery(): void { - $executor = new FaqSearchToolExecutor( - $this->configMock, - $this->searchMock, - $this->faqMock - ); + $executor = new FaqSearchToolExecutor($this->configMock, $this->searchMock, $this->faqMock); $toolCall = new ToolCall('test-id', 'faq_search', ['query' => '']); $result = $executor->call($toolCall); @@ -122,11 +110,7 @@ public function testFaqSearchToolExecutorHandlesEmptyQuery(): void public function testFaqSearchToolExecutorHandlesNoResults(): void { - $executor = new FaqSearchToolExecutor( - $this->configMock, - $this->searchMock, - $this->faqMock - ); + $executor = new FaqSearchToolExecutor($this->configMock, $this->searchMock, $this->faqMock); $this->searchMock->method('search')->willReturn([]); $this->searchMock->expects($this->once())->method('setCategory'); diff --git a/tests/phpMyFAQ/ServicesConfigurationTest.php b/tests/phpMyFAQ/ServicesConfigurationTest.php index ad1878db77..fff01cfc7a 100644 --- a/tests/phpMyFAQ/ServicesConfigurationTest.php +++ b/tests/phpMyFAQ/ServicesConfigurationTest.php @@ -4,8 +4,8 @@ namespace phpMyFAQ; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class ServicesConfigurationTest extends TestCase @@ -54,7 +54,11 @@ public function test_service_configuration_classes_exist_and_factories_are_calla } // Find all factory arrays like [Foo\Bar::class, 'method'] - preg_match_all('/\[\s*([\\\\A-Za-z0-9_]+)::class\s*,\s*[\'\"]([A-Za-z_][A-Za-z0-9_]*)[\'\"]\s*\]/', $contents, $factoryMatches); + preg_match_all( + '/\[\s*([\\\\A-Za-z0-9_]+)::class\s*,\s*[\'\"]([A-Za-z_][A-Za-z0-9_]*)[\'\"]\s*\]/', + $contents, + $factoryMatches, + ); $factories = []; $count = count($factoryMatches[1] ?? []); for ($i = 0; $i < $count; $i++) { @@ -69,7 +73,12 @@ public function test_service_configuration_classes_exist_and_factories_are_calla $missing = []; foreach ($fqSymbols as $fqcn) { - if (!class_exists($fqcn) && !interface_exists($fqcn) && !(function_exists('enum_exists') && enum_exists($fqcn)) && !trait_exists($fqcn)) { + if ( + !class_exists($fqcn) + && !interface_exists($fqcn) + && !(function_exists('enum_exists') && enum_exists($fqcn)) + && !trait_exists($fqcn) + ) { $missing[] = $fqcn; } } diff --git a/tests/phpMyFAQ/Session/SessionWrapperTest.php b/tests/phpMyFAQ/Session/SessionWrapperTest.php index d663b46b86..56582f99d7 100644 --- a/tests/phpMyFAQ/Session/SessionWrapperTest.php +++ b/tests/phpMyFAQ/Session/SessionWrapperTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Session; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SessionWrapperTest extends TestCase @@ -36,6 +36,7 @@ public function testConstructorWithoutSessionParameter(): void // We can't easily test this without starting actual sessions, so we'll just verify // the constructor doesn't throw an exception $this->expectNotToPerformAssertions(); + // Note: In a real scenario, this would create a PhpBridgeSessionStorage session // but for unit testing, we'll focus on the mocked behavior } @@ -154,7 +155,7 @@ public function testSetAndGetWorkTogether(): void // Test the workflow $this->sessionWrapper->set($key, $value); $result = $this->sessionWrapper->get($key); - + $this->assertEquals($value, $result); } @@ -195,4 +196,4 @@ public function testSetWithDifferentDataTypes(): void $this->sessionWrapper->set($key, $value); } } -} \ No newline at end of file +} diff --git a/tests/phpMyFAQ/Session/TokenTest.php b/tests/phpMyFAQ/Session/TokenTest.php index 0f60b219b4..2268a268a9 100644 --- a/tests/phpMyFAQ/Session/TokenTest.php +++ b/tests/phpMyFAQ/Session/TokenTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Session; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\SessionInterface; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class TokenTest extends TestCase @@ -55,9 +55,7 @@ public function testSetAndGetCookieToken(): void public function testGetTokenInput(): void { - $this->sessionMock - ->method('get') - ->willReturn($this->token->setSessionToken('testToken')); + $this->sessionMock->method('get')->willReturn($this->token->setSessionToken('testToken')); $inputHtml = $this->token->getTokenInput('testPage'); $expectedHtml = 'sessionMock - ->method('get') - ->willReturn($this->token->setSessionToken('testToken')); + $this->sessionMock->method('get')->willReturn($this->token->setSessionToken('testToken')); $tokenString = $this->token->getTokenString('testPage'); $this->assertIsString($tokenString); @@ -80,18 +76,14 @@ public function testGetTokenString(): void public function testVerifyTokenReturnsFalseForInvalidToken(): void { $this->token->setSessionToken('testSessionToken'); - $this->sessionMock - ->method('get') - ->willReturn($this->token); + $this->sessionMock->method('get')->willReturn($this->token); $this->assertFalse($this->token->verifyToken('testPage', 'invalidToken')); } public function testRemoveToken(): void { - $this->sessionMock - ->method('remove') - ->with($this->equalTo('pmf-csrf-token.testPage')); + $this->sessionMock->method('remove')->with($this->equalTo('pmf-csrf-token.testPage')); $this->assertTrue($this->token->removeToken('testPage')); } diff --git a/tests/phpMyFAQ/Setup/EnvironmentConfiguratorTest.php b/tests/phpMyFAQ/Setup/EnvironmentConfiguratorTest.php index 6faf4fe11d..2af6adeff1 100644 --- a/tests/phpMyFAQ/Setup/EnvironmentConfiguratorTest.php +++ b/tests/phpMyFAQ/Setup/EnvironmentConfiguratorTest.php @@ -4,8 +4,8 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Core\Exception; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class EnvironmentConfiguratorTest extends TestCase @@ -68,7 +68,7 @@ public function testGetServerPathWithSubdirectoryPath(): void public function testAdjustRewriteBaseHtaccessThrowsExceptionForMissingFile(): void { $configuration = $this->createStub(Configuration::class); - $configuration->method('getRootPath')->willReturn(dirname(__DIR__, 2). '/path/to'); + $configuration->method('getRootPath')->willReturn(dirname(__DIR__, 2) . '/path/to'); $configuration->method('getDefaultUrl')->willReturn('https://localhost/path/to'); $configurator = new EnvironmentConfigurator($configuration); $this->expectException(Exception::class); @@ -99,12 +99,12 @@ public function testAdjustRewriteBaseHtaccessUpdatesErrorDocumentWithRootPath(): // Set up a proper .htaccess file with ErrorDocument directive $htaccessPath = dirname(__DIR__, 2) . '/.htaccess'; $htaccessContent = <<<'HTACCESS' - - RewriteEngine On - RewriteBase /phpmyfaq-test/ - ErrorDocument 404 /404.html - -HTACCESS; + + RewriteEngine On + RewriteBase /phpmyfaq-test/ + ErrorDocument 404 /404.html + + HTACCESS; file_put_contents($htaccessPath, $htaccessContent); $configuration = $this->createStub(Configuration::class); @@ -127,12 +127,12 @@ public function testAdjustRewriteBaseHtaccessUpdatesErrorDocumentWithSubdirector // Set up a proper .htaccess file with ErrorDocument directive $htaccessPath = dirname(__DIR__, 2) . '/.htaccess'; $htaccessContent = <<<'HTACCESS' - - RewriteEngine On - RewriteBase / - ErrorDocument 404 /404.html - -HTACCESS; + + RewriteEngine On + RewriteBase / + ErrorDocument 404 /404.html + + HTACCESS; file_put_contents($htaccessPath, $htaccessContent); $configuration = $this->createStub(Configuration::class); diff --git a/tests/phpMyFAQ/Setup/HtaccessUpdaterTest.php b/tests/phpMyFAQ/Setup/HtaccessUpdaterTest.php index 45b2e056ee..68e5549f45 100644 --- a/tests/phpMyFAQ/Setup/HtaccessUpdaterTest.php +++ b/tests/phpMyFAQ/Setup/HtaccessUpdaterTest.php @@ -3,15 +3,15 @@ namespace phpMyFAQ\Setup; use phpMyFAQ\Core\Exception; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class HtaccessUpdaterTest extends TestCase { private string $testHtaccessPath; private HtaccessUpdater $htaccessUpdater; - + protected function setUp(): void { parent::setUp(); @@ -25,7 +25,7 @@ protected function tearDown(): void if (file_exists($this->testHtaccessPath)) { unlink($this->testHtaccessPath); } - + $backupFiles = glob($this->testHtaccessPath . '.backup-*'); foreach ($backupFiles as $backupFile) { @unlink($backupFile); @@ -34,7 +34,7 @@ protected function tearDown(): void /** * @throws Exception - */public function testCreateBackup(): void + */ public function testCreateBackup(): void { // Create a test .htaccess file $originalContent = "RewriteEngine On\nRewriteBase /\n"; @@ -44,7 +44,7 @@ protected function tearDown(): void $this->assertFileExists($backupPath); $this->assertEquals($originalContent, file_get_contents($backupPath)); - + // Clean up unlink($backupPath); } @@ -53,7 +53,7 @@ public function testCreateBackupFailsForNonExistentFile(): void { $this->expectException(Exception::class); $this->expectExceptionMessage('The .htaccess file does not exist at:'); - + $this->htaccessUpdater->createBackup('/non/existent/file'); } @@ -61,35 +61,35 @@ public function testUpdateRewriteBasePreservesUserContent(): void { // Create a test .htaccess file with user-generated content $originalContent = << - RewriteEngine On - # the path to your phpMyFAQ installation - RewriteBase /old/path/ - - # User added custom rules - RewriteRule ^api/(.*)$ api/index.php [L,QSA] - - # Error pages - ErrorDocument 404 /index.php?action=404 - - -# User added custom headers -Header set X-Custom-Header "Custom Value" -HTACCESS; + ## + # phpMyFAQ .htaccess file for Apache 2.x + # + DirectoryIndex index.php + + # User added custom directory protection + AuthType Basic + AuthName "Protected Area" + AuthUserFile /path/to/.htpasswd + Require valid-user + + # Custom redirect + Redirect 301 /old-page.html /new-page.html + + + RewriteEngine On + # the path to your phpMyFAQ installation + RewriteBase /old/path/ + + # User added custom rules + RewriteRule ^api/(.*)$ api/index.php [L,QSA] + + # Error pages + ErrorDocument 404 /index.php?action=404 + + + # User added custom headers + Header set X-Custom-Header "Custom Value" + HTACCESS; file_put_contents($this->testHtaccessPath, $originalContent); @@ -118,13 +118,13 @@ public function testUpdateRewriteBaseAddsDirectiveIfMissing(): void { // Create a test .htaccess file without RewriteBase $originalContent = << - RewriteEngine On - - # Some custom rules - RewriteRule ^api/(.*)$ api/index.php [L,QSA] - -HTACCESS; + + RewriteEngine On + + # Some custom rules + RewriteRule ^api/(.*)$ api/index.php [L,QSA] + + HTACCESS; file_put_contents($this->testHtaccessPath, $originalContent); @@ -144,11 +144,11 @@ public function testValidateHtaccessStructure(): void { // Create a valid .htaccess file $validContent = << - RewriteEngine On - RewriteBase / - -HTACCESS; + + RewriteEngine On + RewriteBase / + + HTACCESS; file_put_contents($this->testHtaccessPath, $validContent); @@ -171,11 +171,11 @@ public function testUpdateRewriteBaseWithRootPath(): void { // Create a test .htaccess file $originalContent = << - RewriteEngine On - RewriteBase /subfolder/ - -HTACCESS; + + RewriteEngine On + RewriteBase /subfolder/ + + HTACCESS; file_put_contents($this->testHtaccessPath, $originalContent); @@ -194,10 +194,10 @@ public function testUpdateRewriteBaseWithRootPath(): void public function testRepeatedCallsAreIdempotentAndDoNotDuplicateOrCreateMultipleBackups(): void { $content = << - RewriteEngine On - -HTACCESS; + + RewriteEngine On + + HTACCESS; file_put_contents($this->testHtaccessPath, $content); // First run should add one RewriteBase and create exactly one backup @@ -218,11 +218,11 @@ public function testRepeatedCallsAreIdempotentAndDoNotDuplicateOrCreateMultipleB public function testExistingRewriteBaseWithoutTrailingSlashIsTreatedAsEqual(): void { $content = << - RewriteEngine On - RewriteBase /foo/bar - -HTACCESS; + + RewriteEngine On + RewriteBase /foo/bar + + HTACCESS; file_put_contents($this->testHtaccessPath, $content); // Should detect equivalence and not write a backup diff --git a/tests/phpMyFAQ/Setup/InstallerTest.php b/tests/phpMyFAQ/Setup/InstallerTest.php index 824a5a26ec..0261d730b4 100644 --- a/tests/phpMyFAQ/Setup/InstallerTest.php +++ b/tests/phpMyFAQ/Setup/InstallerTest.php @@ -4,8 +4,8 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\System; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class InstallerTest extends TestCase @@ -22,6 +22,7 @@ protected function setUp(): void $this->system = $this->createStub(System::class); $this->installer = new Installer($this->system); } + public function testCheckBasicStuffThrowsExceptionForMissingDatabase(): void { $this->system->method('checkDatabase')->willReturn(false); diff --git a/tests/phpMyFAQ/Setup/UpdateRunnerTest.php b/tests/phpMyFAQ/Setup/UpdateRunnerTest.php index 430a2df5d6..01447bdbb0 100644 --- a/tests/phpMyFAQ/Setup/UpdateRunnerTest.php +++ b/tests/phpMyFAQ/Setup/UpdateRunnerTest.php @@ -4,19 +4,16 @@ use phpMyFAQ\Configuration; use phpMyFAQ\System; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class UpdateRunnerTest extends TestCase { public function testClassNamespaceAndConstruction(): void { - $runner = new UpdateRunner( - Configuration::getConfigurationInstance(), - new System(), - ); + $runner = new UpdateRunner(Configuration::getConfigurationInstance(), new System()); $this->assertInstanceOf(UpdateRunner::class, $runner); @@ -26,10 +23,7 @@ public function testClassNamespaceAndConstruction(): void public function testVersionPropertyExists(): void { - $runner = new UpdateRunner( - Configuration::getConfigurationInstance(), - new System(), - ); + $runner = new UpdateRunner(Configuration::getConfigurationInstance(), new System()); $reflection = new ReflectionClass($runner); $this->assertTrue($reflection->hasProperty('version')); @@ -43,10 +37,7 @@ public function testVersionPropertyExists(): void public function testAllTaskMethodsExistAndArePrivate(): void { - $runner = new UpdateRunner( - Configuration::getConfigurationInstance(), - new System(), - ); + $runner = new UpdateRunner(Configuration::getConfigurationInstance(), new System()); $reflection = new ReflectionClass($runner); @@ -68,7 +59,11 @@ public function testAllTaskMethodsExistAndArePrivate(): void $parameters = $method->getParameters(); $this->assertCount(1, $parameters, sprintf('Method %s should have exactly one parameter', $methodName)); - $this->assertEquals('io', $parameters[0]->getName(), sprintf('Parameter name of %s should be io', $methodName)); + $this->assertEquals( + 'io', + $parameters[0]->getName(), + sprintf('Parameter name of %s should be io', $methodName), + ); $returnType = $method->getReturnType(); $this->assertNotNull($returnType, sprintf('Method %s should have a return type', $methodName)); @@ -78,10 +73,7 @@ public function testAllTaskMethodsExistAndArePrivate(): void public function testRunMethodSignature(): void { - $runner = new UpdateRunner( - Configuration::getConfigurationInstance(), - new System(), - ); + $runner = new UpdateRunner(Configuration::getConfigurationInstance(), new System()); $reflection = new ReflectionClass($runner); $this->assertTrue($reflection->hasMethod('run')); @@ -98,4 +90,3 @@ public function testRunMethodSignature(): void $this->assertEquals('int', $returnType->getName()); } } - diff --git a/tests/phpMyFAQ/Setup/UpdateTest.php b/tests/phpMyFAQ/Setup/UpdateTest.php index 550592ffc1..5f0a80f809 100644 --- a/tests/phpMyFAQ/Setup/UpdateTest.php +++ b/tests/phpMyFAQ/Setup/UpdateTest.php @@ -6,8 +6,8 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\System; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; use Random\RandomException; #[AllowMockObjectsWithoutExpectations] @@ -15,6 +15,7 @@ class UpdateTest extends TestCase { private Sqlite3 $dbHandle; private Update $update; + protected function setUp(): void { parent::setUp(); @@ -59,7 +60,7 @@ public function testCreateConfigBackup(): void $this->assertMatchesRegularExpression( '/^phpmyfaq-config-backup\.\d{4}-\d{2}-\d{2}\.[0-9a-f]{8}\.zip$/', $filename, - 'Backup filename should contain 8-character hexadecimal hash' + 'Backup filename should contain 8-character hexadecimal hash', ); // Cleanup diff --git a/tests/phpMyFAQ/Setup/UpgradeTest.php b/tests/phpMyFAQ/Setup/UpgradeTest.php index 710ef453c2..34b7734f0b 100644 --- a/tests/phpMyFAQ/Setup/UpgradeTest.php +++ b/tests/phpMyFAQ/Setup/UpgradeTest.php @@ -7,11 +7,11 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Enums\DownloadHostType; use phpMyFAQ\System; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class UpgradeTest extends TestCase @@ -92,7 +92,7 @@ public function testCheckFilesystemMissingConfigFiles(): void { $this->expectException('phpMyFAQ\\Core\\Exception'); $this->expectExceptionMessage( - 'The files /content/core/config/constant.php and /content/core/config/database.php are missing.' + 'The files /content/core/config/constant.php and /content/core/config/database.php are missing.', ); $this->upgrade->checkFilesystem(); } diff --git a/tests/phpMyFAQ/SitemapTest.php b/tests/phpMyFAQ/SitemapTest.php index c6e2e3330e..f3774385d1 100644 --- a/tests/phpMyFAQ/SitemapTest.php +++ b/tests/phpMyFAQ/SitemapTest.php @@ -3,12 +3,13 @@ namespace phpMyFAQ; use phpMyFAQ\Configuration\DatabaseConfiguration; -use phpMyFAQ\Database\PdoSqlite;use phpMyFAQ\Database\Sqlite3; +use phpMyFAQ\Database\PdoSqlite; +use phpMyFAQ\Database\Sqlite3; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use stdClass; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class SitemapTest extends TestCase @@ -35,7 +36,7 @@ protected function setUp(): void $dbConfig->getUser(), $dbConfig->getPassword(), $dbConfig->getDatabase(), - $dbConfig->getPort() + $dbConfig->getPort(), ); $configuration = new Configuration($this->db); $configuration->set('main.referenceURL', 'https://example.com/'); @@ -47,9 +48,9 @@ protected function setUp(): void $this->sitemap = new Sitemap($configuration); $this->db->query( - 'INSERT INTO faqdata ' . - '(id, lang, solution_id, sticky, thema, content, keywords, active, author, email, updated) VALUES ' . - '(1, \'en\', 1000, \'yes\', \'sample question\', \'sample answer\', \'sample keywords\', \'yes\', \'Author\', \'test@example.org\', \'date\')' + 'INSERT INTO faqdata ' + . '(id, lang, solution_id, sticky, thema, content, keywords, active, author, email, updated) VALUES ' + . '(1, \'en\', 1000, \'yes\', \'sample question\', \'sample answer\', \'sample keywords\', \'yes\', \'Author\', \'test@example.org\', \'date\')', ); $this->db->query('INSERT INTO faqdata_group (record_id, group_id) VALUES (1,-1)'); $this->db->query('INSERT INTO faqdata_user (record_id, user_id) VALUES (1,-1)'); diff --git a/tests/phpMyFAQ/StopWordsTest.php b/tests/phpMyFAQ/StopWordsTest.php index 62d821d45a..9dfbd3a291 100644 --- a/tests/phpMyFAQ/StopWordsTest.php +++ b/tests/phpMyFAQ/StopWordsTest.php @@ -4,9 +4,9 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Database\Sqlite3; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class StopWordsTest extends TestCase diff --git a/tests/phpMyFAQ/Strings/MbstringTest.php b/tests/phpMyFAQ/Strings/MbstringTest.php index 1967ebe886..4be1fa674c 100644 --- a/tests/phpMyFAQ/Strings/MbstringTest.php +++ b/tests/phpMyFAQ/Strings/MbstringTest.php @@ -18,15 +18,15 @@ protected function setUp(): void public function testStrlen(): void { // Test case 1: Check the length of a regular string - $result = $this->mbString->strlen("Hello, World!"); + $result = $this->mbString->strlen('Hello, World!'); $this->assertEquals(13, $result); // Test case 2: Check the length of an empty string - $result = $this->mbString->strlen(""); + $result = $this->mbString->strlen(''); $this->assertEquals(0, $result); // Test case 3: Check the length of a string with German umlauts - $result = $this->mbString->strlen("äöü"); + $result = $this->mbString->strlen('äöü'); $this->assertEquals(3, $result); // 3 characters, 6 bytes } @@ -74,7 +74,7 @@ public function testPregReplaceCallback(): void function ($matches) { return strtoupper($matches[0]); }, - 'hello' + 'hello', ); $this->assertEquals('HELLO', $result); } diff --git a/tests/phpMyFAQ/Strings/StringBasicTest.php b/tests/phpMyFAQ/Strings/StringBasicTest.php index 653e2110ec..af845a8d7c 100644 --- a/tests/phpMyFAQ/Strings/StringBasicTest.php +++ b/tests/phpMyFAQ/Strings/StringBasicTest.php @@ -18,15 +18,15 @@ protected function setUp(): void public function testStrlen(): void { // Test case 1: Check the length of a regular string - $result = $this->stringBasic->strlen("Hello, World!"); + $result = $this->stringBasic->strlen('Hello, World!'); $this->assertEquals(13, $result); // Test case 2: Check the length of an empty string - $result = $this->stringBasic->strlen(""); + $result = $this->stringBasic->strlen(''); $this->assertEquals(0, $result); // Test case 3: Check the length of a string with German umlauts - $result = $this->stringBasic->strlen("äöü"); + $result = $this->stringBasic->strlen('äöü'); $this->assertEquals(6, $result); // 3 characters, 6 bytes } @@ -79,7 +79,7 @@ public function testPregReplaceCallback(): void function ($matches) { return strtoupper($matches[0]); }, - 'hello' + 'hello', ); $this->assertEquals('HELLO', $result); } diff --git a/tests/phpMyFAQ/StringsTest.php b/tests/phpMyFAQ/StringsTest.php index 9c5846a46e..8a24d94e1e 100644 --- a/tests/phpMyFAQ/StringsTest.php +++ b/tests/phpMyFAQ/StringsTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class StringsTest extends TestCase @@ -62,7 +62,7 @@ public function testPregReplaceCallback(): void function ($matches) { return strtoupper($matches[0]); }, - 'hello' + 'hello', ); $this->assertEquals('HELLO', $result); } @@ -341,18 +341,18 @@ public function testPregReplaceCallbackComplex(): void $result = Strings::preg_replace_callback( '/(\d+)°([CF])/', function ($matches) { - $temp = (int)$matches[1]; + $temp = (int) $matches[1]; $unit = $matches[2]; if ($unit === 'C') { - $fahrenheit = ($temp * 9/5) + 32; + $fahrenheit = (($temp * 9) / 5) + 32; return $temp . '°C (' . $fahrenheit . '°F)'; } else { - $celsius = ($temp - 32) * 5/9; + $celsius = (($temp - 32) * 5) / 9; return $temp . '°F (' . round($celsius, 1) . '°C)'; } }, - $text + $text, ); $this->assertStringContainsString('25°C (77°F)', $result); diff --git a/tests/phpMyFAQ/SystemTest.php b/tests/phpMyFAQ/SystemTest.php index 796bee6dfc..8691ab695e 100644 --- a/tests/phpMyFAQ/SystemTest.php +++ b/tests/phpMyFAQ/SystemTest.php @@ -3,8 +3,8 @@ namespace phpMyFAQ; use phpMyFAQ\Database\DatabaseDriver; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class SystemTest extends TestCase @@ -13,15 +13,18 @@ public function testGetPoweredByPlainString(): void { $this->assertEquals( sprintf('powered with ❤️ and ☕️ by phpMyFAQ %s', System::getVersion()), - System::getPoweredByPlainString() + System::getPoweredByPlainString(), ); } public function testGetPoweredByString(): void { $this->assertEquals( - sprintf('powered with ❤️ and ☕️ by phpMyFAQ %s', System::getVersion()), - System::getPoweredByString() + sprintf( + 'powered with ❤️ and ☕️ by phpMyFAQ %s', + System::getVersion(), + ), + System::getPoweredByString(), ); } @@ -34,8 +37,7 @@ public function testIsSqlite(): void public function testSetDatabase(): void { // Create a mock DatabaseDriver object - $database = $this->getMockBuilder(DatabaseDriver::class) - ->getMock(); + $database = $this->getMockBuilder(DatabaseDriver::class)->getMock(); // Create a System object and set the mock database driver $system = new System(); @@ -79,7 +81,7 @@ public function testGetGitHubIssuesUrl(): void $this->assertEquals($expectedUrl, $actualUrl); } - + public function testGetAvailableTemplates() { $system = new System(); diff --git a/tests/phpMyFAQ/TagsTest.php b/tests/phpMyFAQ/TagsTest.php index 89c49a4425..377b161780 100644 --- a/tests/phpMyFAQ/TagsTest.php +++ b/tests/phpMyFAQ/TagsTest.php @@ -5,10 +5,10 @@ use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Entity\Tag; use phpMyFAQ\Plugin\PluginException; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Session; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class TagsTest extends TestCase diff --git a/tests/phpMyFAQ/TranslationTest.php b/tests/phpMyFAQ/TranslationTest.php index 5cfb6ae370..649ca78b01 100644 --- a/tests/phpMyFAQ/TranslationTest.php +++ b/tests/phpMyFAQ/TranslationTest.php @@ -6,10 +6,10 @@ use FilesystemIterator; use phpMyFAQ\Core\Exception; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class TranslationTest extends TestCase diff --git a/tests/phpMyFAQ/Twig/Extensions/CategoryNameTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/CategoryNameTwigExtensionTest.php index d53700ef95..ca5b258f56 100644 --- a/tests/phpMyFAQ/Twig/Extensions/CategoryNameTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/CategoryNameTwigExtensionTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\AbstractExtension; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for CategoryNameTwigExtension @@ -112,7 +112,7 @@ public function testClassHasCorrectImports(): void 'use phpMyFAQ\Configuration;', 'use Twig\Attribute\AsTwigFilter;', 'use Twig\Attribute\AsTwigFunction;', - 'use Twig\Extension\AbstractExtension;' + 'use Twig\Extension\AbstractExtension;', ]; foreach ($expectedImports as $import) { @@ -184,7 +184,11 @@ public function testDualAttributeImplementation(): void $method = $reflection->getMethod('getCategoryName'); $attributes = $method->getAttributes(); - $this->assertGreaterThanOrEqual(2, count($attributes), 'Should have at least 2 attributes (Filter and Function)'); + $this->assertGreaterThanOrEqual( + 2, + count($attributes), + 'Should have at least 2 attributes (Filter and Function)', + ); $attributeNames = array_map(fn($attr) => $attr->getName(), $attributes); $this->assertContains('Twig\Attribute\AsTwigFilter', $attributeNames); diff --git a/tests/phpMyFAQ/Twig/Extensions/CreateLinkTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/CreateLinkTwigExtensionTest.php index 5283213b36..397912639a 100644 --- a/tests/phpMyFAQ/Twig/Extensions/CreateLinkTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/CreateLinkTwigExtensionTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\AbstractExtension; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for CreateLinkTwigExtension @@ -128,6 +128,7 @@ public function testCategoryLinkWithNegativeId(): void { // Test parameter type enforcement $this->expectNotToPerformAssertions(); + // The method exists and accepts int parameters as verified in other tests } @@ -153,7 +154,7 @@ public function testClassHasCorrectImports(): void 'use phpMyFAQ\Link;', 'use Twig\Attribute\AsTwigFilter;', 'use Twig\Attribute\AsTwigFunction;', - 'use Twig\Extension\AbstractExtension;' + 'use Twig\Extension\AbstractExtension;', ]; foreach ($expectedImports as $import) { @@ -229,7 +230,11 @@ public function testDualAttributeImplementation(): void $method = $reflection->getMethod('categoryLink'); $attributes = $method->getAttributes(); - $this->assertGreaterThanOrEqual(2, count($attributes), 'Should have at least 2 attributes (Filter and Function)'); + $this->assertGreaterThanOrEqual( + 2, + count($attributes), + 'Should have at least 2 attributes (Filter and Function)', + ); $attributeNames = array_map(fn($attr) => $attr->getName(), $attributes); $this->assertContains('Twig\Attribute\AsTwigFilter', $attributeNames); diff --git a/tests/phpMyFAQ/Twig/Extensions/FaqTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/FaqTwigExtensionTest.php index 6017229deb..bfd657c420 100644 --- a/tests/phpMyFAQ/Twig/Extensions/FaqTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/FaqTwigExtensionTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\AbstractExtension; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for FaqTwigExtension @@ -108,6 +108,7 @@ public function testGetFaqQuestionWithNegativeId(): void { // Test parameter type enforcement $this->expectNotToPerformAssertions(); + // The method exists and accepts int parameters as verified in other tests } @@ -170,7 +171,7 @@ public function testClassHasCorrectImports(): void 'use phpMyFAQ\Configuration;', 'use phpMyFAQ\Faq;', 'use Twig\Attribute\AsTwigFilter;', - 'use Twig\Extension\AbstractExtension;' + 'use Twig\Extension\AbstractExtension;', ]; foreach ($expectedImports as $import) { diff --git a/tests/phpMyFAQ/Twig/Extensions/FormatBytesTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/FormatBytesTwigExtensionTest.php index 95c1465bfc..99abe27af3 100644 --- a/tests/phpMyFAQ/Twig/Extensions/FormatBytesTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/FormatBytesTwigExtensionTest.php @@ -2,6 +2,7 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\LoaderError; @@ -9,7 +10,6 @@ use Twig\Error\SyntaxError; use Twig\Extension\AttributeExtension; use Twig\Loader\ArrayLoader; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class FormatBytesTwigExtensionTest extends TestCase diff --git a/tests/phpMyFAQ/Twig/Extensions/FormatDateTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/FormatDateTwigExtensionTest.php index 65820f152b..9809be1781 100644 --- a/tests/phpMyFAQ/Twig/Extensions/FormatDateTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/FormatDateTwigExtensionTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\AbstractExtension; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for FormatDateTwigExtension @@ -91,7 +91,7 @@ public function testClassHasCorrectImports(): void 'use phpMyFAQ\Configuration;', 'use phpMyFAQ\Date;', 'use Twig\Attribute\AsTwigFilter;', - 'use Twig\Extension\AbstractExtension;' + 'use Twig\Extension\AbstractExtension;', ]; foreach ($expectedImports as $import) { diff --git a/tests/phpMyFAQ/Twig/Extensions/IsoDateTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/IsoDateTwigExtensionTest.php index 0eb7a1d0b4..8698f35bf1 100644 --- a/tests/phpMyFAQ/Twig/Extensions/IsoDateTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/IsoDateTwigExtensionTest.php @@ -2,6 +2,7 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\LoaderError; @@ -9,7 +10,6 @@ use Twig\Error\SyntaxError; use Twig\Extension\AttributeExtension; use Twig\Loader\ArrayLoader; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class IsoDateTwigExtensionTest extends TestCase diff --git a/tests/phpMyFAQ/Twig/Extensions/LanguageCodeTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/LanguageCodeTwigExtensionTest.php index 5199c57007..8b340c6d61 100644 --- a/tests/phpMyFAQ/Twig/Extensions/LanguageCodeTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/LanguageCodeTwigExtensionTest.php @@ -2,6 +2,7 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\LoaderError; @@ -9,7 +10,6 @@ use Twig\Error\SyntaxError; use Twig\Extension\AttributeExtension; use Twig\Loader\ArrayLoader; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class LanguageCodeTwigExtensionTest extends TestCase diff --git a/tests/phpMyFAQ/Twig/Extensions/PermissionTranslationTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/PermissionTranslationTwigExtensionTest.php index b1adb79034..633b7dfaf5 100644 --- a/tests/phpMyFAQ/Twig/Extensions/PermissionTranslationTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/PermissionTranslationTwigExtensionTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\AbstractExtension; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for PermissionTranslationTwigExtension @@ -90,7 +90,7 @@ public function testClassHasCorrectImports(): void $expectedImports = [ 'use phpMyFAQ\Translation;', 'use Twig\Attribute\AsTwigFilter;', - 'use Twig\Extension\AbstractExtension;' + 'use Twig\Extension\AbstractExtension;', ]; foreach ($expectedImports as $import) { diff --git a/tests/phpMyFAQ/Twig/Extensions/PluginTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/PluginTwigExtensionTest.php index ad9c585470..cd031443bf 100644 --- a/tests/phpMyFAQ/Twig/Extensions/PluginTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/PluginTwigExtensionTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\AbstractExtension; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for PluginTwigExtension @@ -96,7 +96,7 @@ public function testClassHasCorrectImports(): void $expectedImports = [ 'use phpMyFAQ\Configuration;', 'use Twig\Attribute\AsTwigFunction;', - 'use Twig\Extension\AbstractExtension;' + 'use Twig\Extension\AbstractExtension;', ]; foreach ($expectedImports as $import) { diff --git a/tests/phpMyFAQ/Twig/Extensions/TagNameTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/TagNameTwigExtensionTest.php index 5afc35da73..196cf3a63d 100644 --- a/tests/phpMyFAQ/Twig/Extensions/TagNameTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/TagNameTwigExtensionTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\AbstractExtension; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for TagNameTwigExtension @@ -91,7 +91,7 @@ public function testClassHasCorrectImports(): void 'use phpMyFAQ\Configuration;', 'use phpMyFAQ\Tags;', 'use Twig\Attribute\AsTwigFilter;', - 'use Twig\Extension\AbstractExtension;' + 'use Twig\Extension\AbstractExtension;', ]; foreach ($expectedImports as $import) { diff --git a/tests/phpMyFAQ/Twig/Extensions/TranslateTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/TranslateTwigExtensionTest.php index 22e285c6c2..919d5ff422 100644 --- a/tests/phpMyFAQ/Twig/Extensions/TranslateTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/TranslateTwigExtensionTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\AbstractExtension; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for TranslateTwigExtension @@ -108,6 +108,7 @@ public function testTranslateWithSpecialCharacters(): void { // Test parameter type enforcement $this->expectNotToPerformAssertions(); + // The method exists and accepts string parameters as verified in other tests } @@ -144,7 +145,7 @@ public function testClassHasCorrectImports(): void $expectedImports = [ 'use phpMyFAQ\Translation;', 'use Twig\Attribute\AsTwigFilter;', - 'use Twig\Extension\AbstractExtension;' + 'use Twig\Extension\AbstractExtension;', ]; foreach ($expectedImports as $import) { diff --git a/tests/phpMyFAQ/Twig/Extensions/UserNameTwigExtensionTest.php b/tests/phpMyFAQ/Twig/Extensions/UserNameTwigExtensionTest.php index a45fca7975..b1a4dcdc01 100644 --- a/tests/phpMyFAQ/Twig/Extensions/UserNameTwigExtensionTest.php +++ b/tests/phpMyFAQ/Twig/Extensions/UserNameTwigExtensionTest.php @@ -2,10 +2,10 @@ namespace phpMyFAQ\Twig\Extensions; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\AbstractExtension; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; /** * Test class for UserNameTwigExtension @@ -201,7 +201,7 @@ public function testClassHasCorrectImports(): void 'use phpMyFAQ\Core\Exception;', 'use phpMyFAQ\User;', 'use Twig\Attribute\AsTwigFilter;', - 'use Twig\Extension\AbstractExtension;' + 'use Twig\Extension\AbstractExtension;', ]; foreach ($expectedImports as $import) { @@ -310,7 +310,7 @@ public function testFilterNamesAreCorrect(): void foreach ($attributes as $attribute) { if ($attribute->getName() === 'Twig\\Attribute\\AsTwigFilter') { $arguments = array_values($attribute->getArguments()); - $this->assertContains($arguments[0], ['userName','realName']); + $this->assertContains($arguments[0], ['userName', 'realName']); } } @@ -320,7 +320,7 @@ public function testFilterNamesAreCorrect(): void foreach ($attributes as $attribute) { if ($attribute->getName() === 'Twig\\Attribute\\AsTwigFilter') { $arguments = array_values($attribute->getArguments()); - $this->assertContains($arguments[0], ['userName','realName']); + $this->assertContains($arguments[0], ['userName', 'realName']); } } } diff --git a/tests/phpMyFAQ/Twig/TwigWrapperTest.php b/tests/phpMyFAQ/Twig/TwigWrapperTest.php index 4c99e864fa..d9bd5c5e03 100644 --- a/tests/phpMyFAQ/Twig/TwigWrapperTest.php +++ b/tests/phpMyFAQ/Twig/TwigWrapperTest.php @@ -3,13 +3,13 @@ namespace phpMyFAQ\Twig; use phpMyFAQ\Core\Exception; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; use Twig\Extension\ExtensionInterface; use Twig\TemplateWrapper; use Twig\TwigFilter; use Twig\TwigFunction; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class TwigWrapperTest extends TestCase diff --git a/tests/phpMyFAQ/User/CurrentUserTest.php b/tests/phpMyFAQ/User/CurrentUserTest.php index 19cd7fbf96..09a2462844 100644 --- a/tests/phpMyFAQ/User/CurrentUserTest.php +++ b/tests/phpMyFAQ/User/CurrentUserTest.php @@ -6,8 +6,8 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Strings; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class CurrentUserTest extends TestCase diff --git a/tests/phpMyFAQ/User/TrackingTest.php b/tests/phpMyFAQ/User/TrackingTest.php index 950ed6257e..15bae36a2c 100644 --- a/tests/phpMyFAQ/User/TrackingTest.php +++ b/tests/phpMyFAQ/User/TrackingTest.php @@ -4,13 +4,13 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\DatabaseDriver; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use ReflectionClass; use Symfony\Component\HttpFoundation\HeaderBag; use Symfony\Component\HttpFoundation\InputBag; use Symfony\Component\HttpFoundation\Request; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class TrackingTest extends TestCase @@ -29,7 +29,7 @@ protected function setUp(): void $this->configurationMock = $this->createStub(Configuration::class); $this->requestMock = $this->createStub(Request::class); $this->requestMock->headers = new HeaderBag([ - 'X-Forwarded-For' => '192.168.1.1' + 'X-Forwarded-For' => '192.168.1.1', ]); $this->requestMock->method('getClientIp')->willReturn('127.0.0.1'); $this->userSessionMock = $this->createStub(UserSession::class); @@ -46,7 +46,7 @@ public function testInitializeSessionId(): void { $this->requestMock->query = new InputBag([ UserSession::KEY_NAME_SESSION_ID => 123, - UserSession::COOKIE_NAME_SESSION_ID => 456 + UserSession::COOKIE_NAME_SESSION_ID => 456, ]); $reflection = new ReflectionClass($this->tracking); @@ -64,7 +64,7 @@ public function testGetCookieId(): void { $this->requestMock->query = new InputBag([ UserSession::KEY_NAME_SESSION_ID => 123, - UserSession::COOKIE_NAME_SESSION_ID => 456 + UserSession::COOKIE_NAME_SESSION_ID => 456, ]); $reflection = new ReflectionClass($this->tracking); @@ -79,9 +79,12 @@ public function testGetCookieId(): void */ public function testCountBots(): void { - $this->configurationMock->method('get')->with('main.botIgnoreList')->willReturn('bot1,bot2'); + $this->configurationMock + ->method('get') + ->with('main.botIgnoreList') + ->willReturn('bot1,bot2'); $this->requestMock->headers = new HeaderBag([ - 'user-agent' => 'bot1' + 'user-agent' => 'bot1', ]); $reflection = new ReflectionClass($this->tracking); diff --git a/tests/phpMyFAQ/User/TwoFactorTest.php b/tests/phpMyFAQ/User/TwoFactorTest.php index 7fa1abd011..a8b87dbca1 100644 --- a/tests/phpMyFAQ/User/TwoFactorTest.php +++ b/tests/phpMyFAQ/User/TwoFactorTest.php @@ -2,14 +2,14 @@ namespace phpMyFAQ\User; +use phpMyFAQ\Configuration; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; -use phpMyFAQ\Configuration; use ReflectionClass; -use RobThree\Auth\TwoFactorAuth; use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider; +use RobThree\Auth\TwoFactorAuth; use RobThree\Auth\TwoFactorAuthException; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class TwoFactorTest extends TestCase @@ -38,7 +38,8 @@ public function testGenerateSecret(): void public function testSaveSecret(): void { - $this->currentUser->expects($this->once()) + $this->currentUser + ->expects($this->once()) ->method('setUserData') ->with(['secret' => 'testsecret']) ->willReturn(true); @@ -55,7 +56,8 @@ public function testSaveSecretWithEmptyString(): void public function testGetSecret(): void { - $this->currentUser->method('getUserData') + $this->currentUser + ->method('getUserData') ->with('secret') ->willReturn('testsecret'); @@ -69,22 +71,23 @@ public function testGetSecret(): void */ public function testValidateToken(): void { - $this->configuration->method('get') + $this->configuration + ->method('get') ->with('security.permLevel') ->willReturn('basic'); - $this->currentUser->method('getUserData') + $this->currentUser + ->method('getUserData') ->with('secret') ->willReturn('testsecret'); - $this->currentUser->method('getUserById') + $this->currentUser + ->method('getUserById') ->with(1) ->willReturn(true); $twoFactorAuth = $this->createStub(TwoFactorAuth::class); - $twoFactorAuth->method('verifyCode') - ->with('testsecret', '123456') - ->willReturn(true); + $twoFactorAuth->method('verifyCode')->with('testsecret', '123456')->willReturn(true); $reflection = new ReflectionClass($this->twoFactor); $property = $reflection->getProperty('twoFactorAuth'); @@ -102,19 +105,16 @@ public function testValidateTokenWithInvalidLength(): void public function testGetQrCode(): void { - $this->configuration->method('getTitle') - ->willReturn('phpMyFAQ'); - $this->currentUser->method('getUserData') + $this->configuration->method('getTitle')->willReturn('phpMyFAQ'); + $this->currentUser + ->method('getUserData') ->with('email') ->willReturn('user@example.com'); - $this->configuration->method('getDefaultUrl') - ->willReturn('https://example.com/'); + $this->configuration->method('getDefaultUrl')->willReturn('https://example.com/'); $qrCodeProvider = $this->createStub(EndroidQrCodeProvider::class); - $qrCodeProvider->method('getMimeType') - ->willReturn('image/png'); - $qrCodeProvider->method('getQRCodeImage') - ->willReturn('fakeimage'); + $qrCodeProvider->method('getMimeType')->willReturn('image/png'); + $qrCodeProvider->method('getQRCodeImage')->willReturn('fakeimage'); $reflection = new ReflectionClass($this->twoFactor); $property = $reflection->getProperty('endroidQrCodeProvider'); diff --git a/tests/phpMyFAQ/User/UserAuthenticationTest.php b/tests/phpMyFAQ/User/UserAuthenticationTest.php index 205bae8659..24ff85df0f 100644 --- a/tests/phpMyFAQ/User/UserAuthenticationTest.php +++ b/tests/phpMyFAQ/User/UserAuthenticationTest.php @@ -5,9 +5,9 @@ use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Translation; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class UserAuthenticationTest extends TestCase diff --git a/tests/phpMyFAQ/User/UserDataTest.php b/tests/phpMyFAQ/User/UserDataTest.php index 30d39887f6..3186f489c6 100644 --- a/tests/phpMyFAQ/User/UserDataTest.php +++ b/tests/phpMyFAQ/User/UserDataTest.php @@ -2,11 +2,11 @@ namespace phpMyFAQ\User; +use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; -use phpMyFAQ\Configuration; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class UserDataTest extends TestCase @@ -40,7 +40,7 @@ public function testFetch(): void { $this->database->method('query')->willReturn(true); $this->database->method('numRows')->willReturn(1); - $this->database->method('fetchObject')->willReturn((object)['key' => 'value']); + $this->database->method('fetchObject')->willReturn((object) ['key' => 'value']); $result = $this->userData->fetch('key', 'value'); $this->assertEquals('value', $result); @@ -70,10 +70,12 @@ public function testSave(): void { $this->database->method('query')->willReturn(true); $this->userData->load(1); - $this->userData->set( - ['display_name', 'is_visible', 'twofactor_enabled', 'secret'], - ['value', 'value', 'value', 'value'] - ); + $this->userData->set(['display_name', 'is_visible', 'twofactor_enabled', 'secret'], [ + 'value', + 'value', + 'value', + 'value', + ]); $result = $this->userData->save(); $this->assertTrue($result); diff --git a/tests/phpMyFAQ/UserTest.php b/tests/phpMyFAQ/UserTest.php index 2e7a794e32..4e5ac98223 100644 --- a/tests/phpMyFAQ/UserTest.php +++ b/tests/phpMyFAQ/UserTest.php @@ -5,10 +5,10 @@ use Exception; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\User\UserData; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionClass; -use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; #[AllowMockObjectsWithoutExpectations] class UserTest extends TestCase @@ -25,9 +25,11 @@ protected function setUp(): void $this->userData = $this->createMock(UserData::class); $this->configuration->method('getDb')->willReturn($this->database); - $this->configuration->method('get')->willReturnMap([ - ['security.permLevel', 'basic'], - ]); + $this->configuration + ->method('get') + ->willReturnMap([ + ['security.permLevel', 'basic'], + ]); $this->user = new User($this->configuration); $this->user->userdata = $this->userData; @@ -97,13 +99,15 @@ public function testCreateUserThrowsExceptionWhenLoginNotUnique(): void $this->database->method('escape')->willReturn('existinguser'); $this->database->method('query')->willReturn(true); $this->database->method('numRows')->willReturn(1); - $this->database->method('fetchArray')->willReturn([ - 'user_id' => 1, - 'login' => 'existinguser', - 'account_status' => 'active', - 'is_superadmin' => false, - 'auth_source' => 'local' - ]); + $this->database + ->method('fetchArray') + ->willReturn([ + 'user_id' => 1, + 'login' => 'existinguser', + 'account_status' => 'active', + 'is_superadmin' => false, + 'auth_source' => 'local', + ]); $this->user->createUser('existinguser'); } diff --git a/tests/phpMyFAQ/UtilsTest.php b/tests/phpMyFAQ/UtilsTest.php index f4366b8bfa..df1de26a51 100644 --- a/tests/phpMyFAQ/UtilsTest.php +++ b/tests/phpMyFAQ/UtilsTest.php @@ -2,8 +2,8 @@ namespace phpMyFAQ; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class UtilsTest extends TestCase diff --git a/tests/phpMyFAQ/Visits/VisitsRepositoryTest.php b/tests/phpMyFAQ/Visits/VisitsRepositoryTest.php index a78be336c4..02266f4cb7 100644 --- a/tests/phpMyFAQ/Visits/VisitsRepositoryTest.php +++ b/tests/phpMyFAQ/Visits/VisitsRepositoryTest.php @@ -8,8 +8,8 @@ use phpMyFAQ\Database; use phpMyFAQ\Database\Sqlite3; use phpMyFAQ\Visits\VisitsRepository; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; +use PHPUnit\Framework\TestCase; #[AllowMockObjectsWithoutExpectations] class VisitsRepositoryTest extends TestCase From f1c9df7e71fc5c865a53a8f3a11e9d4bd38eb1bb Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 14:03:39 +0100 Subject: [PATCH 020/286] refactor: migrated FAQ overview page (#3834) --- phpmyfaq/.htaccess | 3 +- phpmyfaq/overview.php | 59 -------------- .../Controller/Frontend/ContactController.php | 2 +- .../Frontend/OverviewController.php | 77 +++++++++++++++++++ phpmyfaq/src/public-routes.php | 7 +- 5 files changed, 86 insertions(+), 62 deletions(-) delete mode 100644 phpmyfaq/overview.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php diff --git a/phpmyfaq/.htaccess b/phpmyfaq/.htaccess index 079c3fcd02..5a12b436ff 100644 --- a/phpmyfaq/.htaccess +++ b/phpmyfaq/.htaccess @@ -110,6 +110,7 @@ Header set Access-Control-Allow-Headers "Content-Type, Authorization" RewriteRule ^show-categories.html$ index.php?action=show [L,QSA] RewriteRule ^forgot-password$ index.php?action=password [L,QSA] RewriteRule ^(search|open-questions|glossary|overview|login|privacy|contact)\.html$ index.php?action=$1 [L,QSA] + RewriteRule ^404\.html$ index.php [L,QSA] RewriteRule ^(login) index.php?action=login [L,QSA] # start page RewriteRule ^index.html$ index.php [L,QSA] @@ -143,7 +144,7 @@ Header set Access-Control-Allow-Headers "Content-Type, Authorization" # Authentication services RewriteRule ^services/webauthn(.*) index.php [L,QSA] # User pages - RewriteRule ^user/(ucp|bookmarks|request-removal|logout|register) index.php?action=$1 [L,QSA] + RewriteRule ^user/(ucp|bookmarks|request-removal|logout|register)/?$ index.php?action=$1 [L,QSA] # Administration API RewriteRule ^admin/api/(.*) admin/api/index.php [L,QSA] # Administration pages diff --git a/phpmyfaq/overview.php b/phpmyfaq/overview.php deleted file mode 100644 index 50361e1ee8..0000000000 --- a/phpmyfaq/overview.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @copyright 2015-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2015-09-27 - */ - -use phpMyFAQ\Helper\FaqHelper; -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\Extensions\CategoryNameTwigExtension; -use phpMyFAQ\Twig\Extensions\CreateLinkTwigExtension; -use phpMyFAQ\Twig\Extensions\FaqTwigExtension; -use phpMyFAQ\Twig\TwigWrapper; -use Twig\Extension\AttributeExtension; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); -$faqSession->userTracking('overview', 0); - -$faqHelper = new FaqHelper($faqConfig); - -$faq->setUser($user->getUserId()); -$faq->setGroups($currentGroups); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twig->addExtension(new AttributeExtension(CategoryNameTwigExtension::class)); -$twig->addExtension(new AttributeExtension(CreateLinkTwigExtension::class)); -$twig->addExtension(new AttributeExtension(FaqTwigExtension::class)); -$twigTemplate = $twig->loadTemplate('./overview.twig'); - -$templateVars = [ - ...$templateVars, - 'title' => sprintf('%s - %s', Translation::get(key: 'faqOverview'), $faqConfig->getTitle()), - 'metaDescription' => sprintf(Translation::get(key: 'msgOverviewMetaDesc'), $faqConfig->getTitle()), - 'pageHeader' => Translation::get(key: 'faqOverview'), - 'faqOverview' => $faqHelper->createOverview($category, $faq, $faqLangCode), - 'msgAuthor' => Translation::get(key: 'msgAuthor'), - 'msgLastUpdateArticle' => Translation::get(key: 'msgLastUpdateArticle'), -]; - -return $templateVars; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/ContactController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/ContactController.php index 9c6b26fd77..abf322fb8c 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/ContactController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/ContactController.php @@ -20,10 +20,10 @@ namespace phpMyFAQ\Controller\Frontend; use Exception; -use phpMyFAQ\Controller\Route; use phpMyFAQ\Translation; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; final class ContactController extends AbstractFrontController { diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php new file mode 100644 index 0000000000..797bfc8d8a --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php @@ -0,0 +1,77 @@ + + * @copyright 2015-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2015-09-27 + */ + +namespace phpMyFAQ\Controller\Frontend; + +use phpMyFAQ\Category; +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Translation; +use phpMyFAQ\Twig\Extensions\CategoryNameTwigExtension; +use phpMyFAQ\Twig\Extensions\CreateLinkTwigExtension; +use phpMyFAQ\Twig\Extensions\FaqTwigExtension; +use phpMyFAQ\User\CurrentUser; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Twig\Error\LoaderError; +use Twig\Extension\AttributeExtension; + +class OverviewController extends AbstractFrontController +{ + /** + * @throws Exception + * @throws LoaderError + * @throws \Exception + */ #[Route(path: '/overview.html', name: 'public.overview')] + public function index(Request $request): Response + { + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('overview', 0); + + $faqHelper = $this->container->get('phpmyfaq.helper.faq'); + + [$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser); + + $category = new Category($this->configuration, $currentGroups, true); + $category->setUser($currentUser)->setGroups($currentGroups); + + $faq = $this->container->get('phpmyfaq.faq'); + $faq->setUser($currentUser); + $faq->setGroups($currentGroups); + + $this->addExtension(new AttributeExtension(CategoryNameTwigExtension::class)); + $this->addExtension(new AttributeExtension(CreateLinkTwigExtension::class)); + $this->addExtension(new AttributeExtension(FaqTwigExtension::class)); + return $this->render('overview.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'faqOverview'), $this->configuration->getTitle()), + 'metaDescription' => sprintf( + Translation::get(key: 'msgOverviewMetaDesc'), + $this->configuration->getTitle(), + ), + 'pageHeader' => Translation::get(key: 'faqOverview'), + 'faqOverview' => $faqHelper->createOverview( + $category, + $faq, + $this->configuration->getLanguage()->getLanguage(), + ), + 'msgAuthor' => Translation::get(key: 'msgAuthor'), + 'msgLastUpdateArticle' => Translation::get(key: 'msgLastUpdateArticle'), + ]); + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 60fa8c3f7a..676b97d4b1 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -19,7 +19,7 @@ use phpMyFAQ\Controller\Frontend\Api\SetupController; use phpMyFAQ\Controller\Frontend\ContactController; -use phpMyFAQ\Controller\Frontend\PageNotFoundController; +use phpMyFAQ\Controller\Frontend\OverviewController;use phpMyFAQ\Controller\Frontend\PageNotFoundController; use phpMyFAQ\Controller\Frontend\WebAuthnController; use phpMyFAQ\Controller\LlmsController; use phpMyFAQ\Controller\RobotsController; @@ -35,6 +35,11 @@ 'controller' => [ContactController::class, 'index'], 'methods' => 'GET|POST', ], + 'public.overview' => [ + 'path' => '/overview.html', + 'controller' => [OverviewController::class, 'index'], + 'methods' => 'GET', + ], 'public.404' => [ 'path' => '/404.html', 'controller' => [PageNotFoundController::class, 'index'], From 808ecac2da20e34de606b1e1f14023b996a8fe41 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 15:18:20 +0100 Subject: [PATCH 021/286] refactor: migrated sitemap page (#3834) --- phpmyfaq/index.php | 12 ++-- phpmyfaq/sitemap.php | 64 ----------------- .../Frontend/OverviewController.php | 2 +- .../Controller/Frontend/SitemapController.php | 69 +++++++++++++++++++ phpmyfaq/src/public-routes.php | 9 ++- 5 files changed, 82 insertions(+), 74 deletions(-) delete mode 100644 phpmyfaq/sitemap.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/SitemapController.php diff --git a/phpmyfaq/index.php b/phpmyfaq/index.php index fda896fb82..87d176e692 100755 --- a/phpmyfaq/index.php +++ b/phpmyfaq/index.php @@ -121,31 +121,27 @@ // Try Symfony Router first // try { - // Load routes $routes = require __DIR__ . '/src/public-routes.php'; - // Create URL matcher $context = new RequestContext(); $context->fromRequest($request); $matcher = new UrlMatcher($routes, $context); - // Try to match the current route $parameters = $matcher->match($request->getPathInfo()); - // Extract controller and method $controllerCallable = $parameters['_controller']; unset($parameters['_controller'], $parameters['_route'], $parameters['_methods']); - // Instantiate controller and call method + $request->attributes->add($matcher->match($request->getPathInfo())); + if (is_array($controllerCallable)) { [$controllerClass, $method] = $controllerCallable; $controller = new $controllerClass(); - $routeResponse = $controller->$method($request, ...$parameters); + $routeResponse = $controller->$method($request); } else { - $routeResponse = $controllerCallable($request, ...$parameters); + $routeResponse = $controllerCallable($request); } - // Send response and exit $routeResponse->send(); exit(); } catch (ResourceNotFoundException $e) { diff --git a/phpmyfaq/sitemap.php b/phpmyfaq/sitemap.php deleted file mode 100644 index 18315475d9..0000000000 --- a/phpmyfaq/sitemap.php +++ /dev/null @@ -1,64 +0,0 @@ - - * @author Thorsten Rinne - * @copyright 2005-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2005-08-21 - */ - -use phpMyFAQ\Filter; -use phpMyFAQ\Strings; -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; -use Symfony\Component\HttpFoundation\Request; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); -$faqSession->userTracking('sitemap', 0); - -$request = Request::createFromGlobals(); -$letter = Filter::filterVar($request->query->get('letter'), FILTER_SANITIZE_SPECIAL_CHARS); -if (!is_null($letter) && 1 == Strings::strlen($letter)) { - $currLetter = strtoupper(Strings::substr($letter, 0, 1)); -} else { - $currLetter = ''; -} - -$siteMap = $container->get('phpmyfaq.sitemap'); -$siteMap->setUser($currentUser); -$siteMap->setGroups($currentGroups); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./sitemap.twig'); - -// Twig template variables -$templateVars = [ - ...$templateVars, - 'title' => sprintf('%s - %s', Translation::get(key: 'msgSitemap'), $faqConfig->getTitle()), - 'metaDescription' => sprintf(Translation::get(key: 'msgSitemapMetaDesc'), $faqConfig->getTitle()), - 'pageHeader' => $currLetter === '' || $currLetter === '0' ? Translation::get(key: 'msgSitemap') : $currLetter, - 'letters' => $siteMap->getAllFirstLetters(), - 'faqs' => $siteMap->getFaqsFromLetter($currLetter), - 'writeCurrentLetter' => - $currLetter === '' || $currLetter === '0' ? Translation::get(key: 'msgSitemap') : $currLetter, -]; - -return $templateVars; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php index 797bfc8d8a..934d1a86e7 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php @@ -1,7 +1,7 @@ + * @author Thorsten Rinne + * @copyright 2005-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2005-08-21 + */ + +namespace phpMyFAQ\Controller\Frontend; + +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Filter; +use phpMyFAQ\Strings; +use phpMyFAQ\Translation; +use phpMyFAQ\User\CurrentUser; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Twig\Error\LoaderError; + +class SitemapController extends AbstractFrontController +{ + /** + * @throws Exception + * @throws LoaderError + * @throws \Exception + */ #[Route(path: '/sitemap/{letter}/{language}.html', name: 'public.sitemap')] + public function index(Request $request): Response + { + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('sitemap', 0); + + $letter = Filter::filterVar($request->attributes->get('letter'), FILTER_SANITIZE_SPECIAL_CHARS); + if (!is_null($letter) && 1 == Strings::strlen($letter)) { + $currLetter = strtoupper(Strings::substr($letter, 0, 1)); + } else { + $currLetter = ''; + } + + [$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser); + + $siteMap = $this->container->get('phpmyfaq.sitemap'); + $siteMap->setUser($currentUser); + $siteMap->setGroups($currentGroups); + + return $this->render('sitemap.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'msgSitemap'), $this->configuration->getTitle()), + 'metaDescription' => sprintf(Translation::get(key: 'msgSitemapMetaDesc'), $this->configuration->getTitle()), + 'pageHeader' => + $currLetter === '' || $currLetter === '0' ? Translation::get(key: 'msgSitemap') : $currLetter, + 'letters' => $siteMap->getAllFirstLetters(), + 'faqs' => $siteMap->getFaqsFromLetter($currLetter), + 'writeCurrentLetter' => + $currLetter === '' || $currLetter === '0' ? Translation::get(key: 'msgSitemap') : $currLetter, + ]); + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 676b97d4b1..c6367902c1 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -19,7 +19,9 @@ use phpMyFAQ\Controller\Frontend\Api\SetupController; use phpMyFAQ\Controller\Frontend\ContactController; -use phpMyFAQ\Controller\Frontend\OverviewController;use phpMyFAQ\Controller\Frontend\PageNotFoundController; +use phpMyFAQ\Controller\Frontend\OverviewController; +use phpMyFAQ\Controller\Frontend\PageNotFoundController; +use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; use phpMyFAQ\Controller\Frontend\WebAuthnController; use phpMyFAQ\Controller\LlmsController; use phpMyFAQ\Controller\RobotsController; @@ -40,6 +42,11 @@ 'controller' => [OverviewController::class, 'index'], 'methods' => 'GET', ], + 'public.sitemap' => [ + 'path' => '/sitemap/{letter}/{language}.html', + 'controller' => [FrontendSitemapController::class, 'index'], + 'methods' => 'GET', + ], 'public.404' => [ 'path' => '/404.html', 'controller' => [PageNotFoundController::class, 'index'], From c1ee9d1f08d519bd1a40e62ccbdd0c38e0126446 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 15:28:05 +0100 Subject: [PATCH 022/286] refactor: migrated glossary page (#3834) --- phpmyfaq/glossary.php | 69 ------------------ .../Frontend/GlossaryController.php | 72 +++++++++++++++++++ phpmyfaq/src/public-routes.php | 7 +- 3 files changed, 78 insertions(+), 70 deletions(-) delete mode 100644 phpmyfaq/glossary.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/GlossaryController.php diff --git a/phpmyfaq/glossary.php b/phpmyfaq/glossary.php deleted file mode 100644 index 55724f420f..0000000000 --- a/phpmyfaq/glossary.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @copyright 2012-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2012-09-03 - */ - -use phpMyFAQ\Filter; -use phpMyFAQ\Glossary; -use phpMyFAQ\Pagination; -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; -use Symfony\Component\HttpFoundation\Request; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); -$faqSession->userTracking('glossary', 0); - -$request = Request::createFromGlobals(); -$page = Filter::filterVar($request->query->get('page'), FILTER_VALIDATE_INT, 1); - -$glossary = new Glossary($faqConfig); -$glossaryItems = $glossary->fetchAll(); -$numItems = is_countable($glossaryItems) ? count($glossaryItems) : 0; -$itemsPerPage = 8; - -$baseUrl = sprintf('%sindex.php?action=glossary&page=%d', $faqConfig->getDefaultUrl(), $page); - -// Pagination options -$options = [ - 'baseUrl' => $baseUrl, - 'total' => is_countable($glossaryItems) ? count($glossaryItems) : 0, - 'perPage' => $itemsPerPage, - 'pageParamName' => 'page', -]; -$pagination = new Pagination($options); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./glossary.twig'); - -// Twig template variables -$templateVars = [ - ...$templateVars, - 'title' => sprintf('%s - %s', Translation::get(key: 'ad_menu_glossary'), $faqConfig->getTitle()), - 'metaDescription' => sprintf(Translation::get(key: 'msgGlossaryMetaDesc'), $faqConfig->getTitle()), - 'pageHeader' => Translation::get(key: 'ad_menu_glossary'), - 'glossaryItems' => array_slice($glossaryItems, ($page - 1) * $itemsPerPage, $itemsPerPage), - 'pagination' => $pagination->render(), -]; - -return $templateVars; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/GlossaryController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/GlossaryController.php new file mode 100644 index 0000000000..58d60a053f --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/GlossaryController.php @@ -0,0 +1,72 @@ + + * @copyright 2012-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2012-09-03 + */ + +namespace phpMyFAQ\Controller\Frontend; + +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Filter; +use phpMyFAQ\Pagination; +use phpMyFAQ\Translation; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Twig\Error\LoaderError; + +class GlossaryController extends AbstractFrontController +{ + /** + * @throws Exception + * @throws LoaderError + * @throws \Exception + */ #[Route(path: '/glossary.html', name: 'public.glossary')] + public function index(Request $request): Response + { + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('glossary', 0); + + $page = Filter::filterVar($request->query->get('page'), FILTER_VALIDATE_INT, 1); + + $glossary = $this->container->get('phpmyfaq.glossary'); + $glossaryItems = $glossary->fetchAll(); + + $itemsPerPage = 8; + + $baseUrl = sprintf('%sglossary.html?page=%d', $this->configuration->getDefaultUrl(), $page); + + // Pagination options + $options = [ + 'baseUrl' => $baseUrl, + 'total' => is_countable($glossaryItems) ? count($glossaryItems) : 0, + 'perPage' => $itemsPerPage, + 'pageParamName' => 'page', + ]; + $pagination = new Pagination($options); + + return $this->render('glossary.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'ad_menu_glossary'), $this->configuration->getTitle()), + 'metaDescription' => sprintf( + Translation::get(key: 'msgGlossaryMetaDesc'), + $this->configuration->getTitle(), + ), + 'pageHeader' => Translation::get(key: 'ad_menu_glossary'), + 'glossaryItems' => array_slice($glossaryItems, ($page - 1) * $itemsPerPage, $itemsPerPage), + 'pagination' => $pagination->render(), + ]); + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index c6367902c1..43ca328bcb 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -19,7 +19,7 @@ use phpMyFAQ\Controller\Frontend\Api\SetupController; use phpMyFAQ\Controller\Frontend\ContactController; -use phpMyFAQ\Controller\Frontend\OverviewController; +use phpMyFAQ\Controller\Frontend\GlossaryController;use phpMyFAQ\Controller\Frontend\OverviewController; use phpMyFAQ\Controller\Frontend\PageNotFoundController; use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; use phpMyFAQ\Controller\Frontend\WebAuthnController; @@ -37,6 +37,11 @@ 'controller' => [ContactController::class, 'index'], 'methods' => 'GET|POST', ], + 'public.glossary' => [ + 'path' => '/glossary.html', + 'controller' => [GlossaryController::class, 'index'], + 'methods' => 'GET', + ], 'public.overview' => [ 'path' => '/overview.html', 'controller' => [OverviewController::class, 'index'], From 2195ee6834784bf561a2eaed22b84f5e1a1bfbe3 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 15:45:44 +0100 Subject: [PATCH 023/286] refactor: migrated login page (#3834) --- phpmyfaq/login.php | 70 ------------------ .../Frontend/GlossaryController.php | 2 +- .../Controller/Frontend/LoginController.php | 74 +++++++++++++++++++ .../Frontend/OverviewController.php | 2 +- .../Controller/Frontend/SitemapController.php | 2 +- phpmyfaq/src/public-routes.php | 7 +- 6 files changed, 83 insertions(+), 74 deletions(-) delete mode 100644 phpmyfaq/login.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php diff --git a/phpmyfaq/login.php b/phpmyfaq/login.php deleted file mode 100644 index e0055d0b10..0000000000 --- a/phpmyfaq/login.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @copyright 2012-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2012-02-12 - */ - -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); -$faqSession->userTracking('login', 0); - -$loginMessage = ''; - -if (!is_null($error)) { - $loginMessage = ''; -} - -$templateFile = './login.twig'; -if ($action == 'twofactor') { - $templateFile = './twofactor.twig'; -} - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate($templateFile); - -$templateVars = [ - ...$templateVars, - 'title' => sprintf('%s - %s', Translation::get(key: 'msgLoginUser'), $faqConfig->getTitle()), - 'loginHeader' => Translation::get(key: 'msgLoginUser'), - 'sendPassword' => Translation::get(key: 'lostPassword'), - 'loginMessage' => $loginMessage, - 'writeLoginPath' => $faqConfig->getDefaultUrl(), - 'faqloginaction' => $action, - 'login' => Translation::get(key: 'ad_auth_ok'), - 'username' => Translation::get(key: 'ad_auth_user'), - 'password' => Translation::get(key: 'ad_auth_passwd'), - 'rememberMe' => Translation::get(key: 'rememberMe'), - 'msgTwofactorEnabled' => Translation::get(key: 'msgTwofactorEnabled'), - 'msgTwofactorTokenModelTitle' => Translation::get(key: 'msgTwofactorTokenModelTitle'), - 'msgEnterTwofactorToken' => Translation::get(key: 'msgEnterTwofactorToken'), - 'msgTwofactorCheck' => Translation::get(key: 'msgTwofactorCheck'), - 'userid' => $userId, - 'enableRegistration' => $faqConfig->get('security.enableRegistration'), - 'registerUser' => Translation::get(key: 'msgRegistration'), - 'useSignInWithMicrosoft' => $faqConfig->isSignInWithMicrosoftActive(), - 'isWebAuthnEnabled' => $faqConfig->get('security.enableWebAuthnSupport'), -]; - -return $templateVars; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/GlossaryController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/GlossaryController.php index 58d60a053f..c19b5a9777 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/GlossaryController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/GlossaryController.php @@ -26,7 +26,7 @@ use Symfony\Component\Routing\Attribute\Route; use Twig\Error\LoaderError; -class GlossaryController extends AbstractFrontController +final class GlossaryController extends AbstractFrontController { /** * @throws Exception diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php new file mode 100644 index 0000000000..3c658297d8 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php @@ -0,0 +1,74 @@ + + * @copyright 2012-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2012-02-12 + */ + +namespace phpMyFAQ\Controller\Frontend; + +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Translation; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Twig\Error\LoaderError; + +final class LoginController extends AbstractFrontController +{ + /** + * @throws Exception + * @throws LoaderError + * @throws \Exception + */ #[Route(path: '/login', name: 'public.login')] + public function index(Request $request): Response + { + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('login', 0); + + // @todo Implement login message and action handling + // $loginMessage = ''; + // if (!is_null($error)) { + // $loginMessage = ''; + // } + // + // $templateFile = './login.twig'; + // if ($action == 'twofactor') { + // $templateFile = './twofactor.twig'; + // } + + return $this->render('login.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'msgLoginUser'), $this->configuration->getTitle()), + 'loginHeader' => Translation::get(key: 'msgLoginUser'), + 'sendPassword' => Translation::get(key: 'lostPassword'), + 'loginMessage' => '', //$loginMessage, + 'writeLoginPath' => $this->configuration->getDefaultUrl(), + 'faqloginaction' => '', //$action, + 'login' => Translation::get(key: 'ad_auth_ok'), + 'username' => Translation::get(key: 'ad_auth_user'), + 'password' => Translation::get(key: 'ad_auth_passwd'), + 'rememberMe' => Translation::get(key: 'rememberMe'), + 'msgTwofactorEnabled' => Translation::get(key: 'msgTwofactorEnabled'), + 'msgTwofactorTokenModelTitle' => Translation::get(key: 'msgTwofactorTokenModelTitle'), + 'msgEnterTwofactorToken' => Translation::get(key: 'msgEnterTwofactorToken'), + 'msgTwofactorCheck' => Translation::get(key: 'msgTwofactorCheck'), + 'userid' => $this->currentUser->getUserId(), + 'enableRegistration' => $this->configuration->get('security.enableRegistration'), + 'registerUser' => Translation::get(key: 'msgRegistration'), + 'useSignInWithMicrosoft' => $this->configuration->isSignInWithMicrosoftActive(), + 'isWebAuthnEnabled' => $this->configuration->get('security.enableWebAuthnSupport'), + ]); + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php index 934d1a86e7..4036463eb7 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OverviewController.php @@ -30,7 +30,7 @@ use Twig\Error\LoaderError; use Twig\Extension\AttributeExtension; -class OverviewController extends AbstractFrontController +final class OverviewController extends AbstractFrontController { /** * @throws Exception diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/SitemapController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/SitemapController.php index 31437a18e3..d8ea3ff711 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/SitemapController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/SitemapController.php @@ -28,7 +28,7 @@ use Symfony\Component\Routing\Attribute\Route; use Twig\Error\LoaderError; -class SitemapController extends AbstractFrontController +final class SitemapController extends AbstractFrontController { /** * @throws Exception diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 43ca328bcb..c110f08e5b 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -19,7 +19,7 @@ use phpMyFAQ\Controller\Frontend\Api\SetupController; use phpMyFAQ\Controller\Frontend\ContactController; -use phpMyFAQ\Controller\Frontend\GlossaryController;use phpMyFAQ\Controller\Frontend\OverviewController; +use phpMyFAQ\Controller\Frontend\GlossaryController;use phpMyFAQ\Controller\Frontend\LoginController;use phpMyFAQ\Controller\Frontend\OverviewController; use phpMyFAQ\Controller\Frontend\PageNotFoundController; use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; use phpMyFAQ\Controller\Frontend\WebAuthnController; @@ -42,6 +42,11 @@ 'controller' => [GlossaryController::class, 'index'], 'methods' => 'GET', ], + 'public.login' => [ + 'path' => '/login', + 'controller' => [LoginController::class, 'index'], + 'methods' => 'GET|POST', + ], 'public.overview' => [ 'path' => '/overview.html', 'controller' => [OverviewController::class, 'index'], From 4d25bd08fb249c1d7fd684cfe0a4f4ae8cbbabee Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 16:00:30 +0100 Subject: [PATCH 024/286] refactor: migrated forgot password page (#3834) --- phpmyfaq/assets/templates/default/login.twig | 2 +- .../assets/templates/default/password.twig | 2 +- phpmyfaq/password.php | 38 ------------------- .../Controller/Frontend/LoginController.php | 20 ++++++++++ phpmyfaq/src/public-routes.php | 5 +++ 5 files changed, 27 insertions(+), 40 deletions(-) delete mode 100644 phpmyfaq/password.php diff --git a/phpmyfaq/assets/templates/default/login.twig b/phpmyfaq/assets/templates/default/login.twig index f4011faaaa..1727e567c9 100644 --- a/phpmyfaq/assets/templates/default/login.twig +++ b/phpmyfaq/assets/templates/default/login.twig @@ -55,7 +55,7 @@ diff --git a/phpmyfaq/assets/templates/default/password.twig b/phpmyfaq/assets/templates/default/password.twig index 87773ac707..855720e77d 100644 --- a/phpmyfaq/assets/templates/default/password.twig +++ b/phpmyfaq/assets/templates/default/password.twig @@ -16,7 +16,7 @@
    -

    {{ 'lostPassword' | translate }}

    +

    {{ 'lostPassword' | translate }}

    - * @copyright 2012-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2012-03-26 - */ - -use phpMyFAQ\Twig\TwigWrapper; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->userTracking('forgot_password', 0); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./password.twig'); - -$templateVars = [ - ... $templateVars, - 'lang' => $faqConfig->getLanguage()->getLanguage(), -]; - -return $templateVars; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php index 3c658297d8..4012feed8d 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php @@ -71,4 +71,24 @@ public function index(Request $request): Response 'isWebAuthnEnabled' => $this->configuration->get('security.enableWebAuthnSupport'), ]); } + + /** + * @throws Exception + * @throws LoaderError + * @throws \Exception + */ + #[Route(path: '/forgot-password', name: 'public.forgot-password')] + public function forgotPassword(Request $request): Response + { + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('forgot_password', 0); + + return $this->render('password.twig', [ + ...$this->getHeader($request), + 'lang' => $this->configuration->getLanguage()->getLanguage(), + 'username' => Translation::get(key: 'ad_auth_user'), + 'password' => Translation::get(key: 'ad_auth_passwd'), + ]); + } } diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index c110f08e5b..aba347f5a4 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -37,6 +37,11 @@ 'controller' => [ContactController::class, 'index'], 'methods' => 'GET|POST', ], + 'public.forgot-password' => [ + 'path' => '/forgot-password', + 'controller' => [LoginController::class, 'forgotPassword'], + 'methods' => 'GET|POST', + ], 'public.glossary' => [ 'path' => '/glossary.html', 'controller' => [GlossaryController::class, 'index'], From 6bf445aba0f7d2503f54375751a7dd17781e40d0 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 19:31:56 +0100 Subject: [PATCH 025/286] refactor: use flash message for error message if login is not successful --- phpmyfaq/assets/templates/default/login.twig | 4 ++- phpmyfaq/index.php | 9 ++--- .../Controller/Frontend/LoginController.php | 16 +++------ phpmyfaq/src/phpMyFAQ/User.php | 6 ++-- phpmyfaq/src/phpMyFAQ/User/CurrentUser.php | 10 +++--- .../src/phpMyFAQ/User/UserAuthentication.php | 33 +++++++++++-------- 6 files changed, 40 insertions(+), 38 deletions(-) diff --git a/phpmyfaq/assets/templates/default/login.twig b/phpmyfaq/assets/templates/default/login.twig index 1727e567c9..8879ca2095 100644 --- a/phpmyfaq/assets/templates/default/login.twig +++ b/phpmyfaq/assets/templates/default/login.twig @@ -8,7 +8,9 @@

    {% endif %} - {{ loginMessage | raw }} + {% if errorMessage %} + + {% endif %}
    diff --git a/phpmyfaq/index.php b/phpmyfaq/index.php index 87d176e692..5fc4cda70e 100755 --- a/phpmyfaq/index.php +++ b/phpmyfaq/index.php @@ -44,7 +44,7 @@ use phpMyFAQ\User\CurrentUser; use phpMyFAQ\User\TwoFactor; use phpMyFAQ\User\UserAuthentication; -use phpMyFAQ\User\UserSession; +use phpMyFAQ\User\UserException;use phpMyFAQ\User\UserSession; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; @@ -235,10 +235,11 @@ try { $user = $userAuth->authenticate($faqusername, $faqpassword); $userId = $user->getUserId(); - } catch (Exception $e) { + } catch (UserException $e) { $faqConfig->getLogger()->error('Failed login: ' . $e->getMessage()); - $action = 'login'; - $error = $e->getMessage(); + $container->get('session')->getFlashBag()->add('error', $e->getMessage()); + $redirect = new RedirectResponse($faqConfig->getDefaultUrl() . 'login'); + $redirect->send(); } } else { // Try to authenticate with cookie information diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php index 4012feed8d..73ff3938ee 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/LoginController.php @@ -37,25 +37,17 @@ public function index(Request $request): Response $faqSession->setCurrentUser($this->currentUser); $faqSession->userTracking('login', 0); - // @todo Implement login message and action handling - // $loginMessage = ''; - // if (!is_null($error)) { - // $loginMessage = ''; - // } - // - // $templateFile = './login.twig'; - // if ($action == 'twofactor') { - // $templateFile = './twofactor.twig'; - // } + $session = $this->container->get('session'); + $errorMessages = $session->getFlashBag()->get('error'); + $errorMessage = !empty($errorMessages) ? $errorMessages[0] : null; return $this->render('login.twig', [ ...$this->getHeader($request), 'title' => sprintf('%s - %s', Translation::get(key: 'msgLoginUser'), $this->configuration->getTitle()), 'loginHeader' => Translation::get(key: 'msgLoginUser'), 'sendPassword' => Translation::get(key: 'lostPassword'), - 'loginMessage' => '', //$loginMessage, + 'errorMessage' => $errorMessage, 'writeLoginPath' => $this->configuration->getDefaultUrl(), - 'faqloginaction' => '', //$action, 'login' => Translation::get(key: 'ad_auth_ok'), 'username' => Translation::get(key: 'ad_auth_user'), 'password' => Translation::get(key: 'ad_auth_passwd'), diff --git a/phpmyfaq/src/phpMyFAQ/User.php b/phpmyfaq/src/phpMyFAQ/User.php index 629fd7e55c..c06df296be 100644 --- a/phpmyfaq/src/phpMyFAQ/User.php +++ b/phpmyfaq/src/phpMyFAQ/User.php @@ -265,7 +265,7 @@ public function getUserByCookie(string $cookie): bool $user = $this->configuration->getDb()->fetchArray($res); - // Don't ever log in via anonymous user + // Don't ever log in via an anonymous user if (-1 === $user['user_id']) { return false; } @@ -300,7 +300,7 @@ public function getUserId(): int } /** - * Checks if display name is already used. Returns true, if already in use. + * Checks if the display name is already used. Returns true, if already in use. */ public function checkDisplayName(string $name): bool { @@ -312,7 +312,7 @@ public function checkDisplayName(string $name): bool } /** - * Checks if email address is already used. Returns true, if already in use. + * Checks if the email address is already used. Returns true, if already in use. */ public function checkMailAddress(string $name): bool { diff --git a/phpmyfaq/src/phpMyFAQ/User/CurrentUser.php b/phpmyfaq/src/phpMyFAQ/User/CurrentUser.php index b96944d89c..e6c16a3923 100644 --- a/phpmyfaq/src/phpMyFAQ/User/CurrentUser.php +++ b/phpmyfaq/src/phpMyFAQ/User/CurrentUser.php @@ -25,6 +25,7 @@ namespace phpMyFAQ\User; use phpMyFAQ\Auth\AuthDriverInterface; +use phpMyFAQ\Auth\AuthException; use phpMyFAQ\Configuration; use phpMyFAQ\Core\Exception; use phpMyFAQ\Database; @@ -119,7 +120,8 @@ public function __construct(Configuration $configuration) * * @param string $login Login name * @param string $password Password - * @throws Exception + * @throws UserException + * @throws AuthException * @throws \Exception */ public function login(string $login, #[SensitiveParameter] string $password): bool @@ -139,7 +141,7 @@ public function login(string $login, #[SensitiveParameter] string $password): bo // First check for brute force attack $this->getUserByLogin($login); if ($this->isFailedLastLoginAttempt()) { - throw new Exception(parent::ERROR_USER_TOO_MANY_FAILED_LOGINS); + throw new UserException(parent::ERROR_USER_TOO_MANY_FAILED_LOGINS); } // Extract domain if LDAP is active and ldap_use_domain_prefix is true @@ -212,11 +214,11 @@ public function login(string $login, #[SensitiveParameter] string $password): bo $this->configuration->get(item: 'security.loginWithEmailAddress') && !Filter::filterVar($login, FILTER_VALIDATE_EMAIL) ) { - throw new Exception(parent::ERROR_USER_INCORRECT_LOGIN); + throw new UserException(parent::ERROR_USER_INCORRECT_LOGIN); } if (!$this->isFailedLastLoginAttempt()) { - throw new Exception(parent::ERROR_USER_INCORRECT_PASSWORD); + throw new UserException(parent::ERROR_USER_INCORRECT_PASSWORD); } return false; diff --git a/phpmyfaq/src/phpMyFAQ/User/UserAuthentication.php b/phpmyfaq/src/phpMyFAQ/User/UserAuthentication.php index 8eb66fa4aa..9924274b03 100644 --- a/phpmyfaq/src/phpMyFAQ/User/UserAuthentication.php +++ b/phpmyfaq/src/phpMyFAQ/User/UserAuthentication.php @@ -24,6 +24,7 @@ namespace phpMyFAQ\User; +use phpMyFAQ\Auth\AuthException; use phpMyFAQ\Auth\AuthLdap; use phpMyFAQ\Auth\AuthSso; use phpMyFAQ\Configuration; @@ -65,9 +66,9 @@ public function setTwoFactorAuth(bool $twoFactorAuth): void /** * Authenticates a user with a given username and password against - * LDAP, SSO or local database. + * LDAP, SSO, or local database. * - * @throws UserException|Exception + * @throws UserException */ public function authenticate(string $username, #[SensitiveParameter] string $password): CurrentUser { @@ -78,20 +79,24 @@ public function authenticate(string $username, #[SensitiveParameter] string $pas $this->authenticateLdap(); $this->authenticateSso(); - if ($this->currentUser->login($username, $password)) { - if ($this->currentUser->getUserData('twofactor_enabled')) { - $this->setTwoFactorAuth(true); - $this->currentUser->setLoggedIn(false); - } elseif ($this->currentUser->getStatus() !== 'blocked') { - $this->currentUser->setLoggedIn(true); + try { + if ($this->currentUser->login($username, $password)) { + if ($this->currentUser->getUserData('twofactor_enabled')) { + $this->setTwoFactorAuth(true); + $this->currentUser->setLoggedIn(false); + } elseif ($this->currentUser->getStatus() !== 'blocked') { + $this->currentUser->setLoggedIn(true); + } else { + $this->currentUser->setLoggedIn(false); + throw new UserException( + (Translation::get(key: 'ad_auth_fail') ?? 'Authentication failed') . ' (' . $username . ')', + ); + } } else { - $this->currentUser->setLoggedIn(false); - throw new UserException( - (Translation::get(key: 'ad_auth_fail') ?? 'Authentication failed') . ' (' . $username . ')', - ); + throw new UserException(Translation::get(key: 'ad_auth_fail') ?? 'Authentication failed'); } - } else { - throw new UserException(Translation::get(key: 'ad_auth_fail') ?? 'Authentication failed'); + } catch (AuthException $e) { + throw new UserException($e->getMessage()); } return $this->currentUser; From ea83024cf38eb45519b3e5dd0b3b22795d3a919e Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 19:40:00 +0100 Subject: [PATCH 026/286] fix: corrected request object handling for changing passwords --- phpmyfaq/src/phpMyFAQ/Auth/AuthDatabase.php | 2 +- .../Administration/PasswordChangeController.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/phpmyfaq/src/phpMyFAQ/Auth/AuthDatabase.php b/phpmyfaq/src/phpMyFAQ/Auth/AuthDatabase.php index 645fabe676..a8cafd8c50 100644 --- a/phpmyfaq/src/phpMyFAQ/Auth/AuthDatabase.php +++ b/phpmyfaq/src/phpMyFAQ/Auth/AuthDatabase.php @@ -170,7 +170,7 @@ public function checkCredentials( throw new AuthException(User::ERROR_USER_NOT_FOUND); } - // if login not unique, raise an error, but continue + // if login not unique, raise an error but continue if ($numRows > 1) { throw new AuthException(User::ERROR_USER_LOGIN_NOT_UNIQUE); } diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/PasswordChangeController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/PasswordChangeController.php index 41103001a3..53e2bd1b2b 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/PasswordChangeController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/PasswordChangeController.php @@ -58,7 +58,7 @@ public function update(Request $request): Response { $this->userHasPermission(PermissionType::PASSWORD_CHANGE); - $csrfToken = Filter::filterVar($request->attributes->get('pmf-csrf-token'), FILTER_SANITIZE_SPECIAL_CHARS); + $csrfToken = Filter::filterVar($request->request->get('pmf-csrf-token'), FILTER_SANITIZE_SPECIAL_CHARS); if (!Token::getInstance($this->container->get(id: 'session'))->verifyToken('password', $csrfToken)) { throw new Exception('Invalid CSRF token'); @@ -73,10 +73,10 @@ public function update(Request $request): Response $authSource->enableReadOnly(); } - $oldPassword = Filter::filterVar($request->attributes->get('faqpassword_old'), FILTER_SANITIZE_SPECIAL_CHARS); - $newPassword = Filter::filterVar($request->attributes->get('faqpassword'), FILTER_SANITIZE_SPECIAL_CHARS); + $oldPassword = Filter::filterVar($request->request->get('faqpassword_old'), FILTER_SANITIZE_SPECIAL_CHARS); + $newPassword = Filter::filterVar($request->request->get('faqpassword'), FILTER_SANITIZE_SPECIAL_CHARS); $retypedPassword = Filter::filterVar( - $request->attributes->get('faqpassword_confirm'), + $request->request->get('faqpassword_confirm'), FILTER_SANITIZE_SPECIAL_CHARS, ); From b85a2f3e81d39e876c292407be5a5691e73df5a6 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 1 Jan 2026 22:20:58 +0100 Subject: [PATCH 027/286] refactor: migrated attachment page (#3834) Added AttachmentService to avoid logic in the controller --- phpmyfaq/attachment.php | 158 ------------------ .../Attachment/AbstractAttachment.php | 4 +- .../phpMyFAQ/Attachment/AttachmentService.php | 150 +++++++++++++++++ .../Frontend/AttachmentController.php | 92 ++++++++++ phpmyfaq/src/public-routes.php | 7 +- .../Attachment/AbstractAttachmentTest.php | 2 +- 6 files changed, 251 insertions(+), 162 deletions(-) delete mode 100644 phpmyfaq/attachment.php create mode 100644 phpmyfaq/src/phpMyFAQ/Attachment/AttachmentService.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/AttachmentController.php diff --git a/phpmyfaq/attachment.php b/phpmyfaq/attachment.php deleted file mode 100644 index 222ba22bca..0000000000 --- a/phpmyfaq/attachment.php +++ /dev/null @@ -1,158 +0,0 @@ - - * @copyright 2009-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2009-06-23 - */ - -use phpMyFAQ\Attachment\AttachmentException; -use phpMyFAQ\Attachment\AttachmentFactory; -use phpMyFAQ\Faq\Permission; -use phpMyFAQ\Filter; -use phpMyFAQ\Permission\MediumPermission; -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\StreamedResponse; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -set_time_limit(0); - -if (headers_sent()) { - die(); -} - -$attachmentErrors = []; - -// -// Service Containers -// -$container = new ContainerBuilder(); -$loader = new PhpFileLoader($container, new FileLocator(__DIR__)); -try { - $loader->load('src/services.php'); -} catch (Exception $exception) { - echo $exception->getMessage(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$request = Request::createFromGlobals(); - -// authenticate with session information -$user = $container->get('phpmyfaq.user.current_user'); - -$id = Filter::filterVar($request->query->get('id'), FILTER_VALIDATE_INT); - -$faqPermission = new Permission($faqConfig); - -$userPermission = []; -$groupPermission = []; - -try { - $attachment = AttachmentFactory::create($id); - $userPermission = $faqPermission->get(Permission::USER, $attachment->getRecordId()); - $groupPermission = $faqPermission->get(Permission::GROUP, $attachment->getRecordId()); -} catch (AttachmentException $attachmentException) { - $attachmentErrors[] = - Translation::get(key: 'msgAttachmentInvalid') . ' (' . $attachmentException->getMessage() . ')'; -} - -// Check on group permissions -if ($user->perm instanceof MediumPermission) { - if ($groupPermission !== []) { - foreach ($user->perm->getUserGroups($user->getUserId()) as $userGroups) { - if (in_array($userGroups, $groupPermission)) { - $groupPermission = true; - break; - } - } - } else { - $groupPermission = false; - } -} else { - $groupPermission = true; -} - -// Check user's permissions -$userPermission = in_array($user->getUserId(), $userPermission); - -// get user rights -$permission = []; -if ($user->isLoggedIn()) { - // read all rights, set false - $allRights = $user->perm->getAllRightsData(); - foreach ($allRights as $right) { - $permission[$right['name']] = false; - } - - // check user rights, set true - $allUserRights = $user->perm->getAllUserRights($user->getUserId()); - foreach ($allRights as $allRight) { - if (in_array($allRight['right_id'], $allUserRights)) { - $permission[$allRight['name']] = true; - } - } -} - -if ( - $attachment - && $attachment->getRecordId() > 0 - && ( - $faqConfig->get('records.allowDownloadsForGuests') - || ($groupPermission || $groupPermission && $userPermission) - && isset($permission['dlattachment']) - ) -) { - $response = new StreamedResponse(function () use ($attachment) { - $attachment->rawOut(); - }); - - $response->headers->set('Content-Type', $attachment->getMimeType()); - $response->headers->set('Content-Length', $attachment->getFilesize()); - - if ($attachment->getMimeType() === 'application/pdf') { - $response->headers->set( - 'Content-Disposition', - 'inline; filename="' . rawurlencode($attachment->getFilename()) . '"', - ); - } else { - $response->headers->set( - 'Content-Disposition', - 'attachment; filename="' . rawurlencode($attachment->getFilename()) . '"', - ); - } - - $response->headers->set('Content-MD5', $attachment->getRealHash()); - $response->send(); -} else { - $attachmentErrors[] = Translation::get(key: 'msgAttachmentInvalid'); -} - -// If we're here, there was an error with file download -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./attachment.twig'); - -// Twig template variables -$templateVars = [ - ...$templateVars, - 'attachmentErrors' => $attachmentErrors, -]; - -return $templateVars; diff --git a/phpmyfaq/src/phpMyFAQ/Attachment/AbstractAttachment.php b/phpmyfaq/src/phpMyFAQ/Attachment/AbstractAttachment.php index 9ed5351379..a9cc8237a0 100644 --- a/phpmyfaq/src/phpMyFAQ/Attachment/AbstractAttachment.php +++ b/phpmyfaq/src/phpMyFAQ/Attachment/AbstractAttachment.php @@ -150,11 +150,11 @@ protected function getMeta(): bool public function buildUrl(): string { - return sprintf('index.php?action=attachment&id=%d', $this->id); + return sprintf('./attachment/%d', $this->id); } /** - * Set encryption key. + * Set the encryption key. * * @param string|null $key Encryption key * @param bool $default if the key is default system-wide diff --git a/phpmyfaq/src/phpMyFAQ/Attachment/AttachmentService.php b/phpmyfaq/src/phpMyFAQ/Attachment/AttachmentService.php new file mode 100644 index 0000000000..6fa601f66b --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Attachment/AttachmentService.php @@ -0,0 +1,150 @@ + + * @copyright 2025 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2025-01-01 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Attachment; + +use phpMyFAQ\Configuration; +use phpMyFAQ\Faq\Permission; +use phpMyFAQ\Permission\MediumPermission; +use phpMyFAQ\Translation; +use phpMyFAQ\User\CurrentUser; + +/** + * Service for attachment operations and permission checks. + */ +final class AttachmentService +{ + public function __construct( + private readonly Configuration $configuration, + private readonly CurrentUser $currentUser, + private readonly Permission $faqPermission, + ) { + } + + /** + * Retrieves an attachment by ID. + * + * @throws AttachmentException + */ + public function getAttachment(int $attachmentId): ?AbstractAttachment + { + return AttachmentFactory::create($attachmentId); + } + + /** + * Checks if the current user has permission to download an attachment. + */ + public function canDownloadAttachment(AbstractAttachment $attachment): bool + { + // Allow downloads for guests if configured + if ($this->configuration->get('records.allowDownloadsForGuests')) { + return true; + } + + // Check group and user permissions + $hasGroupPermission = $this->checkGroupPermission($attachment); + $hasUserPermission = $this->checkUserPermission($attachment); + $userRights = $this->getUserRights(); + + return ( + ($hasGroupPermission || $hasGroupPermission && $hasUserPermission) + && isset($userRights['dlattachment']) + && $userRights['dlattachment'] + ); + } + + /** + * Checks group permission for an attachment. + */ + private function checkGroupPermission(AbstractAttachment $attachment): bool + { + $groupPermission = $this->faqPermission->get(Permission::GROUP, $attachment->getRecordId()); + + if (!$this->currentUser->perm instanceof MediumPermission) { + return true; + } + + if ($groupPermission === []) { + return false; + } + + foreach ($this->currentUser->perm->getUserGroups($this->currentUser->getUserId()) as $userGroup) { + if (in_array($userGroup, $groupPermission, true)) { + return true; + } + } + + return false; + } + + /** + * Checks user permission for an attachment. + */ + private function checkUserPermission(AbstractAttachment $attachment): bool + { + $userPermission = $this->faqPermission->get(Permission::USER, $attachment->getRecordId()); + return in_array($this->currentUser->getUserId(), $userPermission, true); + } + + /** + * Gets all user rights. + * + * @return array + */ + private function getUserRights(): array + { + $permission = []; + + if (!$this->currentUser->isLoggedIn()) { + return $permission; + } + + // Read all rights, set false + $allRights = $this->currentUser->perm->getAllRightsData(); + foreach ($allRights as $right) { + $permission[$right['name']] = false; + } + + // Check user rights, set true + $allUserRights = $this->currentUser->perm->getAllUserRights($this->currentUser->getUserId()); + foreach ($allRights as $allRight) { + if (in_array($allRight['right_id'], $allUserRights, true)) { + $permission[$allRight['name']] = true; + } + } + + return $permission; + } + + /** + * Gets an error message for attachment exceptions. + */ + public function getAttachmentErrorMessage(AttachmentException $exception): string + { + return Translation::get(key: 'msgAttachmentInvalid') . ' (' . $exception->getMessage() . ')'; + } + + /** + * Gets generic attachment error message. + */ + public function getGenericErrorMessage(): string + { + return Translation::get(key: 'msgAttachmentInvalid'); + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AttachmentController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AttachmentController.php new file mode 100644 index 0000000000..6d3a1f0096 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AttachmentController.php @@ -0,0 +1,92 @@ + + * @author Thorsten Rinne + * @copyright 2009-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2009-06-23 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller\Frontend; + +use phpMyFAQ\Attachment\AttachmentException; +use phpMyFAQ\Attachment\AttachmentService; +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Filter; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\Routing\Attribute\Route; + +final class AttachmentController extends AbstractFrontController +{ + /** + * @throws Exception + * @throws \Exception + */ + #[Route(path: '/attachment/{attachmentId}', name: 'public.attachment')] + public function index(Request $request): Response + { + $id = Filter::filterVar($request->attributes->get('attachmentId'), FILTER_VALIDATE_INT); + $attachmentErrors = []; + $attachment = null; + + $attachmentService = new AttachmentService( + $this->configuration, + $this->currentUser, + $this->container->get('phpmyfaq.faq.permission'), + ); + + if ($id === false || $id === null) { + $attachmentErrors[] = $attachmentService->getGenericErrorMessage(); + } else { + try { + $attachment = $attachmentService->getAttachment($id); + } catch (AttachmentException $attachmentException) { + $attachmentErrors[] = $attachmentService->getAttachmentErrorMessage($attachmentException); + } + } + + if ($attachment && $attachment->getRecordId() > 0 && $attachmentService->canDownloadAttachment($attachment)) { + $response = new StreamedResponse(static function () use ($attachment) { + $attachment->rawOut(); + }); + + $response->headers->set('Content-Type', $attachment->getMimeType()); + $response->headers->set('Content-Length', (string) $attachment->getFilesize()); + + if ($attachment->getMimeType() === 'application/pdf') { + $response->headers->set( + 'Content-Disposition', + 'inline; filename="' . rawurlencode($attachment->getFilename()) . '"', + ); + } else { + $response->headers->set( + 'Content-Disposition', + 'attachment; filename="' . rawurlencode($attachment->getFilename()) . '"', + ); + } + + $response->headers->set('Content-MD5', $attachment->getRealHash()); + $response->send(); + } else { + $attachmentErrors[] = $attachmentService->getGenericErrorMessage(); + } + + return $this->render('attachment.twig', [ + ...$this->getHeader($request), + 'attachmentErrors' => $attachmentErrors, + ]); + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index aba347f5a4..6aa09d56ca 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -18,7 +18,7 @@ declare(strict_types=1); use phpMyFAQ\Controller\Frontend\Api\SetupController; -use phpMyFAQ\Controller\Frontend\ContactController; +use phpMyFAQ\Controller\Frontend\AttachmentController;use phpMyFAQ\Controller\Frontend\ContactController; use phpMyFAQ\Controller\Frontend\GlossaryController;use phpMyFAQ\Controller\Frontend\LoginController;use phpMyFAQ\Controller\Frontend\OverviewController; use phpMyFAQ\Controller\Frontend\PageNotFoundController; use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; @@ -32,6 +32,11 @@ $routes = new RouteCollection(); $routesConfig = [ + 'public.attachment' => [ + 'path' => '/attachment/{attachmentId}', + 'controller' => [AttachmentController::class, 'index'], + 'methods' => 'GET', + ], 'public.contact' => [ 'path' => '/contact.html', 'controller' => [ContactController::class, 'index'], diff --git a/tests/phpMyFAQ/Attachment/AbstractAttachmentTest.php b/tests/phpMyFAQ/Attachment/AbstractAttachmentTest.php index 3ab5d9dd71..866fd8f7a7 100644 --- a/tests/phpMyFAQ/Attachment/AbstractAttachmentTest.php +++ b/tests/phpMyFAQ/Attachment/AbstractAttachmentTest.php @@ -173,7 +173,7 @@ public function testBuildUrl(): void $url = $this->attachment->buildUrl(); - $this->assertEquals('index.php?action=attachment&id=42', $url); + $this->assertEquals('./attachment/42', $url); } public function testSetAndGetId(): void From 68479b659fe474ac94fa9e8ca36577b5c7c2f829 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 04:02:00 +0000 Subject: [PATCH 028/286] build(deps): bump symfony/http-foundation from 8.0.1 to 8.0.3 Bumps [symfony/http-foundation](https://github.com/symfony/http-foundation) from 8.0.1 to 8.0.3. - [Release notes](https://github.com/symfony/http-foundation/releases) - [Changelog](https://github.com/symfony/http-foundation/blob/7.3/CHANGELOG.md) - [Commits](https://github.com/symfony/http-foundation/compare/v8.0.1...v8.0.3) --- updated-dependencies: - dependency-name: symfony/http-foundation dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 48630b70ac..3750a26939 100644 --- a/composer.lock +++ b/composer.lock @@ -3777,16 +3777,16 @@ }, { "name": "symfony/http-foundation", - "version": "v8.0.1", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "3690740e2e8b19d877f20d4f10b7a489cddf0fe2" + "reference": "514ec3aa7982f296b0ad0825f75b6be5779ae9e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3690740e2e8b19d877f20d4f10b7a489cddf0fe2", - "reference": "3690740e2e8b19d877f20d4f10b7a489cddf0fe2", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/514ec3aa7982f296b0ad0825f75b6be5779ae9e7", + "reference": "514ec3aa7982f296b0ad0825f75b6be5779ae9e7", "shasum": "" }, "require": { @@ -3833,7 +3833,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v8.0.1" + "source": "https://github.com/symfony/http-foundation/tree/v8.0.3" }, "funding": [ { @@ -3853,7 +3853,7 @@ "type": "tidelift" } ], - "time": "2025-12-07T11:23:24+00:00" + "time": "2025-12-23T14:52:06+00:00" }, { "name": "symfony/http-kernel", From ae551cf3a728571ec7d0b1429bb9886e92f04743 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 04:05:31 +0000 Subject: [PATCH 029/286] build(deps-dev): bump @commitlint/config-conventional Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 20.2.0 to 20.3.0. - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.0/@commitlint/config-conventional) --- updated-dependencies: - dependency-name: "@commitlint/config-conventional" dependency-version: 20.3.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4cfe3f3b67..785c835e22 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", "@commitlint/cli": "^20.2.0", - "@commitlint/config-conventional": "^20.2.0", + "@commitlint/config-conventional": "^20.3.0", "@eslint/js": "^9.39.2", "@types/bootstrap": "^5.2.10", "@types/highlightjs": "^9.12.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc6a77aaec..fc59c97a09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,8 +59,8 @@ devDependencies: specifier: ^20.2.0 version: 20.2.0(@types/node@24.10.4)(typescript@5.9.3) '@commitlint/config-conventional': - specifier: ^20.2.0 - version: 20.2.0 + specifier: ^20.3.0 + version: 20.3.0 '@eslint/js': specifier: ^9.39.2 version: 9.39.2 @@ -1249,8 +1249,8 @@ packages: - typescript dev: true - /@commitlint/config-conventional@20.2.0: - resolution: {integrity: sha512-MsRac+yNIbTB4Q/psstKK4/ciVzACHicSwz+04Sxve+4DW+PiJeTjU0JnS4m/oOnulrXYN+yBPlKaBSGemRfgQ==} + /@commitlint/config-conventional@20.3.0: + resolution: {integrity: sha512-g1OXVl6E2v0xF1Ru2RpxQ+Vfy7XUcUsCmLKzGUrhFLS4hSNykje0QSy6djBtzOiOBQCepBrmIlqx/gRlzrSh5A==} engines: {node: '>=v18'} dependencies: '@commitlint/types': 20.2.0 From 09e114cb51f80ccbe3b770f45df5b1c785ca6968 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:36:45 +0000 Subject: [PATCH 030/286] build(deps-dev): bump @commitlint/cli from 20.2.0 to 20.3.0 Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 20.2.0 to 20.3.0. - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.0/@commitlint/cli) --- updated-dependencies: - dependency-name: "@commitlint/cli" dependency-version: 20.3.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 785c835e22..2e7dc26bf2 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", - "@commitlint/cli": "^20.2.0", + "@commitlint/cli": "^20.3.0", "@commitlint/config-conventional": "^20.3.0", "@eslint/js": "^9.39.2", "@types/bootstrap": "^5.2.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc59c97a09..95c272fd96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,8 +56,8 @@ devDependencies: specifier: ^7.28.5 version: 7.28.5(@babel/core@7.28.5) '@commitlint/cli': - specifier: ^20.2.0 - version: 20.2.0(@types/node@24.10.4)(typescript@5.9.3) + specifier: ^20.3.0 + version: 20.3.0(@types/node@24.10.4)(typescript@5.9.3) '@commitlint/config-conventional': specifier: ^20.3.0 version: 20.3.0 @@ -1232,14 +1232,14 @@ packages: engines: {node: '>=18'} dev: true - /@commitlint/cli@20.2.0(@types/node@24.10.4)(typescript@5.9.3): - resolution: {integrity: sha512-l37HkrPZ2DZy26rKiTUvdq/LZtlMcxz+PeLv9dzK9NzoFGuJdOQyYU7IEkEQj0pO++uYue89wzOpZ0hcTtoqUA==} + /@commitlint/cli@20.3.0(@types/node@24.10.4)(typescript@5.9.3): + resolution: {integrity: sha512-HXO8YVfqdBK+MnlX2zqNrv6waGYPs6Ysjm5W2Y0GMagWXwiIKx7C8dcIX9ca+QdHq4WA0lcMnZLQ0pzQh1piZg==} engines: {node: '>=v18'} hasBin: true dependencies: '@commitlint/format': 20.2.0 - '@commitlint/lint': 20.2.0 - '@commitlint/load': 20.2.0(@types/node@24.10.4)(typescript@5.9.3) + '@commitlint/lint': 20.3.0 + '@commitlint/load': 20.3.0(@types/node@24.10.4)(typescript@5.9.3) '@commitlint/read': 20.2.0 '@commitlint/types': 20.2.0 tinyexec: 1.0.2 @@ -1298,18 +1298,18 @@ packages: semver: 7.7.3 dev: true - /@commitlint/lint@20.2.0: - resolution: {integrity: sha512-cQEEB+jlmyQbyiji/kmh8pUJSDeUmPiWq23kFV0EtW3eM+uAaMLMuoTMajbrtWYWQpPzOMDjYltQ8jxHeHgITg==} + /@commitlint/lint@20.3.0: + resolution: {integrity: sha512-X19HOGU5nRo6i9DIY0kG0mhgtvpn1UGO1D6aLX1ILLyeqSM5yJyMcrRqNj8SLgeSeUDODhLY9QYsBIG0LdNHkA==} engines: {node: '>=v18'} dependencies: '@commitlint/is-ignored': 20.2.0 '@commitlint/parse': 20.2.0 - '@commitlint/rules': 20.2.0 + '@commitlint/rules': 20.3.0 '@commitlint/types': 20.2.0 dev: true - /@commitlint/load@20.2.0(@types/node@24.10.4)(typescript@5.9.3): - resolution: {integrity: sha512-iAK2GaBM8sPFTSwtagI67HrLKHIUxQc2BgpgNc/UMNme6LfmtHpIxQoN1TbP+X1iz58jq32HL1GbrFTCzcMi6g==} + /@commitlint/load@20.3.0(@types/node@24.10.4)(typescript@5.9.3): + resolution: {integrity: sha512-amkdVZTXp5R65bsRXRSCwoNXbJHR2aAIY/RGFkoyd63t8UEwqEgT3f0MgeLqYw4hwXyq+TYXKdaW133E29pnGQ==} engines: {node: '>=v18'} dependencies: '@commitlint/config-validator': 20.2.0 @@ -1364,8 +1364,8 @@ packages: resolve-from: 5.0.0 dev: true - /@commitlint/rules@20.2.0: - resolution: {integrity: sha512-27rHGpeAjnYl/A+qUUiYDa7Yn1WIjof/dFJjYW4gA1Ug+LUGa1P0AexzGZ5NBxTbAlmDgaxSZkLLxtLVqtg8PQ==} + /@commitlint/rules@20.3.0: + resolution: {integrity: sha512-TGgXN/qBEhbzVD13crE1l7YSMJRrbPbUL0OBZALbUM5ER36RZmiZRu2ud2W/AA7HO9YLBRbyx6YVi2t/2Be0yQ==} engines: {node: '>=v18'} dependencies: '@commitlint/ensure': 20.2.0 From 4182bee11c852210f34bbfd5c849817ee81ccc10 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 09:14:21 +0100 Subject: [PATCH 031/286] refactor: migrated PDF export (#3834) --- phpmyfaq/pdf.php | 164 ------------------ .../Controller/Frontend/PdfController.php | 92 ++++++++++ phpmyfaq/src/phpMyFAQ/Services.php | 4 +- phpmyfaq/src/public-routes.php | 7 +- tests/phpMyFAQ/ServicesTest.php | 4 +- 5 files changed, 102 insertions(+), 169 deletions(-) delete mode 100644 phpmyfaq/pdf.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/PdfController.php diff --git a/phpmyfaq/pdf.php b/phpmyfaq/pdf.php deleted file mode 100644 index 469ca164c8..0000000000 --- a/phpmyfaq/pdf.php +++ /dev/null @@ -1,164 +0,0 @@ - - * @author Peter Beauvain - * @author Olivier Plathey - * @author Krzysztof Kruszynski - * @author Matteo Scaramuccia - * @copyright 2003-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2003-02-12 - */ - -use phpMyFAQ\Attachment\AttachmentException; -use phpMyFAQ\Attachment\AttachmentFactory; -use phpMyFAQ\Category; -use phpMyFAQ\Enums\PermissionType; -use phpMyFAQ\Export\Pdf; -use phpMyFAQ\Filter; -use phpMyFAQ\Helper\AttachmentHelper; -use phpMyFAQ\Language; -use phpMyFAQ\Strings; -use phpMyFAQ\Tags; -use phpMyFAQ\Translation; -use phpMyFAQ\User\CurrentUser; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -const IS_VALID_PHPMYFAQ = null; - -// -// Bootstrapping -// -require __DIR__ . '/src/Bootstrap.php'; - -// -// Service Containers -// -$container = new ContainerBuilder(); -$loader = new PhpFileLoader($container, new FileLocator(__DIR__)); -try { - $loader->load('src/services.php'); -} catch (\Exception $exception) { - echo $exception->getMessage(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); - -// get language (default: English) -$Language = $container->get('phpmyfaq.language'); -$faqLangCode = $faqConfig->get('main.languageDetection') - ? $Language->setLanguageWithDetection($faqConfig->get('main.language')) - : $Language->setLanguageFromConfiguration($faqConfig->get('main.language')); -$faqConfig->setLanguage($Language); - -// Found an article language? -$lang = Filter::filterInput(INPUT_POST, 'artlang', FILTER_SANITIZE_SPECIAL_CHARS); -if (is_null($lang) && !Language::isASupportedLanguage($lang)) { - $lang = Filter::filterInput(INPUT_GET, 'artlang', FILTER_SANITIZE_SPECIAL_CHARS); - if (is_null($lang) && !Language::isASupportedLanguage($lang)) { - $lang = $faqLangCode; - } -} - -if (isset($lang) && Language::isASupportedLanguage($lang)) { - require_once 'translations/language_' . strtolower((string) $lang) . '.php'; -} else { - $lang = 'en'; - require_once __DIR__ . '/translations/language_en.php'; -} - -// -// Set the translation class -// -try { - Translation::create() - ->setTranslationsDir(PMF_TRANSLATION_DIR) - ->setDefaultLanguage('en') - ->setCurrentLanguage($faqLangCode); -} catch (Exception $exception) { - echo 'Error: ' . $exception->getMessage(); -} - -// -// Initializing the static string wrapper -// -Strings::init($faqLangCode); - -// authenticate with session information -$user = $container->get('phpmyfaq.user.current_user'); - -// Get current user and group id - default: -1 -[$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($user); - -$request = Request::createFromGlobals(); -$currentCategory = Filter::filterVar($request->query->get('cat'), FILTER_VALIDATE_INT); -$faqId = Filter::filterVar($request->query->get('id'), FILTER_VALIDATE_INT); -$getAll = Filter::filterVar($request->query->get('getAll'), FILTER_VALIDATE_BOOLEAN, false); - -$faq = $container->get('phpmyfaq.faq'); -$faq->setUser($currentUser); -$faq->setGroups($currentGroups); - -$category = new Category($faqConfig, $currentGroups, true); -$category->setUser($currentUser); - -try { - $pdf = new Pdf($faq, $category, $faqConfig); -} catch (Exception) { - // handle exception -} - -$response = new Response(); - -if (true === $getAll) { - $category->buildCategoryTree(); -} - -$tags = new Tags($faqConfig); -$tags->setUser($currentUser)->setGroups($currentGroups); - -$response->setExpires(new DateTime()); - -if (true === $getAll && $user->perm->hasPermission($user->getUserId(), PermissionType::EXPORT)) { - $filename = 'FAQs.pdf'; - $pdfFile = $pdf->generate(0, true, $lang); -} else { - if (is_null($currentCategory) || is_null($faqId)) { - $response->isRedirect($faqConfig->getDefaultUrl()); - $response->send(); - exit(); - } - - $faq->getFaq($faqId); - $faq->faqRecord['category_id'] = $currentCategory; - - if ($faqConfig->get('records.disableAttachments') && 'yes' === $faq->faqRecord['active']) { - try { - $attachmentHelper = new AttachmentHelper(); - $attList = AttachmentFactory::fetchByRecordId($faqConfig, $faqId); - $faq->faqRecord['attachmentList'] = $attachmentHelper->getAttachmentList($attList); - } catch (AttachmentException) { - // handle exception - } - } - - $filename = 'FAQ-' . $faqId . '-' . $lang . '.pdf'; - $pdfFile = $pdf->generateFile($faq->faqRecord, $filename); -} - -$response->headers->set('Content-Type', 'application/pdf'); -$response->setContent($pdfFile); -$response->send(); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PdfController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PdfController.php new file mode 100644 index 0000000000..15afb98639 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PdfController.php @@ -0,0 +1,92 @@ + + * @author Peter Beauvain + * @author Olivier Plathey + * @author Krzysztof Kruszynski + * @author Matteo Scaramuccia + * @copyright 2003-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2003-02-12 + */ + +namespace phpMyFAQ\Controller\Frontend; + +use DateTime; +use League\CommonMark\Exception\CommonMarkException; +use phpMyFAQ\Attachment\AttachmentException; +use phpMyFAQ\Attachment\AttachmentFactory; +use phpMyFAQ\Category; +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Export\Pdf; +use phpMyFAQ\Filter; +use phpMyFAQ\Helper\AttachmentHelper; +use phpMyFAQ\User\CurrentUser; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class PdfController extends AbstractFrontController +{ + /** + * @throws Exception|\Exception|CommonMarkException + */ + #[Route(path: '/pdf/{categoryId}/{faqId}/{faqLanguage}', name: 'public.pdf.faq')] + public function index(Request $request): Response + { + $categoryId = Filter::filterVar($request->attributes->get('categoryId'), FILTER_VALIDATE_INT); + $faqId = Filter::filterVar($request->attributes->get('faqId'), FILTER_VALIDATE_INT); + $faqLanguage = Filter::filterVar($request->attributes->get('faqLanguage'), FILTER_SANITIZE_SPECIAL_CHARS); + + if ($categoryId === false || $categoryId === null || $faqId === false || $faqId === null) { + return new RedirectResponse($this->configuration->getDefaultUrl()); + } + + [$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser); + + $faq = $this->container->get('phpmyfaq.faq'); + $faq->setUser($currentUser); + $faq->setGroups($currentGroups); + + $category = new Category($this->configuration, $currentGroups, true); + $category->setUser($currentUser); + + $tags = $this->container->get('phpmyfaq.tags'); + $tags->setUser($currentUser)->setGroups($currentGroups); + + $pdf = new Pdf($faq, $category, $this->configuration); + + $faq->getFaq($faqId); + $faq->faqRecord['category_id'] = $categoryId; + + if (!$this->configuration->get('records.disableAttachments') && 'yes' === $faq->faqRecord['active']) { + try { + $attachmentHelper = new AttachmentHelper(); + $attList = AttachmentFactory::fetchByRecordId($this->configuration, $faqId); + $faq->faqRecord['attachmentList'] = $attachmentHelper->getAttachmentList($attList); + } catch (AttachmentException) { + $faq->faqRecord['attachmentList'] = ''; + } + } + + $filename = 'FAQ-' . $faqId . '-' . $faqLanguage . '.pdf'; + $pdfFile = $pdf->generateFile($faq->faqRecord, $filename); + + $response = new Response(); + $response->setExpires(new DateTime()); + $response->headers->set('Content-Type', 'application/pdf'); + $response->setContent($pdfFile); + + return $response; + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Services.php b/phpmyfaq/src/phpMyFAQ/Services.php index 2231cece83..a67523f270 100644 --- a/phpmyfaq/src/phpMyFAQ/Services.php +++ b/phpmyfaq/src/phpMyFAQ/Services.php @@ -100,7 +100,7 @@ public function setQuestion(string $question): void public function getPdfLink(): string { return sprintf( - '%spdf.php?cat=%d&id=%d&artlang=%s', + '%spdf/%d/%d/%s', $this->configuration->getDefaultUrl(), $this->getCategoryId(), $this->getFaqId(), @@ -114,7 +114,7 @@ public function getPdfLink(): string public function getPdfApiLink(): string { return sprintf( - '%spdf.php?cat=%d&id=%d&artlang=%s', + '%spdf/%d/%d/%s', $this->configuration->getDefaultUrl(), $this->getCategoryId(), $this->getFaqId(), diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 6aa09d56ca..1b6f80f202 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -21,7 +21,7 @@ use phpMyFAQ\Controller\Frontend\AttachmentController;use phpMyFAQ\Controller\Frontend\ContactController; use phpMyFAQ\Controller\Frontend\GlossaryController;use phpMyFAQ\Controller\Frontend\LoginController;use phpMyFAQ\Controller\Frontend\OverviewController; use phpMyFAQ\Controller\Frontend\PageNotFoundController; -use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; +use phpMyFAQ\Controller\Frontend\PdfController;use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; use phpMyFAQ\Controller\Frontend\WebAuthnController; use phpMyFAQ\Controller\LlmsController; use phpMyFAQ\Controller\RobotsController; @@ -62,6 +62,11 @@ 'controller' => [OverviewController::class, 'index'], 'methods' => 'GET', ], + 'public.pdf.faq' => [ + 'path' => '/pdf/{categoryId}/{faqId}/{faqLanguage}', + 'controller' => [PdfController::class, 'index'], + 'methods' => 'GET', + ], 'public.sitemap' => [ 'path' => '/sitemap/{letter}/{language}.html', 'controller' => [FrontendSitemapController::class, 'index'], diff --git a/tests/phpMyFAQ/ServicesTest.php b/tests/phpMyFAQ/ServicesTest.php index 5602bd9ce8..5a24ff3c26 100644 --- a/tests/phpMyFAQ/ServicesTest.php +++ b/tests/phpMyFAQ/ServicesTest.php @@ -34,7 +34,7 @@ public function testGetPdfLink(): void $services->setLanguage('en'); // Test getPdfLink method - $expected = 'http://example.com/pdf.php?cat=1&id=123&artlang=en'; + $expected = 'http://example.com/pdf/1/123/en'; $this->assertEquals($expected, $services->getPdfLink()); } @@ -51,7 +51,7 @@ public function testGetPdfApiLink(): void $services->setLanguage('en'); // Test getPdfApiLink method - $expected = 'http://example.com/pdf.php?cat=1&id=123&artlang=en'; + $expected = 'http://example.com/pdf/1/123/en'; $this->assertEquals($expected, $services->getPdfApiLink()); } } From c2b31b05871344dc912e072f1137aec3faaa96b5 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 09:32:05 +0100 Subject: [PATCH 032/286] refactor: migrated privacy redirect (#3834) --- phpmyfaq/assets/templates/default/index.twig | 2 +- phpmyfaq/index.php | 1 - phpmyfaq/privacy.php | 32 --------------- .../Frontend/AbstractFrontController.php | 1 - .../Controller/Frontend/PrivacyController.php | 41 +++++++++++++++++++ phpmyfaq/src/public-routes.php | 16 ++++++-- 6 files changed, 55 insertions(+), 38 deletions(-) delete mode 100644 phpmyfaq/privacy.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/PrivacyController.php diff --git a/phpmyfaq/assets/templates/default/index.twig b/phpmyfaq/assets/templates/default/index.twig index 4b2f3980c1..b4984a178a 100644 --- a/phpmyfaq/assets/templates/default/index.twig +++ b/phpmyfaq/assets/templates/default/index.twig @@ -173,7 +173,7 @@ {% if not isMaintenanceMode %} {% if isPrivacyLinkEnabled %} diff --git a/phpmyfaq/index.php b/phpmyfaq/index.php index 5fc4cda70e..80eb372f9c 100755 --- a/phpmyfaq/index.php +++ b/phpmyfaq/index.php @@ -639,7 +639,6 @@ 'isOpenQuestionsEnabled' => $faqConfig->get('main.enableAskQuestions'), 'footerNavigation' => $footerNavigation, 'isPrivacyLinkEnabled' => $faqConfig->get('layout.enablePrivacyLink'), - 'urlPrivacyLink' => $faqConfig->get('main.privacyURL'), 'msgPrivacyNote' => Translation::get(key: 'msgPrivacyNote'), 'isCookieConsentEnabled' => $faqConfig->get('layout.enableCookieConsent'), 'cookiePreferences' => Translation::get(key: 'cookiePreferences'), diff --git a/phpmyfaq/privacy.php b/phpmyfaq/privacy.php deleted file mode 100644 index d80903ebc7..0000000000 --- a/phpmyfaq/privacy.php +++ /dev/null @@ -1,32 +0,0 @@ - - * @copyright 2023-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2023-01-22 - */ - -use Symfony\Component\HttpFoundation\RedirectResponse; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); - -$privacyUrl = $faqConfig->get('main.privacyURL'); -$redirectUrl = strlen((string) $privacyUrl) > 0 ? $privacyUrl : $faqConfig->get('main.referenceURL'); - -$response = new RedirectResponse($redirectUrl); -$response->send(); -exit(); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php index 3157c10d24..a159a1720a 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php @@ -101,7 +101,6 @@ protected function getHeader(Request $request): array 'isOpenQuestionsEnabled' => $this->configuration->get('main.enableAskQuestions'), 'footerNavigation' => $this->getFooterNavigation($request), 'isPrivacyLinkEnabled' => $this->configuration->get('layout.enablePrivacyLink'), - 'urlPrivacyLink' => $this->configuration->get('main.privacyURL'), 'msgPrivacyNote' => Translation::get(key: 'msgPrivacyNote'), 'isCookieConsentEnabled' => $this->configuration->get('layout.enableCookieConsent'), 'cookiePreferences' => Translation::get(key: 'cookiePreferences'), diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PrivacyController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PrivacyController.php new file mode 100644 index 0000000000..c80404f433 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PrivacyController.php @@ -0,0 +1,41 @@ + + * @copyright 2023-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2023-01-22 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller\Frontend; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class PrivacyController extends AbstractFrontController +{ + /** + * Redirects to the privacy page stored in the configuration. + * @throws \Exception + */ + #[Route(path: '/privacy.html', name: 'public.privacy')] + public function index(Request $request): Response + { + $privacyUrl = $this->configuration->get('main.privacyURL'); + $redirectUrl = strlen((string) $privacyUrl) > 0 ? $privacyUrl : $this->configuration->get('main.referenceURL'); + + return new RedirectResponse($redirectUrl); + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 1b6f80f202..89e749602e 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -18,10 +18,15 @@ declare(strict_types=1); use phpMyFAQ\Controller\Frontend\Api\SetupController; -use phpMyFAQ\Controller\Frontend\AttachmentController;use phpMyFAQ\Controller\Frontend\ContactController; -use phpMyFAQ\Controller\Frontend\GlossaryController;use phpMyFAQ\Controller\Frontend\LoginController;use phpMyFAQ\Controller\Frontend\OverviewController; +use phpMyFAQ\Controller\Frontend\AttachmentController; +use phpMyFAQ\Controller\Frontend\ContactController; +use phpMyFAQ\Controller\Frontend\GlossaryController; +use phpMyFAQ\Controller\Frontend\LoginController; +use phpMyFAQ\Controller\Frontend\OverviewController; use phpMyFAQ\Controller\Frontend\PageNotFoundController; -use phpMyFAQ\Controller\Frontend\PdfController;use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; +use phpMyFAQ\Controller\Frontend\PdfController; +use phpMyFAQ\Controller\Frontend\PrivacyController; +use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; use phpMyFAQ\Controller\Frontend\WebAuthnController; use phpMyFAQ\Controller\LlmsController; use phpMyFAQ\Controller\RobotsController; @@ -67,6 +72,11 @@ 'controller' => [PdfController::class, 'index'], 'methods' => 'GET', ], + 'public.privacy' => [ + 'path' => '/privacy.html', + 'controller' => [PrivacyController::class, 'index'], + 'methods' => 'GET', + ], 'public.sitemap' => [ 'path' => '/sitemap/{letter}/{language}.html', 'controller' => [FrontendSitemapController::class, 'index'], From 814e2183678f427667d2aea37ab771ebe027a357 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 09:44:33 +0100 Subject: [PATCH 033/286] refactor: migrated request removal page (#3834) --- phpmyfaq/request-removal.php | 47 --------------- .../Frontend/AbstractFrontController.php | 34 +++++++++++ .../Controller/Frontend/UserController.php | 57 +++++++++++++++++++ phpmyfaq/src/public-routes.php | 6 ++ 4 files changed, 97 insertions(+), 47 deletions(-) delete mode 100644 phpmyfaq/request-removal.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php diff --git a/phpmyfaq/request-removal.php b/phpmyfaq/request-removal.php deleted file mode 100644 index 5fabb39718..0000000000 --- a/phpmyfaq/request-removal.php +++ /dev/null @@ -1,47 +0,0 @@ - - * @copyright 2018-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2018-02-03 - */ - -use phpMyFAQ\Session\Token; -use phpMyFAQ\Twig\TwigWrapper; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); -$faqSession->userTracking('request_removal', 0); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./request-removal.twig'); - -$templateVars = [ - ... $templateVars, - 'privacyURL' => $faqConfig->get('main.privacyURL'), - 'csrf' => Token::getInstance($container->get('session'))->getTokenInput('request-removal'), - 'lang' => $Language->getLanguage(), - 'userId' => $user->getUserId(), - 'defaultContentMail' => ($user->getUserId() > 0) ? $user->getUserData('email') : '', - 'defaultContentName' => ($user->getUserId() > 0) ? $user->getUserData('display_name') : '', - 'defaultLoginName' => ($user->getUserId() > 0) ? $user->getLogin() : '', -]; - -return $templateVars; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php index a159a1720a..51522a7a65 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/AbstractFrontController.php @@ -24,6 +24,7 @@ use phpMyFAQ\Enums\PermissionType; use phpMyFAQ\Environment; use phpMyFAQ\Helper\LanguageHelper; +use phpMyFAQ\Session\Token; use phpMyFAQ\System; use phpMyFAQ\Translation; use phpMyFAQ\Twig\TwigWrapper; @@ -48,6 +49,7 @@ protected function getHeader(Request $request): array ); return [ + ...$this->getUserDropdown(), 'isMaintenanceMode' => $this->configuration->get('main.maintenanceMode'), 'isCompletelySecured' => $this->configuration->get('security.enableLoginOnly'), 'isDebugEnabled' => Environment::isDebugMode(), @@ -135,6 +137,38 @@ private function getTopNavigation(Request $request): array ]; } + private function getUserDropdown(): array + { + $templateVars = []; + if ($this->currentUser->isLoggedIn() && $this->currentUser->getUserId() > 0) { + $csrfLogoutToken = Token::getInstance($this->container->get('session'))->getTokenString('logout'); + + if ( + $this->currentUser->perm->hasPermission( + $this->currentUser->getUserId(), + PermissionType::VIEW_ADMIN_LINK->value, + ) + || $this->currentUser->isSuperAdmin() + ) { + $templateVars = [ + ...$templateVars, + 'msgAdmin' => Translation::get(key: 'adminSection'), + ]; + } + + $templateVars = [ + ...$templateVars, + 'msgUserControlDropDown' => Translation::get(key: 'headerUserControlPanel'), + 'msgBookmarks' => Translation::get(key: 'msgBookmarks'), + 'msgUserRemoval' => Translation::get(key: 'ad_menu_RequestRemove'), + 'msgLogoutUser' => Translation::get(key: 'ad_menu_logout'), + 'csrfLogout' => $csrfLogoutToken, + ]; + } + + return $templateVars; + } + private function getFooterNavigation(Request $request): array { $action = $request->query->get(key: 'action', default: 'index'); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php new file mode 100644 index 0000000000..c442397643 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php @@ -0,0 +1,57 @@ + + * @author Jan Harms + * @copyright 2012-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2012-01-12 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller\Frontend; + +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Session\Token; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class UserController extends AbstractFrontController +{ + /** + * Displays the request removal page. + * + * @throws Exception + * @throws \Exception + */ + #[Route(path: '/user/request-removal', name: 'public.user.request-removal')] + public function requestRemoval(Request $request): Response + { + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('request_removal', 0); + + return $this->render('request-removal.twig', [ + ...$this->getHeader($request), + 'privacyURL' => $this->configuration->get('main.privacyURL'), + 'csrf' => Token::getInstance($this->container->get('session'))->getTokenInput('request-removal'), + 'lang' => $this->configuration->getLanguage()->getLanguage(), + 'userId' => $this->currentUser->getUserId(), + 'defaultContentMail' => $this->currentUser->getUserId() > 0 ? $this->currentUser->getUserData('email') : '', + 'defaultContentName' => $this->currentUser->getUserId() > 0 + ? $this->currentUser->getUserData('display_name') + : '', + 'defaultLoginName' => $this->currentUser->getUserId() > 0 ? $this->currentUser->getLogin() : '', + ]); + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 89e749602e..c1e7f393a1 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -27,6 +27,7 @@ use phpMyFAQ\Controller\Frontend\PdfController; use phpMyFAQ\Controller\Frontend\PrivacyController; use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; +use phpMyFAQ\Controller\Frontend\UserController; use phpMyFAQ\Controller\Frontend\WebAuthnController; use phpMyFAQ\Controller\LlmsController; use phpMyFAQ\Controller\RobotsController; @@ -77,6 +78,11 @@ 'controller' => [PrivacyController::class, 'index'], 'methods' => 'GET', ], + 'public.user.request-removal' => [ + 'path' => '/user/request-removal', + 'controller' => [UserController::class, 'requestRemoval'], + 'methods' => 'GET', + ], 'public.sitemap' => [ 'path' => '/sitemap/{letter}/{language}.html', 'controller' => [FrontendSitemapController::class, 'index'], From cb48e5c9a1fdc002960320d10ef177c54bc38a74 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 09:50:47 +0100 Subject: [PATCH 034/286] refactor: migrated user bookmarks page (#3834) --- phpmyfaq/bookmarks.php | 55 ------------------- .../Controller/Frontend/UserController.php | 36 ++++++++++++ phpmyfaq/src/public-routes.php | 5 ++ 3 files changed, 41 insertions(+), 55 deletions(-) delete mode 100644 phpmyfaq/bookmarks.php diff --git a/phpmyfaq/bookmarks.php b/phpmyfaq/bookmarks.php deleted file mode 100644 index bbf28cf069..0000000000 --- a/phpmyfaq/bookmarks.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @author Jan Harms - * @copyright 2002-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2023-07-20 - */ - -use phpMyFAQ\Bookmark; -use phpMyFAQ\Session\Token; -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; -use Symfony\Component\HttpFoundation\RedirectResponse; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -if ($user->isLoggedIn()) { - $bookmark = new Bookmark($faqConfig, $user); - - $twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); - $twigTemplate = $twig->loadTemplate('./bookmarks.twig'); - - // Twig template variables - $templateVars = [ - ...$templateVars, - 'title' => sprintf('%s - %s', Translation::get(key: 'msgBookmarks'), $faqConfig->getTitle()), - 'bookmarksList' => $bookmark->getBookmarkList(), - 'csrfTokenDeleteBookmark' => Token::getInstance($container->get('session'))->getTokenString('delete-bookmark'), - 'csrfTokenDeleteAllBookmarks' => Token::getInstance($container->get('session'))->getTokenString( - 'delete-all-bookmarks', - ), - ]; - - return $templateVars; -} - -// Redirect to log in -$response = new RedirectResponse($faqConfig->getDefaultUrl()); -$response->send(); diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php index c442397643..9d11f093e4 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php @@ -20,8 +20,11 @@ namespace phpMyFAQ\Controller\Frontend; +use phpMyFAQ\Bookmark; use phpMyFAQ\Core\Exception; use phpMyFAQ\Session\Token; +use phpMyFAQ\Translation; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -37,6 +40,10 @@ final class UserController extends AbstractFrontController #[Route(path: '/user/request-removal', name: 'public.user.request-removal')] public function requestRemoval(Request $request): Response { + if (!$this->currentUser->isLoggedIn()) { + return new RedirectResponse($this->configuration->getDefaultUrl()); + } + $faqSession = $this->container->get('phpmyfaq.user.session'); $faqSession->setCurrentUser($this->currentUser); $faqSession->userTracking('request_removal', 0); @@ -54,4 +61,33 @@ public function requestRemoval(Request $request): Response 'defaultLoginName' => $this->currentUser->getUserId() > 0 ? $this->currentUser->getLogin() : '', ]); } + + /** + * Displays the user's bookmarks page. + * + * @throws Exception + * @throws \Exception + */ + #[Route(path: '/user/bookmarks', name: 'public.user.bookmarks')] + public function bookmarks(Request $request): Response + { + if (!$this->currentUser->isLoggedIn()) { + return new RedirectResponse($this->configuration->getDefaultUrl()); + } + + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('bookmarks', 0); + + $bookmark = new Bookmark($this->configuration, $this->currentUser); + $session = $this->container->get('session'); + + return $this->render('bookmarks.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'msgBookmarks'), $this->configuration->getTitle()), + 'bookmarksList' => $bookmark->getBookmarkList(), + 'csrfTokenDeleteBookmark' => Token::getInstance($session)->getTokenString('delete-bookmark'), + 'csrfTokenDeleteAllBookmarks' => Token::getInstance($session)->getTokenString('delete-all-bookmarks'), + ]); + } } diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index c1e7f393a1..62bfe9d30c 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -83,6 +83,11 @@ 'controller' => [UserController::class, 'requestRemoval'], 'methods' => 'GET', ], + 'public.user.bookmarks' => [ + 'path' => '/user/bookmarks', + 'controller' => [UserController::class, 'bookmarks'], + 'methods' => 'GET', + ], 'public.sitemap' => [ 'path' => '/sitemap/{letter}/{language}.html', 'controller' => [FrontendSitemapController::class, 'index'], From 028b0d5001b5abb72d1edc5a98a7b71dbe9df3a5 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 09:58:12 +0100 Subject: [PATCH 035/286] refactor: migrated user registration page (#3834) --- phpmyfaq/register.php | 63 ------------------- .../Controller/Frontend/UserController.php | 38 ++++++++++- phpmyfaq/src/public-routes.php | 5 ++ 3 files changed, 41 insertions(+), 65 deletions(-) delete mode 100644 phpmyfaq/register.php diff --git a/phpmyfaq/register.php b/phpmyfaq/register.php deleted file mode 100644 index caa1eced48..0000000000 --- a/phpmyfaq/register.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @author Elger Thiele - * @copyright 2008-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2008-01-25 - */ - -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Symfony\Component\HttpFoundation\Request; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$request = Request::createFromGlobals(); -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -if (!$faqConfig->get('security.enableRegistration')) { - $redirect = new RedirectResponse($faqConfig->getDefaultUrl()); - $redirect->send(); -} - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); -$faqSession->userTracking('registration', 0); - -$captcha = $container->get('phpmyfaq.captcha'); - -$captchaHelper = $container->get('phpmyfaq.captcha.helper.captcha_helper'); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./register.twig'); - -// Twig template variables -$templateVars = [ - ...$templateVars, - 'title' => sprintf('%s - %s', Translation::get(key: 'msgRegistration'), $faqConfig->getTitle()), - 'lang' => $faqLangCode, - 'isWebAuthnEnabled' => $faqConfig->get('security.enableWebAuthnSupport'), - 'captchaFieldset' => $captchaHelper->renderCaptcha( - $captcha, - 'register', - Translation::get(key: 'msgCaptcha'), - $user->isLoggedIn(), - ), -]; - -return $templateVars; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php index 9d11f093e4..a8153a63da 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php @@ -10,10 +10,10 @@ * @package phpMyFAQ * @author Thorsten Rinne * @author Jan Harms - * @copyright 2012-2026 phpMyFAQ Team + * @copyright 2008-2026 phpMyFAQ Team * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 * @link https://www.phpmyfaq.de - * @since 2012-01-12 + * @since 2008-01-25 */ declare(strict_types=1); @@ -90,4 +90,38 @@ public function bookmarks(Request $request): Response 'csrfTokenDeleteAllBookmarks' => Token::getInstance($session)->getTokenString('delete-all-bookmarks'), ]); } + + /** + * Displays the user registration page. + * + * @throws Exception + * @throws \Exception + */ + #[Route(path: '/user/register', name: 'public.user.register')] + public function register(Request $request): Response + { + if (!$this->configuration->get('security.enableRegistration')) { + return new RedirectResponse($this->configuration->getDefaultUrl()); + } + + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('registration', 0); + + $captcha = $this->container->get('phpmyfaq.captcha'); + $captchaHelper = $this->container->get('phpmyfaq.captcha.helper.captcha_helper'); + + return $this->render('register.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'msgRegistration'), $this->configuration->getTitle()), + 'lang' => $this->configuration->getLanguage()->getLanguage(), + 'isWebAuthnEnabled' => $this->configuration->get('security.enableWebAuthnSupport'), + 'captchaFieldset' => $captchaHelper->renderCaptcha( + $captcha, + 'register', + Translation::get(key: 'msgCaptcha'), + $this->currentUser->isLoggedIn(), + ), + ]); + } } diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 62bfe9d30c..5bc994ff4e 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -78,6 +78,11 @@ 'controller' => [PrivacyController::class, 'index'], 'methods' => 'GET', ], + 'public.user.register' => [ + 'path' => '/user/register', + 'controller' => [UserController::class, 'register'], + 'methods' => 'GET', + ], 'public.user.request-removal' => [ 'path' => '/user/request-removal', 'controller' => [UserController::class, 'requestRemoval'], From 4c9d8842d443f2ed42750ae32f7ef747b60d1a4a Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 10:10:29 +0100 Subject: [PATCH 036/286] refactor: migrated open questions page (#3834) --- phpmyfaq/open-questions.php | 61 ---------------- .../Frontend/OpenQuestionsController.php | 73 +++++++++++++++++++ phpmyfaq/src/public-routes.php | 6 ++ 3 files changed, 79 insertions(+), 61 deletions(-) delete mode 100644 phpmyfaq/open-questions.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/OpenQuestionsController.php diff --git a/phpmyfaq/open-questions.php b/phpmyfaq/open-questions.php deleted file mode 100644 index db51e25a6f..0000000000 --- a/phpmyfaq/open-questions.php +++ /dev/null @@ -1,61 +0,0 @@ - - * @copyright 2002-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2002-09-17 - */ - -use phpMyFAQ\Category; -use phpMyFAQ\Enums\PermissionType; -use phpMyFAQ\Helper\QuestionHelper; -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); -$faqSession->userTracking('open_questions', 0); - -$category = new Category($faqConfig); -$questionHelper = new QuestionHelper(); -$questionHelper->setConfiguration($faqConfig)->setCategory($category); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./open-questions.twig'); - -$templateVars = [ - ...$templateVars, - 'title' => sprintf('%s - %s', Translation::get(key: 'msgOpenQuestions'), $faqConfig->getTitle()), - 'metaDescription' => sprintf(Translation::get(key: 'msgOpenQuestionsMetaDesc'), $faqConfig->getTitle()), - 'pageHeader' => Translation::get(key: 'msgOpenQuestions'), - 'msgQuestionText' => Translation::get(key: 'msgQuestionText'), - 'msgDate_User' => Translation::get(key: 'msgDate_User'), - 'msgQuestion2' => Translation::get(key: 'msgQuestion2'), - 'openQuestions' => $questionHelper->getOpenQuestions(), - 'isCloseQuestionEnabled' => $faqConfig->get('records.enableCloseQuestion'), - 'userHasPermissionToAnswer' => $user->perm->hasPermission($user->getUserId(), PermissionType::FAQ_ADD->value), - 'msgQuestionsWaiting' => Translation::get(key: 'msgQuestionsWaiting'), - 'msgNoQuestionsAvailable' => Translation::get(key: 'msgNoQuestionsAvailable'), - 'msg2answerFAQ' => Translation::get(key: 'msg2answerFAQ'), - 'msg2answer' => Translation::get(key: 'msg2answer'), -]; - -return $templateVars; - diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OpenQuestionsController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OpenQuestionsController.php new file mode 100644 index 0000000000..d3e4285c99 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/OpenQuestionsController.php @@ -0,0 +1,73 @@ + + * @copyright 2002-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2002-09-17 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller\Frontend; + +use phpMyFAQ\Category; +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Enums\PermissionType; +use phpMyFAQ\Helper\QuestionHelper; +use phpMyFAQ\Translation; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class OpenQuestionsController extends AbstractFrontController +{ + /** + * Displays the open questions page. + * + * @throws Exception + * @throws \Exception + */ + #[Route(path: '/open-questions.html', name: 'public.open-questions')] + public function index(Request $request): Response + { + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('open_questions', 0); + + $category = new Category($this->configuration); + $questionHelper = new QuestionHelper(); + $questionHelper->setConfiguration($this->configuration)->setCategory($category); + + return $this->render('open-questions.twig', [ + ...$this->getHeader($request), + 'title' => sprintf('%s - %s', Translation::get(key: 'msgOpenQuestions'), $this->configuration->getTitle()), + 'metaDescription' => sprintf( + Translation::get(key: 'msgOpenQuestionsMetaDesc'), + $this->configuration->getTitle(), + ), + 'pageHeader' => Translation::get(key: 'msgOpenQuestions'), + 'msgQuestionText' => Translation::get(key: 'msgQuestionText'), + 'msgDate_User' => Translation::get(key: 'msgDate_User'), + 'msgQuestion2' => Translation::get(key: 'msgQuestion2'), + 'openQuestions' => $questionHelper->getOpenQuestions(), + 'isCloseQuestionEnabled' => $this->configuration->get('records.enableCloseQuestion'), + 'userHasPermissionToAnswer' => $this->currentUser->perm->hasPermission( + $this->currentUser->getUserId(), + PermissionType::FAQ_ADD->value, + ), + 'msgQuestionsWaiting' => Translation::get(key: 'msgQuestionsWaiting'), + 'msgNoQuestionsAvailable' => Translation::get(key: 'msgNoQuestionsAvailable'), + 'msg2answerFAQ' => Translation::get(key: 'msg2answerFAQ'), + 'msg2answer' => Translation::get(key: 'msg2answer'), + ]); + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 5bc994ff4e..71a8e69bb6 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -22,6 +22,7 @@ use phpMyFAQ\Controller\Frontend\ContactController; use phpMyFAQ\Controller\Frontend\GlossaryController; use phpMyFAQ\Controller\Frontend\LoginController; +use phpMyFAQ\Controller\Frontend\OpenQuestionsController; use phpMyFAQ\Controller\Frontend\OverviewController; use phpMyFAQ\Controller\Frontend\PageNotFoundController; use phpMyFAQ\Controller\Frontend\PdfController; @@ -63,6 +64,11 @@ 'controller' => [LoginController::class, 'index'], 'methods' => 'GET|POST', ], + 'public.open-questions' => [ + 'path' => '/open-questions.html', + 'controller' => [OpenQuestionsController::class, 'index'], + 'methods' => 'GET', + ], 'public.overview' => [ 'path' => '/overview.html', 'controller' => [OverviewController::class, 'index'], From 732925eac5b379fb361d95991945897a4d84c513 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 10:18:28 +0100 Subject: [PATCH 037/286] refactor: migrated user control panel page (#3834) --- .../Controller/Frontend/UserController.php | 93 ++++++++++++++ phpmyfaq/src/public-routes.php | 5 + phpmyfaq/ucp.php | 119 ------------------ 3 files changed, 98 insertions(+), 119 deletions(-) delete mode 100644 phpmyfaq/ucp.php diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php index a8153a63da..934ed44da7 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/UserController.php @@ -24,6 +24,8 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Session\Token; use phpMyFAQ\Translation; +use phpMyFAQ\User\TwoFactor; +use RobThree\Auth\TwoFactorAuthException; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -124,4 +126,95 @@ public function register(Request $request): Response ), ]); } + + /** + * Displays the User Control Panel. + * + * @throws Exception + * @throws \Exception + */ + #[Route(path: '/user/ucp', name: 'public.user.ucp')] + public function ucp(Request $request): Response + { + if (!$this->currentUser->isLoggedIn()) { + return new RedirectResponse($this->configuration->getDefaultUrl()); + } + + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('user_control_panel', $this->currentUser->getUserId()); + + if ($this->configuration->get('main.enableGravatarSupport')) { + $gravatar = $this->container->get('phpmyfaq.services.gravatar'); + $gravatarImg = sprintf('%s', $gravatar->getImage( + $this->currentUser->getUserData('email'), + ['class' => 'img-responsive rounded-circle', 'size' => 125], + )); + } else { + $gravatarImg = ''; + } + + $qrCode = ''; + $secret = ''; + try { + $twoFactor = new TwoFactor($this->configuration, $this->currentUser); + $secret = $twoFactor->getSecret($this->currentUser); + if ('' === $secret || is_null($secret)) { + try { + $secret = $twoFactor->generateSecret(); + } catch (TwoFactorAuthException $exception) { + $this->configuration->getLogger()->error('Cannot generate 2FA secret: ' . $exception->getMessage()); + } + + $twoFactor->saveSecret($secret); + } + + $qrCode = $twoFactor->getQrCode($secret); + } catch (TwoFactorAuthException|\Exception $exception) { + $this->configuration->getLogger()->error('2FA error: ' . $exception->getMessage()); + } + + $session = $this->container->get('session'); + + return $this->render('ucp.twig', [ + ...$this->getHeader($request), + 'headerUserControlPanel' => Translation::get(key: 'headerUserControlPanel'), + 'ucpGravatarImage' => $gravatarImg, + 'msgHeaderUserData' => Translation::get(key: 'headerUserControlPanel'), + 'userid' => $this->currentUser->getUserId(), + 'csrf' => Token::getInstance($session)->getTokenInput('ucp'), + 'lang' => $this->configuration->getLanguage()->getLanguage(), + 'readonly' => $this->currentUser->isLocalUser() ? '' : 'readonly disabled', + 'msgRealName' => Translation::get(key: 'ad_user_name'), + 'realname' => $this->currentUser->getUserData('display_name'), + 'msgEmail' => Translation::get(key: 'msgNewContentMail'), + 'email' => $this->currentUser->getUserData('email'), + 'msgIsVisible' => Translation::get(key: 'msgUserDataVisible'), + 'checked' => (int) $this->currentUser->getUserData('is_visible') === 1 ? 'checked' : '', + 'msgPassword' => Translation::get(key: 'ad_auth_passwd'), + 'msgConfirm' => Translation::get(key: 'ad_user_confirm'), + 'msgSave' => Translation::get(key: 'msgSave'), + 'msgCancel' => Translation::get(key: 'msgCancel'), + 'twofactor_enabled' => (bool) $this->currentUser->getUserData('twofactor_enabled'), + 'msgTwofactorEnabled' => Translation::get(key: 'msgTwofactorEnabled'), + 'msgTwofactorConfig' => Translation::get(key: 'msgTwofactorConfig'), + 'msgTwofactorConfigModelTitle' => Translation::get(key: 'msgTwofactorConfigModelTitle'), + 'twofactor_secret' => $secret, + 'qr_code_secret' => $qrCode, + 'qr_code_secret_alt' => Translation::get(key: 'qr_code_secret_alt'), + 'msgTwofactorNewSecret' => Translation::get(key: 'msgTwofactorNewSecret'), + 'msgWarning' => Translation::get(key: 'msgWarning'), + 'ad_gen_yes' => Translation::get(key: 'ad_gen_yes'), + 'ad_gen_no' => Translation::get(key: 'ad_gen_no'), + 'msgConfirmTwofactorConfig' => Translation::get(key: 'msgConfirmTwofactorConfig'), + 'csrfTokenRemoveTwofactor' => Token::getInstance($session)->getTokenString('remove-twofactor'), + 'msgGravatarNotConnected' => Translation::get(key: 'msgGravatarNotConnected'), + 'webauthnSupportEnabled' => $this->configuration->get('security.enableWebAuthnSupport'), + 'csrfExportUserData' => Token::getInstance($session)->getTokenInput('export-userdata'), + 'exportUserDataUrl' => 'api/user/data/export', + 'msgDownloadYourData' => Translation::get(key: 'msgDownloadYourData'), + 'msgDataExportDescription' => Translation::get(key: 'msgDataExportDescription'), + 'msgDownload' => Translation::get(key: 'msgDownload'), + ]); + } } diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 71a8e69bb6..0cfa2638e2 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -99,6 +99,11 @@ 'controller' => [UserController::class, 'bookmarks'], 'methods' => 'GET', ], + 'public.user.ucp' => [ + 'path' => '/user/ucp', + 'controller' => [UserController::class, 'ucp'], + 'methods' => 'GET', + ], 'public.sitemap' => [ 'path' => '/sitemap/{letter}/{language}.html', 'controller' => [FrontendSitemapController::class, 'index'], diff --git a/phpmyfaq/ucp.php b/phpmyfaq/ucp.php deleted file mode 100644 index e9c22a2bbd..0000000000 --- a/phpmyfaq/ucp.php +++ /dev/null @@ -1,119 +0,0 @@ - - * @copyright 2012-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2012-01-12 - */ - -use phpMyFAQ\Session\Token; -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; -use phpMyFAQ\User\TwoFactor; -use RobThree\Auth\TwoFactorAuthException; -use Symfony\Component\HttpFoundation\RedirectResponse; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -if ($user->isLoggedIn()) { - $faqSession = $container->get('phpmyfaq.user.session'); - $faqSession->setCurrentUser($user); - $faqSession->userTracking('user_control_panel', $user->getUserId()); - - if ($faqConfig->get('main.enableGravatarSupport')) { - $gravatar = $container->get('phpmyfaq.services.gravatar'); - $gravatarImg = sprintf('%s', $gravatar->getImage( - $user->getUserData('email'), - ['class' => 'img-responsive rounded-circle', 'size' => 125], - )); - } else { - $gravatarImg = ''; - } - - $qrCode = ''; - try { - $twoFactor = new TwoFactor($faqConfig, $user); - $secret = $twoFactor->getSecret($user); - if ('' === $secret || is_null($secret)) { - try { - $secret = $twoFactor->generateSecret(); - } catch (TwoFactorAuthException $e) { - $faqConfig->getLogger()->error('Cannot generate 2FA secret: ' . $e->getMessage()); - } - - $twoFactor->saveSecret($secret); - } - - $qrCode = $twoFactor->getQrCode($secret); - } catch (TwoFactorAuthException|Exception) { - // handle exception - } - - $twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); - $twigTemplate = $twig->loadTemplate('./ucp.twig'); - - // Twig template variables - $templateVars = [ - ...$templateVars, - 'headerUserControlPanel' => Translation::get(key: 'headerUserControlPanel'), - 'ucpGravatarImage' => $gravatarImg, - 'msgHeaderUserData' => Translation::get(key: 'headerUserControlPanel'), - 'userid' => $user->getUserId(), - 'csrf' => Token::getInstance($container->get('session'))->getTokenInput('ucp'), - 'lang' => $faqConfig->getLanguage()->getLanguage(), - 'readonly' => $user->isLocalUser() ? '' : 'readonly disabled', - 'msgRealName' => Translation::get(key: 'ad_user_name'), - 'realname' => $user->getUserData('display_name'), - 'msgEmail' => Translation::get(key: 'msgNewContentMail'), - 'email' => $user->getUserData('email'), - 'msgIsVisible' => Translation::get(key: 'msgUserDataVisible'), - 'checked' => (int) $user->getUserData('is_visible') === 1 ? 'checked' : '', - 'msgPassword' => Translation::get(key: 'ad_auth_passwd'), - 'msgConfirm' => Translation::get(key: 'ad_user_confirm'), - 'msgSave' => Translation::get(key: 'msgSave'), - 'msgCancel' => Translation::get(key: 'msgCancel'), - 'twofactor_enabled' => (bool) $user->getUserData('twofactor_enabled'), - 'msgTwofactorEnabled' => Translation::get(key: 'msgTwofactorEnabled'), - 'msgTwofactorConfig' => Translation::get(key: 'msgTwofactorConfig'), - 'msgTwofactorConfigModelTitle' => Translation::get(key: 'msgTwofactorConfigModelTitle'), - 'twofactor_secret' => $secret ?? '', - 'qr_code_secret' => $qrCode, - 'qr_code_secret_alt' => Translation::get(key: 'qr_code_secret_alt'), - 'msgTwofactorNewSecret' => Translation::get(key: 'msgTwofactorNewSecret'), - 'msgWarning' => Translation::get(key: 'msgWarning'), - 'ad_gen_yes' => Translation::get(key: 'ad_gen_yes'), - 'ad_gen_no' => Translation::get(key: 'ad_gen_no'), - 'msgConfirmTwofactorConfig' => Translation::get(key: 'msgConfirmTwofactorConfig'), - 'csrfTokenRemoveTwofactor' => Token::getInstance($container->get('session'))->getTokenString( - 'remove-twofactor', - ), - 'msgGravatarNotConnected' => Translation::get(key: 'msgGravatarNotConnected'), - 'webauthnSupportEnabled' => $faqConfig->get('security.enableWebAuthnSupport'), - 'csrfExportUserData' => Token::getInstance($container->get('session'))->getTokenInput('export-userdata'), - 'exportUserDataUrl' => 'api/user/data/export', - 'msgDownloadYourData' => Translation::get(key: 'msgDownloadYourData'), - 'msgDataExportDescription' => Translation::get(key: 'msgDataExportDescription'), - 'msgDownload' => Translation::get(key: 'msgDownload'), - ]; - - return $templateVars; -} - -// Redirect to log in -$response = new RedirectResponse($faqConfig->getDefaultUrl()); -$response->send(); From 9a0ccace462fc6dec752f52ac86308ae4f347eb2 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 12:16:04 +0100 Subject: [PATCH 038/286] refactor: migrated news page (#3834) --- phpmyfaq/news.php | 164 ------------------ .../Controller/Frontend/NewsController.php | 109 ++++++++++++ .../Controller/Frontend/PdfController.php | 6 +- phpmyfaq/src/phpMyFAQ/News/NewsService.php | 164 ++++++++++++++++++ phpmyfaq/src/public-routes.php | 6 + 5 files changed, 284 insertions(+), 165 deletions(-) delete mode 100644 phpmyfaq/news.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/NewsController.php create mode 100644 phpmyfaq/src/phpMyFAQ/News/NewsService.php diff --git a/phpmyfaq/news.php b/phpmyfaq/news.php deleted file mode 100644 index ae577c19e2..0000000000 --- a/phpmyfaq/news.php +++ /dev/null @@ -1,164 +0,0 @@ - - * @author Matteo Scaramuccia - * @copyright 2006-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2006-07-23 - */ - -use phpMyFAQ\Captcha\Helper\CaptchaHelper; -use phpMyFAQ\Comments; -use phpMyFAQ\Date; -use phpMyFAQ\Entity\CommentType; -use phpMyFAQ\Enums\PermissionType; -use phpMyFAQ\Filter; -use phpMyFAQ\Glossary; -use phpMyFAQ\Helper\CommentHelper; -use phpMyFAQ\Helper\FaqHelper; -use phpMyFAQ\News; -use phpMyFAQ\Session\Token; -use phpMyFAQ\Strings; -use phpMyFAQ\Translation; -use phpMyFAQ\Twig\TwigWrapper; -use Symfony\Component\HttpFoundation\Request; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); -$user = $container->get('phpmyfaq.user.current_user'); - -$faqSession = $container->get('phpmyfaq.user.session'); -$faqSession->setCurrentUser($user); - -$captcha = $container->get('phpmyfaq.captcha'); - -$comment = new Comments($faqConfig); - -$request = Request::createFromGlobals(); -$newsId = Filter::filterVar($request->query->get('newsid'), FILTER_VALIDATE_INT); - -$oNews = new News($faqConfig); - -$faqSession->userTracking('news_view', $newsId); - -// Define the header of the page -$newsMainHeader = $faqConfig->getTitle() . Translation::get(key: 'msgNews'); - -// Get all data from the news record -$news = $oNews->get($newsId); - -$newsContent = $news['content']; -$newsHeader = $news['header']; - -// Add Glossary entries -$oGlossary = new Glossary($faqConfig); -$newsContent = $oGlossary->insertItemsIntoContent($newsContent ?? ''); -$newsHeader = $oGlossary->insertItemsIntoContent($newsHeader ?? ''); - -$helper = new FaqHelper($faqConfig); -$newsContent = $helper->cleanUpContent($newsContent); - -// Add an information link if existing -if (strlen((string) $news['link']) > 0) { - $newsContent .= sprintf( - '

    %s%s', - Translation::get(key: 'msgInfo'), - Strings::htmlentities($news['link']), - $news['target'], - Strings::htmlentities($news['linkTitle']), - ); -} - -// Show a link to edit the news? -$editThisEntry = ''; -if ($user->perm->hasPermission($user->getUserId(), PermissionType::NEWS_EDIT->value)) { - $editThisEntry = sprintf( - '%s', - $newsId, - Translation::get(key: 'ad_menu_news_edit'), - ); -} - -// Does the user have the right to add a comment? -if ( - -1 === $user->getUserId() && !$faqConfig->get('records.allowCommentsForGuests') - || !$news['active'] - || !$news['allowComments'] -) { - $commentMessage = Translation::get(key: 'msgWriteNoComment'); -} else { - $commentMessage = sprintf( - '%s', - Translation::get(key: 'newsWriteComment'), - ); -} - -// date of news entry -if ($news['active']) { - $date = new Date($faqConfig); - $newsDate = sprintf( - '%s%s', - Translation::get(key: 'msgLastUpdateArticle'), - $date->format($news['date']), - ); -} else { - $newsDate = ''; -} - -$captchaHelper = CaptchaHelper::getInstance($faqConfig); - -$commentHelper = new CommentHelper(); -$commentHelper->setConfiguration($faqConfig); - -$comment = new Comments($faqConfig); -$comments = $comment->getCommentsData($newsId, CommentType::NEWS); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twigTemplate = $twig->loadTemplate('./news.twig'); - -$templateVars = [ - ...$templateVars, - 'writeNewsHeader' => $newsMainHeader, - 'newsHeader' => $newsHeader, - 'mainPageContent' => $newsContent, - 'writeDateMsg' => $newsDate, - 'msgAboutThisNews' => Translation::get(key: 'msgAboutThisNews'), - 'writeAuthor' => $news['active'] ? Translation::get(key: 'msgAuthor') . ': ' . $news['authorName'] : '', - 'editThisEntry' => $editThisEntry, - 'writeCommentMsg' => $commentMessage, - 'msgWriteComment' => Translation::get(key: 'newsWriteComment'), - 'newsId' => $newsId, - 'newsLang' => $news['lang'], - 'msgCommentHeader' => Translation::get(key: 'msgCommentHeader'), - 'msgNewContentName' => Translation::get(key: 'msgNewContentName'), - 'msgNewContentMail' => Translation::get(key: 'msgNewContentMail'), - 'defaultContentMail' => $user->getUserId() > 0 ? $user->getUserData('email') : '', - 'defaultContentName' => $user->getUserId() > 0 ? $user->getUserData('display_name') : '', - 'msgYourComment' => Translation::get(key: 'msgYourComment'), - 'csrfInput' => Token::getInstance($container->get('session'))->getTokenInput('add-comment'), - 'msgCancel' => Translation::get(key: 'ad_gen_cancel'), - 'msgNewContentSubmit' => Translation::get(key: 'msgNewContentSubmit'), - 'captchaFieldset' => $captchaHelper->renderCaptcha( - $captcha, - 'writecomment', - Translation::get(key: 'msgCaptcha'), - $user->isLoggedIn(), - ), - 'renderComments' => $commentHelper->getComments($comments), -]; - -return $templateVars; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/NewsController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/NewsController.php new file mode 100644 index 0000000000..825fa0f028 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/NewsController.php @@ -0,0 +1,109 @@ + + * @author Matteo Scaramuccia + * @copyright 2006-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2006-07-23 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller\Frontend; + +use phpMyFAQ\Captcha\Helper\CaptchaHelper; +use phpMyFAQ\Comments; +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Entity\CommentType; +use phpMyFAQ\Filter; +use phpMyFAQ\Helper\CommentHelper; +use phpMyFAQ\News\NewsService; +use phpMyFAQ\Session\Token; +use phpMyFAQ\Translation; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class NewsController extends AbstractFrontController +{ + /** + * Displays a news article with comments. + * + * @throws Exception + * @throws \Exception + */ + #[Route(path: '/news/{newsId}/{newsLang}/{slug}.html', name: 'public.news', requirements: [ + 'newsId' => '\d+', + 'newsLang' => '[a-z\-_]+', + ])] + public function index(Request $request): Response + { + $newsId = Filter::filterVar($request->attributes->get('newsId'), FILTER_VALIDATE_INT); + + if ($newsId === false || $newsId === null) { + return $this->render('404.twig', [ + ...$this->getHeader($request), + ]); + } + + $faqSession = $this->container->get('phpmyfaq.user.session'); + $faqSession->setCurrentUser($this->currentUser); + $faqSession->userTracking('news_view', $newsId); + + $newsService = new NewsService($this->configuration, $this->currentUser); + $news = $newsService->getProcessedNews($newsId); + + $captcha = $this->container->get('phpmyfaq.captcha'); + $captchaHelper = CaptchaHelper::getInstance($this->configuration); + + $commentHelper = new CommentHelper(); + $commentHelper->setConfiguration($this->configuration); + + $comment = new Comments($this->configuration); + $comments = $comment->getCommentsData($newsId, CommentType::NEWS); + + $session = $this->container->get('session'); + + return $this->render('news.twig', [ + ...$this->getHeader($request), + 'writeNewsHeader' => $this->configuration->getTitle() . Translation::get(key: 'msgNews'), + 'newsHeader' => $news['processedHeader'], + 'mainPageContent' => $news['processedContent'], + 'writeDateMsg' => $newsService->formatNewsDate($news), + 'msgAboutThisNews' => Translation::get(key: 'msgAboutThisNews'), + 'writeAuthor' => $newsService->getAuthorInfo($news), + 'editThisEntry' => $newsService->getEditLink($newsId), + 'writeCommentMsg' => $newsService->getCommentMessage($news), + 'msgWriteComment' => Translation::get(key: 'newsWriteComment'), + 'newsId' => $newsId, + 'newsLang' => $news['lang'], + 'msgCommentHeader' => Translation::get(key: 'msgCommentHeader'), + 'msgNewContentName' => Translation::get(key: 'msgNewContentName'), + 'msgNewContentMail' => Translation::get(key: 'msgNewContentMail'), + 'defaultContentMail' => $this->currentUser->getUserId() > 0 ? $this->currentUser->getUserData('email') : '', + 'defaultContentName' => $this->currentUser->getUserId() > 0 + ? $this->currentUser->getUserData('display_name') + : '', + 'msgYourComment' => Translation::get(key: 'msgYourComment'), + 'csrfInput' => Token::getInstance($session)->getTokenInput('add-comment'), + 'msgCancel' => Translation::get(key: 'ad_gen_cancel'), + 'msgNewContentSubmit' => Translation::get(key: 'msgNewContentSubmit'), + 'captchaFieldset' => $captchaHelper->renderCaptcha( + $captcha, + 'writecomment', + Translation::get(key: 'msgCaptcha'), + $this->currentUser->isLoggedIn(), + ), + 'renderComments' => $commentHelper->getComments($comments), + ]); + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PdfController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PdfController.php index 15afb98639..dae548cb3a 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PdfController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/PdfController.php @@ -41,7 +41,11 @@ final class PdfController extends AbstractFrontController /** * @throws Exception|\Exception|CommonMarkException */ - #[Route(path: '/pdf/{categoryId}/{faqId}/{faqLanguage}', name: 'public.pdf.faq')] + #[Route(path: '/pdf/{categoryId}/{faqId}/{faqLanguage}', name: 'public.pdf.faq', requirements: [ + 'categoryId' => '\d+', + 'faqId' => '\d+', + 'faqLanguage' => '[a-z]{2}(_[a-z]{2})?', + ])] public function index(Request $request): Response { $categoryId = Filter::filterVar($request->attributes->get('categoryId'), FILTER_VALIDATE_INT); diff --git a/phpmyfaq/src/phpMyFAQ/News/NewsService.php b/phpmyfaq/src/phpMyFAQ/News/NewsService.php new file mode 100644 index 0000000000..c835baca3d --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/News/NewsService.php @@ -0,0 +1,164 @@ + + * @copyright 2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2026-01-02 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\News; + +use phpMyFAQ\Configuration; +use phpMyFAQ\Date; +use phpMyFAQ\Enums\PermissionType; +use phpMyFAQ\Glossary; +use phpMyFAQ\Helper\FaqHelper; +use phpMyFAQ\News; +use phpMyFAQ\Strings; +use phpMyFAQ\Translation; +use phpMyFAQ\User\CurrentUser; + +/** + * Service class for news-related business logic. + */ +final class NewsService +{ + private News $news; + private Glossary $glossary; + private FaqHelper $faqHelper; + + public function __construct( + private readonly Configuration $configuration, + private readonly CurrentUser $currentUser, + ) { + $this->news = new News($this->configuration); + $this->glossary = new Glossary($this->configuration); + $this->faqHelper = new FaqHelper($this->configuration); + } + + /** + * Gets and processes news by ID. + * + * @return array + */ + public function getProcessedNews(int $newsId): array + { + $news = $this->news->get($newsId); + + // Process content with glossary + $news['processedContent'] = $this->processContent($news['content'] ?? ''); + $news['processedHeader'] = $this->processContent($news['header'] ?? ''); + + // Add an information link if available + if (strlen((string) $news['link']) > 0) { + $news['processedContent'] .= $this->buildInformationLink( + $news['link'], + $news['target'], + $news['linkTitle'], + ); + } + + return $news; + } + + /** + * Processes content with glossary and cleanup. + */ + private function processContent(string $content): string + { + $content = $this->glossary->insertItemsIntoContent($content); + return $this->faqHelper->cleanUpContent($content); + } + + /** + * Builds the information link HTML. + */ + private function buildInformationLink(string $link, string $target, string $linkTitle): string + { + return sprintf( + '

    %s%s', + Translation::get(key: 'msgInfo'), + Strings::htmlentities($link), + $target, + Strings::htmlentities($linkTitle), + ); + } + + /** + * Checks if a user can edit news and returns an edit link. + */ + public function getEditLink(int $newsId): string + { + if ($this->currentUser->perm->hasPermission( + $this->currentUser->getUserId(), + PermissionType::NEWS_EDIT->value, + )) { + return sprintf( + '%s', + $newsId, + Translation::get(key: 'ad_menu_news_edit'), + ); + } + + return ''; + } + + /** + * Gets a comment message based on permissions and news settings. + */ + public function getCommentMessage(array $news): string + { + if ( + -1 === $this->currentUser->getUserId() && !$this->configuration->get('records.allowCommentsForGuests') + || !$news['active'] + || !$news['allowComments'] + ) { + return Translation::get(key: 'msgWriteNoComment'); + } + + return sprintf( + '%s', + Translation::get(key: 'newsWriteComment'), + ); + } + + /** + * Formats the news date. + */ + public function formatNewsDate(array $news): string + { + if (!$news['active']) { + return ''; + } + + $date = new Date($this->configuration); + return sprintf( + '%s%s', + Translation::get(key: 'msgLastUpdateArticle'), + $date->format($news['date']), + ); + } + + /** + * Gets author information. + */ + public function getAuthorInfo(array $news): string + { + if (!$news['active']) { + return ''; + } + + return Translation::get(key: 'msgAuthor') . ': ' . $news['authorName']; + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 0cfa2638e2..705cee4fb5 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -22,6 +22,7 @@ use phpMyFAQ\Controller\Frontend\ContactController; use phpMyFAQ\Controller\Frontend\GlossaryController; use phpMyFAQ\Controller\Frontend\LoginController; +use phpMyFAQ\Controller\Frontend\NewsController; use phpMyFAQ\Controller\Frontend\OpenQuestionsController; use phpMyFAQ\Controller\Frontend\OverviewController; use phpMyFAQ\Controller\Frontend\PageNotFoundController; @@ -64,6 +65,11 @@ 'controller' => [LoginController::class, 'index'], 'methods' => 'GET|POST', ], + 'public.news' => [ + 'path' => '/news/{newsId}/{newsLang}/{slug}.html', + 'controller' => [NewsController::class, 'index'], + 'methods' => 'GET', + ], 'public.open-questions' => [ 'path' => '/open-questions.html', 'controller' => [OpenQuestionsController::class, 'index'], From 54babf5fd78b8f5c5b6aca4cf4a48e5cc6fc5f65 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 12:46:03 +0100 Subject: [PATCH 039/286] refactor: migrated start page (#3834) --- .../Frontend/StartpageController.php | 103 ++++++++++++++++++ phpmyfaq/src/public-routes.php | 6 + phpmyfaq/startpage.php | 95 ---------------- 3 files changed, 109 insertions(+), 95 deletions(-) create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/StartpageController.php delete mode 100755 phpmyfaq/startpage.php diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/StartpageController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/StartpageController.php new file mode 100644 index 0000000000..f09d4edd51 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/StartpageController.php @@ -0,0 +1,103 @@ + + * @copyright 2002-2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2002-08-23 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Controller\Frontend; + +use phpMyFAQ\Category\Startpage; +use phpMyFAQ\Core\Exception; +use phpMyFAQ\Faq\Statistics; +use phpMyFAQ\Filter; +use phpMyFAQ\Language\Plurals; +use phpMyFAQ\News; +use phpMyFAQ\Translation; +use phpMyFAQ\Twig\Extensions\TagNameTwigExtension; +use phpMyFAQ\User\CurrentUser; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Twig\Extension\AttributeExtension; + +final class StartpageController extends AbstractFrontController +{ + /** + * Displays the start page with categories, Top 10, and latest messages. + * + * @throws Exception + * @throws \Exception + */ + #[Route(path: '/', name: 'public.index')] + public function index(Request $request): Response + { + $news = new News($this->configuration); + $plr = new Plurals(); + $faqStatistics = new Statistics($this->configuration); + + [$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser); + + $startPageCategory = new Startpage($this->configuration); + $startPageCategory + ->setLanguage($this->configuration->getLanguage()->getLanguage()) + ->setUser($currentUser) + ->setGroups($currentGroups); + + $startPageCategories = $startPageCategory->getCategories(); + + // Get top ten parameter + $param = $this->configuration->get('records.orderingPopularFaqs') === 'visits' ? 'visits' : 'voted'; + + $faq = $this->container->get('phpmyfaq.faq'); + $faq->setUser($currentUser); + $faq->setGroups($currentGroups); + + $tags = $this->container->get('phpmyfaq.tags'); + $tags->setUser($currentUser)->setGroups($currentGroups); + + $faqSystem = $this->container->get('phpmyfaq.system'); + $categoryId = Filter::filterVar($request->query->get('cat'), FILTER_VALIDATE_INT, 0); + + $this->addExtension(new AttributeExtension(TagNameTwigExtension::class)); + return $this->render('startpage.twig', [ + ...$this->getHeader($request), + 'baseHref' => $faqSystem->getSystemUri($this->configuration), + 'title' => $this->configuration->getTitle(), + 'pageHeader' => $this->configuration->getTitle(), + 'startPageCategories' => (is_countable($startPageCategories) ? count($startPageCategories) : 0) > 0, + 'startPageCategoryDecks' => $startPageCategories, + 'stickyRecordsHeader' => Translation::get(key: 'stickyRecordsHeader'), + 'stickyRecordsList' => $faq->getStickyFaqsData(), + 'writeTopTenHeader' => Translation::get(key: 'msgTopTen'), + 'topRecordsList' => $faqStatistics->getTopTen($param), + 'errorMsgTopTen' => Translation::get(key: 'err_noTopTen'), + 'writeNewestHeader' => Translation::get(key: 'msgLatestArticles'), + 'latestRecordsList' => $faqStatistics->getLatest(), + 'errorMsgLatest' => Translation::get(key: 'msgErrorNoRecords'), + 'msgTrendingFAQs' => Translation::get(key: 'msgTrendingFAQs'), + 'trendingRecordsList' => $faqStatistics->getTrending(), + 'errorMsgTrendingFaqs' => Translation::get(key: 'msgErrorNoRecords'), + 'msgNewsHeader' => Translation::get(key: 'newsArchive'), + 'newsList' => $news->getAll(), + 'writeNumberOfArticles' => $plr->getMsg( + 'plmsgHomeArticlesOnline', + $faqStatistics->totalFaqs($this->configuration->getLanguage()->getLanguage()), + ), + 'msgTags' => Translation::get(key: 'msgPopularTags'), + 'tagsList' => $tags->getPopularTags(12), + ]); + } +} diff --git a/phpmyfaq/src/public-routes.php b/phpmyfaq/src/public-routes.php index 705cee4fb5..280cec7def 100644 --- a/phpmyfaq/src/public-routes.php +++ b/phpmyfaq/src/public-routes.php @@ -29,6 +29,7 @@ use phpMyFAQ\Controller\Frontend\PdfController; use phpMyFAQ\Controller\Frontend\PrivacyController; use phpMyFAQ\Controller\Frontend\SitemapController as FrontendSitemapController; +use phpMyFAQ\Controller\Frontend\StartpageController; use phpMyFAQ\Controller\Frontend\UserController; use phpMyFAQ\Controller\Frontend\WebAuthnController; use phpMyFAQ\Controller\LlmsController; @@ -90,6 +91,11 @@ 'controller' => [PrivacyController::class, 'index'], 'methods' => 'GET', ], + 'public.index' => [ + 'path' => '/', + 'controller' => [StartpageController::class, 'index'], + 'methods' => 'GET', + ], 'public.user.register' => [ 'path' => '/user/register', 'controller' => [UserController::class, 'register'], diff --git a/phpmyfaq/startpage.php b/phpmyfaq/startpage.php deleted file mode 100755 index a8bb961c42..0000000000 --- a/phpmyfaq/startpage.php +++ /dev/null @@ -1,95 +0,0 @@ - - * @copyright 2002-2026 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2002-08-23 - */ - -use phpMyFAQ\Category\Startpage; -use phpMyFAQ\Faq\Statistics; -use phpMyFAQ\Helper\CategoryHelper; -use phpMyFAQ\Language\Plurals; -use phpMyFAQ\News; -use phpMyFAQ\Strings; -use phpMyFAQ\Twig\Extensions\TagNameTwigExtension; -use phpMyFAQ\Twig\TwigWrapper; -use phpMyFAQ\Translation; -use Symfony\Component\HttpFoundation\Request; -use Twig\Extension\AttributeExtension; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$faqConfig = $container->get('phpmyfaq.configuration'); - -$news = new News($faqConfig); -$categoryHelper = new CategoryHelper(); -$plr = new Plurals(); -$faqStatistics = new Statistics($faqConfig); - -$request = Request::createFromGlobals(); - -$writeNewsHeader = Translation::get(key: 'newsArchive'); - -$startPageCategory = new Startpage($faqConfig); -$startPageCategory - ->setLanguage($faqLangCode) - ->setUser($currentUser) - ->setGroups($currentGroups); - -$startPageCategories = $startPageCategory->getCategories(); - -// generates a top ten list -$param = $faqConfig->get('records.orderingPopularFaqs') == 'visits' ? 'visits' : 'voted'; - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/'); -$twig->addExtension(new AttributeExtension(TagNameTwigExtension::class)); -$twigTemplate = $twig->loadTemplate('./startpage.twig'); - -// Twig template variables -$templateVars = [ - ... $templateVars, - 'baseHref' => $faqSystem->getSystemUri($faqConfig), - 'title' => $faqConfig->getTitle(), - 'pageHeader' => $faqConfig->getTitle(), - 'startPageCategories' => (is_countable($startPageCategories) ? count($startPageCategories) : 0) > 0, - 'startPageCategoryDecks' => $startPageCategories, - 'stickyRecordsHeader' => Translation::get(key: 'stickyRecordsHeader'), - 'stickyRecordsList' => $faq->getStickyFaqsData(), - 'writeTopTenHeader' => Translation::get(key: 'msgTopTen'), - 'topRecordsList' => $faqStatistics->getTopTen($param), - 'errorMsgTopTen' => Translation::get(key: 'err_noTopTen'), - 'writeNewestHeader' => Translation::get(key: 'msgLatestArticles'), - 'latestRecordsList' => $faqStatistics->getLatest(), - 'errorMsgLatest' => Translation::get(key: 'msgErrorNoRecords'), - 'msgTrendingFAQs' => Translation::get(key: 'msgTrendingFAQs'), - 'trendingRecordsList' => $faqStatistics->getTrending(), - 'errorMsgTrendingFaqs' => Translation::get(key: 'msgErrorNoRecords'), - 'msgNewsHeader' => $writeNewsHeader, - 'newsList' => $news->getAll(), - 'writeNumberOfArticles' => $plr->getMsg('plmsgHomeArticlesOnline', $faqStatistics->totalFaqs($faqLangCode)), - 'msgTags' => Translation::get(key: 'msgPopularTags'), - 'tagsList' => $oTag->getPopularTags(12), - 'formActionUrl' => '?' . $sids . 'action=search', - 'searchBox' => Translation::get(key: 'msgSearch'), - 'categoryId' => ($cat === 0) ? '%' : (int)$cat, - 'msgSearch' => sprintf( - '%s', - Strings::htmlentities($faqSystem->getSystemUri($faqConfig)), - Translation::get(key: 'msgAdvancedSearch') - ) -]; - -return $templateVars; From 642c21e6b8624ac6f356495c24ee146e684b733d Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 14:04:23 +0100 Subject: [PATCH 040/286] refactor: moved Elasticsearch and OpenSearch classes in correct folder --- .../Controller/Administration/Api/ElasticsearchController.php | 2 +- .../phpMyFAQ/Controller/Administration/Api/FaqController.php | 4 ++-- .../Controller/Administration/Api/OpenSearchController.php | 2 +- phpmyfaq/src/phpMyFAQ/Faq.php | 2 +- phpmyfaq/src/phpMyFAQ/Instance/{ => Search}/Elasticsearch.php | 2 +- phpmyfaq/src/phpMyFAQ/Instance/{ => Search}/OpenSearch.php | 2 +- phpmyfaq/src/phpMyFAQ/Search.php | 4 ++-- phpmyfaq/src/phpMyFAQ/Search/{ => Search}/Elasticsearch.php | 4 +++- phpmyfaq/src/phpMyFAQ/Search/{ => Search}/OpenSearch.php | 4 +++- phpmyfaq/src/phpMyFAQ/Search/SearchDatabase.php | 2 +- phpmyfaq/src/phpMyFAQ/Setup/Installer.php | 4 ++-- phpmyfaq/src/services.php | 4 ++-- 12 files changed, 20 insertions(+), 16 deletions(-) rename phpmyfaq/src/phpMyFAQ/Instance/{ => Search}/Elasticsearch.php (99%) rename phpmyfaq/src/phpMyFAQ/Instance/{ => Search}/OpenSearch.php (99%) rename phpmyfaq/src/phpMyFAQ/Search/{ => Search}/Elasticsearch.php (98%) rename phpmyfaq/src/phpMyFAQ/Search/{ => Search}/OpenSearch.php (98%) diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/ElasticsearchController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/ElasticsearchController.php index 291100aec8..d961419568 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/ElasticsearchController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/ElasticsearchController.php @@ -25,7 +25,7 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Enums\PermissionType; use phpMyFAQ\Faq; -use phpMyFAQ\Instance\Elasticsearch; +use phpMyFAQ\Instance\Search\Elasticsearch; use phpMyFAQ\Translation; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/FaqController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/FaqController.php index e9a8875581..1b22c43187 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/FaqController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/FaqController.php @@ -38,8 +38,8 @@ use phpMyFAQ\Filter; use phpMyFAQ\Helper\CategoryHelper; use phpMyFAQ\Helper\SearchHelper; -use phpMyFAQ\Instance\Elasticsearch; -use phpMyFAQ\Instance\OpenSearch; +use phpMyFAQ\Instance\Search\Elasticsearch; +use phpMyFAQ\Instance\Search\OpenSearch; use phpMyFAQ\Language; use phpMyFAQ\Link; use phpMyFAQ\Search; diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/OpenSearchController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/OpenSearchController.php index cc6811137d..13c5ffcc2d 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/OpenSearchController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/OpenSearchController.php @@ -23,7 +23,7 @@ use phpMyFAQ\Core\Exception; use phpMyFAQ\Enums\PermissionType; use phpMyFAQ\Faq; -use phpMyFAQ\Instance\OpenSearch; +use phpMyFAQ\Instance\Search\OpenSearch; use phpMyFAQ\Translation; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; diff --git a/phpmyfaq/src/phpMyFAQ/Faq.php b/phpmyfaq/src/phpMyFAQ/Faq.php index 69803ea775..89f7d2a77e 100755 --- a/phpmyfaq/src/phpMyFAQ/Faq.php +++ b/phpmyfaq/src/phpMyFAQ/Faq.php @@ -31,7 +31,7 @@ use phpMyFAQ\Entity\FaqEntity; use phpMyFAQ\Faq\QueryHelper; use phpMyFAQ\Helper\FaqHelper; -use phpMyFAQ\Instance\Elasticsearch; +use phpMyFAQ\Instance\Search\Elasticsearch; use phpMyFAQ\Language\Plurals; use stdClass; diff --git a/phpmyfaq/src/phpMyFAQ/Instance/Elasticsearch.php b/phpmyfaq/src/phpMyFAQ/Instance/Search/Elasticsearch.php similarity index 99% rename from phpmyfaq/src/phpMyFAQ/Instance/Elasticsearch.php rename to phpmyfaq/src/phpMyFAQ/Instance/Search/Elasticsearch.php index c780a814b8..cb47eeb11c 100644 --- a/phpmyfaq/src/phpMyFAQ/Instance/Elasticsearch.php +++ b/phpmyfaq/src/phpMyFAQ/Instance/Search/Elasticsearch.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Instance; +namespace phpMyFAQ\Instance\Search; use Elastic\Elasticsearch\Client; use Elastic\Elasticsearch\Exception\ClientResponseException; diff --git a/phpmyfaq/src/phpMyFAQ/Instance/OpenSearch.php b/phpmyfaq/src/phpMyFAQ/Instance/Search/OpenSearch.php similarity index 99% rename from phpmyfaq/src/phpMyFAQ/Instance/OpenSearch.php rename to phpmyfaq/src/phpMyFAQ/Instance/Search/OpenSearch.php index 7ae3dff922..996876118a 100644 --- a/phpmyfaq/src/phpMyFAQ/Instance/OpenSearch.php +++ b/phpmyfaq/src/phpMyFAQ/Instance/Search/OpenSearch.php @@ -17,7 +17,7 @@ declare(strict_types=1); -namespace phpMyFAQ\Instance; +namespace phpMyFAQ\Instance\Search; use Exception; use OpenSearch\Client; diff --git a/phpmyfaq/src/phpMyFAQ/Search.php b/phpmyfaq/src/phpMyFAQ/Search.php index cb0c9585e9..e9e3939028 100755 --- a/phpmyfaq/src/phpMyFAQ/Search.php +++ b/phpmyfaq/src/phpMyFAQ/Search.php @@ -23,8 +23,8 @@ use DateTime; use Exception; -use phpMyFAQ\Search\Elasticsearch; -use phpMyFAQ\Search\OpenSearch; +use phpMyFAQ\Search\Search\Elasticsearch; +use phpMyFAQ\Search\Search\OpenSearch; use phpMyFAQ\Search\SearchFactory; use stdClass; diff --git a/phpmyfaq/src/phpMyFAQ/Search/Elasticsearch.php b/phpmyfaq/src/phpMyFAQ/Search/Search/Elasticsearch.php similarity index 98% rename from phpmyfaq/src/phpMyFAQ/Search/Elasticsearch.php rename to phpmyfaq/src/phpMyFAQ/Search/Search/Elasticsearch.php index 5f3afc8174..931aeb55af 100644 --- a/phpmyfaq/src/phpMyFAQ/Search/Elasticsearch.php +++ b/phpmyfaq/src/phpMyFAQ/Search/Search/Elasticsearch.php @@ -17,13 +17,15 @@ declare(strict_types=1); -namespace phpMyFAQ\Search; +namespace phpMyFAQ\Search\Search; use Elastic\Elasticsearch\Client; use Elastic\Elasticsearch\Exception\ClientResponseException; use Elastic\Elasticsearch\Exception\ServerResponseException; use phpMyFAQ\Configuration; use phpMyFAQ\Configuration\ElasticsearchConfiguration; +use phpMyFAQ\Search\AbstractSearch; +use phpMyFAQ\Search\SearchInterface; use stdClass; /** diff --git a/phpmyfaq/src/phpMyFAQ/Search/OpenSearch.php b/phpmyfaq/src/phpMyFAQ/Search/Search/OpenSearch.php similarity index 98% rename from phpmyfaq/src/phpMyFAQ/Search/OpenSearch.php rename to phpmyfaq/src/phpMyFAQ/Search/Search/OpenSearch.php index 0ea9290edc..14409ebcaa 100644 --- a/phpmyfaq/src/phpMyFAQ/Search/OpenSearch.php +++ b/phpmyfaq/src/phpMyFAQ/Search/Search/OpenSearch.php @@ -17,11 +17,13 @@ declare(strict_types=1); -namespace phpMyFAQ\Search; +namespace phpMyFAQ\Search\Search; use OpenSearch\Client; use phpMyFAQ\Configuration; use phpMyFAQ\Configuration\OpenSearchConfiguration; +use phpMyFAQ\Search\AbstractSearch; +use phpMyFAQ\Search\SearchInterface; use stdClass; /** diff --git a/phpmyfaq/src/phpMyFAQ/Search/SearchDatabase.php b/phpmyfaq/src/phpMyFAQ/Search/SearchDatabase.php index 8a20c77cf1..580707e7a0 100644 --- a/phpmyfaq/src/phpMyFAQ/Search/SearchDatabase.php +++ b/phpmyfaq/src/phpMyFAQ/Search/SearchDatabase.php @@ -67,7 +67,7 @@ class SearchDatabase extends AbstractSearch implements SearchInterface protected array $conditions = []; /** - * Flag if database supports search relevance. + * Flag if a database supports search relevance. */ protected bool $relevanceSupport = false; diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Installer.php b/phpmyfaq/src/phpMyFAQ/Setup/Installer.php index 72fd384241..6994a60b83 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Installer.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Installer.php @@ -38,9 +38,9 @@ use phpMyFAQ\Instance; use phpMyFAQ\Instance\Database as InstanceDatabase; use phpMyFAQ\Instance\Database\Stopwords; -use phpMyFAQ\Instance\Elasticsearch; use phpMyFAQ\Instance\Main; -use phpMyFAQ\Instance\OpenSearch; +use phpMyFAQ\Instance\Search\Elasticsearch; +use phpMyFAQ\Instance\Search\OpenSearch; use phpMyFAQ\Instance\Setup; use phpMyFAQ\Ldap; use phpMyFAQ\Link; diff --git a/phpmyfaq/src/services.php b/phpmyfaq/src/services.php index 04203ede99..d763b91120 100644 --- a/phpmyfaq/src/services.php +++ b/phpmyfaq/src/services.php @@ -53,8 +53,8 @@ use phpMyFAQ\Helper\StatisticsHelper; use phpMyFAQ\Helper\UserHelper; use phpMyFAQ\Instance; -use phpMyFAQ\Instance\Elasticsearch; -use phpMyFAQ\Instance\OpenSearch; +use phpMyFAQ\Instance\Search\Elasticsearch; +use phpMyFAQ\Instance\Search\OpenSearch; use phpMyFAQ\Language; use phpMyFAQ\Language\Plurals; use phpMyFAQ\Mail; From c8016e6af4d25cc87b9ed7b4944b1413a27b65e5 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Fri, 2 Jan 2026 14:38:19 +0100 Subject: [PATCH 041/286] refactor: migrated search page (#3834) --- phpmyfaq/assets/templates/default/search.twig | 9 +- .../assets/templates/default/startpage.twig | 2 +- phpmyfaq/search.php | 340 -------------- .../Controller/Frontend/SearchController.php | 153 +++++++ phpmyfaq/src/phpMyFAQ/Helper/SearchHelper.php | 3 +- phpmyfaq/src/phpMyFAQ/Helper/TagsHelper.php | 6 +- .../src/phpMyFAQ/Search/SearchService.php | 379 ++++++++++++++++ phpmyfaq/src/phpMyFAQ/Tags.php | 6 +- phpmyfaq/src/public-routes.php | 6 + tests/phpMyFAQ/Helper/TagsHelperTest.php | 8 +- tests/phpMyFAQ/Search/SearchServiceTest.php | 413 ++++++++++++++++++ tests/phpMyFAQ/TagsTest.php | 2 +- 12 files changed, 966 insertions(+), 361 deletions(-) delete mode 100755 phpmyfaq/search.php create mode 100644 phpmyfaq/src/phpMyFAQ/Controller/Frontend/SearchController.php create mode 100644 phpmyfaq/src/phpMyFAQ/Search/SearchService.php create mode 100644 tests/phpMyFAQ/Search/SearchServiceTest.php diff --git a/phpmyfaq/assets/templates/default/search.twig b/phpmyfaq/assets/templates/default/search.twig index fe3bf0c1a8..49be484604 100644 --- a/phpmyfaq/assets/templates/default/search.twig +++ b/phpmyfaq/assets/templates/default/search.twig @@ -8,12 +8,11 @@ {% if isTagSearch == false %}