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
}