REF - Switch to using new `CRM_Core_Component::isEnabled()`
[civicrm-core.git] / CRM / Utils / Check / Component / Env.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17 class CRM_Utils_Check_Component_Env extends CRM_Utils_Check_Component {
18
19 /**
20 * @return CRM_Utils_Check_Message[]
21 */
22 public function checkPhpVersion() {
23 $messages = [];
24 $phpVersion = phpversion();
25
26 if (version_compare($phpVersion, CRM_Upgrade_Incremental_General::RECOMMENDED_PHP_VER) >= 0) {
27 $messages[] = new CRM_Utils_Check_Message(
28 __FUNCTION__,
29 ts('This system uses PHP version %1 which meets or exceeds the recommendation of %2.',
30 [
31 1 => $phpVersion,
32 2 => preg_replace(';^(\d+\.\d+(?:\.[1-9]\d*)?).*$;', '\1', CRM_Upgrade_Incremental_General::RECOMMENDED_PHP_VER),
33 ]),
34 ts('PHP Up-to-Date'),
35 \Psr\Log\LogLevel::INFO,
36 'fa-server'
37 );
38 }
39 elseif (version_compare($phpVersion, CRM_Upgrade_Incremental_General::MIN_RECOMMENDED_PHP_VER) >= 0) {
40 $messages[] = new CRM_Utils_Check_Message(
41 __FUNCTION__,
42 ts('This system uses PHP version %1. This meets the minimum recommendations and you do not need to upgrade immediately, but the preferred version is %2.',
43 [
44 1 => $phpVersion,
45 2 => CRM_Upgrade_Incremental_General::RECOMMENDED_PHP_VER,
46 ]),
47 ts('PHP Out-of-Date'),
48 \Psr\Log\LogLevel::NOTICE,
49 'fa-server'
50 );
51 }
52 elseif (version_compare($phpVersion, CRM_Upgrade_Incremental_General::MIN_INSTALL_PHP_VER) >= 0) {
53 $messages[] = new CRM_Utils_Check_Message(
54 __FUNCTION__,
55 ts('This system uses PHP version %1. This meets the minimum requirements for CiviCRM to function but is not recommended. At least PHP version %2 is recommended; the preferred version is %3.',
56 [
57 1 => $phpVersion,
58 2 => CRM_Upgrade_Incremental_General::MIN_RECOMMENDED_PHP_VER,
59 3 => preg_replace(';^(\d+\.\d+(?:\.[1-9]\d*)?).*$;', '\1', CRM_Upgrade_Incremental_General::RECOMMENDED_PHP_VER),
60 ]),
61 ts('PHP Out-of-Date'),
62 \Psr\Log\LogLevel::WARNING,
63 'fa-server'
64 );
65 }
66 else {
67 $messages[] = new CRM_Utils_Check_Message(
68 __FUNCTION__,
69 ts('This system uses PHP version %1. To ensure the continued operation of CiviCRM, upgrade your server now. At least PHP version %2 is recommended; the preferred version is %3.',
70 [
71 1 => $phpVersion,
72 2 => CRM_Upgrade_Incremental_General::MIN_RECOMMENDED_PHP_VER,
73 3 => preg_replace(';^(\d+\.\d+(?:\.[1-9]\d*)?).*$;', '\1', CRM_Upgrade_Incremental_General::RECOMMENDED_PHP_VER),
74 ]),
75 ts('PHP Out-of-Date'),
76 \Psr\Log\LogLevel::ERROR,
77 'fa-server'
78 );
79 }
80
81 return $messages;
82 }
83
84 /**
85 * @return CRM_Utils_Check_Message[]
86 */
87 public function checkPhpMysqli() {
88 $messages = [];
89
90 if (!extension_loaded('mysqli')) {
91 $messages[] = new CRM_Utils_Check_Message(
92 __FUNCTION__,
93 ts('Future versions of CiviCRM may require the PHP extension "%2". To ensure that your system will be compatible, please install it in advance. For more explanation, see <a href="%1">the announcement</a>.',
94 [
95 1 => 'https://civicrm.org/blog/totten/psa-please-verify-php-extension-mysqli',
96 2 => 'mysqli',
97 ]),
98 ts('Forward Compatibility: Enable "mysqli"'),
99 \Psr\Log\LogLevel::WARNING,
100 'fa-server'
101 );
102 }
103
104 return $messages;
105 }
106
107 /**
108 * Check that the MySQL time settings match the PHP time settings.
109 *
110 * @return CRM_Utils_Check_Message[]
111 */
112 public function checkMysqlTime() {
113 $messages = [];
114
115 $phpNow = date('Y-m-d H:i');
116 $sqlNow = CRM_Core_DAO::singleValueQuery("SELECT date_format(now(), '%Y-%m-%d %H:%i')");
117 if (!CRM_Utils_Time::isEqual($phpNow, $sqlNow, 2.5 * 60)) {
118 $messages[] = new CRM_Utils_Check_Message(
119 __FUNCTION__,
120 ts('Timestamps reported by MySQL (eg "%1") and PHP (eg "%2" ) are mismatched.', [
121 1 => $sqlNow,
122 2 => $phpNow,
123 ]) . '<br />' . CRM_Utils_System::docURL2('sysadmin/requirements/#mysql-time'),
124 ts('Timestamp Mismatch'),
125 \Psr\Log\LogLevel::ERROR,
126 'fa-server'
127 );
128 }
129
130 return $messages;
131 }
132
133 /**
134 * @return CRM_Utils_Check_Message[]
135 */
136 public function checkDebug() {
137 $config = CRM_Core_Config::singleton();
138 if ($config->debug) {
139 $message = new CRM_Utils_Check_Message(
140 __FUNCTION__,
141 ts('Warning: Debug is enabled in <a href="%1">system settings</a>. This should not be enabled on production servers.',
142 [1 => CRM_Utils_System::url('civicrm/admin/setting/debug', 'reset=1')]),
143 ts('Debug Mode Enabled'),
144 CRM_Core_Config::environment() == 'Production' ? \Psr\Log\LogLevel::WARNING : \Psr\Log\LogLevel::INFO,
145 'fa-bug'
146 );
147 $message->addAction(
148 ts('Disable Debug Mode'),
149 ts('Disable debug mode now?'),
150 'api3',
151 ['Setting', 'create', ['debug_enabled' => 0]]
152 );
153 return [$message];
154 }
155
156 return [];
157 }
158
159 /**
160 * @param bool $force
161 * @return CRM_Utils_Check_Message[]
162 */
163 public function checkOutboundMail($force = FALSE) {
164 $messages = [];
165
166 // CiviMail doesn't work in non-production environments; skip.
167 if (!$force && CRM_Core_Config::environment() != 'Production') {
168 return $messages;
169 }
170
171 $mailingInfo = Civi::settings()->get('mailing_backend');
172 if (($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_REDIRECT_TO_DB
173 || (defined('CIVICRM_MAIL_LOG') && CIVICRM_MAIL_LOG)
174 || $mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_DISABLED
175 || $mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_MOCK)
176 ) {
177 $messages[] = new CRM_Utils_Check_Message(
178 __FUNCTION__,
179 ts('Warning: Outbound email is disabled in <a href="%1">system settings</a>. Proper settings should be enabled on production servers.',
180 [1 => CRM_Utils_System::url('civicrm/admin/setting/smtp', 'reset=1')]),
181 ts('Outbound Email Disabled'),
182 \Psr\Log\LogLevel::WARNING,
183 'fa-envelope'
184 );
185 }
186
187 return $messages;
188 }
189
190 /**
191 * Check that domain email and org name are set
192 * @param bool $force
193 * @return CRM_Utils_Check_Message[]
194 */
195 public function checkDomainNameEmail($force = FALSE) {
196 $messages = [];
197
198 // CiviMail doesn't work in non-production environments; skip.
199 if (!$force && CRM_Core_Config::environment() != 'Production') {
200 return $messages;
201 }
202
203 list($domainEmailName, $domainEmailAddress) = CRM_Core_BAO_Domain::getNameAndEmail(TRUE);
204 $domain = CRM_Core_BAO_Domain::getDomain();
205 $domainName = $domain->name;
206 $fixEmailUrl = CRM_Utils_System::url("civicrm/admin/options/from_email_address", "&reset=1");
207 $fixDomainName = CRM_Utils_System::url("civicrm/admin/domain", "action=update&reset=1");
208
209 if (!$domainEmailAddress || $domainEmailAddress == 'info@EXAMPLE.ORG') {
210 if (!$domainName || $domainName == 'Default Domain Name') {
211 $msg = ts("Please enter your organization's <a href=\"%1\">name, primary address </a> and <a href=\"%2\">default FROM Email Address </a> (for system-generated emails).",
212 [
213 1 => $fixDomainName,
214 2 => $fixEmailUrl,
215 ]
216 );
217 }
218 else {
219 $msg = ts('Please enter a <a href="%1">default FROM Email Address</a> (for system-generated emails).',
220 [1 => $fixEmailUrl]);
221 }
222 }
223 elseif (!$domainName || $domainName == 'Default Domain Name') {
224 $msg = ts("Please enter your organization's <a href=\"%1\">name and primary address</a>.",
225 [1 => $fixDomainName]);
226 }
227
228 if (!empty($msg)) {
229 $messages[] = new CRM_Utils_Check_Message(
230 __FUNCTION__,
231 $msg,
232 ts('Organization Setup'),
233 \Psr\Log\LogLevel::WARNING,
234 'fa-check-square-o'
235 );
236 }
237
238 return $messages;
239 }
240
241 /**
242 * Checks if a default bounce handling mailbox is set up
243 * @param bool $force
244 * @return CRM_Utils_Check_Message[]
245 */
246 public function checkDefaultMailbox($force = FALSE) {
247 $messages = [];
248
249 // CiviMail doesn't work in non-production environments; skip.
250 if (!$force && CRM_Core_Config::environment() != 'Production') {
251 return $messages;
252 }
253
254 if (CRM_Core_Component::isEnabled('CiviMail') &&
255 CRM_Core_BAO_MailSettings::defaultDomain() == "EXAMPLE.ORG"
256 ) {
257 $message = new CRM_Utils_Check_Message(
258 __FUNCTION__,
259 ts('Please configure a <a href="%1">default mailbox</a> for CiviMail.',
260 [1 => CRM_Utils_System::url('civicrm/admin/mailSettings', "reset=1")]),
261 ts('Configure Default Mailbox'),
262 \Psr\Log\LogLevel::WARNING,
263 'fa-envelope'
264 );
265 $message->addHelp(
266 ts('A default mailbox must be configured for email bounce processing.') . '<br />' .
267 CRM_Utils_System::docURL2('user/advanced-configuration/email-system-configuration/')
268 );
269 $messages[] = $message;
270 }
271
272 return $messages;
273 }
274
275 /**
276 * Checks if cron has run in the past hour (3600 seconds)
277 * @param bool $force
278 * @return CRM_Utils_Check_Message[]
279 * @throws CRM_Core_Exception
280 */
281 public function checkLastCron($force = FALSE) {
282 // TODO: Remove this check when MINIMUM_UPGRADABLE_VERSION goes to 4.7.
283 if (CRM_Utils_System::version() !== CRM_Core_BAO_Domain::version()) {
284 return [];
285 }
286
287 $messages = [];
288
289 // Cron doesn't work in non-production environments; skip.
290 if (!$force && CRM_Core_Config::environment() != 'Production') {
291 return $messages;
292 }
293
294 $statusPreference = new CRM_Core_DAO_StatusPreference();
295 $statusPreference->domain_id = CRM_Core_Config::domainID();
296 $statusPreference->name = __FUNCTION__;
297
298 $level = \Psr\Log\LogLevel::INFO;
299 $now = gmdate('U');
300
301 // Get timestamp of last cron run
302 if ($statusPreference->find(TRUE) && !empty($statusPreference->check_info)) {
303 $msg = ts('Last cron run at %1.', [1 => CRM_Utils_Date::customFormat(date('c', $statusPreference->check_info))]);
304 }
305 // If cron record doesn't exist, this is a new install. Make a placeholder record (prefs='new').
306 else {
307 $statusPreference = CRM_Core_BAO_StatusPreference::create([
308 'name' => __FUNCTION__,
309 'check_info' => $now,
310 'prefs' => 'new',
311 ]);
312 }
313 $lastCron = $statusPreference->check_info;
314
315 if ($statusPreference->prefs !== 'new' && $lastCron > $now - 3600) {
316 $title = ts('Cron Running OK');
317 }
318 else {
319 // If placeholder record found, give one day "grace period" for admin to set-up cron
320 if ($statusPreference->prefs === 'new') {
321 $title = ts('Set-up Cron');
322 $msg = ts('No cron runs have been recorded.');
323 // After 1 day (86400 seconds) increase the error level
324 $level = ($lastCron > $now - 86400) ? \Psr\Log\LogLevel::NOTICE : \Psr\Log\LogLevel::WARNING;
325 }
326 else {
327 $title = ts('Cron Not Running');
328 // After 1 day (86400 seconds) increase the error level
329 $level = ($lastCron > $now - 86400) ? \Psr\Log\LogLevel::WARNING : \Psr\Log\LogLevel::ERROR;
330 }
331 $msg .= '<p>' . ts('A cron job is required to execute scheduled jobs automatically.') .
332 '<br />' . CRM_Utils_System::docURL2('sysadmin/setup/jobs/') . '</p>';
333 }
334
335 $messages[] = new CRM_Utils_Check_Message(
336 __FUNCTION__,
337 $msg,
338 $title,
339 $level,
340 'fa-clock-o'
341 );
342 return $messages;
343 }
344
345 /**
346 * Recommend that sites use path-variables for their directories and URLs.
347 * @return CRM_Utils_Check_Message[]
348 */
349 public function checkUrlVariables() {
350 $messages = [];
351 $hasOldStyle = FALSE;
352 $settingNames = [
353 'userFrameworkResourceURL',
354 'imageUploadURL',
355 'customCSSURL',
356 'extensionsURL',
357 ];
358
359 foreach ($settingNames as $settingName) {
360 $settingValue = Civi::settings()->get($settingName);
361 if (!empty($settingValue) && $settingValue[0] != '[') {
362 $hasOldStyle = TRUE;
363 break;
364 }
365 }
366
367 if ($hasOldStyle) {
368 $message = new CRM_Utils_Check_Message(
369 __FUNCTION__,
370 ts('<a href="%1">Resource URLs</a> may use absolute paths, relative paths, or variables. Absolute paths are more difficult to maintain. To maximize portability, consider using a variable in each URL (eg "<tt>[cms.root]</tt>" or "<tt>[civicrm.files]</tt>").',
371 [1 => CRM_Utils_System::url('civicrm/admin/setting/url', "reset=1")]),
372 ts('Resource URLs: Make them portable'),
373 \Psr\Log\LogLevel::NOTICE,
374 'fa-server'
375 );
376 $messages[] = $message;
377 }
378
379 return $messages;
380 }
381
382 /**
383 * Recommend that sites use path-variables for their directories and URLs.
384 * @return CRM_Utils_Check_Message[]
385 */
386 public function checkDirVariables() {
387 $messages = [];
388 $hasOldStyle = FALSE;
389 $settingNames = [
390 'uploadDir',
391 'imageUploadDir',
392 'customFileUploadDir',
393 'customTemplateDir',
394 'customPHPPathDir',
395 'extensionsDir',
396 ];
397
398 foreach ($settingNames as $settingName) {
399 $settingValue = Civi::settings()->get($settingName);
400 if (!empty($settingValue) && $settingValue[0] != '[') {
401 $hasOldStyle = TRUE;
402 break;
403 }
404 }
405
406 if ($hasOldStyle) {
407 $message = new CRM_Utils_Check_Message(
408 __FUNCTION__,
409 ts('<a href="%1">Directories</a> may use absolute paths, relative paths, or variables. Absolute paths are more difficult to maintain. To maximize portability, consider using a variable in each directory (eg "<tt>[cms.root]</tt>" or "<tt>[civicrm.files]</tt>").',
410 [1 => CRM_Utils_System::url('civicrm/admin/setting/path', "reset=1")]),
411 ts('Directory Paths: Make them portable'),
412 \Psr\Log\LogLevel::NOTICE,
413 'fa-server'
414 );
415 $messages[] = $message;
416 }
417
418 return $messages;
419 }
420
421 /**
422 * Check that important directories are writable.
423 *
424 * @return CRM_Utils_Check_Message[]
425 */
426 public function checkDirsWritable() {
427 $notWritable = [];
428
429 $config = CRM_Core_Config::singleton();
430 $directories = [
431 'uploadDir' => ts('Temporary Files Directory'),
432 'imageUploadDir' => ts('Images Directory'),
433 'customFileUploadDir' => ts('Custom Files Directory'),
434 ];
435
436 foreach ($directories as $directory => $label) {
437 $file = CRM_Utils_File::createFakeFile($config->$directory);
438
439 if ($file === FALSE) {
440 $notWritable[] = "$label ({$config->$directory})";
441 }
442 else {
443 $dirWithSlash = CRM_Utils_File::addTrailingSlash($config->$directory);
444 unlink($dirWithSlash . $file);
445 }
446 }
447
448 $messages = [];
449
450 if (!empty($notWritable)) {
451 $messages[] = new CRM_Utils_Check_Message(
452 __FUNCTION__,
453 ts('The %1 is not writable. Please check your file permissions.', [
454 1 => implode(', ', $notWritable),
455 'count' => count($notWritable),
456 'plural' => 'The following directories are not writable: %1. Please check your file permissions.',
457 ]),
458 ts('Directory not writable', [
459 'count' => count($notWritable),
460 'plural' => 'Directories not writable',
461 ]),
462 \Psr\Log\LogLevel::ERROR,
463 'fa-ban'
464 );
465 }
466
467 return $messages;
468 }
469
470 /**
471 * Checks if new versions are available
472 * @param bool $force
473 * @return CRM_Utils_Check_Message[]
474 * @throws CRM_Core_Exception
475 */
476 public function checkVersion($force = FALSE) {
477 $messages = [];
478 try {
479 $vc = new CRM_Utils_VersionCheck();
480 $vc->initialize($force);
481 }
482 catch (Exception $e) {
483 $messages[] = new CRM_Utils_Check_Message(
484 'checkVersionError',
485 ts('Directory %1 is not writable. Please change your file permissions.',
486 [1 => dirname($vc->cacheFile)]),
487 ts('Directory not writable'),
488 \Psr\Log\LogLevel::ERROR,
489 'fa-times-circle-o'
490 );
491 return $messages;
492 }
493
494 // Show a notice if the version_check job is disabled
495 if (!$force && empty($vc->cronJob['is_active'])) {
496 $args = empty($vc->cronJob['id']) ? ['reset' => 1] : ['reset' => 1, 'action' => 'update', 'id' => $vc->cronJob['id']];
497 $messages[] = new CRM_Utils_Check_Message(
498 'checkVersionDisabled',
499 ts('The check for new versions of CiviCRM has been disabled. <a %1>Re-enable the scheduled job</a> to receive important security update notifications.', [1 => 'href="' . CRM_Utils_System::url('civicrm/admin/job', $args) . '"']),
500 ts('Update Check Disabled'),
501 \Psr\Log\LogLevel::NOTICE,
502 'fa-times-circle-o'
503 );
504 }
505
506 if ($vc->isInfoAvailable) {
507 foreach ($vc->getVersionMessages() ?? [] as $msg) {
508 $messages[] = new CRM_Utils_Check_Message(__FUNCTION__ . '_' . $msg['name'],
509 $msg['message'], $msg['title'], $msg['severity'], 'fa-cloud-upload');
510 }
511 }
512
513 return $messages;
514 }
515
516 /**
517 * Checks if extensions are set up properly
518 * @return CRM_Utils_Check_Message[]
519 */
520 public function checkExtensions() {
521 $messages = [];
522 $extensionSystem = CRM_Extension_System::singleton();
523 $mapper = $extensionSystem->getMapper();
524 $manager = $extensionSystem->getManager();
525
526 if ($extensionSystem->getDefaultContainer()) {
527 $basedir = $extensionSystem->getDefaultContainer()->baseDir;
528 }
529
530 if (empty($basedir)) {
531 // no extension directory
532 $messages[] = new CRM_Utils_Check_Message(
533 __FUNCTION__,
534 ts('Your extensions directory is not set. Click <a href="%1">here</a> to set the extensions directory.',
535 [1 => CRM_Utils_System::url('civicrm/admin/setting/path', 'reset=1')]),
536 ts('Directory not writable'),
537 \Psr\Log\LogLevel::NOTICE,
538 'fa-plug'
539 );
540 return $messages;
541 }
542
543 if (!is_dir($basedir)) {
544 $messages[] = new CRM_Utils_Check_Message(
545 __FUNCTION__,
546 ts('Your extensions directory path points to %1, which is not a directory. Please check your file system.',
547 [1 => $basedir]),
548 ts('Extensions directory incorrect'),
549 \Psr\Log\LogLevel::ERROR,
550 'fa-plug'
551 );
552 return $messages;
553 }
554 elseif (!is_writable($basedir)) {
555 $messages[] = new CRM_Utils_Check_Message(
556 __FUNCTION__ . 'Writable',
557 ts('Your extensions directory (%1) is read-only. If you would like to perform downloads or upgrades, then change the file permissions.',
558 [1 => $basedir]),
559 ts('Read-Only Extensions'),
560 \Psr\Log\LogLevel::NOTICE,
561 'fa-plug'
562 );
563 }
564
565 if (empty($extensionSystem->getDefaultContainer()->baseUrl)) {
566 $messages[] = new CRM_Utils_Check_Message(
567 __FUNCTION__ . 'URL',
568 ts('The extensions URL is not properly set. Please go to the <a href="%1">URL setting page</a> and correct it.',
569 [1 => CRM_Utils_System::url('civicrm/admin/setting/url', 'reset=1')]),
570 ts('Extensions url missing'),
571 \Psr\Log\LogLevel::ERROR,
572 'fa-plug'
573 );
574 }
575
576 if (!$extensionSystem->getBrowser()->isEnabled()) {
577 $messages[] = new CRM_Utils_Check_Message(
578 __FUNCTION__,
579 ts('Not checking remote URL for extensions since ext_repo_url is set to false.'),
580 ts('Extensions check disabled'),
581 \Psr\Log\LogLevel::NOTICE,
582 'fa-plug'
583 );
584 return $messages;
585 }
586
587 try {
588 $remotes = $extensionSystem->getBrowser()->getExtensions();
589 }
590 catch (CRM_Extension_Exception $e) {
591 $messages[] = new CRM_Utils_Check_Message(
592 __FUNCTION__,
593 $e->getMessage(),
594 ts('Extension download error'),
595 \Psr\Log\LogLevel::ERROR,
596 'fa-plug'
597 );
598 return $messages;
599 }
600
601 $stauses = $manager->getStatuses();
602 $keys = array_keys($stauses);
603 $enabled = array_keys(array_filter($stauses, function($status) {
604 return $status === CRM_Extension_Manager::STATUS_INSTALLED;
605 }));
606 sort($keys);
607 $updates = $errors = $okextensions = [];
608
609 $extPrettyLabel = function($key) use ($mapper) {
610 // We definitely know a $key, but we may not have a $label.
611 // Which is too bad - because it would be nicer if $label could be the reliable start of the string.
612 $keyFmt = '<code>' . htmlentities($key) . '</code>';
613 try {
614 $info = $mapper->keyToInfo($key);
615 if ($info->label) {
616 return sprintf('"<em>%s</em>" (%s)', htmlentities($info->label), $keyFmt);
617 }
618 }
619 catch (CRM_Extension_Exception $ex) {
620 return "($keyFmt)";
621 }
622 };
623
624 foreach ($keys as $key) {
625 try {
626 $obj = $mapper->keyToInfo($key);
627 }
628 catch (CRM_Extension_Exception $ex) {
629 $errors[] = ts('Failed to read extension (%1). Please refresh the extension list.', [1 => $key]);
630 continue;
631 }
632 $row = CRM_Admin_Page_Extensions::createExtendedInfo($obj);
633 switch ($row['status']) {
634 case CRM_Extension_Manager::STATUS_INSTALLED_MISSING:
635 $errors[] = ts('%1 is installed but missing files.', [1 => $extPrettyLabel($key)]);
636 break;
637
638 case CRM_Extension_Manager::STATUS_INSTALLED:
639 $missingRequirements = array_diff($row['requires'], $enabled);
640 if (!empty($row['requires']) && $missingRequirements) {
641 $errors[] = ts('%1 has a missing dependency on %2', [
642 1 => $extPrettyLabel($key),
643 2 => implode(', ', array_map($extPrettyLabel, $missingRequirements)),
644 'plural' => '%1 has missing dependencies: %2',
645 'count' => count($missingRequirements),
646 ]);
647 }
648 elseif (!empty($remotes[$key]) && version_compare($row['version'], $remotes[$key]->version, '<')) {
649 $updates[] = $row['label'] . ': ' . $mapper->getUpgradeLink($remotes[$key], $row);
650 }
651 else {
652 if (empty($row['label'])) {
653 $okextensions[] = $key;
654 }
655 else {
656 $okextensions[] = ts('%1: Version %2', [
657 1 => $row['label'],
658 2 => $row['version'],
659 ]);
660 }
661 }
662 break;
663 }
664 }
665
666 if (!$okextensions && !$updates && !$errors) {
667 $messages[] = new CRM_Utils_Check_Message(
668 __FUNCTION__ . 'Ok',
669 ts('No extensions installed. <a %1>Browse available extensions</a>.', [
670 1 => 'href="' . CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1') . '"',
671 ]),
672 ts('Extensions'),
673 \Psr\Log\LogLevel::INFO,
674 'fa-plug'
675 );
676 }
677
678 if ($errors) {
679 $messages[] = new CRM_Utils_Check_Message(
680 __FUNCTION__ . 'Error',
681 ts('There is one extension error:', [
682 'count' => count($errors),
683 'plural' => 'There are %count extension errors:',
684 ])
685 . '<ul><li>' . implode('</li><li>', $errors) . '</li></ul>'
686 . ts('To resolve any errors, go to <a %1>Manage Extensions</a>.', [
687 1 => 'href="' . CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1') . '"',
688 ]),
689 ts('Extension Error', ['count' => count($errors), 'plural' => 'Extension Errors']),
690 \Psr\Log\LogLevel::ERROR,
691 'fa-plug'
692 );
693 }
694
695 if ($updates) {
696 $messages[] = new CRM_Utils_Check_Message(
697 __FUNCTION__ . 'Updates',
698 '<ul><li>' . implode('</li><li>', $updates) . '</li></ul>',
699 ts('Extension Update Available', ['plural' => '%count Extension Updates Available', 'count' => count($updates)]),
700 \Psr\Log\LogLevel::WARNING,
701 'fa-plug'
702 );
703 }
704
705 if ($okextensions) {
706 if ($updates || $errors) {
707 $message = ts('1 extension is up-to-date:', ['plural' => '%count extensions are up-to-date:', 'count' => count($okextensions)]);
708 }
709 else {
710 $message = ts('All extensions are up-to-date:');
711 }
712 natcasesort($okextensions);
713 $messages[] = new CRM_Utils_Check_Message(
714 __FUNCTION__ . 'Ok',
715 $message . '<ul><li>' . implode('</li><li>', $okextensions) . '</li></ul>',
716 ts('Extensions'),
717 \Psr\Log\LogLevel::INFO,
718 'fa-plug'
719 );
720 }
721
722 return $messages;
723 }
724
725 /**
726 * @return CRM_Utils_Check_Message[]
727 */
728 public function checkScheduledJobLogErrors() {
729 $jobs = civicrm_api3('Job', 'get', [
730 'sequential' => 1,
731 'return' => ["id", "name", "last_run"],
732 'is_active' => 1,
733 'options' => ['limit' => 0],
734 ]);
735 $html = '';
736 foreach ($jobs['values'] as $job) {
737 $lastExecutionMessage = civicrm_api3('JobLog', 'get', [
738 'sequential' => 1,
739 'return' => ["description"],
740 'job_id' => $job['id'],
741 'options' => ['sort' => "id desc", 'limit' => 1],
742 ])['values'][0]['description'] ?? NULL;
743 if (!empty($lastExecutionMessage) && strpos($lastExecutionMessage, 'Failure') !== FALSE) {
744 $viewLogURL = CRM_Utils_System::url('civicrm/admin/joblog', "jid={$job['id']}&reset=1");
745 $html .= '<tr>
746 <td>' . $job['name'] . ' </td>
747 <td>' . $lastExecutionMessage . '</td>
748 <td>' . $job['last_run'] . '</td>
749 <td><a href="' . $viewLogURL . '">' . ts('View Job Log') . '</a></td>
750 </tr>';
751 }
752 }
753 if (empty($html)) {
754 return [];
755 }
756
757 $message = '<p>' . ts('The following scheduled jobs failed on the last run:') . '</p>
758 <p><table><thead><tr><th>' . ts('Job') . '</th><th>' . ts('Message') . '</th><th>' . ts('Last Run') . '</th><th></th>
759 </tr></thead><tbody>' . $html . '
760 </tbody></table></p>';
761
762 $msg = new CRM_Utils_Check_Message(
763 __FUNCTION__,
764 $message,
765 ts('Scheduled Job Failures'),
766 \Psr\Log\LogLevel::WARNING,
767 'fa-server'
768 );
769 $messages[] = $msg;
770 return $messages;
771 }
772
773 /**
774 * Checks if there are pending extension upgrades.
775 *
776 * @return CRM_Utils_Check_Message[]
777 */
778 public function checkExtensionUpgrades() {
779 if (CRM_Extension_Upgrades::hasPending()) {
780 $message = new CRM_Utils_Check_Message(
781 __FUNCTION__,
782 ts('Extension upgrades should be run as soon as possible.'),
783 ts('Extension Upgrades Pending'),
784 \Psr\Log\LogLevel::ERROR,
785 'fa-plug'
786 );
787 $message->addAction(
788 ts('Run Upgrades'),
789 ts('Run extension upgrades now?'),
790 'href',
791 ['path' => 'civicrm/admin/extensions/upgrade', 'query' => ['reset' => 1, 'destination' => CRM_Utils_System::url('civicrm/a/#/status')]]
792 );
793 return [$message];
794 }
795 return [];
796 }
797
798 /**
799 * Checks if CiviCRM database version is up-to-date
800 * @return CRM_Utils_Check_Message[]
801 */
802 public function checkDbVersion() {
803 $messages = [];
804 $dbVersion = CRM_Core_BAO_Domain::version();
805 $upgradeUrl = CRM_Utils_System::url("civicrm/upgrade", "reset=1");
806
807 if (!$dbVersion) {
808 // if db.ver missing
809 $messages[] = new CRM_Utils_Check_Message(
810 __FUNCTION__,
811 ts('Version information found to be missing in database. You will need to determine the correct version corresponding to your current database state.'),
812 ts('Database Version Missing'),
813 \Psr\Log\LogLevel::ERROR,
814 'fa-database'
815 );
816 }
817 elseif (!CRM_Utils_System::isVersionFormatValid($dbVersion)) {
818 $messages[] = new CRM_Utils_Check_Message(
819 __FUNCTION__,
820 ts('Database is marked with invalid version format. You may want to investigate this before you proceed further.'),
821 ts('Database Version Invalid'),
822 \Psr\Log\LogLevel::ERROR,
823 'fa-database'
824 );
825 }
826 elseif (stripos($dbVersion, 'upgrade')) {
827 // if db.ver indicates a partially upgraded db
828 $messages[] = new CRM_Utils_Check_Message(
829 __FUNCTION__,
830 ts('Database check failed - the database looks to have been partially upgraded. You must reload the database with the backup and try the <a href=\'%1\'>upgrade process</a> again.', [1 => $upgradeUrl]),
831 ts('Database Partially Upgraded'),
832 \Psr\Log\LogLevel::ALERT,
833 'fa-database'
834 );
835 }
836 else {
837 // if db.ver < code.ver, time to upgrade
838 if (CRM_Core_BAO_Domain::isDBUpdateRequired()) {
839 $messages[] = new CRM_Utils_Check_Message(
840 __FUNCTION__,
841 ts('New codebase version detected. You must visit <a href=\'%1\'>upgrade screen</a> to upgrade the database.', [1 => $upgradeUrl]),
842 ts('Database Upgrade Required'),
843 \Psr\Log\LogLevel::ALERT,
844 'fa-database'
845 );
846 }
847
848 // if db.ver > code.ver, sth really wrong
849 $codeVersion = CRM_Utils_System::version();
850 if (version_compare($dbVersion, $codeVersion) > 0) {
851 $messages[] = new CRM_Utils_Check_Message(
852 __FUNCTION__,
853 ts('Your database is marked with an unexpected version number: %1. The v%2 codebase may not be compatible with your database state.
854 You will need to determine the correct version corresponding to your current database state. You may want to revert to the codebase
855 you were using until you resolve this problem.<br/>OR if this is a manual install from git, you might want to fix civicrm-version.php file.',
856 [1 => $dbVersion, 2 => $codeVersion]
857 ),
858 ts('Database In Unexpected Version'),
859 \Psr\Log\LogLevel::ERROR,
860 'fa-database'
861 );
862 }
863 }
864
865 return $messages;
866 }
867
868 /**
869 * Ensure that all CiviCRM tables are InnoDB
870 * @return CRM_Utils_Check_Message[]
871 */
872 public function checkDbEngine() {
873 $messages = [];
874
875 if (CRM_Core_DAO::isDBMyISAM(150)) {
876 $messages[] = new CRM_Utils_Check_Message(
877 __FUNCTION__,
878 ts('Your database is configured to use the MyISAM database engine. CiviCRM requires InnoDB. You will need to convert any MyISAM tables in your database to InnoDB. Using MyISAM tables will result in data integrity issues.'),
879 ts('MyISAM Database Engine'),
880 \Psr\Log\LogLevel::ERROR,
881 'fa-database'
882 );
883 }
884 return $messages;
885 }
886
887 /**
888 * Ensure reply id is set to any default value
889 * @param bool $force
890 * @return CRM_Utils_Check_Message[]
891 */
892 public function checkReplyIdForMailing($force = FALSE) {
893 $messages = [];
894
895 // CiviMail doesn't work in non-production environments; skip.
896 if (!$force && CRM_Core_Config::environment() != 'Production') {
897 return $messages;
898 }
899
900 if (!CRM_Mailing_PseudoConstant::defaultComponent('Reply', '')) {
901 $messages[] = new CRM_Utils_Check_Message(
902 __FUNCTION__,
903 ts('Reply Auto Responder is not set to any default value in <a %1>Headers, Footers, and Automated Messages</a>. This will disable the submit operation on any mailing created from CiviMail.', [1 => 'href="' . CRM_Utils_System::url('civicrm/admin/component', 'reset=1') . '"']),
904 ts('No Default value for Auto Responder.'),
905 \Psr\Log\LogLevel::WARNING,
906 'fa-reply'
907 );
908 }
909 return $messages;
910 }
911
912 /**
913 * Check for required mbstring extension
914 * @return CRM_Utils_Check_Message[]
915 */
916 public function checkMbstring() {
917 $messages = [];
918
919 if (!function_exists('mb_substr')) {
920 $messages[] = new CRM_Utils_Check_Message(
921 __FUNCTION__,
922 ts('The PHP Multibyte String extension is needed for CiviCRM to correctly handle user input among other functionality. Ask your system administrator to install it.'),
923 ts('Missing mbstring Extension'),
924 \Psr\Log\LogLevel::WARNING,
925 'fa-server'
926 );
927 }
928 return $messages;
929 }
930
931 /**
932 * Check if environment is Production.
933 * @return CRM_Utils_Check_Message[]
934 */
935 public function checkEnvironment() {
936 $messages = [];
937
938 $environment = CRM_Core_Config::environment();
939 if ($environment != 'Production') {
940 $messages[] = new CRM_Utils_Check_Message(
941 __FUNCTION__,
942 ts('The environment of this CiviCRM instance is set to \'%1\'. Certain functionality like scheduled jobs has been disabled.', [1 => $environment]),
943 ts('Non-Production Environment'),
944 \Psr\Log\LogLevel::NOTICE,
945 'fa-bug'
946 );
947 }
948 return $messages;
949 }
950
951 /**
952 * Check for utf8mb4 support by MySQL.
953 *
954 * @return CRM_Utils_Check_Message[]
955 */
956 public function checkMysqlUtf8mb4() {
957 $messages = [];
958
959 if (CRM_Core_DAO::getConnection()->phptype != 'mysqli') {
960 return $messages;
961 }
962
963 // Use mysqli_query() to avoid logging an error message.
964 $mb4testTableName = CRM_Utils_SQL_TempTable::build()->setCategory('utf8mb4test')->getName();
965 if (mysqli_query(CRM_Core_DAO::getConnection()->connection, 'CREATE TEMPORARY TABLE ' . $mb4testTableName . ' (id VARCHAR(255), PRIMARY KEY(id(255))) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC ENGINE=INNODB')) {
966 CRM_Core_DAO::executeQuery('DROP TEMPORARY TABLE ' . $mb4testTableName);
967 }
968 else {
969 $messages[] = new CRM_Utils_Check_Message(
970 __FUNCTION__,
971 ts("Future versions of CiviCRM may require MySQL to support utf8mb4 encoding. It is recommended, though not yet required. Please discuss with your server administrator about configuring your MySQL server for utf8mb4. CiviCRM's recommended configurations are in the System Administrator Guide") . '<br />' . CRM_Utils_System::docURL2('sysadmin/requirements/#mysql-configuration'),
972 ts('MySQL Emoji Support (utf8mb4)'),
973 \Psr\Log\LogLevel::WARNING,
974 'fa-database'
975 );
976 }
977 // Ensure that the MySQL driver supports utf8mb4 encoding.
978 $version = mysqli_get_client_info();
979 if (strpos($version, 'mysqlnd') !== FALSE) {
980 // The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
981 $version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
982 if (version_compare($version, '5.0.9', '<')) {
983 $messages[] = new CRM_Utils_Check_Message(
984 __FUNCTION__ . 'mysqlnd',
985 ts('It is recommended, though not yet required, to upgrade your PHP MySQL driver (mysqlnd) to >= 5.0.9 for utf8mb4 support.'),
986 ts('PHP MySQL Driver (mysqlnd)'),
987 \Psr\Log\LogLevel::WARNING,
988 'fa-server'
989 );
990 }
991 }
992 else {
993 // The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
994 if (version_compare($version, '5.5.3', '<')) {
995 $messages[] = new CRM_Utils_Check_Message(
996 __FUNCTION__ . 'libmysqlclient',
997 ts('It is recommended, though not yet required, to upgrade your PHP MySQL driver (libmysqlclient) to >= 5.5.3 for utf8mb4 support.'),
998 ts('PHP MySQL Driver (libmysqlclient)'),
999 \Psr\Log\LogLevel::WARNING,
1000 'fa-server'
1001 );
1002 }
1003 }
1004
1005 return $messages;
1006 }
1007
1008 public function checkMysqlVersion() {
1009 $messages = [];
1010 $version = CRM_Utils_SQL::getDatabaseVersion();
1011 $minRecommendedVersion = CRM_Upgrade_Incremental_General::MIN_RECOMMENDED_MYSQL_VER;
1012 $mariaDbRecommendedVersion = '10.1';
1013 $upcomingCiviChangeVersion = '5.34';
1014 if (version_compare(CRM_Utils_SQL::getDatabaseVersion(), $minRecommendedVersion, '<')) {
1015 $messages[] = new CRM_Utils_Check_Message(
1016 __FUNCTION__,
1017 ts('To prepare for CiviCRM v%4, please upgrade MySQL. The recommended version will be MySQL v%2 or MariaDB v%3.', [
1018 1 => $version,
1019 2 => $minRecommendedVersion . '+',
1020 3 => $mariaDbRecommendedVersion . '+',
1021 4 => $upcomingCiviChangeVersion . '+',
1022 ]),
1023 ts('MySQL Out-of-Date'),
1024 \Psr\Log\LogLevel::NOTICE,
1025 'fa-server'
1026 );
1027 }
1028 return $messages;
1029 }
1030
1031 public function checkPHPIntlExists() {
1032 $messages = [];
1033 if (!extension_loaded('intl')) {
1034 $messages[] = new CRM_Utils_Check_Message(
1035 __FUNCTION__,
1036 ts('This system currently does not have the PHP-Intl extension enabled. Please contact your system administrator about getting the extension enabled.'),
1037 ts('Missing PHP Extension: INTL'),
1038 \Psr\Log\LogLevel::WARNING,
1039 'fa-server'
1040 );
1041 }
1042 return $messages;
1043 }
1044
1045 /**
1046 * Ensure that the CMS is providing a supported timezone.
1047 *
1048 * @return CRM_Utils_Check_Message[]
1049 */
1050 public function checkUFTimezoneValid() {
1051 $messages = [];
1052 $check_tz = CRM_Core_Config::singleton()->userSystem->getTimeZoneString();
1053
1054 if (!array_key_exists($check_tz, CRM_Core_SelectValues::timezone())) {
1055 $messages[] = new CRM_Utils_Check_Message(
1056 __FUNCTION__,
1057 ts('This system has an invalid timezone set. Please verify that your CMS has a timezone configured that is listed under the <a href="%1">PHP List of Supported Timezones</a>.', [1 => 'https://www.php.net/manual/en/timezones.php']),
1058 ts('Missing or Invalid Timezone'),
1059 \Psr\Log\LogLevel::ERROR,
1060 'fa-clock-o'
1061 );
1062 }
1063
1064 return $messages;
1065 }
1066
1067 }