Release 01.00.18 — minify pipeline, header color var, hero centering
This commit was merged in pull request #2.
This commit is contained in:
354
src/script.php
354
src/script.php
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
@@ -12,45 +12,33 @@
|
||||
* Joomla calls the methods in this class automatically during template
|
||||
* install, update, and uninstall via the <scriptfile> element in
|
||||
* templateDetails.xml.
|
||||
* Joomla 5 and 6 compatible — uses the InstallerScriptInterface when
|
||||
* available, falls back to the legacy class-based approach otherwise.
|
||||
*
|
||||
* On first install, detects MokoCassiopeia and migrates template styles,
|
||||
* parameters, menu assignments, and user files automatically.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Installer\InstallerAdapter;
|
||||
use Joomla\CMS\Installer\InstallerScriptInterface;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
class Tpl_MokoonyxInstallerScript
|
||||
class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
||||
{
|
||||
/**
|
||||
* Minimum PHP version required by this template.
|
||||
*/
|
||||
private const MIN_PHP = '8.1.0';
|
||||
|
||||
/**
|
||||
* Minimum Joomla version required by this template.
|
||||
*/
|
||||
private const MIN_JOOMLA = '4.4.0';
|
||||
|
||||
/**
|
||||
* Called before install/update/uninstall.
|
||||
*
|
||||
* @param string $type install, update, discover_install, or uninstall.
|
||||
* @param InstallerAdapter $parent The adapter calling this method.
|
||||
*
|
||||
* @return bool True to proceed, false to abort.
|
||||
*/
|
||||
private const OLD_NAME = 'mokocassiopeia';
|
||||
private const NEW_NAME = 'mokoonyx';
|
||||
private const OLD_DISPLAY = 'MokoCassiopeia';
|
||||
private const NEW_DISPLAY = 'MokoOnyx';
|
||||
|
||||
public function preflight(string $type, InstallerAdapter $parent): bool
|
||||
{
|
||||
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
sprintf(
|
||||
'MokoOnyx requires PHP %s or later. You are running PHP %s.',
|
||||
self::MIN_PHP,
|
||||
PHP_VERSION
|
||||
),
|
||||
sprintf('MokoOnyx requires PHP %s or later. You are running PHP %s.', self::MIN_PHP, PHP_VERSION),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
@@ -58,11 +46,7 @@ class Tpl_MokoonyxInstallerScript
|
||||
|
||||
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
sprintf(
|
||||
'MokoOnyx requires Joomla %s or later. You are running Joomla %s.',
|
||||
self::MIN_JOOMLA,
|
||||
JVERSION
|
||||
),
|
||||
sprintf('MokoOnyx requires Joomla %s or later. You are running Joomla %s.', self::MIN_JOOMLA, JVERSION),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
@@ -71,37 +55,17 @@ class Tpl_MokoonyxInstallerScript
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a successful install.
|
||||
*
|
||||
* @param InstallerAdapter $parent The adapter calling this method.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function install(InstallerAdapter $parent): bool
|
||||
{
|
||||
$this->logMessage('MokoOnyx template installed.');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a successful update.
|
||||
*
|
||||
* This is where the CSS variable sync runs — it detects variables that
|
||||
* were added in the new version and injects them into the user's custom
|
||||
* palette files without overwriting existing values.
|
||||
*
|
||||
* @param InstallerAdapter $parent The adapter calling this method.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update(InstallerAdapter $parent): bool
|
||||
{
|
||||
$this->logMessage('MokoOnyx template updated.');
|
||||
|
||||
// Run CSS variable sync to inject any new variables into user's custom palettes.
|
||||
$synced = $this->syncCustomVariables($parent);
|
||||
|
||||
if ($synced > 0) {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
sprintf(
|
||||
@@ -116,47 +80,272 @@ class Tpl_MokoonyxInstallerScript
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a successful uninstall.
|
||||
*
|
||||
* @param InstallerAdapter $parent The adapter calling this method.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall(InstallerAdapter $parent): bool
|
||||
{
|
||||
$this->logMessage('MokoOnyx template uninstalled.');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after install/update completes (regardless of type).
|
||||
*
|
||||
* @param string $type install, update, or discover_install.
|
||||
* @param InstallerAdapter $parent The adapter calling this method.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function postflight(string $type, InstallerAdapter $parent): bool
|
||||
{
|
||||
// On install or update: migrate from MokoCassiopeia if it exists
|
||||
if ($type === 'install' || $type === 'update') {
|
||||
$this->migrateFromCassiopeia();
|
||||
$this->replaceCassiopeiaReferences();
|
||||
$this->clearFaviconStamp();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the CSS variable sync utility.
|
||||
*
|
||||
* Loads sync_custom_vars.php from the template directory and calls
|
||||
* MokoCssVarSync::run() to detect and inject missing variables.
|
||||
*
|
||||
* @param InstallerAdapter $parent The adapter calling this method.
|
||||
*
|
||||
* @return int Number of variables added across all files.
|
||||
* Replace MokoCassiopeia references in article content and module content.
|
||||
*/
|
||||
private function replaceCassiopeiaReferences(): void
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Replace in article content (introtext + fulltext)
|
||||
foreach (['introtext', 'fulltext'] as $col) {
|
||||
try {
|
||||
$query = $db->getQuery(true)
|
||||
->update('#__content')
|
||||
->set(
|
||||
$db->quoteName($col) . ' = REPLACE(REPLACE('
|
||||
. $db->quoteName($col) . ', '
|
||||
. $db->quote(self::OLD_DISPLAY) . ', '
|
||||
. $db->quote(self::NEW_DISPLAY) . '), '
|
||||
. $db->quote(self::OLD_NAME) . ', '
|
||||
. $db->quote(self::NEW_NAME) . ')'
|
||||
)
|
||||
->where(
|
||||
'(' . $db->quoteName($col) . ' LIKE ' . $db->quote('%' . self::OLD_DISPLAY . '%')
|
||||
. ' OR ' . $db->quoteName($col) . ' LIKE ' . $db->quote('%' . self::OLD_NAME . '%') . ')'
|
||||
);
|
||||
$db->setQuery($query)->execute();
|
||||
$n = $db->getAffectedRows();
|
||||
if ($n > 0) {
|
||||
$this->logMessage("Replaced MokoCassiopeia in {$n} content row(s) ({$col}).");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logMessage('Content replacement failed (' . $col . '): ' . $e->getMessage(), 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Replace in module content (custom HTML modules etc.)
|
||||
try {
|
||||
$query = $db->getQuery(true)
|
||||
->update('#__modules')
|
||||
->set(
|
||||
$db->quoteName('content') . ' = REPLACE(REPLACE('
|
||||
. $db->quoteName('content') . ', '
|
||||
. $db->quote(self::OLD_DISPLAY) . ', '
|
||||
. $db->quote(self::NEW_DISPLAY) . '), '
|
||||
. $db->quote(self::OLD_NAME) . ', '
|
||||
. $db->quote(self::NEW_NAME) . ')'
|
||||
)
|
||||
->where(
|
||||
'(' . $db->quoteName('content') . ' LIKE ' . $db->quote('%' . self::OLD_DISPLAY . '%')
|
||||
. ' OR ' . $db->quoteName('content') . ' LIKE ' . $db->quote('%' . self::OLD_NAME . '%') . ')'
|
||||
);
|
||||
$db->setQuery($query)->execute();
|
||||
$n = $db->getAffectedRows();
|
||||
if ($n > 0) {
|
||||
$this->logMessage("Replaced MokoCassiopeia in {$n} module(s).");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logMessage('Module replacement failed: ' . $e->getMessage(), 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the favicon stamp file so favicons and site.webmanifest
|
||||
* are regenerated on the next page load after install/update.
|
||||
* Also removes the old /images/favicons/ location.
|
||||
*/
|
||||
private function clearFaviconStamp(): void
|
||||
{
|
||||
// Clear new location stamp
|
||||
$stampFile = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME . '/images/favicons/.favicon_generated';
|
||||
if (is_file($stampFile)) {
|
||||
@unlink($stampFile);
|
||||
$this->logMessage('Cleared favicon stamp — will regenerate on next page load.');
|
||||
}
|
||||
|
||||
// Remove old /images/favicons/ directory from previous versions
|
||||
$oldDir = JPATH_ROOT . '/images/favicons';
|
||||
if (is_dir($oldDir)) {
|
||||
$files = glob($oldDir . '/*');
|
||||
if ($files) {
|
||||
foreach ($files as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
@unlink($oldDir . '/.favicon_generated');
|
||||
@rmdir($oldDir);
|
||||
$this->logMessage('Removed old favicon directory: images/favicons/');
|
||||
}
|
||||
|
||||
// Remove any favicon files left in the site root
|
||||
$rootFavicons = ['favicon.ico', 'favicon.png', 'apple-touch-icon.png', 'site.webmanifest'];
|
||||
foreach ($rootFavicons as $file) {
|
||||
$path = JPATH_ROOT . '/' . $file;
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
$this->logMessage('Removed root favicon: ' . $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MokoCassiopeia and create matching MokoOnyx styles with the same params.
|
||||
* Creates a MokoOnyx style copy for each MokoCassiopeia style.
|
||||
*/
|
||||
private function migrateFromCassiopeia(): void
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Get all 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)) {
|
||||
$this->logMessage('No MokoCassiopeia styles found — fresh install.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logMessage('MokoCassiopeia detected — creating ' . count($oldStyles) . ' matching MokoOnyx style(s).');
|
||||
|
||||
// Get the installer-created default MokoOnyx style (to apply params to it)
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from('#__template_styles')
|
||||
->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
|
||||
->where($db->quoteName('client_id') . ' = 0')
|
||||
->order($db->quoteName('id') . ' ASC');
|
||||
$defaultOnyxId = (int) $db->setQuery($query, 0, 1)->loadResult();
|
||||
|
||||
$firstStyle = true;
|
||||
|
||||
foreach ($oldStyles as $oldStyle) {
|
||||
$newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title);
|
||||
$newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle);
|
||||
|
||||
$params = is_string($oldStyle->params)
|
||||
? str_replace(self::OLD_NAME, self::NEW_NAME, $oldStyle->params)
|
||||
: $oldStyle->params;
|
||||
|
||||
if ($firstStyle && $defaultOnyxId) {
|
||||
// Update the installer-created default style with the first MokoCassiopeia style's params
|
||||
$update = $db->getQuery(true)
|
||||
->update('#__template_styles')
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote($params))
|
||||
->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
|
||||
->where('id = ' . $defaultOnyxId);
|
||||
$db->setQuery($update)->execute();
|
||||
|
||||
// Set as default if MokoCassiopeia was default
|
||||
if ($oldStyle->home == 1) {
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update('#__template_styles')
|
||||
->set($db->quoteName('home') . ' = 1')
|
||||
->where('id = ' . $defaultOnyxId)
|
||||
)->execute();
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update('#__template_styles')
|
||||
->set($db->quoteName('home') . ' = 0')
|
||||
->where('id = ' . (int) $oldStyle->id)
|
||||
)->execute();
|
||||
|
||||
$this->logMessage('Set MokoOnyx as default site template.');
|
||||
}
|
||||
|
||||
$this->logMessage("Updated default MokoOnyx style with params: {$newTitle}");
|
||||
$firstStyle = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For additional styles: create new MokoOnyx style copies
|
||||
$newStyle = clone $oldStyle;
|
||||
unset($newStyle->id);
|
||||
$newStyle->template = self::NEW_NAME;
|
||||
$newStyle->title = $newTitle;
|
||||
$newStyle->home = 0;
|
||||
$newStyle->params = $params;
|
||||
|
||||
try {
|
||||
$db->insertObject('#__template_styles', $newStyle, 'id');
|
||||
$this->logMessage("Created MokoOnyx style: {$newTitle}");
|
||||
} catch (\Throwable $e) {
|
||||
$this->logMessage("Failed to create style {$newTitle}: " . $e->getMessage(), 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Copy user files (custom themes, user.css, user.js)
|
||||
$this->copyUserFiles();
|
||||
|
||||
// 3. Notify admin
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<strong>MokoOnyx has been installed as a replacement for MokoCassiopeia.</strong><br>'
|
||||
. 'Your template settings and custom files have been migrated automatically. '
|
||||
. 'MokoOnyx is now your active site template. '
|
||||
. 'You can safely uninstall MokoCassiopeia from Extensions → Manage.',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy user-specific files from MokoCassiopeia media to MokoOnyx media.
|
||||
*/
|
||||
private function copyUserFiles(): void
|
||||
{
|
||||
$oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
|
||||
$newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
|
||||
|
||||
if (!is_dir($oldMedia) || !is_dir($newMedia)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userFiles = [
|
||||
'css/theme/light.custom.css',
|
||||
'css/theme/dark.custom.css',
|
||||
'css/theme/light.custom.min.css',
|
||||
'css/theme/dark.custom.min.css',
|
||||
'css/user.css',
|
||||
'css/user.min.css',
|
||||
'js/user.js',
|
||||
'js/user.min.js',
|
||||
];
|
||||
|
||||
$copied = 0;
|
||||
foreach ($userFiles as $relPath) {
|
||||
$src = $oldMedia . '/' . $relPath;
|
||||
$dst = $newMedia . '/' . $relPath;
|
||||
if (is_file($src) && !is_file($dst)) {
|
||||
$dstDir = dirname($dst);
|
||||
if (!is_dir($dstDir)) {
|
||||
mkdir($dstDir, 0755, true);
|
||||
}
|
||||
copy($src, $dst);
|
||||
$copied++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($copied > 0) {
|
||||
$this->logMessage("Copied {$copied} user file(s) from MokoCassiopeia.");
|
||||
}
|
||||
}
|
||||
|
||||
private function syncCustomVariables(InstallerAdapter $parent): int
|
||||
{
|
||||
$templateDir = $parent->getParent()->getPath('source');
|
||||
|
||||
// The sync script lives alongside this script in the template root.
|
||||
$syncScript = $templateDir . '/sync_custom_vars.php';
|
||||
|
||||
if (!is_file($syncScript)) {
|
||||
@@ -172,20 +361,13 @@ class Tpl_MokoonyxInstallerScript
|
||||
}
|
||||
|
||||
try {
|
||||
$joomlaRoot = JPATH_ROOT;
|
||||
$results = MokoCssVarSync::run($joomlaRoot);
|
||||
$results = MokoCssVarSync::run(JPATH_ROOT);
|
||||
|
||||
$totalAdded = 0;
|
||||
foreach ($results as $filePath => $result) {
|
||||
$totalAdded += count($result['added']);
|
||||
if (!empty($result['added'])) {
|
||||
$this->logMessage(
|
||||
sprintf(
|
||||
'CSS sync: added %d variable(s) to %s',
|
||||
count($result['added']),
|
||||
basename($filePath)
|
||||
)
|
||||
);
|
||||
$this->logMessage(sprintf('CSS sync: added %d variable(s) to %s', count($result['added']), basename($filePath)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,12 +378,6 @@ class Tpl_MokoonyxInstallerScript
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to Joomla's log system.
|
||||
*
|
||||
* @param string $message The log message.
|
||||
* @param string $priority Log priority (info, warning, error).
|
||||
*/
|
||||
private function logMessage(string $message, string $priority = 'info'): void
|
||||
{
|
||||
$priorities = [
|
||||
|
||||
Reference in New Issue
Block a user