From b3c05a54a973baabf8540b9faa410dac5fa00956 Mon Sep 17 00:00:00 2001 From: Prestasafe Date: Mon, 9 Nov 2026 09:11:06 +0100 Subject: [PATCH 1/2] Prevent stale saves from restoring deleted repeater items --- _dev/src/components/RightPanel.vue | 19 ++++-- controllers/front/ajax.php | 10 +++ scripts/test-release.sh | 1 + tests/repeater-update-test.php | 99 ++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 tests/repeater-update-test.php diff --git a/_dev/src/components/RightPanel.vue b/_dev/src/components/RightPanel.vue index d6673bc7..1fea57e6 100644 --- a/_dev/src/components/RightPanel.vue +++ b/_dev/src/components/RightPanel.vue @@ -177,7 +177,6 @@ prettyBlocksContext.on('saveConfig', () => { }) prettyBlocksContext.on('saveSubState', () => { save() - prettyBlocksContext.initStates() }) /* @@ -185,16 +184,26 @@ prettyBlocksContext.on('saveSubState', () => { * */ const save = async (success = true) => { - let block = currentBlock.value; - let data = await prettyBlocksContext.updateSubSelectItem(state); + const context = snapshotBlockContext(prettyBlocksContext); + const { id_prettyblocks, subSelected, need_reload } = currentBlock.value; + const data = await prettyBlocksContext.updateSubSelectItem(state); + if (data.code === 'repeater_item_missing') { + if (sameBlockContext(context, snapshotBlockContext(prettyBlocksContext)) + && currentBlock.value.id_prettyblocks === id_prettyblocks + && currentBlock.value.subSelected === subSelected) { + prettyBlocksContext.displayError(data.message); + await prettyBlocksContext.initStates({ selectId: id_prettyblocks, context }); + } + return; + } if (data.success) { if (data.message && success) { prettyBlocksContext.displayMessage(data.message); } - if (block.need_reload) { + if (need_reload) { prettyBlocksContext.reloadIframe() } else { - prettyBlocksContext.sendPrettyBlocksEvents('reloadBlock', {id_prettyblocks: block.id_prettyblocks}) + prettyBlocksContext.sendPrettyBlocksEvents('reloadBlock', {id_prettyblocks}) } } prettyBlocksContext.initStates() diff --git a/controllers/front/ajax.php b/controllers/front/ajax.php index ef499ac7..852bb3fc 100644 --- a/controllers/front/ajax.php +++ b/controllers/front/ajax.php @@ -446,6 +446,16 @@ public function displayAjaxUpdateState() $formattedState = json_decode($stateRequest, true); $state_decoded = json_decode($state->state, true); + // The add action persists the item first; a save must never recreate a deleted key. + if (!is_array($state_decoded) || !array_key_exists($substate_key, $state_decoded)) { + exit(json_encode([ + 'success' => false, + 'saved' => false, + 'code' => 'repeater_item_missing', + 'message' => $this->translator->trans('This repeater item no longer exists.', [], 'Modules.Prettyblocks.Admin'), + ])); + } + $state_decoded[$substate_key] = $formattedState; $state->state = json_encode($state_decoded); diff --git a/scripts/test-release.sh b/scripts/test-release.sh index 9b844d3c..fac8d623 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -150,6 +150,7 @@ docker compose "${compose_files[@]}" up \ for service in ps82 ps91; do for fixture_and_target in \ + 'repeater-update-test:controllers/front/ajax.php' \ 'field-fileupload-test:classes/prettyblocks/core/FieldCore.php' \ 'date-field-test:classes/prettyblocks/core/FieldCore.php' \ 'editor-cache-test:prettyblocks.php'; do diff --git a/tests/repeater-update-test.php b/tests/repeater-update-test.php new file mode 100644 index 00000000..0ffaaa07 --- /dev/null +++ b/tests/repeater-update-test.php @@ -0,0 +1,99 @@ +state = self::$originalState; + } + + public function save() + { + self::$savedState = $this->state; + + return true; + } +} + +require $controllerFile; + +class RepeaterUpdateFixtureController extends PrettyBlocksAjaxModuleFrontController +{ + public function __construct() + { + $this->translator = new class { + public function trans($message, $parameters, $domain) + { + return $message; + } + }; + } +} + +$initialItems = [1 => ['question' => ['value' => 'Keep this']], 3 => ['question' => ['value' => 'Edit this']]]; +$updatedItem = ['question' => ['value' => 'Updated question']]; +PrettyBlocksModel::$originalState = $case === 'invalid-state' ? 'null' : json_encode($initialItems); +$_POST = [ + 'id_prettyblocks' => '12', + 'ctx_id_lang' => '1', + 'ctx_id_shop' => '1', + 'subSelected' => $case === 'existing' ? '3' : '2', + 'state' => json_encode($updatedItem), +]; + +ob_start(); +register_shutdown_function(function () use ($case, $initialItems, $updatedItem) { + $result = json_decode(ob_get_clean(), true); + if ($case === 'existing') { + $initialItems[3] = $updatedItem; + $valid = ($result['success'] ?? null) === true + && json_decode(PrettyBlocksModel::$savedState ?? '', true) === $initialItems; + } else { + $valid = ($result['success'] ?? null) === false + && ($result['code'] ?? null) === 'repeater_item_missing' + && PrettyBlocksModel::$savedState === null; + } + + if (!$valid) { + fwrite(STDERR, "Repeater update fixture failed: $case.\n"); + exit(1); + } + + echo "Repeater update fixture passed: $case.\n"; +}); + +(new RepeaterUpdateFixtureController())->displayAjaxUpdateState(); From c9869b58e1453e418edc3c26b2c29097d4fb13ee Mon Sep 17 00:00:00 2001 From: Prestasafe Date: Mon, 9 Nov 2026 09:13:54 +0100 Subject: [PATCH 2/2] Keep repeater and CMS regression test changes mergeable --- scripts/test-release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test-release.sh b/scripts/test-release.sh index fac8d623..f8bbaa2b 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -150,9 +150,9 @@ docker compose "${compose_files[@]}" up \ for service in ps82 ps91; do for fixture_and_target in \ - 'repeater-update-test:controllers/front/ajax.php' \ 'field-fileupload-test:classes/prettyblocks/core/FieldCore.php' \ 'date-field-test:classes/prettyblocks/core/FieldCore.php' \ + 'repeater-update-test:controllers/front/ajax.php' \ 'editor-cache-test:prettyblocks.php'; do fixture=${fixture_and_target%%:*} target=${fixture_and_target#*:}