Rewrote bridge from scratch as part of script.php postflight(): 1. Rename templates/mokocassiopeia → mokoonyx 2. Rename media/templates/site/mokocassiopeia → mokoonyx 3. Update #__extensions element + name 4. Update all #__template_styles (template, title, params) 5. Redirect #__update_sites to MokoOnyx updates.xml 6. Clear #__updates cache No HTTP requests, no ZIP downloads, no separate bridge.php file. Reverted updates.xml download URLs back to MokoCassiopeia releases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
287 lines
10 KiB
PHP
287 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Installer\InstallerAdapter;
|
|
use Joomla\CMS\Log\Log;
|
|
|
|
class Tpl_MokocassiopeiaInstallerScript
|
|
{
|
|
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';
|
|
|
|
private const ONYX_UPDATES_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
|
|
|
|
// ── Joomla lifecycle methods ───────────────────────────────────────
|
|
|
|
public function preflight(string $type, InstallerAdapter $parent): bool
|
|
{
|
|
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
|
|
Factory::getApplication()->enqueueMessage(
|
|
sprintf('MokoCassiopeia requires PHP %s+. Running %s.', self::MIN_PHP, PHP_VERSION),
|
|
'error'
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
|
|
Factory::getApplication()->enqueueMessage(
|
|
sprintf('MokoCassiopeia requires Joomla %s+. Running %s.', self::MIN_JOOMLA, JVERSION),
|
|
'error'
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function install(InstallerAdapter $parent): bool
|
|
{
|
|
$this->log('MokoCassiopeia installed.');
|
|
return true;
|
|
}
|
|
|
|
public function update(InstallerAdapter $parent): bool
|
|
{
|
|
$this->log('MokoCassiopeia update() — version ' . ($parent->getManifest()->version ?? '?'));
|
|
return true;
|
|
}
|
|
|
|
public function uninstall(InstallerAdapter $parent): bool
|
|
{
|
|
$this->log('MokoCassiopeia uninstalled.');
|
|
return true;
|
|
}
|
|
|
|
public function postflight(string $type, InstallerAdapter $parent): bool
|
|
{
|
|
if ($type === 'update') {
|
|
$this->log('=== MokoCassiopeia → MokoOnyx bridge ===');
|
|
$this->bridge();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ── Bridge: rename-in-place + DB migration ─────────────────────────
|
|
|
|
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)) {
|
|
$app->enqueueMessage(
|
|
'Could not rename template directory to MokoOnyx. '
|
|
. 'Please rename <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'
|
|
);
|
|
|
|
// 3. Update #__extensions
|
|
$this->updateExtensions();
|
|
|
|
// 4. Migrate template styles (create matching 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
|
|
$this->updateUpdateServer();
|
|
|
|
// 7. 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.',
|
|
'success'
|
|
);
|
|
|
|
$this->log('=== Bridge completed ===');
|
|
}
|
|
|
|
// ── Bridge helpers ─────────────────────────────────────────────────
|
|
|
|
private function renameDir(string $old, string $new, string $label): bool
|
|
{
|
|
if (!is_dir($old)) {
|
|
$this->log("Bridge: {$label} dir not found ({$old}) — skipping.");
|
|
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;
|
|
}
|
|
|
|
private function updateExtensions(): 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();
|
|
|
|
$n = $db->getAffectedRows();
|
|
if ($n > 0) {
|
|
$this->log("Bridge: updated {$n} row(s) in #__extensions.");
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->log('Bridge: #__extensions failed: ' . $e->getMessage(), 'error');
|
|
}
|
|
}
|
|
|
|
private function migrateStyles(): void
|
|
{
|
|
$db = Factory::getDbo();
|
|
|
|
// Get all MokoCassiopeia styles (may already be renamed to mokoonyx by updateExtensions)
|
|
$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('client_id') . ' = 0');
|
|
$styles = $db->setQuery($query)->loadObjectList();
|
|
|
|
if (empty($styles)) {
|
|
$this->log('Bridge: no styles found to migrate.');
|
|
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;
|
|
|
|
$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);
|
|
|
|
try {
|
|
$db->setQuery($update)->execute();
|
|
} catch (\Throwable $e) {
|
|
$this->log('Bridge: style update failed (id=' . $style->id . '): ' . $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
|
|
{
|
|
$db = Factory::getDbo();
|
|
|
|
try {
|
|
$query = $db->getQuery(true)
|
|
->update('#__update_sites')
|
|
->set($db->quoteName('location') . ' = ' . $db->quote(self::ONYX_UPDATES_URL))
|
|
->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY))
|
|
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
|
|
$db->setQuery($query)->execute();
|
|
|
|
$n = $db->getAffectedRows();
|
|
if ($n > 0) {
|
|
$this->log("Bridge: redirected {$n} update site(s) to MokoOnyx.");
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning');
|
|
}
|
|
|
|
// Clear cached updates for old element
|
|
try {
|
|
$query = $db->getQuery(true)
|
|
->delete('#__updates')
|
|
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME));
|
|
$db->setQuery($query)->execute();
|
|
} catch (\Throwable $e) {
|
|
// Not critical
|
|
}
|
|
}
|
|
|
|
// ── Logging ────────────────────────────────────────────────────────
|
|
|
|
private function log(string $message, string $priority = 'info'): void
|
|
{
|
|
static $init = false;
|
|
if (!$init) {
|
|
Log::addLogger(
|
|
['text_file' => 'mokocassiopeia_bridge.log.php'],
|
|
Log::ALL,
|
|
['mokocassiopeia_bridge']
|
|
);
|
|
$init = true;
|
|
}
|
|
|
|
$levels = ['info' => Log::INFO, 'warning' => Log::WARNING, 'error' => Log::ERROR];
|
|
Log::add($message, $levels[$priority] ?? Log::INFO, 'mokocassiopeia_bridge');
|
|
}
|
|
}
|