Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d104b7b936 | |||
| 80110ac111 | |||
| 3bd1f63833 | |||
| 93f0fa0a47 | |||
| 268b3d54d7 | |||
| 1cfe7c6c6e | |||
| f0da0c02b4 | |||
| 2f8a65388c | |||
| 9978622960 | |||
| 35e5fc1503 | |||
| 2338ba5197 | |||
| e67eedbc93 | |||
| d812aca832 | |||
| 4315f36c6a | |||
| 10467835ac | |||
| f26d58504e | |||
| 07fb4dcc24 | |||
| 21a4352b3b | |||
| 9d26f59f98 | |||
| 3488434f28 | |||
| f97cd30c95 | |||
| 836d1bc8b7 | |||
| 79b3caa35a | |||
| 6102c8f590 | |||
| 88e53c5698 | |||
| ec1c3486c5 |
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.43.07
|
||||
# VERSION: 01.43.22
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
+23
-1
@@ -2,6 +2,28 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Customizable restore script filename per backup profile (reduces discoverability on remote servers)
|
||||
- MokoRestore standalone mode: multi-ZIP selector when multiple backup archives are present
|
||||
- MokoRestore preflight: Joomla installation detection warning before overwriting an existing site
|
||||
- MokoRestore error handling: try/catch on fetch calls, HTTP status checks, JSON parse recovery
|
||||
- Download button on individual backup record detail toolbar
|
||||
- Profile column in backup records list links to the profile edit view
|
||||
|
||||
### Changed
|
||||
- Moved download, browse archive, and view log actions from backup list rows into the individual backup record view
|
||||
- Removed "Run Backup" / "Backup Now" buttons from profiles list, profile edit toolbar, and backup records view (backups are triggered from the dashboard only)
|
||||
- Removed ordering field from profiles; default sort is now by ID ascending
|
||||
- MokoRestore cleanup and security messages now reference the actual script filename instead of hardcoded "restore.php"
|
||||
|
||||
### Fixed
|
||||
- Bootstrap 5 modal conversion for snapshots view (data-bs-dismiss, modal-footer, getOrCreateInstance)
|
||||
- ntfy default URL changed from ntfy.sh to ntfy.mokoconsulting.tech
|
||||
- Untranslated JFIELD_ORDERING_ASC / JFIELD_ORDERING_LABEL language keys replaced with component-specific keys
|
||||
- Options page title now shows "MokoSuiteBackup Options" instead of raw language key
|
||||
- Profile dropdown IDs in backup records and dashboard show "#ID — Title (type)" format
|
||||
- MokoRestore stalling: unhandled promise rejections from network errors or non-JSON responses left UI in loading state
|
||||
|
||||
## [01.43.00] --- 2026-06-24
|
||||
|
||||
|
||||
@@ -71,7 +93,7 @@
|
||||
- Backup comparison: select two backups for side-by-side diff
|
||||
- Archive browser: view files inside backup without extracting
|
||||
- Manual purge: delete backups older than a date with count preview
|
||||
- Run Backup button on profile list and edit views with backup count badges
|
||||
- Backup count badges on profile list
|
||||
- "Do not navigate away" warning in backup/restore progress modals
|
||||
- Clickable placeholder pills for backup directory and archive name fields
|
||||
- Comprehensive help modal with absolute/relative/placeholder path documentation
|
||||
|
||||
@@ -24,10 +24,9 @@
|
||||
name="fullordering"
|
||||
type="list"
|
||||
label="JGLOBAL_SORT_BY"
|
||||
default="a.ordering ASC"
|
||||
default="a.id ASC"
|
||||
onchange="this.form.submit();"
|
||||
>
|
||||
<option value="a.ordering ASC">JFIELD_ORDERING_ASC</option>
|
||||
<option value="a.title ASC">COM_MOKOJOOMBACKUP_HEADING_TITLE_ASC</option>
|
||||
<option value="a.title DESC">COM_MOKOJOOMBACKUP_HEADING_TITLE_DESC</option>
|
||||
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
|
||||
|
||||
@@ -93,6 +93,16 @@
|
||||
<option value="1">COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED</option>
|
||||
<option value="standalone">COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE</option>
|
||||
</field>
|
||||
<field
|
||||
name="restore_script_name"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME_DESC"
|
||||
default="restore.php"
|
||||
maxlength="128"
|
||||
filter="string"
|
||||
showon="include_mokorestore!:0"
|
||||
/>
|
||||
<field
|
||||
name="encryption_password"
|
||||
type="password"
|
||||
@@ -164,12 +174,6 @@
|
||||
<option value="1">JPUBLISHED</option>
|
||||
<option value="0">JUNPUBLISHED</option>
|
||||
</field>
|
||||
<field
|
||||
name="ordering"
|
||||
type="number"
|
||||
label="JFIELD_ORDERING_LABEL"
|
||||
default="0"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="filters" label="COM_MOKOJOOMBACKUP_FIELDSET_FILTERS">
|
||||
|
||||
@@ -42,6 +42,8 @@ COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE_BREAKDOWN="Storage by Profile"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND="Backup Trend (30 days)"
|
||||
|
||||
; Backups view
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED="%d backup records deleted."
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED_1="%d backup record deleted."
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records"
|
||||
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
||||
@@ -140,6 +142,8 @@ COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="None: no restore script. Wrap
|
||||
COM_MOKOJOOMBACKUP_MOKORESTORE_NONE="None"
|
||||
COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED="Wrapped (inside backup ZIP)"
|
||||
COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE="Standalone (separate restore.php)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME="Restore Script Filename"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME_DESC="Custom filename for the restore script. Must end in .php. Use a non-obvious name to reduce discoverability on remote servers (e.g. moko-install-xyz.php)."
|
||||
|
||||
; Data Sanitization
|
||||
COM_MOKOJOOMBACKUP_FIELDSET_SANITIZATION="Data Sanitization"
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>MokoSuiteBackup</name>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.43.11 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.43.19 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.43.20 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.43.21 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.43.22 — no schema changes */
|
||||
@@ -259,14 +259,14 @@ class BackupEngine
|
||||
|
||||
// Step 2.5: MokoRestore script (if enabled)
|
||||
$mokoRestoreMode = $profile->include_mokorestore ?? '0';
|
||||
$restoreScriptName = $profile->restore_script_name ?? 'restore.php';
|
||||
$restoreScriptPath = '';
|
||||
|
||||
if ($mokoRestoreMode === '1') {
|
||||
// Wrapped mode: backup ZIP inside an outer ZIP with restore.php
|
||||
$this->log('Wrapping with MokoRestore script...');
|
||||
$mokoRestoreName = str_replace('.zip', '-mokorestore.zip', $archiveName);
|
||||
$mokoRestorePath = $this->backupDir . '/' . $mokoRestoreName;
|
||||
MokoRestore::wrap($archivePath, $mokoRestorePath);
|
||||
MokoRestore::wrap($archivePath, $mokoRestorePath, $restoreScriptName);
|
||||
|
||||
if (is_file($archivePath) && !unlink($archivePath)) {
|
||||
$this->log('WARNING: Could not remove pre-wrap archive');
|
||||
@@ -278,11 +278,11 @@ class BackupEngine
|
||||
$this->log('MokoRestore archive created: ' . $sizeHuman);
|
||||
$this->log('SHA-256 (wrapped): ' . $checksum);
|
||||
} elseif ($mokoRestoreMode === 'standalone') {
|
||||
// Standalone mode: restore.php as a separate file next to the backup ZIP
|
||||
$this->log('Generating standalone restore.php...');
|
||||
$restoreScriptPath = $this->backupDir . '/restore.php';
|
||||
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
|
||||
$this->log('Generating standalone ' . $restoreScriptName . '...');
|
||||
$restoreScriptPath = $this->backupDir . '/' . $restoreScriptName;
|
||||
MokoRestore::generateStandalone($restoreScriptPath);
|
||||
$this->log('Standalone restore.php generated (' . number_format(filesize($restoreScriptPath)) . ' bytes)');
|
||||
$this->log('Standalone ' . $restoreScriptName . ' generated (' . number_format(filesize($restoreScriptPath)) . ' bytes)');
|
||||
}
|
||||
|
||||
$remoteFilename = '';
|
||||
@@ -303,9 +303,8 @@ class BackupEngine
|
||||
$remoteFilename = $result['remote_path'] ?? $archiveName;
|
||||
$this->log(' Upload complete: ' . $result['message']);
|
||||
|
||||
/* Upload standalone restore.php if in standalone mode */
|
||||
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
|
||||
$uploader->upload($restoreScriptPath, 'restore.php');
|
||||
$uploader->upload($restoreScriptPath, basename($restoreScriptPath));
|
||||
}
|
||||
} else {
|
||||
$uploadFailed = true;
|
||||
@@ -336,15 +335,15 @@ class BackupEngine
|
||||
$remoteFilename = $uploadResult['remote_path'] ?? $archiveName;
|
||||
$this->log('Remote upload complete: ' . $uploadResult['message']);
|
||||
|
||||
// Upload standalone restore.php alongside the backup if in standalone mode
|
||||
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
|
||||
$this->log('Uploading standalone restore.php...');
|
||||
$restoreUpload = $uploader->upload($restoreScriptPath, 'restore.php');
|
||||
$restoreBasename = basename($restoreScriptPath);
|
||||
$this->log('Uploading standalone ' . $restoreBasename . '...');
|
||||
$restoreUpload = $uploader->upload($restoreScriptPath, $restoreBasename);
|
||||
|
||||
if ($restoreUpload['success']) {
|
||||
$this->log('Standalone restore.php uploaded');
|
||||
$this->log('Standalone ' . $restoreBasename . ' uploaded');
|
||||
} else {
|
||||
$this->log('WARNING: restore.php upload failed: ' . $restoreUpload['message']);
|
||||
$this->log('WARNING: ' . $restoreBasename . ' upload failed: ' . $restoreUpload['message']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,25 +35,36 @@ class MokoRestore
|
||||
*
|
||||
* @return string Path to the wrapped archive
|
||||
*/
|
||||
public static function wrap(string $backupArchive, string $outputPath): string
|
||||
public static function wrap(string $backupArchive, string $outputPath, string $scriptName = 'restore.php'): string
|
||||
{
|
||||
$scriptName = self::sanitizeScriptName($scriptName);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||
throw new \RuntimeException('Cannot create MokoRestore archive: ' . $outputPath);
|
||||
}
|
||||
|
||||
// Add the standalone restore script
|
||||
$zip->addFromString('restore.php', self::generateRestoreScript());
|
||||
|
||||
// Add the original backup as a nested ZIP
|
||||
$zip->addFromString($scriptName, self::generateRestoreScript());
|
||||
$zip->addFile($backupArchive, 'site-backup.zip');
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $outputPath;
|
||||
}
|
||||
|
||||
public static function sanitizeScriptName(string $name): string
|
||||
{
|
||||
$name = basename(trim($name));
|
||||
|
||||
if ($name === '' || !str_ends_with(strtolower($name), '.php')) {
|
||||
$name = 'restore.php';
|
||||
}
|
||||
|
||||
$name = preg_replace('/[^a-zA-Z0-9._-]/', '', $name);
|
||||
|
||||
return $name ?: 'restore.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the standalone restore.php script as a separate file.
|
||||
*
|
||||
@@ -173,7 +184,7 @@ SCANNER;
|
||||
'label' => 'Backup Archive',
|
||||
'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found',
|
||||
'ok' => file_exists(BACKUP_FILE),
|
||||
'hint' => 'site-backup.zip must be in the same directory as restore.php',
|
||||
'hint' => 'site-backup.zip must be in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
|
||||
];
|
||||
ORIG,
|
||||
<<<'REPL'
|
||||
@@ -191,7 +202,7 @@ ORIG,
|
||||
'label' => 'Backup Archive',
|
||||
'value' => $archiveValue,
|
||||
'ok' => $backupCount > 0,
|
||||
'hint' => 'Place one or more backup ZIP files in the same directory as restore.php',
|
||||
'hint' => 'Place one or more backup ZIP files in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
|
||||
];
|
||||
REPL
|
||||
);
|
||||
@@ -484,7 +495,7 @@ function actionPreflight(): array
|
||||
'label' => 'Backup Archive',
|
||||
'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found',
|
||||
'ok' => file_exists(BACKUP_FILE),
|
||||
'hint' => 'site-backup.zip must be in the same directory as restore.php',
|
||||
'hint' => 'site-backup.zip must be in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
|
||||
];
|
||||
|
||||
$checks[] = [
|
||||
@@ -1540,7 +1551,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
|
||||
<div class="mr-container">
|
||||
<div class="mr-alert mr-alert-danger">
|
||||
<strong>Security:</strong> Delete restore.php immediately after installation is complete.
|
||||
<strong>Security:</strong> Delete <code><?php echo htmlspecialchars(basename($_SERVER['SCRIPT_NAME'])); ?></code> immediately after installation is complete.
|
||||
</div>
|
||||
|
||||
<!-- Step Progress -->
|
||||
@@ -1788,7 +1799,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
<strong>Success!</strong> The site restoration is complete.
|
||||
</div>
|
||||
<div class="mr-alert mr-alert-danger">
|
||||
<strong>Important:</strong> Delete <code>restore.php</code> and <code>site-backup.zip</code> from your server immediately for security.
|
||||
<strong>Important:</strong> Delete <code><?php echo htmlspecialchars(basename($_SERVER['SCRIPT_NAME'])); ?></code> and <code>site-backup.zip</code> from your server immediately for security.
|
||||
</div>
|
||||
<div style="margin-top:1rem">
|
||||
<button class="mr-btn mr-btn-danger" onclick="runCleanup()">Remove Restore Files</button>
|
||||
@@ -1812,6 +1823,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
|
||||
<script>
|
||||
const TOKEN = <?php echo json_encode($token); ?>;
|
||||
const SCRIPT_URL = <?php echo json_encode(basename($_SERVER['SCRIPT_NAME'])); ?>;
|
||||
let currentStep = 1;
|
||||
let dbConfig = {};
|
||||
|
||||
@@ -1837,7 +1849,7 @@ async function post(action, extra) {
|
||||
}
|
||||
var res;
|
||||
try {
|
||||
res = await fetch('restore.php', { method: 'POST', body: form });
|
||||
res = await fetch(SCRIPT_URL, { method: 'POST', body: form });
|
||||
} catch (e) {
|
||||
log('Network error: ' + e.message);
|
||||
return { success: false, message: 'Network error: ' + e.message, checks: [] };
|
||||
|
||||
@@ -70,7 +70,8 @@ class SteppedBackupEngine
|
||||
$session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
|
||||
$session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
|
||||
$session->remoteStorage = $profile->remote_storage ?? 'none';
|
||||
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
|
||||
$session->includeMokoRestore = $profile->include_mokorestore ?? '0';
|
||||
$session->restoreScriptName = $profile->restore_script_name ?? 'restore.php';
|
||||
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
|
||||
|
||||
// Load multi-remote destinations from the remotes table
|
||||
@@ -377,15 +378,24 @@ class SteppedBackupEngine
|
||||
$this->verifyArchive($session->archivePath, $session->backupType);
|
||||
$session->log('Archive integrity verified');
|
||||
|
||||
// MokoRestore wrapper
|
||||
if ($session->includeMokoRestore) {
|
||||
// MokoRestore
|
||||
$mokoRestoreMode = $session->includeMokoRestore ?? '0';
|
||||
$restoreScriptName = $session->restoreScriptName ?? 'restore.php';
|
||||
|
||||
if ($mokoRestoreMode === '1') {
|
||||
$session->log('Wrapping with MokoRestore script...');
|
||||
$mokoRestorePath = $session->archivePath . '.mokorestore.zip';
|
||||
MokoRestore::wrap($session->archivePath, $mokoRestorePath);
|
||||
MokoRestore::wrap($session->archivePath, $mokoRestorePath, $restoreScriptName);
|
||||
@unlink($session->archivePath);
|
||||
rename($mokoRestorePath, $session->archivePath);
|
||||
$totalSize = filesize($session->archivePath);
|
||||
$session->log('MokoRestore archive created');
|
||||
} elseif ($mokoRestoreMode === 'standalone') {
|
||||
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
|
||||
$restoreDir = dirname($session->archivePath);
|
||||
$session->restoreScriptPath = $restoreDir . '/' . $restoreScriptName;
|
||||
MokoRestore::generateStandalone($session->restoreScriptPath);
|
||||
$session->log('Standalone ' . $restoreScriptName . ' generated');
|
||||
}
|
||||
|
||||
// Update record
|
||||
@@ -463,6 +473,10 @@ class SteppedBackupEngine
|
||||
if ($result['success']) {
|
||||
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
|
||||
$session->log(' Upload complete: ' . $result['message']);
|
||||
|
||||
if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) {
|
||||
$uploader->upload($session->restoreScriptPath, basename($session->restoreScriptPath));
|
||||
}
|
||||
} else {
|
||||
$uploadFailed = true;
|
||||
$session->log(' WARNING: Upload failed: ' . $result['message']);
|
||||
@@ -525,6 +539,12 @@ class SteppedBackupEngine
|
||||
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
|
||||
$session->log('Remote upload complete: ' . $result['message']);
|
||||
|
||||
if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) {
|
||||
$restoreBasename = basename($session->restoreScriptPath);
|
||||
$session->log('Uploading standalone ' . $restoreBasename . '...');
|
||||
$uploader->upload($session->restoreScriptPath, $restoreBasename);
|
||||
}
|
||||
|
||||
if (!$session->remoteKeepLocal && is_file($session->archivePath)) {
|
||||
@unlink($session->archivePath);
|
||||
$session->log('Local copy removed');
|
||||
|
||||
@@ -51,7 +51,9 @@ class SteppedSession
|
||||
public array $excludeFiles = [];
|
||||
public array $excludeTables = [];
|
||||
public string $remoteStorage = 'none';
|
||||
public bool $includeMokoRestore = false;
|
||||
public string $includeMokoRestore = '0';
|
||||
public string $restoreScriptName = 'restore.php';
|
||||
public string $restoreScriptPath = '';
|
||||
public bool $remoteKeepLocal = true;
|
||||
public string $encryptionPassword = '';
|
||||
|
||||
|
||||
@@ -29,7 +29,10 @@ class SshKeyField extends FormField
|
||||
$id = $this->id;
|
||||
$name = $this->name;
|
||||
|
||||
$hasKey = !empty($value) && str_contains($value, 'PRIVATE KEY');
|
||||
$decoded = !empty($value) ? (base64_decode($value, true) ?: '') : '';
|
||||
$hasKey = !empty($value) && ($value === '__KEEP_EXISTING__'
|
||||
|| str_contains($value, 'PRIVATE KEY')
|
||||
|| str_contains($decoded, 'PRIVATE KEY'));
|
||||
|
||||
$html = '<div id="' . htmlspecialchars($id) . '-wrapper">';
|
||||
|
||||
|
||||
@@ -60,14 +60,14 @@ class ProfilesModel extends ListModel
|
||||
$query->where('(' . $db->quoteName('a.title') . ' LIKE ' . $search . ')');
|
||||
}
|
||||
|
||||
$orderCol = $this->state->get('list.ordering', 'a.ordering');
|
||||
$orderCol = $this->state->get('list.ordering', 'a.id');
|
||||
$orderDir = $this->state->get('list.direction', 'ASC');
|
||||
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function populateState($ordering = 'a.ordering', $direction = 'ASC'): void
|
||||
protected function populateState($ordering = 'a.id', $direction = 'ASC'): void
|
||||
{
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,12 @@ namespace Joomla\Component\MokoSuiteBackup\Administrator\View\Backup;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
@@ -34,6 +38,24 @@ class HtmlView extends BaseHtmlView
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
|
||||
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
if ($this->item->status === 'complete'
|
||||
&& !empty($this->item->filesexist)
|
||||
&& $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')
|
||||
) {
|
||||
$toolbar = Toolbar::getInstance();
|
||||
$downloadUrl = Route::_(
|
||||
'index.php?option=com_mokosuitebackup&task=backups.download&id='
|
||||
. (int) $this->item->id . '&' . Session::getFormToken() . '=1'
|
||||
);
|
||||
$toolbar->linkButton('download', 'COM_MOKOJOOMBACKUP_DOWNLOAD')
|
||||
->url($downloadUrl)
|
||||
->icon('icon-download')
|
||||
->buttonClass('btn btn-success');
|
||||
}
|
||||
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuitebackup&view=backups');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ class HtmlView extends BaseHtmlView
|
||||
protected $state;
|
||||
public $filterForm;
|
||||
public $activeFilters = [];
|
||||
public $profiles = [];
|
||||
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
@@ -35,16 +34,6 @@ class HtmlView extends BaseHtmlView
|
||||
$this->filterForm = $this->get('FilterForm');
|
||||
$this->activeFilters = $this->get('ActiveFilters');
|
||||
|
||||
// Load published profiles for the backup selector
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName(['id', 'title', 'backup_type']))
|
||||
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
$db->setQuery($query);
|
||||
$this->profiles = $db->loadObjectList() ?: [];
|
||||
|
||||
$this->checkUpdateSite();
|
||||
$this->addToolbar();
|
||||
|
||||
@@ -112,10 +101,6 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
|
||||
|
||||
if ($user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
||||
ToolbarHelper::custom('backups.start', 'download', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW', false);
|
||||
}
|
||||
|
||||
if ($user->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
|
||||
ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE', true);
|
||||
}
|
||||
|
||||
@@ -55,16 +55,6 @@ class HtmlView extends BaseHtmlView
|
||||
$toolbar = Toolbar::getInstance();
|
||||
$profileId = (int) $this->item->id;
|
||||
|
||||
// "Run Backup Now" button — links to backup start with CSRF token
|
||||
if ($user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
||||
$runUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&task=backups.start&profile_id=' . $profileId . '&' . Session::getFormToken() . '=1');
|
||||
$toolbar->linkButton('run-backup', 'COM_MOKOJOOMBACKUP_RUN_BACKUP_NOW')
|
||||
->url($runUrl)
|
||||
->icon('icon-play')
|
||||
->buttonClass('btn btn-success');
|
||||
}
|
||||
|
||||
// "View Backups" link button
|
||||
$backupsUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&filter[PROFILE_ID]=' . $profileId);
|
||||
$toolbar->linkButton('view-backups', 'COM_MOKOJOOMBACKUP_VIEW_BACKUPS')
|
||||
->url($backupsUrl)
|
||||
|
||||
@@ -31,31 +31,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="j-main-container" class="j-main-container">
|
||||
<!-- Profile selector for Backup Now -->
|
||||
<?php $canRun = $user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup'); ?>
|
||||
<?php if (!empty($this->profiles) && $canRun) : ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body d-flex align-items-center gap-3">
|
||||
<label for="mb-profile-select" class="form-label mb-0 fw-bold">
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_BACKUP_PROFILE'); ?>:
|
||||
</label>
|
||||
<select id="mb-profile-select" class="form-select" style="max-width:300px;">
|
||||
<?php foreach ($this->profiles as $profile) : ?>
|
||||
<option value="<?php echo (int) $profile->id; ?>">
|
||||
#<?php echo (int) $profile->id; ?> —
|
||||
<?php echo $this->escape($profile->title); ?>
|
||||
(<?php echo $this->escape($profile->backup_type); ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="btn btn-primary" onclick="window.mokosuitebackupStart()">
|
||||
<span class="icon-download" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
|
||||
|
||||
<?php if (empty($this->items)) : ?>
|
||||
@@ -89,9 +64,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<th scope="col" class="w-10">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DATE', 'a.backupstart', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-5">
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-5">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
@@ -112,7 +84,9 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=profile.edit&id=' . (int) $item->profile_id); ?>">
|
||||
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
@@ -140,35 +114,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<td>
|
||||
<?php echo HTMLHelper::_('date', $item->backupstart, Text::_('DATE_FORMAT_LC4')); ?>
|
||||
</td>
|
||||
<td class="d-flex gap-1">
|
||||
<?php if ($item->status === 'complete' && $item->filesexist && $canDownload) : ?>
|
||||
<?php
|
||||
$isWebAccessible = !empty($item->absolute_path)
|
||||
&& strpos(realpath($item->absolute_path) ?: $item->absolute_path, realpath(JPATH_ROOT) ?: JPATH_ROOT) === 0;
|
||||
?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.download&id=' . $item->id . '&' . Session::getFormToken() . '=1'); ?>"
|
||||
class="btn btn-sm btn-outline-primary" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_DOWNLOAD'); ?>">
|
||||
<span class="icon-download"></span>
|
||||
</a>
|
||||
<?php if ($isWebAccessible) : ?>
|
||||
<span class="badge bg-warning text-dark" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING'); ?>">
|
||||
<span class="icon-warning-circle" aria-hidden="true"></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($item->status === 'complete' && $item->filesexist) : ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-info mb-browse-archive"
|
||||
data-id="<?php echo (int) $item->id; ?>"
|
||||
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>">
|
||||
<span class="icon-folder-open"></span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mb-view-log"
|
||||
data-id="<?php echo (int) $item->id; ?>"
|
||||
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?>">
|
||||
<span class="icon-file-alt"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo (int) $item->id; ?>
|
||||
</td>
|
||||
@@ -215,19 +160,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
|
||||
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
|
||||
|
||||
// Override the toolbar "Backup Now" button to use stepped backup
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Find the backup toolbar button and override it
|
||||
const toolbarBtn = document.querySelector('[onclick*="backups.start"], .button-download');
|
||||
if (toolbarBtn) {
|
||||
toolbarBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
startSteppedBackup();
|
||||
return false;
|
||||
}, true);
|
||||
}
|
||||
});
|
||||
|
||||
var backupRunning = false;
|
||||
|
||||
@@ -459,124 +391,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
}
|
||||
});
|
||||
|
||||
// View Log modal handler
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.mb-view-log');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
var recordId = btn.getAttribute('data-id');
|
||||
var modal = document.getElementById('mb-log-modal');
|
||||
var body = document.getElementById('mb-log-body');
|
||||
body.textContent = 'Loading...';
|
||||
bootstrap.Modal.getOrCreateInstance(modal).show();
|
||||
|
||||
var form = new URLSearchParams();
|
||||
form.append('task', 'ajax.viewLog');
|
||||
form.append('id', recordId);
|
||||
form.append(TOKEN_NAME, '1');
|
||||
|
||||
fetch(AJAX_URL, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.error) {
|
||||
body.textContent = data.message || 'Error loading log';
|
||||
} else {
|
||||
body.textContent = data.log;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
body.textContent = 'Error: ' + err.message;
|
||||
});
|
||||
});
|
||||
|
||||
// Log modal close handled by Bootstrap data-bs-dismiss
|
||||
|
||||
// Browse Archive modal handler
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
var units = ['B', 'KB', 'MB', 'GB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
if (i >= units.length) i = units.length - 1;
|
||||
return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function browseSetMessage(tbody, message, cssClass) {
|
||||
tbody.textContent = '';
|
||||
var tr = document.createElement('tr');
|
||||
var td = document.createElement('td');
|
||||
td.setAttribute('colspan', '3');
|
||||
td.className = cssClass || 'text-center';
|
||||
td.textContent = message;
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
function browseAddFileRow(tbody, file) {
|
||||
var tr = document.createElement('tr');
|
||||
|
||||
var tdName = document.createElement('td');
|
||||
tdName.style.wordBreak = 'break-all';
|
||||
tdName.style.fontSize = '0.85rem';
|
||||
var code = document.createElement('code');
|
||||
code.textContent = file.name;
|
||||
tdName.appendChild(code);
|
||||
tr.appendChild(tdName);
|
||||
|
||||
var tdSize = document.createElement('td');
|
||||
tdSize.className = 'text-end text-nowrap';
|
||||
tdSize.textContent = formatFileSize(file.size);
|
||||
tr.appendChild(tdSize);
|
||||
|
||||
var tdComp = document.createElement('td');
|
||||
tdComp.className = 'text-end text-nowrap';
|
||||
tdComp.textContent = formatFileSize(file.compressed_size);
|
||||
tr.appendChild(tdComp);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.mb-browse-archive');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
var recordId = btn.getAttribute('data-id');
|
||||
var modal = document.getElementById('mb-browse-modal');
|
||||
var tbody = document.getElementById('mb-browse-tbody');
|
||||
var summary = document.getElementById('mb-browse-summary');
|
||||
browseSetMessage(tbody, 'Loading...');
|
||||
summary.textContent = '';
|
||||
bootstrap.Modal.getOrCreateInstance(modal).show();
|
||||
|
||||
postAjax({ task: 'ajax.browseArchive', id: recordId })
|
||||
.then(function(data) {
|
||||
if (data.error) {
|
||||
browseSetMessage(tbody, data.message || 'Error', 'text-danger');
|
||||
return;
|
||||
}
|
||||
tbody.textContent = '';
|
||||
if (data.files.length === 0) {
|
||||
browseSetMessage(tbody, 'Archive is empty', 'text-center text-muted');
|
||||
} else {
|
||||
for (var i = 0; i < data.files.length; i++) {
|
||||
browseAddFileRow(tbody, data.files[i]);
|
||||
}
|
||||
}
|
||||
var text = data.total_files + ' files, ' + formatFileSize(data.total_size) + ' uncompressed';
|
||||
if (data.truncated) {
|
||||
text += ' (showing first ' + data.files.length + ')';
|
||||
}
|
||||
summary.textContent = text;
|
||||
})
|
||||
.catch(function(err) {
|
||||
browseSetMessage(tbody, 'Error: ' + err.message, 'text-danger');
|
||||
});
|
||||
});
|
||||
|
||||
// Browse modal close handled by Bootstrap data-bs-dismiss
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -654,52 +468,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Viewer Modal -->
|
||||
<div class="modal fade" id="mb-log-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" style="max-height:60vh; overflow-y:auto;">
|
||||
<pre id="mb-log-body" style="white-space:pre-wrap; word-break:break-word; font-size:0.85rem; margin:0; background:#f8f9fa; padding:1rem; border-radius:4px;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Archive Browser Modal -->
|
||||
<div class="modal fade" id="mb-browse-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<span class="icon-folder-open" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div class="px-3 py-2 bg-light border-bottom">
|
||||
<small id="mb-browse-summary" class="text-muted"></small>
|
||||
</div>
|
||||
<div style="max-height:60vh; overflow-y:auto;">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_NAME'); ?></th>
|
||||
<th class="text-end" style="width:100px;"><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_SIZE'); ?></th>
|
||||
<th class="text-end" style="width:120px;"><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_COMPRESSED'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="mb-browse-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Purge Backups Modal -->
|
||||
<?php $canDelete = $user->authorise('core.delete', 'com_mokosuitebackup'); ?>
|
||||
|
||||
@@ -52,9 +52,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<th scope="col" class="w-10">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.published', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-10 text-center">
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-5">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
@@ -87,16 +84,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<td>
|
||||
<?php echo HTMLHelper::_('jgrid.published', $item->published, $i, 'profiles.'); ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php if ($item->published == 1) : ?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups&task=backups.start&profile_id=' . $item->id . '&' . Session::getFormToken() . '=1'); ?>"
|
||||
class="btn btn-sm btn-outline-success"
|
||||
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_RUN_BACKUP'); ?>">
|
||||
<span class="icon-play" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_RUN_BACKUP'); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo (int) $item->id; ?>
|
||||
</td>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
-->
|
||||
<extension type="module" client="administrator" method="upgrade">
|
||||
<name>mod_mokosuitebackup_cpanel</name>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
-->
|
||||
<extension type="plugin" group="actionlog" method="upgrade">
|
||||
<name>Action Log - MokoSuiteBackup</name>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
-->
|
||||
<extension type="plugin" group="console" method="upgrade">
|
||||
<name>Console - MokoSuiteBackup</name>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
-->
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoSuiteBackup</name>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="quickicon" method="upgrade">
|
||||
<name>Quick Icon - MokoSuiteBackup</name>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteBackup</name>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteBackup</name>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteBackup</name>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteBackup</name>
|
||||
<packagename>mokosuitebackup</packagename>
|
||||
<version>01.43.07</version>
|
||||
<version>01.43.22</version>
|
||||
<version>01.43.22</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user