Some checks failed
Repo Health / Access control (push) Successful in 2s
Auto-Update SHA Hash / Update SHA-256 Hash in updates.xml (release) Failing after 6s
Repo Health / Release configuration (push) Failing after 4s
Repo Health / Scripts governance (push) Successful in 5s
Repo Health / Repository health (push) Failing after 5s
1. Download & install MokoOnyx from Gitea release 2. Copy user files (custom themes, user.css/js) to MokoOnyx 3. Migrate template styles with params Fallback: if download fails, copy user files only (MokoOnyx must be installed manually) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
339 lines
11 KiB
PHP
339 lines
11 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
|
|
*
|
|
* 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\Installer\Installer;
|
|
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';
|
|
|
|
/** 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.
|
|
*/
|
|
public static function run(): bool
|
|
{
|
|
$app = Factory::getApplication();
|
|
|
|
// 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. Try downloading and installing MokoOnyx from Gitea release
|
|
$installed = false;
|
|
$zipPath = self::downloadRelease();
|
|
if ($zipPath) {
|
|
$installed = self::installPackage($zipPath);
|
|
@unlink($zipPath);
|
|
}
|
|
|
|
// 2. Fallback: copy from MokoCassiopeia and rename
|
|
if (!$installed) {
|
|
self::log('Bridge: download/install failed, falling back to file copy');
|
|
$installed = self::copyAndRename();
|
|
}
|
|
|
|
if (!$installed) {
|
|
$app->enqueueMessage(
|
|
'MokoOnyx migration: automatic 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. Copy user files (custom themes, user.css, user.js)
|
|
self::copyAndRename();
|
|
|
|
// 4. Migrate template styles and params
|
|
self::migrateStyles();
|
|
|
|
// 5. Notify admin
|
|
self::notifyUser($app);
|
|
|
|
self::log('Bridge migration completed successfully.');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Download the MokoOnyx ZIP to Joomla's tmp directory.
|
|
*/
|
|
private static function downloadRelease(): ?string
|
|
{
|
|
$tmpDir = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp');
|
|
$zipPath = $tmpDir . '/mokoonyx-install.zip';
|
|
|
|
$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);
|
|
}
|
|
|
|
// 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);
|
|
|
|
if ($httpCode !== 200) {
|
|
$content = false;
|
|
}
|
|
}
|
|
|
|
if ($content === false || strlen($content) < 1000) {
|
|
self::log('Bridge: failed to download MokoOnyx ZIP from ' . self::RELEASE_URL, 'error');
|
|
return null;
|
|
}
|
|
|
|
if (file_put_contents($zipPath, $content) === false) {
|
|
self::log('Bridge: failed to write ZIP to ' . $zipPath, 'error');
|
|
return null;
|
|
}
|
|
|
|
self::log('Bridge: downloaded MokoOnyx ZIP (' . strlen($content) . ' bytes)');
|
|
return $zipPath;
|
|
}
|
|
|
|
/**
|
|
* Install the downloaded ZIP via Joomla's Installer.
|
|
*/
|
|
private static function installPackage(string $zipPath): bool
|
|
{
|
|
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')
|
|
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
|
|
->where($db->quoteName('client_id') . ' = 0');
|
|
$oldStyles = $db->setQuery($query)->loadObjectList();
|
|
|
|
if (empty($oldStyles)) {
|
|
self::log('No MokoCassiopeia styles found — nothing to migrate.');
|
|
return;
|
|
}
|
|
|
|
foreach ($oldStyles as $oldStyle) {
|
|
$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($newTitle));
|
|
if ((int) $db->setQuery($query)->loadResult() > 0) {
|
|
continue;
|
|
}
|
|
|
|
$newStyle = clone $oldStyle;
|
|
unset($newStyle->id);
|
|
$newStyle->template = self::NEW_NAME;
|
|
$newStyle->title = $newTitle;
|
|
|
|
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;
|
|
|
|
if ($oldStyle->home == 1) {
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->update('#__template_styles')
|
|
->set($db->quoteName('home') . ' = 1')
|
|
->where('id = ' . (int) $newId)
|
|
)->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.');
|
|
}
|
|
}
|
|
|
|
self::log('Migrated ' . count($oldStyles) . ' template style(s).');
|
|
}
|
|
|
|
/**
|
|
* Copy user-specific files from MokoCassiopeia to MokoOnyx.
|
|
* Only copies custom themes, user.css, and user.js — not the full template.
|
|
* MokoOnyx must already be installed (via download or manual).
|
|
*/
|
|
private static function copyAndRename(): bool
|
|
{
|
|
$oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
|
|
$newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
|
|
|
|
if (!is_dir($newMedia)) {
|
|
self::log('Bridge: MokoOnyx media dir not found — cannot copy user files', 'warning');
|
|
return false;
|
|
}
|
|
|
|
$copied = 0;
|
|
|
|
// Copy custom theme palettes
|
|
$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',
|
|
];
|
|
|
|
foreach ($userFiles as $relPath) {
|
|
$srcFile = $oldMedia . '/' . $relPath;
|
|
$dstFile = $newMedia . '/' . $relPath;
|
|
if (is_file($srcFile) && !is_file($dstFile)) {
|
|
$dstDir = dirname($dstFile);
|
|
if (!is_dir($dstDir)) {
|
|
mkdir($dstDir, 0755, true);
|
|
}
|
|
copy($srcFile, $dstFile);
|
|
$copied++;
|
|
}
|
|
}
|
|
|
|
// Copy favicon directory
|
|
$faviconSrc = JPATH_ROOT . '/images/favicons';
|
|
if (is_dir($faviconSrc)) {
|
|
self::log('Bridge: favicons already at images/favicons — shared between templates');
|
|
}
|
|
|
|
self::log("Bridge: copied {$copied} user file(s) to MokoOnyx");
|
|
return true;
|
|
}
|
|
|
|
private static function notifyUser($app): void
|
|
{
|
|
$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);
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|