* * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later */ /** * MokoCassiopeia install/update/uninstall script. * * On update: renames the template to MokoOnyx (dirs + database), * copies all style params, creates matching styles, copies user files, * and redirects the update server. No external downloads needed. */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Log\Log; class Tpl_MokocassiopeiaInstallerScript { private const MIN_PHP = '8.1.0'; private const MIN_JOOMLA = '4.4.0'; private const OLD_NAME = 'mokocassiopeia'; private const NEW_NAME = 'mokoonyx'; private const OLD_DISPLAY = 'MokoCassiopeia'; private const NEW_DISPLAY = 'MokoOnyx'; private const ONYX_UPDATES_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; // ── Joomla lifecycle methods ─────────────────────────────────────── public function preflight(string $type, InstallerAdapter $parent): bool { if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) { Factory::getApplication()->enqueueMessage( sprintf('MokoCassiopeia requires PHP %s+. Running %s.', self::MIN_PHP, PHP_VERSION), 'error' ); return false; } if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) { Factory::getApplication()->enqueueMessage( sprintf('MokoCassiopeia requires Joomla %s+. Running %s.', self::MIN_JOOMLA, JVERSION), 'error' ); return false; } return true; } public function install(InstallerAdapter $parent): bool { $this->log('MokoCassiopeia installed.'); return true; } public function update(InstallerAdapter $parent): bool { $this->log('MokoCassiopeia update() — version ' . ($parent->getManifest()->version ?? '?')); return true; } public function uninstall(InstallerAdapter $parent): bool { $this->log('MokoCassiopeia uninstalled.'); return true; } public function postflight(string $type, InstallerAdapter $parent): bool { if ($type === 'update') { $this->log('=== MokoCassiopeia → MokoOnyx bridge ==='); $this->bridge(); } return true; } // ── Bridge: rename-in-place + DB migration ───────────────────────── private function bridge(): void { $app = Factory::getApplication(); // 1. Rename template directory $templateRenamed = $this->renameDir( JPATH_ROOT . '/templates/' . self::OLD_NAME, JPATH_ROOT . '/templates/' . self::NEW_NAME, 'template' ); if (!$templateRenamed && !is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) { $app->enqueueMessage( 'Could not rename template directory to MokoOnyx. ' . 'Please rename templates/mokocassiopeia to templates/mokoonyx manually.', 'warning' ); return; } // 2. Rename media directory $this->renameDir( JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME, JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME, 'media' ); // 3. Update #__extensions $this->updateExtensions(); // 4. Migrate template styles (create matching MokoOnyx styles with same params) $this->migrateStyles(); // 5. Copy user files (custom themes, user.css, user.js) $this->copyUserFiles(); // 6. Redirect update server to MokoOnyx $this->updateUpdateServer(); // 7. Notify $app->enqueueMessage( 'MokoCassiopeia has been renamed to MokoOnyx.
' . 'All template settings, styles, and custom files have been migrated. ' . 'MokoOnyx is now your active site template.', 'success' ); $this->log('=== Bridge completed ==='); } // ── Bridge helpers ───────────────────────────────────────────────── private function renameDir(string $old, string $new, string $label): bool { if (!is_dir($old)) { $this->log("Bridge: {$label} dir not found ({$old}) — skipping."); return false; } if (is_dir($new)) { $this->log("Bridge: {$label} dir already exists ({$new}) — skipping rename."); return true; } if (@rename($old, $new)) { $this->log("Bridge: renamed {$label} dir → " . self::NEW_NAME); return true; } $this->log("Bridge: failed to rename {$label} dir.", 'error'); return false; } private function updateExtensions(): void { $db = Factory::getDbo(); try { $query = $db->getQuery(true) ->update('#__extensions') ->set($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME)) ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_NAME)) ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) ->where($db->quoteName('type') . ' = ' . $db->quote('template')); $db->setQuery($query)->execute(); $n = $db->getAffectedRows(); if ($n > 0) { $this->log("Bridge: updated {$n} row(s) in #__extensions."); } } catch (\Throwable $e) { $this->log('Bridge: #__extensions failed: ' . $e->getMessage(), 'error'); } } private function migrateStyles(): void { $db = Factory::getDbo(); // Get all MokoCassiopeia styles (may already be renamed to mokoonyx by updateExtensions) $query = $db->getQuery(true) ->select('*') ->from('#__template_styles') ->where('(' . $db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME) . ' OR ' . $db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME) . ')') ->where($db->quoteName('client_id') . ' = 0'); $styles = $db->setQuery($query)->loadObjectList(); if (empty($styles)) { $this->log('Bridge: no styles found to migrate.'); return; } foreach ($styles as $style) { $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $style->title); $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); $newParams = is_string($style->params) ? str_replace(self::OLD_NAME, self::NEW_NAME, $style->params) : $style->params; $update = $db->getQuery(true) ->update('#__template_styles') ->set($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle)) ->set($db->quoteName('params') . ' = ' . $db->quote($newParams)) ->where('id = ' . (int) $style->id); try { $db->setQuery($update)->execute(); } catch (\Throwable $e) { $this->log('Bridge: style update failed (id=' . $style->id . '): ' . $e->getMessage(), 'warning'); } } $this->log('Bridge: migrated ' . count($styles) . ' style(s).'); } private function copyUserFiles(): void { $media = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; if (!is_dir($media)) { return; } // User files are already in the renamed media dir — nothing to copy. // They were moved with the rename. Log for clarity. $this->log('Bridge: user files preserved via directory rename.'); } private function updateUpdateServer(): void { $db = Factory::getDbo(); try { $query = $db->getQuery(true) ->update('#__update_sites') ->set($db->quoteName('location') . ' = ' . $db->quote(self::ONYX_UPDATES_URL)) ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY)) ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%')); $db->setQuery($query)->execute(); $n = $db->getAffectedRows(); if ($n > 0) { $this->log("Bridge: redirected {$n} update site(s) to MokoOnyx."); } } catch (\Throwable $e) { $this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning'); } // Clear cached updates for old element try { $query = $db->getQuery(true) ->delete('#__updates') ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)); $db->setQuery($query)->execute(); } catch (\Throwable $e) { // Not critical } } // ── Logging ──────────────────────────────────────────────────────── private function log(string $message, string $priority = 'info'): void { static $init = false; if (!$init) { Log::addLogger( ['text_file' => 'mokocassiopeia_bridge.log.php'], Log::ALL, ['mokocassiopeia_bridge'] ); $init = true; } $levels = ['info' => Log::INFO, 'warning' => Log::WARNING, 'error' => Log::ERROR]; Log::add($message, $levels[$priority] ?? Log::INFO, 'mokocassiopeia_bridge'); } }