diff --git a/src/script.php b/src/script.php index b379812..0a95e73 100644 --- a/src/script.php +++ b/src/script.php @@ -10,9 +10,10 @@ /** * 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. + * On update: copies the template as MokoOnyx (new directory), updates the + * database to register MokoOnyx, migrates styles + params, and sets it as + * the default site template. The old MokoCassiopeia directory stays intact + * (Joomla's installer still needs it) — the user can uninstall it later. */ defined('_JEXEC') or die; @@ -33,7 +34,7 @@ class Tpl_MokocassiopeiaInstallerScript private const ONYX_UPDATES_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; - // ── Joomla lifecycle methods ─────────────────────────────────────── + // ── Joomla lifecycle ─────────────────────────────────────────────── public function preflight(string $type, InstallerAdapter $parent): bool { @@ -84,100 +85,165 @@ class Tpl_MokocassiopeiaInstallerScript return true; } - // ── Bridge: rename-in-place + DB migration ───────────────────────── + // ── Bridge ───────────────────────────────────────────────────────── 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)) { + // 1. Copy template directory (don't rename — Joomla still needs the old one) + $copied = $this->copyTemplateDir(); + if (!$copied && !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.', + 'MokoOnyx bridge: could not create template directory. ' + . 'Please copy 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' - ); + // 2. Copy media directory + $this->copyMediaDir(); - // 3. Update #__extensions - $this->updateExtensions(); + // 3. Register MokoOnyx in #__extensions (if not already there) + $this->registerExtension(); - // 4. Migrate template styles (create matching MokoOnyx styles with same params) + // 4. Migrate template styles (create 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 + // 5. Redirect update server to MokoOnyx $this->updateUpdateServer(); - // 7. Notify + // 6. 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.', + 'MokoOnyx has been installed as a replacement for MokoCassiopeia.
' + . 'Your template settings have been migrated. MokoOnyx is now your active site template.
' + . 'You can safely uninstall MokoCassiopeia from Extensions → Manage.', 'success' ); $this->log('=== Bridge completed ==='); } - // ── Bridge helpers ───────────────────────────────────────────────── + // ── Copy directories ─────────────────────────────────────────────── - private function renameDir(string $old, string $new, string $label): bool + private function copyTemplateDir(): bool { - if (!is_dir($old)) { - $this->log("Bridge: {$label} dir not found ({$old}) — skipping."); + $src = JPATH_ROOT . '/templates/' . self::OLD_NAME; + $dst = JPATH_ROOT . '/templates/' . self::NEW_NAME; + + if (is_dir($dst)) { + $this->log('Bridge: templates/' . self::NEW_NAME . ' already exists — skipping copy.'); + return true; + } + + if (!is_dir($src)) { + $this->log('Bridge: source template dir not found.', 'error'); 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; + $result = $this->recursiveCopy($src, $dst); + $this->log('Bridge: ' . ($result ? 'copied' : 'FAILED to copy') . ' template dir → ' . self::NEW_NAME); + return $result; } - private function updateExtensions(): void + private function copyMediaDir(): void + { + $src = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME; + $dst = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; + + if (is_dir($dst)) { + $this->log('Bridge: media dir already exists — skipping.'); + return; + } + + if (!is_dir($src)) { + $this->log('Bridge: source media dir not found — skipping.'); + return; + } + + $result = $this->recursiveCopy($src, $dst); + $this->log('Bridge: ' . ($result ? 'copied' : 'FAILED to copy') . ' media dir → ' . self::NEW_NAME); + } + + private function recursiveCopy(string $src, string $dst): bool + { + if (!mkdir($dst, 0755, true) && !is_dir($dst)) { + return false; + } + + $dir = opendir($src); + if ($dir === false) { + return false; + } + + while (($file = readdir($dir)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + + $srcPath = $src . '/' . $file; + $dstPath = $dst . '/' . $file; + + if (is_dir($srcPath)) { + $this->recursiveCopy($srcPath, $dstPath); + } else { + copy($srcPath, $dstPath); + } + } + + closedir($dir); + return true; + } + + // ── Database updates ─────────────────────────────────────────────── + + private function registerExtension(): 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(); + // Check if MokoOnyx is 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')); + $exists = (int) $db->setQuery($query)->loadResult(); - $n = $db->getAffectedRows(); - if ($n > 0) { - $this->log("Bridge: updated {$n} row(s) in #__extensions."); - } + if ($exists) { + $this->log('Bridge: MokoOnyx already registered in #__extensions (id=' . $exists . ').'); + return; + } + + // Copy the MokoCassiopeia extension row and change element/name + $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) { + $this->log('Bridge: MokoCassiopeia not found in #__extensions.', 'warning'); + return; + } + + $newExt = clone $oldExt; + unset($newExt->extension_id); + $newExt->element = self::NEW_NAME; + $newExt->name = self::NEW_NAME; + + // Update manifest_cache to reflect new name + if (is_string($newExt->manifest_cache)) { + $newExt->manifest_cache = str_replace(self::OLD_NAME, self::NEW_NAME, $newExt->manifest_cache); + $newExt->manifest_cache = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $newExt->manifest_cache); + } + + try { + $db->insertObject('#__extensions', $newExt, 'extension_id'); + $this->log('Bridge: registered MokoOnyx in #__extensions (id=' . $newExt->extension_id . ').'); } catch (\Throwable $e) { - $this->log('Bridge: #__extensions failed: ' . $e->getMessage(), 'error'); + $this->log('Bridge: failed to register extension: ' . $e->getMessage(), 'error'); } } @@ -185,54 +251,75 @@ class Tpl_MokocassiopeiaInstallerScript { $db = Factory::getDbo(); - // Get all MokoCassiopeia styles (may already be renamed to mokoonyx by updateExtensions) + // Get all MokoCassiopeia styles $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('template') . ' = ' . $db->quote(self::OLD_NAME)) ->where($db->quoteName('client_id') . ' = 0'); - $styles = $db->setQuery($query)->loadObjectList(); + $oldStyles = $db->setQuery($query)->loadObjectList(); - if (empty($styles)) { - $this->log('Bridge: no styles found to migrate.'); + if (empty($oldStyles)) { + $this->log('Bridge: no MokoCassiopeia styles found.'); 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; + $this->log('Bridge: migrating ' . count($oldStyles) . ' style(s).'); - $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); + foreach ($oldStyles as $old) { + $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $old->title); + $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); + + // Skip if MokoOnyx already has this style + $check = $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($check)->loadResult() > 0) { + $this->log("Bridge: style '{$newTitle}' already exists — skipping."); + continue; + } + + $newParams = is_string($old->params) + ? str_replace(self::OLD_NAME, self::NEW_NAME, $old->params) + : $old->params; + + $new = clone $old; + unset($new->id); + $new->template = self::NEW_NAME; + $new->title = $newTitle; + $new->params = $newParams; + $new->home = 0; try { - $db->setQuery($update)->execute(); + $db->insertObject('#__template_styles', $new, 'id'); + $newId = $new->id; + + // If old was default, make new default + if ($old->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) $old->id) + )->execute(); + + $this->log('Bridge: set MokoOnyx as default site template.'); + } + + $this->log("Bridge: created style '{$newTitle}'."); } catch (\Throwable $e) { - $this->log('Bridge: style update failed (id=' . $style->id . '): ' . $e->getMessage(), 'warning'); + $this->log("Bridge: failed to create style '{$newTitle}': " . $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 @@ -255,12 +342,13 @@ class Tpl_MokocassiopeiaInstallerScript $this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning'); } - // Clear cached updates for old element + // Clear cached updates try { - $query = $db->getQuery(true) - ->delete('#__updates') - ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)); - $db->setQuery($query)->execute(); + $db->setQuery( + $db->getQuery(true) + ->delete('#__updates') + ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) + )->execute(); } catch (\Throwable $e) { // Not critical }