From e366421120b67baa6bbd2cbe3f512bcd1b6b993e Mon Sep 17 00:00:00 2001 From: Francois Suter Date: Fri, 27 Dec 2024 11:34:37 +0100 Subject: [PATCH 1/3] [FEATURE] Add a delete reaction, resolves #361 --- ChangeLog | 4 + Classes/Domain/Repository/ItemRepository.php | 77 ++++++++ Classes/Event/GetExternalKeyEvent.php | 54 ++++++ .../InvalidConfigurationException.php | 25 +++ Classes/Exception/ReactionFailedException.php | 25 +++ Classes/Reaction/AbstractReaction.php | 110 +++++++++++ Classes/Reaction/DeleteReaction.php | 178 ++++++++++++++++++ Classes/Reaction/ImportReaction.php | 81 +------- Configuration/TCA/Overrides/sys_reaction.php | 55 ++++-- Documentation/Developer/Events/Index.rst | 30 +++ Documentation/Installation/Index.rst | 8 + Documentation/Introduction/Index.rst | 4 +- Documentation/KnownProblems/Index.rst | 7 +- Documentation/User/Reaction/Index.rst | 82 +++++++- Resources/Private/Language/locallang_db.xlf | 3 + 15 files changed, 637 insertions(+), 106 deletions(-) create mode 100644 Classes/Domain/Repository/ItemRepository.php create mode 100644 Classes/Event/GetExternalKeyEvent.php create mode 100644 Classes/Exception/InvalidConfigurationException.php create mode 100644 Classes/Exception/ReactionFailedException.php create mode 100644 Classes/Reaction/AbstractReaction.php create mode 100644 Classes/Reaction/DeleteReaction.php diff --git a/ChangeLog b/ChangeLog index e7c731ac..4bbf9961 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2024-12-27 Francois Suter (Idéative) + + * Add a delete reaction, resolves #361 + 2024-11-24 Francois Suter (Idéative) * Improve passing properly structured data to DatamapPostprocessEvent, references #355 diff --git a/Classes/Domain/Repository/ItemRepository.php b/Classes/Domain/Repository/ItemRepository.php new file mode 100644 index 00000000..95327d70 --- /dev/null +++ b/Classes/Domain/Repository/ItemRepository.php @@ -0,0 +1,77 @@ +getQueryBuilderForTable($table); + // Get any possible record, except an already deleted one + $queryBuilder->getRestrictions() + ->removeAll() + ->add( + GeneralUtility::makeInstance( + DeletedRestriction::class + ) + ); + $queryConstraints = []; + foreach ($constraints as $key => $value) { + $queryConstraints[] = $queryBuilder->expr()->eq( + $key, + is_string($value) ? $queryBuilder->createNamedParameter($value) : $value + ); + } + if (!empty($additionalConstraint)) { + $queryConstraints[] = $additionalConstraint; + } + $result = $queryBuilder->select('uid') + ->from($table) + ->where(...$queryConstraints) + ->executeQuery() + ->fetchOne(); + if ($result === false) { + throw new InvalidRecordException( + sprintf( + 'No record found in table "%s" matching "%s', + $table, + serialize($constraints) + ), + 1735288514 + ); + } + return $result; + } +} diff --git a/Classes/Event/GetExternalKeyEvent.php b/Classes/Event/GetExternalKeyEvent.php new file mode 100644 index 00000000..2fdeb5ce --- /dev/null +++ b/Classes/Event/GetExternalKeyEvent.php @@ -0,0 +1,54 @@ +data = $data; + $this->configuration = $configuration; + $this->externalKey = $externalKey; + } + + public function getData(): array + { + return $this->data; + } + + public function getConfiguration(): Configuration + { + return $this->configuration; + } + + public function getExternalKey() + { + return $this->externalKey; + } + + public function setExternalKey($externalKey): void + { + $this->externalKey = $externalKey; + } +} diff --git a/Classes/Exception/InvalidConfigurationException.php b/Classes/Exception/InvalidConfigurationException.php new file mode 100644 index 00000000..a78b2ac1 --- /dev/null +++ b/Classes/Exception/InvalidConfigurationException.php @@ -0,0 +1,25 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->eventDispatcher = $eventDispatcher; + } + + /** + * Prepares and returns the JSON response + * + * @param array $data + * @param int $statusCode + * @return ResponseInterface + */ + protected function jsonResponse(array $data, int $statusCode = 200): ResponseInterface + { + return $this->responseFactory + ->createResponse($statusCode) + ->withHeader('Content-Type', 'application/json') + ->withBody($this->streamFactory->createStream((string)json_encode($data))); + } + + /** + * Validates that the payloads contains the proper structure for the External Import reaction + * and that the configuration exists. + */ + protected function validatePayloadAndConfigurationKey(array $payload, string $configurationKey): ConfigurationKey + { + if (!isset($payload['data'])) { + throw new InvalidPayloadException( + 'The payload does not contain any data to import', + 1681482804 + ); + } + + if ($configurationKey === '') { + if (!isset($payload['table'], $payload['index'])) { + throw new InvalidPayloadException( + 'The payload must contain both a "table" and an "index" information', + 1681482506 + ); + } + + $configurationRepository = GeneralUtility::makeInstance(ConfigurationRepository::class); + + try { + $configurationRepository->findByTableAndIndex($payload['table'], $payload['index']); + } catch (NoConfigurationException $e) { + throw new InvalidPayloadException( + 'The "table" and "index" information given in the payload does not match an existing configuration', + 1681482838, + $e + ); + } + + $configurationKeyObject = GeneralUtility::makeInstance(ConfigurationKey::class); + $configurationKeyObject->setTableAndIndex($payload['table'], (string)$payload['index']); + } else { + if (isset($payload['table'], $payload['index'])) { + throw new InvalidPayloadException( + 'The payload must not contain a "table" and an "index" information', + 1726559649 + ); + } + + $configurationKeyObject = GeneralUtility::makeInstance(ConfigurationKey::class); + $configurationKeyObject->setConfigurationKey($configurationKey); + } + + return $configurationKeyObject; + } +} diff --git a/Classes/Reaction/DeleteReaction.php b/Classes/Reaction/DeleteReaction.php new file mode 100644 index 00000000..6593d315 --- /dev/null +++ b/Classes/Reaction/DeleteReaction.php @@ -0,0 +1,178 @@ +toArray()['external_import_configuration'] ?? ''); + try { + $configurationKeyObject = $this->validatePayloadAndConfigurationKey($payload, $configurationKey); + $deletedItems = $this->deleteItems($configurationKeyObject, $payload); + $responseBody = [ + 'success' => true, + 'message' => sprintf( + '%d item(s) successfully deleted', + $deletedItems + ), + ]; + return $this->jsonResponse($responseBody); + } catch (\Throwable $e) { + return $this->jsonResponse( + [ + 'success' => false, + 'error' => sprintf( + '%s [%d]', + $e->getMessage(), + $e->getCode() + ), + ], + 400 + ); + } + } + + /** + * Delete the items designated in the payload + * + * @throws \Cobweb\ExternalImport\Exception\NoConfigurationException + * @throws InvalidConfigurationException + * @throws ReactionFailedException + */ + protected function deleteItems(ConfigurationKey $configurationKey, array $payload): int + { + $deletedItems = 0; + // Get the corresponding configuration and validate it + $configurationRepository = GeneralUtility::makeInstance(ConfigurationRepository::class); + $configuration = $configurationRepository->findConfigurationObject( + $configurationKey->getTable(), + $configurationKey->getIndex() + ); + $validator = GeneralUtility::makeInstance(GeneralConfigurationValidator::class); + if ($validator->isValid($configuration)) { + $generalConfiguration = $configuration->getGeneralConfiguration(); + $externalKeyField = $generalConfiguration['referenceUid']; + + // Loop on the items, skip those with no external key + foreach ($payload['data'] as $item) { + $externalKey = $this->getExternalKey($configuration, $item); + if ($externalKey !== null) { + // Fetch the corresponding record from the database, applying relevant constraints from External Import + if (array_key_exists( + 'whereClause', + $generalConfiguration + ) && !empty($generalConfiguration['whereClause'])) { + $additionalConstraint = $generalConfiguration['whereClause']; + } else { + $additionalConstraint = ''; + } + $constraints = [ + $externalKeyField => $externalKey, + ]; + if ((bool)($generalConfiguration['enforcePid'] ?? false)) { + $constraints['pid'] = (int)$configuration->getStoragePid(); + } + $itemRepository = GeneralUtility::makeInstance(ItemRepository::class); + $itemId = $itemRepository->find( + $configurationKey->getTable(), + $constraints, + $additionalConstraint + ); + // Delete the selected record + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start( + [], + [ + $configurationKey->getTable() => [ + $itemId => [ + 'delete' => 1, + ], + ], + ], + ); + $dataHandler->process_cmdmap(); + // Check for errors + if (count($dataHandler->errorLog) > 0) { + throw new ReactionFailedException( + 'One or more errors occurred while trying to delete the item(s). Please refer to the TYPO3 log', + 1735291140 + ); + } + $deletedItems++; + } + } + } else { + throw new InvalidConfigurationException( + sprintf( + 'Invalid configuration for table %s, index %s. Please use the backend module to check it.', + $configurationKey->getTable(), + $configurationKey->getIndex() + ), + 1735286147 + ); + } + return $deletedItems; + } + + /** + * Extract the external key from the data, firing an event for further manipulation + */ + protected function getExternalKey(Configuration $configuration, array $data) + { + /** @var GetExternalKeyEvent $event */ + $event = $this->eventDispatcher->dispatch( + new GetExternalKeyEvent( + $data, + $configuration, + $data['external_id'] ?? null + ) + ); + return $event->getExternalKey(); + } +} diff --git a/Classes/Reaction/ImportReaction.php b/Classes/Reaction/ImportReaction.php index 67ebf404..8eacb3e4 100644 --- a/Classes/Reaction/ImportReaction.php +++ b/Classes/Reaction/ImportReaction.php @@ -17,32 +17,18 @@ namespace Cobweb\ExternalImport\Reaction; -use Cobweb\ExternalImport\Domain\Model\ConfigurationKey; -use Cobweb\ExternalImport\Domain\Repository\ConfigurationRepository; use Cobweb\ExternalImport\Exception\InvalidPayloadException; -use Cobweb\ExternalImport\Exception\NoConfigurationException; use Cobweb\ExternalImport\Importer; -use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\StreamFactoryInterface; use TYPO3\CMS\Core\Messaging\AbstractMessage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Reactions\Model\ReactionInstruction; use TYPO3\CMS\Reactions\Reaction\ReactionInterface; -class ImportReaction implements ReactionInterface +class ImportReaction extends AbstractReaction implements ReactionInterface { - protected ResponseFactoryInterface $responseFactory; - protected StreamFactoryInterface $streamFactory; - - public function __construct(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory) - { - $this->responseFactory = $responseFactory; - $this->streamFactory = $streamFactory; - } - public static function getType(): string { return 'import-external-data'; @@ -116,69 +102,4 @@ public function react(ServerRequestInterface $request, array $payload, ReactionI ); } } - - /** - * Validates that the payloads contains the proper structure for the External Import reaction - * and that the configuration exists. - */ - protected function validatePayloadAndConfigurationKey(array $payload, string $configurationKey): ConfigurationKey - { - if (!isset($payload['data'])) { - throw new InvalidPayloadException( - 'The payload does not contain any data to import', - 1681482804 - ); - } - - if ($configurationKey === '') { - if (!isset($payload['table'], $payload['index'])) { - throw new InvalidPayloadException( - 'The payload must contain both a "table" and an "index" information', - 1681482506 - ); - } - - $configurationRepository = GeneralUtility::makeInstance(ConfigurationRepository::class); - - try { - $configurationRepository->findByTableAndIndex($payload['table'], $payload['index']); - } catch (NoConfigurationException $e) { - throw new InvalidPayloadException( - 'The "table" and "index" information given in the payload does not match an existing configuration', - 1681482838, - $e - ); - } - - $configurationKeyObject = GeneralUtility::makeInstance(ConfigurationKey::class); - $configurationKeyObject->setTableAndIndex($payload['table'], (string)$payload['index']); - } else { - if (isset($payload['table'], $payload['index'])) { - throw new InvalidPayloadException( - 'The payload must not contain a "table" and an "index" information', - 1726559649 - ); - } - - $configurationKeyObject = GeneralUtility::makeInstance(ConfigurationKey::class); - $configurationKeyObject->setConfigurationKey($configurationKey); - } - - return $configurationKeyObject; - } - - /** - * Prepares and returns the JSON response - * - * @param array $data - * @param int $statusCode - * @return ResponseInterface - */ - protected function jsonResponse(array $data, int $statusCode = 200): ResponseInterface - { - return $this->responseFactory - ->createResponse($statusCode) - ->withHeader('Content-Type', 'application/json') - ->withBody($this->streamFactory->createStream((string)json_encode($data))); - } } diff --git a/Configuration/TCA/Overrides/sys_reaction.php b/Configuration/TCA/Overrides/sys_reaction.php index 533d3dd5..c07e3d79 100644 --- a/Configuration/TCA/Overrides/sys_reaction.php +++ b/Configuration/TCA/Overrides/sys_reaction.php @@ -1,20 +1,12 @@ ImportReaction::getDescription(), - 'value' => ImportReaction::getType(), - 'icon' => ImportReaction::getIconIdentifier(), - ], - ); - $GLOBALS['TCA']['sys_reaction']['ctrl']['typeicon_classes'][ImportReaction::getType()] = ImportReaction::getIconIdentifier(); - + // Add extra field listing external import configurations ExtensionManagementUtility::addTCAcolumns( 'sys_reaction', [ @@ -30,17 +22,28 @@ 'value' => '', ], ], - 'itemsProcFunc' => \Cobweb\ExternalImport\UserFunction\ConfigurationItems::class . '->listConfigurationItems', + 'itemsProcFunc' => ConfigurationItems::class . '->listConfigurationItems', ], ], ], ); - + // Add new palette $GLOBALS['TCA']['sys_reaction']['palettes']['externalImport'] = [ 'label' => 'LLL:EXT:external_import/Resources/Private/Language/locallang_db.xlf:sys_reaction.palette.additional', 'showitem' => 'external_import_configuration, --linebreak--, impersonate_user', ]; + // Declare import reaction + ExtensionManagementUtility::addTcaSelectItem( + 'sys_reaction', + 'reaction_type', + [ + 'label' => ImportReaction::getDescription(), + 'value' => ImportReaction::getType(), + 'icon' => ImportReaction::getIconIdentifier(), + ], + ); + $GLOBALS['TCA']['sys_reaction']['ctrl']['typeicon_classes'][ImportReaction::getType()] = ImportReaction::getIconIdentifier(); $GLOBALS['TCA']['sys_reaction']['types'][ImportReaction::getType()] = [ 'showitem' => ' --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general, @@ -55,4 +58,30 @@ ], ], ]; + + // Declare delete reaction + ExtensionManagementUtility::addTcaSelectItem( + 'sys_reaction', + 'reaction_type', + [ + 'label' => DeleteReaction::getDescription(), + 'value' => DeleteReaction::getType(), + 'icon' => DeleteReaction::getIconIdentifier(), + ], + ); + $GLOBALS['TCA']['sys_reaction']['ctrl']['typeicon_classes'][DeleteReaction::getType()] = DeleteReaction::getIconIdentifier(); + $GLOBALS['TCA']['sys_reaction']['types'][DeleteReaction::getType()] = [ + 'showitem' => ' + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general, + --palette--;;config, --palette--;;externalImport, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, + --palette--;;access', + 'columnsOverrides' => [ + 'impersonate_user' => [ + 'config' => [ + 'minitems' => 1, + ], + ], + ], + ]; } diff --git a/Documentation/Developer/Events/Index.rst b/Documentation/Developer/Events/Index.rst index 70a948d6..b32a72b1 100644 --- a/Documentation/Developer/Events/Index.rst +++ b/Documentation/Developer/Events/Index.rst @@ -281,3 +281,33 @@ Report .. php:method:: getImporter() Current instance of :php:`\Cobweb\ExternalImport\Importer`. + + +.. _developer-events-get-external-key: + +Report +"""""" + +.. php:namespace:: Cobweb\ExternalImport\Event + +.. php:class:: GetExternalKeyEvent + + This event is not related to the import process. It is triggered by the + "Delete external data" reaction. It makes it possible to retrieve the key + to the external data, if it is not stored in the "external_id" field as expected. + + .. php:method:: getConfiguration() + + Instance of :php:`\Cobweb\ExternalImport\Domain\Model\Configuration` with the targeted configuration. + + .. php:method:: getData() + + An array with the data for the item to delete. + + .. php:method:: getExternalKey() + + Value of the external key before the event is fired. It will be :code:`null` if the key was not found as expected. + + .. php:method:: setExternalKey() + + Use this method to set the value of the external key, once you have performed your custom processing of the data. diff --git a/Documentation/Installation/Index.rst b/Documentation/Installation/Index.rst index 84b2cae3..f8c9fb06 100644 --- a/Documentation/Installation/Index.rst +++ b/Documentation/Installation/Index.rst @@ -23,6 +23,14 @@ Upgrading and what's new ^^^^^^^^^^^^^^^^^^^^^^^^ +.. _installation-upgrade-730: + +Upgrade to 7.3.0 +"""""""""""""""" + +This version introduces a new reaction dedicated to deleting already import data. + + .. _installation-upgrade-720: Upgrade to 7.2.0 diff --git a/Documentation/Introduction/Index.rst b/Documentation/Introduction/Index.rst index 4248022a..b4a9c406 100644 --- a/Documentation/Introduction/Index.rst +++ b/Documentation/Introduction/Index.rst @@ -14,6 +14,8 @@ A backend module provides a way to synchronize any table manually or to define a scheduling for all synchronizations. Synchronizations can also be run using the command-line interface. Automatic scheduling can be defined using a Scheduler task. +Finally, this extension provides reactions (starting with TYPO3 12) +to import or delete data, responding to calls from remote sources. The main idea of getting external data into the TYPO3 CMS database is to be able to use TYPO3 CMS standard functions on that data @@ -26,7 +28,7 @@ called "connectors", the base of which is available as a separate extension Data from several external sources can be stored into the same table allowing data aggregation. -The extension also provides an API for sending it data from some other source. +The extension also provides an API for receiving data from some other source. This data is stored into the TYPO3 CMS database using the same mapping process as when data is fetched directly by the extension. diff --git a/Documentation/KnownProblems/Index.rst b/Documentation/KnownProblems/Index.rst index 4c87bcd8..5bfa78fc 100644 --- a/Documentation/KnownProblems/Index.rst +++ b/Documentation/KnownProblems/Index.rst @@ -6,11 +6,6 @@ Known problems -------------- -In order to support Reactions in TYPO3 12, a new database field was added to the -:code:`sys_reaction` table. Since this table does not exist in TYPO3 11, the Maintenance Tool -will create an incomplete :code:`sys_reaction` table with just that single field. -Since there's no associated TCA in TYPO3 11, this doesn't cause any problem. It's just -not very elegant. - +There are not currently any particular issues, except those already reported. In general please report bugs and improvements at: https://github.com/cobwebch/external_import/issues. diff --git a/Documentation/User/Reaction/Index.rst b/Documentation/User/Reaction/Index.rst index bf5efaf8..18e8f8be 100644 --- a/Documentation/User/Reaction/Index.rst +++ b/Documentation/User/Reaction/Index.rst @@ -3,11 +3,26 @@ .. _user-reaction: -Reaction (External Import endpoint) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Reactions (External Import endpoints) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using TYPO3 12, External Import provides reactions, i.e. endpoints +which can be called by any third-party software to push data to import or +to delete imported data. + +Both reactions are defined in the same way. The expected payload is different, +and this is explained further down. + +The "Import external data" reaction will import the data defined in the payload +as per the usual External Import process, inserting, updating and deteting records +by matching the incoming data set with the existing data set. However the import +reaction could also be used to import a single record (for example, if it used as +a webhook in a third-party application). In such a case, it is still easy +to insert or update, but the deleting of records cannot be automated anymore. +This is where the "Delete external data" reaction comes in. With it, one or more +records can be targeted for deletion, using their external primary key to identify +them. -When using TYPO3 12, External Import provides a reaction, i.e. an endpoint -which can be called by any third-party software to push data to import. .. _user-reaction-reaction: @@ -43,6 +58,13 @@ The External Import configuration does not need anything special to be used by a reaction. However if it is only ever used by reactions, then it does not need connector information and can thus be a :ref:`Non-synchronizable table `. +.. note:: + + When using the "Delete external data" reaction, matched data will be deleted even + if the "delete" operation is disabled in the configuration + (using :ref:`disabledOperations `). + It is assumed that you are meaning to delete anyway. + .. _user-reaction-payload: @@ -54,21 +76,48 @@ the URI given by the reaction and pass it the secret key in the headers. The pay in the request body is comprised of the following information: table - The name of the table targeted by the import (not necessary when a configuration is explicitly defined). + The name of the table targeted by the reaction (not necessary when a configuration is explicitly defined). index The index of the targeted External Import configuration (not necessary when a configuration is explicitly defined). data - The actual data to import. This can be either a JSON array (for + The data to handle. + + For the "Import external data" reaction, this can be either a JSON array (for :ref:`array-type data `) or a (XML) string for :ref:`XML-type data `). + For the "Delete external data" reaction, it must be a JSON array, with the item(s) to delete. + The key for identifying the external data must be in a field called "external_id". Example: + + .. code-block:: json + + { + "table": "tx_externalimporttest_tag", + "index": "api", + "data": [ + { + "external_id": "miraculous" + }, + { + "external_id": "rotten" + } + ] + } + + If the incoming data cannot match this structure (but is still a JSON array), + use the :ref:`GetExternalKeyEvent ` event + to extract the external key from the incoming data. If the incoming data does not + match the above structure at all, you have to develop your own reaction. + pid (optional) If defined, this uid from the "pages" table will override the :ref:`pid property ` from the general configuration. + This is not used by the "Delete external data" reaction. + Here is how it could look like (example made with Postman): .. figure:: ../../Images/ReactionRequestHeaders.png @@ -81,3 +130,24 @@ Here is how it could look like (example made with Postman): :alt: Request body The body of the payload with the table name, configuration index and data to import + + +.. _user-reaction-delete-reaction: + +The delete reaction +""""""""""""""""""" + +Since the "Delete external data" reaction is dedicated to deleting records, it is +quite different from the other bits of code in External Import. As far as reaction +payload is concerned, this has been discussed above. + +About the configuration, it is important to understand that most of the configuration +is not used by the delete process. In fact the only properties that are used from the +:ref:`general configuration ` are: + +- :ref:`referenceUid ` to know + in which field the external primary key is stored. +- :ref:`enforcePid `, which could be + useful is a scenario where you would import the same records to different places in your + TYPO3 installation, and thus have external primary keys which are unique only per pid. +- :ref:`whereClause ` diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 4664e31e..37e1248d 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -30,6 +30,9 @@ Import external data + + Delete external data + External Import configuration From 897aa5f8bd77e63878323d2b1b1787215336f7c6 Mon Sep 17 00:00:00 2001 From: Francois Suter Date: Fri, 27 Dec 2024 11:56:53 +0100 Subject: [PATCH 2/3] [BUGFIX] Avoid accessing substructure when not of the expected type, resolves #360 --- ChangeLog | 1 + Classes/Handler/ArrayHandler.php | 10 ++++++---- Classes/Handler/XmlHandler.php | 15 ++++++++++----- Tests/Unit/Handler/ArrayHandlerTest.php | 11 +++++++++++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/ChangeLog b/ChangeLog index 4bbf9961..f8500d4e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,7 @@ 2024-12-27 Francois Suter (Idéative) * Add a delete reaction, resolves #361 + * Avoid accessing substructure when not of the expected type, resolves #360 2024-11-24 Francois Suter (Idéative) diff --git a/Classes/Handler/ArrayHandler.php b/Classes/Handler/ArrayHandler.php index 46d92ee5..63f80e93 100644 --- a/Classes/Handler/ArrayHandler.php +++ b/Classes/Handler/ArrayHandler.php @@ -124,10 +124,12 @@ public function handleData($rawData, Importer $importer): array ) ); $theValue = $event->getSubstructure(); - $rows[$columnName] = $this->getSubstructureValues( - $theValue, - $columnData['substructureFields'] - ); + if (is_array($theValue)) { + $rows[$columnName] = $this->getSubstructureValues( + $theValue, + $columnData['substructureFields'] + ); + } // Prepare for the case where no substructure was found // If one was found, it is added later $data[$referenceCounter][$columnName] = null; diff --git a/Classes/Handler/XmlHandler.php b/Classes/Handler/XmlHandler.php index 04633096..3fa74672 100644 --- a/Classes/Handler/XmlHandler.php +++ b/Classes/Handler/XmlHandler.php @@ -109,11 +109,16 @@ public function handleData($rawData, Importer $importer): array ) ); $nodeList = $event->getSubstructure(); - $rows[$columnName] = $this->getSubstructureValues( - $nodeList, - $columnData['substructureFields'], - $xPathObject - ); + if ($nodeList instanceof \DOMNodeList) { + $rows[$columnName] = $this->getSubstructureValues( + $nodeList, + $columnData['substructureFields'], + $xPathObject + ); + } + // Prepare for the case where no substructure was found + // If one was found, it is added later + $data[$referenceCounter][$columnName] = null; } else { $data[$referenceCounter][$columnName] = $this->getValue( $theRecord, diff --git a/Tests/Unit/Handler/ArrayHandlerTest.php b/Tests/Unit/Handler/ArrayHandlerTest.php index b886b710..213a2a13 100644 --- a/Tests/Unit/Handler/ArrayHandlerTest.php +++ b/Tests/Unit/Handler/ArrayHandlerTest.php @@ -802,6 +802,17 @@ public function rawDataProvider(): array ], ], ], + 'raw data with general array path but invalid value type' => [ + 'generalConfiguration' => [ + 'arrayPath' => 'data/items', + ], + 'rawData' => [ + 'data' => [ + 'items' => 'This should be an array', + ], + ], + 'expectedStructure' => [], + ], 'raw data with general invalid array path (empty)' => [ 'generalConfiguration' => [ 'arrayPath' => '', From eabe7b08cdbab1822c87dc570c5562764aec7e94 Mon Sep 17 00:00:00 2001 From: Francois Suter Date: Fri, 27 Dec 2024 13:37:36 +0100 Subject: [PATCH 3/3] [TASK] Adapt functional test and release version 7.3.0 --- ChangeLog | 1 + Documentation/guides.xml | 4 ++-- Tests/Functional/ImporterPreviewTest.php | 16 ++++++++-------- ext_emconf.php | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/ChangeLog b/ChangeLog index f8500d4e..0839c263 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,7 @@ * Add a delete reaction, resolves #361 * Avoid accessing substructure when not of the expected type, resolves #360 + * Release version 7.3.0 2024-11-24 Francois Suter (Idéative) diff --git a/Documentation/guides.xml b/Documentation/guides.xml index 229115ea..e70c298c 100644 --- a/Documentation/guides.xml +++ b/Documentation/guides.xml @@ -12,8 +12,8 @@ /> diff --git a/Tests/Functional/ImporterPreviewTest.php b/Tests/Functional/ImporterPreviewTest.php index 1460ab48..edc09b98 100644 --- a/Tests/Functional/ImporterPreviewTest.php +++ b/Tests/Functional/ImporterPreviewTest.php @@ -219,9 +219,9 @@ public function handlePreviewProvider(): array 'categories' => 'USEFUL', 'created' => '2021-10-15 08:29:00', 'name' => 'Long sword', + 'pictures' => 'https://loremflickr.com/320/240/scotland', 'sku' => '000001', 'tags' => 'attack,metal', - 'pictures' => 'https://loremflickr.com/320/240/scotland', 'picture_title' => 'View from the left', 'picture_order' => '2', ], @@ -230,9 +230,9 @@ public function handlePreviewProvider(): array 'categories' => 'USEFUL', 'created' => '2021-10-15 08:29:00', 'name' => 'Long sword', + 'pictures' => 'https://loremflickr.com/320/240/volcano', 'sku' => '000001', 'tags' => 'attack,metal', - 'pictures' => 'https://loremflickr.com/320/240/volcano', 'picture_title' => 'View from above', 'picture_order' => '1', ], @@ -241,9 +241,9 @@ public function handlePreviewProvider(): array 'categories' => 'USEFUL', 'created' => '2021-10-15 08:29:00', 'name' => 'Long sword', + 'pictures' => 'https://sdnfjwrthioweorg.gdsg/wtf', 'sku' => '000001', 'tags' => 'attack,metal', - 'pictures' => 'https://sdnfjwrthioweorg.gdsg/wtf', 'picture_title' => 'View that does not exist', ], [ @@ -251,9 +251,9 @@ public function handlePreviewProvider(): array 'categories' => 'USEFUL', 'created' => '2021-08-26 12:43:00', 'name' => 'Chain mail', + 'pictures' => '', 'sku' => '000005', 'tags' => 'defense,metal', - 'pictures' => '', 'picture_title' => '', ], ], @@ -425,8 +425,8 @@ public function transformPreviewProvider(): array 'categories' => '', 'created' => 1634286540, 'name' => 'Long sword (base)', - 'sku' => '000001', 'pictures' => ImageTransformation::$previewMessage, + 'sku' => '000001', 'picture_title' => 'View from the left', 'picture_order' => '2', ], @@ -435,8 +435,8 @@ public function transformPreviewProvider(): array 'categories' => '', 'created' => 1634286540, 'name' => 'Long sword (base)', - 'sku' => '000001', 'pictures' => ImageTransformation::$previewMessage, + 'sku' => '000001', 'picture_title' => 'View from above', 'picture_order' => '1', ], @@ -445,8 +445,8 @@ public function transformPreviewProvider(): array 'categories' => '', 'created' => 1634286540, 'name' => 'Long sword (base)', - 'sku' => '000001', 'pictures' => ImageTransformation::$previewMessage, + 'sku' => '000001', 'picture_title' => 'View that does not exist', ], [ @@ -454,8 +454,8 @@ public function transformPreviewProvider(): array 'categories' => '', 'created' => 1629981780, 'name' => 'Chain mail (base)', - 'sku' => '000005', 'pictures' => null, + 'sku' => '000005', 'picture_title' => '', ], ], diff --git a/ext_emconf.php b/ext_emconf.php index 6e84ead5..e6f1d87e 100755 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,7 +11,7 @@ 'createDirs' => '', 'clearCacheOnLoad' => 0, 'author_company' => '', - 'version' => '7.2.9', + 'version' => '7.3.0', 'constraints' => [ 'depends' =>