diff --git a/eZ/Bundle/EzPublishMigrationBundle/Command/LegacyStorage/FixImagesVarDirCommand.php b/eZ/Bundle/EzPublishMigrationBundle/Command/LegacyStorage/FixImagesVarDirCommand.php new file mode 100644 index 00000000000..b362ac5cb7f --- /dev/null +++ b/eZ/Bundle/EzPublishMigrationBundle/Command/LegacyStorage/FixImagesVarDirCommand.php @@ -0,0 +1,343 @@ +configResolver = $configResolver; + $this->siteaccess = $siteaccess; + $this->contentGateway = $contentGateway; + $this->imageGateway = $imageGateway; + } + + protected function configure() + { + $this + ->setName('ezplatform:fix_images_var_dir') + ->setDescription( + 'This update script will fix database references to images that are not placed in the current var_dir.' + ) + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'Execute a dry run' + ) + ->addOption( + 'iteration-count', + null, + InputArgument::OPTIONAL, + 'Limit how many records get updated by single process', + self::DEFAULT_ITERATION_COUNT + ) + ->setHelp( + <<%command.name% fixes database references to images that are not placed in the current var_dir. + +This may for instance occur when the var_dir setting is changed. This script will update the database references to the new path + +Since this script can potentially run for a very long time, to avoid memory exhaustion run it in +production environment using --env=prod switch and with --no-debug for non-prod environments. + +EOT + ); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $iterationCount = (int) $input->getOption('iteration-count'); + $this->dryRun = $input->getOption('dry-run'); + $consoleScript = $_SERVER['argv'][0]; + + $this->varDir = $this->configResolver->getParameter( + 'var_dir', + null, + $this->siteaccess->name + ); + + if (getenv('INNER_CALL')) { + $this->processImages($iterationCount, $output); + $output->writeln($this->done); + } else { + $output->writeln([ + sprintf('Fixing image references using siteaccess %s (var_dir: %s)', $this->siteaccess->name, $this->varDir), + 'Calculating number of Images to fix...', + ]); + + $count = $this->countImagesToFix(); + $output->writeln([ + sprintf('Found total of Images for fixing: %d', $count), + '', + ]); + + if ($count == 0) { + $output->writeln('Nothing to process, exiting.'); + + return; + } + + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion( + 'Are you sure you want to proceed? ', + false + ); + + if (!$helper->ask($input, $output, $question)) { + $output->writeln(''); + + return; + } + + $progressBar = $this->getProgressBar($count, $output); + $progressBar->start(); + + for ($fixed = 0; $fixed < $count; $fixed += $iterationCount) { + $processScriptFragments = [ + $this->getPhpPath(), + $consoleScript, + $this->getName(), + '--iteration-count=' . $iterationCount, + '--siteaccess=' . $this->siteaccess->name, + ]; + + $process = new Process( + implode(' ', $processScriptFragments) + ); + + $process->setEnv(['INNER_CALL' => 1]); + $process->run(); + + if (!$process->isSuccessful()) { + throw new RuntimeException($process->getErrorOutput()); + } + + $doneInProcess = (int)$process->getOutput(); + $this->done += $doneInProcess; + $progressBar->advance($doneInProcess); + } + + $progressBar->finish(); + $output->writeln([ + '', + sprintf('Done: %d', $this->done), + ]); + } + } + + /** + * @param int $limit + * @param \Symfony\Component\Console\Output\OutputInterface $output + */ + protected function processImages($limit, OutputInterface $output) + { + $images = $this->getImagesToFix($limit); + + foreach ($images as $image) { + $filePath = $image['filepath']; + $relativePath = substr( + $filePath, + strpos($filePath, self::STORAGE_IMAGES_PATH) + ); + + $newFilePath = $this->varDir . $relativePath; + + if (!$this->dryRun) { + $this->updateImage($image['id'], $image['contentobject_attribute_id'], $filePath, $newFilePath); + } + + ++$this->done; + } + + if (!$this->dryRun) { + $this->updateContentObjectAtributes(); + } + } + + /** + * @param int $imageId + * @param int $contentObjectAttributeId + * @param string $oldFilePath + * @param string $newFilePath + */ + protected function updateImage($imageId, $contentObjectAttributeId, $oldFilePath, $newFilePath) + { + $this->imageGateway->updateImageFilePath($imageId, $newFilePath); + $this->imageAttributes[$contentObjectAttributeId][$oldFilePath] = $newFilePath; + } + + protected function updateContentObjectAtributes() + { + foreach ($this->imageAttributes as $attributeId => $files) { + $attributeObjects = $this->contentGateway->getContentObjectAttributesById($attributeId); + + foreach ($attributeObjects as $attributeObject) { + $dom = new DOMDocument('1.0', 'utf-8'); + + try { + $dom->loadXML(''); + } catch (Exception $e) { + continue; + } + + foreach ($dom->getElementsByTagName('ezimage') as $ezimageNode) { + $oldPath = $ezimageNode->getAttribute('url'); + + if (isset($files[$oldPath])) { + $ezimageNode->setAttribute('url', $files[$oldPath]); + $ezimageNode->setAttribute('dirpath', \dirname($files[$oldPath])); + } + + foreach ($ezimageNode->getElementsByTagName('alias') as $ezimageAlias) { + $oldPath = $ezimageAlias->getAttribute('url'); + if (isset($files[$oldPath])) { + $ezimageAlias->setAttribute('url', $files[$oldPath]); + $ezimageAlias->setAttribute('dirpath', \dirname($files[$oldPath])); + } + } + } + + $this->contentGateway->updateContentObjectAtribute($attributeObject['id'], $attributeObject['version'], $dom->saveXML()); + } + } + } + + /** + * @param int $limit + * + * @return array + */ + protected function getImagesToFix($limit) + { + return $this->imageGateway->getImagesOutsidePath('/' . $this->varDir . '/storage/', $limit, 0); + } + + /** + * @return int + */ + protected function countImagesToFix() + { + return $this->imageGateway->countImageReferencesOutsidePath('/' . $this->varDir . '/storage/'); + } + + /** + * @param int $maxSteps + * @param \Symfony\Component\Console\Output\OutputInterface $output + * + * @return \Symfony\Component\Console\Helper\ProgressBar + */ + protected function getProgressBar($maxSteps, OutputInterface $output) + { + $progressBar = new ProgressBar($output, $maxSteps); + $progressBar->setFormat( + ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%' + ); + + return $progressBar; + } + + /** + * @return string + */ + private function getPhpPath() + { + if ($this->phpPath) { + return $this->phpPath; + } + $phpFinder = new PhpExecutableFinder(); + $this->phpPath = $phpFinder->find(); + if (!$this->phpPath) { + throw new RuntimeException( + 'The php executable could not be found, it\'s needed for executing parable sub processes, so add it to your PATH environment variable and try again' + ); + } + + return $this->phpPath; + } +} diff --git a/eZ/Bundle/EzPublishMigrationBundle/DependencyInjection/EzPublishMigrationExtension.php b/eZ/Bundle/EzPublishMigrationBundle/DependencyInjection/EzPublishMigrationExtension.php new file mode 100644 index 00000000000..487a7edcec9 --- /dev/null +++ b/eZ/Bundle/EzPublishMigrationBundle/DependencyInjection/EzPublishMigrationExtension.php @@ -0,0 +1,24 @@ +load('services.yml'); + } +} diff --git a/eZ/Bundle/EzPublishMigrationBundle/Resources/config/services.yml b/eZ/Bundle/EzPublishMigrationBundle/Resources/config/services.yml new file mode 100644 index 00000000000..19cf7bc5de2 --- /dev/null +++ b/eZ/Bundle/EzPublishMigrationBundle/Resources/config/services.yml @@ -0,0 +1,11 @@ +services: + ezpublish.migration.fix_images_var_dir: + class: eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\FixImagesVarDirCommand + arguments: + - '@ezpublish.config.resolver' + - '@ezpublish.siteaccess' + - '@ezpublish.persistence.legacy.content.gateway' + - '@ezpublish.fieldType.ezimage.storage_gateway' + tags: + - { name: console.command } + diff --git a/eZ/Publish/Core/FieldType/Image/ImageStorage/Gateway.php b/eZ/Publish/Core/FieldType/Image/ImageStorage/Gateway.php index 45b8e1ed5ef..5c0b09bd304 100644 --- a/eZ/Publish/Core/FieldType/Image/ImageStorage/Gateway.php +++ b/eZ/Publish/Core/FieldType/Image/ImageStorage/Gateway.php @@ -61,6 +61,34 @@ abstract public function removeImageReferences($uri, $versionNo, $fieldId); */ abstract public function countImageReferences($uri); + /** + * Returns the number of recorded references outside of the given $path. + * + * @param string $uri File IO uri (not legacy uri) + * + * @return int + */ + abstract public function countImageReferencesOutsidePath($uri); + + /** + * Returns references outside of the given $path. + * + * @param string $uri File IO uri (not legacy) + * @param int $limit + * @param int $offset + * + * @return array + */ + abstract public function getImagesOutsidePath($uri, $limit = null, $offset = 0); + + /** + * Updates the filepath of given Image. + * + * @param $imageId + * @param $newFilePath + */ + abstract public function updateImageFilePath($imageId, $newFilePath); + /** * Returns the public uris for the images stored in $xml. */ diff --git a/eZ/Publish/Core/FieldType/Image/ImageStorage/Gateway/DoctrineStorage.php b/eZ/Publish/Core/FieldType/Image/ImageStorage/Gateway/DoctrineStorage.php index a4af9f9dc68..d57e218e1b8 100644 --- a/eZ/Publish/Core/FieldType/Image/ImageStorage/Gateway/DoctrineStorage.php +++ b/eZ/Publish/Core/FieldType/Image/ImageStorage/Gateway/DoctrineStorage.php @@ -10,9 +10,9 @@ use Doctrine\DBAL\Connection; use DOMDocument; +use eZ\Publish\Core\FieldType\Image\ImageStorage\Gateway; use eZ\Publish\Core\IO\UrlRedecoratorInterface; use eZ\Publish\SPI\Persistence\Content\VersionInfo; -use eZ\Publish\Core\FieldType\Image\ImageStorage\Gateway; use PDO; /** @@ -225,6 +225,101 @@ public function countImageReferences($uri) return (int) $statement->fetchColumn(); } + /** + * Returns the number of recorded references outside of the given $path. + * + * @param string $uri File IO uri (not legacy) + * + * @return int + */ + public function countImageReferencesOutsidePath($uri) + { + $path = $this->redecorator->redecorateFromSource($uri); + + $selectQuery = $this->connection->createQueryBuilder(); + $selectQuery + ->select('COUNT(' . $this->connection->quoteIdentifier('id') . ')') + ->from($this->connection->quoteIdentifier(self::IMAGE_FILE_TABLE)) + ->where( + $selectQuery->expr()->notLike( + $this->connection->quoteIdentifier('filepath'), + ':likePath' + ) + ) + ->setParameter(':likePath', $path . '%') + ; + + $statement = $selectQuery->execute(); + + return (int) $statement->fetchColumn(); + } + + /** + * Updates the filepath of given Image. + * + * @param int $imageId + * @param string $newFilePath + */ + public function updateImageFilePath($imageId, $newFilePath) + { + $updateQuery = $this->connection->createQueryBuilder(); + $updateQuery + ->update(self::IMAGE_FILE_TABLE) + ->set( + $this->connection->quoteIdentifier('filepath'), + $updateQuery->expr()->literal($newFilePath) + ) + ->where( + $updateQuery->expr()->eq( + $this->connection->quoteIdentifier('id'), + ':id' + ) + ) + ->setParameter(':id', $imageId) + ; + + $updateQuery->execute(); + } + + /** + * Return references outside of the given $path. + * + * @param string $uri File IO uri (not legacy) + * @param int $limit + * @param int $offset + * + * @return array + */ + public function getImagesOutsidePath($uri, $limit = null, $offset = 0) + { + $path = $this->redecorator->redecorateFromSource($uri); + + $selectQuery = $this->connection->createQueryBuilder(); + $selectQuery->select( + $this->connection->quoteIdentifier('id'), + $this->connection->quoteIdentifier('contentobject_attribute_id'), + $this->connection->quoteIdentifier('filepath') + ) + ->from(self::IMAGE_FILE_TABLE) + ->where( + $selectQuery->expr()->notLike( + $this->connection->quoteIdentifier('filepath'), + ':filePath' + ) + ) + ->setParameter(':filePath', $path . '%') + ; + + if ($limit !== null) { + $selectQuery->setMaxResults($limit); + $selectQuery->setFirstResult($offset); + } + + $statement = $selectQuery->execute(); + + return $statement->fetchAll(\PDO::FETCH_ASSOC); + } + /** * Check if image $path can be removed when deleting $versionNo and $fieldId. * diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway.php b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway.php index 05e0c869032..3d9f4e3fc44 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway.php @@ -429,6 +429,24 @@ abstract public function loadVersionedNameData($rows); */ abstract public function copyRelations($originalContentId, $copiedContentId, $versionNo = null); + /** + * Updates Content's attribute text value. + * + * @param int $attributeId + * @param int $version + * @param string $text + */ + abstract public function updateContentObjectAttributeText($attributeId, $version, $text); + + /** + * Returns an array containing all content attributes with the specified id. + * + * @param int $id + * + * @return array + */ + abstract public function getContentObjectAttributesById($id); + /** * Remove the specified translation from all the Versions of a Content Object. * diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php index a9ebf7ff916..0ae058ff6fe 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php @@ -2058,6 +2058,54 @@ public function copyRelations($originalContentId, $copiedContentId, $versionNo = $stmt->execute(); } + /** + * Updates Content's attribute text value. + * + * @param int $attributeId + * @param int $version + * @param string $text + */ + public function updateContentObjectAttributeText($attributeId, $version, $text) + { + $query = $this->connection->createQueryBuilder(); + $query + ->update('ezcontentobject_attribute', 'oa') + ->set('data_text', ':text') + ->where('oa.id = :id') + ->andWhere('oa.version = :version') + ->setParameters([ + 'text' => $text, + 'id' => $attributeId, + 'version' => $version, + ], [ + 'text' => PDO::PARAM_STR, + 'id' => PDO::PARAM_INT, + 'version' => PDO::PARAM_INT, + ]); + + $query->execute(); + } + + /** + * Returns an array containing all content attributes with the specified id. + * + * @param int $id + * + * @return array + */ + public function getContentObjectAttributesById($id) + { + $query = $this->connection->createQueryBuilder(); + $query + ->select('oa.data_text, oa.id, oa.version') + ->from('ezcontentobject_attribute', 'oa') + ->where('oa.id = :id') + ->setParameter('id', $id, PDO::PARAM_INT); + $statement = $query->execute(); + + return $statement->fetchAll(PDO::FETCH_ASSOC); + } + /** * Remove the specified translation from the Content Object Version. * diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/ExceptionConversion.php b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/ExceptionConversion.php index 0574d852e8e..54691192cc9 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/ExceptionConversion.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/ExceptionConversion.php @@ -823,4 +823,40 @@ public function deleteTranslationFromVersion($contentId, $versionNo, $languageCo throw new RuntimeException('Database error', 0, $e); } } + + /** + * Updates Content's attribute text value. + * + * @param int $attributeId + * @param int $version + * @param string $text + */ + public function updateContentObjectAttributeText($attributeId, $version, $text) + { + try { + $this->innerGateway->updateContentObjectAttributeText($attributeId, $version, $text); + } catch (DBALException $e) { + throw new RuntimeException('Database error', 0, $e); + } catch (PDOException $e) { + throw new RuntimeException('Database error', 0, $e); + } + } + + /** + * Returns an array containing all content attributes with the specified id. + * + * @param int $id + * + * @return array + */ + public function getContentObjectAttributesById($id) + { + try { + return $this->innerGateway->getContentObjectAttributesById($id); + } catch (DBALException $e) { + throw new RuntimeException('Database error', 0, $e); + } catch (PDOException $e) { + throw new RuntimeException('Database error', 0, $e); + } + } }