6c668dae20
- togglePublished: return JSON on CSRF failure instead of die() - Conditions view: fold group/rule counts into main query as subselects instead of N+1 per-item queries - All 5 toggle-published templates: add .catch() for AJAX error feedback Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
909 lines
27 KiB
PHP
909 lines
27 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\Controller;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\MVC\Controller\BaseController;
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\CMS\Session\Session;
|
|
|
|
class DisplayController extends BaseController
|
|
{
|
|
protected $default_view = 'dashboard';
|
|
|
|
/**
|
|
* ACL map: view name => required permission.
|
|
*/
|
|
private const VIEW_ACL = [
|
|
'dashboard' => 'mokosuiteclient.dashboard',
|
|
'extensions' => 'mokosuiteclient.extensions',
|
|
'htaccess' => 'mokosuiteclient.htaccess',
|
|
'privacy' => 'core.admin',
|
|
'waflog' => 'mokosuiteclient.security.waflog',
|
|
'automation' => 'core.admin',
|
|
'database' => 'core.admin',
|
|
'cleanup' => 'mokosuiteclient.cache',
|
|
'snippets' => 'mokosuiteclient.snippets.manage',
|
|
'templates' => 'mokosuiteclient.templates.manage',
|
|
'replacements' => 'mokosuiteclient.replacements.manage',
|
|
'conditions' => 'mokosuiteclient.conditions.manage',
|
|
'modules' => 'core.admin',
|
|
];
|
|
|
|
public function display($cachable = false, $urlparams = [])
|
|
{
|
|
$view = $this->input->get('view', $this->default_view);
|
|
$acl = self::VIEW_ACL[$view] ?? 'core.manage';
|
|
|
|
if (!$this->checkAcl($acl))
|
|
{
|
|
Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
|
Factory::getApplication()->redirect(Route::_('index.php', false));
|
|
|
|
return;
|
|
}
|
|
|
|
return parent::display($cachable, $urlparams);
|
|
}
|
|
|
|
// ==================================================================
|
|
// Plugin toggle
|
|
// ==================================================================
|
|
|
|
public function togglePlugin()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuiteclient.plugins.toggle'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$app = Factory::getApplication();
|
|
$model = $this->getModel('Dashboard');
|
|
|
|
$result = $model->togglePlugin(
|
|
$app->getInput()->getInt('extension_id', 0),
|
|
$app->getInput()->getInt('enabled', 0)
|
|
);
|
|
|
|
$this->jsonResponse($result);
|
|
}
|
|
|
|
// ==================================================================
|
|
// Heartbeat
|
|
// ==================================================================
|
|
|
|
public function sendHeartbeat()
|
|
{
|
|
if (!Session::checkToken())
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Session expired — please reload the page.']);
|
|
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
$corePlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuiteclient');
|
|
|
|
if (!$corePlugin)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Core plugin not enabled.']);
|
|
|
|
return;
|
|
}
|
|
|
|
$params = new \Joomla\Registry\Registry($corePlugin->params);
|
|
$baseUrl = rtrim($params->get('monitor_base_url', ''), '/');
|
|
|
|
// Fall back to manifest XML default
|
|
if (empty($baseUrl))
|
|
{
|
|
$manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient/mokosuiteclient.xml';
|
|
|
|
if (is_file($manifestFile))
|
|
{
|
|
$xml = simplexml_load_file($manifestFile);
|
|
|
|
if ($xml)
|
|
{
|
|
foreach ($xml->xpath('//field[@name="monitor_base_url"]') as $field)
|
|
{
|
|
$baseUrl = rtrim((string) $field['default'], '/');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($baseUrl))
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'MokoSuiteClientHQ URL not configured.']);
|
|
|
|
return;
|
|
}
|
|
|
|
$healthToken = $params->get('health_api_token', '');
|
|
|
|
if (empty($healthToken))
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Health token not configured.']);
|
|
|
|
return;
|
|
}
|
|
|
|
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
|
|
$domain = parse_url($siteUrl, PHP_URL_HOST) ?: '';
|
|
$timestamp = time();
|
|
|
|
// Discover all MokoSuite ecosystem packages for HQ
|
|
$mokoPackages = [];
|
|
try {
|
|
$pkgDb = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
|
$pkgQuery = $pkgDb->getQuery(true)
|
|
->select([$pkgDb->quoteName('element'), $pkgDb->quoteName('manifest_cache')])
|
|
->from($pkgDb->quoteName('#__extensions'))
|
|
->where('(' . $pkgDb->quoteName('element') . ' LIKE ' . $pkgDb->quote('pkg_mokosuite%')
|
|
. ' OR ' . $pkgDb->quoteName('element') . ' LIKE ' . $pkgDb->quote('pkg_mokojoom%') . ')');
|
|
$pkgDb->setQuery($pkgQuery);
|
|
foreach ($pkgDb->loadObjectList() ?: [] as $pkg) {
|
|
$m = json_decode($pkg->manifest_cache ?? '{}');
|
|
$mokoPackages[$pkg->element] = $m->version ?? '';
|
|
}
|
|
} catch (\Throwable $e) {}
|
|
|
|
$payload = json_encode([
|
|
'token' => $healthToken,
|
|
'domain' => $domain,
|
|
'site_name' => Factory::getConfig()->get('sitename', 'Joomla'),
|
|
'site_url' => $siteUrl,
|
|
'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(),
|
|
'php_version' => PHP_VERSION,
|
|
'timestamp' => $timestamp,
|
|
'moko_packages' => $mokoPackages,
|
|
], JSON_UNESCAPED_SLASHES);
|
|
|
|
// RSA sign the request
|
|
$headers = ['Content-Type: application/json'];
|
|
$signingKeyB64 = $params->get('monitor_signing_key', '');
|
|
|
|
// Fall back to manifest XML default
|
|
if (empty($signingKeyB64))
|
|
{
|
|
$manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient/mokosuiteclient.xml';
|
|
|
|
if (is_file($manifestFile))
|
|
{
|
|
$xml = simplexml_load_file($manifestFile);
|
|
|
|
if ($xml)
|
|
{
|
|
foreach ($xml->xpath('//field[@name="monitor_signing_key"]') as $field)
|
|
{
|
|
$signingKeyB64 = (string) $field['default'];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($signingKeyB64))
|
|
{
|
|
$privateKeyPem = base64_decode($signingKeyB64);
|
|
$privateKey = openssl_pkey_get_private($privateKeyPem);
|
|
|
|
if ($privateKey !== false)
|
|
{
|
|
$message = $domain . '|' . $timestamp . '|' . $healthToken;
|
|
$signature = '';
|
|
|
|
if (openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256))
|
|
{
|
|
$headers[] = 'X-MokoSuite-Signature: ' . base64_encode($signature);
|
|
$headers[] = 'X-MokoSuite-Timestamp: ' . $timestamp;
|
|
}
|
|
}
|
|
}
|
|
|
|
$endpoint = $baseUrl . '/api/index.php/v1/mokosuitehq/heartbeat';
|
|
|
|
$ch = curl_init($endpoint);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 15,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($error)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Connection failed: ' . $error]);
|
|
}
|
|
elseif ($code >= 200 && $code < 300)
|
|
{
|
|
$body = json_decode($response, true);
|
|
$this->jsonResponse(['success' => true, 'message' => 'Heartbeat sent: ' . ($body['status'] ?? 'ok')]);
|
|
}
|
|
else
|
|
{
|
|
$body = json_decode($response, true);
|
|
$this->jsonResponse(['success' => false, 'message' => 'HTTP ' . $code . ': ' . ($body['error'] ?? $body['message'] ?? 'Unknown')]);
|
|
}
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
// Cache
|
|
// ==================================================================
|
|
|
|
public function clearCache()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuiteclient.cache'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($this->getModel('Dashboard')->clearCache());
|
|
}
|
|
|
|
public function clearTemp()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuiteclient.cache'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($this->getModel('Dashboard')->clearTemp());
|
|
}
|
|
|
|
// ==================================================================
|
|
// Extensions
|
|
// ==================================================================
|
|
|
|
public function installExtension()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuiteclient.extensions'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$downloadUrl = Factory::getApplication()->getInput()->getString('download_url', '');
|
|
|
|
if (empty($downloadUrl))
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Missing download URL.']);
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($this->getModel('Extensions')->installFromUrl($downloadUrl));
|
|
}
|
|
|
|
// ==================================================================
|
|
// .htaccess
|
|
// ==================================================================
|
|
|
|
public function saveHtaccess()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuiteclient.htaccess'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$app = Factory::getApplication();
|
|
$input = $app->getInput();
|
|
$model = $this->getModel('Htaccess');
|
|
|
|
$options = [];
|
|
|
|
foreach ($input->getArray() as $key => $value)
|
|
{
|
|
if (str_starts_with($key, 'opt_'))
|
|
{
|
|
$options[substr($key, 4)] = $value;
|
|
}
|
|
}
|
|
|
|
if (!empty($options))
|
|
{
|
|
$model->saveOptions($options);
|
|
}
|
|
|
|
$this->jsonResponse($model->saveHtaccess($input->getRaw('content', '')));
|
|
}
|
|
|
|
public function generateHtaccess()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuiteclient.htaccess'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$model = $this->getModel('Htaccess');
|
|
$options = Factory::getApplication()->getInput()->getArray();
|
|
|
|
$model->saveOptions($options);
|
|
|
|
$app = Factory::getApplication();
|
|
$app->setHeader('Content-Type', 'application/json');
|
|
echo json_encode([
|
|
'htaccess' => $model->generateHtaccess($options),
|
|
'nginx' => $model->generateNginx($options),
|
|
]);
|
|
$app->close();
|
|
}
|
|
|
|
// ==================================================================
|
|
// Regular Labs Import
|
|
// ==================================================================
|
|
|
|
public function importRegularLabs()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
|
$prefix = $db->getPrefix();
|
|
$tables = $db->getTableList();
|
|
$results = [];
|
|
|
|
// ── Conditions (4 tables) ──────────────────────────────
|
|
if (in_array($prefix . 'conditions', $tables)
|
|
&& in_array($prefix . 'mokosuiteclient_conditions', $tables))
|
|
{
|
|
// Check if already imported
|
|
$existing = (int) $db->setQuery("SELECT COUNT(*) FROM " . $db->quoteName('#__mokosuiteclient_conditions'))->loadResult();
|
|
|
|
if ($existing === 0)
|
|
{
|
|
// conditions
|
|
$db->setQuery("INSERT INTO " . $db->quoteName('#__mokosuiteclient_conditions')
|
|
. " (id, alias, name, description, category, color, match_all, published, hash, checked_out, checked_out_time)"
|
|
. " SELECT id, alias, name, description, category, color, match_all, published, hash, checked_out, checked_out_time"
|
|
. " FROM " . $db->quoteName('#__conditions'))->execute();
|
|
$c1 = $db->getAffectedRows();
|
|
|
|
// conditions_groups
|
|
if (in_array($prefix . 'conditions_groups', $tables))
|
|
{
|
|
$db->setQuery("INSERT INTO " . $db->quoteName('#__mokosuiteclient_conditions_groups')
|
|
. " (id, condition_id, match_all, ordering)"
|
|
. " SELECT id, condition_id, match_all, ordering"
|
|
. " FROM " . $db->quoteName('#__conditions_groups'))->execute();
|
|
}
|
|
|
|
// conditions_rules
|
|
if (in_array($prefix . 'conditions_rules', $tables))
|
|
{
|
|
$db->setQuery("INSERT INTO " . $db->quoteName('#__mokosuiteclient_conditions_rules')
|
|
. " (id, group_id, type, exclude, params, ordering)"
|
|
. " SELECT id, group_id, type, exclude, params, ordering"
|
|
. " FROM " . $db->quoteName('#__conditions_rules'))->execute();
|
|
}
|
|
|
|
// conditions_map
|
|
if (in_array($prefix . 'conditions_map', $tables))
|
|
{
|
|
$db->setQuery("INSERT INTO " . $db->quoteName('#__mokosuiteclient_conditions_map')
|
|
. " (condition_id, extension, item_id)"
|
|
. " SELECT condition_id, extension, item_id"
|
|
. " FROM " . $db->quoteName('#__conditions_map'))->execute();
|
|
}
|
|
|
|
$results['conditions'] = $c1 . ' condition sets imported';
|
|
}
|
|
else
|
|
{
|
|
$results['conditions'] = 'skipped (already has data)';
|
|
}
|
|
}
|
|
|
|
// ── Snippets ──────────────────────────────────────────
|
|
if (in_array($prefix . 'snippets', $tables)
|
|
&& in_array($prefix . 'mokosuiteclient_snippets', $tables))
|
|
{
|
|
$existing = (int) $db->setQuery("SELECT COUNT(*) FROM " . $db->quoteName('#__mokosuiteclient_snippets'))->loadResult();
|
|
|
|
if ($existing === 0)
|
|
{
|
|
$db->setQuery("INSERT INTO " . $db->quoteName('#__mokosuiteclient_snippets')
|
|
. " (id, alias, name, description, category, color, content, params, published, ordering, checked_out, checked_out_time)"
|
|
. " SELECT id, alias, name, description, category, color, content, params, published, ordering, checked_out, checked_out_time"
|
|
. " FROM " . $db->quoteName('#__snippets'))->execute();
|
|
$results['snippets'] = $db->getAffectedRows() . ' snippets imported';
|
|
}
|
|
else
|
|
{
|
|
$results['snippets'] = 'skipped (already has data)';
|
|
}
|
|
}
|
|
|
|
// ── ReReplacer ────────────────────────────────────────
|
|
if (in_array($prefix . 'rereplacer', $tables)
|
|
&& in_array($prefix . 'mokosuiteclient_replacements', $tables))
|
|
{
|
|
$existing = (int) $db->setQuery("SELECT COUNT(*) FROM " . $db->quoteName('#__mokosuiteclient_replacements'))->loadResult();
|
|
|
|
if ($existing === 0)
|
|
{
|
|
// RL uses 'replace' column, we use 'replace_value'; RL 'area' is text (JSON), we use varchar
|
|
$db->setQuery("INSERT INTO " . $db->quoteName('#__mokosuiteclient_replacements')
|
|
. " (id, name, search, replace_value, area, published, description, ordering, checked_out, checked_out_time)"
|
|
. " SELECT id, name, search, `replace`, 'both', published, description, ordering, checked_out, checked_out_time"
|
|
. " FROM " . $db->quoteName('#__rereplacer'))->execute();
|
|
$results['replacements'] = $db->getAffectedRows() . ' replacement rules imported';
|
|
}
|
|
else
|
|
{
|
|
$results['replacements'] = 'skipped (already has data)';
|
|
}
|
|
}
|
|
|
|
// ── Content Templater ─────────────────────────────────
|
|
if (in_array($prefix . 'contenttemplater', $tables)
|
|
&& in_array($prefix . 'mokosuiteclient_content_templates', $tables))
|
|
{
|
|
$existing = (int) $db->setQuery("SELECT COUNT(*) FROM " . $db->quoteName('#__mokosuiteclient_content_templates'))->loadResult();
|
|
|
|
if ($existing === 0)
|
|
{
|
|
$db->setQuery("INSERT INTO " . $db->quoteName('#__mokosuiteclient_content_templates')
|
|
. " (id, name, description, category, color, template_data, published, ordering, checked_out, checked_out_time)"
|
|
. " SELECT id, name, description, category, color, content, published, ordering, checked_out, checked_out_time"
|
|
. " FROM " . $db->quoteName('#__contenttemplater'))->execute();
|
|
$results['templates'] = $db->getAffectedRows() . ' content templates imported';
|
|
}
|
|
else
|
|
{
|
|
$results['templates'] = 'skipped (already has data)';
|
|
}
|
|
}
|
|
|
|
if (empty($results))
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'No Regular Labs data found to import.']);
|
|
}
|
|
else
|
|
{
|
|
$summary = implode('; ', array_map(fn($k, $v) => ucfirst($k) . ': ' . $v, array_keys($results), $results));
|
|
$this->jsonResponse(['success' => true, 'message' => 'Import complete. ' . $summary]);
|
|
}
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Import error: ' . $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
// ==================================================================
|
|
// Support PIN
|
|
// ==================================================================
|
|
|
|
public function requestPin()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuiteclient.dashboard'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
|
$result = \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::requestNew($db);
|
|
|
|
$this->jsonResponse($result);
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
// ==================================================================
|
|
// Maintenance (#127, #128)
|
|
// ==================================================================
|
|
|
|
public function optimizeDb()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
|
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
|
|
$this->jsonResponse($model->optimizeTables());
|
|
}
|
|
|
|
public function repairDb()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
|
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
|
|
$this->jsonResponse($model->repairTables());
|
|
}
|
|
|
|
public function purgeSessions()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
|
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
|
|
$this->jsonResponse($model->purgeSessions());
|
|
}
|
|
|
|
public function cleanDirectory()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuiteclient.cache')) { $this->jsonForbidden(); return; }
|
|
$dirKey = Factory::getApplication()->getInput()->getString('dir_key', '');
|
|
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
|
|
$this->jsonResponse($model->cleanDirectory($dirKey));
|
|
}
|
|
|
|
// ==================================================================
|
|
// Settings Import/Export (#132)
|
|
// ==================================================================
|
|
|
|
public function exportSettings()
|
|
{
|
|
Session::checkToken('get') or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$db = Factory::getDbo();
|
|
$settings = [];
|
|
|
|
// Export all MokoSuiteClient plugin params
|
|
$plugins = ['mokosuiteclient', 'mokosuiteclient_firewall', 'mokosuiteclient_tenant', 'mokosuiteclient_devtools', 'mokosuiteclient_offline'];
|
|
|
|
foreach ($plugins as $element)
|
|
{
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select($db->quoteName('params'))
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote($element))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
|
);
|
|
$settings['plugins'][$element] = json_decode($db->loadResult() ?? '{}', true);
|
|
}
|
|
|
|
// Export component params
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select($db->quoteName('params'))
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuiteclient'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
|
);
|
|
$settings['component'] = json_decode($db->loadResult() ?? '{}', true);
|
|
$settings['exported'] = gmdate('Y-m-d\TH:i:s\Z');
|
|
$settings['site'] = Factory::getConfig()->get('sitename', '');
|
|
|
|
$this->jsonResponse(['success' => true, 'settings' => $settings]);
|
|
}
|
|
|
|
public function importSettings()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$json = Factory::getApplication()->getInput()->getRaw('settings_json', '');
|
|
$data = json_decode($json, true);
|
|
|
|
if (empty($data) || empty($data['plugins']))
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Invalid settings JSON.']);
|
|
return;
|
|
}
|
|
|
|
$db = Factory::getDbo();
|
|
$count = 0;
|
|
|
|
foreach ($data['plugins'] ?? [] as $element => $params)
|
|
{
|
|
if (!is_array($params))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote($element))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
|
)->execute();
|
|
$count++;
|
|
}
|
|
|
|
if (!empty($data['component']) && is_array($data['component']))
|
|
{
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($data['component'])))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuiteclient'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
|
)->execute();
|
|
$count++;
|
|
}
|
|
|
|
$this->jsonResponse(['success' => true, 'message' => "Imported settings for {$count} extensions."]);
|
|
}
|
|
|
|
// ==================================================================
|
|
// WAF Log
|
|
// ==================================================================
|
|
|
|
public function purgeWafLog()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuiteclient.security.waflog'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$days = Factory::getApplication()->getInput()->getInt('days', 30);
|
|
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\WaflogModel();
|
|
|
|
$this->jsonResponse($model->purgeLogs($days));
|
|
}
|
|
|
|
public function banIpFromLog()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuiteclient.security.waflog'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$ip = Factory::getApplication()->getInput()->getString('ip', '');
|
|
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\WaflogModel();
|
|
|
|
$this->jsonResponse($model->banIp($ip));
|
|
}
|
|
|
|
// ==================================================================
|
|
// Privacy Guard
|
|
// ==================================================================
|
|
|
|
public function processDataRequest()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$input = Factory::getApplication()->getInput();
|
|
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\PrivacyModel();
|
|
$action = $input->getString('action', 'deny');
|
|
|
|
if ($action === 'create')
|
|
{
|
|
$result = $model->createRequest(
|
|
$input->getInt('user_id', 0),
|
|
$input->getString('type', 'export')
|
|
);
|
|
$this->jsonResponse($result);
|
|
return;
|
|
}
|
|
|
|
if ($action === 'approve' && !$input->getInt('request_id', 0) && $input->getInt('user_id', 0))
|
|
{
|
|
// Auto-process: create then immediately approve
|
|
$result = $model->createRequest(
|
|
$input->getInt('user_id', 0),
|
|
$input->getString('type', 'export')
|
|
);
|
|
|
|
if ($result['success'] && !empty($result['id']))
|
|
{
|
|
$result = $model->processRequest((int) $result['id'], 'approve');
|
|
}
|
|
|
|
$this->jsonResponse($result);
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($model->processRequest(
|
|
$input->getInt('request_id', 0),
|
|
$action
|
|
));
|
|
}
|
|
|
|
public function exportUserData()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\PrivacyModel();
|
|
|
|
$this->jsonResponse($model->exportUserData(
|
|
Factory::getApplication()->getInput()->getInt('user_id', 0)
|
|
));
|
|
}
|
|
|
|
// ==================================================================
|
|
// Importers
|
|
// ==================================================================
|
|
|
|
public function importAdminTools()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($this->getModel('Import')->importAdminTools());
|
|
}
|
|
|
|
// ==================================================================
|
|
// Toggle Published
|
|
// ==================================================================
|
|
|
|
public function togglePublished()
|
|
{
|
|
if (!Session::checkToken())
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => Text::_('JINVALID_TOKEN')]);
|
|
return;
|
|
}
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$app = Factory::getApplication();
|
|
$table = $app->getInput()->getString('table', '');
|
|
$id = $app->getInput()->getInt('id', 0);
|
|
|
|
$allowed = ['mokosuiteclient_conditions', 'mokosuiteclient_snippets',
|
|
'mokosuiteclient_replacements', 'mokosuiteclient_content_templates', 'modules'];
|
|
|
|
if (!in_array($table, $allowed, true) || $id <= 0)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Invalid table or ID.']);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
|
$dbTable = '#__' . $table;
|
|
$current = (int) $db->setQuery(
|
|
$db->getQuery(true)
|
|
->select($db->quoteName('published'))
|
|
->from($db->quoteName($dbTable))
|
|
->where($db->quoteName('id') . ' = ' . $id)
|
|
)->loadResult();
|
|
|
|
$newState = $current ? 0 : 1;
|
|
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->update($db->quoteName($dbTable))
|
|
->set($db->quoteName('published') . ' = ' . $newState)
|
|
->where($db->quoteName('id') . ' = ' . $id)
|
|
)->execute();
|
|
|
|
$this->jsonResponse(['success' => true, 'published' => $newState]);
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
// ==================================================================
|
|
// Helpers
|
|
// ==================================================================
|
|
|
|
/**
|
|
* Check a MokoSuiteClient ACL permission for the current user.
|
|
*/
|
|
private function checkAcl(string $action): bool
|
|
{
|
|
$user = Factory::getApplication()->getIdentity();
|
|
|
|
// Super admins always pass
|
|
if ($user->authorise('core.admin', 'com_mokosuiteclient'))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return $user->authorise($action, 'com_mokosuiteclient');
|
|
}
|
|
|
|
/**
|
|
* Send a JSON response and close.
|
|
*/
|
|
private function jsonResponse(array $data): void
|
|
{
|
|
$app = Factory::getApplication();
|
|
$app->setHeader('Content-Type', 'application/json');
|
|
echo json_encode($data);
|
|
$app->close();
|
|
}
|
|
|
|
/**
|
|
* Send a 403 JSON response and close.
|
|
*/
|
|
private function jsonForbidden(): void
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]);
|
|
return;
|
|
}
|
|
|
|
}
|