814d1b147c
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
- Create BackupDirectory utility class with centralized: - DEFAULT_RELATIVE constant and PLACEHOLDER constant - resolve() — path resolution with [DEFAULT_DIR] and relative path handling - hasPlaceholders() — check for unresolved placeholder tokens - isWebAccessible() — web-root boundary check - protect() — .htaccess and index.html creation with error logging - ensureReady() — mkdir + protect in one call - parseNewlineList() — newline-separated text parsing - logPathFromArchive() — derive .log path from archive path - Remove duplicated methods from BackupEngine, SteppedBackupEngine, ProfileTable, AjaxController, and DashboardModel - All consumers now use BackupDirectory static methods - Net reduction: ~180 lines of duplicated code eliminated
209 lines
6.2 KiB
PHP
209 lines
6.2 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoJoomBackup
|
|
* @subpackage com_mokojoombackup
|
|
* @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\MokoJoomBackup\Administrator\Model;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
|
|
|
|
class DashboardModel extends BaseDatabaseModel
|
|
{
|
|
/**
|
|
* Get the most recent completed backup record.
|
|
*
|
|
* @return object|null
|
|
*/
|
|
public function getLastBackup(): ?object
|
|
{
|
|
$db = $this->getDatabase();
|
|
$query = $db->getQuery(true)
|
|
->select('r.*, p.title AS profile_title')
|
|
->from($db->quoteName('#__mokojoombackup_records', 'r'))
|
|
->join('LEFT', $db->quoteName('#__mokojoombackup_profiles', 'p') . ' ON p.id = r.profile_id')
|
|
->where($db->quoteName('r.status') . ' = ' . $db->quote('complete'))
|
|
->order($db->quoteName('r.backupend') . ' DESC');
|
|
$db->setQuery($query, 0, 1);
|
|
|
|
return $db->loadObject() ?: null;
|
|
}
|
|
|
|
/**
|
|
* Query com_scheduler for the next scheduled MokoJoomBackup task.
|
|
*
|
|
* @return object|null Object with next_execution and title, or null
|
|
*/
|
|
public function getNextScheduled(): ?object
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
try {
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName(['t.next_execution', 't.title']))
|
|
->from($db->quoteName('#__scheduler_tasks', 't'))
|
|
->where($db->quoteName('t.type') . ' = ' . $db->quote('mokojoombackup.run_profile'))
|
|
->where($db->quoteName('t.state') . ' = 1')
|
|
->order($db->quoteName('t.next_execution') . ' ASC');
|
|
$db->setQuery($query, 0, 1);
|
|
|
|
return $db->loadObject() ?: null;
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get backup statistics.
|
|
*
|
|
* @return object Object with total_count, total_size, fail_count_7d
|
|
*/
|
|
public function getStats(): object
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
// Total completed backups and storage
|
|
$query = $db->getQuery(true)
|
|
->select('COUNT(*) AS total_count')
|
|
->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size')
|
|
->from($db->quoteName('#__mokojoombackup_records'))
|
|
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
|
$db->setQuery($query);
|
|
$stats = $db->loadObject();
|
|
|
|
// Failures in last 7 days
|
|
$cutoff = date('Y-m-d H:i:s', strtotime('-7 days'));
|
|
$query = $db->getQuery(true)
|
|
->select('COUNT(*) AS fail_count')
|
|
->from($db->quoteName('#__mokojoombackup_records'))
|
|
->where($db->quoteName('status') . ' = ' . $db->quote('fail'))
|
|
->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff));
|
|
$db->setQuery($query);
|
|
$stats->fail_count_7d = (int) $db->loadResult();
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* Check system health for backup readiness.
|
|
*
|
|
* @return array Array of check results [{label, status, detail}]
|
|
*/
|
|
public function getSystemHealth(): array
|
|
{
|
|
$checks = [];
|
|
|
|
// PHP version
|
|
$checks[] = (object) [
|
|
'label' => 'PHP Version',
|
|
'status' => version_compare(PHP_VERSION, '8.1.0', '>='),
|
|
'detail' => PHP_VERSION,
|
|
];
|
|
|
|
// ZipArchive extension
|
|
$checks[] = (object) [
|
|
'label' => 'ZipArchive',
|
|
'status' => extension_loaded('zip'),
|
|
'detail' => extension_loaded('zip') ? 'Loaded' : 'Not loaded',
|
|
];
|
|
|
|
// AES-256 encryption support
|
|
$aesSupport = defined('ZipArchive::EM_AES_256');
|
|
$checks[] = (object) [
|
|
'label' => 'AES-256 Encryption',
|
|
'status' => $aesSupport,
|
|
'detail' => $aesSupport ? 'Available' : 'Requires libzip 1.2.0+',
|
|
];
|
|
|
|
// Backup directory writable — check the first published profile's dir
|
|
$backupDir = BackupDirectory::getDefaultAbsolute();
|
|
|
|
$db2 = $this->getDatabase();
|
|
$qDir = $db2->getQuery(true)
|
|
->select($db2->quoteName('backup_dir'))
|
|
->from($db2->quoteName('#__mokojoombackup_profiles'))
|
|
->where($db2->quoteName('published') . ' = 1')
|
|
->where($db2->quoteName('backup_dir') . ' != ' . $db2->quote(''))
|
|
->where($db2->quoteName('backup_dir') . ' IS NOT NULL');
|
|
$db2->setQuery($qDir, 0, 1);
|
|
$profileDir = $db2->loadResult();
|
|
|
|
if ($profileDir) {
|
|
$backupDir = BackupDirectory::resolve($profileDir);
|
|
}
|
|
|
|
if (BackupDirectory::hasPlaceholders($backupDir)) {
|
|
$checks[] = (object) [
|
|
'label' => 'Backup Directory',
|
|
'status' => true,
|
|
'detail' => 'Uses placeholders (resolved at backup time) — ' . $backupDir,
|
|
];
|
|
} else {
|
|
$writable = is_dir($backupDir) && is_writable($backupDir);
|
|
$checks[] = (object) [
|
|
'label' => 'Backup Directory',
|
|
'status' => $writable,
|
|
'detail' => ($writable ? 'Writable' : 'Not writable or missing') . ' — ' . $backupDir,
|
|
];
|
|
}
|
|
|
|
// Disk space
|
|
$freeSpace = @disk_free_space($backupDir ?: JPATH_ROOT);
|
|
$freeGB = $freeSpace ? round($freeSpace / 1073741824, 1) : 0;
|
|
$checks[] = (object) [
|
|
'label' => 'Free Disk Space',
|
|
'status' => $freeGB >= 1.0,
|
|
'detail' => $freeGB . ' GB free',
|
|
];
|
|
|
|
return $checks;
|
|
}
|
|
|
|
/**
|
|
* Check if any profiles use the default (web-root) backup directory.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isUsingDefaultBackupDir(): bool
|
|
{
|
|
$db = $this->getDatabase();
|
|
$query = $db->getQuery(true)
|
|
->select('COUNT(*)')
|
|
->from($db->quoteName('#__mokojoombackup_profiles'))
|
|
->where($db->quoteName('published') . ' = 1')
|
|
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote(BackupDirectory::DEFAULT_RELATIVE)
|
|
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote(BackupDirectory::PLACEHOLDER)
|
|
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
|
|
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
|
|
$db->setQuery($query);
|
|
|
|
return (int) $db->loadResult() > 0;
|
|
}
|
|
|
|
/**
|
|
* Get published backup profiles for the quick-action selector.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getProfiles(): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName(['id', 'title', 'backup_type']))
|
|
->from($db->quoteName('#__mokojoombackup_profiles'))
|
|
->where($db->quoteName('published') . ' = 1')
|
|
->order($db->quoteName('ordering') . ' ASC');
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadObjectList() ?: [];
|
|
}
|
|
}
|