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
|
# 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
|
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
|
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
|
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
|
fi
|
||||||
|
|
||||||
git add updates.xml
|
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)
|
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
|
||||||
|
|
||||||
# Update SHA-256
|
# 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
|
# Update Gitea download URL
|
||||||
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
|
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>
|
<downloads>
|
||||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.16-dev.zip</downloadurl>
|
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.16-dev.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>sha256:2986f08b59617a18d489e0d9e6e49d329ceb8297ae4755b6697f3326c2a41fc4</sha256>
|
<sha256>2986f08b59617a18d489e0d9e6e49d329ceb8297ae4755b6697f3326c2a41fc4</sha256>
|
||||||
<tags><tag>development</tag></tags>
|
<tags><tag>development</tag></tags>
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
|||||||
Reference in New Issue
Block a user