2026-02-26 20:22:24 +00:00
<? php
/**
2026-05-23 22:41:46 -05:00
* @package MokoWaaS
* @subpackage pkg_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
2026-02-26 20:22:24 +00:00
*/
defined ( '_JEXEC' ) or die ;
use Joomla\CMS\Factory ;
use Joomla\CMS\Installer\InstallerAdapter ;
use Joomla\CMS\Log\Log ;
/**
2026-05-23 22:41:46 -05:00
* Package installation script for MokoWaaS.
2026-02-26 20:22:24 +00:00
*
2026-05-24 03:46:05 -05:00
* Handles migration from standalone plugin to package, enables plugins,
* and triggers heartbeat registration on install/update.
2026-02-26 20:22:24 +00:00
*
2026-05-23 22:41:46 -05:00
* @since 2.2.0
2026-02-26 20:22:24 +00:00
*/
2026-05-23 22:41:46 -05:00
class Pkg_MokowaasInstallerScript
2026-02-26 20:22:24 +00:00
{
/**
2026-05-23 22:41:46 -05:00
* Runs after package installation/update.
2026-02-26 20:22:24 +00:00
*
2026-05-23 22:41:46 -05:00
* @param string $type Installation type
* @param InstallerAdapter $parent Parent installer
2026-04-07 16:11:21 -05:00
*
* @return void
*
2026-05-23 22:41:46 -05:00
* @since 2.2.0
2026-04-07 16:11:21 -05:00
*/
2026-06-04 10:41:26 -05:00
/**
* Runs before package installation/update.
*
* Fixes MySQL strict mode incompatibility: #__extensions.element is NOT NULL
* with no default, causing INSERT failures when Joomla's package installer
* creates placeholder rows before processing sub-extension manifests.
*/
public function preflight ( $type , $parent )
{
try
{
$db = Factory :: getDbo ();
$db -> setQuery ( "ALTER TABLE " . $db -> quoteName ( '#__extensions' )
. " MODIFY " . $db -> quoteName ( 'element' ) . " VARCHAR(100) NOT NULL DEFAULT ''" );
$db -> execute ();
}
catch ( \Throwable $e )
{
// Non-fatal — column may already have a default
}
}
2026-05-23 22:41:46 -05:00
public function postflight ( $type , $parent )
2026-04-07 16:11:21 -05:00
{
2026-06-04 10:31:43 -05:00
// Remove legacy extensions and migrate settings before retiring
2026-05-30 14:02:39 -05:00
$this -> cleanupLegacyExtensions ();
2026-06-02 15:39:04 -05:00
$this -> migrateStandalonePlugins ();
2026-06-04 10:31:43 -05:00
$this -> removeRetiredExtensions ();
2026-05-30 14:02:39 -05:00
2026-05-24 03:46:05 -05:00
$this -> enablePlugin ( 'system' , 'mokowaas' );
2026-06-02 08:12:26 -05:00
$this -> enablePlugin ( 'system' , 'mokowaas_firewall' );
$this -> enablePlugin ( 'system' , 'mokowaas_tenant' );
$this -> enablePlugin ( 'system' , 'mokowaas_devtools' );
2026-06-02 15:39:04 -05:00
$this -> enablePlugin ( 'system' , 'mokowaas_offline' );
2026-05-24 03:46:05 -05:00
$this -> enablePlugin ( 'webservices' , 'mokowaas' );
2026-05-30 12:50:11 -05:00
$this -> enablePlugin ( 'task' , 'mokowaasdemo' );
2026-05-30 22:45:54 -05:00
$this -> enablePlugin ( 'task' , 'mokowaassync' );
2026-06-02 19:29:10 -05:00
$this -> enablePlugin ( 'task' , 'mokowaas_tickets' );
2026-05-24 03:46:05 -05:00
2026-06-02 08:12:26 -05:00
// Migrate params from core plugin to feature plugins (one-time)
$this -> migrateFeatureParams ();
2026-06-02 08:53:47 -05:00
// Set up cpanel module on the admin dashboard
$this -> setupCpanelModule ();
2026-06-04 07:05:27 -05:00
// Set up admin sidebar menu module
$this -> setupAdminMenuModule ();
2026-06-04 09:09:38 -05:00
// Set up cache cleaner status bar module
$this -> setupCacheModule ();
2026-06-02 16:47:21 -05:00
// Create Support portal menu item on frontend
$this -> setupSupportMenuItem ();
2026-06-04 09:38:05 -05:00
// Set menu_icon params on submenu items (Joomla only renders img on level 1)
$this -> fixMenuIcons ();
2026-06-04 14:04:02 -05:00
// Set up MokoWaaS guided tours and unpublish Joomla defaults
$this -> setupGuidedTours ();
2026-05-24 03:53:33 -05:00
// Mark MokoWaaS extensions as protected (prevents disable/uninstall at framework level)
$this -> protectExtensions ();
2026-06-04 10:15:07 -05:00
// Migrate all Moko update server URLs to new format
$this -> migrateUpdateServerUrls ();
2026-05-31 11:23:54 -05:00
// Clean up stale/duplicate update sites
$this -> cleanupStaleUpdateSites ();
2026-06-04 17:50:52 -05:00
// Fix orphaned update records (extension_id=0)
$this -> fixUpdateRecords ();
2026-05-24 03:46:05 -05:00
// Trigger heartbeat registration
$this -> sendHeartbeat ();
2026-06-04 07:55:16 -05:00
// Warn if no license key is configured
$this -> warnMissingLicenseKey ();
2026-04-07 16:22:01 -05:00
}
2026-05-30 14:02:39 -05:00
/**
* Remove legacy/stale extension entries and filesystem remnants.
*
* The old standalone plugin was named "mokowaasbrand" (plg_system_mokowaasbrand).
* After the rewrite into the pkg_mokowaas package, the old entries and files
* may linger — especially on sites restored from old backups.
*
* @return void
*
* @since 02.21.00
*/
private function cleanupLegacyExtensions () : void
{
try
{
$db = Factory :: getDbo ();
// Legacy element names to remove from #__extensions
$legacy = [
$db -> quote ( 'mokowaasbrand' ),
$db -> quote ( 'plg_system_mokowaasbrand' ),
];
// Delete from #__extensions
$query = $db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'element' ) . ' IN (' . implode ( ',' , $legacy ) . ')' );
$db -> setQuery ( $query );
$affected = $db -> execute ();
$count = $db -> getAffectedRows ();
// Remove legacy plugin files from the filesystem
$legacyDirs = [
JPATH_PLUGINS . '/system/mokowaasbrand' ,
];
foreach ( $legacyDirs as $dir )
{
if ( is_dir ( $dir ))
{
$this -> rmdirRecursive ( $dir );
}
}
if ( $count > 0 )
{
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'Removed %d legacy MokoWaaS extension(s).' , $count ),
'message'
);
Log :: add (
sprintf ( 'Cleaned up %d legacy MokoWaaS extension entries' , $count ),
Log :: INFO ,
'mokowaas'
);
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Legacy cleanup error: ' . $e -> getMessage (), Log :: WARNING , 'jerror' );
}
}
2026-06-02 14:17:21 -05:00
/**
* Remove extensions that have been retired and merged into core.
*
* plg_system_mokowaas_monitor was merged into the core plugin in 02.32.00.
* Health monitoring is now built into plg_system_mokowaas directly.
*
* @return void
*
* @since 02.32.00
*/
2026-06-02 15:39:04 -05:00
private function migrateStandalonePlugins () : void
{
// Migrate standalone MokoJoomTOS plugin to MokoWaaS Offline Bypass
$migrations = [
[ 'old_element' => 'mokojoomtos' , 'old_folder' => 'system' , 'new_element' => 'mokowaas_offline' , 'new_folder' => 'system' ],
];
try
{
$db = Factory :: getDbo ();
foreach ( $migrations as $m )
{
// Check if old plugin exists
$query = $db -> getQuery ( true )
-> select ([ $db -> quoteName ( 'extension_id' ), $db -> quoteName ( 'params' )])
-> from ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( $m [ 'old_element' ]))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( $m [ 'old_folder' ]));
$db -> setQuery ( $query );
$old = $db -> loadObject ();
if ( ! $old )
{
continue ;
}
$oldParams = $old -> params ?? '{}' ;
// Copy params to new plugin (only if new plugin has empty params)
$query = $db -> getQuery ( true )
-> select ( $db -> quoteName ( 'params' ))
-> from ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( $m [ 'new_element' ]))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( $m [ 'new_folder' ]));
$db -> setQuery ( $query );
$newParams = ( string ) $db -> loadResult ();
if ( empty ( $newParams ) || $newParams === '{}' || $newParams === '[]' )
{
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'params' ) . ' = ' . $db -> quote ( $oldParams ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( $m [ 'new_element' ]))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( $m [ 'new_folder' ]))
) -> execute ();
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'Migrated settings from %s to %s.' , $m [ 'old_element' ], $m [ 'new_element' ]),
'message'
);
}
// Unprotect old plugin
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'protected' ) . ' = 0' )
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . ( int ) $old -> extension_id )
) -> execute ();
// Remove old extension record
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . ( int ) $old -> extension_id )
) -> execute ();
// Remove old update site entries
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__update_sites_extensions' ))
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . ( int ) $old -> extension_id )
) -> execute ();
// Remove old files
$dir = JPATH_PLUGINS . '/' . $m [ 'old_folder' ] . '/' . $m [ 'old_element' ];
if ( is_dir ( $dir ))
{
$this -> rmdirRecursive ( $dir );
}
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'Removed standalone %s plugin (replaced by %s).' , $m [ 'old_element' ], $m [ 'new_element' ]),
'message'
);
Log :: add (
sprintf ( 'Migrated %s → %s and removed old plugin' , $m [ 'old_element' ], $m [ 'new_element' ]),
Log :: INFO ,
'mokowaas'
);
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Standalone plugin migration error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
/**
* Remove extensions that have been retired and merged into core.
*
* @return void
*
* @since 02.32.00
*/
2026-06-02 14:17:21 -05:00
private function removeRetiredExtensions () : void
{
$retired = [
[ 'type' => 'plugin' , 'folder' => 'system' , 'element' => 'mokowaas_monitor' ],
2026-06-04 10:30:09 -05:00
[ 'type' => 'plugin' , 'folder' => 'system' , 'element' => 'mokojoomtos' ],
[ 'type' => 'plugin' , 'folder' => 'system' , 'element' => 'mokoatsautomation' ],
[ 'type' => 'plugin' , 'folder' => 'webservices' , 'element' => 'mokodpcalendarapi' ],
[ 'type' => 'plugin' , 'folder' => 'system' , 'element' => 'mokogallerycalendar' ],
2026-06-02 14:17:21 -05:00
];
try
{
$db = Factory :: getDbo ();
foreach ( $retired as $ext )
{
// Check if installed
$query = $db -> getQuery ( true )
-> select ( $db -> quoteName ( 'extension_id' ))
-> from ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( $ext [ 'type' ]))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( $ext [ 'folder' ]))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( $ext [ 'element' ]));
$db -> setQuery ( $query );
$extId = ( int ) $db -> loadResult ();
if ( ! $extId )
{
continue ;
}
// Unprotect so Joomla allows removal
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'protected' ) . ' = 0' )
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . $extId )
) -> execute ();
2026-06-04 10:30:09 -05:00
// Remove update site links and update sites
$db -> setQuery (
$db -> getQuery ( true )
-> select ( $db -> quoteName ( 'update_site_id' ))
-> from ( $db -> quoteName ( '#__update_sites_extensions' ))
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . $extId )
);
$siteIds = $db -> loadColumn ();
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__update_sites_extensions' ))
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . $extId )
) -> execute ();
if ( ! empty ( $siteIds ))
{
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__updates' ))
-> where ( $db -> quoteName ( 'update_site_id' ) . ' IN (' . implode ( ',' , $siteIds ) . ')' )
) -> execute ();
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__update_sites' ))
-> where ( $db -> quoteName ( 'update_site_id' ) . ' IN (' . implode ( ',' , $siteIds ) . ')' )
) -> execute ();
}
2026-06-02 14:17:21 -05:00
// Remove extension record
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . $extId )
) -> execute ();
// Remove files
$dir = JPATH_PLUGINS . '/' . $ext [ 'folder' ] . '/' . $ext [ 'element' ];
if ( is_dir ( $dir ))
{
$this -> rmdirRecursive ( $dir );
}
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'Removed retired extension: %s/%s' , $ext [ 'folder' ], $ext [ 'element' ]),
'message'
);
Log :: add (
sprintf ( 'Removed retired extension %s/%s (ID %d)' , $ext [ 'folder' ], $ext [ 'element' ], $extId ),
Log :: INFO ,
'mokowaas'
);
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Retired extension cleanup error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
2026-05-30 14:02:39 -05:00
/**
* Recursively remove a directory.
*
* @param string $dir Directory path
*
* @return void
*
* @since 02.21.00
*/
private function rmdirRecursive ( string $dir ) : void
{
$items = new \RecursiveIteratorIterator (
new \RecursiveDirectoryIterator ( $dir , \RecursiveDirectoryIterator :: SKIP_DOTS ),
\RecursiveIteratorIterator :: CHILD_FIRST
);
foreach ( $items as $item )
{
if ( $item -> isDir ())
{
@ rmdir ( $item -> getPathname ());
}
else
{
@ unlink ( $item -> getPathname ());
}
}
@ rmdir ( $dir );
}
2026-04-07 16:28:08 -05:00
/**
2026-05-23 22:41:46 -05:00
* Enable a plugin by group and element.
2026-04-07 16:39:08 -05:00
*
2026-05-23 22:41:46 -05:00
* @param string $group Plugin group
* @param string $element Plugin element name
2026-04-07 16:39:08 -05:00
*
* @return void
*
2026-05-23 22:41:46 -05:00
* @since 2.2.0
2026-04-07 16:39:08 -05:00
*/
2026-05-23 22:41:46 -05:00
private function enablePlugin ( string $group , string $element ) : void
2026-04-07 16:39:08 -05:00
{
try
{
2026-05-23 22:41:46 -05:00
$db = Factory :: getDbo ();
$query = $db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'enabled' ) . ' = 1' )
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( $group ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( $element ));
$db -> setQuery ( $query );
2026-05-21 15:42:58 -05:00
$db -> execute ();
}
2026-05-23 22:41:46 -05:00
catch ( \Throwable $e )
2026-03-26 13:53:24 -05:00
{
2026-05-23 22:41:46 -05:00
Log :: add ( 'Error enabling plugin ' . $group . '/' . $element . ': ' . $e -> getMessage (), Log :: WARNING , 'jerror' );
2026-03-26 13:53:24 -05:00
}
2026-02-26 20:22:24 +00:00
}
2026-05-24 03:46:05 -05:00
2026-05-24 03:53:33 -05:00
/**
* Set the protected flag on all MokoWaaS extensions.
*
* Joomla's protected flag prevents disabling and uninstalling at the
* framework level — no plugin-side interception needed.
*
* @return void
*
* @since 02.03.10
*/
private function protectExtensions () : void
{
try
{
$db = Factory :: getDbo ();
2026-05-30 14:02:39 -05:00
// All MokoWaaS elements: package, system plugin, component,
// webservices plugins, task plugin
$elements = [
$db -> quote ( 'pkg_mokowaas' ),
$db -> quote ( 'mokowaas' ),
2026-06-02 08:12:26 -05:00
$db -> quote ( 'mokowaas_firewall' ),
$db -> quote ( 'mokowaas_tenant' ),
$db -> quote ( 'mokowaas_devtools' ),
2026-06-02 15:39:04 -05:00
$db -> quote ( 'mokowaas_offline' ),
2026-05-30 14:02:39 -05:00
$db -> quote ( 'com_mokowaas' ),
2026-06-02 08:53:47 -05:00
$db -> quote ( 'mod_mokowaas_cpanel' ),
2026-05-30 14:02:39 -05:00
$db -> quote ( 'mokowaasdemo' ),
2026-05-30 22:45:54 -05:00
$db -> quote ( 'mokowaassync' ),
2026-06-02 19:29:10 -05:00
$db -> quote ( 'mokowaas_tickets' ),
2026-05-30 14:02:39 -05:00
$db -> quote ( 'perfectpublisher' ),
2026-06-02 09:57:49 -05:00
$db -> quote ( 'mokoonyx' ),
2026-05-30 14:02:39 -05:00
];
2026-05-24 03:53:33 -05:00
$query = $db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'protected' ) . ' = 1' )
-> set ( $db -> quoteName ( 'locked' ) . ' = 0' )
2026-05-30 14:02:39 -05:00
-> where ( $db -> quoteName ( 'element' ) . ' IN (' . implode ( ',' , $elements ) . ')' );
2026-05-24 03:53:33 -05:00
$db -> setQuery ( $query );
$db -> execute ();
2026-05-30 14:02:39 -05:00
// Ensure update server stays enabled
$this -> enableUpdateServer ();
2026-05-24 03:53:33 -05:00
}
catch ( \Throwable $e )
{
Log :: add ( 'Error protecting MokoWaaS extensions: ' . $e -> getMessage (), Log :: WARNING , 'jerror' );
}
}
2026-06-04 10:15:07 -05:00
/**
* Rewrite all Moko Consulting update server URLs from the old
* raw/branch/main pattern to the new clean /updates.xml pattern.
*
* Old: https://git.mokoconsulting.tech/MokoConsulting/{repo}/raw/branch/main/updates.xml
* New: https://git.mokoconsulting.tech/MokoConsulting/{repo}/updates.xml
*/
private function migrateUpdateServerUrls () : void
{
try
{
$db = Factory :: getDbo ();
$db -> setQuery (
"UPDATE " . $db -> quoteName ( '#__update_sites' )
. " SET " . $db -> quoteName ( 'location' ) . " = REPLACE("
. $db -> quoteName ( 'location' ) . ", '/raw/branch/main/updates.xml', '/updates.xml')"
. " WHERE " . $db -> quoteName ( 'location' ) . " LIKE " . $db -> quote ( '%mokoconsulting.tech%/raw/branch/main/updates.xml' )
);
$db -> execute ();
$count = $db -> getAffectedRows ();
if ( $count > 0 )
{
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'Migrated %d Moko update server URL(s) to new format.' , $count ),
'message'
);
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Update server URL migration error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
2026-05-31 11:23:54 -05:00
/**
* Remove stale and duplicate MokoWaaS update site entries.
*
* Keeps only the package-level update site pointing to the dynamic
* MokoGitea endpoint. Removes plugin-level entries, old static URLs,
* and orphaned #__updates rows tied to deleted update sites.
*
* @return void
*
2026-05-31 12:40:05 -05:00
* @since 02.31.00
2026-05-31 11:23:54 -05:00
*/
2026-06-04 17:50:52 -05:00
private function fixUpdateRecords () : void
{
try
{
$db = Factory :: getDbo ();
// Link orphaned #__updates records to the installed extension
$db -> setQuery (
"UPDATE " . $db -> quoteName ( '#__updates' ) . " u"
. " JOIN " . $db -> quoteName ( '#__extensions' ) . " e"
. " ON u.element = e.element AND u.type = e.type"
. " SET u.extension_id = e.extension_id"
. " WHERE u.extension_id = 0"
. " AND u.element LIKE " . $db -> quote ( '%mokowaas%' )
);
$db -> execute ();
}
catch ( \Throwable $e )
{
// Non-critical
}
}
2026-05-31 11:23:54 -05:00
private function cleanupStaleUpdateSites () : void
{
try
{
$db = Factory :: getDbo ();
2026-06-02 08:12:26 -05:00
$dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml' ;
2026-05-31 11:23:54 -05:00
// Find all MokoWaaS update sites
$query = $db -> getQuery ( true )
-> select ( $db -> quoteName ([ 'update_site_id' , 'location' ]))
-> from ( $db -> quoteName ( '#__update_sites' ))
-> where ( '(' . $db -> quoteName ( 'name' ) . ' LIKE ' . $db -> quote ( '%MokoWaaS%' )
. ' OR ' . $db -> quoteName ( 'location' ) . ' LIKE ' . $db -> quote ( '%MokoWaaS%' ) . ')' );
$db -> setQuery ( $query );
$sites = $db -> loadObjectList ();
$keepId = null ;
$removeIds = [];
foreach ( $sites as $site )
{
if ( $site -> location === $dynamicUrl && $keepId === null )
{
$keepId = ( int ) $site -> update_site_id ;
}
else
{
$removeIds [] = ( int ) $site -> update_site_id ;
}
}
if ( empty ( $removeIds ))
{
return ;
}
$idList = implode ( ',' , $removeIds );
// Remove orphaned #__updates rows
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__updates' ))
-> where ( $db -> quoteName ( 'update_site_id' ) . ' IN (' . $idList . ')' )
) -> execute ();
// Remove link rows
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__update_sites_extensions' ))
-> where ( $db -> quoteName ( 'update_site_id' ) . ' IN (' . $idList . ')' )
) -> execute ();
// Remove stale update sites
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__update_sites' ))
-> where ( $db -> quoteName ( 'update_site_id' ) . ' IN (' . $idList . ')' )
) -> execute ();
$count = count ( $removeIds );
if ( $count > 0 )
{
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'Cleaned up %d stale MokoWaaS update site(s).' , $count ),
'message'
);
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Error cleaning up stale update sites: ' . $e -> getMessage (), Log :: WARNING , 'jerror' );
}
}
2026-05-30 14:02:39 -05:00
/**
2026-05-31 10:36:53 -05:00
* Ensure the MokoWaaS update server entry stays enabled and points
* to the correct dynamic endpoint with the license key attached.
2026-05-30 14:02:39 -05:00
*
2026-05-31 10:36:53 -05:00
* Migrates legacy static URLs (raw/branch/main/updates.xml) to the
* dynamic MokoGitea update feed, and syncs the license key from
* plugin params into extra_query so Joomla sends it as dlid.
2026-05-30 14:02:39 -05:00
*
* @return void
*
* @since 02.21.00
*/
private function enableUpdateServer () : void
{
try
{
$db = Factory :: getDbo ();
2026-06-02 08:12:26 -05:00
$staticUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml' ;
// Migrate old dynamic URL to static raw file URL
2026-05-31 10:36:53 -05:00
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__update_sites' ))
2026-06-02 08:12:26 -05:00
-> set ( $db -> quoteName ( 'location' ) . ' = ' . $db -> quote ( $staticUrl ))
-> where ( '(' . $db -> quoteName ( 'name' ) . ' LIKE ' . $db -> quote ( '%MokoWaaS%' )
. ' OR ' . $db -> quoteName ( 'location' ) . ' LIKE ' . $db -> quote ( '%MokoWaaS%' ) . ')' )
-> where ( $db -> quoteName ( 'location' ) . ' != ' . $db -> quote ( $staticUrl ))
2026-05-31 10:36:53 -05:00
);
$db -> execute ();
// Enable all MokoWaaS update sites
2026-05-30 14:02:39 -05:00
$query = $db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__update_sites' ))
-> set ( $db -> quoteName ( 'enabled' ) . ' = 1' )
-> where ( '(' . $db -> quoteName ( 'name' ) . ' LIKE ' . $db -> quote ( '%MokoWaaS%' )
. ' OR ' . $db -> quoteName ( 'location' ) . ' LIKE ' . $db -> quote ( '%MokoWaaS%' ) . ')' );
$db -> setQuery ( $query );
$db -> execute ();
}
catch ( \Throwable $e )
{
Log :: add ( 'Error enabling update server: ' . $e -> getMessage (), Log :: WARNING , 'jerror' );
}
}
2026-05-24 03:46:05 -05:00
/**
* Send heartbeat to the MokoWaaS monitoring receiver.
*
* @return void
*
* @since 02.03.08
*/
private function sendHeartbeat () : void
{
try
{
$db = Factory :: getDbo ();
$query = $db -> getQuery ( true )
-> select ( $db -> quoteName ( 'params' ))
-> from ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mokowaas' ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( 'system' ));
$params = json_decode (( string ) $db -> setQuery ( $query ) -> loadResult ());
$healthToken = $params -> health_api_token ?? '' ;
if ( empty ( $healthToken ))
{
return ;
}
$siteUrl = rtrim ( \Joomla\CMS\Uri\Uri :: root (), '/' );
$siteName = Factory :: getConfig () -> get ( 'sitename' , 'Joomla' );
$payload = json_encode ([
'site_url' => $siteUrl ,
'site_name' => $siteName ,
'health_token' => $healthToken ,
'action' => 'register' ,
], JSON_UNESCAPED_SLASHES );
$ch = curl_init ( 'https://bench.mokoconsulting.tech/api/waas-heartbeat/register' );
curl_setopt ( $ch , CURLOPT_POST , true );
curl_setopt ( $ch , CURLOPT_HTTPHEADER , [
'Content-Type: application/json' ,
'X-MokoWaaS-Key: moko-waas-hb-2026-x9k4m' ,
]);
curl_setopt ( $ch , CURLOPT_POSTFIELDS , $payload );
curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $ch , CURLOPT_TIMEOUT , 15 );
curl_setopt ( $ch , CURLOPT_FOLLOWLOCATION , true );
curl_setopt ( $ch , CURLOPT_SSL_VERIFYPEER , false );
$response = curl_exec ( $ch );
$code = ( int ) curl_getinfo ( $ch , CURLINFO_HTTP_CODE );
curl_close ( $ch );
if ( $code >= 200 && $code < 300 )
{
Factory :: getApplication () -> enqueueMessage ( 'Grafana heartbeat: site registered' , 'message' );
}
}
catch ( \Throwable $e )
{
// Silent failure — heartbeat is non-critical
}
}
2026-06-02 08:12:26 -05:00
/**
* One-time migration of params from the monolithic core plugin to
* the new feature plugins. Copies security, tenant, and dev params.
2026-06-02 08:53:47 -05:00
*
* @return void
*
* @since 02.32.00
*/
private function setupCpanelModule () : void
{
try
{
$db = Factory :: getDbo ();
// Enable the module
$query = $db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'enabled' ) . ' = 1' )
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'module' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mod_mokowaas_cpanel' ));
$db -> setQuery ( $query );
$db -> execute ();
// Check if a module instance already exists in #__modules
$query = $db -> getQuery ( true )
-> select ( 'COUNT(*)' )
-> from ( $db -> quoteName ( '#__modules' ))
-> where ( $db -> quoteName ( 'module' ) . ' = ' . $db -> quote ( 'mod_mokowaas_cpanel' ));
$db -> setQuery ( $query );
if (( int ) $db -> loadResult () > 0 )
{
return ;
}
// Create the module instance on the cpanel position
$module = ( object ) [
'title' => 'MokoWaaS' ,
'note' => '' ,
'content' => '' ,
2026-06-02 09:08:25 -05:00
'ordering' => 0 ,
2026-06-02 09:24:00 -05:00
'position' => 'top' ,
2026-06-02 08:53:47 -05:00
'checked_out' => null ,
'checked_out_time' => null ,
'publish_up' => null ,
'publish_down' => null ,
'published' => 1 ,
'module' => 'mod_mokowaas_cpanel' ,
2026-06-02 09:07:57 -05:00
'access' => 6 , // Super Users only
2026-06-02 10:14:14 -05:00
'showtitle' => 0 ,
2026-06-02 08:53:47 -05:00
'params' => '{"show_health":"1","show_plugins":"1"}' ,
'client_id' => 1 , // Administrator
'language' => '*' ,
];
$db -> insertObject ( '#__modules' , $module , 'id' );
$moduleId = ( int ) $module -> id ;
if ( $moduleId )
{
// Assign to all admin pages
$map = ( object ) [
'moduleid' => $moduleId ,
'menuid' => 0 , // 0 = all pages
];
$db -> insertObject ( '#__modules_menu' , $map );
}
}
catch ( \Throwable $e )
{
Log :: add ( 'CPanel module setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
2026-06-04 07:05:27 -05:00
/**
* Set up the MokoWaaS admin sidebar menu module at position 0.
*/
private function setupAdminMenuModule () : void
{
try
{
$db = Factory :: getDbo ();
// Enable the module extension
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'enabled' ) . ' = 1' )
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'module' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mod_mokowaas_menu' ))
) -> execute ();
// Check if module instance exists
$db -> setQuery (
$db -> getQuery ( true )
-> select ( 'COUNT(*)' )
-> from ( $db -> quoteName ( '#__modules' ))
-> where ( $db -> quoteName ( 'module' ) . ' = ' . $db -> quote ( 'mod_mokowaas_menu' ))
);
if (( int ) $db -> loadResult () > 0 )
{
return ;
}
$module = ( object ) [
'title' => 'MokoWaaS Menu' ,
'note' => '' ,
'content' => '' ,
'ordering' => 0 ,
'position' => 'menu' ,
'checked_out' => null ,
'checked_out_time' => null ,
'publish_up' => null ,
'publish_down' => null ,
'published' => 1 ,
'module' => 'mod_mokowaas_menu' ,
'access' => 3 ,
'showtitle' => 0 ,
'params' => '{}' ,
'client_id' => 1 ,
'language' => '*' ,
];
$db -> insertObject ( '#__modules' , $module , 'id' );
if (( int ) $module -> id )
{
$db -> insertObject ( '#__modules_menu' , ( object ) [ 'moduleid' => ( int ) $module -> id , 'menuid' => 0 ]);
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Admin menu module setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
2026-06-04 09:09:38 -05:00
/**
* Set up the cache cleaner module in the admin status bar position.
*/
private function setupCacheModule () : void
{
try
{
$db = Factory :: getDbo ();
// Enable the module extension
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'enabled' ) . ' = 1' )
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'module' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mod_mokowaas_cache' ))
) -> execute ();
// Check if module instance exists
$db -> setQuery (
$db -> getQuery ( true )
-> select ( 'COUNT(*)' )
-> from ( $db -> quoteName ( '#__modules' ))
-> where ( $db -> quoteName ( 'module' ) . ' = ' . $db -> quote ( 'mod_mokowaas_cache' ))
);
if (( int ) $db -> loadResult () > 0 )
{
return ;
}
$module = ( object ) [
'title' => 'MokoWaaS Cache Cleaner' ,
'note' => '' ,
'content' => '' ,
'ordering' => 8 ,
'position' => 'status' ,
'checked_out' => null ,
'checked_out_time' => null ,
'publish_up' => null ,
'publish_down' => null ,
'published' => 1 ,
'module' => 'mod_mokowaas_cache' ,
'access' => 3 ,
'showtitle' => 0 ,
'params' => '{}' ,
'client_id' => 1 ,
'language' => '*' ,
];
$db -> insertObject ( '#__modules' , $module , 'id' );
if (( int ) $module -> id )
{
$mm = ( object ) [ 'moduleid' => ( int ) $module -> id , 'menuid' => 0 ];
$db -> insertObject ( '#__modules_menu' , $mm , 'moduleid' );
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Cache module setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
2026-06-04 09:38:05 -05:00
/**
* Joomla only renders the img column icon for level-1 menu items.
* Submenu items (level 2) need menu_icon set in the params JSON.
*/
private function fixMenuIcons () : void
{
try
{
$db = Factory :: getDbo ();
$iconMap = [
'class:cogs' => 'icon-cogs' ,
'class:puzzle-piece' => 'icon-puzzle-piece' ,
2026-06-04 20:33:56 -05:00
'class:headphones' => 'fa-solid fa-handshake-angle' ,
'class:file-code' => 'fa-solid fa-file-code' ,
2026-06-04 09:38:05 -05:00
'class:lock' => 'icon-lock' ,
'class:shield-alt' => 'icon-shield-alt' ,
'class:database' => 'icon-database' ,
'class:trash' => 'icon-trash' ,
'class:power-off' => 'icon-power-off' ,
'class:refresh' => 'icon-refresh' ,
'class:check-square' => 'icon-check-square' ,
'class:bolt' => 'icon-bolt' ,
];
2026-06-04 20:33:56 -05:00
// Find all MokoWaaS component submenu items (including those linking to other components)
2026-06-04 09:38:05 -05:00
$db -> setQuery (
2026-06-04 20:33:56 -05:00
$db -> getQuery ( true )
-> select ([ 'm.id' , 'm.img' , 'm.params' ])
-> from ( $db -> quoteName ( '#__menu' , 'm' ))
-> where ( 'm.client_id = 1' )
-> where ( 'm.level >= 2' )
-> where ( 'm.parent_id IN (SELECT id FROM ' . $db -> quoteName ( '#__menu' )
. ' WHERE client_id = 1 AND level = 1 AND link LIKE ' . $db -> quote ( '%com_mokowaas%' ) . ')' )
2026-06-04 09:38:05 -05:00
);
foreach ( $db -> loadObjectList () as $item )
{
$icon = $iconMap [ $item -> img ] ?? '' ;
if ( ! $icon )
{
continue ;
}
$params = json_decode ( $item -> params ?: '{}' , true ) ?: [];
if ( ! empty ( $params [ 'menu_icon' ]))
{
continue ;
}
$params [ 'menu_icon' ] = $icon ;
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__menu' ))
-> set ( $db -> quoteName ( 'params' ) . ' = ' . $db -> quote ( json_encode ( $params )))
-> where ( $db -> quoteName ( 'id' ) . ' = ' . ( int ) $item -> id )
) -> execute ();
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Menu icon fix error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
2026-06-04 14:04:02 -05:00
/**
* Unpublish default Joomla guided tours and create MokoWaaS tours.
* Re-enables the guided tours plugin if disabled.
*/
private function setupGuidedTours () : void
{
try
{
$db = Factory :: getDbo ();
// Re-enable guided tours plugin (may have been disabled)
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'enabled' ) . ' = 1' )
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'guidedtours' ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
) -> execute ();
// Re-enable the guided tours module (shows our tours, not Joomla's)
$db -> setQuery (
"UPDATE " . $db -> quoteName ( '#__modules' )
. " SET published = 1, title = 'MokoWaaS Tours'"
. " WHERE module = 'mod_guidedtours'"
);
$db -> execute ();
// Override the guided tours module language string
$overridePath = JPATH_ADMINISTRATOR . '/language/overrides/en-GB.override.ini' ;
$overrides = file_exists ( $overridePath ) ? parse_ini_file ( $overridePath ) : [];
if ( empty ( $overrides [ 'MOD_GUIDEDTOURS' ]))
{
$overrides [ 'MOD_GUIDEDTOURS' ] = 'MokoWaaS Tours' ;
$overrides [ 'MOD_GUIDEDTOURS_TITLE' ] = 'MokoWaaS Tours' ;
$lines = [];
foreach ( $overrides as $k => $v )
{
$lines [] = $k . '="' . str_replace ( '"' , '\"' , $v ) . '"' ;
}
file_put_contents ( $overridePath , implode ( " \n " , $lines ) . " \n " );
}
// Unpublish all default Joomla tours
$db -> setQuery (
"UPDATE " . $db -> quoteName ( '#__guidedtours' )
. " SET published = 0"
. " WHERE " . $db -> quoteName ( 'uid' ) . " LIKE 'joomla-%'"
);
$db -> execute ();
// Define MokoWaaS tours
$tours = [
[
'uid' => 'mokowaas-welcome' ,
'title' => 'Welcome to MokoWaaS' ,
'desc' => 'Get started with the MokoWaaS Admin Tools Suite. This tour shows you the key areas of your admin dashboard.' ,
'url' => 'administrator/index.php?option=com_mokowaas' ,
'steps' => [
[ 'title' => 'MokoWaaS Dashboard' , 'desc' => 'This is your MokoWaaS control center. You can see site info, feature plugins, WAF activity, and quick actions all in one place.' , 'target' => '#mokowaas-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' => '.mokowaas-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' => '#mokowaas-btn-cache' , 'type' => 0 ],
[ 'title' => 'Feature Plugins' , 'desc' => 'MokoWaaS features are split into toggleable plugins. Enable or disable security, tenant restrictions, developer tools, and more from here.' , 'target' => '.mokowaas-plugin-grid' , 'type' => 0 ],
[ 'title' => 'MokoWaaS Menu' , 'desc' => 'The MokoWaaS sidebar menu gives you quick access to all admin tools — Helpdesk, Extensions, WAF Log, Database Tools, and more.' , 'target' => '.mokowaas-admin-menu, [class*="mokowaas"]' , 'type' => 0 ],
],
],
[
'uid' => 'mokowaas-firewall' ,
'title' => 'MokoWaaS 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]=mokowaas_firewall' ,
'steps' => [
[ 'title' => 'Firewall Plugin' , 'desc' => 'The MokoWaaS 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' => 'mokowaas-helpdesk' ,
'title' => 'MokoWaaS Helpdesk' ,
'desc' => 'Learn how to manage support tickets, categories, and automation rules.' ,
'url' => 'administrator/index.php?option=com_mokowaas&view=tickets' ,
'steps' => [
[ 'title' => 'Ticket List' , 'desc' => 'View all support tickets with status, priority, SLA tracking, and assignment. Filter by status or search to find specific tickets.' , 'target' => '' , 'type' => 0 ],
[ 'title' => 'Create a Ticket' , 'desc' => 'Click the New button to create a support ticket. Assign a category, priority, and optional SLA deadline.' , 'target' => '' , 'type' => 0 ],
[ 'title' => 'Ticket Automation' , 'desc' => 'Set up automation rules that trigger on ticket events (new ticket, status change) or Joomla events (user login, registration). Automate assignment, notifications, and status changes.' , 'target' => '' , 'type' => 0 ],
],
],
[
'uid' => 'mokowaas-extensions' ,
'title' => 'Moko Extensions Manager' ,
'desc' => 'Browse and install Moko Consulting extensions from the built-in catalog.' ,
'url' => 'administrator/index.php?option=com_mokowaas&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 ],
],
],
];
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' ]))
);
if ( $db -> loadResult ())
{
continue ;
}
$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' => 'MokoWaaS' ,
'access' => 3 ,
'ordering' => 0 ,
'autostart' => 0 ,
];
$db -> insertObject ( '#__guidedtours' , $tour , 'id' );
$tourId = ( int ) $tour -> id ;
foreach ( $tourDef [ 'steps' ] as $i => $stepDef )
{
$step = ( object ) [
'tour_id' => $tourId ,
'title' => $stepDef [ 'title' ],
'description' => $stepDef [ 'desc' ],
'target' => $stepDef [ 'target' ],
'type' => $stepDef [ 'type' ],
'interactive_type' => 1 ,
'url' => '' ,
'position' => 'bottom' ,
'ordering' => $i + 1 ,
'published' => 1 ,
'created' => date ( 'Y-m-d H:i:s' ),
'created_by' => 0 ,
'modified' => date ( 'Y-m-d H:i:s' ),
'modified_by' => 0 ,
'language' => '*' ,
'note' => '' ,
'params' => '{}' ,
];
$db -> insertObject ( '#__guidedtour_steps' , $step , 'id' );
}
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Guided tours setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
2026-06-02 16:47:21 -05:00
/**
* Create a "Support" menu item on the frontend main menu.
*/
private function setupSupportMenuItem () : void
{
try
{
$db = Factory :: getDbo ();
$db -> setQuery (
$db -> getQuery ( true )
-> select ( 'COUNT(*)' )
-> from ( $db -> quoteName ( '#__menu' ))
-> where ( $db -> quoteName ( 'link' ) . ' LIKE ' . $db -> quote ( '%com_mokowaas&view=tickets%' ))
-> where ( $db -> quoteName ( 'client_id' ) . ' = 0' )
);
if (( int ) $db -> loadResult () > 0 )
{
return ;
}
$db -> setQuery (
$db -> getQuery ( true )
-> select ( $db -> quoteName ( 'extension_id' ))
-> from ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'com_mokowaas' ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'component' ))
);
$componentId = ( int ) $db -> loadResult ();
if ( ! $componentId )
{
return ;
}
$db -> setQuery ( "SELECT id FROM #__menu WHERE menutype = '' AND level = 0 AND client_id = 0 LIMIT 1" );
$rootId = ( int ) $db -> loadResult () ?: 1 ;
$db -> setQuery ( 'SELECT MAX(rgt) FROM #__menu WHERE client_id = 0' );
$maxRgt = ( int ) $db -> loadResult ();
$item = ( object ) [
'menutype' => 'mainmenu' ,
'title' => 'Support' ,
'alias' => 'support' ,
'note' => '' ,
'path' => 'support' ,
'link' => 'index.php?option=com_mokowaas&view=tickets' ,
'type' => 'component' ,
'published' => 1 ,
'parent_id' => $rootId ,
'level' => 1 ,
'component_id' => $componentId ,
'checked_out' => null ,
'checked_out_time' => null ,
'browserNav' => 0 ,
'access' => 2 ,
'img' => '' ,
'template_style_id' => 0 ,
'params' => '{}' ,
'lft' => $maxRgt + 1 ,
'rgt' => $maxRgt + 2 ,
'home' => 0 ,
'language' => '*' ,
'client_id' => 0 ,
];
$db -> insertObject ( '#__menu' , $item , 'id' );
2026-06-02 18:46:12 -05:00
$supportId = ( int ) $item -> id ;
// Create "Submit a Ticket" child menu item
if ( $supportId )
{
$db -> setQuery ( 'SELECT MAX(rgt) FROM #__menu WHERE client_id = 0' );
$maxRgt2 = ( int ) $db -> loadResult ();
$child = ( object ) [
'menutype' => 'mainmenu' ,
'title' => 'Submit a Ticket' ,
'alias' => 'submit-ticket' ,
'note' => '' ,
'path' => 'support/submit-ticket' ,
'link' => 'index.php?option=com_mokowaas&view=tickets&layout=submit' ,
'type' => 'component' ,
'published' => 1 ,
'parent_id' => $supportId ,
'level' => 2 ,
'component_id' => $componentId ,
'checked_out' => null ,
'checked_out_time' => null ,
'browserNav' => 0 ,
'access' => 2 ,
'img' => '' ,
'template_style_id' => 0 ,
'params' => '{}' ,
'lft' => $maxRgt2 + 1 ,
'rgt' => $maxRgt2 + 2 ,
'home' => 0 ,
'language' => '*' ,
'client_id' => 0 ,
];
$db -> insertObject ( '#__menu' , $child , 'id' );
}
2026-06-02 16:47:21 -05:00
}
catch ( \Throwable $e )
{
Log :: add ( 'Support menu setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
2026-06-02 08:53:47 -05:00
/**
* One-time migration of params from the monolithic core plugin to
* the new feature plugins. Copies security, tenant, and dev params.
2026-06-02 08:12:26 -05:00
*
* @return void
*
* @since 02.32.00
*/
private function migrateFeatureParams () : void
{
try
{
$db = Factory :: getDbo ();
// Read core plugin params
$query = $db -> getQuery ( true )
-> select ( $db -> quoteName ( 'params' ))
-> from ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mokowaas' ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( 'system' ));
$db -> setQuery ( $query );
$coreParamsJson = ( string ) $db -> loadResult ();
if ( empty ( $coreParamsJson ) || $coreParamsJson === '{}' )
{
return ;
}
$core = json_decode ( $coreParamsJson , true );
if ( empty ( $core ))
{
return ;
}
// Check migration marker
if ( ! empty ( $core [ '_params_migrated_032' ]))
{
return ;
}
// Firewall params
$firewallKeys = [
'force_https' , 'admin_session_timeout' , 'trusted_ips' ,
'password_min_length' , 'password_require_uppercase' ,
'password_require_number' , 'password_require_special' ,
'upload_allowed_types' , 'upload_max_size_mb' ,
];
// Tenant params
$tenantKeys = [
'restrict_installer' , 'allow_extension_updates' , 'hide_sysinfo' ,
'restrict_global_config' , 'restrict_template_editing' ,
'disable_install_url' , 'hidden_menu_items' ,
];
// DevTools params
$devtoolsKeys = [ 'dev_mode' , 'reset_hits' , 'delete_versions' ];
$migrations = [
'mokowaas_firewall' => $firewallKeys ,
'mokowaas_tenant' => $tenantKeys ,
'mokowaas_devtools' => $devtoolsKeys ,
];
foreach ( $migrations as $element => $keys )
{
$featureParams = [];
foreach ( $keys as $key )
{
if ( isset ( $core [ $key ]))
{
$featureParams [ $key ] = $core [ $key ];
}
}
if ( empty ( $featureParams ))
{
continue ;
}
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'params' ) . ' = ' . $db -> quote ( json_encode ( $featureParams )))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( $element ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( 'system' ))
) -> execute ();
}
// Set migration marker on core plugin
$core [ '_params_migrated_032' ] = 1 ;
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'params' ) . ' = ' . $db -> quote ( json_encode ( $core )))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mokowaas' ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( 'system' ))
) -> execute ();
Factory :: getApplication () -> enqueueMessage (
'MokoWaaS: migrated settings to feature plugins (Firewall, Tenant, DevTools).' ,
'message'
);
}
catch ( \Throwable $e )
{
Log :: add ( 'Feature param migration error: ' . $e -> getMessage (), Log :: WARNING , 'mokowaas' );
}
}
2026-06-04 07:55:16 -05:00
/**
* Warn after install/update if no license key (dlid) is configured on the update site.
*/
private function warnMissingLicenseKey () : void
{
try
{
$db = Factory :: getDbo ();
$app = Factory :: getApplication ();
$query = $db -> getQuery ( true )
-> select ([ $db -> quoteName ( 'update_site_id' ), $db -> quoteName ( 'extra_query' )])
-> from ( $db -> quoteName ( '#__update_sites' ))
-> where ( '(' . $db -> quoteName ( 'name' ) . ' LIKE ' . $db -> quote ( '%MokoWaaS%' )
. ' OR ' . $db -> quoteName ( 'location' ) . ' LIKE ' . $db -> quote ( '%MokoWaaS%' ) . ')' )
-> setLimit ( 1 );
$db -> setQuery ( $query );
$site = $db -> loadObject ();
if ( $site )
{
$extraQuery = ( string ) ( $site -> extra_query ?? '' );
if ( ! empty ( $extraQuery ) && strpos ( $extraQuery , 'dlid=' ) !== false )
{
parse_str ( $extraQuery , $parsed );
if ( ! empty ( $parsed [ 'dlid' ]))
{
return ;
}
}
$editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . ( int ) $site -> update_site_id ;
}
else
{
$editUrl = 'index.php?option=com_installer&view=updatesites' ;
}
$app -> enqueueMessage (
'<strong>Moko Consulting License Key Required</strong> — '
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
. '<a href="' . $editUrl . '" class="btn btn-sm btn-warning ms-2">Enter License Key</a>' ,
'warning'
);
}
catch ( \Throwable $e )
{
// Silent
}
}
2026-02-26 20:22:24 +00:00
}