diff --git a/application/Espo/Core/Console/Commands/Migrate.php b/application/Espo/Core/Console/Commands/Migrate.php new file mode 100644 index 0000000000..cf7e20541b --- /dev/null +++ b/application/Espo/Core/Console/Commands/Migrate.php @@ -0,0 +1,50 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Console\Commands; + +use Espo\Core\Console\Command; +use Espo\Core\Console\Command\Params; +use Espo\Core\Console\IO; +use Espo\Core\Upgrades\Migration\Runner; + +/** + * @noinspection PhpUnused + */ +class Migrate implements Command +{ + public function __construct( + private Runner $runner + ) {} + + public function run(Params $params, IO $io): void + { + $this->runner->run($io); + } +} diff --git a/application/Espo/Core/Console/Commands/MigrationVersionStep.php b/application/Espo/Core/Console/Commands/MigrationVersionStep.php new file mode 100644 index 0000000000..860ed3805e --- /dev/null +++ b/application/Espo/Core/Console/Commands/MigrationVersionStep.php @@ -0,0 +1,81 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Console\Commands; + +use Espo\Core\Console\Command; +use Espo\Core\Console\Command\Params; +use Espo\Core\Console\IO; +use Espo\Core\DataManager; +use Espo\Core\Exceptions\Error; +use Espo\Core\InjectableFactory; +use Espo\Core\Upgrades\Migration; +use RuntimeException; + +/** + * @noinspection PhpUnused + */ +class MigrationVersionStep implements Command +{ + public function __construct( + private InjectableFactory $injectableFactory, + private DataManager $dataManager + ) {} + + public function run(Params $params, IO $io): void + { + $step = $params->getOption('step'); + + if (!$step) { + throw new RuntimeException("No step parameter."); + } + + $dir = 'V' . str_replace('.', '_', $step); + + /** @var class-string $className */ + $className = "Espo\\Core\\Upgrades\\Migrations\\$dir\\AfterUpgrade"; + + if (!class_exists($className)) { + $io->writeLine("No after-upgrade script."); + } + + /** @var Migration\Script $script */ + $script = $this->injectableFactory->create($className); + $script->run(); + + try { + $this->dataManager->rebuild(); + } + catch (Error $e) { + throw new RuntimeException("Error while rebuild: " . $e->getMessage()); + } + + $io->writeLine("Done."); + } +} diff --git a/application/Espo/Core/Upgrades/Migration/Runner.php b/application/Espo/Core/Upgrades/Migration/Runner.php new file mode 100644 index 0000000000..7e703530b3 --- /dev/null +++ b/application/Espo/Core/Upgrades/Migration/Runner.php @@ -0,0 +1,121 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Upgrades\Migration; + +use Espo\Core\Console\IO; +use Espo\Core\Utils\Config; +use Espo\Core\Utils\File\Manager; +use RuntimeException; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + +class Runner +{ + private string $defaultConfigPath = 'application/Espo/Resources/defaults/config.php'; + + public function __construct( + private Config $config, + private Manager $fileManager, + private StepsExtractor $stepsExtractor, + private StepsProvider $stepsProvider + ) {} + + public function run(IO $io): void + { + $version = $this->config->get('version'); + $targetVersion = $this->getTargetVersion(); + + $fullList = $this->stepsProvider->get(); + $steps = $this->stepsExtractor->extract($version, $targetVersion, $fullList); + + if ($steps === []) { + $io->writeLine(" No migrations to run."); + + return; + } + + $io->write(" Running migrations..."); + + foreach ($steps as $step) { + $this->runVersionStep($io, $step); + } + } + + private function runVersionStep(IO $io, string $step): void + { + $phpExecutablePath = $this->getPhpExecutablePath(); + + $command = "command.php migration-version-step --step=$step"; + + $io->write(" $step..."); + + $process = new Process([$phpExecutablePath, $command]); + $process->setTimeout(null); + $process->run(); + + if ($process->isSuccessful()) { + $io->writeLine(" DONE"); + + return; + } + + $io->writeLine(" FAIL"); + + throw new RuntimeException(); + } + + private function getPhpExecutablePath(): string + { + $phpExecutablePath = $this->config->get('phpExecutablePath'); + + if (!$phpExecutablePath) { + $phpExecutablePath = (new PhpExecutableFinder)->find(); + } + + return $phpExecutablePath; + } + + private function getTargetVersion(): string + { + $data = $this->fileManager->getPhpContents($this->defaultConfigPath); + + if (!is_array($data)) { + throw new RuntimeException("No default config."); + } + + $version = $data['version'] ?? null; + + if (!$version || !is_string($version)) { + throw new RuntimeException("No or bad 'version' parameter in default config."); + } + + return $version; + } +} diff --git a/application/Espo/Core/Upgrades/Migration/Script.php b/application/Espo/Core/Upgrades/Migration/Script.php new file mode 100644 index 0000000000..77121f7554 --- /dev/null +++ b/application/Espo/Core/Upgrades/Migration/Script.php @@ -0,0 +1,35 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Upgrades\Migration; + +interface Script +{ + public function run(): void; +} diff --git a/application/Espo/Core/Upgrades/Migration/StepsExtractor.php b/application/Espo/Core/Upgrades/Migration/StepsExtractor.php new file mode 100644 index 0000000000..94a3737f6c --- /dev/null +++ b/application/Espo/Core/Upgrades/Migration/StepsExtractor.php @@ -0,0 +1,93 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Upgrades\Migration; + +use RuntimeException; +use const SORT_STRING; + +class StepsExtractor +{ + /** + * @param string[] $fullList + * @return string[] + */ + public function extract(string $from, string $to, array $fullList): array + { + sort($fullList, SORT_STRING); + + $aFrom = self::split($from); + $aTo = self::split($to); + + $isHotfix = $aFrom[0] === $aTo[0] && $aFrom[1] === $aTo[1]; + + $list = []; + + foreach ($fullList as $item) { + $a = self::split($item); + + $v1 = $a[0] . '.' . $a[1] . '.' . ($a[2] ?? '0'); + + if (version_compare($v1, $from) <= 0) { + continue; + } + + if (version_compare($v1, $to) > 0) { + continue; + } + + if (!$isHotfix && $a[2] !== null) { + continue; + } + + $list[] = $item; + } + + return $list; + } + + /** + * @return array{string, string, ?string} + */ + private static function split(string $version): array + { + $array = explode('.', $version, 3); + + if (count($array) === 2) { + return [$array[0], $array[1], null]; + } + + if (count($array) !== 3) { + throw new RuntimeException("Bad version number $version."); + } + + /** @var array{string, string, string} */ + return $array; + } +} diff --git a/application/Espo/Core/Upgrades/Migration/StepsProvider.php b/application/Espo/Core/Upgrades/Migration/StepsProvider.php new file mode 100644 index 0000000000..91dc9054ac --- /dev/null +++ b/application/Espo/Core/Upgrades/Migration/StepsProvider.php @@ -0,0 +1,63 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Upgrades\Migration; + +use Espo\Core\Utils\File\Manager; +use const SORT_STRING; + +class StepsProvider +{ + private string $dir = 'application/Espo/Core/Upgrades/Migrations'; + + public function __construct( + private Manager $fileManager + ) {} + + /** + * @return string[] + */ + public function get(): array + { + $list = $this->fileManager->getDirList($this->dir); + + $list = array_filter($list, function ($item) { + $dir = $this->dir . '/' . $item; + + return $this->fileManager->isFile($dir . '/AfterUpgrade.php'); + }); + + $list = array_values($list); + $list = array_map(fn ($item) => substr(str_replace('_', '.', $item), 1), $list); + + sort($list, SORT_STRING); + + return $list; + } +} diff --git a/application/Espo/Core/Upgrades/Migrations/V8_0/AfterUpgrade.php b/application/Espo/Core/Upgrades/Migrations/V8_0/AfterUpgrade.php new file mode 100644 index 0000000000..9693951be9 --- /dev/null +++ b/application/Espo/Core/Upgrades/Migrations/V8_0/AfterUpgrade.php @@ -0,0 +1,119 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Upgrades\Migrations\V8_0; + +use Espo\Core\Container; +use Espo\Core\Upgrades\Migration\Script; +use Espo\Core\Utils\Metadata; +use Espo\Entities\Role; +use Espo\ORM\EntityManager; +use Espo\ORM\Query\Part\Expression; +use Espo\ORM\Query\UpdateBuilder; + +class AfterUpgrade implements Script +{ + public function __construct( + private Container $container + ) {} + + public function run(): void + { + $this->updateRoles( + $this->container->getByClass(EntityManager::class) + ); + + $this->updateMetadata( + $this->container->getByClass(Metadata::class) + ); + } + + private function updateRoles(EntityManager $entityManager): void + { + $query = UpdateBuilder::create() + ->in(Role::ENTITY_TYPE) + ->set(['messagePermission' => Expression::column('assignmentPermission')]) + ->build(); + + $entityManager->getQueryExecutor()->execute($query); + } + + private function updateMetadata(Metadata $metadata): void + { + $defs = $metadata->get(['scopes']); + + foreach ($defs as $entityType => $item) { + $isCustom = $item['isCustom'] ?? false; + $type = $item['type'] ?? false; + + if (!$isCustom) { + continue; + } + + if ($type === 'Event') { + $metadata->set('entityDefs', $entityType, [ + 'fields' => [ + 'dateEnd' => [ + 'suppressValidationList' => ['required'], + ], + ] + ]); + + $metadata->save(); + + continue; + } + + if ( + !in_array($type, [ + 'BasePlus', + 'Base', + 'Company', + 'Person', + ]) + ) { + continue; + } + + $recordDefs = $metadata->getCustom('recordDefs', $entityType) ?? (object) []; + $scopes = $metadata->getCustom('scopes', $entityType) ?? (object) []; + + $recordDefs->duplicateWhereBuilderClassName = "Espo\\Classes\\DuplicateWhereBuilders\\General"; + + $scopes->duplicateCheckFieldList = []; + + if ($type === 'Company' || $type === 'Person') { + $scopes->duplicateCheckFieldList = ['name', 'emailAddress']; + } + + $metadata->saveCustom('recordDefs', $entityType, $recordDefs); + $metadata->saveCustom('scopes', $entityType, $scopes); + } + } +} diff --git a/application/Espo/Core/Upgrades/Migrations/V8_1/AfterUpgrade.php b/application/Espo/Core/Upgrades/Migrations/V8_1/AfterUpgrade.php new file mode 100644 index 0000000000..02e75d8c60 --- /dev/null +++ b/application/Espo/Core/Upgrades/Migrations/V8_1/AfterUpgrade.php @@ -0,0 +1,61 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Upgrades\Migrations\V8_1; + +use Espo\Core\Container; +use Espo\Core\Upgrades\Migration\Script; +use Espo\Core\InjectableFactory; +use Espo\Core\Utils\Config; + +class AfterUpgrade implements Script +{ + public function __construct( + private Container $container + ) {} + + public function run(): void + { + $config = $this->container->getByClass(Config::class); + + $configWriter = $this->container->getByClass(InjectableFactory::class) + ->create(Config\ConfigWriter::class); + + $configWriter->setMultiple([ + 'phoneNumberNumericSearch' => false, + 'phoneNumberInternational' => false, + ]); + + if ($config->get('pdfEngine') === 'Tcpdf') { + $configWriter->set('pdfEngine', 'Dompdf'); + } + + $configWriter->save(); + } +} diff --git a/application/Espo/Core/Upgrades/Migrations/V8_2/AfterUpgrade.php b/application/Espo/Core/Upgrades/Migrations/V8_2/AfterUpgrade.php new file mode 100644 index 0000000000..79579aa8fc --- /dev/null +++ b/application/Espo/Core/Upgrades/Migrations/V8_2/AfterUpgrade.php @@ -0,0 +1,125 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Upgrades\Migrations\V8_2; + +use Espo\Core\Upgrades\Migration\Script; +use Espo\Core\InjectableFactory; +use Espo\Core\Utils\Config; +use Espo\Core\Utils\Metadata; +use Espo\Entities\Template; +use Espo\ORM\EntityManager; +use Espo\Tools\Pdf\Template as PdfTemplate; + +class AfterUpgrade implements Script +{ + public function __construct( + private InjectableFactory $injectableFactory, + private Config $config, + private EntityManager $entityManager, + private Metadata $metadata + ) {} + + public function run(): void + { + $configWriter = $this->injectableFactory->create(Config\ConfigWriter::class); + + $configWriter->setMultiple([ + 'jobForceUtc' => true, + ]); + + $configWriter->save(); + + $em = $this->entityManager; + $config = $this->config; + + $this->updateTemplates($em, $config); + $this->updateTargetList($this->metadata); + } + + private function updateTemplates(EntityManager $entityManager, Config $config): void + { + if ($config->get('pdfEngine') !== 'Dompdf') { + return; + } + + /** @var iterable