Fix SHA-256 checksum: remove sha256: prefix (Joomla expects raw hex)
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:
4
.github/workflows/auto-release.yml
vendored
4
.github/workflows/auto-release.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
360
src/helper/bridge.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user