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 * @var \CRM_Core_Config
40 * Upgrade for multilingual.
44 public $multilingual = FALSE;
47 * Locales available for multilingual upgrade.
54 * Constructor for the basic form page.
56 * We should not use QuickForm directly. This class provides a lot
57 * of default convenient functions, rules and buttons
59 * @param object $state
60 * State associated with this form.
61 * @param const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
62 * @param string $method
63 * The type of http method used (GET/POST).
65 * The name of the form if different from class name.
67 public function __construct(
69 $action = CRM_Core_Action
::NONE
,
73 $this->_config
= CRM_Core_Config
::singleton();
75 $domain = new CRM_Core_DAO_Domain();
78 $this->multilingual
= (bool) $domain->locales
;
79 $this->locales
= explode(CRM_Core_DAO
::VALUE_SEPARATOR
, $domain->locales
);
81 $smarty = CRM_Core_Smarty
::singleton();
82 //$smarty->compile_dir = $this->_config->templateCompileDir;
83 $smarty->assign('multilingual', $this->multilingual
);
84 $smarty->assign('locales', $this->locales
);
86 // we didn't call CRM_Core_BAO_ConfigSetting::retrieve(), so we need to set $dbLocale by hand
87 if ($this->multilingual
) {
89 $dbLocale = "_{$this->_config->lcMessages}";
92 parent
::__construct($state, $action, $method, $name);
100 public static function &incrementalPhpObject($version) {
101 static $incrementalPhpObject = [];
103 $versionParts = explode('.', $version);
104 $versionName = CRM_Utils_EnglishNumber
::toCamelCase($versionParts[0]) . CRM_Utils_EnglishNumber
::toCamelCase($versionParts[1]);
106 if (!array_key_exists($versionName, $incrementalPhpObject)) {
107 $className = "CRM_Upgrade_Incremental_php_{$versionName}";
108 $incrementalPhpObject[$versionName] = new $className();
110 return $incrementalPhpObject[$versionName];
119 public function checkVersionRelease($version, $release) {
120 $versionParts = explode('.', $version);
121 return ($versionParts[2] == $release);
125 * @param $constraints
129 public function checkSQLConstraints(&$constraints) {
131 foreach ($constraints as $constraint) {
132 if ($this->checkSQLConstraint($constraint)) {
138 return [$pass, $fail];
147 public function checkSQLConstraint($constraint) {
148 // check constraint here
153 * @param string $fileName
154 * @param bool $isQueryString
156 public function source($fileName, $isQueryString = FALSE) {
157 if ($isQueryString) {
158 CRM_Utils_File
::runSqlQuery($this->_config
->dsn
,
163 CRM_Utils_File
::sourceSQLFile($this->_config
->dsn
,
169 public function preProcess() {
170 CRM_Utils_System
::setTitle($this->getTitle());
171 if (!$this->verifyPreDBState($errorMessage)) {
172 if (!isset($errorMessage)) {
173 $errorMessage = 'pre-condition failed for current upgrade step';
175 CRM_Core_Error
::fatal($errorMessage);
177 $this->assign('recentlyViewed', FALSE);
180 public function buildQuickForm() {
181 $this->addDefaultButtons($this->getButtonTitle(),
189 * Getter function for title. Should be over-ridden by derived class
197 public function getTitle() {
198 return ts('Title not Set');
204 public function getFieldsetTitle() {
211 public function getButtonTitle() {
212 return ts('Continue');
216 * Use the form name to create the tpl file name.
224 public function getTemplateFileName() {
225 $this->assign('title',
226 $this->getFieldsetTitle()
228 $this->assign('message',
229 $this->getTemplateMessage()
231 return 'CRM/Upgrade/Base.tpl';
234 public function postProcess() {
237 if (!$this->verifyPostDBState($errorMessage)) {
238 if (!isset($errorMessage)) {
239 $errorMessage = 'post-condition failed for current upgrade step';
241 CRM_Core_Error
::fatal($errorMessage);
250 public function runQuery($query) {
251 return CRM_Core_DAO
::executeQuery($query);
259 public function setVersion($version) {
260 $this->logVersion($version);
263 UPDATE civicrm_domain
264 SET version = '$version'
266 return $this->runQuery($query);
274 public function logVersion($newVersion) {
276 $oldVersion = CRM_Core_BAO_Domain
::version();
278 $session = CRM_Core_Session
::singleton();
280 'entity_table' => 'civicrm_domain',
282 'data' => "upgrade:{$oldVersion}->{$newVersion}",
283 // lets skip 'modified_id' for now, as it causes FK issues And
284 // is not very important for now.
285 'modified_date' => date('YmdHis'),
287 CRM_Core_BAO_Log
::add($logParams);
299 public function checkVersion($version) {
300 $domainID = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_Domain',
304 return (bool) $domainID;
311 public function getRevisionSequence() {
313 $sqlDir = implode(DIRECTORY_SEPARATOR
,
314 [dirname(__FILE__
), 'Incremental', 'sql']
316 $sqlFiles = scandir($sqlDir);
318 $sqlFilePattern = '/^((\d{1,2}\.\d{1,2})\.(\d{1,2}\.)?(\d{1,2}|\w{4,7}))\.(my)?sql(\.tpl)?$/i';
319 foreach ($sqlFiles as $file) {
320 if (preg_match($sqlFilePattern, $file, $matches)) {
321 if (!in_array($matches[1], $revList)) {
322 $revList[] = $matches[1];
327 usort($revList, 'version_compare');
337 public static function getRevisionPart($rev, $index = 1) {
338 $revPattern = '/^((\d{1,2})\.\d{1,2})\.(\d{1,2}|\w{4,7})?$/i';
339 preg_match($revPattern, $rev, $matches);
341 return array_key_exists($index, $matches) ?
$matches[$index] : NULL;
350 public function processLocales($tplFile, $rev) {
351 $smarty = CRM_Core_Smarty
::singleton();
352 $smarty->assign('domainID', CRM_Core_Config
::domainID());
354 $this->source($smarty->fetch($tplFile), TRUE);
356 if ($this->multilingual
) {
357 CRM_Core_I18n_Schema
::rebuildMultilingualSchema($this->locales
, $rev);
359 return $this->multilingual
;
365 public function setSchemaStructureTables($rev) {
366 if ($this->multilingual
) {
367 CRM_Core_I18n_Schema
::schemaStructureTables($rev, TRUE);
376 public function processSQL($rev) {
377 $sqlFile = implode(DIRECTORY_SEPARATOR
,
385 $tplFile = "$sqlFile.tpl";
387 if (file_exists($tplFile)) {
388 $this->processLocales($tplFile, $rev);
391 if (!file_exists($sqlFile)) {
392 CRM_Core_Error
::fatal("sqlfile - $rev.mysql not found.");
394 $this->source($sqlFile);
399 * Determine the start and end version of the upgrade process.
401 * @return array(0=>$currentVer, 1=>$latestVer)
403 public function getUpgradeVersions() {
404 $latestVer = CRM_Utils_System
::version();
405 $currentVer = CRM_Core_BAO_Domain
::version(TRUE);
407 CRM_Core_Error
::fatal(ts('Version information missing in civicrm database.'));
409 elseif (stripos($currentVer, 'upgrade')) {
410 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.'));
413 CRM_Core_Error
::fatal(ts('Version information missing in civicrm codebase.'));
416 return [$currentVer, $latestVer];
420 * Determine if $currentVer can be upgraded to $latestVer
425 * @return mixed, a string error message or boolean 'false' if OK
427 public function checkUpgradeableVersion($currentVer, $latestVer) {
429 // since version is suppose to be in valid format at this point, especially after conversion ($convertVer),
430 // lets do a pattern check -
431 if (!CRM_Utils_System
::isVersionFormatValid($currentVer)) {
432 $error = ts('Database is marked with invalid version format. You may want to investigate this before you proceed further.');
434 elseif (version_compare($currentVer, $latestVer) > 0) {
435 // DB version number is higher than codebase being upgraded to. This is unexpected condition-fatal error.
436 $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.',
437 [1 => $currentVer, 2 => $latestVer]
440 elseif (version_compare($currentVer, $latestVer) == 0) {
441 $error = ts('Your database has already been upgraded to CiviCRM %1',
445 elseif (version_compare($currentVer, self
::MINIMUM_UPGRADABLE_VERSION
) < 0) {
446 $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.',
447 [1 => self
::MINIMUM_UPGRADABLE_VERSION
, 2 => $latestVer]
451 if (version_compare(phpversion(), CRM_Upgrade_Incremental_General
::MIN_INSTALL_PHP_VER
) < 0) {
452 $error = ts('CiviCRM %3 requires PHP version %1 (or newer), but the current system uses %2 ',
454 1 => CRM_Upgrade_Incremental_General
::MIN_INSTALL_PHP_VER
,
460 // check for mysql trigger privileges
461 if (!\Civi
::settings()->get('logging_no_trigger_permission') && !CRM_Core_DAO
::checkTriggerViewPermission(FALSE, TRUE)) {
462 $error = ts('CiviCRM %1 requires MySQL trigger privileges.',
466 if (CRM_Core_DAO
::getGlobalSetting('thread_stack', 0) < (1024 * self
::MINIMUM_THREAD_STACK
)) {
467 $error = ts('CiviCRM %1 requires MySQL thread stack >= %2k', [
469 2 => self
::MINIMUM_THREAD_STACK
,
477 * Determine if $currentver already matches $latestVer
482 * @return mixed, a string error message or boolean 'false' if OK
484 public function checkCurrentVersion($currentVer, $latestVer) {
487 // since version is suppose to be in valid format at this point, especially after conversion ($convertVer),
488 // lets do a pattern check -
489 if (!CRM_Utils_System
::isVersionFormatValid($currentVer)) {
490 $error = ts('Database is marked with invalid version format. You may want to investigate this before you proceed further.');
492 elseif (version_compare($currentVer, $latestVer) != 0) {
493 $error = ts('Your database is not configured for version %1',
501 * Fill the queue with upgrade tasks.
503 * @param string $currentVer
504 * the original revision.
505 * @param string $latestVer
506 * the target (final) revision.
507 * @param string $postUpgradeMessageFile
508 * path of a modifiable file which lists the post-upgrade messages.
510 * @return CRM_Queue_Service
512 public static function buildQueue($currentVer, $latestVer, $postUpgradeMessageFile) {
513 $upgrade = new CRM_Upgrade_Form();
515 // Ensure that queue can be created
516 if (!CRM_Queue_BAO_QueueItem
::findCreateTable()) {
517 CRM_Core_Error
::fatal(ts('Failed to find or create queueing table'));
519 $queue = CRM_Queue_Service
::singleton()->create([
520 'name' => self
::QUEUE_NAME
,
525 $task = new CRM_Queue_Task(
526 ['CRM_Upgrade_Form', 'doFileCleanup'],
527 [$postUpgradeMessageFile],
530 $queue->createItem($task);
532 $task = new CRM_Queue_Task(
533 ['CRM_Upgrade_Form', 'disableOldExtensions'],
534 [$postUpgradeMessageFile],
535 "Checking extensions"
537 $queue->createItem($task);
539 $revisions = $upgrade->getRevisionSequence();
540 foreach ($revisions as $rev) {
541 // proceed only if $currentVer < $rev
542 if (version_compare($currentVer, $rev) < 0) {
543 $beginTask = new CRM_Queue_Task(
545 ['CRM_Upgrade_Form', 'doIncrementalUpgradeStart'],
548 "Begin Upgrade to $rev"
550 $queue->createItem($beginTask);
552 $task = new CRM_Queue_Task(
554 ['CRM_Upgrade_Form', 'doIncrementalUpgradeStep'],
556 [$rev, $currentVer, $latestVer, $postUpgradeMessageFile],
559 $queue->createItem($task);
561 $task = new CRM_Queue_Task(
563 ['CRM_Upgrade_Form', 'doIncrementalUpgradeFinish'],
565 [$rev, $currentVer, $latestVer, $postUpgradeMessageFile],
566 "Finish Upgrade DB to $rev"
568 $queue->createItem($task);
576 * Find any old, orphaned files that should have been deleted.
578 * These files can get left behind, eg, if you use the Joomla
581 * The earlier we can do this, the better - don't want upgrade logic
582 * to inadvertently rely on old/relocated files.
584 * @param \CRM_Queue_TaskContext $ctx
585 * @param string $postUpgradeMessageFile
588 public static function doFileCleanup(CRM_Queue_TaskContext
$ctx, $postUpgradeMessageFile) {
589 $source = new CRM_Utils_Check_Component_Source();
590 $files = $source->findOrphanedFiles();
592 foreach ($files as $file) {
593 if (is_dir($file['path'])) {
594 @rmdir
($file['path']);
597 @unlink
($file['path']);
600 if (file_exists($file['path'])) {
601 $errors[] = sprintf("<li>%s</li>", htmlentities($file['path']));
605 if (!empty($errors)) {
606 file_put_contents($postUpgradeMessageFile,
607 '<br/><br/>' . ts('Some old files could not be removed. Please remove them.')
608 . '<ul>' . implode("\n", $errors) . '</ul>',
617 * Disable/uninstall any extensions not compatible with this new version.
619 * @param \CRM_Queue_TaskContext $ctx
620 * @param string $postUpgradeMessageFile
623 public static function disableOldExtensions(CRM_Queue_TaskContext
$ctx, $postUpgradeMessageFile) {
625 $manager = CRM_Extension_System
::singleton()->getManager();
626 foreach ($manager->getStatuses() as $key => $status) {
627 $obsolete = $manager->isIncompatible($key);
629 if (!empty($obsolete['disable']) && in_array($status, [$manager::STATUS_INSTALLED
, $manager::STATUS_INSTALLED_MISSING
])) {
631 $manager->disable($key);
632 // Update the status for the sake of uninstall below.
633 $status = $status == $manager::STATUS_INSTALLED ?
$manager::STATUS_DISABLED
: $manager::STATUS_DISABLED_MISSING
;
634 // This message is intentionally overwritten by uninstall below as it would be redundant
635 $messages[$key] = ts('The extension %1 is now obsolete and has been disabled.', [1 => $key]);
637 catch (CRM_Extension_Exception
$e) {
638 $messages[] = ts('The obsolete extension %1 could not be removed due to an error. It is recommended to remove this extension manually.', [1 => $key]);
641 if (!empty($obsolete['uninstall']) && in_array($status, [$manager::STATUS_DISABLED
, $manager::STATUS_DISABLED_MISSING
])) {
643 $manager->uninstall($key);
644 $messages[$key] = ts('The extension %1 is now obsolete and has been uninstalled.', [1 => $key]);
645 if ($status == $manager::STATUS_DISABLED
) {
646 $messages[$key] .= ' ' . ts('You can remove it from your extensions directory.');
649 catch (CRM_Extension_Exception
$e) {
650 $messages[] = ts('The obsolete extension %1 could not be removed due to an error. It is recommended to remove this extension manually.', [1 => $key]);
653 if (!empty($obsolete['force-uninstall'])) {
654 CRM_Core_DAO
::executeQuery('UPDATE civicrm_extension SET is_active = 0 WHERE full_name = %1', [
655 1 => [$key, 'String'],
661 file_put_contents($postUpgradeMessageFile,
662 '<br/><br/><ul><li>' . implode("</li>\n<li>", $messages) . '</li></ul>',
671 * Perform an incremental version update.
673 * @param CRM_Queue_TaskContext $ctx
675 * the target (intermediate) revision e.g '3.2.alpha1'.
679 public static function doIncrementalUpgradeStart(CRM_Queue_TaskContext
$ctx, $rev) {
680 $upgrade = new CRM_Upgrade_Form();
682 // as soon as we start doing anything we append ".upgrade" to version.
683 // this also helps detect any partial upgrade issues
684 $upgrade->setVersion($rev . '.upgrade');
690 * Perform an incremental version update.
692 * @param CRM_Queue_TaskContext $ctx
694 * the target (intermediate) revision e.g '3.2.alpha1'.
695 * @param string $originalVer
696 * the original revision.
697 * @param string $latestVer
698 * the target (final) revision.
699 * @param string $postUpgradeMessageFile
700 * path of a modifiable file which lists the post-upgrade messages.
704 public static function doIncrementalUpgradeStep(CRM_Queue_TaskContext
$ctx, $rev, $originalVer, $latestVer, $postUpgradeMessageFile) {
705 $upgrade = new CRM_Upgrade_Form();
707 $phpFunctionName = 'upgrade_' . str_replace('.', '_', $rev);
709 $versionObject = $upgrade->incrementalPhpObject($rev);
711 // pre-db check for major release.
712 if ($upgrade->checkVersionRelease($rev, 'alpha1')) {
713 if (!(is_callable([$versionObject, 'verifyPreDBstate']))) {
714 CRM_Core_Error
::fatal("verifyPreDBstate method was not found for $rev");
718 if (!($versionObject->verifyPreDBstate($error))) {
719 if (!isset($error)) {
720 $error = "post-condition failed for current upgrade for $rev";
722 CRM_Core_Error
::fatal($error);
727 $upgrade->setSchemaStructureTables($rev);
729 if (is_callable([$versionObject, $phpFunctionName])) {
730 $versionObject->$phpFunctionName($rev, $originalVer, $latestVer);
733 $upgrade->processSQL($rev);
736 // set post-upgrade-message if any
737 if (is_callable([$versionObject, 'setPostUpgradeMessage'])) {
738 $postUpgradeMessage = file_get_contents($postUpgradeMessageFile);
739 $versionObject->setPostUpgradeMessage($postUpgradeMessage, $rev);
740 file_put_contents($postUpgradeMessageFile, $postUpgradeMessage);
747 * Perform an incremental version update.
749 * @param CRM_Queue_TaskContext $ctx
751 * the target (intermediate) revision e.g '3.2.alpha1'.
752 * @param string $currentVer
753 * the original revision.
754 * @param string $latestVer
755 * the target (final) revision.
756 * @param string $postUpgradeMessageFile
757 * path of a modifiable file which lists the post-upgrade messages.
761 public static function doIncrementalUpgradeFinish(CRM_Queue_TaskContext
$ctx, $rev, $currentVer, $latestVer, $postUpgradeMessageFile) {
762 $upgrade = new CRM_Upgrade_Form();
763 $upgrade->setVersion($rev);
764 CRM_Utils_System
::flushCache();
766 $config = CRM_Core_Config
::singleton();
767 $config->userSystem
->flush();
771 public static function doFinish() {
772 Civi
::dispatcher()->setDispatchPolicy(\CRM_Upgrade_DispatchPolicy
::get('upgrade.finish'));
773 $restore = \CRM_Utils_AutoClean
::with(function() {
774 Civi
::dispatcher()->setDispatchPolicy(\CRM_Upgrade_DispatchPolicy
::get('upgrade.main'));
777 $upgrade = new CRM_Upgrade_Form();
778 list($ignore, $latestVer) = $upgrade->getUpgradeVersions();
779 // Seems extraneous in context, but we'll preserve old behavior
780 $upgrade->setVersion($latestVer);
782 CRM_Core_Invoke
::rebuildMenuAndCaches(FALSE, TRUE);
783 // NOTE: triggerRebuild is FALSE becaues it will run again in a moment (via fixSchemaDifferences).
785 $versionCheck = new CRM_Utils_VersionCheck();
786 $versionCheck->flushCache();
788 // Rebuild all triggers and re-enable logging if needed
789 $logging = new CRM_Logging_Schema();
790 $logging->fixSchemaDifferences();
794 * Compute any messages which should be displayed before upgrade
795 * by calling the 'setPreUpgradeMessage' on each incremental upgrade
798 * @param string $preUpgradeMessage
803 public function setPreUpgradeMessage(&$preUpgradeMessage, $currentVer, $latestVer) {
804 // check for changed message templates
805 CRM_Upgrade_Incremental_General
::checkMessageTemplate($preUpgradeMessage, $latestVer, $currentVer);
806 // set global messages
807 CRM_Upgrade_Incremental_General
::setPreUpgradeMessage($preUpgradeMessage, $currentVer, $latestVer);
809 // Scan through all php files and see if any file is interested in setting pre-upgrade-message
810 // based on $currentVer, $latestVer.
811 // Please note, at this point upgrade hasn't started executing queries.
812 $revisions = $this->getRevisionSequence();
813 foreach ($revisions as $rev) {
814 if (version_compare($currentVer, $rev) < 0) {
815 $versionObject = $this->incrementalPhpObject($rev);
816 CRM_Upgrade_Incremental_General
::updateMessageTemplate($preUpgradeMessage, $rev);
817 if (is_callable([$versionObject, 'setPreUpgradeMessage'])) {
818 $versionObject->setPreUpgradeMessage($preUpgradeMessage, $rev, $currentVer);