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