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 = [