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