- Rename templates/mokocassiopeia → mokoonyx - Rename media/templates/site/mokocassiopeia → mokoonyx - Update #__extensions element and name - Update #__template_styles template, title, and params - Update #__menu link references - Update #__update_sites to point to MokoOnyx updates.xml - Clear #__updates cached entries for old extension - No HTTP requests, no ZIP downloads, no Installer conflicts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
257 lines
9.1 KiB
PHP
257 lines
9.1 KiB
PHP
<?php
|
|
/**
|
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* 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(
|
|
'<strong>MokoCassiopeia has been renamed to MokoOnyx.</strong><br>'
|
|
. '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');
|
|
}
|
|
}
|