4b9a675d0f
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 36s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 40s
Generic: Project CI / Tests (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (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
All Joomla element names, PHP classes, language files, folder structure, and manifest references renamed from mokosuite to mokosuiteclient. This repo is now the client-facing tracker for the MokoSuite platform.
640 lines
18 KiB
PHP
640 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* @package MokoSuiteClient
|
|
* @subpackage com_mokosuiteclient
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
use Joomla\CMS\Version;
|
|
|
|
class DashboardModel extends BaseDatabaseModel
|
|
{
|
|
/**
|
|
* Feature plugin metadata keyed by element name.
|
|
* Provides icon, category, and description for dashboard display.
|
|
*/
|
|
private const PLUGIN_META = [
|
|
'mokosuiteclient' => [
|
|
'icon' => 'icon-shield-alt',
|
|
'category' => 'core',
|
|
'label' => 'Core',
|
|
'description' => 'Heartbeat, health monitoring, site aliases, extension coordination, and download key preservation.',
|
|
'protected' => true,
|
|
'configure_only' => false,
|
|
],
|
|
'mokosuiteclient_firewall' => [
|
|
'icon' => 'icon-lock',
|
|
'category' => 'security',
|
|
'label' => 'Firewall',
|
|
'description' => 'Web Application Firewall — SQLi, XSS, RFI, DFI shields, IP blocklist, admin secret URL, file protection.',
|
|
'protected' => false,
|
|
'configure_only' => false,
|
|
],
|
|
'mokosuiteclient_tenant' => [
|
|
'icon' => 'icon-users',
|
|
'category' => 'security',
|
|
'label' => 'Tenant Restrictions',
|
|
'description' => 'Installer, sysinfo, config, and template access restrictions for non-master users.',
|
|
'protected' => false,
|
|
'configure_only' => false,
|
|
],
|
|
'mokosuiteclient_offline' => [
|
|
'icon' => 'icon-globe',
|
|
'category' => 'security',
|
|
'label' => 'Offline Bypass',
|
|
'description' => 'Keep selected pages (TOS, Privacy Policy) accessible during offline mode.',
|
|
'protected' => false,
|
|
'configure_only' => true,
|
|
],
|
|
'mokosuiteclient_devtools' => [
|
|
'icon' => 'icon-wrench',
|
|
'category' => 'tools',
|
|
'label' => 'Developer Tools',
|
|
'description' => 'Dev mode, hit counter reset, content version cleanup. Features are controlled inside the plugin settings.',
|
|
'protected' => false,
|
|
'configure_only' => true,
|
|
],
|
|
'mokosuiteclientdemo' => [
|
|
'icon' => 'icon-undo',
|
|
'category' => 'content',
|
|
'label' => 'Demo Reset Task',
|
|
'description' => 'Scheduled demo site reset with content snapshots.',
|
|
'protected' => false,
|
|
'configure_only' => true,
|
|
],
|
|
'mokosuiteclientsync' => [
|
|
'icon' => 'icon-sync',
|
|
'category' => 'content',
|
|
'label' => 'Content Sync Task',
|
|
'description' => 'Scheduled content synchronisation to remote MokoSuiteClient sites.',
|
|
'protected' => false,
|
|
'configure_only' => true,
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Category display labels and colours.
|
|
*/
|
|
private const CATEGORIES = [
|
|
'core' => ['label' => 'Core', 'badge' => 'bg-dark'],
|
|
'security' => ['label' => 'Security', 'badge' => 'bg-danger'],
|
|
'tools' => ['label' => 'Tools', 'badge' => 'bg-info'],
|
|
'monitoring' => ['label' => 'Monitoring', 'badge' => 'bg-success'],
|
|
'content' => ['label' => 'Content', 'badge' => 'bg-primary'],
|
|
'api' => ['label' => 'API', 'badge' => 'bg-secondary'],
|
|
];
|
|
|
|
/**
|
|
* Discover all installed MokoSuiteClient plugins.
|
|
*
|
|
* @return array Plugin rows enriched with dashboard metadata.
|
|
*/
|
|
public function getFeaturePlugins(): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
$db->quoteName('extension_id'),
|
|
$db->quoteName('name'),
|
|
$db->quoteName('element'),
|
|
$db->quoteName('folder'),
|
|
$db->quoteName('type'),
|
|
$db->quoteName('enabled'),
|
|
$db->quoteName('protected'),
|
|
$db->quoteName('params'),
|
|
$db->quoteName('manifest_cache'),
|
|
])
|
|
->from($db->quoteName('#__extensions'))
|
|
->where([
|
|
'(' .
|
|
// System plugins: mokosuiteclient, mokosuiteclient_*
|
|
'(' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
|
|
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system')
|
|
. ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient')
|
|
. ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuiteclient\_%') . ')'
|
|
. ' AND ' . $db->quoteName('element') . ' != ' . $db->quote('mokosuiteclient_monitor') . ')'
|
|
// Webservices plugins
|
|
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
|
|
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices')
|
|
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient') . ')'
|
|
// Task plugins
|
|
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
|
|
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('task')
|
|
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuiteclient%') . ')'
|
|
. ')',
|
|
])
|
|
->order($db->quoteName('folder') . ' ASC, ' . $db->quoteName('element') . ' ASC');
|
|
|
|
$db->setQuery($query);
|
|
$rows = $db->loadObjectList() ?: [];
|
|
|
|
$plugins = [];
|
|
|
|
foreach ($rows as $row)
|
|
{
|
|
$manifest = json_decode($row->manifest_cache ?? '{}');
|
|
$version = $manifest->version ?? '';
|
|
|
|
// Only system plugins and task plugins match PLUGIN_META by element
|
|
$metaKey = ($row->folder === 'system' || $row->folder === 'task')
|
|
? $row->element
|
|
: $row->folder . '_' . $row->element;
|
|
|
|
$meta = self::PLUGIN_META[$metaKey] ?? null;
|
|
|
|
// Auto-generate meta for task/webservices plugins not in the map
|
|
if (!$meta)
|
|
{
|
|
$meta = $this->guessPluginMeta($row);
|
|
}
|
|
|
|
$categoryKey = $meta['category'] ?? 'tools';
|
|
$categoryInfo = self::CATEGORIES[$categoryKey] ?? self::CATEGORIES['tools'];
|
|
|
|
$plugins[] = (object) [
|
|
'extension_id' => (int) $row->extension_id,
|
|
'name' => $meta['label'] ?? $row->name,
|
|
'element' => $row->element,
|
|
'folder' => $row->folder,
|
|
'type' => $row->type,
|
|
'enabled' => (int) $row->enabled,
|
|
'protected' => (bool) ($meta['protected'] ?? false),
|
|
'configure_only' => (bool) ($meta['configure_only'] ?? false),
|
|
'version' => $version,
|
|
'icon' => $meta['icon'] ?? 'icon-puzzle-piece',
|
|
'category' => $categoryKey,
|
|
'categoryLabel' => $categoryInfo['label'],
|
|
'categoryBadge' => $categoryInfo['badge'],
|
|
'description' => $meta['description'] ?? '',
|
|
];
|
|
}
|
|
|
|
return $plugins;
|
|
}
|
|
|
|
/**
|
|
* Get basic site information for the info bar.
|
|
*
|
|
* @return object
|
|
*/
|
|
public function getSiteInfo(): object
|
|
{
|
|
$app = Factory::getApplication();
|
|
$config = $app->getConfig();
|
|
$db = $this->getDatabase();
|
|
|
|
// Get MokoSuiteClient package version
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('manifest_cache'))
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokosuiteclient'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('package'));
|
|
$db->setQuery($query);
|
|
$pkgCache = json_decode($db->loadResult() ?? '{}');
|
|
|
|
return (object) [
|
|
'sitename' => $config->get('sitename', ''),
|
|
'joomla_version' => (new Version())->getShortVersion(),
|
|
'php_version' => PHP_VERSION,
|
|
'db_type' => $db->getServerType(),
|
|
'mokosuiteclient_version' => $pkgCache->version ?? '—',
|
|
'debug' => (bool) $config->get('debug'),
|
|
'offline' => (bool) $config->get('offline'),
|
|
'sef' => (bool) $config->get('sef'),
|
|
'caching' => (int) $config->get('caching'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get installed MokoSuiteClient component and modules with versions.
|
|
*
|
|
* @return array Array of extension objects with name, element, type, version.
|
|
*/
|
|
public function getMokoExtensions(): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
$db->quoteName('element'),
|
|
$db->quoteName('name'),
|
|
$db->quoteName('type'),
|
|
$db->quoteName('enabled'),
|
|
$db->quoteName('manifest_cache'),
|
|
])
|
|
->from($db->quoteName('#__extensions'))
|
|
->where('('
|
|
// The component
|
|
. '(' . $db->quoteName('type') . ' = ' . $db->quote('component')
|
|
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_mokosuiteclient') . ')'
|
|
// Admin modules
|
|
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('module')
|
|
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mod_mokosuiteclient%') . ')'
|
|
. ')')
|
|
->order($db->quoteName('type') . ' ASC, ' . $db->quoteName('element') . ' ASC');
|
|
|
|
$db->setQuery($query);
|
|
$rows = $db->loadObjectList() ?: [];
|
|
|
|
$extensions = [];
|
|
|
|
foreach ($rows as $row)
|
|
{
|
|
$manifest = json_decode($row->manifest_cache ?? '{}');
|
|
|
|
$extensions[] = (object) [
|
|
'element' => $row->element,
|
|
'name' => $manifest->name ?? $row->name,
|
|
'type' => $row->type,
|
|
'version' => $manifest->version ?? '',
|
|
'enabled' => (int) $row->enabled,
|
|
];
|
|
}
|
|
|
|
return $extensions;
|
|
}
|
|
|
|
/**
|
|
* Toggle a plugin's enabled state.
|
|
*
|
|
* @param int $extensionId The extension ID.
|
|
* @param int $enabled 1 = enable, 0 = disable.
|
|
*
|
|
* @return array Result with success and message keys.
|
|
*/
|
|
public function togglePlugin(int $extensionId, int $enabled): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
// Verify the extension exists and is a MokoSuiteClient plugin
|
|
$query = $db->getQuery(true)
|
|
->select([$db->quoteName('element'), $db->quoteName('protected')])
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('extension_id') . ' = ' . $extensionId)
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'));
|
|
$db->setQuery($query);
|
|
$ext = $db->loadObject();
|
|
|
|
if (!$ext)
|
|
{
|
|
return ['success' => false, 'message' => 'Extension not found.'];
|
|
}
|
|
|
|
// Don't allow disabling protected/core plugins
|
|
if (!$enabled && ((int) $ext->protected || $ext->element === 'mokosuiteclient'))
|
|
{
|
|
return ['success' => false, 'message' => 'This plugin is protected and cannot be disabled.'];
|
|
}
|
|
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('enabled') . ' = ' . ($enabled ? 1 : 0))
|
|
->where($db->quoteName('extension_id') . ' = ' . $extensionId);
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => $ext->element . ($enabled ? ' enabled.' : ' disabled.'),
|
|
'enabled' => $enabled,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Clear all Joomla caches.
|
|
*
|
|
* @return array Result with success and message keys.
|
|
*/
|
|
public function clearCache(): array
|
|
{
|
|
try
|
|
{
|
|
// Use Joomla's native cache API — same as com_cache
|
|
$cache = Factory::getContainer()->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class);
|
|
$cache->createCacheController('', ['defaultgroup' => ''])->cache->clean('');
|
|
|
|
// Also clean admin cache
|
|
$conf = Factory::getApplication()->get('cache_handler', 'file');
|
|
$options = [
|
|
'defaultgroup' => '',
|
|
'cachebase' => JPATH_ADMINISTRATOR . '/cache',
|
|
'storage' => $conf,
|
|
];
|
|
$cache->createCacheController('', $options)->cache->clean('');
|
|
|
|
// Clear opcache if available
|
|
if (\function_exists('opcache_reset'))
|
|
{
|
|
\opcache_reset();
|
|
}
|
|
|
|
return ['success' => true, 'message' => 'All cache cleared successfully.'];
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return ['success' => false, 'message' => 'Cache clear failed: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the Joomla tmp directory.
|
|
*
|
|
* Removes all files and subdirectories from the configured tmp_path,
|
|
* preserving the directory itself and any .htaccess / web.config files.
|
|
*
|
|
* @return array Result with success and message keys.
|
|
*/
|
|
public function clearTemp(): array
|
|
{
|
|
try
|
|
{
|
|
$tmpPath = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp');
|
|
|
|
if (!is_dir($tmpPath))
|
|
{
|
|
return ['success' => false, 'message' => 'Temp directory does not exist: ' . $tmpPath];
|
|
}
|
|
|
|
$count = 0;
|
|
$protected = ['.htaccess', 'web.config', 'index.html', '.gitkeep'];
|
|
|
|
$items = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($tmpPath, \RecursiveDirectoryIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::CHILD_FIRST
|
|
);
|
|
|
|
foreach ($items as $item)
|
|
{
|
|
$basename = $item->getBasename();
|
|
|
|
// Skip protected files in the root tmp directory
|
|
if ($item->getPath() === $tmpPath && \in_array($basename, $protected, true))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ($item->isDir())
|
|
{
|
|
@rmdir($item->getPathname());
|
|
}
|
|
else
|
|
{
|
|
@unlink($item->getPathname());
|
|
$count++;
|
|
}
|
|
}
|
|
|
|
return ['success' => true, 'message' => sprintf('Temp directory cleaned (%d files removed).', $count)];
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return ['success' => false, 'message' => 'Temp clear failed: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Auto-generate dashboard metadata for plugins not in the static map.
|
|
*/
|
|
private function guessPluginMeta(object $row): array
|
|
{
|
|
$meta = [
|
|
'icon' => 'icon-puzzle-piece',
|
|
'category' => 'tools',
|
|
'label' => $row->name,
|
|
'description' => '',
|
|
'protected' => false,
|
|
];
|
|
|
|
if ($row->folder === 'webservices')
|
|
{
|
|
$meta['icon'] = 'icon-plug';
|
|
$meta['category'] = 'api';
|
|
$meta['label'] = 'Web Services — ' . ucfirst($row->element);
|
|
}
|
|
elseif ($row->folder === 'task')
|
|
{
|
|
$meta['icon'] = 'icon-clock';
|
|
$meta['category'] = 'content';
|
|
|
|
if (str_contains($row->element, 'sync'))
|
|
{
|
|
$meta['label'] = 'Content Sync Task';
|
|
$meta['description'] = 'Scheduled content synchronisation to remote MokoSuiteClient sites.';
|
|
}
|
|
elseif (str_contains($row->element, 'demo'))
|
|
{
|
|
$meta['label'] = 'Demo Reset Task';
|
|
$meta['description'] = 'Scheduled demo site reset with content snapshots.';
|
|
}
|
|
}
|
|
|
|
return $meta;
|
|
}
|
|
|
|
/**
|
|
* Get recent admin login attempts from action logs.
|
|
*/
|
|
public function getRecentLogins(int $limit = 10): array
|
|
{
|
|
try
|
|
{
|
|
$db = $this->getDatabase();
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
$db->quoteName('a.message'),
|
|
$db->quoteName('a.log_date'),
|
|
$db->quoteName('a.ip_address'),
|
|
$db->quoteName('u.username'),
|
|
])
|
|
->from($db->quoteName('#__action_logs', 'a'))
|
|
->leftJoin($db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id'))
|
|
->where($db->quoteName('a.message_language_key') . ' LIKE ' . $db->quote('%LOGIN%'))
|
|
->order($db->quoteName('a.log_date') . ' DESC')
|
|
->setLimit($limit);
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadObjectList() ?: [];
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get pending extension updates.
|
|
*/
|
|
public function getPendingUpdates(): array
|
|
{
|
|
try
|
|
{
|
|
$db = $this->getDatabase();
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
$db->quoteName('u.name'),
|
|
$db->quoteName('u.version'),
|
|
$db->quoteName('u.type'),
|
|
$db->quoteName('e.manifest_cache'),
|
|
])
|
|
->from($db->quoteName('#__updates', 'u'))
|
|
->leftJoin($db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id'))
|
|
->where($db->quoteName('u.extension_id') . ' != 0')
|
|
->order($db->quoteName('u.name') . ' ASC');
|
|
$db->setQuery($query);
|
|
$rows = $db->loadObjectList() ?: [];
|
|
|
|
foreach ($rows as $row)
|
|
{
|
|
$mc = json_decode($row->manifest_cache ?? '{}');
|
|
$row->current_version = $mc->version ?? '';
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get checked-out items count and details.
|
|
*/
|
|
public function getCheckedOutItems(): array
|
|
{
|
|
try
|
|
{
|
|
$db = $this->getDatabase();
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
$db->quoteName('c.title'),
|
|
$db->quoteName('c.checked_out'),
|
|
$db->quoteName('c.checked_out_time'),
|
|
$db->quoteName('u.username'),
|
|
])
|
|
->from($db->quoteName('#__content', 'c'))
|
|
->leftJoin($db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('c.checked_out'))
|
|
->where($db->quoteName('c.checked_out') . ' IS NOT NULL')
|
|
->where($db->quoteName('c.checked_out') . ' != 0')
|
|
->order($db->quoteName('c.checked_out_time') . ' DESC')
|
|
->setLimit(10);
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadObjectList() ?: [];
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get recent WAF blocks from the log table.
|
|
*/
|
|
public function getRecentWafBlocks(int $limit = 10): array
|
|
{
|
|
try
|
|
{
|
|
$db = $this->getDatabase();
|
|
$query = $db->getQuery(true)
|
|
->select('*')
|
|
->from($db->quoteName('#__mokosuiteclient_waf_log'))
|
|
->order($db->quoteName('created') . ' DESC')
|
|
->setLimit($limit);
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadObjectList() ?: [];
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* WAF blocks per day for the last 14 days.
|
|
*/
|
|
public function getWafBlocksByDay(int $days = 14): array
|
|
{
|
|
try
|
|
{
|
|
$db = $this->getDatabase();
|
|
$db->setQuery(
|
|
"SELECT DATE(" . $db->quoteName('created') . ") AS day, COUNT(*) AS total"
|
|
. " FROM " . $db->quoteName('#__mokosuiteclient_waf_log')
|
|
. " WHERE " . $db->quoteName('created') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)"
|
|
. " GROUP BY day ORDER BY day"
|
|
);
|
|
$rows = $db->loadObjectList() ?: [];
|
|
|
|
// Fill in missing days with zero
|
|
$result = [];
|
|
$date = new \DateTime("-{$days} days");
|
|
$now = new \DateTime('now');
|
|
$map = [];
|
|
foreach ($rows as $r)
|
|
{
|
|
$map[$r->day] = (int) $r->total;
|
|
}
|
|
while ($date <= $now)
|
|
{
|
|
$key = $date->format('Y-m-d');
|
|
$result[] = (object) ['day' => $date->format('M d'), 'total' => $map[$key] ?? 0];
|
|
$date->modify('+1 day');
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Admin logins per day for the last 14 days.
|
|
*/
|
|
public function getLoginsByDay(int $days = 14): array
|
|
{
|
|
try
|
|
{
|
|
$db = $this->getDatabase();
|
|
$db->setQuery(
|
|
"SELECT DATE(" . $db->quoteName('log_date') . ") AS day, COUNT(*) AS total"
|
|
. " FROM " . $db->quoteName('#__action_logs')
|
|
. " WHERE " . $db->quoteName('message_language_key') . " = 'PLG_ACTIONLOG_JOOMLA_USER_LOGGED_IN'"
|
|
. " AND " . $db->quoteName('log_date') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)"
|
|
. " GROUP BY day ORDER BY day"
|
|
);
|
|
$rows = $db->loadObjectList() ?: [];
|
|
|
|
$result = [];
|
|
$date = new \DateTime("-{$days} days");
|
|
$now = new \DateTime('now');
|
|
$map = [];
|
|
foreach ($rows as $r)
|
|
{
|
|
$map[$r->day] = (int) $r->total;
|
|
}
|
|
while ($date <= $now)
|
|
{
|
|
$key = $date->format('Y-m-d');
|
|
$result[] = (object) ['day' => $date->format('M d'), 'total' => $map[$key] ?? 0];
|
|
$date->modify('+1 day');
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
}
|