Files
MokoSuiteBackup/source/packages/com_mokosuitebackup/api/src/Controller/BackupsController.php
T
Jonathan Miller 2395a4eabc
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
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
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

155 lines
4.0 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\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\ApiController;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
class BackupsController extends ApiController
{
protected $contentType = 'backups';
protected $default_view = 'backups';
/**
* Start a new backup (POST /api/index.php/v1/mokosuitebackup/backup)
*/
public function backup(): static
{
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
$this->app->setHeader('status', 403);
echo json_encode(['errors' => [['title' => 'Access denied']]]);
$this->app->close();
return $this;
}
$data = json_decode($this->input->json->getRaw(), true) ?: [];
$profileId = (int) ($data['profile'] ?? 1);
$description = $data['description'] ?? 'API backup ' . date('Y-m-d H:i:s');
$engine = new BackupEngine();
$result = $engine->run($profileId, $description, 'api');
if ($result['success']) {
$this->app->setHeader('status', 200);
echo json_encode(['data' => $result]);
} else {
$this->app->setHeader('status', 500);
echo json_encode(['errors' => [['title' => $result['message']]]]);
}
$this->app->close();
return $this;
}
/**
* Download a backup archive (GET /api/index.php/v1/mokosuitebackup/backup/:id/download)
*/
public function download(): static
{
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')) {
$this->app->setHeader('status', 403);
echo json_encode(['errors' => [['title' => 'Access denied']]]);
$this->app->close();
return $this;
}
$id = $this->input->getInt('id', 0);
$model = $this->getModel('Backup', 'Administrator');
$item = $model->getItem($id);
if (!$item || !$item->id || !$item->filesexist || !is_file($item->absolute_path)) {
$this->app->setHeader('status', 404);
echo json_encode(['errors' => [['title' => 'Backup file not found']]]);
$this->app->close();
return $this;
}
// Stream as binary download instead of base64 to avoid memory exhaustion
while (@ob_end_clean()) {
// clear all buffers
}
$filename = basename($item->archivename ?? $item->absolute_path);
$filesize = filesize($item->absolute_path);
$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');
readfile($item->absolute_path);
$this->app->close();
return $this;
}
/**
* List backup profiles (GET /api/index.php/v1/mokosuitebackup/profiles)
*/
public function profiles(): static
{
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) {
$this->app->setHeader('status', 403);
echo json_encode(['errors' => [['title' => 'Access denied']]]);
$this->app->close();
return $this;
}
$model = $this->getModel('Profiles', 'Administrator');
$items = $model->getItems();
$data = [];
// Strip sensitive credentials before serialization
$sensitiveFields = [
'ftp_password', 'ftp_username',
's3_access_key', 's3_secret_key',
'gdrive_client_secret', 'gdrive_refresh_token',
'encryption_password', 'ntfy_token',
];
foreach ($items as $item) {
$safe = clone $item;
foreach ($sensitiveFields as $field) {
if (isset($safe->$field) && $safe->$field !== '') {
$safe->$field = '***';
}
}
$data[] = [
'type' => 'profiles',
'id' => $safe->id,
'attributes' => $safe,
];
}
$this->app->setHeader('status', 200);
echo json_encode(['data' => $data]);
$this->app->close();
return $this;
}
}