Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.67% covered (success)
83.67%
41 / 49
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConfigDirectoryResolver
83.67% covered (success)
83.67%
41 / 49
40.00% covered (danger)
40.00%
2 / 5
32.66
0.00% covered (danger)
0.00%
0 / 1
 resolve
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
8.12
 resolveDatabaseFile
84.21% covered (success)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
11.48
 loadConfigConstants
88.89% covered (success)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 resolveAttachmentsDir
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 computeAttachmentsPath
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
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
18declare(strict_types=1);
19
20namespace phpMyFAQ\Bootstrap;
21
22use Symfony\Component\HttpFoundation\RedirectResponse;
23
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}