Lines 83.67% 41 / 49
Methods 40.00% 2 / 5
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 resolve 50.00% 4 / 8 0.00% 0 / 1 8.12
 resolveDatabaseFile 84.21% 16 / 19 0.00% 0 / 1 11.48
 loadConfigConstants 88.88% 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
24class 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}