feat: index.php bootstrap migration from MokoCassiopeia

Joomla 6 doesn't call <scriptfile> for templates. Instead, run
migration on first page load via index.php:
- Detect MokoCassiopeia styles → copy params to MokoOnyx
- Create matching style copies for additional styles
- Set MokoOnyx as default if Cassiopeia was default
- Copy user files (custom themes, user.css, user.js)
- Redirect update server to MokoOnyx
- Creates .migrated marker so it only runs once

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-04-21 13:58:15 -05:00
parent 8db0a4fa88
commit ad1070be03
2 changed files with 210 additions and 0 deletions

205
src/helper/migrate.php Normal file
View File

@@ -0,0 +1,205 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* One-time migration from MokoCassiopeia → MokoOnyx.
* Called from index.php on first page load. Creates a .migrated
* marker file so it only runs once.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
(function () {
$markerFile = __DIR__ . '/../.migrated';
// Already migrated
if (file_exists($markerFile)) {
return;
}
$db = Factory::getDbo();
$app = Factory::getApplication();
$oldName = 'mokocassiopeia';
$newName = 'mokoonyx';
$oldDisplay = 'MokoCassiopeia';
$newDisplay = 'MokoOnyx';
// Init logger
Log::addLogger(
['text_file' => 'mokoonyx_migrate.log.php'],
Log::ALL,
['mokoonyx_migrate']
);
$log = function (string $msg, int $level = Log::INFO) {
Log::add($msg, $level, 'mokoonyx_migrate');
};
$log('=== MokoOnyx migration started (index.php bootstrap) ===');
// Check if MokoCassiopeia has styles to migrate
$query = $db->getQuery(true)
->select('*')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote($oldName))
->where($db->quoteName('client_id') . ' = 0');
$oldStyles = $db->setQuery($query)->loadObjectList();
if (empty($oldStyles)) {
$log('No MokoCassiopeia styles found — fresh install, nothing to migrate.');
@file_put_contents($markerFile, date('Y-m-d H:i:s') . ' fresh install');
return;
}
$log('Found ' . count($oldStyles) . ' MokoCassiopeia style(s) to migrate.');
// Get the default MokoOnyx style (created by Joomla installer)
$query = $db->getQuery(true)
->select('id')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote($newName))
->where($db->quoteName('client_id') . ' = 0')
->order($db->quoteName('id') . ' ASC');
$defaultOnyxId = (int) $db->setQuery($query, 0, 1)->loadResult();
$isFirst = true;
foreach ($oldStyles as $old) {
$newTitle = str_replace($oldDisplay, $newDisplay, $old->title);
$newTitle = str_replace($oldName, $newName, $newTitle);
$newParams = is_string($old->params)
? str_replace($oldName, $newName, $old->params)
: $old->params;
if ($isFirst && $defaultOnyxId) {
// Apply params to the installer-created default MokoOnyx style
$update = $db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
->set($db->quoteName('params') . ' = ' . $db->quote($newParams))
->where('id = ' . $defaultOnyxId);
$db->setQuery($update)->execute();
// Set as default if MokoCassiopeia was default
if ($old->home == 1) {
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 1')
->where('id = ' . $defaultOnyxId)
)->execute();
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 0')
->where('id = ' . (int) $old->id)
)->execute();
$log('Set MokoOnyx as default site template.');
}
$log("Applied params to default MokoOnyx style: {$newTitle}");
$isFirst = false;
continue;
}
// Additional styles: check if already exists
$check = $db->getQuery(true)
->select('COUNT(*)')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote($newName))
->where($db->quoteName('title') . ' = ' . $db->quote($newTitle));
if ((int) $db->setQuery($check)->loadResult() > 0) {
$log("Style '{$newTitle}' already exists — skipping.");
continue;
}
// Create new MokoOnyx style copy
$new = clone $old;
unset($new->id);
$new->template = $newName;
$new->title = $newTitle;
$new->params = $newParams;
$new->home = 0;
try {
$db->insertObject('#__template_styles', $new, 'id');
$log("Created MokoOnyx style: {$newTitle}");
} catch (\Throwable $e) {
$log("Failed to create style '{$newTitle}': " . $e->getMessage(), Log::WARNING);
}
}
// Copy user files from MokoCassiopeia media
$oldMedia = JPATH_ROOT . '/media/templates/site/' . $oldName;
$newMedia = JPATH_ROOT . '/media/templates/site/' . $newName;
if (is_dir($oldMedia) && is_dir($newMedia)) {
$userFiles = [
'css/theme/light.custom.css',
'css/theme/dark.custom.css',
'css/theme/light.custom.min.css',
'css/theme/dark.custom.min.css',
'css/user.css',
'css/user.min.css',
'js/user.js',
'js/user.min.js',
];
$copied = 0;
foreach ($userFiles as $rel) {
$src = $oldMedia . '/' . $rel;
$dst = $newMedia . '/' . $rel;
if (is_file($src) && !is_file($dst)) {
$dir = dirname($dst);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
copy($src, $dst);
$copied++;
}
}
if ($copied > 0) {
$log("Copied {$copied} user file(s) from MokoCassiopeia.");
}
}
// Update the update server
try {
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
$query = $db->getQuery(true)
->update('#__update_sites')
->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
->set($db->quoteName('name') . ' = ' . $db->quote($newDisplay))
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
$db->setQuery($query)->execute();
$n = $db->getAffectedRows();
if ($n > 0) {
$log("Redirected {$n} update site(s) to MokoOnyx.");
}
} catch (\Throwable $e) {
$log('Update server redirect failed: ' . $e->getMessage(), Log::WARNING);
}
// Write marker file
@file_put_contents($markerFile, date('Y-m-d H:i:s') . " migrated {$oldName}{$newName}");
$log('=== Migration completed ===');
// Enqueue message for admin
if ($app->isClient('administrator')) {
$app->enqueueMessage(
'<strong>MokoOnyx has imported your MokoCassiopeia settings.</strong><br>'
. 'You can safely uninstall MokoCassiopeia from Extensions &rarr; Manage.',
'success'
);
}
})();

View File

@@ -9,6 +9,11 @@
defined('_JEXEC') or die; defined('_JEXEC') or die;
// One-time migration from MokoCassiopeia (runs once, creates .migrated marker)
if (!file_exists(__DIR__ . '/.migrated')) {
require_once __DIR__ . '/helper/migrate.php';
}
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;