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