3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 class CRM_Upgrade_Form
extends CRM_Core_Form
{
20 const QUEUE_NAME
= 'CRM_Upgrade';
23 * Minimum size of MySQL's thread_stack option
25 * @see install/index.php MINIMUM_THREAD_STACK
27 const MINIMUM_THREAD_STACK
= 192;
30 * Minimum previous CiviCRM version we can directly upgrade from
32 const MINIMUM_UPGRADABLE_VERSION
= '4.2.9';
35 * Minimum php version required to run (equal to or lower than the minimum install version)
37 * As of Civi 5.16, using PHP 5.x will lead to a hard crash during bootstrap.
39 * Tip: Keep in sync with composer.json ("config => platform => php")
41 const MINIMUM_PHP_VERSION
= '7.1.0';
44 * @var \CRM_Core_Config
49 * Upgrade for multilingual.
53 public $multilingual = FALSE;
56 * Locales available for multilingual upgrade.
63 * Constructor for the basic form page.
65 * We should not use QuickForm directly. This class provides a lot
66 * of default convenient functions, rules and buttons
68 * @param object $state
69 * State associated with this form.
70 * @param const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
71 * @param string $method
72 * The type of http method used (GET/POST).
74 * The name of the form if different from class name.
76 public function __construct(
78 $action = CRM_Core_Action
::NONE
,
82 $this->_config
= CRM_Core_Config
::singleton();
84 $domain = new CRM_Core_DAO_Domain();
87 $this->multilingual
= (bool) $domain->locales
;
88 $this->locales
= explode(CRM_Core_DAO
::VALUE_SEPARATOR
, $domain->locales
);
90 $smarty = CRM_Core_Smarty
::singleton();
91 //$smarty->compile_dir = $this->_config->templateCompileDir;
92 $smarty->assign('multilingual', $this->multilingual
);
93 $smarty->assign('locales', $this->locales
);
95 // we didn't call CRM_Core_BAO_ConfigSetting::retrieve(), so we need to set $dbLocale by hand
96 if ($this->multilingual
) {
98 $dbLocale = "_{$this->_config->lcMessages}";
101 parent
::__construct($state, $action, $method, $name);
109 public static function &incrementalPhpObject($version) {
110 static $incrementalPhpObject = [];
112 $versionParts = explode('.', $version);
113 $versionName = CRM_Utils_EnglishNumber
::toCamelCase($versionParts[0]) . CRM_Utils_EnglishNumber
::toCamelCase($versionParts[1]);
115 if (!array_key_exists($versionName, $incrementalPhpObject)) {
116 $className = "CRM_Upgrade_Incremental_php_{$versionName}";
117 $incrementalPhpObject[$versionName] = new $className();
119 return $incrementalPhpObject[$versionName];
128 public function checkVersionRelease($version, $release) {
129 $versionParts = explode('.', $version);
130 return ($versionParts[2] == $release);
134 * @param $constraints
138 public function checkSQLConstraints(&$constraints) {
140 foreach ($constraints as $constraint) {
141 if ($this->checkSQLConstraint($constraint)) {
147 return [$pass, $fail];
156 public function checkSQLConstraint($constraint) {
157 // check constraint here
162 * @param string $fileName
163 * @param bool $isQueryString
165 public function source($fileName, $isQueryString = FALSE) {
166 if ($isQueryString) {
167 CRM_Utils_File
::runSqlQuery($this->_config
->dsn
,
172 CRM_Utils_File
::sourceSQLFile($this->_config
->dsn
,
178 public function preProcess() {
179 CRM_Utils_System
::setTitle($this->getTitle());
180 if (!$this->verifyPreDBState($errorMessage)) {
181 if (!isset($errorMessage)) {
182 $errorMessage = 'pre-condition failed for current upgrade step';
184 CRM_Core_Error
::fatal($errorMessage);
186 $this->assign('recentlyViewed', FALSE);
189 public function buildQuickForm() {
190 $this->addDefaultButtons($this->getButtonTitle(),
198 * Getter function for title. Should be over-ridden by derived class
206 public function getTitle() {
207 return ts('Title not Set');
213 public function getFieldsetTitle() {
220 public function getButtonTitle() {
221 return ts('Continue');
225 * Use the form name to create the tpl file name.
233 public function getTemplateFileName() {
234 $this->assign('title',
235 $this->getFieldsetTitle()
237 $this->assign('message',
238 $this->getTemplateMessage()
240 return 'CRM/Upgrade/Base.tpl';
243 public function postProcess() {
246 if (!$this->verifyPostDBState($errorMessage)) {
247 if (!isset($errorMessage)) {
248 $errorMessage = 'post-condition failed for current upgrade step';
250 CRM_Core_Error
::fatal($errorMessage);
259 public function runQuery($query) {
260 return CRM_Core_DAO
::executeQuery($query);
268 public function setVersion($version) {
269 $this->logVersion($version);
272 UPDATE civicrm_domain
273 SET version = '$version'
275 return $this->runQuery($query);
283 public function logVersion($newVersion) {
285 $oldVersion = CRM_Core_BAO_Domain
::version();
287 $session = CRM_Core_Session
::singleton();
289 'entity_table' => 'civicrm_domain',
291 'data' => "upgrade:{$oldVersion}->{$newVersion}",
292 // lets skip 'modified_id' for now, as it causes FK issues And
293 // is not very important for now.
294 'modified_date' => date('YmdHis'),
296 CRM_Core_BAO_Log
::add($logParams);
308 public function checkVersion($version) {
309 $domainID = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_Domain',
313 return (bool) $domainID;
320 public function getRevisionSequence() {
322 $sqlDir = implode(DIRECTORY_SEPARATOR
,
323 [dirname(__FILE__
), 'Incremental', 'sql']
325 $sqlFiles = scandir($sqlDir);
327 $sqlFilePattern = '/^((\d{1,2}\.\d{1,2})\.(\d{1,2}\.)?(\d{1,2}|\w{4,7}))\.(my)?sql(\.tpl)?$/i';
328 foreach ($sqlFiles as $file) {
329 if (preg_match($sqlFilePattern, $file, $matches)) {
330 if (!in_array($matches[1], $revList)) {
331 $revList[] = $matches[1];
336 usort($revList, 'version_compare');
346 public static function getRevisionPart($rev, $index = 1) {
347 $revPattern = '/^((\d{1,2})\.\d{1,2})\.(\d{1,2}|\w{4,7})?$/i';
348 preg_match($revPattern, $rev, $matches);
350 return array_key_exists($index, $matches) ?
$matches[$index] : NULL;
359 public function processLocales($tplFile, $rev) {
360 $smarty = CRM_Core_Smarty
::singleton();
361 $smarty->assign('domainID', CRM_Core_Config
::domainID());
363 $this->source($smarty->fetch($tplFile), TRUE);
365 if ($this->multilingual
) {
366 CRM_Core_I18n_Schema
::rebuildMultilingualSchema($this->locales
, $rev);
368 return $this->multilingual
;
374 public function setSchemaStructureTables($rev) {
375 if ($this->multilingual
) {
376 CRM_Core_I18n_Schema
::schemaStructureTables($rev, TRUE);
385 public function processSQL($rev) {
386 $sqlFile = implode(DIRECTORY_SEPARATOR
,
394 $tplFile = "$sqlFile.tpl";
396 if (file_exists($tplFile)) {
397 $this->processLocales($tplFile, $rev);
400 if (!file_exists($sqlFile)) {
401 CRM_Core_Error
::fatal("sqlfile - $rev.mysql not found.");
403 $this->source($sqlFile);
408 * Determine the start and end version of the upgrade process.
410 * @return array(0=>$currentVer, 1=>$latestVer)
412 public function getUpgradeVersions() {
413 $latestVer = CRM_Utils_System
::version();
414 $currentVer = CRM_Core_BAO_Domain
::version(TRUE);
416 CRM_Core_Error
::fatal(ts('Version information missing in civicrm database.'));
418 elseif (stripos($currentVer, 'upgrade')) {
419 CRM_Core_Error
::fatal(ts('Database check failed - the database looks to have been partially upgraded. You may want to reload the database with the backup and try the upgrade process again.'));
422 CRM_Core_Error
::fatal(ts('Version information missing in civicrm codebase.'));
425 return [$currentVer, $latestVer];
429 * Determine if $currentVer can be upgraded to $latestVer
434 * @return mixed, a string error message or boolean 'false' if OK
436 public function checkUpgradeableVersion($currentVer, $latestVer) {
438 // since version is suppose to be in valid format at this point, especially after conversion ($convertVer),
439 // lets do a pattern check -
440 if (!CRM_Utils_System
::isVersionFormatValid($currentVer)) {
441 $error = ts('Database is marked with invalid version format. You may want to investigate this before you proceed further.');
443 elseif (version_compare($currentVer, $latestVer) > 0) {
444 // DB version number is higher than codebase being upgraded to. This is unexpected condition-fatal error.
445 $error = ts('Your database is marked with an unexpected version number: %1. The automated upgrade to version %2 can not be run - and the %2 codebase may not be compatible with your database state. You will need to determine the correct version corresponding to your current database state. You may want to revert to the codebase you were using prior to beginning this upgrade until you resolve this problem.',
446 [1 => $currentVer, 2 => $latestVer]
449 elseif (version_compare($currentVer, $latestVer) == 0) {
450 $error = ts('Your database has already been upgraded to CiviCRM %1',
454 elseif (version_compare($currentVer, self
::MINIMUM_UPGRADABLE_VERSION
) < 0) {
455 $error = ts('CiviCRM versions prior to %1 cannot be upgraded directly to %2. This upgrade will need to be done in stages. First download an intermediate version (the LTS may be a good choice) and upgrade to that before proceeding to this version.',
456 [1 => self
::MINIMUM_UPGRADABLE_VERSION
, 2 => $latestVer]
460 if (version_compare(phpversion(), self
::MINIMUM_PHP_VERSION
) < 0) {
461 $error = ts('CiviCRM %3 requires PHP version %1 (or newer), but the current system uses %2 ',
463 1 => self
::MINIMUM_PHP_VERSION
,
469 // check for mysql trigger privileges
470 if (!\Civi
::settings()->get('logging_no_trigger_permission') && !CRM_Core_DAO
::checkTriggerViewPermission(FALSE, TRUE)) {
471 $error = ts('CiviCRM %1 requires MySQL trigger privileges.',
475 if (CRM_Core_DAO
::getGlobalSetting('thread_stack', 0) < (1024 * self
::MINIMUM_THREAD_STACK
)) {
476 $error = ts('CiviCRM %1 requires MySQL thread stack >= %2k', [
478 2 => self
::MINIMUM_THREAD_STACK
,
486 * Determine if $currentver already matches $latestVer
491 * @return mixed, a string error message or boolean 'false' if OK
493 public function checkCurrentVersion($currentVer, $latestVer) {
496 // since version is suppose to be in valid format at this point, especially after conversion ($convertVer),
497 // lets do a pattern check -
498 if (!CRM_Utils_System
::isVersionFormatValid($currentVer)) {
499 $error = ts('Database is marked with invalid version format. You may want to investigate this before you proceed further.');
501 elseif (version_compare($currentVer, $latestVer) != 0) {
502 $error = ts('Your database is not configured for version %1',
510 * Fill the queue with upgrade tasks.
512 * @param string $currentVer
513 * the original revision.
514 * @param string $latestVer
515 * the target (final) revision.
516 * @param string $postUpgradeMessageFile
517 * path of a modifiable file which lists the post-upgrade messages.
519 * @return CRM_Queue_Service
521 public static function buildQueue($currentVer, $latestVer, $postUpgradeMessageFile) {
522 $upgrade = new CRM_Upgrade_Form();
524 // Ensure that queue can be created
525 if (!CRM_Queue_BAO_QueueItem
::findCreateTable()) {
526 CRM_Core_Error
::fatal(ts('Failed to find or create queueing table'));
528 $queue = CRM_Queue_Service
::singleton()->create([
529 'name' => self
::QUEUE_NAME
,
534 $task = new CRM_Queue_Task(
535 ['CRM_Upgrade_Form', 'doFileCleanup'],
536 [$postUpgradeMessageFile],
539 $queue->createItem($task);
541 $task = new CRM_Queue_Task(
542 ['CRM_Upgrade_Form', 'disableOldExtensions'],
543 [$postUpgradeMessageFile],
544 "Checking extensions"
546 $queue->createItem($task);
548 $revisions = $upgrade->getRevisionSequence();
549 foreach ($revisions as $rev) {
550 // proceed only if $currentVer < $rev
551 if (version_compare($currentVer, $rev) < 0) {
552 $beginTask = new CRM_Queue_Task(
554 ['CRM_Upgrade_Form', 'doIncrementalUpgradeStart'],
557 "Begin Upgrade to $rev"
559 $queue->createItem($beginTask);
561 $task = new CRM_Queue_Task(
563 ['CRM_Upgrade_Form', 'doIncrementalUpgradeStep'],
565 [$rev, $currentVer, $latestVer, $postUpgradeMessageFile],
568 $queue->createItem($task);
570 $task = new CRM_Queue_Task(
572 ['CRM_Upgrade_Form', 'doIncrementalUpgradeFinish'],
574 [$rev, $currentVer, $latestVer, $postUpgradeMessageFile],
575 "Finish Upgrade DB to $rev"
577 $queue->createItem($task);
585 * Find any old, orphaned files that should have been deleted.
587 * These files can get left behind, eg, if you use the Joomla
590 * The earlier we can do this, the better - don't want upgrade logic
591 * to inadvertently rely on old/relocated files.
593 * @param \CRM_Queue_TaskContext $ctx
594 * @param string $postUpgradeMessageFile
597 public static function doFileCleanup(CRM_Queue_TaskContext
$ctx, $postUpgradeMessageFile) {
598 $source = new CRM_Utils_Check_Component_Source();
599 $files = $source->findOrphanedFiles();
601 foreach ($files as $file) {
602 if (is_dir($file['path'])) {
603 @rmdir
($file['path']);
606 @unlink
($file['path']);
609 if (file_exists($file['path'])) {
610 $errors[] = sprintf("<li>%s</li>", htmlentities($file['path']));
614 if (!empty($errors)) {
615 file_put_contents($postUpgradeMessageFile,
616 '<br/><br/>' . ts('Some old files could not be removed. Please remove them.')
617 . '<ul>' . implode("\n", $errors) . '</ul>',
626 * Disable/uninstall any extensions not compatible with this new version.
628 * @param \CRM_Queue_TaskContext $ctx
629 * @param string $postUpgradeMessageFile
632 public static function disableOldExtensions(CRM_Queue_TaskContext
$ctx, $postUpgradeMessageFile) {
634 $manager = CRM_Extension_System
::singleton()->getManager();
635 foreach ($manager->getStatuses() as $key => $status) {
636 $obsolete = $manager->isIncompatible($key);
638 if (!empty($obsolete['disable']) && in_array($status, [$manager::STATUS_INSTALLED
, $manager::STATUS_INSTALLED_MISSING
])) {
640 $manager->disable($key);
641 // Update the status for the sake of uninstall below.
642 $status = $status == $manager::STATUS_INSTALLED ?
$manager::STATUS_DISABLED
: $manager::STATUS_DISABLED_MISSING
;
643 // This message is intentionally overwritten by uninstall below as it would be redundant
644 $messages[$key] = ts('The extension %1 is now obsolete and has been disabled.', [1 => $key]);
646 catch (CRM_Extension_Exception
$e) {
647 $messages[] = ts('The obsolete extension %1 could not be removed due to an error. It is recommended to remove this extension manually.', [1 => $key]);
650 if (!empty($obsolete['uninstall']) && in_array($status, [$manager::STATUS_DISABLED
, $manager::STATUS_DISABLED_MISSING
])) {
652 $manager->uninstall($key);
653 $messages[$key] = ts('The extension %1 is now obsolete and has been uninstalled.', [1 => $key]);
654 if ($status == $manager::STATUS_DISABLED
) {
655 $messages[$key] .= ' ' . ts('You can remove it from your extensions directory.');
658 catch (CRM_Extension_Exception
$e) {
659 $messages[] = ts('The obsolete extension %1 could not be removed due to an error. It is recommended to remove this extension manually.', [1 => $key]);
662 if (!empty($obsolete['force-uninstall'])) {
663 CRM_Core_DAO
::executeQuery('UPDATE civicrm_extension SET is_active = 0 WHERE full_name = %1', [
664 1 => [$key, 'String'],
670 file_put_contents($postUpgradeMessageFile,
671 '<br/><br/><ul><li>' . implode("</li>\n<li>", $messages) . '</li></ul>',
680 * Perform an incremental version update.
682 * @param CRM_Queue_TaskContext $ctx
684 * the target (intermediate) revision e.g '3.2.alpha1'.
688 public static function doIncrementalUpgradeStart(CRM_Queue_TaskContext
$ctx, $rev) {
689 $upgrade = new CRM_Upgrade_Form();
691 // as soon as we start doing anything we append ".upgrade" to version.
692 // this also helps detect any partial upgrade issues
693 $upgrade->setVersion($rev . '.upgrade');
699 * Perform an incremental version update.
701 * @param CRM_Queue_TaskContext $ctx
703 * the target (intermediate) revision e.g '3.2.alpha1'.
704 * @param string $originalVer
705 * the original revision.
706 * @param string $latestVer
707 * the target (final) revision.
708 * @param string $postUpgradeMessageFile
709 * path of a modifiable file which lists the post-upgrade messages.
713 public static function doIncrementalUpgradeStep(CRM_Queue_TaskContext
$ctx, $rev, $originalVer, $latestVer, $postUpgradeMessageFile) {
714 $upgrade = new CRM_Upgrade_Form();
716 $phpFunctionName = 'upgrade_' . str_replace('.', '_', $rev);
718 $versionObject = $upgrade->incrementalPhpObject($rev);
720 // pre-db check for major release.
721 if ($upgrade->checkVersionRelease($rev, 'alpha1')) {
722 if (!(is_callable([$versionObject, 'verifyPreDBstate']))) {
723 CRM_Core_Error
::fatal("verifyPreDBstate method was not found for $rev");
727 if (!($versionObject->verifyPreDBstate($error))) {
728 if (!isset($error)) {
729 $error = "post-condition failed for current upgrade for $rev";
731 CRM_Core_Error
::fatal($error);
736 $upgrade->setSchemaStructureTables($rev);
738 if (is_callable([$versionObject, $phpFunctionName])) {
739 $versionObject->$phpFunctionName($rev, $originalVer, $latestVer);
742 $upgrade->processSQL($rev);
745 // set post-upgrade-message if any
746 if (is_callable([$versionObject, 'setPostUpgradeMessage'])) {
747 $postUpgradeMessage = file_get_contents($postUpgradeMessageFile);
748 $versionObject->setPostUpgradeMessage($postUpgradeMessage, $rev);
749 file_put_contents($postUpgradeMessageFile, $postUpgradeMessage);
756 * Perform an incremental version update.
758 * @param CRM_Queue_TaskContext $ctx
760 * the target (intermediate) revision e.g '3.2.alpha1'.
761 * @param string $currentVer
762 * the original revision.
763 * @param string $latestVer
764 * the target (final) revision.
765 * @param string $postUpgradeMessageFile
766 * path of a modifiable file which lists the post-upgrade messages.
770 public static function doIncrementalUpgradeFinish(CRM_Queue_TaskContext
$ctx, $rev, $currentVer, $latestVer, $postUpgradeMessageFile) {
771 $upgrade = new CRM_Upgrade_Form();
772 $upgrade->setVersion($rev);
773 CRM_Utils_System
::flushCache();
775 $config = CRM_Core_Config
::singleton();
776 $config->userSystem
->flush();
780 public static function doFinish() {
781 $upgrade = new CRM_Upgrade_Form();
782 list($ignore, $latestVer) = $upgrade->getUpgradeVersions();
783 // Seems extraneous in context, but we'll preserve old behavior
784 $upgrade->setVersion($latestVer);
786 // Clear cached metadata.
787 Civi
::service('settings_manager')->flush();
789 // cleanup caches CRM-8739
790 $config = CRM_Core_Config
::singleton();
791 $config->cleanupCaches(1);
793 $versionCheck = new CRM_Utils_VersionCheck();
794 $versionCheck->flushCache();
796 // Rebuild all triggers and re-enable logging if needed
797 $logging = new CRM_Logging_Schema();
798 $logging->fixSchemaDifferences();
800 CRM_Core_ManagedEntities
::singleton(TRUE)->reconcile(TRUE);
804 * Compute any messages which should be displayed before upgrade
805 * by calling the 'setPreUpgradeMessage' on each incremental upgrade
808 * @param string $preUpgradeMessage
813 public function setPreUpgradeMessage(&$preUpgradeMessage, $currentVer, $latestVer) {
814 // check for changed message templates
815 CRM_Upgrade_Incremental_General
::checkMessageTemplate($preUpgradeMessage, $latestVer, $currentVer);
816 // set global messages
817 CRM_Upgrade_Incremental_General
::setPreUpgradeMessage($preUpgradeMessage, $currentVer, $latestVer);
819 // Scan through all php files and see if any file is interested in setting pre-upgrade-message
820 // based on $currentVer, $latestVer.
821 // Please note, at this point upgrade hasn't started executing queries.
822 $revisions = $this->getRevisionSequence();
823 foreach ($revisions as $rev) {
824 if (version_compare($currentVer, $rev) < 0) {
825 $versionObject = $this->incrementalPhpObject($rev);
826 CRM_Upgrade_Incremental_General
::updateMessageTemplate($preUpgradeMessage, $rev);
827 if (is_callable([$versionObject, 'setPreUpgradeMessage'])) {
828 $versionObject->setPreUpgradeMessage($preUpgradeMessage, $rev, $currentVer);