diff --git a/controller.php b/controller.php index 9bea3160..4734d952 100755 --- a/controller.php +++ b/controller.php @@ -2,6 +2,7 @@ namespace Concrete\Package\MigrationTool; use Concrete\Core\Asset\AssetList; +use Concrete\Core\Database\Connection\Connection; use Concrete\Core\Messenger\MessageBusManager; use Concrete\Core\Package\Package; use Concrete\Core\Page\Type\Type; @@ -28,12 +29,13 @@ use PortlandLabs\Concrete5\MigrationTool\Publisher\ContentImporter\ValueInspector\InspectionRoutine\BatchPageRoutine; use PortlandLabs\Concrete5\MigrationTool\Publisher\Routine\Manager as PublisherManager; use SinglePage; +use Throwable; class Controller extends Package { protected $pkgHandle = 'migration_tool'; protected $appVersionRequired = '9.0.0'; - protected $pkgVersion = '9.1.0'; + protected $pkgVersion = '9.2.0'; protected $pkgAutoloaderMapCoreExtensions = true; protected $pkgAutoloaderRegistries = array( 'src/PortlandLabs/Concrete5/MigrationTool' => '\PortlandLabs\Concrete5\MigrationTool', @@ -282,5 +284,13 @@ public function upgrade() $pkg = \Package::getByHandle('migration_tool'); $this->installSinglePages($pkg); $this->installPageTypes($pkg); + try { + $cn = $this->app->make(Connection::class); + // Before version 9.2.0 we didn't have the MigrationImportImportedBlockValues.originalValue field. + // The code rewrites "value" and its original content is already lost: so this query won't fix that, + // but at least we won't break existing installations. + $cn->executeStatement('UPDATE MigrationImportImportedBlockValues SET originalValue = value WHERE originalValue IS NULL'); + } catch (Throwable $_) { + } } } diff --git a/src/PortlandLabs/Concrete5/MigrationTool/Batch/Command/NormalizePagePathsCommandHandler.php b/src/PortlandLabs/Concrete5/MigrationTool/Batch/Command/NormalizePagePathsCommandHandler.php index 1be34829..ba869827 100644 --- a/src/PortlandLabs/Concrete5/MigrationTool/Batch/Command/NormalizePagePathsCommandHandler.php +++ b/src/PortlandLabs/Concrete5/MigrationTool/Batch/Command/NormalizePagePathsCommandHandler.php @@ -2,81 +2,138 @@ namespace PortlandLabs\Concrete5\MigrationTool\Batch\Command; -use PortlandLabs\Concrete5\MigrationTool\Batch\ContentMapper\Item\Item; -use PortlandLabs\Concrete5\MigrationTool\Batch\ContentMapper\PresetManager; +use Doctrine\ORM\EntityManagerInterface; +use Generator; +use PortlandLabs\Concrete5\MigrationTool\Entity\Import\Batch; use PortlandLabs\Concrete5\MigrationTool\Entity\Import\BlockValue\ImportedBlockValue; class NormalizePagePathsCommandHandler { - public function __invoke(NormalizePagePathsCommand $command) { - // Since batch is serialized we do this: - $em = \Database::connection()->getEntityManager(); - $batch = $em->getRepository('PortlandLabs\Concrete5\MigrationTool\Entity\Import\Batch')->findOneById($command->getBatchId()); + $em = app(EntityManagerInterface::class); + $batch = $em->find(Batch::class, $command->getBatchId()); $pages = $batch->getPages(); - $paths = []; - foreach($pages as $page) { - $paths[] = trim($page->getOriginalPath(), '/'); - } - $n = count($paths); - $common = ''; - $offset = 1; - if (isset($paths[0]) && $paths[0]) { - while (strpos($paths[0], '/', $offset) !== false) { - $offset = strpos($paths[0], '/', $offset) + 1; - $c = substr($paths[0], 0, $offset); - for ($i = 1; $i < $n; ++$i) { - if (substr($paths[$i], 0, $offset) !== $c) { - break 2; - } - } - $common = $c; + $pagesToNormalize = []; + foreach ($pages as $page) { + if ($batch->isPublishToSitemap() || !$page->canNormalizePath()) { + $page->setBatchPath($page->getOriginalPath()); + } else { + $pagesToNormalize[] = $page; } } + $map = $this->normalizePagePaths($pagesToNormalize); + $this->applyMapToPageLinks($pages, $batch, $map); + $em->flush(); + } - if (count($pages) == 1) { - // Then $common equals the part of the path up to the last slug. - // So if our only page is /path/to/my/page, then $common = '/path/to/my'; - $common = substr($pages[0]->getOriginalPath(), 0, strrpos($pages[0]->getOriginalPath(), '/')); + /** + * Set the "Batch Path" of the pages, and returns a map from the "original" page paths and the "batch" paths. + * + * @param \PortlandLabs\Concrete5\MigrationTool\Entity\Import\Page[] $pages + */ + private function normalizePagePaths(array $pagesToNormalize): array + { + $map = []; + $commonPrefix = $this->calculateCommonPathPrefix($pagesToNormalize); + foreach ($pagesToNormalize as $page) { + $originalPath = '/' . ltrim($page->getOriginalPath() ?? '', '/'); + $newPath = substr($originalPath, strlen($commonPrefix) - 1); + $page->setBatchPath($newPath); + $map[$originalPath] = $newPath; } - if ($common) { - $common = '/' . trim($common, '/'); - $contentSearchURL = "/\{ccm:export:page:" . preg_quote($common, '/') . "(.*?)\}/i"; - $contentReplaceURL = "{ccm:export:page:$1}"; - foreach ($pages as $page) { - $originalPath = $page->getOriginalPath(); - $newPath = substr($originalPath, strlen($common)); - if ($page->canNormalizePath()) { - $page->setBatchPath($newPath); - } + return $map; + } - $areas = $page->getAreas(); - foreach ($areas as $area) { - $blocks = $area->getBlocks(); - foreach ($blocks as $block) { - $value = $block->getBlockValue(); - if ($value instanceof ImportedBlockValue) { - $content = preg_replace($contentSearchURL, $contentReplaceURL, $value->getValue()); - // doctrine is doing some dumb shit here so let's do a direct query - $db = $em->getConnection(); - $db->executeQuery('update MigrationImportImportedBlockValues set value = ? where id = ?', - array($content, $value->getID()) - ); - } + /** + * Calculate the common prefix of page paths. + * + * @param \PortlandLabs\Concrete5\MigrationTool\Entity\Import\Page[] $pages + * + * @example for 0 pages: you'll get '/' + * @example for 1 page at '/path/to/my/page': you'll get '/path/to/my/' + * @example for 2 pages at '/path/to/my/page' and '/path/to/another/page': you'll get '/path/to/' + * @example for 2 pages at '/first/page' and '/second/page': you'll get '/' + */ + private function calculateCommonPathPrefix(array $pages): string + { + $commonSlugs = null; + foreach ($pages as $page) { + $pageSlugs = preg_split('{/}', $page->getOriginalPath() ?? '', -1, PREG_SPLIT_NO_EMPTY); + array_pop($pageSlugs); + if ($commonSlugs === null) { + $commonSlugs = $pageSlugs; + } else { + $newCommonSlugs = []; + foreach ($commonSlugs as $index => $slug) { + if (!isset($pageSlugs[$index]) || $pageSlugs[$index] !== $slug) { + break; } + $newCommonSlugs[] = $slug; } + $commonSlugs = $newCommonSlugs; } - } else { - foreach ($pages as $page) { - if ($page->canNormalizePath()) { - $page->setBatchPath($page->getOriginalPath()); - } + if ($commonSlugs === []) { + break; } } - $em->flush(); + if ($commonSlugs === null || $commonSlugs === []) { + return '/'; + } + + return '/' . implode('/', $commonSlugs) . '/'; } + /** + * Update the '{ccm:export:page:...}` placeholders of the blocks. + * + * @param \PortlandLabs\Concrete5\MigrationTool\Entity\Import\Page[] $pages + * @param array $map the map from the "original" page paths and the "batch" paths. + */ + private function applyMapToPageLinks(array $pages, Batch $batch, array $map): void + { + $pathPrefix = $batch->isPublishToSitemap() ? '' : "/!import_batches/{$batch->getID()}"; + foreach ($this->listImportedBlockValues($pages) as $importedBlockValue) { + $value = $importedBlockValue->getOriginalValue(); + if ($value) { + $value = preg_replace_callback( + '/\{ccm:export:page:(?.*?)\}/', + static function (array $matches) use (&$map, $pathPrefix): string { + $path = '/' . ltrim($matches['path'], '/'); + if (isset($map[$path])) { + $path = $pathPrefix . $map[$path]; + } + if ($path === '/') { + $path = ''; + } + return "{ccm:export:page:{$path}}"; + }, + $value + ); + } + $importedBlockValue->setValue($value); + } + } -} \ No newline at end of file + /** + * List all the ImportedBlockValue instances owned by pages. + * + * @param \PortlandLabs\Concrete5\MigrationTool\Entity\Import\Page[] $pages + * + * @return \PortlandLabs\Concrete5\MigrationTool\Entity\Import\BlockValue\ImportedBlockValue[] + */ + private function listImportedBlockValues(array $pages): Generator + { + foreach ($pages as $page) { + foreach ($page->getAreas() as $area) { + foreach ($area->getBlocks() as $block) { + $value = $block->getBlockValue(); + if ($value instanceof ImportedBlockValue) { + yield $value; + } + } + } + } + } +} diff --git a/src/PortlandLabs/Concrete5/MigrationTool/Batch/Formatter/Page/TreeJsonFormatter.php b/src/PortlandLabs/Concrete5/MigrationTool/Batch/Formatter/Page/TreeJsonFormatter.php index 810373ba..1d963d62 100644 --- a/src/PortlandLabs/Concrete5/MigrationTool/Batch/Formatter/Page/TreeJsonFormatter.php +++ b/src/PortlandLabs/Concrete5/MigrationTool/Batch/Formatter/Page/TreeJsonFormatter.php @@ -38,7 +38,7 @@ public function jsonSerialize() } $node->id = $page->getId(); - $node->pagePath = '/' . $page->getBatchPath(); + $node->pagePath = '/' . ltrim($page->getBatchPath() ?? '', '/'); $node->pageType = $page->getType(); $node->pageTemplate = $page->getTemplate(); if (!$skipItem) { diff --git a/src/PortlandLabs/Concrete5/MigrationTool/Batch/Validator/Page/Content/PageItemValidator.php b/src/PortlandLabs/Concrete5/MigrationTool/Batch/Validator/Page/Content/PageItemValidator.php index 4defd7e2..f4159fe5 100644 --- a/src/PortlandLabs/Concrete5/MigrationTool/Batch/Validator/Page/Content/PageItemValidator.php +++ b/src/PortlandLabs/Concrete5/MigrationTool/Batch/Validator/Page/Content/PageItemValidator.php @@ -15,12 +15,12 @@ public function itemExists(ItemInterface $item, Batch $batch) if (is_object($item->getContentObject())) { return true; } - + $itemPath = $this->normalizePath($item->getReference()); foreach ($batch->getPages() as $page) { - if ($page->getBatchPath() == $item->getReference()) { + if ($this->normalizePath($page->getBatchPath()) === $itemPath) { return true; } - if ($page->getOriginalPath() == $item->getReference()) { + if ($this->normalizePath($page->getOriginalPath()) === $itemPath) { return true; } } @@ -32,4 +32,9 @@ public function addMissingItemMessage(ItemInterface $item, MessageCollection $me new Message(t('Referenced page at path %s cannot be found in the site or in the current content batch.', $item->getReference()), Message::E_WARNING) ); } + + private function normalizePath(?string $path): string + { + return '/' . ltrim($path ?? '', '/'); + } } diff --git a/src/PortlandLabs/Concrete5/MigrationTool/Entity/Import/BlockValue/ImportedBlockValue.php b/src/PortlandLabs/Concrete5/MigrationTool/Entity/Import/BlockValue/ImportedBlockValue.php index fd2b3017..0f82a282 100644 --- a/src/PortlandLabs/Concrete5/MigrationTool/Entity/Import/BlockValue/ImportedBlockValue.php +++ b/src/PortlandLabs/Concrete5/MigrationTool/Entity/Import/BlockValue/ImportedBlockValue.php @@ -1,7 +1,7 @@ originalValue; + } + + /** + * @return $this + */ + public function setOriginalValue(?string $value): self + { + $this->originalValue = $value; + + return $this; + } + + public function getValue(): ?string { return $this->value; } /** - * @param mixed $value + * @return $this */ - public function setValue($value) + public function setValue(?string $value): self { $this->value = $value; + + return $this; } public function getFormatter() diff --git a/src/PortlandLabs/Concrete5/MigrationTool/Importer/CIF/Block/Importer.php b/src/PortlandLabs/Concrete5/MigrationTool/Importer/CIF/Block/Importer.php index 634309f9..e196ce29 100644 --- a/src/PortlandLabs/Concrete5/MigrationTool/Importer/CIF/Block/Importer.php +++ b/src/PortlandLabs/Concrete5/MigrationTool/Importer/CIF/Block/Importer.php @@ -10,8 +10,11 @@ class Importer extends AbstractImporter public function parse(\SimpleXMLElement $node) { $value = new ImportedBlockValue(); - $value->setValue((string) $node->asXML()); + $xml = (string) $node->asXML(); - return $value; + return $value + ->setOriginalValue($xml) + ->setValue($xml) + ; } } diff --git a/src/PortlandLabs/Concrete5/MigrationTool/Publisher/Block/CIFPublisher.php b/src/PortlandLabs/Concrete5/MigrationTool/Publisher/Block/CIFPublisher.php index 2d23fcc4..6ebaddf1 100644 --- a/src/PortlandLabs/Concrete5/MigrationTool/Publisher/Block/CIFPublisher.php +++ b/src/PortlandLabs/Concrete5/MigrationTool/Publisher/Block/CIFPublisher.php @@ -1,9 +1,11 @@ getController(); - $bx = simplexml_load_string($value->getValue()); + $xml = $value->getValue(); + $bx = simplexml_load_string($xml); + if (!$bx instanceof SimpleXMLElement) { + $message = t('Invalid XML found:') . "\n"; + if (!is_string($xml)) { + $message .= t('Expected a string, got %s', gettype($xml)); + } elseif ($xml === '') { + $message .= t('Empty string'); + } else { + $message .= $xml; + } + throw new UserMessageException($message); + } return $btc->import($page, $area, $bx); } diff --git a/src/PortlandLabs/Concrete5/MigrationTool/Publisher/ContentImporter/ValueInspector/InspectionRoutine/BatchPageRoutine.php b/src/PortlandLabs/Concrete5/MigrationTool/Publisher/ContentImporter/ValueInspector/InspectionRoutine/BatchPageRoutine.php index 7b1235ce..59b14dc9 100644 --- a/src/PortlandLabs/Concrete5/MigrationTool/Publisher/ContentImporter/ValueInspector/InspectionRoutine/BatchPageRoutine.php +++ b/src/PortlandLabs/Concrete5/MigrationTool/Publisher/ContentImporter/ValueInspector/InspectionRoutine/BatchPageRoutine.php @@ -2,12 +2,14 @@ namespace PortlandLabs\Concrete5\MigrationTool\Publisher\ContentImporter\ValueInspector\InspectionRoutine; use Concrete\Core\Backup\ContentImporter\ValueInspector\InspectionRoutine\PageRoutine; -use Concrete\Core\Backup\ContentImporter\ValueInspector\Item\PageItem; use Concrete\Package\MigrationTool\Backup\ContentImporter\ValueInspector\Item\BatchPageItem; use PortlandLabs\Concrete5\MigrationTool\Entity\Import\Batch; class BatchPageRoutine extends PageRoutine { + /** + * @var \PortlandLabs\Concrete5\MigrationTool\Entity\Import\Batch + */ protected $batch; public function __construct(Batch $batch) @@ -15,9 +17,20 @@ public function __construct(Batch $batch) $this->batch = $batch; } + /** + * {@inheritdoc} + * + * @see \Concrete\Core\Backup\ContentImporter\ValueInspector\InspectionRoutine\PageRoutine::getItem() + */ public function getItem($identifier) { - $item = new BatchPageItem($this->batch, $identifier); - return $item; + if ($identifier && !$this->batch->isPublishToSitemap()) { + $prefix = "/!import_batches/{$this->batch->getID()}/"; + if (strpos($identifier, $prefix) === 0) { + $identifier = substr($identifier, strlen($prefix) - 1); + } + } + + return new BatchPageItem($this->batch, $identifier); } }