diff --git a/mokosuitebackup.php b/mokosuitebackup.php
deleted file mode 100644
index 2954662..0000000
--- a/mokosuitebackup.php
+++ /dev/null
@@ -1,11 +0,0 @@
-
- * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
- * @license GNU General Public License version 3 or later; see LICENSE
- */
-
-defined('_JEXEC') or die;
diff --git a/mokosuitebackup.xml b/mokosuitebackup.xml
deleted file mode 100644
index 962b0f7..0000000
--- a/mokosuitebackup.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- Web Services - MokoSuiteBackup
- 01.27.03
- 2026-06-02
- Moko Consulting
- hello@mokoconsulting.tech
- https://mokoconsulting.tech
- Copyright (C) 2026 Moko Consulting. All rights reserved.
- GPL-3.0-or-later
- PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION
-
- Joomla\Plugin\WebServices\MokoSuiteBackup
-
-
- mokosuitebackup.php
- services
- src
-
-
-
- language/en-GB/plg_webservices_mokosuitebackup.ini
- language/en-GB/plg_webservices_mokosuitebackup.sys.ini
-
-
diff --git a/source/packages/com_mokosuitebackup/forms/filter_backups.xml b/source/packages/com_mokosuitebackup/forms/filter_backups.xml
index 11af4cc..a44abd5 100644
--- a/source/packages/com_mokosuitebackup/forms/filter_backups.xml
+++ b/source/packages/com_mokosuitebackup/forms/filter_backups.xml
@@ -19,6 +19,18 @@
+
+
+
+
+
+
+
diff --git a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini
index 72366dd..bf9735b 100644
--- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini
+++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini
@@ -167,6 +167,7 @@ COM_MOKOJOOMBACKUP_STATUS_PENDING="Pending"
COM_MOKOJOOMBACKUP_FILTER_SEARCH="Search"
COM_MOKOJOOMBACKUP_FILTER_STATUS="Status"
COM_MOKOJOOMBACKUP_FILTER_STATUS_ALL="- Select Status -"
+COM_MOKOJOOMBACKUP_FILTER_TYPE_ALL="- Select Type -"
; Tabs and fieldsets
COM_MOKOJOOMBACKUP_TAB_GENERAL="General"
diff --git a/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php
index 4c2f63f..06254ed 100644
--- a/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php
+++ b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php
@@ -303,6 +303,20 @@ function actionExtract(array $data): array
$zip->setPassword($password);
}
+ // Validate all entries before extraction (path traversal protection)
+ for ($i = 0; $i < $zip->numFiles; $i++) {
+ $entryName = $zip->getNameIndex($i);
+
+ if ($entryName === false) {
+ continue;
+ }
+
+ if (str_contains($entryName, '../') || str_contains($entryName, '..\\') || str_starts_with($entryName, '/') || str_starts_with($entryName, '\\')) {
+ $zip->close();
+ throw new RuntimeException('Archive contains unsafe path: ' . $entryName);
+ }
+ }
+
if (!$zip->extractTo(RESTORE_DIR)) {
$zip->close();
throw new RuntimeException(
diff --git a/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php b/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php
index c95ae67..7ffd2b6 100644
--- a/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php
+++ b/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php
@@ -191,6 +191,20 @@ class RestoreEngine
$this->log('Decryption password set');
}
+ // Validate all entries before extraction (path traversal protection)
+ for ($i = 0; $i < $zip->numFiles; $i++) {
+ $entryName = $zip->getNameIndex($i);
+
+ if ($entryName === false) {
+ continue;
+ }
+
+ if (str_contains($entryName, '../') || str_contains($entryName, '..\\') || str_starts_with($entryName, '/') || str_starts_with($entryName, '\\')) {
+ $zip->close();
+ throw new \RuntimeException('Archive contains unsafe path: ' . $entryName);
+ }
+ }
+
if (!$zip->extractTo($this->stagingDir)) {
$zip->close();
@@ -210,6 +224,18 @@ class RestoreEngine
private function extractTarGz(string $archivePath): void
{
$phar = new \PharData($archivePath);
+
+ // Validate all entries before extraction (path traversal protection)
+ foreach (new \RecursiveIteratorIterator($phar) as $entry) {
+ $entryName = $entry->getPathname();
+ // PharData paths are prefixed with phar:// — extract the relative part
+ $relative = substr($entryName, strlen('phar://' . $archivePath) + 1);
+
+ if (str_contains($relative, '../') || str_contains($relative, '..\\') || str_starts_with($relative, '/') || str_starts_with($relative, '\\')) {
+ throw new \RuntimeException('Archive contains unsafe path: ' . $relative);
+ }
+ }
+
$phar->extractTo($this->stagingDir, null, true);
$this->log('Extracted tar.gz archive');
}
diff --git a/source/packages/com_mokosuitebackup/src/Model/BackupsModel.php b/source/packages/com_mokosuitebackup/src/Model/BackupsModel.php
index c7466d3..d3290ad 100644
--- a/source/packages/com_mokosuitebackup/src/Model/BackupsModel.php
+++ b/source/packages/com_mokosuitebackup/src/Model/BackupsModel.php
@@ -61,6 +61,13 @@ class BackupsModel extends ListModel
$query->where($db->quoteName('a.profile_id') . ' = ' . (int) $profileId);
}
+ // Filter by backup type
+ $backupType = $this->getState('filter.backup_type');
+
+ if (!empty($backupType)) {
+ $query->where($db->quoteName('a.backup_type') . ' = ' . $db->quote($backupType));
+ }
+
// Filter by search
$search = $this->getState('filter.search');