diff --git a/Cron/InconsistenciesInBlockEmailTest.php b/Cron/InconsistenciesInBlockEmailTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8afcba26d1ad26eae9e03e2d31d0467c1e5bd76b --- /dev/null +++ b/Cron/InconsistenciesInBlockEmailTest.php @@ -0,0 +1,129 @@ +<?php + +declare(strict_types=1); + +/** + * Copyright (c) 2024 TechDivision GmbH + * All rights reserved + * + * This product includes proprietary software developed at TechDivision GmbH, Germany + * For more information see https://www.techdivision.com/ + * + * To obtain a valid license for using this software please contact us at + * license@techdivision.com + * + * @copyright Copyright (c) 2024 TechDivision GmbH (https://www.techdivision.com) + * @author TechDivision Team Zero <zero@techdivision.com> + * @link https://www.techdivision.com/ + */ + +namespace Firegento\ContentProvisioning\Cron; + +use Firegento\ContentProvisioning\Model\Console\BlockListCommand; +use Magento\Framework\Mail\Message; +use Psr\Log\LoggerInterface; +use Magento\Framework\Mail\TransportInterfaceFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; + +class InconsistenciesInBlockEmailTest +{ + public const EMAIL_SUBJECT = "Inconsistencies in CMS block"; + + public const SENDER_EMAIL = 'trans_email/ident_general/email'; + + public const RECIPIENT_EMAIL = 'trans_email/ident_content_provisioning/email'; + + /** + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * @var BlockListCommand + */ + private BlockListCommand $blockList; + + /** + * @var TransportInterfaceFactory + */ + private TransportInterfaceFactory $mailTransportFactory; + + /** + * @var Message + */ + private Message $message; + + /** + * @var ScopeConfigInterface + */ + private ScopeConfigInterface $scopeConfig; + + /** + * @param LoggerInterface $logger + * @param BlockListCommand $blockList + * @param TransportInterfaceFactory $mailTransportFactory + * @param Message $message + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + LoggerInterface $logger, + BlockListCommand $blockList, + TransportInterfaceFactory $mailTransportFactory, + Message $message, + ScopeConfigInterface $scopeConfig + ) { + $this->logger = $logger; + $this->blockList = $blockList; + $this->mailTransportFactory = $mailTransportFactory; + $this->message = $message; + $this->scopeConfig = $scopeConfig; + } + + /** + * call sendEmail function or write to system.log + * + * @return void + * @throws \Exception + */ + public function execute(): void + { + $changedBlocks = $this->blockList->getChangedEntries(); + if ($changedBlocks) { + $emailMessage = $this->blockList->renderValuesForEmail($changedBlocks); + $this->sendEmail($emailMessage); + } else { + $this->logger->info("No changed blocks found."); + } + } + + /** + * @param string $message + * @return void + */ + public function sendEmail(string $message): void + { + if ($this->hasRecipientEmail()) { + try { + $this->message->setFrom($this->scopeConfig->getValue(self::SENDER_EMAIL)); + $this->message->addTo($this->scopeConfig->getValue(self::RECIPIENT_EMAIL)); + $this->message->setSubject(self::EMAIL_SUBJECT); + $this->message->setBody($message); + $transport = $this->mailTransportFactory->create(['message' => $this->message]); + $transport->sendMessage(); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + } + } else { + $this->logger->error("Email not sent! No recipient email found."); + } + } + + /** + * Check if there is any email saved in the admin panel + * @return bool + */ + public function hasRecipientEmail(): bool + { + return $this->scopeConfig->getValue(self::RECIPIENT_EMAIL) ? true : false; + } +} diff --git a/Model/Console/BlockListCommand.php b/Model/Console/BlockListCommand.php index efbe56e453bb88478347a6309c57fa5778f8673c..4a9988d11ed330bf5918bc7c94f0d247c2c48bcc 100644 --- a/Model/Console/BlockListCommand.php +++ b/Model/Console/BlockListCommand.php @@ -1,42 +1,84 @@ <?php + declare(strict_types=1); namespace Firegento\ContentProvisioning\Model\Console; -use Firegento\ContentProvisioning\Api\Data\BlockEntryInterface; +use Firegento\ContentProvisioning\Model\Query\GetBlockEntryByKey; use Firegento\ContentProvisioning\Model\Query\GetBlockEntryList as GetBlockEntryList; use Firegento\ContentProvisioning\Model\Query\GetBlocksByBlockEntry as GetBlocksByBlockEntry; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\Io\File as FileSystem; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Magento\Framework\Filesystem\Driver\File; +use Firegento\ContentProvisioning\Model\Query\GetContentProvisioningFiles; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class BlockListCommand extends Command { + public const PARAM_CHANGED_ONLY = 'changed-only'; + /** * @var GetBlockEntryList */ - private $getAllBlockEntries; + private GetBlockEntryList $getAllBlockEntries; /** * @var GetBlocksByBlockEntry */ - private $getBlocksByBlockEntry; + private GetBlocksByBlockEntry $getBlocksByBlockEntry; + + /** + * @var File + */ + private File $fileDriver; + + /** + * @var GetBlockEntryByKey + */ + private GetBlockEntryByKey $getBlockEntryByKey; + + /** + * @var FileSystem + */ + private FileSystem $fileSystem; + + /** + * @var GetContentProvisioningFiles + */ + private GetContentProvisioningFiles $getContentProvisioningFiles; /** * @param GetBlockEntryList $getAllBlockEntries * @param GetBlocksByBlockEntry $getBlocksByBlockEntry + * @param File $fileDriver + * @param GetBlockEntryByKey $getBlockEntryByKey + * @param FileSystem $fileSystem + * @param GetContentProvisioningFiles $getContentProvisioningFiles * @param string|null $name */ public function __construct( GetBlockEntryList $getAllBlockEntries, GetBlocksByBlockEntry $getBlocksByBlockEntry, + File $fileDriver, + GetBlockEntryByKey $getBlockEntryByKey, + FileSystem $fileSystem, + GetContentProvisioningFiles $getContentProvisioningFiles, string $name = null ) { - parent::__construct($name); $this->getAllBlockEntries = $getAllBlockEntries; $this->getBlocksByBlockEntry = $getBlocksByBlockEntry; + $this->fileDriver = $fileDriver; + $this->getBlockEntryByKey = $getBlockEntryByKey; + $this->fileSystem = $fileSystem; + $this->getContentProvisioningFiles = $getContentProvisioningFiles; + parent::__construct($name); } /** @@ -44,23 +86,70 @@ class BlockListCommand extends Command * @param OutputInterface $output * @return void * + * @throws \Exception * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output) + { + if ($input->getOption(self::PARAM_CHANGED_ONLY)) { + $entries = $this->getChangedEntries(); + } else { + $entries = $this->getAllBlockEntries->get(); + } + + $this->renderTable($input, $output, $entries); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName('content-provisioning:block:list'); + $this->setDescription('List all configured CMS block entries'); + $this->addOption( + self::PARAM_CHANGED_ONLY, + null, + InputOption::VALUE_OPTIONAL, + 'Parameter to list (only) changed blocks.', + ); + parent::configure(); + } + + /** + * @return array|null + * @throws \Exception + */ + public function getChangedEntries(): ?array + { + $files = $this->getContentProvisioningFiles->execute(); + return $this->getChangedBlocksIdentifier($this->getBlockInformation($files)); + } + + /** + * Render table with CMS block information + * @param InputInterface $input + * @param OutputInterface $output + * @param array $entries + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function renderTable(InputInterface $input, OutputInterface $output, array $entries) { $table = new Table($output); $table->setHeaders(['Key', 'Identifier', 'Stores', 'Maintained', 'Active', 'Title', 'in DB (IDs)']); - foreach ($this->getAllBlockEntries->get() as $entry) { + foreach ($entries as $entry) { $table->addRow( [ - $entry->getKey(), - $entry->getIdentifier(), - implode(', ', $entry->getStores()), - $entry->isMaintained() ? 'yes' : 'no', - $entry->isActive() ? 'yes' : 'no', - $entry->getTitle(), - $this->getExistsInDbValue($entry), + $entry['key'], + $entry['identifier'], + (!empty($entry['stores'])) ? (is_array($entry['stores']) ? + implode(', ', $entry['stores']) : $entry['stores']) : "null", + $entry['is_maintained'], + $entry['is_active'], + $entry['title'], + $this->getExistsInDbValue($entry['key']), ] ); } @@ -69,21 +158,28 @@ class BlockListCommand extends Command } /** - * @inheritdoc + * Transform array values into a string to be displayed in the email + * @param array $entries + * @return string */ - protected function configure() + public function renderValuesForEmail(array $entries): string { - $this->setName('content-provisioning:block:list'); - $this->setDescription('List all configured CMS block entries'); - parent::configure(); + $entriesList = []; + foreach ($entries as $entry) { + $entriesList[] = "key: " . $entry['key']; + $entriesList[] = "identifier: " . $entry['identifier']; + } + $separatedEntries = implode(", ", $entriesList); + return $separatedEntries; } /** - * @param BlockEntryInterface $entry + * @param string $key * @return string */ - private function getExistsInDbValue(BlockEntryInterface $entry): string + private function getExistsInDbValue(string $key): string { + $entry = $this->getBlockEntryByKey->get($key); try { $ids = []; foreach ($this->getBlocksByBlockEntry->execute($entry) as $page) { @@ -99,4 +195,79 @@ class BlockListCommand extends Command return 'ERROR: ' . $noSuchEntityException->getMessage(); } } + + /** + * Return array with changed blocks identifier + * @param array $filesPath + * @return array|null + */ + public function getChangedBlocksIdentifier(array $filesPath): ?array + { + $changedBlocks = []; + foreach ($filesPath as $file) { + try { + // Get Block content from code + $filePath = $this->resolvePath($file['content']); + $codeFileContent = $this->fileDriver->fileGetContents($filePath); + } catch (LocalizedException $e) { + return 'ERROR: ' . $e->getMessage(); + } + // Get Block content from database + $dbBlockContent = $this->getBlockEntryByKey->get($file['key'])->getContent(); + + if ($codeFileContent !== $dbBlockContent) { + $changedBlocks[] = $file; + } + } + return $changedBlocks; + } + + /** + * Return array with block information from content_provisioning file + * @param array $files + * @return array + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + public function getBlockInformation(array $files): array + { + $results = []; + foreach ($files as $fileName => $fileContent) { + try { + $xml = new \SimpleXMLElement($fileContent); + + foreach ($xml->block as $block) { + $stores = (string)(isset($block->stores->store) && $block->stores->store->attributes() ? + $block->stores->store->attributes()->code : null); + $results[] = [ + 'identifier' => (string)$block['identifier'], + 'key' => (string)$block['key'], + 'content' => (string)$block->content, + 'stores' => $stores, + 'is_active' => (string)$block['active'] ? 'yes' : 'no', + 'is_maintained' => (string)$block['maintained'] ? 'yes' : 'no', + 'title' => $block->title + ]; + } + } catch (LocalizedException $e) { + error_log($e->getMessage()); + } + } + return $results; + } + + /** + * Transform given file path in PHP readble path + * @param string $filePath + * @return string + */ + public function resolvePath(string $filePath): string + { + $pathInfo = $this->fileSystem->getPathInfo($filePath); + $resolvedDirName = str_replace(['::', "_"], '/', $pathInfo['dirname']); + $fullPath= $resolvedDirName . "/" . $pathInfo['basename']; + $readblePath = $this->fileDriver->getRealPath("src/app/code/" . $fullPath); + + return $readblePath; + } } diff --git a/Model/Query/GetContentProvisioningFiles.php b/Model/Query/GetContentProvisioningFiles.php new file mode 100644 index 0000000000000000000000000000000000000000..c5cda9ebfa3137ba0de447268718b0222309abc6 --- /dev/null +++ b/Model/Query/GetContentProvisioningFiles.php @@ -0,0 +1,91 @@ +<?php + +declare(strict_types=1); + +/** + * Copyright (c) 2024 TechDivision GmbH + * All rights reserved + * + * This product includes proprietary software developed at TechDivision GmbH, Germany + * For more information see https://www.techdivision.com/ + * + * To obtain a valid license for using this software please contact us at + * license@techdivision.com + * + * @copyright Copyright (c) 2024 TechDivision GmbH (https://www.techdivision.com) + * @author TechDivision Team Zero <zero@techdivision.com> + * @link https://www.techdivision.com/ + */ + +namespace Firegento\ContentProvisioning\Model\Query; + +use Magento\Framework\Module\ModuleList; +use Magento\Framework\Module\Dir; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Exception\LocalizedException; + +class GetContentProvisioningFiles +{ + /** + * @var ModuleList + */ + private ModuleList $moduleList; + + /** + * @var Dir + */ + private Dir $moduleDirReader; + + /** + * @var File + */ + private File $fileDriver; + + /** + * @param ModuleList $moduleList + * @param Dir $moduleDirReader + * @param File $fileDriver + */ + public function __construct( + ModuleList $moduleList, + Dir $moduleDirReader, + File $fileDriver + ) { + $this->moduleList = $moduleList; + $this->moduleDirReader = $moduleDirReader; + $this->fileDriver = $fileDriver; + } + + /** + * @return array|string + * @throws \LogicException + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + public function execute() + { + $files = []; + $modules = $this->moduleList->getAll(); + + foreach ($modules as $moduleName => $moduleData) { + try { + $modulePath = $this->moduleDirReader->getDir($moduleName); + + // only app/code modules + if (!str_contains($modulePath, 'app/code')) { + continue; + } + + $moduleEtcPath = $this->moduleDirReader->getDir($moduleName, Dir::MODULE_ETC_DIR); + $provisionFilePath = $moduleEtcPath . DIRECTORY_SEPARATOR . 'content_provisioning.xml'; + + if ($this->fileDriver->isExists($provisionFilePath)) { + $content = $this->fileDriver->fileGetContents($provisionFilePath); + $files[$moduleName] = $content; + } + } catch (LocalizedException $e) { + return 'ERROR: ' . $e->getMessage(); + } + } + return $files; + } +} diff --git a/README.md b/README.md index 1a1dc178208ab32b6e09a067ee8e85aff7120708..8c309eec5f1adc809a488365191ae9685f662167 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,9 @@ bin/magento content-provisioning:page:apply "myKey" # list all configured CMS block entries bin/magento content-provisioning:block:list +# list all changed CMS block entries +bin/magento content-provisioning:block:list --changed-only all + # list all configured CMS page entries bin/magento content-provisioning:page:list ``` diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml new file mode 100644 index 0000000000000000000000000000000000000000..4e97659bebd745972986984b988d0bced20c007b --- /dev/null +++ b/etc/adminhtml/system.xml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright (c) 2024 TechDivision GmbH + * All rights reserved + * + * This product includes proprietary software developed at TechDivision GmbH, Germany + * For more information see https://www.techdivision.com/ + * + * To obtain a valid license for using this software please contact us at + * license@techdivision.com + * + * @copyright Copyright (c) 2024 TechDivision GmbH (https://www.techdivision.com) + * @author TechDivision Team Zero <zero@techdivision.com> + * @link https://www.techdivision.com/ + */ + --> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="trans_email" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1" + showInStore="1"> + <class>separator-top</class> + <label>Store Email Addresses</label> + <tab>general</tab> + <resource>Magento_Config::trans_email</resource> + <group id="ident_content_provisioning" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Content Provisioning</label> + <field id="email" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Recipient Email</label> + <comment>Emails here will receive a notification if there are some CMS blocks changed. For more than one email, insert comma to separate.</comment> + <validate>validate-email</validate> + <backend_model>Magento\Config\Model\Config\Backend\Email\Address</backend_model> + </field> + </group> + </section> + </system> +</config> diff --git a/etc/crontab.xml b/etc/crontab.xml new file mode 100644 index 0000000000000000000000000000000000000000..eaa21b2d13c1dd346ac5162aa30c4c6917301fc1 --- /dev/null +++ b/etc/crontab.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" ?> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd"> + <group id="default"> + <job instance="Firegento\ContentProvisioning\Cron\InconsistenciesInBlockEmailTest" method="execute" name="content_provisioning_cron"> + <schedule>* * * * *</schedule> + </job> + </group> +</config>