diff --git a/source/packages/plg_system_mokosuiteclient_devtools/language/en-GB/plg_system_mokosuiteclient_devtools.ini b/source/packages/plg_system_mokosuiteclient_devtools/language/en-GB/plg_system_mokosuiteclient_devtools.ini
index 44d7cca4..b9680d26 100644
--- a/source/packages/plg_system_mokosuiteclient_devtools/language/en-GB/plg_system_mokosuiteclient_devtools.ini
+++ b/source/packages/plg_system_mokosuiteclient_devtools/language/en-GB/plg_system_mokosuiteclient_devtools.ini
@@ -15,6 +15,8 @@ PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DELETE_VERSIONS_LABEL="Delete All Versions"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DELETE_VERSIONS_DESC="One-shot: delete all content version history on save. Automatically turns off after execution."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_DLKEYS_LABEL="Reset Download Keys"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_DLKEYS_DESC="One-shot: clear all download keys (dlid) from update sites on save. Automatically turns off after execution."
+PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_LABEL="Reset Tour Prompts"
+PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_DESC="One-shot: reset all guided tour completion flags on save. Allows tours to re-trigger for all users. Automatically turns off after execution."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_ALIASES="Mirror Domains & Staging Environments"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_ALIASES_DESC="Configure domain aliases that share this site's hosting folder. Each mirror can independently bypass offline mode and control search engine indexing."
diff --git a/source/packages/plg_system_mokosuiteclient_devtools/mokosuiteclient_devtools.xml b/source/packages/plg_system_mokosuiteclient_devtools/mokosuiteclient_devtools.xml
index 6845c554..b879f72c 100644
--- a/source/packages/plg_system_mokosuiteclient_devtools/mokosuiteclient_devtools.xml
+++ b/source/packages/plg_system_mokosuiteclient_devtools/mokosuiteclient_devtools.xml
@@ -61,6 +61,14 @@
JYES
JNO
+
+
+ JYES
+ JNO
+
set('reset_download_keys', 0);
}
+ // Reset tour prompts on save if toggled on
+ if ($params->get('reset_tour_prompts', 0))
+ {
+ $this->resetTourPrompts();
+ $params->set('reset_tour_prompts', 0);
+ }
+
// Reset the one-shot toggles
if ($table->params !== $params->toString())
{
@@ -160,6 +167,21 @@ class DevTools extends CMSPlugin implements SubscriberInterface
return $count;
}
+ private function resetTourPrompts(): int
+ {
+ $db = Factory::getDbo();
+ $db->setQuery(
+ $db->getQuery(true)
+ ->delete($db->quoteName('#__user_profiles'))
+ ->where($db->quoteName('profile_key') . ' LIKE ' . $db->quote('guidedtours.tour%'))
+ )->execute();
+
+ $count = $db->getAffectedRows();
+ $this->getApplication()->enqueueMessage(\sprintf('Reset %d guided tour completion flags.', $count), 'message');
+
+ return $count;
+ }
+
private function resetDownloadKeys(): int
{
$db = Factory::getDbo();
diff --git a/source/script.php b/source/script.php
index f604699c..a332c0b3 100644
--- a/source/script.php
+++ b/source/script.php
@@ -108,6 +108,9 @@ class Pkg_MokosuiteclientInstallerScript
// Set up MokoSuiteClient guided tours and unpublish Joomla defaults
$this->setupGuidedTours();
+ // Register MokoSuiteClient guided tour content (tours + steps)
+ $this->registerGuidedTours();
+
// Clean up orphaned empty-element rows and stale files from old DEFAULT '' bug
$this->cleanupEmptyElements();
@@ -1486,97 +1489,217 @@ class Pkg_MokosuiteclientInstallerScript
);
$db->execute();
- // Define MokoSuiteClient tours
+ // Remove old-format tours (superseded by com_mokosuiteclient.* UIDs)
+ $oldUids = [
+ $db->quote('mokosuiteclient-welcome'),
+ $db->quote('mokosuiteclient-firewall'),
+ $db->quote('mokosuiteclient-extensions'),
+ ];
+
+ // Delete orphaned steps first
+ $subQuery = $db->getQuery(true)
+ ->select($db->quoteName('id'))
+ ->from($db->quoteName('#__guidedtours'))
+ ->where($db->quoteName('uid') . ' IN (' . implode(',', $oldUids) . ')');
+ $db->setQuery($subQuery);
+ $oldTourIds = $db->loadColumn();
+
+ if (!empty($oldTourIds))
+ {
+ $db->setQuery(
+ $db->getQuery(true)
+ ->delete($db->quoteName('#__guidedtour_steps'))
+ ->where($db->quoteName('tour_id') . ' IN (' . implode(',', array_map('intval', $oldTourIds)) . ')')
+ )->execute();
+
+ $db->setQuery(
+ $db->getQuery(true)
+ ->delete($db->quoteName('#__guidedtours'))
+ ->where($db->quoteName('uid') . ' IN (' . implode(',', $oldUids) . ')')
+ )->execute();
+ }
+
+ // Tour registration is now handled by registerGuidedTours()
+ }
+ catch (\Throwable $e)
+ {
+ Log::add('Guided tours setup error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
+ }
+ }
+
+ /**
+ * Register MokoSuiteClient guided tours and their steps.
+ *
+ * Inserts tour definitions into #__guidedtours and step definitions into
+ * #__guidedtour_steps. Skips if the tables do not exist (pre-Joomla 4.3)
+ * or if a tour with the same uid already exists.
+ *
+ * @return void
+ *
+ * @since 02.47.09
+ */
+ private function registerGuidedTours(): void
+ {
+ try
+ {
+ $db = Factory::getDbo();
+
+ // Check if #__guidedtours table exists (Joomla 4.3+)
+ $tables = $db->getTableList();
+ $prefix = $db->getPrefix();
+
+ if (!\in_array($prefix . 'guidedtours', $tables, true))
+ {
+ return;
+ }
+
+ $now = date('Y-m-d H:i:s');
+
+ // Define tours
$tours = [
[
- 'uid' => 'mokosuiteclient-welcome',
- 'title' => 'Welcome to MokoSuiteClient',
- 'desc' => 'Get started with the MokoSuiteClient Admin Tools Suite. This tour shows you the key areas of your admin dashboard.',
- 'url' => 'administrator/index.php?option=com_mokosuiteclient',
- 'steps' => [
- ['title' => 'MokoSuiteClient Dashboard', 'desc' => 'This is your MokoSuiteClient control center. You can see site info, feature plugins, WAF activity, and quick actions all in one place.', 'target' => '#mokosuiteclient-dashboard', 'type' => 0],
- ['title' => 'Site Information', 'desc' => 'The info bar shows your Joomla version, PHP version, database type, and debug/offline status at a glance.', 'target' => '.mokosuiteclient-info-bar', 'type' => 0],
- ['title' => 'Quick Actions', 'desc' => 'Use these buttons to clear cache, check updates, manage extensions, and perform common admin tasks with one click.', 'target' => '#mokosuiteclient-btn-cache', 'type' => 0],
- ['title' => 'Feature Plugins', 'desc' => 'MokoSuiteClient features are split into toggleable plugins. Enable or disable security, tenant restrictions, developer tools, and more from here.', 'target' => '.mokosuiteclient-plugin-grid', 'type' => 0],
- ['title' => 'MokoSuiteClient Menu', 'desc' => 'The MokoSuiteClient sidebar menu gives you quick access to all admin tools — Helpdesk, Extensions, WAF Log, Database Tools, and more.', 'target' => '.mokosuiteclient-admin-menu, [class*="mokosuiteclient"]', 'type' => 0],
+ 'uid' => 'com_mokosuiteclient.welcome',
+ 'title' => 'MokoSuite Welcome',
+ 'description' => 'Get started with MokoSuite — configure your health token, send your first heartbeat, and set up trusted IPs.',
+ 'extensions' => '["com_mokosuiteclient"]',
+ 'url' => 'administrator/index.php?option=com_mokosuiteclient',
+ 'steps' => [
+ [
+ 'title' => 'Welcome to MokoSuite',
+ 'description' => 'This is your MokoSuite control panel. Let\'s walk through the key features.',
+ 'target' => '#mokosuiteclient-dashboard',
+ 'type' => 2,
+ 'position' => 'bottom',
+ ],
+ [
+ 'title' => 'Site Info Bar',
+ 'description' => 'Your site name, version, support PIN, and system info at a glance.',
+ 'target' => '.card.mb-4:first-child',
+ 'type' => 2,
+ 'position' => 'bottom',
+ ],
+ [
+ 'title' => 'Quick Actions',
+ 'description' => 'Clear cache, check updates, manage extensions, and more.',
+ 'target' => '#mokosuiteclient-btn-cache',
+ 'type' => 2,
+ 'position' => 'right',
+ ],
+ [
+ 'title' => 'Plugin Cards',
+ 'description' => 'Enable, disable, and configure MokoSuite plugins from here.',
+ 'target' => '.mokosuiteclient-plugin-card:first-child',
+ 'type' => 2,
+ 'position' => 'top',
+ ],
],
],
[
- 'uid' => 'mokosuiteclient-firewall',
- 'title' => 'MokoSuiteClient Firewall Setup',
- 'desc' => 'Configure the Web Application Firewall to protect your site from common attacks.',
- 'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&filter[search]=mokosuiteclient_firewall',
- 'steps' => [
- ['title' => 'Firewall Plugin', 'desc' => 'The MokoSuiteClient Firewall provides 10 security shields including SQL injection, XSS, and malicious user agent detection.', 'target' => '', 'type' => 0],
- ['title' => 'WAF Shields', 'desc' => 'Enable or disable individual WAF shields. Each shield protects against a specific attack vector. All shields are enabled by default.', 'target' => '', 'type' => 0],
- ['title' => 'Security Headers', 'desc' => 'Configure HTTP security headers like X-Frame-Options, Content-Security-Policy, and HSTS to harden your site against browser-based attacks.', 'target' => '', 'type' => 0],
- ['title' => 'IP Blocklist', 'desc' => 'Block specific IP addresses, CIDR ranges, or wildcard patterns. The auto-ban feature automatically blocks IPs that trigger too many WAF alerts.', 'target' => '', 'type' => 0],
- ],
- ],
- [
- 'uid' => 'mokosuiteclient-extensions',
- 'title' => 'Moko Extensions Manager',
- 'desc' => 'Browse and install Moko Consulting extensions from the built-in catalog.',
- 'url' => 'administrator/index.php?option=com_mokosuiteclient&view=extensions',
- 'steps' => [
- ['title' => 'Extension Catalog', 'desc' => 'Browse all available Moko Consulting extensions. Each card shows the extension name, description, install status, and current version.', 'target' => '', 'type' => 0],
- ['title' => 'Install Extensions', 'desc' => 'Click Install to add an extension from the Moko Consulting repository. Updates are handled through Joomla\'s standard update system.', 'target' => '', 'type' => 0],
+ 'uid' => 'com_mokosuiteclient.firewall',
+ 'title' => 'MokoSuite Firewall Setup',
+ 'description' => 'Configure your Web Application Firewall — trusted IPs, WAF shields, and security headers.',
+ 'extensions' => '["com_mokosuiteclient"]',
+ 'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&extension_id=0',
+ 'steps' => [
+ [
+ 'title' => 'Your Current IP',
+ 'description' => 'This shows your IP address. Copy it to add to the Trusted IPs list.',
+ 'target' => '#mokosuiteclient-current-ip',
+ 'type' => 2,
+ 'position' => 'bottom',
+ ],
+ [
+ 'title' => 'Trusted IPs',
+ 'description' => 'Add IPs that should bypass WAF checks — your office, VPN, etc.',
+ 'target' => '#jform_params_trusted_ips',
+ 'type' => 2,
+ 'position' => 'top',
+ ],
+ [
+ 'title' => 'WAF Shields',
+ 'description' => 'Enable protection against SQL injection, XSS, malicious agents, and more.',
+ 'target' => '#attrib-waf',
+ 'type' => 2,
+ 'position' => 'bottom',
+ ],
],
],
];
foreach ($tours as $tourDef)
{
- // Check if tour already exists
- $db->setQuery(
- $db->getQuery(true)
- ->select('id')
- ->from($db->quoteName('#__guidedtours'))
- ->where($db->quoteName('uid') . ' = ' . $db->quote($tourDef['uid']))
- );
+ // Check if tour already exists by uid
+ $query = $db->getQuery(true)
+ ->select($db->quoteName('id'))
+ ->from($db->quoteName('#__guidedtours'))
+ ->where($db->quoteName('uid') . ' = ' . $db->quote($tourDef['uid']));
+ $db->setQuery($query);
+ $existingId = (int) $db->loadResult();
- if ($db->loadResult())
+ if ($existingId)
{
- continue;
+ // Update existing tour metadata
+ $update = $db->getQuery(true)
+ ->update($db->quoteName('#__guidedtours'))
+ ->set($db->quoteName('title') . ' = ' . $db->quote($tourDef['title']))
+ ->set($db->quoteName('description') . ' = ' . $db->quote($tourDef['description']))
+ ->set($db->quoteName('extensions') . ' = ' . $db->quote($tourDef['extensions']))
+ ->set($db->quoteName('url') . ' = ' . $db->quote($tourDef['url']))
+ ->set($db->quoteName('published') . ' = 1')
+ ->set($db->quoteName('modified') . ' = ' . $db->quote($now))
+ ->where($db->quoteName('id') . ' = ' . $existingId);
+ $db->setQuery($update)->execute();
+
+ // Delete existing steps so they are re-inserted fresh
+ $delete = $db->getQuery(true)
+ ->delete($db->quoteName('#__guidedtour_steps'))
+ ->where($db->quoteName('tour_id') . ' = ' . $existingId);
+ $db->setQuery($delete)->execute();
+
+ $tourId = $existingId;
+ }
+ else
+ {
+ // Insert new tour
+ $tour = (object) [
+ 'title' => $tourDef['title'],
+ 'uid' => $tourDef['uid'],
+ 'description' => $tourDef['description'],
+ 'extensions' => $tourDef['extensions'],
+ 'url' => $tourDef['url'],
+ 'created' => $now,
+ 'created_by' => 0,
+ 'modified' => $now,
+ 'modified_by' => 0,
+ 'published' => 1,
+ 'language' => '*',
+ 'note' => 'MokoSuiteClient',
+ 'access' => 1,
+ 'ordering' => 0,
+ 'autostart' => 0,
+ ];
+
+ $db->insertObject('#__guidedtours', $tour, 'id');
+ $tourId = (int) $tour->id;
}
- $tour = (object) [
- 'title' => $tourDef['title'],
- 'uid' => $tourDef['uid'],
- 'description' => $tourDef['desc'],
- 'extensions' => '',
- 'url' => $tourDef['url'],
- 'created' => date('Y-m-d H:i:s'),
- 'created_by' => 0,
- 'modified' => date('Y-m-d H:i:s'),
- 'modified_by' => 0,
- 'published' => 1,
- 'language' => '*',
- 'note' => 'MokoSuiteClient',
- 'access' => 3,
- 'ordering' => 0,
- 'autostart' => 0,
- ];
-
- $db->insertObject('#__guidedtours', $tour, 'id');
- $tourId = (int) $tour->id;
-
+ // Insert steps
foreach ($tourDef['steps'] as $i => $stepDef)
{
$step = (object) [
'tour_id' => $tourId,
'title' => $stepDef['title'],
- 'description' => $stepDef['desc'],
+ 'description' => $stepDef['description'],
'target' => $stepDef['target'],
'type' => $stepDef['type'],
'interactive_type' => 1,
'url' => '',
- 'position' => 'bottom',
+ 'position' => $stepDef['position'],
'ordering' => $i + 1,
'published' => 1,
- 'created' => date('Y-m-d H:i:s'),
+ 'created' => $now,
'created_by' => 0,
- 'modified' => date('Y-m-d H:i:s'),
+ 'modified' => $now,
'modified_by' => 0,
'language' => '*',
'note' => '',
@@ -1586,10 +1709,12 @@ class Pkg_MokosuiteclientInstallerScript
$db->insertObject('#__guidedtour_steps', $step, 'id');
}
}
+
+ Log::add('Registered ' . \count($tours) . ' MokoSuiteClient guided tours.', Log::INFO, 'mokosuiteclient');
}
catch (\Throwable $e)
{
- Log::add('Guided tours setup error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
+ Log::add('Guided tour registration error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}