fc41e1801a
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 26s
FolderPickerField: shows resolved placeholder values below input as badges (e.g. [HOME]=/home/user, [host]=example.com), plus full resolved path. Updates live as user types. BackupsController::start(): accept CSRF token from both GET and POST so the "Run Backup Now" link button on profile edit works without triggering "security token did not match" error.
227 lines
6.9 KiB
PHP
227 lines
6.9 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteBackup
|
|
* @subpackage com_mokosuitebackup
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\MVC\Controller\AdminController;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\CMS\Session\Session;
|
|
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
|
|
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RestoreEngine;
|
|
|
|
class BackupsController extends AdminController
|
|
{
|
|
protected $text_prefix = 'COM_MOKOJOOMBACKUP_BACKUPS';
|
|
|
|
public function getModel($name = 'Backup', $prefix = 'Administrator', $config = ['ignore_request' => true])
|
|
{
|
|
return parent::getModel($name, $prefix, $config);
|
|
}
|
|
|
|
/**
|
|
* Start a new backup using the specified profile.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function start(): void
|
|
{
|
|
/* Accept token from both GET (profile Run button) and POST (backup form).
|
|
Joomla's checkToken() throws on failure, so try GET first. */
|
|
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
|
$this->setMessage(Text::_('JINVALID_TOKEN_NOTICE'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
|
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
$profileId = $this->input->getInt('profile_id', 1);
|
|
$description = $this->input->getString('description', '');
|
|
|
|
$engine = new BackupEngine();
|
|
$result = $engine->run($profileId, $description, 'backend');
|
|
|
|
// Surface preflight warnings as Joomla messages
|
|
if (!empty($result['warnings'])) {
|
|
foreach ($result['warnings'] as $warning) {
|
|
$this->app->enqueueMessage($warning, 'warning');
|
|
}
|
|
}
|
|
|
|
if ($result['success']) {
|
|
$this->setMessage($result['message']);
|
|
} else {
|
|
$this->setMessage($result['message'], 'error');
|
|
}
|
|
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
}
|
|
|
|
/**
|
|
* Download a backup archive.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function download(): void
|
|
{
|
|
$this->checkToken('get');
|
|
|
|
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')) {
|
|
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
$id = $this->input->getInt('id', 0);
|
|
$model = $this->getModel('Backup');
|
|
$item = $model->getItem($id);
|
|
|
|
if (!$item || !$item->id || !$item->filesexist || !is_file($item->absolute_path)) {
|
|
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
// Flush any output buffers to prevent HTML mixing with binary data
|
|
while (@ob_end_clean()) {
|
|
// clear all buffers
|
|
}
|
|
|
|
$filename = basename($item->archivename);
|
|
$filesize = filesize($item->absolute_path);
|
|
|
|
// Detect content type from file extension
|
|
$contentType = str_ends_with($filename, '.tar.gz')
|
|
? 'application/gzip'
|
|
: 'application/zip';
|
|
|
|
header('Content-Type: ' . $contentType);
|
|
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename));
|
|
header('Content-Length: ' . $filesize);
|
|
header('Cache-Control: no-cache, must-revalidate');
|
|
header('Pragma: no-cache');
|
|
|
|
readfile($item->absolute_path);
|
|
|
|
$this->app->close();
|
|
}
|
|
|
|
/**
|
|
* Restore from a backup record.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function restore(): void
|
|
{
|
|
$this->checkToken();
|
|
|
|
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
|
|
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
$id = $this->input->getInt('id', 0);
|
|
$restoreFiles = (bool) $this->input->getInt('restore_files', 1);
|
|
$restoreDb = (bool) $this->input->getInt('restore_db', 1);
|
|
$preserveConfig = (bool) $this->input->getInt('preserve_config', 1);
|
|
$password = $this->input->getString('encryption_password', '');
|
|
|
|
if (!$id) {
|
|
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
$engine = new RestoreEngine();
|
|
$result = $engine->restore($id, $restoreFiles, $restoreDb, $preserveConfig, $password);
|
|
|
|
if ($result['success']) {
|
|
$this->setMessage($result['message']);
|
|
} else {
|
|
$this->setMessage($result['message'], 'error');
|
|
}
|
|
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
}
|
|
|
|
/**
|
|
* Verify integrity of a backup archive by re-computing SHA-256.
|
|
*/
|
|
public function verify(): void
|
|
{
|
|
$this->checkToken();
|
|
|
|
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) {
|
|
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
$cid = $this->input->get('cid', [], 'array');
|
|
$id = !empty($cid) ? (int) $cid[0] : $this->input->getInt('id', 0);
|
|
|
|
if (!$id) {
|
|
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
$model = $this->getModel('Backup');
|
|
$item = $model->getItem($id);
|
|
|
|
if (!$item || !$item->id) {
|
|
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
if (!is_file($item->absolute_path)) {
|
|
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND'), 'error');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
if (empty($item->checksum)) {
|
|
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM'), 'warning');
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
|
|
return;
|
|
}
|
|
|
|
$currentHash = hash_file('sha256', $item->absolute_path);
|
|
|
|
if ($currentHash === $item->checksum) {
|
|
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_VERIFY_OK'));
|
|
} else {
|
|
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_VERIFY_FAILED'), 'error');
|
|
}
|
|
|
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
|
}
|
|
}
|