fix: scope module DELETE to snapshot IDs in replace mode
The truncateFiltered() method ran unfiltered DELETE FROM #__modules in replace mode, which would wipe ALL site modules (admin toolbar, login, menus) — not just the ones in the snapshot. Now scoped to only delete modules whose IDs exist in the snapshot data. Also scopes #__modules_menu delete to snapshot module IDs, and adds defense-in-depth validation of restore_mode in the controller.
This commit is contained in:
@@ -82,6 +82,11 @@ class SnapshotsController extends AdminController
|
||||
$mode = $this->input->getCmd('restore_mode', 'replace');
|
||||
$contentTypes = $this->input->get('restore_types', [], 'array');
|
||||
|
||||
// Enforce valid restore mode at controller boundary
|
||||
if (!in_array($mode, ['replace', 'merge'], true)) {
|
||||
$mode = 'replace';
|
||||
}
|
||||
|
||||
if (!$id) {
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_NO_RECORD'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=snapshots', false));
|
||||
|
||||
@@ -162,12 +162,12 @@ class SnapshotRestoreEngine
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace mode: truncate table, then insert all rows.
|
||||
* Replace mode: delete existing rows, then insert all snapshot rows.
|
||||
*/
|
||||
private function restoreReplace(object $db, string $realTable, string $abstractTable, array $rows): int
|
||||
{
|
||||
// Use DELETE instead of TRUNCATE to stay within transaction
|
||||
$this->truncateFiltered($db, $realTable, $abstractTable);
|
||||
$this->truncateFiltered($db, $realTable, $abstractTable, $rows);
|
||||
|
||||
$count = 0;
|
||||
|
||||
@@ -221,13 +221,16 @@ class SnapshotRestoreEngine
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete rows from a table, filtering to relevant content where needed.
|
||||
* Delete rows from a table, scoping to relevant content only.
|
||||
*
|
||||
* Shared tables (#__categories, #__modules, etc.) are filtered so
|
||||
* only the rows belonging to our content types are deleted — never
|
||||
* the entire table.
|
||||
*/
|
||||
private function truncateFiltered(object $db, string $realTable, string $abstractTable): void
|
||||
private function truncateFiltered(object $db, string $realTable, string $abstractTable, array $rows): void
|
||||
{
|
||||
$query = $db->getQuery(true)->delete($db->quoteName($realTable));
|
||||
|
||||
// Only delete com_content rows from shared tables
|
||||
switch ($abstractTable) {
|
||||
case '#__categories':
|
||||
$query->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'));
|
||||
@@ -241,7 +244,31 @@ class SnapshotRestoreEngine
|
||||
$query->where($db->quoteName('type_alias') . ' LIKE ' . $db->quote('com_content.%'));
|
||||
break;
|
||||
|
||||
// These tables are fully owned by the content type
|
||||
case '#__modules':
|
||||
// Only delete modules that exist in the snapshot — never wipe all site modules
|
||||
$ids = array_filter(array_column($rows, 'id'));
|
||||
|
||||
if (empty($ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = array_map('intval', $ids);
|
||||
$query->where($db->quoteName('id') . ' IN (' . implode(',', $ids) . ')');
|
||||
break;
|
||||
|
||||
case '#__modules_menu':
|
||||
// Only delete menu assignments for modules in the snapshot
|
||||
$moduleIds = array_filter(array_column($rows, 'moduleid'));
|
||||
|
||||
if (empty($moduleIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$moduleIds = array_map('intval', array_unique($moduleIds));
|
||||
$query->where($db->quoteName('moduleid') . ' IN (' . implode(',', $moduleIds) . ')');
|
||||
break;
|
||||
|
||||
// #__content and #__content_frontpage are fully owned by com_content
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user