chore: rename src/ to source/ per MokoStandards convention
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
Generic: Repo Health / Release configuration (push) Has been cancelled
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
Generic: Repo Health / Release configuration (push) Has been cancelled
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
Update all references in Makefile, manifest.xml, .gitignore, and CI workflows (ci-joomla, pr-check, repo-health) to use source/ as the primary directory with src/ as a fallback for compatibility. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokobackup
|
||||
* @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\MokoBackup\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
|
||||
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('#__mokobackup_records', 'r'))
|
||||
->join('LEFT', $db->quoteName('#__mokobackup_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 MokoBackup 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('mokobackup.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('#__mokobackup_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('#__mokobackup_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 default path
|
||||
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokobackup/backups';
|
||||
$backupDir = $defaultDir;
|
||||
|
||||
// If profiles use a custom directory, check that instead
|
||||
$db2 = $this->getDatabase();
|
||||
$qDir = $db2->getQuery(true)
|
||||
->select($db2->quoteName('backup_dir'))
|
||||
->from($db2->quoteName('#__mokobackup_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) {
|
||||
// Absolute paths used as-is, relative resolved from JPATH_ROOT
|
||||
if ($profileDir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $profileDir)) {
|
||||
$backupDir = rtrim($profileDir, '/\\');
|
||||
} else {
|
||||
$backupDir = JPATH_ROOT . '/' . $profileDir;
|
||||
}
|
||||
}
|
||||
|
||||
$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();
|
||||
$default = 'administrator/components/com_mokobackup/backups';
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__mokobackup_profiles'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote($default)
|
||||
. ' 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('#__mokobackup_profiles'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user