f471ca5fd1
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
Joomla 6's mod_menu only renders the img column as an icon span for level 1 items. For level 2+ items, the menu_icon param in the JSON params column is used instead. Set this param on both new and existing submenu items so icons render correctly.
562 lines
18 KiB
PHP
562 lines
18 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteBackup
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Installer\InstallerAdapter;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Router\Route;
|
|
|
|
class Pkg_MokoSuiteBackupInstallerScript
|
|
{
|
|
/**
|
|
* Minimum Joomla version required
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $minimumJoomla = '4.0.0';
|
|
|
|
/**
|
|
* Minimum PHP version required
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $minimumPhp = '8.1.0';
|
|
|
|
/**
|
|
* Called before any install/update/uninstall action.
|
|
*
|
|
* @param string $type Action type (install, update, uninstall)
|
|
* @param InstallerAdapter $parent Installer adapter
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function preflight(string $type, InstallerAdapter $parent): bool
|
|
{
|
|
if (version_compare(PHP_VERSION, $this->minimumPhp, '<')) {
|
|
Factory::getApplication()->enqueueMessage(
|
|
Text::sprintf('PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR', $this->minimumPhp),
|
|
'error'
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Save download key before Joomla re-registers the update site
|
|
if ($type === 'update') {
|
|
$this->preflight_saveKey();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Called after install/update.
|
|
*
|
|
* @param string $type Action type
|
|
* @param InstallerAdapter $parent Installer adapter
|
|
*
|
|
* @return void
|
|
*/
|
|
/**
|
|
* Called before install/update to preserve the download key.
|
|
*
|
|
* Joomla re-registers update sites from the manifest on every update,
|
|
* which can reset the extra_query (download key). We save it here
|
|
* and restore it in postflight.
|
|
*/
|
|
private ?string $savedDownloadKey = null;
|
|
|
|
public function preflight_saveKey(): void
|
|
{
|
|
try {
|
|
$db = Factory::getDbo();
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('us.extra_query'))
|
|
->from($db->quoteName('#__update_sites', 'us'))
|
|
->join(
|
|
'INNER',
|
|
$db->quoteName('#__update_sites_extensions', 'use')
|
|
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
|
)
|
|
->join(
|
|
'INNER',
|
|
$db->quoteName('#__extensions', 'e')
|
|
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
|
)
|
|
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitebackup'))
|
|
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
|
->setLimit(1);
|
|
$db->setQuery($query);
|
|
$key = $db->loadResult();
|
|
|
|
if (!empty($key)) {
|
|
$this->savedDownloadKey = $key;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
error_log('MokoSuiteBackup: Could not save download key: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function postflight(string $type, InstallerAdapter $parent): void
|
|
{
|
|
// Restore download key if it was saved before update
|
|
if ($this->savedDownloadKey !== null) {
|
|
$this->restoreDownloadKey();
|
|
}
|
|
|
|
if ($type === 'install') {
|
|
// Enable the system plugin automatically on fresh install
|
|
$db = Factory::getDbo();
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('enabled') . ' = 1')
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
|
|
// Enable the quickicon plugin automatically
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('enabled') . ' = 1')
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('quickicon'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
|
|
// Enable the task plugin automatically
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('enabled') . ' = 1')
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('task'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
|
|
// Enable the webservices plugin automatically
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('enabled') . ' = 1')
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('webservices'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
|
|
// Enable the console plugin automatically
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('enabled') . ' = 1')
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('console'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
|
|
// Enable the content plugin automatically
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('enabled') . ' = 1')
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('content'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
|
|
// Enable the actionlog plugin automatically
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('enabled') . ' = 1')
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
|
|
// Create default backup directory in site root
|
|
$backupDir = JPATH_ROOT . '/backups';
|
|
|
|
if (!is_dir($backupDir)) {
|
|
@mkdir($backupDir, 0755, true);
|
|
}
|
|
|
|
// Create default scheduled task — every 30 days, profile 1
|
|
$this->createDefaultScheduledTask();
|
|
}
|
|
|
|
if ($type === 'uninstall') {
|
|
return;
|
|
}
|
|
|
|
// Ensure submenu items exist (Joomla only creates them on fresh install)
|
|
$this->ensureSubmenuItems();
|
|
|
|
// Sync submenu icons in #__menu (Joomla doesn't update icons on upgrades)
|
|
$this->syncMenuIcons();
|
|
|
|
// Warn if no license key configured
|
|
$this->warnMissingLicenseKey();
|
|
|
|
// Warn if any profile still uses the default backup directory
|
|
$this->warnDefaultBackupDir();
|
|
|
|
// Remind user to review backup profile settings
|
|
if ($type === 'install') {
|
|
$profileUrl = Route::_('index.php?option=com_mokosuitebackup&view=profiles');
|
|
|
|
Factory::getApplication()->enqueueMessage(
|
|
'<strong>Review Your Backup Settings</strong> — '
|
|
. 'A default backup profile has been created. Review the profile settings to configure '
|
|
. 'backup type, schedule, storage location, and notifications. '
|
|
. '<a href="' . $profileUrl . '" class="btn btn-sm btn-primary ms-2">Review Profiles</a>',
|
|
'info'
|
|
);
|
|
}
|
|
}
|
|
|
|
private function warnDefaultBackupDir(): void
|
|
{
|
|
try {
|
|
$db = Factory::getDbo();
|
|
// Check for profiles using old literal defaults — migrate to [DEFAULT_DIR]
|
|
$oldDefaults = [
|
|
'administrator/components/com_mokosuitebackup/backups',
|
|
'administrator/components/com_mokojoombackup/backups',
|
|
'./backups',
|
|
'backups',
|
|
];
|
|
$query = $db->getQuery(true)
|
|
->select('COUNT(*)')
|
|
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
|
->where($db->quoteName('published') . ' = 1')
|
|
->where('(' . $db->quoteName('backup_dir') . ' IN ('
|
|
. implode(',', array_map([$db, 'quote'], $oldDefaults))
|
|
. ') OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
|
|
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
|
|
$db->setQuery($query);
|
|
|
|
if ((int) $db->loadResult() > 0) {
|
|
// Auto-migrate old defaults to [DEFAULT_DIR] placeholder
|
|
$update = $db->getQuery(true)
|
|
->update($db->quoteName('#__mokosuitebackup_profiles'))
|
|
->set($db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]'))
|
|
->where('(' . $db->quoteName('backup_dir') . ' IN ('
|
|
. implode(',', array_map([$db, 'quote'], $oldDefaults))
|
|
. ') OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
|
|
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
|
|
$db->setQuery($update);
|
|
$db->execute();
|
|
}
|
|
} catch (\Throwable $e) {
|
|
error_log('MokoSuiteBackup: warnDefaultBackupDir() failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function createDefaultScheduledTask(): void
|
|
{
|
|
try {
|
|
$db = Factory::getDbo();
|
|
|
|
// Check if a MokoSuiteBackup task already exists
|
|
$query = $db->getQuery(true)
|
|
->select('COUNT(*)')
|
|
->from($db->quoteName('#__scheduler_tasks'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('mokosuitebackup.run_profile'));
|
|
$db->setQuery($query);
|
|
|
|
if ((int) $db->loadResult() > 0) {
|
|
return;
|
|
}
|
|
|
|
$now = date('Y-m-d H:i:s');
|
|
|
|
$task = (object) [
|
|
'title' => 'MokoSuiteBackup — Monthly Full Backup',
|
|
'type' => 'mokosuitebackup.run_profile',
|
|
'execution_rules' => json_encode([
|
|
'rule-type' => 'interval-days',
|
|
'interval-days' => '30',
|
|
'exec-day' => '1',
|
|
'exec-time' => '03:00:00',
|
|
]),
|
|
'cron_rules' => json_encode([
|
|
'type' => 'interval',
|
|
'exp' => 'P30D',
|
|
]),
|
|
'state' => 1,
|
|
'params' => json_encode([
|
|
'profile_id' => 1,
|
|
'individual_log' => true,
|
|
'log_file' => '',
|
|
'notifications' => [
|
|
'success_mail' => '0',
|
|
'failure_mail' => '1',
|
|
'notification_failure_groups' => ['8'],
|
|
'fatal_failure_mail' => '1',
|
|
'notification_fatal_groups' => ['8'],
|
|
'orphan_mail' => '0',
|
|
],
|
|
]),
|
|
'priority' => 0,
|
|
'ordering' => 0,
|
|
'cli_exclusive' => 0,
|
|
'note' => '',
|
|
'created' => $now,
|
|
'created_by' => Factory::getApplication()->getIdentity()->id ?? 0,
|
|
'next_execution' => date('Y-m-d 03:00:00', strtotime('+1 day')),
|
|
];
|
|
|
|
$db->insertObject('#__scheduler_tasks', $task);
|
|
} catch (\Throwable $e) {
|
|
error_log('MokoSuiteBackup: createDefaultScheduledTask() failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure admin submenu items exist in #__menu.
|
|
*
|
|
* On updates Joomla does not re-create submenu entries from the manifest,
|
|
* so we use the Installer's own _buildAdminMenus pathway via the
|
|
* component's MenuTable API to create any missing items.
|
|
*/
|
|
private function ensureSubmenuItems(): void
|
|
{
|
|
$submenus = [
|
|
[
|
|
'link' => 'index.php?option=com_mokosuitebackup&view=dashboard',
|
|
'title' => 'COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD',
|
|
'img' => 'class:home',
|
|
'menu_icon' => 'icon-home',
|
|
],
|
|
[
|
|
'link' => 'index.php?option=com_mokosuitebackup&view=backups',
|
|
'title' => 'COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS',
|
|
'img' => 'class:database',
|
|
'menu_icon' => 'icon-database',
|
|
],
|
|
[
|
|
'link' => 'index.php?option=com_mokosuitebackup&view=profiles',
|
|
'title' => 'COM_MOKOJOOMBACKUP_SUBMENU_PROFILES',
|
|
'img' => 'class:cog',
|
|
'menu_icon' => 'icon-cog',
|
|
],
|
|
];
|
|
|
|
try {
|
|
$db = Factory::getDbo();
|
|
|
|
// Find the parent menu item for our component
|
|
$query = $db->getQuery(true)
|
|
->select([$db->quoteName('id'), $db->quoteName('menutype')])
|
|
->from($db->quoteName('#__menu'))
|
|
->where($db->quoteName('client_id') . ' = 1')
|
|
->where($db->quoteName('level') . ' = 1')
|
|
->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup%'))
|
|
->setLimit(1);
|
|
$db->setQuery($query);
|
|
$parent = $db->loadObject();
|
|
|
|
if (!$parent) {
|
|
return;
|
|
}
|
|
|
|
// Get the component extension_id
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('extension_id'))
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitebackup'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
|
->setLimit(1);
|
|
$db->setQuery($query);
|
|
$componentId = (int) $db->loadResult();
|
|
|
|
if (!$componentId) {
|
|
return;
|
|
}
|
|
|
|
foreach ($submenus as $submenu) {
|
|
$params = json_encode(['menu_icon' => $submenu['menu_icon']]);
|
|
|
|
// Check if this submenu item already exists
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('id'))
|
|
->from($db->quoteName('#__menu'))
|
|
->where($db->quoteName('client_id') . ' = 1')
|
|
->where($db->quoteName('link') . ' = ' . $db->quote($submenu['link']))
|
|
->setLimit(1);
|
|
$db->setQuery($query);
|
|
$existingId = (int) $db->loadResult();
|
|
|
|
if ($existingId > 0) {
|
|
// Update params on existing item to ensure menu_icon is set
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__menu'))
|
|
->set($db->quoteName('params') . ' = ' . $db->quote($params))
|
|
->where($db->quoteName('id') . ' = ' . $existingId);
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
continue;
|
|
}
|
|
|
|
// Use Joomla's MenuTable to create the item properly
|
|
$table = Factory::getApplication()
|
|
->bootComponent('com_menus')
|
|
->getMVCFactory()
|
|
->createTable('Menu', 'Administrator');
|
|
|
|
$table->menutype = $parent->menutype;
|
|
$table->title = $submenu['title'];
|
|
$table->alias = strtolower(str_replace(' ', '-', $submenu['title']));
|
|
$table->link = $submenu['link'];
|
|
$table->type = 'component';
|
|
$table->published = 1;
|
|
$table->parent_id = $parent->id;
|
|
$table->level = 2;
|
|
$table->component_id = $componentId;
|
|
$table->client_id = 1;
|
|
$table->img = $submenu['img'];
|
|
$table->params = $params;
|
|
$table->language = '*';
|
|
$table->access = 1;
|
|
|
|
$table->setLocation($parent->id, 'last-child');
|
|
|
|
if (!$table->check() || !$table->store()) {
|
|
error_log('MokoSuiteBackup: Failed to create submenu "' . $submenu['title'] . '": ' . $table->getError());
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
error_log('MokoSuiteBackup: ensureSubmenuItems() failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function syncMenuIcons(): void
|
|
{
|
|
$iconMap = [
|
|
'view=dashboard' => 'class:home',
|
|
'view=backups' => 'class:database',
|
|
'view=profiles' => 'class:cog',
|
|
];
|
|
|
|
try {
|
|
$db = Factory::getDbo();
|
|
|
|
foreach ($iconMap as $linkFragment => $icon) {
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__menu'))
|
|
->set($db->quoteName('img') . ' = ' . $db->quote($icon))
|
|
->where($db->quoteName('client_id') . ' = 1')
|
|
->where($db->quoteName('link') . ' LIKE ' . $db->quote('%com_mokosuitebackup%' . $linkFragment . '%'));
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
}
|
|
|
|
// Set top-level component menu icon
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__menu'))
|
|
->set($db->quoteName('img') . ' = ' . $db->quote('class:archive'))
|
|
->where($db->quoteName('client_id') . ' = 1')
|
|
->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup'))
|
|
->where($db->quoteName('level') . ' = 1');
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
} catch (\Throwable $e) {
|
|
error_log('MokoSuiteBackup: syncMenuIcons() failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore the download key to the (possibly new) update site record.
|
|
*/
|
|
private function restoreDownloadKey(): void
|
|
{
|
|
try {
|
|
$db = Factory::getDbo();
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('us.update_site_id'))
|
|
->from($db->quoteName('#__update_sites', 'us'))
|
|
->join(
|
|
'INNER',
|
|
$db->quoteName('#__update_sites_extensions', 'use')
|
|
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
|
)
|
|
->join(
|
|
'INNER',
|
|
$db->quoteName('#__extensions', 'e')
|
|
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
|
)
|
|
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitebackup'))
|
|
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
|
->setLimit(1);
|
|
$db->setQuery($query);
|
|
$updateSiteId = (int) $db->loadResult();
|
|
|
|
if ($updateSiteId > 0) {
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__update_sites'))
|
|
->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey))
|
|
->where($db->quoteName('update_site_id') . ' = ' . $updateSiteId);
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
}
|
|
} catch (\Throwable $e) {
|
|
error_log('MokoSuiteBackup: Could not restore download key: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function warnMissingLicenseKey(): void
|
|
{
|
|
try
|
|
{
|
|
$db = Factory::getDbo();
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')])
|
|
->from($db->quoteName('#__update_sites'))
|
|
->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteBackup%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteBackup%') . ')')
|
|
->setLimit(1)
|
|
);
|
|
$site = $db->loadObject();
|
|
|
|
if ($site)
|
|
{
|
|
$eq = (string) ($site->extra_query ?? '');
|
|
if (!empty($eq) && strpos($eq, 'dlid=') !== false) { parse_str($eq, $p); if (!empty($p['dlid'])) { return; } }
|
|
$editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id;
|
|
}
|
|
else
|
|
{
|
|
$editUrl = 'index.php?option=com_installer&view=updatesites';
|
|
}
|
|
|
|
Factory::getApplication()->enqueueMessage(
|
|
'<strong>Moko Consulting License Key Required</strong> — '
|
|
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
|
|
. '<a href="' . $editUrl . '" class="btn btn-sm btn-warning ms-2">Enter License Key</a>',
|
|
'warning'
|
|
);
|
|
}
|
|
catch (\Throwable $e) {
|
|
error_log('MokoSuiteBackup: License key check failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
}
|