2026-02-26 20:22:24 +00:00
<? php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License (./LICENSE.md).
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
2026-03-26 13:19:16 -05:00
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
2026-03-26 13:38:19 -05:00
* VERSION: 02.00.00
2026-03-04 05:55:04 +00:00
* PATH: /src/script.php
2026-03-26 13:19:16 -05:00
* BRIEF: Installation script for MokoWaaS plugin
2026-02-26 20:22:24 +00:00
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
*/
defined ( '_JEXEC' ) or die ;
use Joomla\CMS\Factory ;
use Joomla\CMS\Installer\InstallerAdapter ;
use Joomla\CMS\Installer\InstallerScriptInterface ;
use Joomla\CMS\Language\Text ;
use Joomla\CMS\Log\Log ;
use Joomla\Filesystem\File ;
use Joomla\Filesystem\Folder ;
/**
2026-03-26 13:19:16 -05:00
* Installation script for MokoWaaS plugin
2026-02-26 20:22:24 +00:00
*
* This script handles the installation and uninstallation of language override files
* to Joomla's global language override directories.
*
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
2026-03-26 13:19:16 -05:00
class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
2026-02-26 20:22:24 +00:00
{
/**
* Minimum Joomla version required to install the extension.
*
* @var string
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
private $minimumJoomla = '5.0.0' ;
/**
* Minimum PHP version required to install the extension.
*
* @var string
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
private $minimumPhp = '8.1.0' ;
/**
* Language tags supported by this plugin.
*
* @var array
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
private $languageTags = [ 'en-GB' , 'en-US' ];
/**
* Called before any type of action.
*
* @param string $type Which action is happening (install|uninstall|discover_install|update)
* @param InstallerAdapter $adapter The object responsible for running this script
*
* @return boolean True on success
*
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
2026-02-26 20:29:03 +00:00
public function preflight ( $type , $adapter ) : bool
2026-02-26 20:22:24 +00:00
{
// Check minimum Joomla version
if ( version_compare ( JVERSION , $this -> minimumJoomla , '<' ))
{
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'This extension requires Joomla %s or later.' , $this -> minimumJoomla ),
'error'
);
return false ;
}
// Check minimum PHP version
if ( version_compare ( PHP_VERSION , $this -> minimumPhp , '<' ))
{
Factory :: getApplication () -> enqueueMessage (
sprintf ( 'This extension requires PHP %s or later.' , $this -> minimumPhp ),
'error'
);
return false ;
}
return true ;
}
/**
* Called after any type of action.
*
* @param string $type Which action is happening (install|uninstall|discover_install|update)
* @param InstallerAdapter $adapter The object responsible for running this script
*
* @return boolean True on success
*
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
2026-02-26 20:29:03 +00:00
public function postflight ( $type , $adapter ) : bool
2026-02-26 20:22:24 +00:00
{
// Only install overrides on install or update
if ( $type === 'install' || $type === 'update' )
{
$this -> installLanguageOverrides ();
2026-04-02 16:22:06 -05:00
$this -> updateLoginSupportUrls ();
2026-02-26 20:22:24 +00:00
}
return true ;
}
/**
* Called on installation.
*
* @param InstallerAdapter $adapter The object responsible for running this script
*
* @return boolean True on success
*
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
2026-02-26 20:29:03 +00:00
public function install ( InstallerAdapter $adapter ) : bool
2026-02-26 20:22:24 +00:00
{
return true ;
}
/**
* Called on update.
*
* @param InstallerAdapter $adapter The object responsible for running this script
*
* @return boolean True on success
*
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
2026-02-26 20:29:03 +00:00
public function update ( InstallerAdapter $adapter ) : bool
2026-02-26 20:22:24 +00:00
{
return true ;
}
/**
* Called on uninstallation.
*
* @param InstallerAdapter $adapter The object responsible for running this script
*
* @return boolean True on success
*
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
2026-02-26 20:29:03 +00:00
public function uninstall ( InstallerAdapter $adapter ) : bool
2026-02-26 20:22:24 +00:00
{
// Remove language overrides on uninstall
$this -> uninstallLanguageOverrides ();
return true ;
}
2026-03-26 13:53:24 -05:00
/** Sentinel comment that marks the start of MokoWaaS overrides inside a Joomla override file. */
private const BLOCK_START = '; ===== BEGIN MokoWaaS Overrides (do not edit this block) =====' ;
/** Sentinel comment that marks the end of MokoWaaS overrides inside a Joomla override file. */
private const BLOCK_END = '; ===== END MokoWaaS Overrides =====' ;
2026-03-26 14:10:27 -05:00
/**
* Build the placeholder → value map from the plugin's saved params.
*
* On first install the params row may not exist yet, so every value
* falls back to a sensible default.
*
* @return array Associative array of placeholder => replacement value
*
* @since 02.00.00
*/
private function getPlaceholders ()
{
$params = $this -> getPluginParams ();
return [
'{{BRAND_NAME}}' => $params -> get ( 'brand_name' , 'MokoWaaS' ),
'{{COMPANY_NAME}}' => $params -> get ( 'company_name' , 'Moko Consulting' ),
'{{SUPPORT_URL}}' => $params -> get ( 'support_url' , 'https://mokoconsulting.tech' ),
];
}
/**
* Load the plugin's saved params from the database.
*
* @return \Joomla\Registry\Registry
*
* @since 02.00.00
*/
private function getPluginParams ()
{
$db = Factory :: getDbo ();
$query = $db -> getQuery ( true )
-> select ( $db -> quoteName ( 'params' ))
-> from ( $db -> quoteName ( '#__extensions' ))
-> where ( $db -> quoteName ( 'element' ) . ' = ' . $db -> quote ( 'mokowaas' ))
-> where ( $db -> quoteName ( 'folder' ) . ' = ' . $db -> quote ( 'system' ))
-> where ( $db -> quoteName ( 'type' ) . ' = ' . $db -> quote ( 'plugin' ));
$db -> setQuery ( $query );
$json = $db -> loadResult ();
return new \Joomla\Registry\Registry ( $json ?: '{}' );
}
/**
* Resolve placeholders in an array of language strings.
*
* @param array $strings Key/value pairs (values may contain {{…}} tokens)
*
* @return array The same array with placeholders replaced
*
* @since 02.00.00
*/
private function resolvePlaceholders ( array $strings )
{
$placeholders = $this -> getPlaceholders ();
$search = array_keys ( $placeholders );
$replace = array_values ( $placeholders );
foreach ( $strings as $key => $value )
{
$strings [ $key ] = str_replace ( $search , $replace , $value );
}
return $strings ;
}
2026-02-26 20:22:24 +00:00
/**
* Install language override files to Joomla's global override directories.
*
2026-03-26 14:10:27 -05:00
* Reads each source override template shipped with the plugin, resolves
* {{BRAND_NAME}} etc. from plugin params, then merges the resolved keys
2026-03-26 13:53:24 -05:00
* into the destination file inside a clearly delimited block. Existing
* overrides outside the block are never touched.
2026-02-26 20:22:24 +00:00
*
* @return void
*
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
private function installLanguageOverrides ()
{
$app = Factory :: getApplication ();
2026-03-26 13:19:16 -05:00
$pluginPath = JPATH_PLUGINS . '/system/mokowaas' ;
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
$overrideSets = [
// [source folder relative to plugin, Joomla destination base]
[ 'language/overrides' , JPATH_ROOT . '/language/overrides' , 'frontend' ],
[ 'administrator/language/overrides' , JPATH_ADMINISTRATOR . '/language/overrides' , 'administrator' ],
];
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
foreach ( $overrideSets as [ $sourceDir , $destDir , $label ])
{
foreach ( $this -> languageTags as $tag )
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
$source = $pluginPath . '/' . $sourceDir . '/' . $tag . '.override.ini' ;
$dest = $destDir . '/' . $tag . '.override.ini' ;
if ( ! file_exists ( $source ))
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
continue ;
2026-02-26 20:22:24 +00:00
}
2026-03-26 13:53:24 -05:00
if ( ! is_dir ( $destDir ))
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
Folder :: create ( $destDir );
2026-02-26 20:22:24 +00:00
}
2026-03-26 14:10:27 -05:00
$pluginOverrides = $this -> resolvePlaceholders ( $this -> parseLanguageFile ( $source ));
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
if ( empty ( $pluginOverrides ))
{
continue ;
}
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
if ( $this -> mergeOverridesIntoFile ( $dest , $pluginOverrides ))
2026-02-26 20:22:24 +00:00
{
$app -> enqueueMessage (
2026-03-26 13:53:24 -05:00
sprintf ( 'Installed %s language overrides for %s' , $label , $tag ),
2026-02-26 20:22:24 +00:00
'message'
);
}
else
{
$app -> enqueueMessage (
2026-03-26 13:53:24 -05:00
sprintf ( 'Failed to install %s language overrides for %s' , $label , $tag ),
2026-02-26 20:22:24 +00:00
'warning'
);
}
}
}
2026-03-26 13:53:24 -05:00
}
2026-02-26 20:22:24 +00:00
2026-04-02 16:22:06 -05:00
/**
* Update the mod_loginsupport module params to point to Moko Consulting URLs.
*
* Joomla's login support module stores forum, documentation, and news URLs
* as module parameters in the database. Language overrides can change the
* link text but not the href — this method rewrites the module params so
* the actual links point to mokoconsulting.tech.
*
* @return void
*
* @since 02.00.00
*/
private function updateLoginSupportUrls ()
{
$db = Factory :: getDbo ();
$query = $db -> getQuery ( true )
-> select ([ $db -> quoteName ( 'id' ), $db -> quoteName ( 'params' )])
-> from ( $db -> quoteName ( '#__modules' ))
-> where ( $db -> quoteName ( 'module' ) . ' = ' . $db -> quote ( 'mod_loginsupport' ));
$db -> setQuery ( $query );
$modules = $db -> loadObjectList ();
if ( empty ( $modules ))
{
return ;
}
$supportUrls = [
'forum_url' => 'https://mokoconsulting.tech/support' ,
'documentation_url' => 'https://mokoconsulting.tech/kb' ,
'news_url' => 'https://mokoconsulting.tech/news' ,
];
foreach ( $modules as $module )
{
$params = new \Joomla\Registry\Registry ( $module -> params ?: '{}' );
foreach ( $supportUrls as $key => $url )
{
$params -> set ( $key , $url );
}
$update = $db -> getQuery ( true )
-> update ( $db -> quoteName ( '#__modules' ))
-> set ( $db -> quoteName ( 'params' ) . ' = ' . $db -> quote ( $params -> toString ()))
-> where ( $db -> quoteName ( 'id' ) . ' = ' . ( int ) $module -> id );
$db -> setQuery ( $update );
$db -> execute ();
}
Factory :: getApplication () -> enqueueMessage ( 'Updated login support URLs to Moko Consulting.' , 'message' );
}
2026-03-26 13:53:24 -05:00
/**
* Remove only MokoWaaS overrides from Joomla's global override files.
*
* Strips the delimited MokoWaaS block and any duplicate keys that appear
* outside the block (safety net for upgrades from older versions that wrote
* keys inline). All other content is preserved verbatim.
*
* @return void
*
* @since 02.00.00
*/
private function uninstallLanguageOverrides ()
{
$app = Factory :: getApplication ();
$pluginPath = JPATH_PLUGINS . '/system/mokowaas' ;
$overrideSets = [
[ 'language/overrides' , JPATH_ROOT . '/language/overrides' , 'frontend' ],
[ 'administrator/language/overrides' , JPATH_ADMINISTRATOR . '/language/overrides' , 'administrator' ],
];
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
foreach ( $overrideSets as [ $sourceDir , $destDir , $label ])
{
foreach ( $this -> languageTags as $tag )
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
$source = $pluginPath . '/' . $sourceDir . '/' . $tag . '.override.ini' ;
$dest = $destDir . '/' . $tag . '.override.ini' ;
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
if ( ! file_exists ( $dest ))
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
continue ;
2026-02-26 20:22:24 +00:00
}
2026-03-26 13:53:24 -05:00
$pluginKeys = array_keys ( $this -> parseLanguageFile ( $source ));
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
if ( $this -> removeOverridesFromFile ( $dest , $pluginKeys ))
2026-02-26 20:22:24 +00:00
{
$app -> enqueueMessage (
2026-03-26 13:53:24 -05:00
sprintf ( 'Removed %s language overrides for %s' , $label , $tag ),
2026-02-26 20:22:24 +00:00
'message'
);
}
}
}
}
/**
2026-03-26 13:53:24 -05:00
* Merge plugin overrides into an existing Joomla override file.
2026-02-26 20:22:24 +00:00
*
2026-03-26 13:53:24 -05:00
* The method:
* 1. Reads the destination file (if it exists) and preserves every line.
* 2. Strips any previous MokoWaaS block so it can be rewritten cleanly.
* 3. Removes duplicate keys that now live inside the MokoWaaS block.
* 4. Appends a new MokoWaaS block at the end of the file.
2026-02-26 20:22:24 +00:00
*
2026-03-26 13:53:24 -05:00
* @param string $dest Absolute path to the Joomla override file
* @param array $overrides Key/value pairs to inject
*
* @return boolean True on success
2026-02-26 20:22:24 +00:00
*
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
2026-03-26 13:53:24 -05:00
private function mergeOverridesIntoFile ( $dest , array $overrides )
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
$existingLines = [];
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
if ( file_exists ( $dest ))
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
$existingLines = file ( $dest , FILE_IGNORE_NEW_LINES );
}
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
// Strip any previous MokoWaaS block
$existingLines = $this -> stripMokoWaaSBlock ( $existingLines );
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
// Remove any keys outside the block that we are about to inject
$overrideKeys = array_map ( 'strtoupper' , array_keys ( $overrides ));
$cleanedLines = [];
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
foreach ( $existingLines as $line )
{
$trimmed = trim ( $line );
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
if ( $trimmed !== '' && $trimmed [ 0 ] !== ';' )
{
if ( preg_match ( '/^([A-Z0-9_]+)\s*=/i' , $trimmed , $m ))
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
if ( in_array ( strtoupper ( $m [ 1 ]), $overrideKeys , true ))
{
// Skip - this key will be in the MokoWaaS block
continue ;
}
2026-02-26 20:22:24 +00:00
}
2026-03-26 13:53:24 -05:00
}
$cleanedLines [] = $line ;
}
// Remove trailing blank lines so the block starts cleanly
while ( ! empty ( $cleanedLines ) && trim ( end ( $cleanedLines )) === '' )
{
array_pop ( $cleanedLines );
}
// Build the MokoWaaS block
$block = [];
$block [] = '' ;
$block [] = self :: BLOCK_START ;
$block [] = '; Auto-generated on ' . date ( 'Y-m-d H:i:s' ) . ' — do not edit manually.' ;
foreach ( $overrides as $key => $value )
{
$block [] = strtoupper ( $key ) . '="' . $value . '"' ;
}
$block [] = self :: BLOCK_END ;
$block [] = '' ;
$content = implode ( " \n " , array_merge ( $cleanedLines , $block ));
return File :: write ( $dest , $content );
}
/**
* Remove MokoWaaS overrides from an existing Joomla override file.
*
* Strips the delimited block and any stray keys that match, then rewrites
* the file. If the file would be empty (or comments-only) it is deleted.
*
* @param string $dest Absolute path to the override file
* @param array $keys The override keys to remove (uppercase)
*
* @return boolean True on success
*
* @since 02.00.00
*/
private function removeOverridesFromFile ( $dest , array $keys )
{
if ( ! file_exists ( $dest ))
{
return true ;
}
$lines = file ( $dest , FILE_IGNORE_NEW_LINES );
// Strip the MokoWaaS block
$lines = $this -> stripMokoWaaSBlock ( $lines );
// Also strip any stray keys that match (legacy installs)
$upperKeys = array_map ( 'strtoupper' , $keys );
$cleaned = [];
foreach ( $lines as $line )
{
$trimmed = trim ( $line );
if ( $trimmed !== '' && $trimmed [ 0 ] !== ';' )
{
if ( preg_match ( '/^([A-Z0-9_]+)\s*=/i' , $trimmed , $m ))
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
if ( in_array ( strtoupper ( $m [ 1 ]), $upperKeys , true ))
{
continue ;
}
2026-02-26 20:22:24 +00:00
}
}
2026-03-26 13:53:24 -05:00
$cleaned [] = $line ;
2026-02-26 20:22:24 +00:00
}
2026-03-26 13:53:24 -05:00
// Check whether any real keys remain
$hasKeys = false ;
foreach ( $cleaned as $line )
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
$trimmed = trim ( $line );
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
if ( $trimmed !== '' && $trimmed [ 0 ] !== ';' )
2026-02-26 20:22:24 +00:00
{
2026-03-26 13:53:24 -05:00
$hasKeys = true ;
break ;
}
}
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
if ( ! $hasKeys )
{
return File :: delete ( $dest );
}
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
return File :: write ( $dest , implode ( " \n " , $cleaned ) . " \n " );
}
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
/**
* Remove the MokoWaaS sentinel block from an array of file lines.
*
* @param array $lines Lines of the file (no trailing newlines)
*
* @return array Lines with the block removed
*
* @since 02.00.00
*/
private function stripMokoWaaSBlock ( array $lines )
{
$out = [];
$inBlock = false ;
2026-02-26 20:22:24 +00:00
2026-03-26 13:53:24 -05:00
foreach ( $lines as $line )
{
if ( trim ( $line ) === self :: BLOCK_START )
{
$inBlock = true ;
continue ;
}
if ( trim ( $line ) === self :: BLOCK_END )
{
$inBlock = false ;
continue ;
}
if ( ! $inBlock )
{
$out [] = $line ;
2026-02-26 20:22:24 +00:00
}
}
2026-03-26 13:53:24 -05:00
return $out ;
2026-02-26 20:22:24 +00:00
}
/**
* Parse a language INI file and return the strings as an associative array.
*
* @param string $filePath The path to the language file
*
* @return array Array of language strings (key => value)
*
2026-03-26 13:38:19 -05:00
* @since 02.00.00
2026-02-26 20:22:24 +00:00
*/
private function parseLanguageFile ( $filePath )
{
$strings = [];
if ( ! file_exists ( $filePath ))
{
return $strings ;
}
$content = file_get_contents ( $filePath );
2026-03-26 13:53:24 -05:00
$lines = explode ( " \n " , $content );
2026-02-26 20:22:24 +00:00
foreach ( $lines as $line )
{
$line = trim ( $line );
// Skip empty lines and comments
2026-03-26 13:53:24 -05:00
if ( $line === '' || $line [ 0 ] === ';' )
2026-02-26 20:22:24 +00:00
{
continue ;
}
// Parse KEY="VALUE" format
if ( preg_match ( '/^([A-Z0-9_]+)="(.+)"$/i' , $line , $matches ))
{
2026-03-26 13:53:24 -05:00
$strings [ strtoupper ( $matches [ 1 ])] = $matches [ 2 ];
2026-02-26 20:22:24 +00:00
}
}
return $strings ;
}
}