diff --git a/src/helper/bridge.php b/src/helper/bridge.php
index 71d2546..998e036 100644
--- a/src/helper/bridge.php
+++ b/src/helper/bridge.php
@@ -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 '
+ . 'Gitea Releases.',
+ '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 '
+ . 'Gitea Releases.',
'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(
- 'MokoCassiopeia has been renamed to MokoOnyx.
'
- . '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(
+ 'MokoCassiopeia has been renamed to MokoOnyx.
'
+ . '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 = [