Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
24.27% covered (danger)
24.27%
25 / 103
50.00% covered (danger)
50.00%
6 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Installer
24.27% covered (danger)
24.27%
25 / 103
50.00% covered (danger)
50.00%
6 / 12
536.03
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
 cleanFailedInstallationFiles
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 checkBasicStuff
86.67% covered (success)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
6.09
 checkFilesystemPermissions
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 checkNoncriticalSettings
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
72
 checkInitialRewriteBasePath
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 startInstall
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
2.35
 checkMinimumPhpVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasLdapSupport
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasElasticsearchSupport
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getFormInputs
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isAlreadyInstalled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * The Installer class installs phpMyFAQ. Classy.
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    Florian Anderiasch <florian@phpmyfaq.net>
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     2012-08-27
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Setup;
21
22use Elastic\Elasticsearch\Exception\AuthenticationException;
23use phpMyFAQ\Core\Exception;
24use phpMyFAQ\Instance\Setup;
25use phpMyFAQ\Setup\Installation\DefaultDataSeeder;
26use phpMyFAQ\System;
27use Symfony\Component\HttpFoundation\Request;
28
29/**
30 * Class Installer
31 *
32 * @package phpMyFAQ
33 */
34class Installer extends Setup
35{
36    /**
37     * Constructor.
38     *
39     * @throws \Exception
40     */
41    public function __construct(
42        private readonly System $system,
43    ) {
44        parent::__construct();
45    }
46
47    /**
48     * Removes the database.php and the ldap.php if an installation failed.
49     */
50    public static function cleanFailedInstallationFiles(): void
51    {
52        $databaseFile = (string) PMF_ROOT_DIR . '/content/core/config/database.php';
53        if (file_exists($databaseFile)) {
54            unlink($databaseFile);
55        }
56
57        $ldapFile = (string) PMF_ROOT_DIR . '/content/core/config/ldap.php';
58        if (file_exists($ldapFile)) {
59            unlink($ldapFile);
60        }
61    }
62
63    /**
64     * Check the necessary stuff and throw an exception if something is wrong.
65     * @throws Exception
66     */
67    public function checkBasicStuff(): void
68    {
69        if (!$this->checkMinimumPhpVersion()) {
70            throw new Exception(sprintf('Sorry, but you need PHP %s or later!', System::VERSION_MINIMUM_PHP));
71        }
72
73        if (!function_exists('date_default_timezone_set')) {
74            throw new Exception('Sorry, but setting a default timezone does not work in your environment!');
75        }
76
77        if (!$this->system->checkDatabase()) {
78            throw new Exception('No supported database detected!');
79        }
80
81        if (!$this->system->checkRequiredExtensions()) {
82            throw new Exception(sprintf('Some required PHP extensions are missing: %s', implode(
83                ', ',
84                $this->system->getMissingExtensions(),
85            )));
86        }
87
88        if (!$this->system->checkInstallation()) {
89            throw new Exception(
90                'Looks like phpMyFAQ is already installed! Please use the <a href="../update">update</a>.',
91            );
92        }
93    }
94
95    /**
96     * Checks if the file permissions are okay.
97     */
98    public function checkFilesystemPermissions(): ?string
99    {
100        $instanceSetup = new Setup();
101        $instanceSetup->setRootDir((string) PMF_ROOT_DIR);
102
103        $dirs = [
104            '/content/core/config',
105            '/content/core/data',
106            '/content/core/logs',
107            '/content/user/images',
108            '/content/user/attachments',
109        ];
110        $failedDirs = $instanceSetup->checkDirs($dirs);
111        $numDirs = count($failedDirs);
112
113        $hints = '';
114        if (1 <= $numDirs) {
115            $hints .= sprintf(
116                '<p class="alert alert-danger">The following %s could not be created or %s not writable:</p><ul>',
117                1 < $numDirs ? 'directories' : 'directory',
118                1 < $numDirs ? 'are' : 'is',
119            );
120            foreach ($failedDirs as $failedDir) {
121                $hints .= "<li>{$failedDir}</li>\n";
122            }
123
124            return (
125                $hints
126                . '</ul><p class="alert alert-danger">Please create '
127                . (1 < $numDirs ? 'them' : 'it')
128                . ' manually and/or change access to chmod 775 (or greater if necessary).</p>'
129            );
130        }
131
132        return null;
133    }
134
135    /**
136     * Checks some non-critical settings and print some hints.
137     *
138     * @return string[]
139     */
140    public function checkNoncriticalSettings(): array
141    {
142        $hints = [];
143        if (!$this->system->getHttpsStatus()) {
144            $hints[] =
145                '<p class="alert alert-warning">HTTPS support is not enabled in your web server.'
146                . ' To ensure the security of your data and protect against potential vulnerabilities,'
147                . ' we highly recommend enabling HTTPS. Please configure your web server to support HTTPS as soon as'
148                . ' possible.</p>';
149        }
150
151        if (!extension_loaded('gd')) {
152            $hints[] =
153                '<p class="alert alert-warning">You don\'t have GD support enabled in your PHP installation. '
154                . "Please enable GD support in your php.ini file otherwise you can't use Captchas for spam protection."
155                . '</p>';
156        }
157
158        if (!function_exists('imagettftext')) {
159            $hints[] =
160                '<p class="alert alert-warning">You don\'t have Freetype support enabled in the GD extension '
161                . ' of your PHP installation. Please enable Freetype support in GD extension otherwise the Captchas '
162                . 'for spam protection will be quite easy to break.</p>';
163        }
164
165        if (!extension_loaded('curl') || !extension_loaded('openssl')) {
166            $hints[] =
167                '<p class="alert alert-warning">You don\'t have cURL and/or OpenSSL support enabled in your '
168                . 'PHP installation. Please enable cURL and/or OpenSSL support in your php.ini file otherwise you '
169                . " can't use Elasticsearch.</p>";
170        }
171
172        if (!extension_loaded('fileinfo')) {
173            $hints[] =
174                '<p class="alert alert-warning">You don\'t have Fileinfo support enabled in your PHP '
175                . "installation. Please enable Fileinfo support in your php.ini file otherwise you can't use our "
176                . 'backup/restore functionality.</p>';
177        }
178
179        if (!extension_loaded('sodium')) {
180            $hints[] =
181                '<p class="alert alert-warning">You don\'t have Sodium support enabled in your PHP '
182                . "installation. Please enable Sodium support in your php.ini file otherwise you can't use our "
183                . 'backup/restore functionality.</p>';
184        }
185
186        return $hints;
187    }
188
189    /**
190     * @throws Exception
191     */
192    public function checkInitialRewriteBasePath(Request $request): bool
193    {
194        $basePath = $request->getBasePath();
195        if (str_ends_with($basePath, 'setup')) {
196            $basePath = substr($basePath, offset: 0, length: -strlen('setup'));
197        }
198
199        $htaccessPath = (string) PMF_ROOT_DIR . '/.htaccess';
200
201        $htaccessUpdater = new HtaccessUpdater();
202        return $htaccessUpdater->updateRewriteBase($htaccessPath, $basePath);
203    }
204
205    /**
206     * Starts the installation.
207     *
208     * Delegates to InstallationInputValidator for input parsing and
209     * InstallationRunner for the actual installation steps.
210     *
211     * @param array<string, mixed>|null $setup Optional setup array (for programmatic/test installs)
212     * @throws Exception|AuthenticationException
213     * @throws \Exception
214     */
215    public function startInstall(?array $setup = null): void
216    {
217        $rootDir = $setup['rootDir'] ?? PMF_ROOT_DIR;
218        if ($this->isAlreadyInstalled((string) $rootDir)) {
219            throw new Exception(
220                'Looks like phpMyFAQ is already installed! Please use the <a href="../update">update</a>.',
221            );
222        }
223
224        $validator = new InstallationInputValidator();
225        $input = $validator->validate($setup);
226
227        $runner = new InstallationRunner($this->system);
228        $runner->run($input);
229    }
230
231    /**
232     * Checks the minimum required PHP version, defined in System class.
233     * Returns true if it's okay.
234     */
235    public function checkMinimumPhpVersion(): bool
236    {
237        return version_compare(PHP_VERSION, System::VERSION_MINIMUM_PHP) >= 0;
238    }
239
240    public function hasLdapSupport(): bool
241    {
242        return extension_loaded('ldap');
243    }
244
245    public function hasElasticsearchSupport(): bool
246    {
247        return extension_loaded('curl') && extension_loaded('openssl');
248    }
249
250    /**
251     * Returns the form inputs array, delegating to DefaultDataSeeder.
252     *
253     * @return array<array<string, int|string>>
254     * @throws \Exception
255     */
256    public function getFormInputs(): array
257    {
258        $seeder = new DefaultDataSeeder();
259        return $seeder->getFormInputs();
260    }
261
262    private function isAlreadyInstalled(string $rootDir): bool
263    {
264        return is_file(rtrim($rootDir, DIRECTORY_SEPARATOR) . '/content/core/config/database.php');
265    }
266}