2026-02-26 20:22:24 +00:00
<? php
/**
2026-06-07 09:25:45 -05:00
* @package MokoSuite
* @subpackage pkg_mokosuite
2026-05-23 22:41:46 -05:00
* @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-06-07 09:25:45 -05:00
* Package installation script for MokoSuite.
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-06-07 09:25:45 -05:00
class Pkg_MokosuiteInstallerScript
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.
*/
2026-06-06 17:36:17 -05:00
/** @var string|null Download key saved before Joomla wipes update sites */
private ? string $savedDownloadKey = null ;
2026-06-06 15:13:43 -05:00
2026-06-04 10:41:26 -05:00
public function preflight ( $type , $parent )
{
2026-06-06 17:36:17 -05:00
$this -> saveDownloadKey ();
2026-06-06 15:13:43 -05:00
2026-06-04 10:41:26 -05:00
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-07 09:35:22 -05:00
// Migrate MokoWaaS database tables to MokoSuite naming
$this -> migrateWaasTables ();
2026-06-07 09:38:16 -05:00
// Migrate params from old mokowaas extensions to mokosuite equivalents
$this -> migrateWaasExtensionParams ();
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-06-07 09:25:45 -05:00
$this -> enablePlugin ( 'system' , 'mokosuite' );
$this -> enablePlugin ( 'system' , 'mokosuite_firewall' );
$this -> enablePlugin ( 'system' , 'mokosuite_tenant' );
$this -> enablePlugin ( 'system' , 'mokosuite_devtools' );
$this -> enablePlugin ( 'system' , 'mokosuite_offline' );
$this -> enablePlugin ( 'webservices' , 'mokosuite' );
$this -> enablePlugin ( 'task' , 'mokosuitedemo' );
$this -> enablePlugin ( 'task' , 'mokosuitesync' );
$this -> enablePlugin ( 'task' , 'mokosuite_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-07 09:25:45 -05:00
// Set up MokoSuite guided tours and unpublish Joomla defaults
2026-06-04 14:04:02 -05:00
$this -> setupGuidedTours ();
2026-06-07 09:25:45 -05:00
// Mark MokoSuite extensions as protected (prevents disable/uninstall at framework level)
2026-05-24 03:53:33 -05:00
$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-06 17:36:17 -05:00
// Restore download key saved in preflight
$this -> restoreDownloadKey ();
2026-06-06 14:59:59 -05:00
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.
*
2026-06-07 09:25:45 -05:00
* The old standalone plugin was named "mokosuitebrand" (plg_system_mokosuitebrand).
* After the rewrite into the pkg_mokosuite package, the old entries and files
2026-05-30 14:02:39 -05:00
* 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 = [
2026-06-07 09:25:45 -05:00
$db -> quote ( 'mokosuitebrand' ),
$db -> quote ( 'plg_system_mokosuitebrand' ),
2026-05-30 14:02:39 -05:00
];
// 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 = [
2026-06-07 09:25:45 -05:00
JPATH_PLUGINS . '/system/mokosuitebrand' ,
2026-05-30 14:02:39 -05:00
];
foreach ( $legacyDirs as $dir )
{
if ( is_dir ( $dir ))
{
$this -> rmdirRecursive ( $dir );
}
}
if ( $count > 0 )
{
Factory :: getApplication () -> enqueueMessage (
2026-06-07 09:25:45 -05:00
sprintf ( 'Removed %d legacy MokoSuite extension(s).' , $count ),
2026-05-30 14:02:39 -05:00
'message'
);
Log :: add (
2026-06-07 09:25:45 -05:00
sprintf ( 'Cleaned up %d legacy MokoSuite extension entries' , $count ),
2026-05-30 14:02:39 -05:00
Log :: INFO ,
2026-06-07 09:25:45 -05:00
'mokosuite'
2026-05-30 14:02:39 -05:00
);
}
}
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.
*
2026-06-07 09:25:45 -05:00
* plg_system_mokosuite_monitor was merged into the core plugin in 02.32.00.
* Health monitoring is now built into plg_system_mokosuite directly.
2026-06-02 14:17:21 -05:00
*
* @return void
*
* @since 02.32.00
*/
2026-06-02 15:39:04 -05:00
private function migrateStandalonePlugins () : void
{
2026-06-07 09:25:45 -05:00
// Migrate standalone MokoJoomTOS plugin to MokoSuite Offline Bypass
2026-06-02 15:39:04 -05:00
$migrations = [
2026-06-07 09:25:45 -05:00
[ 'old_element' => 'mokojoomtos' , 'old_folder' => 'system' , 'new_element' => 'mokosuite_offline' , 'new_folder' => 'system' ],
2026-06-02 15:39:04 -05:00
];
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 ,
2026-06-07 09:25:45 -05:00
'mokosuite'
2026-06-02 15:39:04 -05:00
);
}
}
catch ( \Throwable $e )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Standalone plugin migration error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-02 15:39:04 -05:00
}
}
/**
* 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 = [
2026-06-07 09:25:45 -05:00
[ 'type' => 'plugin' , 'folder' => 'system' , 'element' => 'mokosuite_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 ,
2026-06-07 09:25:45 -05:00
'mokosuite'
2026-06-02 14:17:21 -05:00
);
}
}
catch ( \Throwable $e )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Retired extension cleanup error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-02 14:17:21 -05:00
}
}
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
/**
2026-06-07 09:25:45 -05:00
* Set the protected flag on all MokoSuite extensions.
2026-05-24 03:53:33 -05:00
*
* 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
2026-06-07 09:25:45 -05:00
// All MokoSuite elements: package, system plugin, component,
2026-05-30 14:02:39 -05:00
// webservices plugins, task plugin
$elements = [
2026-06-07 09:25:45 -05:00
$db -> quote ( 'pkg_mokosuite' ),
$db -> quote ( 'mokosuite' ),
$db -> quote ( 'mokosuite_firewall' ),
$db -> quote ( 'mokosuite_tenant' ),
$db -> quote ( 'mokosuite_devtools' ),
$db -> quote ( 'mokosuite_offline' ),
$db -> quote ( 'com_mokosuite' ),
$db -> quote ( 'mod_mokosuite_cpanel' ),
$db -> quote ( 'mokosuitedemo' ),
$db -> quote ( 'mokosuitesync' ),
$db -> quote ( 'mokosuite_tickets' ),
2026-06-06 19:59:06 -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 )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Error protecting MokoSuite extensions: ' . $e -> getMessage (), Log :: WARNING , 'jerror' );
2026-05-24 03:53:33 -05:00
}
}
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 )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Update server URL migration error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-04 10:15:07 -05:00
}
}
2026-05-31 11:23:54 -05:00
/**
2026-06-07 09:25:45 -05:00
* Remove stale and duplicate MokoSuite update site entries.
2026-05-31 11:23:54 -05:00
*
* 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"
2026-06-07 09:25:45 -05:00
. " AND u.element LIKE " . $db -> quote ( '%mokosuite%' )
2026-06-04 17:50:52 -05:00
);
$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-07 09:25:45 -05:00
$dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/updates.xml' ;
2026-05-31 11:23:54 -05:00
2026-06-07 09:25:45 -05:00
// Find MokoSuite update sites (exclude MokoSuiteHQ and other Moko extensions)
2026-05-31 11:23:54 -05:00
$query = $db -> getQuery ( true )
-> select ( $db -> quoteName ([ 'update_site_id' , 'location' ]))
-> from ( $db -> quoteName ( '#__update_sites' ))
2026-06-07 09:25:45 -05:00
-> where ( '(' . $db -> quoteName ( 'name' ) . ' LIKE ' . $db -> quote ( '%MokoSuite%' )
. ' OR ' . $db -> quoteName ( 'location' ) . ' LIKE ' . $db -> quote ( '%MokoSuite%' ) . ')' )
-> where ( $db -> quoteName ( 'name' ) . ' NOT LIKE ' . $db -> quote ( '%MokoSuiteHQ%' ))
-> where ( $db -> quoteName ( 'location' ) . ' NOT LIKE ' . $db -> quote ( '%MokoSuiteHQ%' ));
2026-05-31 11:23:54 -05:00
$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 (
2026-06-07 09:25:45 -05:00
sprintf ( 'Cleaned up %d stale MokoSuite update site(s).' , $count ),
2026-05-31 11:23:54 -05:00
'message'
);
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Error cleaning up stale update sites: ' . $e -> getMessage (), Log :: WARNING , 'jerror' );
}
}
2026-06-06 14:59:59 -05:00
/**
* Backup all non-empty extra_query values from update sites.
*
* @return array Map of update_site_id => extra_query
*/
2026-06-06 17:36:17 -05:00
private function saveDownloadKey () : void
2026-06-06 14:59:59 -05:00
{
try
{
$db = Factory :: getDbo ();
2026-06-07 11:12:24 -05:00
// Check pkg_mokosuite first, then fall back to old pkg_mokowaas
foreach ([ 'pkg_mokosuite' , 'pkg_mokowaas' ] as $element )
2026-06-06 14:59:59 -05:00
{
2026-06-07 11:12:24 -05:00
$db -> setQuery (
$db -> getQuery ( true )
-> select ( $db -> quoteName ( 'us.extra_query' ))
-> from ( $db -> quoteName ( '#__update_sites' , 'us' ))
-> join ( 'INNER' , $db -> quoteName ( '#__update_sites_extensions' , 'use' ) . ' ON use.update_site_id = us.update_site_id' )
-> join ( 'INNER' , $db -> quoteName ( '#__extensions' , 'e' ) . ' ON e.extension_id = use.extension_id' )
-> where ( $db -> quoteName ( 'e.element' ) . ' = ' . $db -> quote ( $element ))
-> setLimit ( 1 )
);
$key = $db -> loadResult ();
if ( ! empty ( $key ) && strpos ( $key , 'dlid=' ) !== false )
{
$this -> savedDownloadKey = $key ;
break ;
}
2026-06-06 14:59:59 -05:00
}
2026-06-06 15:31:04 -05:00
}
2026-06-06 17:36:17 -05:00
catch ( \Throwable $e ) {}
2026-06-06 15:31:04 -05:00
}
2026-06-06 17:36:17 -05:00
private function restoreDownloadKey () : void
2026-06-06 14:59:59 -05:00
{
2026-06-06 17:36:17 -05:00
if ( $this -> savedDownloadKey === null )
2026-06-06 14:59:59 -05:00
{
return ;
}
try
{
$db = Factory :: getDbo ();
$db -> setQuery (
$db -> getQuery ( true )
2026-06-06 17:36:17 -05:00
-> select ( $db -> quoteName ( 'us.update_site_id' ))
2026-06-06 15:47:38 -05:00
-> from ( $db -> quoteName ( '#__update_sites' , 'us' ))
2026-06-06 17:36:17 -05:00
-> join ( 'INNER' , $db -> quoteName ( '#__update_sites_extensions' , 'use' ) . ' ON use.update_site_id = us.update_site_id' )
-> join ( 'INNER' , $db -> quoteName ( '#__extensions' , 'e' ) . ' ON e.extension_id = use.extension_id' )
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'e.element' ) . ' = ' . $db -> quote ( 'pkg_mokosuite' ))
2026-06-06 17:36:17 -05:00
-> setLimit ( 1 )
2026-06-06 14:59:59 -05:00
);
2026-06-06 17:36:17 -05:00
$siteId = ( int ) $db -> loadResult ();
2026-06-06 14:59:59 -05:00
2026-06-06 17:36:17 -05:00
if ( $siteId > 0 )
2026-06-06 14:59:59 -05:00
{
2026-06-06 17:36:17 -05:00
$db -> setQuery (
$db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__update_sites' ))
-> set ( $db -> quoteName ( 'extra_query' ) . ' = ' . $db -> quote ( $this -> savedDownloadKey ))
-> where ( $db -> quoteName ( 'update_site_id' ) . ' = ' . $siteId )
) -> execute ();
2026-06-06 14:59:59 -05:00
}
}
2026-06-06 17:36:17 -05:00
catch ( \Throwable $e ) {}
2026-06-06 14:59:59 -05:00
}
2026-05-30 14:02:39 -05:00
/**
2026-06-07 09:25:45 -05:00
* Ensure the MokoSuite update server entry stays enabled and points
2026-05-31 10:36:53 -05:00
* 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-07 09:25:45 -05:00
$staticUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/raw/branch/main/updates.xml' ;
2026-06-02 08:12:26 -05:00
// 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 ))
2026-06-07 09:25:45 -05:00
-> where ( '(' . $db -> quoteName ( 'name' ) . ' LIKE ' . $db -> quote ( '%MokoSuite%' )
. ' OR ' . $db -> quoteName ( 'location' ) . ' LIKE ' . $db -> quote ( '%MokoSuite%' ) . ')' )
2026-06-02 08:12:26 -05:00
-> where ( $db -> quoteName ( 'location' ) . ' != ' . $db -> quote ( $staticUrl ))
2026-05-31 10:36:53 -05:00
);
$db -> execute ();
2026-06-07 09:25:45 -05:00
// Enable all MokoSuite update sites
2026-05-30 14:02:39 -05:00
$query = $db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__update_sites' ))
-> set ( $db -> quoteName ( 'enabled' ) . ' = 1' )
2026-06-07 09:25:45 -05:00
-> where ( '(' . $db -> quoteName ( 'name' ) . ' LIKE ' . $db -> quote ( '%MokoSuite%' )
. ' OR ' . $db -> quoteName ( 'location' ) . ' LIKE ' . $db -> quote ( '%MokoSuite%' ) . ')' );
2026-05-30 14:02:39 -05:00
$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
/**
2026-06-07 09:25:45 -05:00
* Send heartbeat to the MokoSuite monitoring receiver.
2026-05-24 03:46:05 -05:00
*
* @return void
*
* @since 02.03.08
*/
private function sendHeartbeat () : void
{
try
{
$db = Factory :: getDbo ();
2026-06-06 20:22:13 -05:00
// Get health token from core plugin
2026-05-24 03:46:05 -05:00
$query = $db -> getQuery ( true )
-> select ( $db -> quoteName ( 'params' ))
-> from ( $db -> quoteName ( '#__extensions' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mokosuite' ))
2026-05-24 03:46:05 -05:00
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( 'system' ));
2026-06-06 20:22:13 -05:00
$coreParams = json_decode (( string ) $db -> setQuery ( $query ) -> loadResult ());
$healthToken = $coreParams -> health_api_token ?? '' ;
2026-05-24 03:46:05 -05:00
if ( empty ( $healthToken ))
{
return ;
}
2026-06-06 20:22:13 -05:00
// Get base URL and signing key from monitor plugin
$query = $db -> getQuery ( true )
-> select ( $db -> quoteName ( 'params' ))
-> from ( $db -> quoteName ( '#__extensions' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mokosuite_monitor' ))
2026-06-06 20:22:13 -05:00
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( 'system' ));
$monitorParams = json_decode (( string ) $db -> setQuery ( $query ) -> loadResult ());
$baseUrl = rtrim ( $monitorParams -> base_url ?? '' , '/' );
2026-06-06 22:42:01 -05:00
// Fall back to manifest XML default if not yet saved in params
if ( empty ( $baseUrl ))
{
2026-06-07 09:25:45 -05:00
$manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml' ;
2026-06-06 22:42:01 -05:00
if ( is_file ( $manifestFile ))
{
$xml = simplexml_load_file ( $manifestFile );
if ( $xml )
{
foreach ( $xml -> xpath ( '//field[@name="base_url"]' ) as $field )
{
$baseUrl = rtrim (( string ) $field [ 'default' ], '/' );
break ;
}
}
}
}
2026-06-06 20:22:13 -05:00
if ( empty ( $baseUrl ))
{
return ;
}
$siteUrl = rtrim ( \Joomla\CMS\Uri\Uri :: root (), '/' );
$domain = parse_url ( $siteUrl , PHP_URL_HOST ) ?: '' ;
$timestamp = time ();
2026-05-24 03:46:05 -05:00
$payload = json_encode ([
2026-06-06 20:22:13 -05:00
'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 ,
2026-05-24 03:46:05 -05:00
], JSON_UNESCAPED_SLASHES );
2026-06-06 20:22:13 -05:00
$headers = [ 'Content-Type: application/json' ];
2026-06-06 20:39:05 -05:00
// RSA sign the request — fall back to manifest XML default
2026-06-06 20:22:13 -05:00
$signingKeyB64 = $monitorParams -> signing_key ?? '' ;
2026-06-06 20:39:05 -05:00
if ( empty ( $signingKeyB64 ))
{
2026-06-07 09:25:45 -05:00
$manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml' ;
2026-06-06 20:39:05 -05:00
if ( is_file ( $manifestFile ))
{
$xml = simplexml_load_file ( $manifestFile );
if ( $xml )
{
foreach ( $xml -> xpath ( '//field[@name="signing_key"]' ) as $field )
{
$signingKeyB64 = ( string ) $field [ 'default' ];
break ;
}
}
}
}
2026-06-06 20:22:13 -05:00
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 ))
{
2026-06-07 09:25:45 -05:00
$headers [] = 'X-MokoSuite-Signature: ' . base64_encode ( $signature );
$headers [] = 'X-MokoSuite-Timestamp: ' . $timestamp ;
2026-06-06 20:22:13 -05:00
}
}
}
2026-06-07 09:25:45 -05:00
$endpoint = $baseUrl . '/api/index.php/v1/mokosuitehq/heartbeat' ;
2026-06-06 20:22:13 -05:00
$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 => false ,
2026-05-24 03:46:05 -05:00
]);
$response = curl_exec ( $ch );
$code = ( int ) curl_getinfo ( $ch , CURLINFO_HTTP_CODE );
curl_close ( $ch );
if ( $code >= 200 && $code < 300 )
{
2026-06-07 09:25:45 -05:00
Factory :: getApplication () -> enqueueMessage ( 'MokoSuiteHQ heartbeat: site registered' , 'message' );
2026-05-24 03:46:05 -05:00
}
}
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' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mod_mokosuite_cpanel' ));
2026-06-02 08:53:47 -05:00
$db -> setQuery ( $query );
$db -> execute ();
// Check if a module instance already exists in #__modules
$query = $db -> getQuery ( true )
-> select ( 'COUNT(*)' )
-> from ( $db -> quoteName ( '#__modules' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'module' ) . ' = ' . $db -> quote ( 'mod_mokosuite_cpanel' ));
2026-06-02 08:53:47 -05:00
$db -> setQuery ( $query );
if (( int ) $db -> loadResult () > 0 )
{
return ;
}
// Create the module instance on the cpanel position
$module = ( object ) [
2026-06-07 09:25:45 -05:00
'title' => 'MokoSuite' ,
2026-06-02 08:53:47 -05:00
'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 ,
2026-06-07 09:25:45 -05:00
'module' => 'mod_mokosuite_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 )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'CPanel module setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-02 08:53:47 -05:00
}
}
2026-06-04 07:05:27 -05:00
/**
2026-06-07 09:25:45 -05:00
* Set up the MokoSuite admin sidebar menu module at position 0.
2026-06-04 07:05:27 -05:00
*/
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' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mod_mokosuite_menu' ))
2026-06-04 07:05:27 -05:00
) -> execute ();
// Check if module instance exists
$db -> setQuery (
$db -> getQuery ( true )
-> select ( 'COUNT(*)' )
-> from ( $db -> quoteName ( '#__modules' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'module' ) . ' = ' . $db -> quote ( 'mod_mokosuite_menu' ))
2026-06-04 07:05:27 -05:00
);
if (( int ) $db -> loadResult () > 0 )
{
return ;
}
$module = ( object ) [
2026-06-07 09:25:45 -05:00
'title' => 'MokoSuite Menu' ,
2026-06-04 07:05:27 -05:00
'note' => '' ,
'content' => '' ,
'ordering' => 0 ,
'position' => 'menu' ,
'checked_out' => null ,
'checked_out_time' => null ,
'publish_up' => null ,
'publish_down' => null ,
'published' => 1 ,
2026-06-07 09:25:45 -05:00
'module' => 'mod_mokosuite_menu' ,
2026-06-04 07:05:27 -05:00
'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 )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Admin menu module setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-04 07:05:27 -05:00
}
}
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' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mod_mokosuite_cache' ))
2026-06-04 09:09:38 -05:00
) -> execute ();
// Check if module instance exists
$db -> setQuery (
$db -> getQuery ( true )
-> select ( 'COUNT(*)' )
-> from ( $db -> quoteName ( '#__modules' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'module' ) . ' = ' . $db -> quote ( 'mod_mokosuite_cache' ))
2026-06-04 09:09:38 -05:00
);
if (( int ) $db -> loadResult () > 0 )
{
return ;
}
$module = ( object ) [
2026-06-07 09:25:45 -05:00
'title' => 'MokoSuite Cache Cleaner' ,
2026-06-04 09:09:38 -05:00
'note' => '' ,
'content' => '' ,
'ordering' => 8 ,
'position' => 'status' ,
'checked_out' => null ,
'checked_out_time' => null ,
'publish_up' => null ,
'publish_down' => null ,
'published' => 1 ,
2026-06-07 09:25:45 -05:00
'module' => 'mod_mokosuite_cache' ,
2026-06-04 09:09:38 -05:00
'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 )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Cache module setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-04 09:09:38 -05:00
}
}
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-07 09:25:45 -05:00
// Find all MokoSuite 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' )
2026-06-07 09:25:45 -05:00
. ' WHERE client_id = 1 AND level = 1 AND link LIKE ' . $db -> quote ( '%com_mokosuite%' ) . ')' )
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 )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Menu icon fix error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-04 09:38:05 -05:00
}
}
2026-06-04 14:04:02 -05:00
/**
2026-06-07 09:25:45 -05:00
* Unpublish default Joomla guided tours and create MokoSuite tours.
2026-06-04 14:04:02 -05:00
* 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' )
2026-06-07 09:25:45 -05:00
. " SET published = 1, title = 'MokoSuite Tours'"
2026-06-04 14:04:02 -05:00
. " 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' ]))
{
2026-06-07 09:25:45 -05:00
$overrides [ 'MOD_GUIDEDTOURS' ] = 'MokoSuite Tours' ;
$overrides [ 'MOD_GUIDEDTOURS_TITLE' ] = 'MokoSuite Tours' ;
2026-06-04 14:04:02 -05:00
$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 ();
2026-06-07 09:25:45 -05:00
// Define MokoSuite tours
2026-06-04 14:04:02 -05:00
$tours = [
[
2026-06-07 09:25:45 -05:00
'uid' => 'mokosuite-welcome' ,
'title' => 'Welcome to MokoSuite' ,
'desc' => 'Get started with the MokoSuite Admin Tools Suite. This tour shows you the key areas of your admin dashboard.' ,
'url' => 'administrator/index.php?option=com_mokosuite' ,
2026-06-04 14:04:02 -05:00
'steps' => [
2026-06-07 09:25:45 -05:00
[ 'title' => 'MokoSuite Dashboard' , 'desc' => 'This is your MokoSuite control center. You can see site info, feature plugins, WAF activity, and quick actions all in one place.' , 'target' => '#mokosuite-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' => '.mokosuite-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' => '#mokosuite-btn-cache' , 'type' => 0 ],
[ 'title' => 'Feature Plugins' , 'desc' => 'MokoSuite features are split into toggleable plugins. Enable or disable security, tenant restrictions, developer tools, and more from here.' , 'target' => '.mokosuite-plugin-grid' , 'type' => 0 ],
[ 'title' => 'MokoSuite Menu' , 'desc' => 'The MokoSuite sidebar menu gives you quick access to all admin tools — Helpdesk, Extensions, WAF Log, Database Tools, and more.' , 'target' => '.mokosuite-admin-menu, [class*="mokosuite"]' , 'type' => 0 ],
2026-06-04 14:04:02 -05:00
],
],
[
2026-06-07 09:25:45 -05:00
'uid' => 'mokosuite-firewall' ,
'title' => 'MokoSuite Firewall Setup' ,
2026-06-04 14:04:02 -05:00
'desc' => 'Configure the Web Application Firewall to protect your site from common attacks.' ,
2026-06-07 09:25:45 -05:00
'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&filter[search]=mokosuite_firewall' ,
2026-06-04 14:04:02 -05:00
'steps' => [
2026-06-07 09:25:45 -05:00
[ 'title' => 'Firewall Plugin' , 'desc' => 'The MokoSuite Firewall provides 10 security shields including SQL injection, XSS, and malicious user agent detection.' , 'target' => '' , 'type' => 0 ],
2026-06-04 14:04:02 -05:00
[ '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 ],
],
],
[
2026-06-07 09:25:45 -05:00
'uid' => 'mokosuite-helpdesk' ,
'title' => 'MokoSuite Helpdesk' ,
2026-06-04 14:04:02 -05:00
'desc' => 'Learn how to manage support tickets, categories, and automation rules.' ,
2026-06-07 09:25:45 -05:00
'url' => 'administrator/index.php?option=com_mokosuite&view=tickets' ,
2026-06-04 14:04:02 -05:00
'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 ],
],
],
[
2026-06-07 09:25:45 -05:00
'uid' => 'mokosuite-extensions' ,
2026-06-04 14:04:02 -05:00
'title' => 'Moko Extensions Manager' ,
'desc' => 'Browse and install Moko Consulting extensions from the built-in catalog.' ,
2026-06-07 09:25:45 -05:00
'url' => 'administrator/index.php?option=com_mokosuite&view=extensions' ,
2026-06-04 14:04:02 -05:00
'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' => '*' ,
2026-06-07 09:25:45 -05:00
'note' => 'MokoSuite' ,
2026-06-04 14:04:02 -05:00
'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 )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Guided tours setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-04 14:04:02 -05:00
}
}
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' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'link' ) . ' LIKE ' . $db -> quote ( '%com_mokosuite&view=tickets%' ))
2026-06-02 16:47:21 -05:00
-> 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' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'com_mokosuite' ))
2026-06-02 16:47:21 -05:00
-> 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' ,
2026-06-07 09:25:45 -05:00
'link' => 'index.php?option=com_mokosuite&view=tickets' ,
2026-06-02 16:47:21 -05:00
'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' ,
2026-06-07 09:25:45 -05:00
'link' => 'index.php?option=com_mokosuite&view=tickets&layout=submit' ,
2026-06-02 18:46:12 -05:00
'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 )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Support menu setup error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-02 16:47:21 -05:00
}
}
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' ))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mokosuite' ))
2026-06-02 08:12:26 -05:00
-> 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 = [
2026-06-07 09:25:45 -05:00
'mokosuite_firewall' => $firewallKeys ,
'mokosuite_tenant' => $tenantKeys ,
'mokosuite_devtools' => $devtoolsKeys ,
2026-06-02 08:12:26 -05:00
];
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 )))
2026-06-07 09:25:45 -05:00
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mokosuite' ))
2026-06-02 08:12:26 -05:00
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( 'system' ))
) -> execute ();
Factory :: getApplication () -> enqueueMessage (
2026-06-07 09:25:45 -05:00
'MokoSuite: migrated settings to feature plugins (Firewall, Tenant, DevTools).' ,
2026-06-02 08:12:26 -05:00
'message'
);
}
catch ( \Throwable $e )
{
2026-06-07 09:25:45 -05:00
Log :: add ( 'Feature param migration error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
2026-06-02 08:12:26 -05:00
}
}
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' ))
2026-06-07 09:25:45 -05:00
-> where ( '(' . $db -> quoteName ( 'name' ) . ' LIKE ' . $db -> quote ( '%MokoSuite%' )
. ' OR ' . $db -> quoteName ( 'location' ) . ' LIKE ' . $db -> quote ( '%MokoSuite%' ) . ')' )
2026-06-04 07:55:16 -05:00
-> 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-06-07 09:35:22 -05:00
/**
* Migrate MokoWaaS database tables to MokoSuite naming.
*
* For each table: create new mokosuite_* table → copy data from mokowaas_* → drop old table.
* Safe to run multiple times — skips tables that don't exist or are already migrated.
*
* @return void
*
* @since 02.35.00
*/
private function migrateWaasTables () : void
{
$tableMap = [
'mokowaas_ticket_categories' => 'mokosuite_ticket_categories' ,
'mokowaas_tickets' => 'mokosuite_tickets' ,
'mokowaas_ticket_replies' => 'mokosuite_ticket_replies' ,
'mokowaas_ticket_canned' => 'mokosuite_ticket_canned' ,
'mokowaas_ticket_automation' => 'mokosuite_ticket_automation' ,
'mokowaas_consent_log' => 'mokosuite_consent_log' ,
'mokowaas_data_requests' => 'mokosuite_data_requests' ,
'mokowaas_retention_policies' => 'mokosuite_retention_policies' ,
'mokowaas_waf_log' => 'mokosuite_waf_log' ,
];
try
{
$db = Factory :: getDbo ();
$prefix = $db -> getPrefix ();
$migrated = 0 ;
foreach ( $tableMap as $oldSuffix => $newSuffix )
{
$oldTable = $prefix . $oldSuffix ;
$newTable = $prefix . $newSuffix ;
// Check if old table exists
$db -> setQuery ( "SHOW TABLES LIKE " . $db -> quote ( $oldTable ));
if ( ! $db -> loadResult ())
{
continue ;
}
// Create new table with same structure if it doesn't exist
$db -> setQuery ( "SHOW TABLES LIKE " . $db -> quote ( $newTable ));
if ( ! $db -> loadResult ())
{
$db -> setQuery ( "CREATE TABLE " . $db -> quoteName ( '#__' . $newSuffix )
. " LIKE " . $db -> quoteName ( '#__' . $oldSuffix ));
$db -> execute ();
}
// Copy data from old to new (skip duplicates on primary key)
$db -> setQuery ( "INSERT IGNORE INTO " . $db -> quoteName ( '#__' . $newSuffix )
. " SELECT * FROM " . $db -> quoteName ( '#__' . $oldSuffix ));
$db -> execute ();
$copied = $db -> getAffectedRows ();
// Drop old table
$db -> setQuery ( "DROP TABLE IF EXISTS " . $db -> quoteName ( '#__' . $oldSuffix ));
$db -> execute ();
$migrated ++ ;
Log :: add (
sprintf ( 'Migrated table %s → %s (%d rows)' , $oldSuffix , $newSuffix , $copied ),
Log :: INFO ,
'mokosuite'
);
}
if ( $migrated > 0 )
{
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'Migrated %d MokoWaaS database table(s) to MokoSuite naming.' , $migrated ),
'message'
);
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Table migration error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
}
}
2026-06-07 09:38:16 -05:00
/**
* Migrate params from old mokowaas extension entries to mokosuite equivalents.
*
* Copies params where the new extension has empty/default params, then deletes
* the old extension entries and their filesystem remnants.
*
* @return void
*
* @since 02.35.00
*/
private function migrateWaasExtensionParams () : void
{
// [old_element, old_folder, new_element, new_folder, type]
$map = [
[ 'mokowaas' , 'system' , 'mokosuite' , 'system' , 'plugin' ],
[ 'mokowaas_firewall' , 'system' , 'mokosuite_firewall' , 'system' , 'plugin' ],
[ 'mokowaas_tenant' , 'system' , 'mokosuite_tenant' , 'system' , 'plugin' ],
[ 'mokowaas_devtools' , 'system' , 'mokosuite_devtools' , 'system' , 'plugin' ],
[ 'mokowaas_offline' , 'system' , 'mokosuite_offline' , 'system' , 'plugin' ],
[ 'mokowaas_monitor' , 'system' , 'mokosuite_monitor' , 'system' , 'plugin' ],
[ 'mokowaas' , 'webservices' , 'mokosuite' , 'webservices' , 'plugin' ],
[ 'mokowaassync' , 'task' , 'mokosuitesync' , 'task' , 'plugin' ],
[ 'mokowaasdemo' , 'task' , 'mokosuitedemo' , 'task' , 'plugin' ],
[ 'mokowaas_tickets' , 'task' , 'mokosuite_tickets' , 'task' , 'plugin' ],
[ 'com_mokowaas' , '' , 'com_mokosuite' , '' , 'component' ],
[ 'mod_mokowaas_cpanel' , '' , 'mod_mokosuite_cpanel' , '' , 'module' ],
[ 'mod_mokowaas_menu' , '' , 'mod_mokosuite_menu' , '' , 'module' ],
[ 'mod_mokowaas_cache' , '' , 'mod_mokosuite_cache' , '' , 'module' ],
[ 'mod_mokowaas_categories' , '' , 'mod_mokosuite_categories' , '' , 'module' ],
[ 'pkg_mokowaas' , '' , 'pkg_mokosuite' , '' , 'package' ],
];
try
{
$db = Factory :: getDbo ();
$migrated = 0 ;
foreach ( $map as [ $oldEl , $oldFolder , $newEl , $newFolder , $type ])
{
// Find old extension
$query = $db -> getQuery ( true )
-> select ([ $db -> quoteName ( 'extension_id' ), $db -> quoteName ( 'params' )])
-> from ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( $oldEl ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( $type ));
if ( $type === 'plugin' )
{
$query -> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( $oldFolder ));
}
$db -> setQuery ( $query );
$old = $db -> loadObject ();
if ( ! $old )
{
continue ;
}
$oldParams = ( string ) ( $old -> params ?? '{}' );
// Copy params to new extension only if new has empty params
if ( $oldParams !== '' && $oldParams !== '{}' && $oldParams !== '[]' )
{
$newQuery = $db -> getQuery ( true )
-> select ( $db -> quoteName ( 'params' ))
-> from ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( $newEl ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( $type ));
if ( $type === 'plugin' )
{
$newQuery -> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( $newFolder ));
}
$db -> setQuery ( $newQuery );
$newParams = ( string ) $db -> loadResult ();
if ( empty ( $newParams ) || $newParams === '{}' || $newParams === '[]' )
{
$updateQuery = $db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__extensions' ))
-> set ( $db -> quoteName ( 'params' ) . ' = ' . $db -> quote ( $oldParams ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( $newEl ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( $type ));
if ( $type === 'plugin' )
{
$updateQuery -> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( $newFolder ));
}
$db -> setQuery ( $updateQuery ) -> execute ();
Log :: add (
sprintf ( 'Migrated params from %s to %s' , $oldEl , $newEl ),
Log :: INFO ,
'mokosuite'
);
}
}
// Unprotect old extension
$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 update site links
$db -> setQuery (
$db -> getQuery ( true )
-> select ( $db -> quoteName ( 'update_site_id' ))
-> from ( $db -> quoteName ( '#__update_sites_extensions' ))
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . ( int ) $old -> extension_id )
);
$siteIds = $db -> loadColumn ();
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__update_sites_extensions' ))
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . ( int ) $old -> extension_id )
) -> execute ();
if ( ! empty ( $siteIds ))
{
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__updates' ))
-> where ( $db -> quoteName ( 'update_site_id' ) . ' IN (' . implode ( ',' , array_map ( 'intval' , $siteIds )) . ')' )
) -> execute ();
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__update_sites' ))
-> where ( $db -> quoteName ( 'update_site_id' ) . ' IN (' . implode ( ',' , array_map ( 'intval' , $siteIds )) . ')' )
) -> execute ();
}
// Delete old extension entry
$db -> setQuery (
$db -> getQuery ( true )
-> delete ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'extension_id' ) . ' = ' . ( int ) $old -> extension_id )
) -> execute ();
// Remove old plugin/module filesystem remnants
$dir = null ;
if ( $type === 'plugin' )
{
$dir = JPATH_PLUGINS . '/' . $oldFolder . '/' . $oldEl ;
}
elseif ( $type === 'module' )
{
$dir = JPATH_ADMINISTRATOR . '/modules/' . $oldEl ;
}
elseif ( $type === 'component' )
{
// Components have admin + site dirs
foreach ([ JPATH_ADMINISTRATOR . '/components/' . $oldEl , JPATH_SITE . '/components/' . $oldEl ] as $cDir )
{
if ( is_dir ( $cDir ))
{
$this -> rmdirRecursive ( $cDir );
}
}
}
if ( $dir && is_dir ( $dir ))
{
$this -> rmdirRecursive ( $dir );
}
$migrated ++ ;
}
if ( $migrated > 0 )
{
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'Migrated params from %d MokoWaaS extension(s) and removed old entries.' , $migrated ),
'message'
);
}
}
catch ( \Throwable $e )
{
Log :: add ( 'Extension param migration error: ' . $e -> getMessage (), Log :: WARNING , 'mokosuite' );
}
}
2026-02-26 20:22:24 +00:00
}