Add foreign key system check
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / acp / page / SystemCheckPage.class.php
CommitLineData
e87a0df0
AE
1<?php
2namespace wcf\acp\page;
3use wcf\data\application\Application;
4use wcf\page\AbstractPage;
5use wcf\system\exception\SystemException;
c65817a8 6use wcf\system\Regex;
e87a0df0
AE
7use wcf\system\WCF;
8use wcf\util\FileUtil;
9
10/**
e87a0df0 11 * @author Alexander Ebert
7b7b9764 12 * @copyright 2001-2019 WoltLab GmbH
e87a0df0
AE
13 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
14 * @package WoltLabSuite\Core\Acp\Page
15 * @since 5.2
16 */
17class SystemCheckPage extends AbstractPage {
18 /**
19 * @inheritDoc
20 */
21 public $activeMenuItem = 'wcf.acp.menu.link.systemCheck';
22
23 /**
24 * @inheritDoc
25 */
26 public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
27
28 /**
29 * A list of directories that need to be writable at all times, grouped by their application
30 * identifier. Only the directory itself is checked, unless the path ends with `/*`.
31 * @var string[][]
32 */
33 public $directories = [
34 'wcf' => [
35 '/',
36 '/acp/style',
37 '/acp/templates/compiled',
85f05df8 38 '/attachments/*',
e87a0df0
AE
39 '/cache',
40 '/images/*',
41 '/language',
42 '/log',
43 '/media_files/*',
44 '/sitemaps',
45 '/style',
46 '/templates/compiled',
47 '/tmp',
48 ],
49 ];
50
51 public $mysqlVersions = [
52 'mysql' => '5.5.35',
53 'mariadb' => [
54 // MariaDB 5.5.47+ or 10.0.22+ are required
55 // https://jira.mariadb.org/browse/MDEV-8756
56 '5' => '5.5.47',
57 '10' => '10.0.22',
58 ],
59 ];
60
61 public $phpExtensions = [
62 'mbstring',
63 'libxml',
64 'dom',
65 'zlib',
66 'pdo',
67 'pdo_mysql',
68 'json',
69 'pcre',
70 'gd',
71 'hash',
85f05df8 72 'exif',
e87a0df0
AE
73 ];
74
75 public $phpMemoryLimit = 128;
76
77 public $phpVersions = [
78 'minimum' => '7.0.22',
79 'sufficient' => ['7.0'],
80 'recommended' => ['7.1', '7.2', '7.3'],
81 ];
82
c65817a8
JR
83 public $foreignKeys = [
84 'wcf'. WCF_N .'_user' => [
85 'avatarID' => [
86 'referenceTable' => 'wcf'. WCF_N .'_user_avatar',
87 'referenceColumn' => 'avatarID'
88 ]
89 ],
90 'wcf'. WCF_N .'_comment' => [
91 'userID' => [
92 'referenceTable' => 'wcf'. WCF_N .'_user',
93 'referenceColumn' => 'userID'
94 ],
95 'objectTypeID' => [
96 'referenceTable' => 'wcf'. WCF_N .'_object_type',
97 'referenceColumn' => 'objectTypeID'
98 ]
99 ],
100 'wcf'. WCF_N .'_moderation_queue' => [
101 'objectTypeID' => [
102 'referenceTable' => 'wcf'. WCF_N .'_object_type',
103 'referenceColumn' => 'objectTypeID'
104 ],
105 'assignedUserID' => [
106 'referenceTable' => 'wcf'. WCF_N .'_user',
107 'referenceColumn' => 'userID'
108 ],
109 'userID' => [
110 'referenceTable' => 'wcf'. WCF_N .'_user',
111 'referenceColumn' => 'userID'
112 ]
113 ]
114 ];
115
e87a0df0
AE
116 public $results = [
117 'directories' => [],
118 'mysql' => [
119 'innodb' => false,
120 'mariadb' => false,
121 'result' => false,
122 'version' => '0.0.0',
c65817a8 123 'foreignKeys' => false,
e87a0df0
AE
124 ],
125 'php' => [
126 'extension' => [],
127 'memoryLimit' => [
128 'required' => '0',
129 'result' => false,
130 'value' => '0',
131 ],
132 'sha256' => false,
133 'version' => [
134 'result' => 'unsupported',
135 'value' => '0.0.0',
136 ],
137 ],
138 'status' => [
139 'directories' => false,
140 'mysql' => false,
141 'php' => false,
142 ],
143 ];
144
50b7b362
MS
145 /**
146 * indicates that this page is only accessible to owners in enterprise mode
147 */
148 const BLACKLISTED_IN_ENTERPRISE_MODE = true;
149
e87a0df0
AE
150 /**
151 * @inheritDoc
152 */
153 public function readData() {
154 parent::readData();
155
156 if (IMAGE_ADAPTER_TYPE === 'imagick' && !in_array('imagick', $this->phpExtensions)) {
157 $this->phpExtensions[] = 'imagick';
158 }
159
bdf942aa
AE
160 if (CACHE_SOURCE_TYPE === 'memcached' && !in_array('memcached', $this->phpExtensions)) {
161 $this->phpExtensions[] = 'memcached';
162 }
163
164 if (CACHE_SOURCE_TYPE === 'redis' && !in_array('redis', $this->phpExtensions)) {
165 $this->phpExtensions[] = 'redis';
166 }
167
e87a0df0
AE
168 $this->validateMysql();
169 $this->validatePhpExtensions();
170 $this->validatePhpMemoryLimit();
171 $this->validatePhpVersion();
172 $this->validateWritableDirectories();
173 }
174
175 /**
176 * @inheritDoc
177 */
178 public function assignVariables() {
179 parent::assignVariables();
180
181 WCF::getTPL()->assign([
182 'mysqlVersions' => $this->mysqlVersions,
183 'phpExtensions' => $this->phpExtensions,
184 'phpMemoryLimit' => $this->phpMemoryLimit,
185 'phpVersions' => $this->phpVersions,
186 'results' => $this->results,
187 ]);
188 }
189
190 protected function validateMysql() {
191 // check sql version
192 $sqlVersion = WCF::getDB()->getVersion();
193 $compareSQLVersion = preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $sqlVersion);
194 // Do not use the "raw" version, it usually contains a lot of noise.
195 $this->results['mysql']['version'] = $compareSQLVersion;
196 if (stripos($sqlVersion, 'MariaDB') !== false) {
197 $this->results['mysql']['mariadb'] = true;
198
199 // MariaDB has some legacy version that use the major version '5'.
200 if ($compareSQLVersion[0] === '5') {
201 $this->results['mysql']['result'] = (version_compare($compareSQLVersion, $this->mysqlVersions['mariadb']['5']) >= 0);
202 }
203 else {
204 $this->results['mysql']['result'] = (version_compare($compareSQLVersion, $this->mysqlVersions['mariadb']['10']) >= 0);
205 }
206 }
207 else if (version_compare($compareSQLVersion, $this->mysqlVersions['mysql']) >= 0) {
208 $this->results['mysql']['result'] = true;
209 }
210
211 // check innodb support
212 $sql = "SHOW ENGINES";
213 $statement = WCF::getDB()->prepareStatement($sql);
214 $statement->execute();
215 while ($row = $statement->fetchArray()) {
216 if ($row['Engine'] == 'InnoDB' && in_array($row['Support'], ['DEFAULT', 'YES'])) {
217 $this->results['mysql']['innodb'] = true;
218 break;
219 }
220 }
221
c65817a8
JR
222 // validate foreign keys
223 $this->results['mysql']['foreignKeys'] = true;
224 foreach ($this->foreignKeys as $table => $keys) {
225 $sql = "SHOW CREATE TABLE ". $table;
226 $statement = WCF::getDB()->prepareStatement($sql);
227 $statement->execute();
228
229 $command = $statement->fetchSingleColumn(1);
230 foreach ($keys as $column => $reference) {
231 if (!Regex::compile('CONSTRAINT [`"]?(.)*[`"]? FOREIGN KEY \([`"]?'. $column .'[`"]?\) REFERENCES [`"]?'. $reference['referenceTable'] .'[`"]? \([`"]?'. $reference['referenceColumn'] .'[`"]?\)')->match($command)) {
232 $this->results['mysql']['foreignKeys'] = false;
233 break 2;
234 }
235 }
236 }
237
238 if ($this->results['mysql']['result'] && $this->results['mysql']['innodb'] && $this->results['mysql']['foreignKeys']) {
e87a0df0
AE
239 $this->results['status']['mysql'] = true;
240 }
241 }
242
243 protected function validatePhpExtensions() {
244 foreach ($this->phpExtensions as $phpExtension) {
245 $result = extension_loaded($phpExtension);
246 if (!$result) {
247 $this->results['php']['extension'][] = $phpExtension;
248 }
249 }
250
251 if (extension_loaded('hash')) {
252 $this->results['php']['sha256'] = in_array('sha256', hash_algos());
253 }
254
255 $this->results['status']['php'] = empty($this->results['php']['extension']) && $this->results['php']['sha256'];
256 }
257
258 protected function validatePhpMemoryLimit() {
259 $this->results['php']['memoryLimit']['required'] = $this->phpMemoryLimit . 'M';
260
261 $memoryLimit = ini_get('memory_limit');
262
263 // Memory is not limited through PHP.
264 if ($memoryLimit == -1) {
ee7740fc 265 $this->results['php']['memoryLimit']['value'] = "\u{221E}";
e87a0df0
AE
266 $this->results['php']['memoryLimit']['result'] = true;
267 }
268 else {
269 // Completely numeric, PHP assumes this to be a value in bytes.
270 if (is_numeric($memoryLimit)) {
271 $memoryLimit = $memoryLimit / 1024 / 1024;
272
273 $this->results['php']['memoryLimit']['value'] = $memoryLimit . 'M';
274 $this->results['php']['memoryLimit']['result'] = ($memoryLimit >= $this->phpMemoryLimit);
275 }
276 else {
277 // PHP supports the 'K', 'M' and 'G' shorthand notations.
278 if (preg_match('~^(\d+)([KMG])$~', $memoryLimit, $matches)) {
279 switch ($matches[2]) {
280 case 'K':
ee7740fc 281 $memoryLimit = $matches[1] / 1024;
e87a0df0
AE
282
283 $this->results['php']['memoryLimit']['value'] = $memoryLimit . 'M';
284 $this->results['php']['memoryLimit']['result'] = ($memoryLimit >= $this->phpMemoryLimit);
285 break;
286
287 case 'M':
288 $this->results['php']['memoryLimit']['value'] = $memoryLimit;
289 $this->results['php']['memoryLimit']['result'] = ($matches[1] >= $this->phpMemoryLimit);
290 break;
291
292 case 'G':
293 $this->results['php']['memoryLimit']['value'] = $memoryLimit;
294 $this->results['php']['memoryLimit']['result'] = ($matches[1] * 1024 >= $this->phpMemoryLimit);
295 break;
296
297 default:
298 $this->results['php']['memoryLimit']['value'] = $memoryLimit;
299 $this->results['php']['memoryLimit']['result'] = false;
300 return;
301 }
302 }
303 }
304 }
305
306 $this->results['status']['php'] = $this->results['status']['php'] && $this->results['php']['memoryLimit']['result'];
307 }
308
309 protected function validatePhpVersion() {
310 $phpVersion = phpversion();
311 $comparePhpVersion = preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $phpVersion);
312 // Do not use the "raw" version, it usually contains a lot of noise.
313 $this->results['php']['version']['value'] = $comparePhpVersion;
314 if (version_compare($comparePhpVersion, $this->phpVersions['minimum']) >= 0) {
315 $majorMinor = preg_replace('~^(\d+\.\d+).*$~', '\\1', $phpVersion);
316 foreach (['recommended', 'sufficient'] as $type) {
317 foreach ($this->phpVersions[$type] as $version) {
318 if ($majorMinor === $version) {
319 $this->results['php']['version']['result'] = $type;
320 break 2;
321 }
322 }
323 }
324 }
325 else {
326 $this->results['php']['version']['result'] = 'unsupported';
327 }
328
329 $this->results['status']['php'] = $this->results['status']['php'] && ($this->results['php']['version']['result'] !== 'unsupported');
330 }
331
332 protected function validateWritableDirectories() {
333 foreach ($this->directories as $abbreviation => $directories) {
334 $basePath = Application::getDirectory($abbreviation);
335 foreach ($directories as $directory) {
336 $recursive = false;
337 if (preg_match('~(.*)/\*$~', $directory, $matches)) {
338 $recursive = true;
339 $directory = $matches[1];
340 }
341
342 $path = $basePath . FileUtil::removeLeadingSlash(FileUtil::addTrailingSlash($directory));
343 if ($this->checkDirectory($path) && $recursive) {
344 $rdi = new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS);
345 $it = new \RecursiveIteratorIterator($rdi, \RecursiveIteratorIterator::SELF_FIRST);
346 /** @var \SplFileInfo $item */
347 foreach ($it as $item) {
348 if ($item->isDir()) {
349 $this->makeDirectoryWritable($item->getPathname());
350 }
351 }
352 }
353 }
354 }
355
356 $this->results['status']['directories'] = empty($this->results['directories']);
357 }
358
359 protected function checkDirectory($path) {
360 if (!$this->createDirectoryIfNotExists($path)) {
361 $this->results['directories'][] = FileUtil::unifyDirSeparator($path);
362 return false;
363 }
364
365 return $this->makeDirectoryWritable($path);
366 }
367
368 protected function createDirectoryIfNotExists($path) {
369 if (!file_exists($path) && !FileUtil::makePath($path)) {
370 // FileUtil::makePath() returns false if either the directory cannot be created
371 // or if it cannot be made writable.
372 if (!file_exists($path)) {
373 return false;
374 }
375 }
376
377 return true;
378 }
379
380 protected function makeDirectoryWritable($path) {
381 try {
382 FileUtil::makeWritable($path);
383 }
384 catch (SystemException $e) {
385 $this->results['directories'][] = FileUtil::unifyDirSeparator($path);
386 return false;
387 }
388
389 return true;
390 }
391}