From e4b38df9756423f092175da1b186ac26924a718d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 18 Apr 2026 11:33:16 -0500 Subject: [PATCH] Fix SHA-256 checksum: remove sha256: prefix (Joomla expects raw hex) Joomla's update system compares hash_file() output (raw hex) against the element value. The sha256: prefix caused mismatch. Also adds bridge migration helper for future MokoOnyx rename. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/auto-release.yml | 4 +- .github/workflows/release.yml | 2 +- src/helper/bridge.php | 360 +++++++++++++++++++++++++++++ updates.xml | 2 +- 4 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 src/helper/bridge.php diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 131ecab..4eb0ee2 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -517,9 +517,9 @@ jobs: # Replace downloads block with both formats + SHA sed -i "s|.*|\n ${ZIP_URL}\n ${TAR_URL}\n |" updates.xml 2>/dev/null || true if grep -q '' updates.xml; then - sed -i "s|.*|sha256:${SHA256_ZIP}|" updates.xml + sed -i "s|.*|${SHA256_ZIP}|" updates.xml else - sed -i "s||\n sha256:${SHA256_ZIP}|" updates.xml + sed -i "s||\n ${SHA256_ZIP}|" updates.xml fi git add updates.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd051f1..7c0dc93 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -333,7 +333,7 @@ jobs: block = re.sub(r"[^<]*", f"{date}", block) # Update SHA-256 - block = re.sub(r"[^<]*", f"sha256:{sha256}", block) + block = re.sub(r"[^<]*", f"{sha256}", block) # Update Gitea download URL gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" diff --git a/src/helper/bridge.php b/src/helper/bridge.php new file mode 100644 index 0000000..71d2546 --- /dev/null +++ b/src/helper/bridge.php @@ -0,0 +1,360 @@ + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * Bridge migration helper — MokoCassiopeia → MokoOnyx + * + * Called from script.php during the v03.10.00 update. Copies the template + * to the new directory name, migrates database records, and sets MokoOnyx + * as the active site template. + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Filesystem\File; +use Joomla\CMS\Filesystem\Folder; +use Joomla\CMS\Log\Log; + +class MokoBridgeMigration +{ + private const OLD_NAME = 'mokocassiopeia'; + private const NEW_NAME = 'mokoonyx'; + + private const OLD_DISPLAY = 'MokoCassiopeia'; + private const NEW_DISPLAY = 'MokoOnyx'; + + /** + * Run the full migration. + * + * @return bool True on success, false on failure. + */ + public static function run(): bool + { + $app = Factory::getApplication(); + $db = Factory::getDbo(); + + // 1. Copy template files + if (!self::copyTemplateFiles()) { + $app->enqueueMessage( + 'MokoOnyx migration: failed to copy template files. ' + . 'You can manually copy templates/mokocassiopeia to templates/mokoonyx.', + 'error' + ); + return false; + } + + // 2. Copy media files + if (!self::copyMediaFiles()) { + $app->enqueueMessage( + 'MokoOnyx migration: failed to copy media files. ' + . 'You can manually copy media/templates/site/mokocassiopeia to media/templates/site/mokoonyx.', + 'warning' + ); + } + + // 3. Rename internals in the new copy (templateDetails.xml, language files, etc.) + self::renameInternals(); + + // 4. Register the new template in the database + self::migrateDatabase($db); + + // 5. Notify the admin + $app->enqueueMessage( + 'MokoCassiopeia has been renamed to MokoOnyx.
' + . 'Your template settings have been migrated automatically. ' + . 'MokoOnyx is now your active site template. ' + . 'You can safely uninstall MokoCassiopeia from Extensions → Manage.', + 'success' + ); + + self::log('Bridge migration completed successfully.'); + return true; + } + + /** + * Copy template directory. + */ + private static function copyTemplateFiles(): bool + { + $src = JPATH_ROOT . '/templates/' . self::OLD_NAME; + $dst = JPATH_ROOT . '/templates/' . self::NEW_NAME; + + if (is_dir($dst)) { + self::log('MokoOnyx template directory already exists — skipping copy.'); + return true; + } + + if (!is_dir($src)) { + self::log('Source template directory not found: ' . $src, 'error'); + return false; + } + + return Folder::copy($src, $dst); + } + + /** + * Copy media directory. + */ + private static function copyMediaFiles(): bool + { + $src = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME; + $dst = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; + + if (is_dir($dst)) { + self::log('MokoOnyx media directory already exists — skipping copy.'); + return true; + } + + if (!is_dir($src)) { + self::log('Source media directory not found: ' . $src, 'warning'); + return true; // Non-critical + } + + return Folder::copy($src, $dst); + } + + /** + * Rename internal references in the copied template. + */ + private static function renameInternals(): void + { + $base = JPATH_ROOT . '/templates/' . self::NEW_NAME; + $mediaBase = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; + + // templateDetails.xml — name, element, update servers, paths + $manifest = $base . '/templateDetails.xml'; + if (is_file($manifest)) { + $content = file_get_contents($manifest); + $content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content); + $content = str_replace(self::OLD_NAME, self::NEW_NAME, $content); + // Update the update server URLs to point to MokoOnyx repo + $content = str_replace('MokoCassiopeia', 'MokoOnyx', $content); + file_put_contents($manifest, $content); + self::log('Updated templateDetails.xml for MokoOnyx.'); + } + + // joomla.asset.json + $assetFile = $base . '/joomla.asset.json'; + if (is_file($assetFile)) { + $content = file_get_contents($assetFile); + $content = str_replace(self::OLD_NAME, self::NEW_NAME, $content); + $content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content); + file_put_contents($assetFile, $content); + } + + // Language files + $langDirs = [ + $base . '/language/en-GB', + $base . '/language/en-US', + ]; + foreach ($langDirs as $langDir) { + if (!is_dir($langDir)) continue; + + foreach (glob($langDir . '/*mokocassiopeia*') as $file) { + $newFile = str_replace(self::OLD_NAME, self::NEW_NAME, $file); + if (is_file($file)) { + $content = file_get_contents($file); + $content = str_replace('MOKOCASSIOPEIA', 'MOKOONYX', $content); + $content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content); + $content = str_replace(self::OLD_NAME, self::NEW_NAME, $content); + file_put_contents($newFile, $content); + if ($newFile !== $file) { + File::delete($file); + } + } + } + } + + // script.php — class name + $scriptFile = $base . '/script.php'; + if (is_file($scriptFile)) { + $content = file_get_contents($scriptFile); + $content = str_replace('Tpl_MokocassiopeiaInstallerScript', 'Tpl_MokoonyxInstallerScript', $content); + $content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content); + $content = str_replace(self::OLD_NAME, self::NEW_NAME, $content); + // Remove the bridge migration call from the new template's script + $content = preg_replace( + '/\/\/ Bridge migration.*?MokoBridgeMigration::run\(\);/s', + '// Migration complete — this is MokoOnyx', + $content + ); + file_put_contents($scriptFile, $content); + } + + // Remove bridge helper from the new template (not needed) + $bridgeFile = $base . '/helper/bridge.php'; + if (is_file($bridgeFile)) { + File::delete($bridgeFile); + } + + self::log('Renamed internal references in MokoOnyx.'); + } + + /** + * Migrate database records: template_styles, menu assignments. + */ + private static function migrateDatabase(\Joomla\Database\DatabaseInterface $db): void + { + // Get existing MokoCassiopeia styles + $query = $db->getQuery(true) + ->select('*') + ->from('#__template_styles') + ->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME)) + ->where($db->quoteName('client_id') . ' = 0'); + $oldStyles = $db->setQuery($query)->loadObjectList(); + + if (empty($oldStyles)) { + self::log('No MokoCassiopeia styles found in database.', 'warning'); + return; + } + + foreach ($oldStyles as $oldStyle) { + // Check if MokoOnyx style already exists + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from('#__template_styles') + ->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) + ->where($db->quoteName('title') . ' = ' . $db->quote( + str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title) + )); + $exists = (int) $db->setQuery($query)->loadResult(); + + if ($exists > 0) { + continue; + } + + // Create new style with same params + $newStyle = clone $oldStyle; + unset($newStyle->id); + $newStyle->template = self::NEW_NAME; + $newStyle->title = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title); + + // Update params: replace any mokocassiopeia paths + $params = $newStyle->params; + if (is_string($params)) { + $params = str_replace(self::OLD_NAME, self::NEW_NAME, $params); + $newStyle->params = $params; + } + + $db->insertObject('#__template_styles', $newStyle, 'id'); + $newId = $newStyle->id; + + // Copy menu assignments + $query = $db->getQuery(true) + ->select('menuid') + ->from('#__template_styles_menus') // Joomla 5 uses this table + ->where('template_style_id = ' . (int) $oldStyle->id); + + try { + $menuIds = $db->setQuery($query)->loadColumn(); + foreach ($menuIds as $menuId) { + $obj = (object) [ + 'template_style_id' => $newId, + 'menuid' => $menuId, + ]; + $db->insertObject('#__template_styles_menus', $obj); + } + } catch (\Exception $e) { + // Table may not exist in all Joomla versions + } + + // If this was the default style, make MokoOnyx the default + if ($oldStyle->home == 1) { + // Set MokoOnyx as default + $query = $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('home') . ' = 1') + ->where('id = ' . (int) $newId); + $db->setQuery($query)->execute(); + + // Unset MokoCassiopeia as default + $query = $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('home') . ' = 0') + ->where('id = ' . (int) $oldStyle->id); + $db->setQuery($query)->execute(); + + self::log('Set MokoOnyx as default site template.'); + } + } + + // Register the new template in the extensions table + self::registerExtension($db); + + self::log('Database migration completed. ' . count($oldStyles) . ' style(s) migrated.'); + } + + /** + * Register MokoOnyx in the extensions table so Joomla recognizes it. + */ + private static function registerExtension(\Joomla\Database\DatabaseInterface $db): void + { + // Check if 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')); + $existing = $db->setQuery($query)->loadResult(); + + if ($existing) { + self::log('MokoOnyx already registered in extensions table.'); + return; + } + + // Get the old extension record as a base + $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) { + self::log('MokoCassiopeia extension record not found.', 'warning'); + return; + } + + $newExt = clone $oldExt; + unset($newExt->extension_id); + $newExt->element = self::NEW_NAME; + $newExt->name = self::NEW_NAME; + + // Update manifest_cache with new name + $cache = json_decode($newExt->manifest_cache, true); + if (is_array($cache)) { + $cache['name'] = self::NEW_DISPLAY; + $newExt->manifest_cache = json_encode($cache); + } + + $db->insertObject('#__extensions', $newExt, 'extension_id'); + self::log('Registered MokoOnyx in extensions table (ID: ' . $newExt->extension_id . ').'); + } + + /** + * Log a message. + */ + private static function log(string $message, string $priority = 'info'): void + { + $priorities = [ + 'info' => Log::INFO, + 'warning' => Log::WARNING, + 'error' => Log::ERROR, + ]; + + Log::addLogger( + ['text_file' => 'mokocassiopeia_bridge.log.php'], + Log::ALL, + ['mokocassiopeia_bridge'] + ); + + Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia_bridge'); + } +} diff --git a/updates.xml b/updates.xml index 1439dec..35e22d8 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.16-dev.zip - sha256:2986f08b59617a18d489e0d9e6e49d329ceb8297ae4755b6697f3326c2a41fc4 + 2986f08b59617a18d489e0d9e6e49d329ceb8297ae4755b6697f3326c2a41fc4 development Moko Consulting https://mokoconsulting.tech