Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
83.67% |
41 / 49 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
| ConfigDirectoryResolver | |
83.67% |
41 / 49 |
|
40.00% |
2 / 5 |
32.66 | |
0.00% |
0 / 1 |
| resolve | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
8.12 | |||
| resolveDatabaseFile | |
84.21% |
16 / 19 |
|
0.00% |
0 / 1 |
11.48 | |||
| loadConfigConstants | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
4.02 | |||
| resolveAttachmentsDir | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| computeAttachmentsPath | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
7 | |||
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * Config directory and filesystem path resolution for phpMyFAQ bootstrap |
| 5 | * |
| 6 | * This Source Code Form is subject to the terms of the Mozilla Public License, |
| 7 | * v. 2.0. If a copy of the MPL was not distributed with this file, You can |
| 8 | * obtain one at https://mozilla.org/MPL/2.0/. |
| 9 | * |
| 10 | * @package phpMyFAQ |
| 11 | * @author Thorsten Rinne <thorsten@phpmyfaq.de> |
| 12 | * @copyright 2012-2026 phpMyFAQ Team |
| 13 | * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 |
| 14 | * @link https://www.phpmyfaq.de |
| 15 | * @since 2026-02-08 |
| 16 | */ |
| 17 | |
| 18 | declare(strict_types=1); |
| 19 | |
| 20 | namespace phpMyFAQ\Bootstrap; |
| 21 | |
| 22 | use Symfony\Component\HttpFoundation\RedirectResponse; |
| 23 | |
| 24 | class ConfigDirectoryResolver |
| 25 | { |
| 26 | /** |
| 27 | * Defines PMF_CONFIG_DIR and PMF_LEGACY_CONFIG_DIR constants. |
| 28 | */ |
| 29 | public static function resolve(): void |
| 30 | { |
| 31 | if (defined('PMF_MULTI_INSTANCE_CONFIG_DIR')) { |
| 32 | if (!defined('PMF_CONFIG_DIR')) { |
| 33 | define('PMF_CONFIG_DIR', (string) constant('PMF_MULTI_INSTANCE_CONFIG_DIR')); |
| 34 | } |
| 35 | |
| 36 | return; |
| 37 | } |
| 38 | |
| 39 | if (!defined('PMF_CONFIG_DIR')) { |
| 40 | define('PMF_CONFIG_DIR', (string) PMF_ROOT_DIR . '/content/core/config'); |
| 41 | } |
| 42 | |
| 43 | // For backward compatibility, we also define PMF_LEGACY_CONFIG_DIR if not already defined, |
| 44 | // but it should not be used by new code. |
| 45 | // This can be removed if we drop support updates from phpMyFAQ 3.x versions that still use the old config |
| 46 | // location. |
| 47 | if (!defined('PMF_LEGACY_CONFIG_DIR')) { |
| 48 | define('PMF_LEGACY_CONFIG_DIR', (string) PMF_ROOT_DIR . '/config'); |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Detects the database.php config file. Redirects to /setup/ if missing, |
| 54 | * and we're not yet in a setup/update context. |
| 55 | * |
| 56 | * @return string|null Path to the database.php file, or null if not found (setup redirect sent) |
| 57 | */ |
| 58 | public static function resolveDatabaseFile(): ?string |
| 59 | { |
| 60 | $requestUri = $_SERVER['REQUEST_URI'] ?? ''; |
| 61 | $isSetupContext = |
| 62 | PHP_SAPI === 'cli' |
| 63 | || str_contains($requestUri, '/setup/') |
| 64 | || str_contains($requestUri, '/api/setup/') |
| 65 | || str_contains($requestUri, '/update') |
| 66 | || str_contains($requestUri, '/update/'); |
| 67 | |
| 68 | $legacyConfigDir = defined('PMF_LEGACY_CONFIG_DIR') ? (string) PMF_LEGACY_CONFIG_DIR : null; |
| 69 | |
| 70 | $configExists = file_exists((string) PMF_CONFIG_DIR . '/database.php'); |
| 71 | $legacyExists = $legacyConfigDir !== null && file_exists($legacyConfigDir . '/database.php'); |
| 72 | |
| 73 | if (!$configExists && !$legacyExists) { |
| 74 | if (!$isSetupContext) { |
| 75 | $response = new RedirectResponse('/setup/'); |
| 76 | $response->send(); |
| 77 | exit(); |
| 78 | } |
| 79 | |
| 80 | return null; |
| 81 | } |
| 82 | |
| 83 | if ($configExists) { |
| 84 | return (string) PMF_CONFIG_DIR . '/database.php'; |
| 85 | } |
| 86 | |
| 87 | return $legacyConfigDir . '/database.php'; |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * Loads the config-specific constants.php file. |
| 92 | */ |
| 93 | public static function loadConfigConstants(): void |
| 94 | { |
| 95 | $constantsFile = (string) PMF_CONFIG_DIR . '/constants.php'; |
| 96 | if (file_exists($constantsFile)) { |
| 97 | require_once $constantsFile; |
| 98 | return; |
| 99 | } |
| 100 | |
| 101 | if (!defined('PMF_LEGACY_CONFIG_DIR')) { |
| 102 | return; |
| 103 | } |
| 104 | |
| 105 | $legacyConstantsFile = (string) PMF_LEGACY_CONFIG_DIR . '/constants.php'; |
| 106 | if (file_exists($legacyConstantsFile)) { |
| 107 | require_once $legacyConstantsFile; |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Resolves the attachments directory with path-traversal protection and defines PMF_ATTACHMENTS_DIR. |
| 113 | */ |
| 114 | public static function resolveAttachmentsDir(string $confAttachmentsPath, string $rootDir): void |
| 115 | { |
| 116 | if (!defined('PMF_ATTACHMENTS_DIR')) { |
| 117 | define('PMF_ATTACHMENTS_DIR', self::computeAttachmentsPath($confAttachmentsPath, $rootDir)); |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Computes the resolved attachments path without side effects (no constant definition). |
| 123 | * |
| 124 | * @return string|false The resolved path, or false if path traversal was detected |
| 125 | */ |
| 126 | public static function computeAttachmentsPath(string $confAttachmentsPath, string $rootDir): string|false |
| 127 | { |
| 128 | $confAttachmentsPath = trim($confAttachmentsPath); |
| 129 | if ($confAttachmentsPath === '') { |
| 130 | return $rootDir . DIRECTORY_SEPARATOR; |
| 131 | } |
| 132 | |
| 133 | if ($confAttachmentsPath[0] === '/' || preg_match('%^[a-z]:[\\\\/]%i', $confAttachmentsPath)) { |
| 134 | return $confAttachmentsPath; |
| 135 | } |
| 136 | |
| 137 | $pathSegments = preg_split('%[\\\\/]%', $confAttachmentsPath); |
| 138 | $pathSegments = $pathSegments === false ? [] : $pathSegments; |
| 139 | foreach ($pathSegments as $pathSegment) { |
| 140 | if ($pathSegment === '..') { |
| 141 | return false; |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | return $rootDir . DIRECTORY_SEPARATOR . $confAttachmentsPath; |
| 146 | } |
| 147 | } |