Bridge: download and install MokoOnyx from Gitea release
Some checks failed
Repo Health / Access control (push) Successful in 1s
Auto-Update SHA Hash / Update SHA-256 Hash in updates.xml (release) Failing after 5s
Repo Health / Release configuration (push) Failing after 4s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 4s
Some checks failed
Repo Health / Access control (push) Successful in 1s
Auto-Update SHA Hash / Update SHA-256 Hash in updates.xml (release) Failing after 5s
Repo Health / Release configuration (push) Failing after 4s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 4s
Instead of copying/renaming files, the bridge now: 1. Downloads mokoonyx ZIP from Gitea releases 2. Installs via Joomla's Installer (proper extension registration) 3. Migrates template styles and default assignment 4. Falls back gracefully with manual install link if download fails Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,16 +10,14 @@
|
||||
/**
|
||||
* Bridge migration helper — MokoCassiopeia → MokoOnyx
|
||||
*
|
||||
* Called from script.php during the v03.10.00 update. Copies the template
|
||||
* to the new directory name, migrates database records, and sets MokoOnyx
|
||||
* as the active site template.
|
||||
* Downloads and installs MokoOnyx from the Gitea release, then migrates
|
||||
* template styles and menu assignments from MokoCassiopeia.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Filesystem\File;
|
||||
use Joomla\CMS\Filesystem\Folder;
|
||||
use Joomla\CMS\Installer\Installer;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
class MokoBridgeMigration
|
||||
@@ -30,179 +28,166 @@ class MokoBridgeMigration
|
||||
private const OLD_DISPLAY = 'MokoCassiopeia';
|
||||
private const NEW_DISPLAY = 'MokoOnyx';
|
||||
|
||||
/** URL to the latest MokoOnyx stable release ZIP */
|
||||
private const RELEASE_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip';
|
||||
|
||||
/**
|
||||
* Run the full migration.
|
||||
*
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public static function run(): bool
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// 1. Copy template files
|
||||
if (!self::copyTemplateFiles()) {
|
||||
// Check if MokoOnyx is already installed
|
||||
if (is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) {
|
||||
self::log('MokoOnyx already installed — skipping download.');
|
||||
self::migrateStyles();
|
||||
self::notifyUser($app);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1. Download MokoOnyx ZIP
|
||||
$zipPath = self::downloadRelease();
|
||||
if (!$zipPath) {
|
||||
$app->enqueueMessage(
|
||||
'MokoOnyx migration: failed to copy template files. '
|
||||
. 'You can manually copy templates/mokocassiopeia to templates/mokoonyx.',
|
||||
'error'
|
||||
'MokoOnyx migration: could not download the MokoOnyx template package. '
|
||||
. 'Please install MokoOnyx manually from '
|
||||
. '<a href="https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases" target="_blank">Gitea Releases</a>.',
|
||||
'warning'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Copy media files
|
||||
if (!self::copyMediaFiles()) {
|
||||
// 2. Install MokoOnyx via Joomla's installer
|
||||
$installed = self::installPackage($zipPath);
|
||||
|
||||
// Clean up downloaded ZIP
|
||||
@unlink($zipPath);
|
||||
|
||||
if (!$installed) {
|
||||
$app->enqueueMessage(
|
||||
'MokoOnyx migration: failed to copy media files. '
|
||||
. 'You can manually copy media/templates/site/mokocassiopeia to media/templates/site/mokoonyx.',
|
||||
'MokoOnyx migration: installation failed. '
|
||||
. 'Please install MokoOnyx manually from '
|
||||
. '<a href="https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases" target="_blank">Gitea Releases</a>.',
|
||||
'warning'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Rename internals in the new copy (templateDetails.xml, language files, etc.)
|
||||
self::renameInternals();
|
||||
// 3. Migrate template styles
|
||||
self::migrateStyles();
|
||||
|
||||
// 4. Register the new template in the database
|
||||
self::migrateDatabase($db);
|
||||
|
||||
// 5. Notify the admin
|
||||
$app->enqueueMessage(
|
||||
'<strong>MokoCassiopeia has been renamed to MokoOnyx.</strong><br>'
|
||||
. 'Your template settings have been migrated automatically. '
|
||||
. 'MokoOnyx is now your active site template. '
|
||||
. 'You can safely uninstall MokoCassiopeia from Extensions → Manage.',
|
||||
'success'
|
||||
);
|
||||
// 4. Notify admin
|
||||
self::notifyUser($app);
|
||||
|
||||
self::log('Bridge migration completed successfully.');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy template directory.
|
||||
* Download the MokoOnyx ZIP to Joomla's tmp directory.
|
||||
*/
|
||||
private static function copyTemplateFiles(): bool
|
||||
private static function downloadRelease(): ?string
|
||||
{
|
||||
$src = JPATH_ROOT . '/templates/' . self::OLD_NAME;
|
||||
$dst = JPATH_ROOT . '/templates/' . self::NEW_NAME;
|
||||
$tmpDir = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp');
|
||||
$zipPath = $tmpDir . '/mokoonyx-install.zip';
|
||||
|
||||
if (is_dir($dst)) {
|
||||
self::log('MokoOnyx template directory already exists — skipping copy.');
|
||||
return true;
|
||||
$content = false;
|
||||
|
||||
// Method 1: file_get_contents
|
||||
if (ini_get('allow_url_fopen')) {
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 60,
|
||||
'follow_location' => true,
|
||||
'max_redirects' => 5,
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
$content = @file_get_contents(self::RELEASE_URL, false, $ctx);
|
||||
}
|
||||
|
||||
if (!is_dir($src)) {
|
||||
self::log('Source template directory not found: ' . $src, 'error');
|
||||
return false;
|
||||
}
|
||||
// Method 2: cURL
|
||||
if ($content === false && function_exists('curl_init')) {
|
||||
$ch = curl_init(self::RELEASE_URL);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_TIMEOUT => 60,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
$content = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
return Folder::copy($src, $dst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy media directory.
|
||||
*/
|
||||
private static function copyMediaFiles(): bool
|
||||
{
|
||||
$src = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
|
||||
$dst = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
|
||||
|
||||
if (is_dir($dst)) {
|
||||
self::log('MokoOnyx media directory already exists — skipping copy.');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_dir($src)) {
|
||||
self::log('Source media directory not found: ' . $src, 'warning');
|
||||
return true; // Non-critical
|
||||
}
|
||||
|
||||
return Folder::copy($src, $dst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename internal references in the copied template.
|
||||
*/
|
||||
private static function renameInternals(): void
|
||||
{
|
||||
$base = JPATH_ROOT . '/templates/' . self::NEW_NAME;
|
||||
$mediaBase = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
|
||||
|
||||
// templateDetails.xml — name, element, update servers, paths
|
||||
$manifest = $base . '/templateDetails.xml';
|
||||
if (is_file($manifest)) {
|
||||
$content = file_get_contents($manifest);
|
||||
$content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content);
|
||||
$content = str_replace(self::OLD_NAME, self::NEW_NAME, $content);
|
||||
// Update the update server URLs to point to MokoOnyx repo
|
||||
$content = str_replace('MokoCassiopeia', 'MokoOnyx', $content);
|
||||
file_put_contents($manifest, $content);
|
||||
self::log('Updated templateDetails.xml for MokoOnyx.');
|
||||
}
|
||||
|
||||
// joomla.asset.json
|
||||
$assetFile = $base . '/joomla.asset.json';
|
||||
if (is_file($assetFile)) {
|
||||
$content = file_get_contents($assetFile);
|
||||
$content = str_replace(self::OLD_NAME, self::NEW_NAME, $content);
|
||||
$content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content);
|
||||
file_put_contents($assetFile, $content);
|
||||
}
|
||||
|
||||
// Language files
|
||||
$langDirs = [
|
||||
$base . '/language/en-GB',
|
||||
$base . '/language/en-US',
|
||||
];
|
||||
foreach ($langDirs as $langDir) {
|
||||
if (!is_dir($langDir)) continue;
|
||||
|
||||
foreach (glob($langDir . '/*mokocassiopeia*') as $file) {
|
||||
$newFile = str_replace(self::OLD_NAME, self::NEW_NAME, $file);
|
||||
if (is_file($file)) {
|
||||
$content = file_get_contents($file);
|
||||
$content = str_replace('MOKOCASSIOPEIA', 'MOKOONYX', $content);
|
||||
$content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content);
|
||||
$content = str_replace(self::OLD_NAME, self::NEW_NAME, $content);
|
||||
file_put_contents($newFile, $content);
|
||||
if ($newFile !== $file) {
|
||||
File::delete($file);
|
||||
}
|
||||
}
|
||||
if ($httpCode !== 200) {
|
||||
$content = false;
|
||||
}
|
||||
}
|
||||
|
||||
// script.php — class name
|
||||
$scriptFile = $base . '/script.php';
|
||||
if (is_file($scriptFile)) {
|
||||
$content = file_get_contents($scriptFile);
|
||||
$content = str_replace('Tpl_MokocassiopeiaInstallerScript', 'Tpl_MokoonyxInstallerScript', $content);
|
||||
$content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content);
|
||||
$content = str_replace(self::OLD_NAME, self::NEW_NAME, $content);
|
||||
// Remove the bridge migration call from the new template's script
|
||||
$content = preg_replace(
|
||||
'/\/\/ Bridge migration.*?MokoBridgeMigration::run\(\);/s',
|
||||
'// Migration complete — this is MokoOnyx',
|
||||
$content
|
||||
);
|
||||
file_put_contents($scriptFile, $content);
|
||||
if ($content === false || strlen($content) < 1000) {
|
||||
self::log('Bridge: failed to download MokoOnyx ZIP from ' . self::RELEASE_URL, 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove bridge helper from the new template (not needed)
|
||||
$bridgeFile = $base . '/helper/bridge.php';
|
||||
if (is_file($bridgeFile)) {
|
||||
File::delete($bridgeFile);
|
||||
if (file_put_contents($zipPath, $content) === false) {
|
||||
self::log('Bridge: failed to write ZIP to ' . $zipPath, 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
self::log('Renamed internal references in MokoOnyx.');
|
||||
self::log('Bridge: downloaded MokoOnyx ZIP (' . strlen($content) . ' bytes)');
|
||||
return $zipPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate database records: template_styles, menu assignments.
|
||||
* Install the downloaded ZIP via Joomla's Installer.
|
||||
*/
|
||||
private static function migrateDatabase(\Joomla\Database\DatabaseInterface $db): void
|
||||
private static function installPackage(string $zipPath): bool
|
||||
{
|
||||
// Get existing MokoCassiopeia styles
|
||||
try {
|
||||
$installer = Installer::getInstance();
|
||||
|
||||
$tmpDir = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp');
|
||||
$extractDir = $tmpDir . '/mokoonyx_install_' . time();
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($zipPath) !== true) {
|
||||
self::log('Bridge: failed to open ZIP', 'error');
|
||||
return false;
|
||||
}
|
||||
$zip->extractTo($extractDir);
|
||||
$zip->close();
|
||||
|
||||
$result = $installer->install($extractDir);
|
||||
|
||||
if (is_dir($extractDir)) {
|
||||
self::removeDirectory($extractDir);
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
self::log('Bridge: MokoOnyx installed via Joomla Installer');
|
||||
} else {
|
||||
self::log('Bridge: Joomla Installer returned false', 'error');
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
} catch (\Throwable $e) {
|
||||
self::log('Bridge: install failed: ' . $e->getMessage(), 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate template styles and menu assignments from MokoCassiopeia to MokoOnyx.
|
||||
*/
|
||||
private static function migrateStyles(): void
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from('#__template_styles')
|
||||
@@ -211,136 +196,78 @@ class MokoBridgeMigration
|
||||
$oldStyles = $db->setQuery($query)->loadObjectList();
|
||||
|
||||
if (empty($oldStyles)) {
|
||||
self::log('No MokoCassiopeia styles found in database.', 'warning');
|
||||
self::log('No MokoCassiopeia styles found — nothing to migrate.');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($oldStyles as $oldStyle) {
|
||||
// Check if MokoOnyx style already exists
|
||||
$newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title);
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__template_styles')
|
||||
->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
|
||||
->where($db->quoteName('title') . ' = ' . $db->quote(
|
||||
str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title)
|
||||
));
|
||||
$exists = (int) $db->setQuery($query)->loadResult();
|
||||
|
||||
if ($exists > 0) {
|
||||
->where($db->quoteName('title') . ' = ' . $db->quote($newTitle));
|
||||
if ((int) $db->setQuery($query)->loadResult() > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create new style with same params
|
||||
$newStyle = clone $oldStyle;
|
||||
unset($newStyle->id);
|
||||
$newStyle->template = self::NEW_NAME;
|
||||
$newStyle->title = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title);
|
||||
$newStyle->title = $newTitle;
|
||||
|
||||
// Update params: replace any mokocassiopeia paths
|
||||
$params = $newStyle->params;
|
||||
if (is_string($params)) {
|
||||
$params = str_replace(self::OLD_NAME, self::NEW_NAME, $params);
|
||||
$newStyle->params = $params;
|
||||
if (is_string($newStyle->params)) {
|
||||
$newStyle->params = str_replace(self::OLD_NAME, self::NEW_NAME, $newStyle->params);
|
||||
}
|
||||
|
||||
$db->insertObject('#__template_styles', $newStyle, 'id');
|
||||
$newId = $newStyle->id;
|
||||
|
||||
// Copy menu assignments
|
||||
$query = $db->getQuery(true)
|
||||
->select('menuid')
|
||||
->from('#__template_styles_menus') // Joomla 5 uses this table
|
||||
->where('template_style_id = ' . (int) $oldStyle->id);
|
||||
|
||||
try {
|
||||
$menuIds = $db->setQuery($query)->loadColumn();
|
||||
foreach ($menuIds as $menuId) {
|
||||
$obj = (object) [
|
||||
'template_style_id' => $newId,
|
||||
'menuid' => $menuId,
|
||||
];
|
||||
$db->insertObject('#__template_styles_menus', $obj);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Table may not exist in all Joomla versions
|
||||
}
|
||||
|
||||
// If this was the default style, make MokoOnyx the default
|
||||
if ($oldStyle->home == 1) {
|
||||
// Set MokoOnyx as default
|
||||
$query = $db->getQuery(true)
|
||||
->update('#__template_styles')
|
||||
->set($db->quoteName('home') . ' = 1')
|
||||
->where('id = ' . (int) $newId);
|
||||
$db->setQuery($query)->execute();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update('#__template_styles')
|
||||
->set($db->quoteName('home') . ' = 1')
|
||||
->where('id = ' . (int) $newId)
|
||||
)->execute();
|
||||
|
||||
// Unset MokoCassiopeia as default
|
||||
$query = $db->getQuery(true)
|
||||
->update('#__template_styles')
|
||||
->set($db->quoteName('home') . ' = 0')
|
||||
->where('id = ' . (int) $oldStyle->id);
|
||||
$db->setQuery($query)->execute();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update('#__template_styles')
|
||||
->set($db->quoteName('home') . ' = 0')
|
||||
->where('id = ' . (int) $oldStyle->id)
|
||||
)->execute();
|
||||
|
||||
self::log('Set MokoOnyx as default site template.');
|
||||
}
|
||||
}
|
||||
|
||||
// Register the new template in the extensions table
|
||||
self::registerExtension($db);
|
||||
|
||||
self::log('Database migration completed. ' . count($oldStyles) . ' style(s) migrated.');
|
||||
self::log('Migrated ' . count($oldStyles) . ' template style(s).');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register MokoOnyx in the extensions table so Joomla recognizes it.
|
||||
*/
|
||||
private static function registerExtension(\Joomla\Database\DatabaseInterface $db): void
|
||||
private static function notifyUser($app): void
|
||||
{
|
||||
// Check if already registered
|
||||
$query = $db->getQuery(true)
|
||||
->select('extension_id')
|
||||
->from('#__extensions')
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
|
||||
$existing = $db->setQuery($query)->loadResult();
|
||||
|
||||
if ($existing) {
|
||||
self::log('MokoOnyx already registered in extensions table.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the old extension record as a base
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from('#__extensions')
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
|
||||
$oldExt = $db->setQuery($query)->loadObject();
|
||||
|
||||
if (!$oldExt) {
|
||||
self::log('MokoCassiopeia extension record not found.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
$newExt = clone $oldExt;
|
||||
unset($newExt->extension_id);
|
||||
$newExt->element = self::NEW_NAME;
|
||||
$newExt->name = self::NEW_NAME;
|
||||
|
||||
// Update manifest_cache with new name
|
||||
$cache = json_decode($newExt->manifest_cache, true);
|
||||
if (is_array($cache)) {
|
||||
$cache['name'] = self::NEW_DISPLAY;
|
||||
$newExt->manifest_cache = json_encode($cache);
|
||||
}
|
||||
|
||||
$db->insertObject('#__extensions', $newExt, 'extension_id');
|
||||
self::log('Registered MokoOnyx in extensions table (ID: ' . $newExt->extension_id . ').');
|
||||
$app->enqueueMessage(
|
||||
'<strong>MokoCassiopeia has been renamed to MokoOnyx.</strong><br>'
|
||||
. 'Your template settings have been migrated automatically. '
|
||||
. 'MokoOnyx is now your active site template. '
|
||||
. 'You can safely uninstall MokoCassiopeia from Extensions → Manage.',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
|
||||
private static function removeDirectory(string $dir): void
|
||||
{
|
||||
$items = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
foreach ($items as $item) {
|
||||
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
|
||||
}
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message.
|
||||
*/
|
||||
private static function log(string $message, string $priority = 'info'): void
|
||||
{
|
||||
$priorities = [
|
||||
|
||||
Reference in New Issue
Block a user