* * 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 * INGROUP: MokoWaaS * REPO: https://github.com/mokoconsulting-tech/mokowaas * VERSION: 02.00.00 * PATH: /src/script.php * BRIEF: Installation script for MokoWaaS plugin * 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; /** * Installation script for MokoWaaS plugin * * This script handles the installation and uninstallation of language override files * to Joomla's global language override directories. * * @since 02.00.00 */ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface { /** * Minimum Joomla version required to install the extension. * * @var string * @since 02.00.00 */ private $minimumJoomla = '5.0.0'; /** * Minimum PHP version required to install the extension. * * @var string * @since 02.00.00 */ private $minimumPhp = '8.1.0'; /** * Language tags supported by this plugin. * * @var array * @since 02.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 * * @since 02.00.00 */ public function preflight($type, $adapter): bool { // 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 * * @since 02.00.00 */ public function postflight($type, $adapter): bool { // Only install overrides on install or update if ($type === 'install' || $type === 'update') { $this->installLanguageOverrides(); } return true; } /** * Called on installation. * * @param InstallerAdapter $adapter The object responsible for running this script * * @return boolean True on success * * @since 02.00.00 */ public function install(InstallerAdapter $adapter): bool { return true; } /** * Called on update. * * @param InstallerAdapter $adapter The object responsible for running this script * * @return boolean True on success * * @since 02.00.00 */ public function update(InstallerAdapter $adapter): bool { return true; } /** * Called on uninstallation. * * @param InstallerAdapter $adapter The object responsible for running this script * * @return boolean True on success * * @since 02.00.00 */ public function uninstall(InstallerAdapter $adapter): bool { // Remove language overrides on uninstall $this->uninstallLanguageOverrides(); return true; } /** 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 ====='; /** * Install language override files to Joomla's global override directories. * * Reads each source override shipped with the plugin, then merges the keys * into the destination file inside a clearly delimited block. Existing * overrides outside the block are never touched. * * @return void * * @since 02.00.00 */ private function installLanguageOverrides() { $app = Factory::getApplication(); $pluginPath = JPATH_PLUGINS . '/system/mokowaas'; $overrideSets = [ // [source folder relative to plugin, Joomla destination base] ['language/overrides', JPATH_ROOT . '/language/overrides', 'frontend'], ['administrator/language/overrides', JPATH_ADMINISTRATOR . '/language/overrides', 'administrator'], ]; foreach ($overrideSets as [$sourceDir, $destDir, $label]) { foreach ($this->languageTags as $tag) { $source = $pluginPath . '/' . $sourceDir . '/' . $tag . '.override.ini'; $dest = $destDir . '/' . $tag . '.override.ini'; if (!file_exists($source)) { continue; } if (!is_dir($destDir)) { Folder::create($destDir); } $pluginOverrides = $this->parseLanguageFile($source); if (empty($pluginOverrides)) { continue; } if ($this->mergeOverridesIntoFile($dest, $pluginOverrides)) { $app->enqueueMessage( sprintf('Installed %s language overrides for %s', $label, $tag), 'message' ); } else { $app->enqueueMessage( sprintf('Failed to install %s language overrides for %s', $label, $tag), 'warning' ); } } } } /** * 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'], ]; foreach ($overrideSets as [$sourceDir, $destDir, $label]) { foreach ($this->languageTags as $tag) { $source = $pluginPath . '/' . $sourceDir . '/' . $tag . '.override.ini'; $dest = $destDir . '/' . $tag . '.override.ini'; if (!file_exists($dest)) { continue; } $pluginKeys = array_keys($this->parseLanguageFile($source)); if ($this->removeOverridesFromFile($dest, $pluginKeys)) { $app->enqueueMessage( sprintf('Removed %s language overrides for %s', $label, $tag), 'message' ); } } } } /** * Merge plugin overrides into an existing Joomla override file. * * 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. * * @param string $dest Absolute path to the Joomla override file * @param array $overrides Key/value pairs to inject * * @return boolean True on success * * @since 02.00.00 */ private function mergeOverridesIntoFile($dest, array $overrides) { $existingLines = []; if (file_exists($dest)) { $existingLines = file($dest, FILE_IGNORE_NEW_LINES); } // Strip any previous MokoWaaS block $existingLines = $this->stripMokoWaaSBlock($existingLines); // Remove any keys outside the block that we are about to inject $overrideKeys = array_map('strtoupper', array_keys($overrides)); $cleanedLines = []; foreach ($existingLines as $line) { $trimmed = trim($line); if ($trimmed !== '' && $trimmed[0] !== ';') { if (preg_match('/^([A-Z0-9_]+)\s*=/i', $trimmed, $m)) { if (in_array(strtoupper($m[1]), $overrideKeys, true)) { // Skip - this key will be in the MokoWaaS block continue; } } } $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)) { if (in_array(strtoupper($m[1]), $upperKeys, true)) { continue; } } } $cleaned[] = $line; } // Check whether any real keys remain $hasKeys = false; foreach ($cleaned as $line) { $trimmed = trim($line); if ($trimmed !== '' && $trimmed[0] !== ';') { $hasKeys = true; break; } } if (!$hasKeys) { return File::delete($dest); } return File::write($dest, implode("\n", $cleaned) . "\n"); } /** * 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; 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; } } return $out; } /** * 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) * * @since 02.00.00 */ private function parseLanguageFile($filePath) { $strings = []; if (!file_exists($filePath)) { return $strings; } $content = file_get_contents($filePath); $lines = explode("\n", $content); foreach ($lines as $line) { $line = trim($line); // Skip empty lines and comments if ($line === '' || $line[0] === ';') { continue; } // Parse KEY="VALUE" format if (preg_match('/^([A-Z0-9_]+)="(.+)"$/i', $line, $matches)) { $strings[strtoupper($matches[1])] = $matches[2]; } } return $strings; } }