Fix SHA-256 checksum: remove sha256: prefix (Joomla expects raw hex)
Some checks failed
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 4s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 4s

Joomla's update system compares hash_file() output (raw hex) against
the <sha256> 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) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-04-18 11:33:16 -05:00
parent bb4f82b795
commit e4b38df975
4 changed files with 364 additions and 4 deletions

View File

@@ -517,9 +517,9 @@ jobs:
# Replace downloads block with both formats + SHA
sed -i "s|<downloads>.*</downloads>|<downloads>\n <downloadurl type=\"full\" format=\"zip\">${ZIP_URL}</downloadurl>\n <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n </downloads>|" updates.xml 2>/dev/null || true
if grep -q '<sha256>' updates.xml; then
sed -i "s|<sha256>.*</sha256>|<sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
sed -i "s|<sha256>.*</sha256>|<sha256>${SHA256_ZIP}</sha256>|" updates.xml
else
sed -i "s|</downloads>|</downloads>\n <sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
sed -i "s|</downloads>|</downloads>\n <sha256>${SHA256_ZIP}</sha256>|" updates.xml
fi
git add updates.xml

View File

@@ -333,7 +333,7 @@ jobs:
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
# Update SHA-256
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>sha256:{sha256}</sha256>", block)
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", block)
# Update Gitea download URL
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"

360
src/helper/bridge.php Normal file
View File

@@ -0,0 +1,360 @@
<?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
*/
/**
* 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(
'<strong>MokoCassiopeia has been renamed to MokoOnyx.</strong><br>'
. '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');
}
}

View File

@@ -19,7 +19,7 @@
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.16-dev.zip</downloadurl>
</downloads>
<sha256>sha256:2986f08b59617a18d489e0d9e6e49d329ceb8297ae4755b6697f3326c2a41fc4</sha256>
<sha256>2986f08b59617a18d489e0d9e6e49d329ceb8297ae4755b6697f3326c2a41fc4</sha256>
<tags><tag>development</tag></tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>