NFC - Short array syntax - auto-convert Civi dir
[civicrm-core.git] / Civi / Install / Requirements.php
1 <?php
2
3 namespace Civi\Install;
4
5 /**
6 * Class Requirements
7 * @package Civi\Install
8 */
9 class Requirements {
10
11 /**
12 * Requirement severity -- Requirement successfully met.
13 */
14 const REQUIREMENT_OK = 0;
15
16 /**
17 * Requirement severity -- Warning condition; proceed but flag warning.
18 */
19 const REQUIREMENT_WARNING = 1;
20
21 /**
22 * Requirement severity -- Error condition; abort installation.
23 */
24 const REQUIREMENT_ERROR = 2;
25
26 protected $system_checks = [
27 'checkMemory',
28 'checkServerVariables',
29 'checkMysqlConnectExists',
30 'checkJsonEncodeExists',
31 'checkMultibyteExists',
32 ];
33
34 protected $database_checks = [
35 'checkMysqlConnection',
36 'checkMysqlVersion',
37 'checkMysqlInnodb',
38 'checkMysqlTempTables',
39 'checkMySQLAutoIncrementIncrementOne',
40 'checkMysqlTrigger',
41 'checkMysqlThreadStack',
42 'checkMysqlLockTables',
43 'checkMysqlUtf8mb4',
44 ];
45
46 /**
47 * Run all requirements tests.
48 *
49 * @param array $config
50 * An array with two keys:
51 * - file_paths
52 * - db_config
53 *
54 * @return array
55 * An array of check summaries. Each array contains the keys 'title', 'severity', and 'details'.
56 */
57 public function checkAll(array $config) {
58 return array_merge($this->checkSystem($config['file_paths']), $this->checkDatabase($config['db_config']));
59 }
60
61 /**
62 * Check system requirements are met, such as sufficient memory,
63 * necessary file paths are writable and required php extensions
64 * are available.
65 *
66 * @param array $file_paths
67 * An array of file paths that will be checked to confirm they
68 * are writable.
69 *
70 * @return array
71 */
72 public function checkSystem(array $file_paths) {
73 $errors = [];
74
75 $errors[] = $this->checkFilepathIsWritable($file_paths);
76 foreach ($this->system_checks as $check) {
77 $errors[] = $this->$check();
78 }
79
80 return $errors;
81 }
82
83 /**
84 * Check database connection, database version and other
85 * database requirements are met.
86 *
87 * @param array $db_config
88 * An array with keys:
89 * - host (with optional port specified eg. localhost:12345)
90 * - database (name of database to select)
91 * - username
92 * - password
93 *
94 * @return array
95 */
96 public function checkDatabase(array $db_config) {
97 $errors = [];
98
99 foreach ($this->database_checks as $check) {
100 $errors[] = $this->$check($db_config);
101 }
102
103 return $errors;
104 }
105
106 /**
107 * Generates a mysql connection
108 *
109 * @param $db_confic array
110 * @return object mysqli connection
111 */
112 protected function connect($db_config) {
113 $host = NULL;
114 if (!empty($db_config['host'])) {
115 $host = $db_config['host'];
116 }
117 elseif (!empty($db_config['server'])) {
118 $host = $db_config['server'];
119 }
120 $conn = @mysqli_connect($host, $db_config['username'], $db_config['password'], $db_config['database'], !empty($db_config['port']) ? $db_config['port'] : NULL);
121 return $conn;
122 }
123
124 /**
125 * Check configured php Memory.
126 * @return array
127 */
128 public function checkMemory() {
129 $min = 1024 * 1024 * 32;
130 $recommended = 1024 * 1024 * 64;
131
132 $mem = $this->getPHPMemory();
133 $mem_string = ini_get('memory_limit');
134
135 $results = [
136 'title' => 'CiviCRM memory check',
137 'severity' => $this::REQUIREMENT_OK,
138 'details' => "You have $mem_string allocated (minimum 32Mb, recommended 64Mb)",
139 ];
140
141 if ($mem < $min && $mem > 0) {
142 $results['severity'] = $this::REQUIREMENT_ERROR;
143 }
144 elseif ($mem < $recommended && $mem != 0) {
145 $results['severity'] = $this::REQUIREMENT_WARNING;
146 }
147 elseif ($mem == 0) {
148 $results['details'] = "Cannot determine PHP memory allocation. Install only if you're sure you've allocated at least 32 MB.";
149 $results['severity'] = $this::REQUIREMENT_WARNING;
150 }
151
152 return $results;
153 }
154
155 /**
156 * Get Configured PHP memory.
157 * @return float
158 */
159 protected function getPHPMemory() {
160 $memString = ini_get("memory_limit");
161
162 switch (strtolower(substr($memString, -1))) {
163 case "k":
164 return round(substr($memString, 0, -1) * 1024);
165
166 case "m":
167 return round(substr($memString, 0, -1) * 1024 * 1024);
168
169 case "g":
170 return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
171
172 default:
173 return round($memString);
174 }
175 }
176
177 /**
178 * @return array
179 */
180 public function checkServerVariables() {
181 $results = [
182 'title' => 'CiviCRM PHP server variables',
183 'severity' => $this::REQUIREMENT_OK,
184 'details' => 'The required $_SERVER variables are set',
185 ];
186
187 $required_variables = ['SCRIPT_NAME', 'HTTP_HOST', 'SCRIPT_FILENAME'];
188 $missing = [];
189
190 foreach ($required_variables as $required_variable) {
191 if (empty($_SERVER[$required_variable])) {
192 $missing[] = '$_SERVER[' . $required_variable . ']';
193 }
194 }
195
196 if ($missing) {
197 $results['severity'] = $this::REQUIREMENT_ERROR;
198 $results['details'] = 'The following PHP variables are not set: ' . implode(', ', $missing);
199 }
200
201 return $results;
202 }
203
204 /**
205 * @return array
206 */
207 public function checkJsonEncodeExists() {
208 $results = [
209 'title' => 'CiviCRM JSON encoding support',
210 'severity' => $this::REQUIREMENT_OK,
211 'details' => 'Function json_encode() found',
212 ];
213 if (!function_exists('json_encode')) {
214 $results['severity'] = $this::REQUIREMENT_ERROR;
215 $results['details'] = 'Function json_encode() does not exist';
216 }
217
218 return $results;
219 }
220
221 /**
222 * CHeck that PHP Multibyte functions are enabled.
223 * @return array
224 */
225 public function checkMultibyteExists() {
226 $results = [
227 'title' => 'CiviCRM MultiByte encoding support',
228 'severity' => $this::REQUIREMENT_OK,
229 'details' => 'PHP Multibyte etension found',
230 ];
231 if (!function_exists('mb_substr')) {
232 $results['severity'] = $this::REQUIREMENT_ERROR;
233 $results['details'] = 'PHP Multibyte extension has not been installed and enabled';
234 }
235
236 return $results;
237 }
238
239 /**
240 * @return array
241 */
242 public function checkMysqlConnectExists() {
243 $results = [
244 'title' => 'CiviCRM MySQL check',
245 'severity' => $this::REQUIREMENT_OK,
246 'details' => 'Function mysqli_connect() found',
247 ];
248 if (!function_exists('mysqli_connect')) {
249 $results['severity'] = $this::REQUIREMENT_ERROR;
250 $results['details'] = 'Function mysqli_connect() does not exist';
251 }
252
253 return $results;
254 }
255
256 /**
257 * @param array $db_config
258 *
259 * @return array
260 */
261 public function checkMysqlConnection(array $db_config) {
262 $results = [
263 'title' => 'CiviCRM MySQL connection',
264 'severity' => $this::REQUIREMENT_OK,
265 'details' => "Connected",
266 ];
267
268 $conn = $this->connect($db_config);
269
270 if (!$conn) {
271 $results['details'] = mysqli_connect_error();
272 $results['severity'] = $this::REQUIREMENT_ERROR;
273 return $results;
274 }
275
276 if (!@mysqli_select_db($conn, $db_config['database'])) {
277 $results['details'] = mysqli_error($conn);
278 $results['severity'] = $this::REQUIREMENT_ERROR;
279 return $results;
280 }
281
282 return $results;
283 }
284
285 /**
286 * @param array $db_config
287 *
288 * @return array
289 */
290 public function checkMysqlVersion(array $db_config) {
291 $min = '5.1';
292 $results = [
293 'title' => 'CiviCRM MySQL Version',
294 'severity' => $this::REQUIREMENT_OK,
295 ];
296
297 $conn = $this->connect($db_config);
298 if (!$conn || !($info = mysqli_get_server_info($conn))) {
299 $results['severity'] = $this::REQUIREMENT_WARNING;
300 $results['details'] = "Cannot determine the version of MySQL installed. Please ensure at least version {$min} is installed.";
301 return $results;
302 }
303
304 if (version_compare($info, $min) == -1) {
305 $results['severity'] = $this::REQUIREMENT_ERROR;
306 $results['details'] = "MySQL version is {$info}; minimum required is {$min}";
307 return $results;
308 }
309
310 $results['details'] = "MySQL version is {$info}";
311 return $results;
312 }
313
314 /**
315 * @param array $db_config
316 *
317 * @return array
318 */
319 public function checkMysqlInnodb(array $db_config) {
320 $results = [
321 'title' => 'CiviCRM InnoDB support',
322 'severity' => $this::REQUIREMENT_ERROR,
323 'details' => 'Could not determine if MySQL has InnoDB support. Assuming none.',
324 ];
325
326 $conn = $this->connect($db_config);
327 if (!$conn) {
328 return $results;
329 }
330
331 $innodb_support = FALSE;
332 $result = mysqli_query($conn, "SHOW ENGINES");
333 while ($values = mysqli_fetch_array($result)) {
334 if ($values['Engine'] == 'InnoDB') {
335 if (strtolower($values['Support']) == 'yes' || strtolower($values['Support']) == 'default') {
336 $innodb_support = TRUE;
337 break;
338 }
339 }
340 }
341
342 if ($innodb_support) {
343 $results['severity'] = $this::REQUIREMENT_OK;
344 $results['details'] = 'MySQL supports InnoDB';
345 }
346 return $results;
347 }
348
349 /**
350 * @param array $db_config
351 *
352 * @return array
353 */
354 public function checkMysqlTempTables(array $db_config) {
355 $results = [
356 'title' => 'CiviCRM MySQL Temp Tables',
357 'severity' => $this::REQUIREMENT_OK,
358 'details' => 'MySQL server supports temporary tables',
359 ];
360
361 $conn = $this->connect($db_config);
362 if (!$conn) {
363 $results['severity'] = $this::REQUIREMENT_ERROR;
364 $results['details'] = "Could not connect to database";
365 return $results;
366 }
367
368 if (!@mysqli_select_db($conn, $db_config['database'])) {
369 $results['severity'] = $this::REQUIREMENT_ERROR;
370 $results['details'] = "Could not select the database";
371 return $results;
372 }
373
374 $r = mysqli_query($conn, 'CREATE TEMPORARY TABLE civicrm_install_temp_table_test (test text)');
375 if (!$r) {
376 $results['severity'] = $this::REQUIREMENT_ERROR;
377 $results['details'] = "Database does not support creation of temporary tables";
378 return $results;
379 }
380
381 mysqli_query($conn, 'DROP TEMPORARY TABLE civicrm_install_temp_table_test');
382 return $results;
383 }
384
385 /**
386 * @param $db_config
387 *
388 * @return array
389 */
390 public function checkMysqlTrigger($db_config) {
391 $results = [
392 'title' => 'CiviCRM MySQL Trigger',
393 'severity' => $this::REQUIREMENT_OK,
394 'details' => 'Database supports MySQL triggers',
395 ];
396
397 $conn = $this->connect($db_config);
398 if (!$conn) {
399 $results['severity'] = $this::REQUIREMENT_ERROR;
400 $results['details'] = 'Could not connect to database';
401 return $results;
402 }
403
404 if (!@mysqli_select_db($conn, $db_config['database'])) {
405 $results['severity'] = $this::REQUIREMENT_ERROR;
406 $results['details'] = "Could not select the database";
407 return $results;
408 }
409
410 $r = mysqli_query($conn, 'CREATE TABLE civicrm_install_temp_table_test (test text)');
411 if (!$r) {
412 $results['severity'] = $this::REQUIREMENT_ERROR;
413 $results['details'] = 'Could not create a table to run test';
414 return $results;
415 }
416
417 $r = mysqli_query($conn, 'CREATE TRIGGER civicrm_install_temp_table_test_trigger BEFORE INSERT ON civicrm_install_temp_table_test FOR EACH ROW BEGIN END');
418 if (!$r) {
419 $results['severity'] = $this::REQUIREMENT_ERROR;
420 $results['details'] = 'Database does not support creation of triggers';
421 }
422 else {
423 mysqli_query($conn, 'DROP TRIGGER civicrm_install_temp_table_test_trigger');
424 }
425
426 mysqli_query($conn, 'DROP TABLE civicrm_install_temp_table_test');
427 return $results;
428 }
429
430 /**
431 * @param array $db_config
432 *
433 * @return array
434 */
435 public function checkMySQLAutoIncrementIncrementOne(array $db_config) {
436 $results = [
437 'title' => 'CiviCRM MySQL AutoIncrementIncrement',
438 'severity' => $this::REQUIREMENT_OK,
439 'details' => 'MySQL server auto_increment_increment is 1',
440 ];
441
442 $conn = $this->connect($db_config);
443 if (!$conn) {
444 $results['severity'] = $this::REQUIREMENT_ERROR;
445 $results['details'] = 'Could not connect to database';
446 return $results;
447 }
448
449 $r = mysqli_query($conn, "SHOW variables like 'auto_increment_increment'");
450 if (!$r) {
451 $results['severity'] = $this::REQUIREMENT_ERROR;
452 $results['details'] = 'Could not query database server variables';
453 return $results;
454 }
455
456 $values = mysqli_fetch_row($r);
457 if ($values[1] != 1) {
458 $results['severity'] = $this::REQUIREMENT_ERROR;
459 $results['details'] = 'MySQL server auto_increment_increment is not 1';
460 }
461 return $results;
462 }
463
464 /**
465 * @param $db_config
466 *
467 * @return array
468 */
469 public function checkMysqlThreadStack($db_config) {
470 $min_thread_stack = 192;
471
472 $results = [
473 'title' => 'CiviCRM Mysql thread stack',
474 'severity' => $this::REQUIREMENT_OK,
475 'details' => 'MySQL thread_stack is OK',
476 ];
477
478 $conn = $this->connect($db_config);
479 if (!$conn) {
480 $results['severity'] = $this::REQUIREMENT_ERROR;
481 $results['details'] = 'Could not connect to database';
482 return $results;
483 }
484
485 if (!@mysqli_select_db($conn, $db_config['database'])) {
486 $results['severity'] = $this::REQUIREMENT_ERROR;
487 $results['details'] = 'Could not select the database';
488 return $results;
489 }
490
491 $r = mysqli_query($conn, "SHOW VARIABLES LIKE 'thread_stack'"); // bytes => kb
492 if (!$r) {
493 $results['severity'] = $this::REQUIREMENT_ERROR;
494 $results['details'] = 'Could not query thread_stack value';
495 }
496 else {
497 $values = mysqli_fetch_row($r);
498 if ($values[1] < (1024 * $min_thread_stack)) {
499 $results['severity'] = $this::REQUIREMENT_ERROR;
500 $results['details'] = 'MySQL thread_stack is ' . ($values[1] / 1024) . "kb (minimum required is {$min_thread_stack} kb";
501 }
502 }
503
504 return $results;
505 }
506
507 /**
508 * @param $db_config
509 *
510 * @return array
511 */
512 public function checkMysqlLockTables($db_config) {
513 $results = [
514 'title' => 'CiviCRM MySQL Lock Tables',
515 'severity' => $this::REQUIREMENT_OK,
516 'details' => 'Can successfully lock and unlock tables',
517 ];
518
519 $conn = $this->connect($db_config);
520 if (!$conn) {
521 $results['severity'] = $this::REQUIREMENT_ERROR;
522 $results['details'] = 'Could not connect to database';
523 return $results;
524 }
525
526 if (!@mysqli_select_db($conn, $db_config['database'])) {
527 $results['severity'] = $this::REQUIREMENT_ERROR;
528 $results['details'] = 'Could not select the database';
529 mysqli_close($conn);
530 return $results;
531 }
532
533 $r = mysqli_query($conn, 'CREATE TEMPORARY TABLE civicrm_install_temp_table_test (test text)');
534 if (!$r) {
535 $results['severity'] = $this::REQUIREMENT_ERROR;
536 $results['details'] = 'Could not create a table';
537 mysqli_close($conn);
538 return $results;
539 }
540
541 $r = mysqli_query($conn, 'LOCK TABLES civicrm_install_temp_table_test WRITE');
542 if (!$r) {
543 $results['severity'] = $this::REQUIREMENT_ERROR;
544 $results['details'] = 'Could not obtain a write lock';
545 mysqli_close($conn);
546 return $results;
547 }
548
549 $r = mysqli_query($conn, 'UNLOCK TABLES');
550 if (!$r) {
551 $results['severity'] = $this::REQUIREMENT_ERROR;
552 $results['details'] = 'Could not release table lock';
553 }
554
555 mysqli_close($conn);
556 return $results;
557 }
558
559 /**
560 * @param $file_paths
561 *
562 * @return array
563 */
564 public function checkFilepathIsWritable($file_paths) {
565 $results = [
566 'title' => 'CiviCRM directories are writable',
567 'severity' => $this::REQUIREMENT_OK,
568 'details' => 'All required directories are writable: ' . implode(', ', $file_paths),
569 ];
570
571 $unwritable_dirs = [];
572 foreach ($file_paths as $path) {
573 if (!is_writable($path)) {
574 $unwritable_dirs[] = $path;
575 }
576 }
577
578 if ($unwritable_dirs) {
579 $results['severity'] = $this::REQUIREMENT_ERROR;
580 $results['details'] = "The following directories need to be made writable by the webserver: " . implode(', ', $unwritable_dirs);
581 }
582
583 return $results;
584 }
585
586 /**
587 * @param $db_config
588 *
589 * @return array
590 */
591 public function checkMysqlUtf8mb4($db_config) {
592 $results = [
593 'title' => 'CiviCRM MySQL utf8mb4 Support',
594 'severity' => $this::REQUIREMENT_OK,
595 'details' => 'Your system supports the MySQL utf8mb4 character set.',
596 ];
597
598 $conn = $this->connect($db_config);
599 if (!$conn) {
600 $results['severity'] = $this::REQUIREMENT_ERROR;
601 $results['details'] = 'Could not connect to database';
602 return $results;
603 }
604
605 if (!@mysqli_select_db($conn, $db_config['database'])) {
606 $results['severity'] = $this::REQUIREMENT_ERROR;
607 $results['details'] = 'Could not select the database';
608 mysqli_close($conn);
609 return $results;
610 }
611
612 $r = mysqli_query($conn, 'CREATE TABLE civicrm_utf8mb4_test (id VARCHAR(255), PRIMARY KEY(id(255))) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC ENGINE=INNODB');
613 if (!$r) {
614 $results['severity'] = $this::REQUIREMENT_WARNING;
615 $results['details'] = 'It is recommended, though not yet required, to configure your MySQL server for utf8mb4 support. You will need the following MySQL server configuration: innodb_large_prefix=true innodb_file_format=barracuda innodb_file_per_table=true';
616 mysqli_close($conn);
617 return $results;
618 }
619 mysqli_query('DROP TABLE civicrm_utf8mb4_test');
620
621 // Ensure that the MySQL driver supports utf8mb4 encoding.
622 $version = mysqli_get_client_info($conn);
623 if (strpos($version, 'mysqlnd') !== FALSE) {
624 // The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
625 $version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
626 if (version_compare($version, '5.0.9', '<')) {
627 $results['severity'] = $this::REQUIREMENT_WARNING;
628 $results['details'] = 'It is recommended, though not yet required, to upgrade your PHP MySQL driver (mysqlnd) to >= 5.0.9 for utf8mb4 support.';
629 mysqli_close($conn);
630 return $results;
631 }
632 }
633 else {
634 // The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
635 if (version_compare($version, '5.5.3', '<')) {
636 $results['severity'] = $this::REQUIREMENT_WARNING;
637 $results['details'] = 'It is recommended, though not yet required, to upgrade your PHP MySQL driver (libmysqlclient) to >= 5.5.3 for utf8mb4 support.';
638 mysqli_close($conn);
639 return $results;
640 }
641 }
642
643 mysqli_close($conn);
644 return $results;
645 }
646
647 }