Lines
59.36%
130 / 219
Methods
60.00%
12 / 20
Classes
0.00%
0 / 1
| 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 | ||
| 34 | class 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 | } |