Bridge: copy instead of rename, register as new extension
rename() fails because Joomla's installer locks the directory during postflight. New approach: - Copy templates/mokocassiopeia → templates/mokoonyx (recursive) - Copy media dir the same way - Register MokoOnyx as a new extension in #__extensions - Create matching MokoOnyx styles with copied params - Set MokoOnyx as default, redirect update server - Old mokocassiopeia dir stays (user uninstalls later) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
290
src/script.php
290
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 <code>templates/mokocassiopeia</code> to <code>templates/mokoonyx</code> manually.',
|
||||
'MokoOnyx bridge: could not create template directory. '
|
||||
. 'Please copy <code>templates/mokocassiopeia</code> to <code>templates/mokoonyx</code> 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(
|
||||
'<strong>MokoCassiopeia has been renamed to MokoOnyx.</strong><br>'
|
||||
. 'All template settings, styles, and custom files have been migrated. '
|
||||
. 'MokoOnyx is now your active site template.',
|
||||
'<strong>MokoOnyx has been installed as a replacement for MokoCassiopeia.</strong><br>'
|
||||
. 'Your template settings have been migrated. MokoOnyx is now your active site template.<br>'
|
||||
. '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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user