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