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 @@ + + + + +
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'); } }