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