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.
|
* MokoCassiopeia install/update/uninstall script.
|
||||||
*
|
*
|
||||||
* On update: renames the template to MokoOnyx (dirs + database),
|
* On update: copies the template as MokoOnyx (new directory), updates the
|
||||||
* copies all style params, creates matching styles, copies user files,
|
* database to register MokoOnyx, migrates styles + params, and sets it as
|
||||||
* and redirects the update server. No external downloads needed.
|
* 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;
|
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';
|
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
|
public function preflight(string $type, InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
@@ -84,100 +85,165 @@ class Tpl_MokocassiopeiaInstallerScript
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bridge: rename-in-place + DB migration ─────────────────────────
|
// ── Bridge ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private function bridge(): void
|
private function bridge(): void
|
||||||
{
|
{
|
||||||
$app = Factory::getApplication();
|
$app = Factory::getApplication();
|
||||||
|
|
||||||
// 1. Rename template directory
|
// 1. Copy template directory (don't rename — Joomla still needs the old one)
|
||||||
$templateRenamed = $this->renameDir(
|
$copied = $this->copyTemplateDir();
|
||||||
JPATH_ROOT . '/templates/' . self::OLD_NAME,
|
if (!$copied && !is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) {
|
||||||
JPATH_ROOT . '/templates/' . self::NEW_NAME,
|
|
||||||
'template'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$templateRenamed && !is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) {
|
|
||||||
$app->enqueueMessage(
|
$app->enqueueMessage(
|
||||||
'Could not rename template directory to MokoOnyx. '
|
'MokoOnyx bridge: could not create template directory. '
|
||||||
. 'Please rename <code>templates/mokocassiopeia</code> to <code>templates/mokoonyx</code> manually.',
|
. 'Please copy <code>templates/mokocassiopeia</code> to <code>templates/mokoonyx</code> manually.',
|
||||||
'warning'
|
'warning'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Rename media directory
|
// 2. Copy media directory
|
||||||
$this->renameDir(
|
$this->copyMediaDir();
|
||||||
JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME,
|
|
||||||
JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME,
|
|
||||||
'media'
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Update #__extensions
|
// 3. Register MokoOnyx in #__extensions (if not already there)
|
||||||
$this->updateExtensions();
|
$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();
|
$this->migrateStyles();
|
||||||
|
|
||||||
// 5. Copy user files (custom themes, user.css, user.js)
|
// 5. Redirect update server to MokoOnyx
|
||||||
$this->copyUserFiles();
|
|
||||||
|
|
||||||
// 6. Redirect update server to MokoOnyx
|
|
||||||
$this->updateUpdateServer();
|
$this->updateUpdateServer();
|
||||||
|
|
||||||
// 7. Notify
|
// 6. Notify
|
||||||
$app->enqueueMessage(
|
$app->enqueueMessage(
|
||||||
'<strong>MokoCassiopeia has been renamed to MokoOnyx.</strong><br>'
|
'<strong>MokoOnyx has been installed as a replacement for MokoCassiopeia.</strong><br>'
|
||||||
. 'All template settings, styles, and custom files have been migrated. '
|
. 'Your template settings have been migrated. MokoOnyx is now your active site template.<br>'
|
||||||
. 'MokoOnyx is now your active site template.',
|
. 'You can safely uninstall MokoCassiopeia from Extensions → Manage.',
|
||||||
'success'
|
'success'
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->log('=== Bridge completed ===');
|
$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)) {
|
$src = JPATH_ROOT . '/templates/' . self::OLD_NAME;
|
||||||
$this->log("Bridge: {$label} dir not found ({$old}) — skipping.");
|
$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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_dir($new)) {
|
$result = $this->recursiveCopy($src, $dst);
|
||||||
$this->log("Bridge: {$label} dir already exists ({$new}) — skipping rename.");
|
$this->log('Bridge: ' . ($result ? 'copied' : 'FAILED to copy') . ' template dir → ' . self::NEW_NAME);
|
||||||
return true;
|
return $result;
|
||||||
}
|
|
||||||
|
|
||||||
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
|
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();
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
try {
|
// Check if MokoOnyx is already registered
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->update('#__extensions')
|
->select('extension_id')
|
||||||
->set($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
|
->from('#__extensions')
|
||||||
->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_NAME))
|
->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
|
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
|
$exists = (int) $db->setQuery($query)->loadResult();
|
||||||
$db->setQuery($query)->execute();
|
|
||||||
|
|
||||||
$n = $db->getAffectedRows();
|
if ($exists) {
|
||||||
if ($n > 0) {
|
$this->log('Bridge: MokoOnyx already registered in #__extensions (id=' . $exists . ').');
|
||||||
$this->log("Bridge: updated {$n} row(s) in #__extensions.");
|
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) {
|
} 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();
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
// Get all MokoCassiopeia styles (may already be renamed to mokoonyx by updateExtensions)
|
// Get all MokoCassiopeia styles
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select('*')
|
->select('*')
|
||||||
->from('#__template_styles')
|
->from('#__template_styles')
|
||||||
->where('(' . $db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME)
|
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
|
||||||
. ' OR ' . $db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME) . ')')
|
|
||||||
->where($db->quoteName('client_id') . ' = 0');
|
->where($db->quoteName('client_id') . ' = 0');
|
||||||
$styles = $db->setQuery($query)->loadObjectList();
|
$oldStyles = $db->setQuery($query)->loadObjectList();
|
||||||
|
|
||||||
if (empty($styles)) {
|
if (empty($oldStyles)) {
|
||||||
$this->log('Bridge: no styles found to migrate.');
|
$this->log('Bridge: no MokoCassiopeia styles found.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($styles as $style) {
|
$this->log('Bridge: migrating ' . count($oldStyles) . ' style(s).');
|
||||||
$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)
|
foreach ($oldStyles as $old) {
|
||||||
->update('#__template_styles')
|
$newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $old->title);
|
||||||
->set($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
|
$newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle);
|
||||||
->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
|
|
||||||
->set($db->quoteName('params') . ' = ' . $db->quote($newParams))
|
// Skip if MokoOnyx already has this style
|
||||||
->where('id = ' . (int) $style->id);
|
$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 {
|
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) {
|
} 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
|
private function updateUpdateServer(): void
|
||||||
@@ -255,12 +342,13 @@ class Tpl_MokocassiopeiaInstallerScript
|
|||||||
$this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning');
|
$this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cached updates for old element
|
// Clear cached updates
|
||||||
try {
|
try {
|
||||||
$query = $db->getQuery(true)
|
$db->setQuery(
|
||||||
->delete('#__updates')
|
$db->getQuery(true)
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME));
|
->delete('#__updates')
|
||||||
$db->setQuery($query)->execute();
|
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
|
||||||
|
)->execute();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Not critical
|
// Not critical
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user