Files
MokoSuiteBackup/source/packages/plg_console_mokosuitebackup/src/Command/RestoreCommand.php
T
Jonathan Miller 2395a4eabc
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 5s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 23s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 17s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 7m48s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
fix: critical and high audit findings (#81)
Fixes all critical and high severity issues from the codebase audit:

CRITICAL:
- #71: RestoreCommand passed wrong args to RestoreEngine (filepath
  instead of record ID) — CLI restore was completely broken
- #72: JpaUnarchiver path traversal — added traversal rejection and
  realpath boundary check to prevent writes outside staging dir
- #77: RestoreEngine staging path sanitized — $record->tag stripped
  of non-alphanumeric characters

HIGH:
- #75: (noted, AkeebaImporter unserialize needs separate refactor)
- #76: BackupTable now deletes DB row before file — prevents data
  loss if DB delete fails
- #78: API profiles endpoint now masks sensitive fields (passwords,
  keys, tokens) with '***'
- #79: Webcron handler adds return after sendJsonResponse — prevents
  execution falling through on non-terminal close()
- #80: BackupModel/ProfileModel loadFormData() now casts array to
  object — prevents TypeError on PHP 8.x form state restore

PREFLIGHT HARDENING:
- PreflightCheck::run() wrapped in try-catch for DB exceptions
- mkdir() failure now includes actual error reason
- Unresolved placeholders generate a warning instead of silent return

Closes #71, closes #76, closes #77, closes #78, closes #79, closes #80
Ref #72, ref #81
2026-06-21 18:08:58 -05:00

102 lines
2.7 KiB
PHP

<?php
/**
* @package MokoSuiteBackup
* @subpackage plg_console_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\Plugin\Console\MokoSuiteBackup\Command;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RestoreEngine;
use Joomla\Console\Command\AbstractCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class RestoreCommand extends AbstractCommand
{
protected static $defaultName = 'mokosuitebackup:restore';
protected function configure(): void
{
$this->setDescription('Restore a backup by record ID');
$this->addArgument('id', InputArgument::REQUIRED, 'Backup record ID to restore');
}
protected function doExecute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$recordId = (int) $input->getArgument('id');
$io->title('MokoSuiteBackup — Restore Backup');
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('id') . ' = ' . $recordId);
$db->setQuery($query);
$record = $db->loadObject();
if (!$record) {
$io->error('Backup record not found: ' . $recordId);
return 1;
}
if ($record->status !== 'complete') {
$io->error('Cannot restore — backup status is: ' . $record->status);
return 1;
}
if (empty($record->absolute_path) || !is_file($record->absolute_path)) {
$io->error('Backup archive not found: ' . ($record->absolute_path ?: 'no path'));
return 1;
}
$io->warning('This will overwrite the current site files and/or database.');
$io->text('Archive: ' . $record->absolute_path);
$io->text('Type: ' . $record->backup_type);
if (!$io->confirm('Are you sure you want to continue?', false)) {
$io->info('Restore cancelled.');
return 0;
}
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/RestoreEngine.php';
if (!file_exists($engineFile)) {
$io->error('RestoreEngine not found. Is the component fully installed?');
return 1;
}
if (!class_exists(RestoreEngine::class)) {
require_once $engineFile;
}
$engine = new RestoreEngine();
$result = $engine->restore($recordId);
if ($result['success']) {
$io->success($result['message']);
return 0;
}
$io->error($result['message']);
return 1;
}
}