* * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later */ /** * Template install/update/uninstall script. * Joomla calls the methods in this class automatically during template * install, update, and uninstall via the element in * templateDetails.xml. * * On first install, detects MokoCassiopeia and migrates template styles, * parameters, menu assignments, and user files automatically. */ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Installer\InstallerScriptInterface; use Joomla\CMS\Log\Log; class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface { 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'; public function preflight(string $type, InstallerAdapter $parent): bool { if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) { Factory::getApplication()->enqueueMessage( sprintf('MokoOnyx requires PHP %s or later. You are running PHP %s.', self::MIN_PHP, PHP_VERSION), 'error' ); return false; } if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) { Factory::getApplication()->enqueueMessage( sprintf('MokoOnyx requires Joomla %s or later. You are running Joomla %s.', self::MIN_JOOMLA, JVERSION), 'error' ); return false; } return true; } public function install(InstallerAdapter $parent): bool { $this->logMessage('MokoOnyx template installed.'); return true; } public function update(InstallerAdapter $parent): bool { $this->logMessage('MokoOnyx template updated.'); $synced = $this->syncCustomVariables($parent); if ($synced > 0) { Factory::getApplication()->enqueueMessage( sprintf( 'MokoOnyx: %d new CSS variable(s) were added to your custom palette files. ' . 'Review them in your light.custom.css and/or dark.custom.css to customise the new defaults.', $synced ), 'notice' ); } return true; } public function uninstall(InstallerAdapter $parent): bool { $this->logMessage('MokoOnyx template uninstalled.'); return true; } public function postflight(string $type, InstallerAdapter $parent): bool { // On install or update: migrate from MokoCassiopeia if it exists if ($type === 'install' || $type === 'update') { $this->migrateFromCassiopeia(); } return true; } /** * Detect MokoCassiopeia and create matching MokoOnyx styles with the same params. * Creates a MokoOnyx style copy for each MokoCassiopeia style. */ private function migrateFromCassiopeia(): void { $db = Factory::getDbo(); // Get all MokoCassiopeia styles $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)) { $this->logMessage('No MokoCassiopeia styles found — fresh install.'); return; } $this->logMessage('MokoCassiopeia detected — creating ' . count($oldStyles) . ' matching MokoOnyx style(s).'); // Get the installer-created default MokoOnyx style (to apply params to it) $query = $db->getQuery(true) ->select('id') ->from('#__template_styles') ->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) ->where($db->quoteName('client_id') . ' = 0') ->order($db->quoteName('id') . ' ASC'); $defaultOnyxId = (int) $db->setQuery($query, 0, 1)->loadResult(); $firstStyle = true; foreach ($oldStyles as $oldStyle) { $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title); $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); $params = is_string($oldStyle->params) ? str_replace(self::OLD_NAME, self::NEW_NAME, $oldStyle->params) : $oldStyle->params; if ($firstStyle && $defaultOnyxId) { // Update the installer-created default style with the first MokoCassiopeia style's params $update = $db->getQuery(true) ->update('#__template_styles') ->set($db->quoteName('params') . ' = ' . $db->quote($params)) ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle)) ->where('id = ' . $defaultOnyxId); $db->setQuery($update)->execute(); // Set as default if MokoCassiopeia was default if ($oldStyle->home == 1) { $db->setQuery( $db->getQuery(true) ->update('#__template_styles') ->set($db->quoteName('home') . ' = 1') ->where('id = ' . $defaultOnyxId) )->execute(); $db->setQuery( $db->getQuery(true) ->update('#__template_styles') ->set($db->quoteName('home') . ' = 0') ->where('id = ' . (int) $oldStyle->id) )->execute(); $this->logMessage('Set MokoOnyx as default site template.'); } $this->logMessage("Updated default MokoOnyx style with params: {$newTitle}"); $firstStyle = false; continue; } // For additional styles: create new MokoOnyx style copies $newStyle = clone $oldStyle; unset($newStyle->id); $newStyle->template = self::NEW_NAME; $newStyle->title = $newTitle; $newStyle->home = 0; $newStyle->params = $params; try { $db->insertObject('#__template_styles', $newStyle, 'id'); $this->logMessage("Created MokoOnyx style: {$newTitle}"); } catch (\Throwable $e) { $this->logMessage("Failed to create style {$newTitle}: " . $e->getMessage(), 'warning'); } } // 2. Copy user files (custom themes, user.css, user.js) $this->copyUserFiles(); // 3. Notify admin Factory::getApplication()->enqueueMessage( 'MokoOnyx has been installed as a replacement for MokoCassiopeia.
' . 'Your template settings and custom files have been migrated automatically. ' . 'MokoOnyx is now your active site template. ' . 'You can safely uninstall MokoCassiopeia from Extensions → Manage.', 'success' ); } /** * Copy user-specific files from MokoCassiopeia media to MokoOnyx media. */ private function copyUserFiles(): void { $oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME; $newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; if (!is_dir($oldMedia) || !is_dir($newMedia)) { return; } $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', ]; $copied = 0; foreach ($userFiles as $relPath) { $src = $oldMedia . '/' . $relPath; $dst = $newMedia . '/' . $relPath; if (is_file($src) && !is_file($dst)) { $dstDir = dirname($dst); if (!is_dir($dstDir)) { mkdir($dstDir, 0755, true); } copy($src, $dst); $copied++; } } if ($copied > 0) { $this->logMessage("Copied {$copied} user file(s) from MokoCassiopeia."); } } private function syncCustomVariables(InstallerAdapter $parent): int { $templateDir = $parent->getParent()->getPath('source'); $syncScript = $templateDir . '/sync_custom_vars.php'; if (!is_file($syncScript)) { $this->logMessage('CSS variable sync script not found at: ' . $syncScript, 'warning'); return 0; } require_once $syncScript; if (!class_exists('MokoCssVarSync')) { $this->logMessage('MokoCssVarSync class not found after loading script.', 'warning'); return 0; } try { $results = MokoCssVarSync::run(JPATH_ROOT); $totalAdded = 0; foreach ($results as $filePath => $result) { $totalAdded += count($result['added']); if (!empty($result['added'])) { $this->logMessage(sprintf('CSS sync: added %d variable(s) to %s', count($result['added']), basename($filePath))); } } return $totalAdded; } catch (\Throwable $e) { $this->logMessage('CSS variable sync failed: ' . $e->getMessage(), 'error'); return 0; } } private function logMessage(string $message, string $priority = 'info'): void { $priorities = [ 'info' => Log::INFO, 'warning' => Log::WARNING, 'error' => Log::ERROR, ]; Log::addLogger( ['text_file' => 'mokoonyx.log.php'], Log::ALL, ['mokoonyx'] ); Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokoonyx'); } }