* * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later */ /** * Bridge migration helper — MokoCassiopeia → MokoOnyx * * Renames template files/folders and updates the database to migrate * from MokoCassiopeia to MokoOnyx. No external downloads required. */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Log\Log; class MokoBridgeMigration { private const OLD_NAME = 'mokocassiopeia'; private const NEW_NAME = 'mokoonyx'; private const OLD_DISPLAY = 'MokoCassiopeia'; private const NEW_DISPLAY = 'MokoOnyx'; /** * Run the full migration. */ public static function run(): bool { $app = Factory::getApplication(); // Already migrated? if (is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) { self::log('MokoOnyx template dir already exists — updating database only.'); self::updateDatabase(); self::notifyUser($app); return true; } // 1. Rename template directory $renamed = self::renameTemplateDir(); if (!$renamed) { $app->enqueueMessage( 'MokoOnyx migration: could not rename template directory. ' . 'Please rename templates/mokocassiopeia to templates/mokoonyx manually.', 'warning' ); return false; } // 2. Rename media directory self::renameMediaDir(); // 3. Update database (extensions, template_styles, menu assignments) self::updateDatabase(); // 4. Notify admin self::notifyUser($app); self::log('Bridge migration completed successfully.'); return true; } /** * Rename templates/mokocassiopeia → templates/mokoonyx */ private static function renameTemplateDir(): bool { $oldDir = JPATH_ROOT . '/templates/' . self::OLD_NAME; $newDir = JPATH_ROOT . '/templates/' . self::NEW_NAME; if (!is_dir($oldDir)) { self::log('Bridge: old template dir not found: ' . $oldDir, 'warning'); return false; } if (is_dir($newDir)) { self::log('Bridge: new template dir already exists — skipping rename'); return true; } $result = @rename($oldDir, $newDir); if ($result) { self::log('Bridge: renamed template dir to ' . self::NEW_NAME); } else { self::log('Bridge: failed to rename template dir', 'error'); } return $result; } /** * Rename media/templates/site/mokocassiopeia → mokoonyx */ private static function renameMediaDir(): void { $oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME; $newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; if (!is_dir($oldMedia)) { self::log('Bridge: old media dir not found — skipping'); return; } if (is_dir($newMedia)) { self::log('Bridge: new media dir already exists — skipping rename'); return; } if (@rename($oldMedia, $newMedia)) { self::log('Bridge: renamed media dir to ' . self::NEW_NAME); } else { self::log('Bridge: failed to rename media dir', 'warning'); } } /** * Update all database references from mokocassiopeia → mokoonyx. */ private static function updateDatabase(): void { $db = Factory::getDbo(); // 1. Update #__extensions — change element and name $query = $db->getQuery(true) ->update('#__extensions') ->set($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME)) ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_NAME)) ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) ->where($db->quoteName('type') . ' = ' . $db->quote('template')); try { $db->setQuery($query)->execute(); $affected = $db->getAffectedRows(); if ($affected > 0) { self::log("Bridge: updated {$affected} row(s) in #__extensions"); } } catch (\Throwable $e) { self::log('Bridge: #__extensions update failed: ' . $e->getMessage(), 'error'); } // 2. Update #__template_styles — rename template and title $query = $db->getQuery(true) ->select('*') ->from('#__template_styles') ->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME)); $styles = $db->setQuery($query)->loadObjectList(); foreach ($styles as $style) { $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $style->title); // Also catch lowercase variant $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); $newParams = $style->params; if (is_string($newParams)) { $newParams = str_replace(self::OLD_NAME, self::NEW_NAME, $newParams); } $update = $db->getQuery(true) ->update('#__template_styles') ->set($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle)) ->set($db->quoteName('params') . ' = ' . $db->quote($newParams)) ->where('id = ' . (int) $style->id); try { $db->setQuery($update)->execute(); } catch (\Throwable $e) { self::log('Bridge: style update failed for id=' . $style->id . ': ' . $e->getMessage(), 'error'); } } if (!empty($styles)) { self::log('Bridge: updated ' . count($styles) . ' template style(s) in #__template_styles'); } // 3. Update #__menu — fix template_style_id link field references // Menu items store the template name in the link for template-specific assignments try { $query = $db->getQuery(true) ->update('#__menu') ->set($db->quoteName('link') . ' = REPLACE(' . $db->quoteName('link') . ', ' . $db->quote(self::OLD_NAME) . ', ' . $db->quote(self::NEW_NAME) . ')') ->where($db->quoteName('link') . ' LIKE ' . $db->quote('%' . self::OLD_NAME . '%')); $db->setQuery($query)->execute(); $affected = $db->getAffectedRows(); if ($affected > 0) { self::log("Bridge: updated {$affected} menu link(s)"); } } catch (\Throwable $e) { self::log('Bridge: #__menu update failed: ' . $e->getMessage(), 'warning'); } // 4. Update #__update_sites — point to MokoOnyx updates.xml try { $newLocation = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; $query = $db->getQuery(true) ->update('#__update_sites') ->set($db->quoteName('location') . ' = ' . $db->quote($newLocation)) ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY)) ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%')); $db->setQuery($query)->execute(); $affected = $db->getAffectedRows(); if ($affected > 0) { self::log("Bridge: updated {$affected} update site(s) to MokoOnyx"); } } catch (\Throwable $e) { self::log('Bridge: #__update_sites update failed: ' . $e->getMessage(), 'warning'); } // 5. Update #__updates — clear cached updates for old extension try { $query = $db->getQuery(true) ->delete('#__updates') ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)); $db->setQuery($query)->execute(); $affected = $db->getAffectedRows(); if ($affected > 0) { self::log("Bridge: cleared {$affected} cached update(s) for old extension"); } } catch (\Throwable $e) { self::log('Bridge: #__updates cleanup failed: ' . $e->getMessage(), 'warning'); } } private static function notifyUser($app): void { $app->enqueueMessage( 'MokoCassiopeia has been renamed to MokoOnyx.
' . 'Your template files, settings, and menu assignments have been migrated automatically. ' . 'MokoOnyx is now your active site template.', 'success' ); } private static function log(string $message, string $priority = 'info'): void { $priorities = [ 'info' => Log::INFO, 'warning' => Log::WARNING, 'error' => Log::ERROR, ]; Log::addLogger( ['text_file' => 'mokocassiopeia_bridge.log.php'], Log::ALL, ['mokocassiopeia_bridge'] ); Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia_bridge'); } }