Lines 59.36% 130 / 219
Methods 60.00% 12 / 20
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 cleanFailedInstallationFiles 0.00% 0 / 6 0.00% 0 / 1 12
 checkBasicStuff 86.66% 13 / 15 0.00% 0 / 1 6.09
 checkFilesystemPermissions 0.00% 0 / 27 0.00% 0 / 1 42
 checkNoncriticalSettings 0.00% 0 / 33 0.00% 0 / 1 72
 checkInitialRewriteBasePath 0.00% 0 / 6 0.00% 0 / 1 6
 startInstall 55.55% 5 / 9 0.00% 0 / 1 2.35
 checkMinimumPhpVersion 100.00% 1 / 1 100.00% 1 / 1 1
 hasLdapSupport 100.00% 1 / 1 100.00% 1 / 1 1
 hasElasticsearchSupport 100.00% 1 / 1 100.00% 1 / 1 2
 getFormInputs 100.00% 2 / 2 100.00% 1 / 1 1
 isAlreadyInstalled 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Instance\Setup] setRootDir 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Instance\Setup] createAnonymousUser 0.00% 0 / 8 0.00% 0 / 1 2
 [phpMyFAQ\Instance\Setup] checkDirs 78.57% 11 / 14 0.00% 0 / 1 9.80
 [phpMyFAQ\Instance\Setup] createDatabaseFile 100.00% 44 / 44 100.00% 1 / 1 5
 [phpMyFAQ\Instance\Setup] escapeForSingleQuotedPhpString 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Instance\Setup] createLdapFile 100.00% 20 / 20 100.00% 1 / 1 1
 [phpMyFAQ\Instance\Setup] createElasticsearchFile 100.00% 14 / 14 100.00% 1 / 1 2
 [phpMyFAQ\Instance\Setup] createOpenSearchFile 100.00% 14 / 14 100.00% 1 / 1 2
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}

Inherited from phpMyFAQ\Instance\Setup

