Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.00% covered (success)
84.00%
21 / 25
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
EnvironmentConfigurator
84.00% covered (success)
84.00%
21 / 25
50.00% covered (danger)
50.00%
3 / 6
14.80
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHtaccessPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getServerPath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
 getRewriteBase
80.00% covered (success)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 adjustRewriteBaseHtaccess
83.33% covered (success)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
4.07
 readHtaccess
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
1<?php
2
3/**
4 * The environment configurator is responsible for adjusting the .htaccess file to the user's environment.
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 2024-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     2024-11-16
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Setup;
21
22use phpMyFAQ\Configuration;
23use phpMyFAQ\Core\Exception;
24
25readonly class EnvironmentConfigurator
26{
27    private string $htaccessPath;
28
29    public function __construct(
30        private Configuration $configuration,
31    ) {
32        $this->htaccessPath = $this->configuration->getRootPath() . '/.htaccess';
33    }
34
35    public function getHtaccessPath(): string
36    {
37        return $this->htaccessPath;
38    }
39
40    public function getServerPath(): string
41    {
42        $path = parse_url($this->configuration->getDefaultUrl(), PHP_URL_PATH);
43
44        return $path === null || $path === false || $path === '' ? '/' : $path;
45    }
46
47    /**
48     * @throws Exception
49     */
50    public function getRewriteBase(): string
51    {
52        $content = $this->readHtaccess();
53
54        $matches = [];
55        if (preg_match('/^\s*RewriteBase\s+(\S+)/mi', $content, $matches) !== 1) {
56            throw new Exception('RewriteBase directive not found in .htaccess file');
57        }
58
59        return $matches[1];
60    }
61
62    /**
63     * Adjusts the RewriteBase and ErrorDocument 404 in the .htaccess file for the user's environment.
64     *
65     * This method ensures that URL routing works correctly and 404 errors are properly handled.
66     *
67     * - RewriteBase is set to the application's installation path (e.g., /faq/)
68     * - ErrorDocument 404 is configured to route errors to the application's error handler (e.g., /faq/index.php?action=404)
69     *
70     * @return bool Returns true if the .htaccess file was successfully modified, false otherwise.
71     * @throws Exception If the .htaccess file does not exist or cannot be read.
72     */
73    public function adjustRewriteBaseHtaccess(): bool
74    {
75        if (!file_exists($this->htaccessPath)) {
76            throw new Exception(sprintf('The %s/.htaccess file does not exist!', $this->getServerPath()));
77        }
78
79        $content = $this->readHtaccess();
80
81        $serverPath = $this->getServerPath();
82        $new404Path = rtrim($serverPath, characters: '/') . '/index.php?action=404';
83
84        $updated = preg_replace('/^(\s*RewriteBase\s+)\S+/mi', '${1}' . $serverPath, $content);
85        if ($updated === null) {
86            throw new Exception('Failed to update RewriteBase directive');
87        }
88
89        $updated = preg_replace('/^(\s*ErrorDocument\s+404\s+)\S+/mi', '${1}' . $new404Path, $updated);
90        if ($updated === null) {
91            throw new Exception('Failed to update ErrorDocument 404 directive');
92        }
93
94        return (bool) file_put_contents($this->htaccessPath, $updated);
95    }
96
97    /**
98     * @throws Exception
99     */
100    /* @mago-expect lint:no-error-control-operator - the read failure is handled explicitly on the next line */
101    private function readHtaccess(): string
102    {
103        $content = @file_get_contents($this->htaccessPath);
104        if ($content === false) {
105            throw new Exception(sprintf('Cannot read .htaccess file: %s', $this->htaccessPath));
106        }
107
108        return $content;
109    }
110}