2 namespace wcf\acp\page
;
3 use wcf\data\application\Application
;
4 use wcf\data\
object\type\ObjectTypeCache
;
5 use wcf\page\AbstractPage
;
6 use wcf\system\database\util\PreparedStatementConditionBuilder
;
7 use wcf\system\exception\SystemException
;
8 use wcf\system\search\SearchIndexManager
;
10 use wcf\util\FileUtil
;
13 * @author Alexander Ebert
14 * @copyright 2001-2020 WoltLab GmbH
15 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
16 * @package WoltLabSuite\Core\Acp\Page
19 class SystemCheckPage
extends AbstractPage
{
23 public $activeMenuItem = 'wcf.acp.menu.link.systemCheck';
28 public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
31 * A list of directories that need to be writable at all times, grouped by their application
32 * identifier. Only the directory itself is checked, unless the path ends with `/*`.
35 public $directories = [
39 '/acp/templates/compiled',
48 '/templates/compiled',
53 public $mysqlVersions = [
63 public $phpExtensions = [
78 public $phpMemoryLimit = 128;
80 public $phpVersions = [
81 'minimum' => '7.2.24',
82 'sufficient' => ['7.2'],
83 'recommended' => ['7.3', '7.4', '8.0'],
86 public $foreignKeys = [
87 'wcf'. WCF_N
.'_user' => [
89 'referenceTable' => 'wcf'. WCF_N
.'_user_avatar',
90 'referenceColumn' => 'avatarID',
93 'wcf'. WCF_N
.'_comment' => [
95 'referenceTable' => 'wcf'. WCF_N
.'_user',
96 'referenceColumn' => 'userID',
99 'referenceTable' => 'wcf'. WCF_N
.'_object_type',
100 'referenceColumn' => 'objectTypeID',
103 'wcf'. WCF_N
.'_moderation_queue' => [
105 'referenceTable' => 'wcf'. WCF_N
.'_object_type',
106 'referenceColumn' => 'objectTypeID',
108 'assignedUserID' => [
109 'referenceTable' => 'wcf'. WCF_N
.'_user',
110 'referenceColumn' => 'userID',
113 'referenceTable' => 'wcf'. WCF_N
.'_user',
114 'referenceColumn' => 'userID',
125 'version' => '0.0.0',
126 'foreignKeys' => false,
129 'incorrectTables' => [],
147 'result' => 'unsupported',
152 'directories' => false,
159 * indicates that this page is only accessible to owners in enterprise mode
161 const BLACKLISTED_IN_ENTERPRISE_MODE
= true;
166 public function readData() {
169 if (IMAGE_ADAPTER_TYPE
=== 'imagick' && !in_array('imagick', $this->phpExtensions
)) {
170 $this->phpExtensions
[] = 'imagick';
173 if (CACHE_SOURCE_TYPE
=== 'memcached' && !in_array('memcached', $this->phpExtensions
)) {
174 $this->phpExtensions
[] = 'memcached';
177 if (CACHE_SOURCE_TYPE
=== 'redis' && !in_array('redis', $this->phpExtensions
)) {
178 $this->phpExtensions
[] = 'redis';
181 $this->validateMysql();
182 $this->validatePhpExtensions();
183 $this->validatePhpMemoryLimit();
184 $this->validatePhpVersion();
185 $this->validatePhpGdSupport();
186 $this->validateWritableDirectories();
192 public function assignVariables() {
193 parent
::assignVariables();
195 WCF
::getTPL()->assign([
196 'mysqlVersions' => $this->mysqlVersions
,
197 'phpExtensions' => $this->phpExtensions
,
198 'phpMemoryLimit' => $this->phpMemoryLimit
,
199 'phpVersions' => $this->phpVersions
,
200 'results' => $this->results
,
204 protected function validateMysql() {
206 $sqlVersion = WCF
::getDB()->getVersion();
207 $compareSQLVersion = preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $sqlVersion);
208 // Do not use the "raw" version, it usually contains a lot of noise.
209 $this->results
['mysql']['version'] = $compareSQLVersion;
210 if (stripos($sqlVersion, 'MariaDB') !== false) {
211 $this->results
['mysql']['mariadb'] = true;
213 $this->results
['mysql']['result'] = (version_compare($compareSQLVersion, $this->mysqlVersions
['mariadb']['10']) >= 0);
216 if ($compareSQLVersion[0] === '5') {
217 $this->results
['mysql']['result'] = (version_compare($compareSQLVersion, $this->mysqlVersions
['mysql']['5']) >= 0);
220 $this->results
['mysql']['result'] = (version_compare($compareSQLVersion, $this->mysqlVersions
['mysql']['8']) >= 0);
224 // check innodb support
225 $sql = "SHOW ENGINES";
226 $statement = WCF
::getDB()->prepareStatement($sql);
227 $statement->execute();
228 while ($row = $statement->fetchArray()) {
229 if ($row['Engine'] == 'InnoDB' && in_array($row['Support'], ['DEFAULT', 'YES'])) {
230 $this->results
['mysql']['innodb'] = true;
235 // validate foreign keys
236 $expectedForeignKeyCount = 0;
237 $conditionBuilder = new PreparedStatementConditionBuilder(true, 'OR');
238 foreach ($this->foreignKeys
as $table => $keys) {
239 foreach ($keys as $column => $reference) {
240 $innerConditionBuilder = new PreparedStatementConditionBuilder(false);
241 $innerConditionBuilder->add('REFERENCED_TABLE_SCHEMA = ?', [WCF
::getDB()->getDatabaseName()]);
242 $innerConditionBuilder->add('REFERENCED_TABLE_NAME = ?', [$reference['referenceTable']]);
243 $innerConditionBuilder->add('REFERENCED_COLUMN_NAME = ?', [$reference['referenceColumn']]);
244 $innerConditionBuilder->add('TABLE_NAME = ?', [$table]);
245 $innerConditionBuilder->add('COLUMN_NAME = ?', [$column]);
247 $conditionBuilder->add('('. $innerConditionBuilder .')', $innerConditionBuilder->getParameters());
249 $expectedForeignKeyCount++
;
253 $sql = "SELECT COUNT(*)
254 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
255 ". $conditionBuilder;
256 $statement = WCF
::getDB()->prepareStatement($sql);
257 $statement->execute($conditionBuilder->getParameters());
259 $this->results
['mysql']['foreignKeys'] = $statement->fetchSingleColumn() == $expectedForeignKeyCount;
261 // check search engine tables
262 $objectTypes = ObjectTypeCache
::getInstance()->getObjectTypes('com.woltlab.wcf.searchableObjectType');
264 foreach ($objectTypes as $objectType) {
265 $tableNames[] = SearchIndexManager
::getTableName($objectType->objectType
);
267 $conditionBuilder = new PreparedStatementConditionBuilder(true);
268 $conditionBuilder->add('TABLE_NAME IN (?)', [$tableNames]);
269 $conditionBuilder->add('TABLE_SCHEMA = ?', [WCF
::getDB()->getDatabaseName()]);
271 $sql = "SELECT TABLE_NAME, ENGINE
272 FROM INFORMATION_SCHEMA.TABLES
273 ". $conditionBuilder;
274 $statement = WCF
::getDB()->prepareStatement($sql);
275 $statement->execute($conditionBuilder->getParameters());
277 while ($row = $statement->fetchArray()) {
278 if ($row['ENGINE'] !== 'InnoDB') {
279 $this->results
['mysql']['searchEngine']['incorrectTables'][$row['TABLE_NAME']] = $row['ENGINE'];
283 $this->results
['mysql']['searchEngine']['result'] = empty($this->results
['mysql']['searchEngine']['incorrectTables']);
285 if ($this->results
['mysql']['result'] && $this->results
['mysql']['innodb'] && $this->results
['mysql']['foreignKeys'] && $this->results
['mysql']['searchEngine']['result']) {
286 $this->results
['status']['mysql'] = true;
290 protected function validatePhpExtensions() {
291 foreach ($this->phpExtensions
as $phpExtension) {
292 $result = extension_loaded($phpExtension);
294 $this->results
['php']['extension'][] = $phpExtension;
298 if (extension_loaded('hash')) {
299 $this->results
['php']['sha256'] = in_array('sha256', hash_algos());
302 $this->results
['status']['php'] = empty($this->results
['php']['extension']) && $this->results
['php']['sha256'];
305 protected function validatePhpMemoryLimit() {
306 $this->results
['php']['memoryLimit']['required'] = $this->phpMemoryLimit
. 'M';
308 $memoryLimit = ini_get('memory_limit');
310 // Memory is not limited through PHP.
311 if ($memoryLimit == -1) {
312 $this->results
['php']['memoryLimit']['value'] = "\u{221E}";
313 $this->results
['php']['memoryLimit']['result'] = true;
316 // Completely numeric, PHP assumes this to be a value in bytes.
317 if (is_numeric($memoryLimit)) {
318 $memoryLimit = $memoryLimit / 1024 / 1024;
320 $this->results
['php']['memoryLimit']['value'] = $memoryLimit . 'M';
321 $this->results
['php']['memoryLimit']['result'] = ($memoryLimit >= $this->phpMemoryLimit
);
324 // PHP supports the 'K', 'M' and 'G' shorthand notations.
325 if (preg_match('~^(\d+)([KMG])$~', $memoryLimit, $matches)) {
326 switch ($matches[2]) {
328 $memoryLimit = $matches[1] / 1024;
330 $this->results
['php']['memoryLimit']['value'] = $memoryLimit . 'M';
331 $this->results
['php']['memoryLimit']['result'] = ($memoryLimit >= $this->phpMemoryLimit
);
335 $this->results
['php']['memoryLimit']['value'] = $memoryLimit;
336 $this->results
['php']['memoryLimit']['result'] = ($matches[1] >= $this->phpMemoryLimit
);
340 $this->results
['php']['memoryLimit']['value'] = $memoryLimit;
341 $this->results
['php']['memoryLimit']['result'] = ($matches[1] * 1024 >= $this->phpMemoryLimit
);
345 $this->results
['php']['memoryLimit']['value'] = $memoryLimit;
346 $this->results
['php']['memoryLimit']['result'] = false;
353 $this->results
['status']['php'] = $this->results
['status']['php'] && $this->results
['php']['memoryLimit']['result'];
356 protected function validatePhpVersion() {
357 $phpVersion = phpversion();
358 $comparePhpVersion = preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $phpVersion);
359 // Do not use the "raw" version, it usually contains a lot of noise.
360 $this->results
['php']['version']['value'] = $comparePhpVersion;
361 if (version_compare($comparePhpVersion, $this->phpVersions
['minimum']) >= 0) {
362 $majorMinor = preg_replace('~^(\d+\.\d+).*$~', '\\1', $phpVersion);
363 foreach (['recommended', 'sufficient'] as $type) {
364 foreach ($this->phpVersions
[$type] as $version) {
365 if ($majorMinor === $version) {
366 $this->results
['php']['version']['result'] = $type;
373 $this->results
['php']['version']['result'] = 'unsupported';
376 $this->results
['status']['php'] = $this->results
['status']['php'] && ($this->results
['php']['version']['result'] !== 'unsupported');
379 protected function validatePhpGdSupport() {
380 if (!function_exists('\gd_info')) {
381 $this->results
['status']['php'] = false;
385 $gdInfo = \
gd_info();
386 $this->results
['php']['gd']['jpeg'] = !empty($gdInfo['JPEG Support']);
387 $this->results
['php']['gd']['png'] = !empty($gdInfo['PNG Support']);
388 $this->results
['php']['gd']['webp'] = !empty($gdInfo['WebP Support']);
390 $this->results
['php']['gd']['result'] = $this->results
['php']['gd']['jpeg']
391 && $this->results
['php']['gd']['png']
392 && $this->results
['php']['gd']['webp'];
394 $this->results
['status']['php'] = $this->results
['status']['php'] && $this->results
['php']['gd']['result'];
397 protected function validateWritableDirectories() {
398 foreach ($this->directories
as $abbreviation => $directories) {
399 $basePath = Application
::getDirectory($abbreviation);
400 foreach ($directories as $directory) {
402 if (preg_match('~(.*)/\*$~', $directory, $matches)) {
404 $directory = $matches[1];
407 $path = $basePath . FileUtil
::removeLeadingSlash(FileUtil
::addTrailingSlash($directory));
408 if ($this->checkDirectory($path) && $recursive) {
409 $rdi = new \
RecursiveDirectoryIterator($path, \FilesystemIterator
::SKIP_DOTS
);
410 $it = new \
RecursiveIteratorIterator($rdi, \RecursiveIteratorIterator
::SELF_FIRST
);
411 /** @var \SplFileInfo $item */
412 foreach ($it as $item) {
413 if ($item->isDir()) {
414 $this->makeDirectoryWritable($item->getPathname());
421 $this->results
['status']['directories'] = empty($this->results
['directories']);
424 protected function checkDirectory($path) {
425 if (!$this->createDirectoryIfNotExists($path)) {
426 $this->results
['directories'][] = FileUtil
::unifyDirSeparator($path);
430 return $this->makeDirectoryWritable($path);
433 protected function createDirectoryIfNotExists($path) {
434 if (!file_exists($path) && !FileUtil
::makePath($path)) {
435 // FileUtil::makePath() returns false if either the directory cannot be created
436 // or if it cannot be made writable.
437 if (!file_exists($path)) {
445 protected function makeDirectoryWritable($path) {
447 FileUtil
::makeWritable($path);
449 catch (SystemException
$e) {
450 $this->results
['directories'][] = FileUtil
::unifyDirSeparator($path);