46    public function setRootDir(string $rootDir): void
47    {
48        $this->rootDir = $rootDir;
49    }
56    public function createAnonymousUser(Configuration $configuration): void
57    {
58        $user = new User($configuration);
59        $user->createUser('anonymous', '', '', -1);
60        $user->setStatus('protected');
61
62        $anonymousData = [
63            'display_name' => 'Anonymous User',
64            'email' => '',
65        ];
66        $user->setUserData($anonymousData);
67    }
75    public function checkDirs(array $dirs): array
76    {
77        $failedDirs = [];
78
79        foreach ($dirs as $dir) {
80            if (false === is_dir($this->rootDir . $dir)) {
81                // If the folder does not exist, try to create it
82                if (false === mkdir($this->rootDir . $dir)) {
83                    // If the folder creation fails
84                    $failedDirs[] = 'Folder [' . $dir . '] could not be created.';
85                }
86
87                if (is_dir($this->rootDir . $dir) && false === chmod($this->rootDir . $dir, permissions: 0o775)) {
88                    $failedDirs[] = 'Folder [' . $dir . '] could not be given correct permissions (775).';
89                }
90            }
91
92            if (false === is_dir($this->rootDir . $dir)) {
93                continue;
94            }
95
96            if (false === is_writable($this->rootDir . $dir)) {
97                // The folder exists, check permissions
98                // If the folder exists but is not writeable
99                $failedDirs[] = 'Folder [' . $dir . '] exists but is not writable.';
100            }
101
102            if ([] === $failedDirs) {
103                // if no failed dirs exist
104                copy($this->rootDir . '/setup/index.html', $this->rootDir . $dir . '/index.html');
105            }
106        }
107
108        return $failedDirs;
109    }
118    public function createDatabaseFile(array $data, string $folder = '/content/core/config'): int|bool
119    {
120        if (!file_exists($this->rootDir . $folder)) {
121            throw new Exception('File [' . $this->rootDir . $folder . '] does not exist.');
122        }
123
124        if (!is_writable($this->rootDir . $folder)) {
125            throw new Exception('File [' . $this->rootDir . $folder . '] is not writable.');
126        }
127
128        $schema = (string) ($data['dbSchema'] ?? '');
129        if ($schema !== '' && !preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $schema)) {
130            throw new Exception('Invalid database schema name.');
131        }
132
133        $dbServer = $this->escapeForSingleQuotedPhpString((string) ($data['dbServer'] ?? ''));
134        $dbPort = $this->escapeForSingleQuotedPhpString((string) ($data['dbPort'] ?? ''));
135        $dbUser = $this->escapeForSingleQuotedPhpString((string) ($data['dbUser'] ?? ''));
136        $dbPassword = $this->escapeForSingleQuotedPhpString((string) ($data['dbPassword'] ?? ''));
137        $dbDatabaseName = $this->escapeForSingleQuotedPhpString((string) ($data['dbDatabaseName'] ?? ''));
138        $dbPrefix = $this->escapeForSingleQuotedPhpString((string) ($data['dbPrefix'] ?? ''));
139        $dbType = $this->escapeForSingleQuotedPhpString((string) ($data['dbType'] ?? ''));
140        $dbSchema = $this->escapeForSingleQuotedPhpString($schema);
141
142        return file_put_contents(
143            $this->rootDir . $folder . '/database.php',
144            '<?php
145$DB[\'server\'] = \''
146            . $dbServer
147            . "';\n"
148            . "\$DB['port'] = '"
149            . $dbPort
150            . "';\n"
151            . "\$DB['user'] = '"
152            . $dbUser
153            . "';\n"
154            . "\$DB['password'] = '"
155            . $dbPassword
156            . "';\n"
157            . "\$DB['db'] = '"
158            . $dbDatabaseName
159            . "';\n"
160            . "\$DB['prefix'] = '"
161            . $dbPrefix
162            . "';\n"
163            . "\$DB['type'] = '"
164            . $dbType
165            . "';\n"
166            . "\$DB['schema'] = '"
167            . $dbSchema
168            . "';",
169            LOCK_EX,
170        );
171    }
173    private function escapeForSingleQuotedPhpString(string $value): string
174    {
175        return str_replace(['\\', "'"], ['\\\\', "\\'"], $value);
176    }
184    public function createLdapFile(array $data, string $folder = '/content/core/config'): int|bool
185    {
186        return file_put_contents(
187            $this->rootDir . $folder . '/config/ldap.php',
188            '<?php
189$PMF_LDAP[\'ldap_server\'] = \''
190            . (string) ($data['ldapServer'] ?? '')
191            . "';\n"
192            . "\$PMF_LDAP['ldap_port'] = '"
193            . (string) ($data['ldapPort'] ?? '')
194            . "';\n"
195            . "\$PMF_LDAP['ldap_user'] = '"
196            . (string) ($data['ldapUser'] ?? '')
197            . "';\n"
198            . "\$PMF_LDAP['ldap_password'] = '"
199            . (string) ($data['ldapPassword'] ?? '')
200            . "';\n"
201            . "\$PMF_LDAP['ldap_base'] = '"
202            . (string) ($data['ldapBase'] ?? '')
203            . "';",
204            LOCK_EX,
205        );
206    }
214    public function createElasticsearchFile(array $data, string $folder = '/content/core/config'): int|bool
215    {
216        return file_put_contents(
217            $this->rootDir . $folder . '/config/elasticsearch.php',
218            '<?php
219$PMF_ES[\'hosts\'] = [\''
220            . implode("'], ['", array_map(
221                static fn(mixed $host): string => (string) $host,
222                is_array($data['hosts'] ?? null) ? $data['hosts'] : [],
223            ))
224            . "'];\n"
225            . "\$PMF_ES['index'] = '"
226            . (string) ($data['index'] ?? '')
227            . "';\n",
228            LOCK_EX,
229        );
230    }
238    public function createOpenSearchFile(array $data, string $folder = '/content/core/config'): int|bool
239    {
240        return file_put_contents(
241            $this->rootDir . $folder . '/config/opensearch.php',
242            '<?php
243$PMF_OS[\'hosts\'] = [\''
244            . implode("'], ['", array_map(
245                static fn(mixed $host): string => (string) $host,
246                is_array($data['hosts'] ?? null) ? $data['hosts'] : [],
247            ))
248            . "'];\n"
249            . "\$PMF_OS['index'] = '"
250            . (string) ($data['index'] ?? '')
251            . "';\n",
252            LOCK_EX,
253        );
254    }