Merge pull request 'feat: full-screen backup for all updates + uninstalls (Joomla 6 core + extensions)' (#240) from fix/preupdate-j6-viewupdate into dev
This commit was merged in pull request #240.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 02.58.17
|
||||
# VERSION: 02.58.20
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Full-screen backup now also fronts **extension updates and uninstalls** (Extensions → Update / Manage), not just core Joomla updates. Because `com_installer`'s `update.update` / `manage.remove` are POST actions (CSRF token + a checked `cid[]` list) that a server-side redirect can't cleanly resume, this is done client-side: the toolbar Update/Uninstall click is intercepted, the selection is captured, the full-screen backup runs, and on return the original selection is restored and the real POST form is re-submitted. Gated by `backup_before_update` / `backup_before_uninstall`, super-user only, and skipped if a backup already ran this session.
|
||||
- Manual **"Backup Now"** completion now offers a **View backup record** button (links straight to the record that was just created). It appears only for manual backups — the pre-update / pre-uninstall flow hands control back to Joomla instead.
|
||||
- **Full-screen backup screen** (`view=runbackup`) for both pre-update and manual backups, modelled on Akeeba's Backup-on-Update — replaces the earlier popup-modal approach. Clicking Joomla's **Install the update** now redirects (server-side, Akeeba-style) to a dedicated full-page backup screen that runs the stepped backup with a real progress bar and then **automatically continues the update** via a validated `returnurl` (flagged `is_backed_up=1` so the backup isn't repeated). Crucially, **no backup runs synchronously inside the update request** — which is what white-screened large sites. The dashboard **Backup Now** now opens the same full-screen screen instead of an inline modal. (#196)
|
||||
- Retention now prunes **remote** copies too: when a backup is pruned by age/count, its archive is deleted from every enabled remote destination (SFTP / FTP / S3 / Google Drive), not just the local copy. Each uploader gained an idempotent `delete()` method (already-absent file = success), and removal is best-effort — a failing destination is logged but never blocks local pruning. The shared standalone `restore.php` is intentionally left in place (every backup overwrites it, so newer backups still depend on it). (#229)
|
||||
|
||||
@@ -14,6 +16,7 @@
|
||||
|
||||
### Fixed
|
||||
- Package installer is now **honest about success**: the postflight no longer prints "installed successfully" / next-steps when the install actually failed or only partially completed. Joomla logs a failed child extension but still runs the package postflight, so the postflight now (a) verifies every bundled child declared in the manifest actually registered in `#__extensions` (matched by element + type, and folder/group for plugins) and (b) verifies the component's schema tables — derived dynamically from the installed `install.mysql.sql` — actually exist. If anything is missing it enqueues an error listing what's missing and stops before the success message. Fail-open: any manifest/DB/IO glitch is treated as "nothing missing" so a transient error never turns a good install into a false failure. Unconditional housekeeping (e.g. download-key restore) still runs regardless.
|
||||
- Pre-update full-screen backup screen now actually triggers on **Joomla 6** (and 4/5). The redirect matched only the legacy `update.install` task, which Joomla 4/5/6 don't use — they server-side-redirect to the **updating page `view=update`**, which then extracts the downloaded package from JavaScript. The plugin now intercepts the `view=update` page **load** (the last point before any files change) and returns the browser there flagged `is_backed_up=1` so the extraction proceeds after the backup. (`update.finalise` is intentionally not intercepted — by then the files are already extracted.)
|
||||
- Pre-update/uninstall backup no longer **white-screens** the update on large sites. The synchronous backup that runs inside the extension update/uninstall request now raises `max_execution_time`/`memory_limit` (and `ignore_user_abort`) like the web-cron path, so it can't exhaust the request's default limits mid-backup. (Core Joomla updates additionally get a full-screen backup screen — see below.)
|
||||
- Archive names for **CLI/console-triggered backups** no longer come out as `joomla.invalid_…`. The `[HOST]` placeholder took `$_SERVER['HTTP_HOST']` verbatim, but Joomla's console fills that with the reserved sentinel host `joomla.invalid`; the resolver now treats that (like empty/`localhost`) as unusable and falls back to the configured `live_site` host, then the system hostname. (Set the site's *live_site* to get the exact domain in CLI-built names.)
|
||||
- Standalone restore script generation no longer aborts backups with `str_replace() expects at least 3 arguments, 2 given`. `MokoRestore::generateStandaloneScript()` had a `str_replace()` call (the "Backup Archive" pre-check rewrite) that was missing its `$php` subject argument, so **every** standalone-mode backup fatally errored while "Generating standalone restore.php…" — the archive still finalized and uploaded, but no `restore.php` was ever produced (the true root cause behind #226). (#226)
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 02.58.17
|
||||
VERSION: 02.58.20
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -347,6 +347,7 @@ COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED="Backup failed"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_RETRY="Retry backup"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_ANYWAY="Continue without backup"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD="View backup record"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING="A backup is in progress. Leaving now will cancel it."
|
||||
|
||||
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
||||
|
||||
@@ -69,6 +69,7 @@ COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED="Backup failed"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_RETRY="Retry backup"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_ANYWAY="Continue without backup"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD="View backup record"
|
||||
COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING="A backup is in progress. Leaving now will cancel it."
|
||||
|
||||
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
display label there.
|
||||
-->
|
||||
<name>MokoSuiteBackup</name>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.19 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.20 — no schema changes */
|
||||
@@ -85,12 +85,14 @@ class AjaxController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the pre-update backup as satisfied for this session.
|
||||
* Mark the pre-action backup as satisfied for this session.
|
||||
*
|
||||
* Called by the update-page modal after a successful on-demand backup so the
|
||||
* imminent server-side onExtensionBeforeUpdate backup is skipped — it sets
|
||||
* the same 10-minute throttle key the system plugin checks, preventing a
|
||||
* duplicate backup when the admin then clicks Joomla's Update button.
|
||||
* Called by the full-screen backup screen after a successful backup so the
|
||||
* imminent server-side onExtensionBeforeUpdate / onExtensionBeforeUninstall
|
||||
* backup is skipped — it arms the same 10-minute throttle keys the system
|
||||
* plugin checks, preventing a duplicate backup when the update / uninstall
|
||||
* then proceeds. Arms BOTH keys so a single ack covers whichever action the
|
||||
* full-screen backup was fronting.
|
||||
* POST: task=ajax.preupdateAck
|
||||
*/
|
||||
public function preupdateAck(): void
|
||||
@@ -107,10 +109,13 @@ class AjaxController extends BaseController
|
||||
return;
|
||||
}
|
||||
|
||||
// Same key + semantics as plg_system_mokosuitebackup::runPreActionBackup().
|
||||
Factory::getSession()->set('mokosuitebackup.preaction_backup_before_update', time());
|
||||
// Same keys + semantics as plg_system_mokosuitebackup::runPreActionBackup().
|
||||
$session = Factory::getSession();
|
||||
$now = time();
|
||||
$session->set('mokosuitebackup.preaction_backup_before_update', $now);
|
||||
$session->set('mokosuitebackup.preaction_backup_before_uninstall', $now);
|
||||
|
||||
$this->sendJson(['error' => false, 'message' => 'Pre-update backup acknowledged']);
|
||||
$this->sendJson(['error' => false, 'message' => 'Pre-action backup acknowledged']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -217,6 +217,7 @@ class SteppedBackupEngine
|
||||
'progress' => 100,
|
||||
'message' => 'Backup complete: ' . $session->archiveName,
|
||||
'done' => true,
|
||||
'record_id' => $session->recordId,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -228,6 +229,7 @@ class SteppedBackupEngine
|
||||
'progress' => $session->getProgress(),
|
||||
'message' => $session->statusMessage,
|
||||
'done' => $session->phase === 'complete',
|
||||
'record_id' => $session->recordId,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
$session->log('FATAL: ' . $e->getMessage());
|
||||
|
||||
@@ -64,6 +64,7 @@ $config = [
|
||||
'description' => $this->description,
|
||||
'returnUrl' => $safeReturnUrl,
|
||||
'dashboardUrl' => $dashboardUrl,
|
||||
'recordUrl' => Route::_('index.php?option=com_mokosuitebackup&view=backup&id=__MSBID__', false),
|
||||
'autostart' => $this->autostart,
|
||||
'labels' => [
|
||||
'starting' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_STARTING'),
|
||||
@@ -74,6 +75,7 @@ $config = [
|
||||
'retry' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_RETRY'),
|
||||
'continue' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_ANYWAY'),
|
||||
'dashboard' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD'),
|
||||
'viewRecord' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD'),
|
||||
'leaveWarn' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING'),
|
||||
],
|
||||
];
|
||||
@@ -105,6 +107,7 @@ $config = [
|
||||
|
||||
<div id="msb-rb-actions" class="d-none">
|
||||
<button type="button" id="msb-rb-retry" class="btn btn-primary d-none"></button>
|
||||
<a href="#" id="msb-rb-record" class="btn btn-primary d-none"></a>
|
||||
<a href="#" id="msb-rb-continue" class="btn btn-warning d-none"></a>
|
||||
<a href="<?php echo $dashboardUrl; ?>" id="msb-rb-dashboard" class="btn btn-secondary d-none"></a>
|
||||
</div>
|
||||
@@ -119,6 +122,7 @@ $config = [
|
||||
var CFG = <?php echo json_encode($config); ?>;
|
||||
var L = CFG.labels || {};
|
||||
var running = false;
|
||||
var lastRecordId = 0;
|
||||
|
||||
var el = {
|
||||
bar: document.getElementById('msb-rb-bar'),
|
||||
@@ -127,6 +131,7 @@ $config = [
|
||||
title: document.getElementById('msb-rb-title'),
|
||||
actions: document.getElementById('msb-rb-actions'),
|
||||
retry: document.getElementById('msb-rb-retry'),
|
||||
record: document.getElementById('msb-rb-record'),
|
||||
continue: document.getElementById('msb-rb-continue'),
|
||||
dashboard: document.getElementById('msb-rb-dashboard')
|
||||
};
|
||||
@@ -161,18 +166,32 @@ $config = [
|
||||
node.classList.remove('d-none');
|
||||
}
|
||||
|
||||
function onComplete() {
|
||||
async function onComplete() {
|
||||
el.bar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
||||
el.bar.classList.add('bg-success');
|
||||
setBar(100, false);
|
||||
el.title.textContent = L.complete || 'Backup complete';
|
||||
|
||||
/* Arm the pre-action throttles so a following server-side onExtension
|
||||
Before(Update|Uninstall) backup is skipped (no duplicate backup).
|
||||
Awaited so it lands before we hand back to the update/uninstall. */
|
||||
try { await post({ task: 'ajax.preupdateAck' }); } catch (e) {}
|
||||
|
||||
if (CFG.returnUrl) {
|
||||
/* Pre-update / pre-uninstall flow: continue the original action.
|
||||
No "view record" button here — we're handing back to Joomla. */
|
||||
setStatus(L.continuing || 'Continuing…');
|
||||
window.setTimeout(function () { window.location.href = CFG.returnUrl; }, 1200);
|
||||
} else {
|
||||
/* Manual "Backup Now": offer to view the record that was just made. */
|
||||
setStatus(L.complete || 'Backup complete');
|
||||
showActions();
|
||||
|
||||
if (lastRecordId > 0 && CFG.recordUrl) {
|
||||
showBtn(el.record, L.viewRecord || 'View backup record',
|
||||
CFG.recordUrl.replace('__MSBID__', String(lastRecordId)));
|
||||
}
|
||||
|
||||
showBtn(el.dashboard, L.dashboard || 'Back to dashboard', CFG.dashboardUrl);
|
||||
}
|
||||
}
|
||||
@@ -219,6 +238,7 @@ $config = [
|
||||
setBar(init.progress, true);
|
||||
setPhase(init.phase || '');
|
||||
setStatus(init.message || (L.running || 'Backing up…'));
|
||||
if (init.record_id) { lastRecordId = init.record_id; }
|
||||
|
||||
var done = false;
|
||||
while (!done) {
|
||||
@@ -231,11 +251,12 @@ $config = [
|
||||
setBar(step.progress, true);
|
||||
setPhase(step.phase || '');
|
||||
setStatus(step.message || '');
|
||||
if (step.record_id) { lastRecordId = step.record_id; }
|
||||
done = step.done || false;
|
||||
}
|
||||
|
||||
running = false;
|
||||
onComplete();
|
||||
await onComplete();
|
||||
} catch (err) {
|
||||
onError(err && err.message ? err.message : String(err));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="module" client="administrator" method="upgrade">
|
||||
<name>Module - MokoSuiteBackup - cPanel</name>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="actionlog" method="upgrade">
|
||||
<name>Action Log - MokoSuiteBackup</name>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="console" method="upgrade">
|
||||
<name>Console - MokoSuiteBackup</name>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoSuiteBackup</name>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="quickicon" method="upgrade">
|
||||
<name>Quick Icon - MokoSuiteBackup</name>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* MokoSuiteBackup — full-screen backup before an EXTENSION update / uninstall.
|
||||
*
|
||||
* Injected by plg_system_mokosuitebackup on the Extensions → Update and
|
||||
* Extensions → Manage pages. Unlike the core Joomla update (which has a
|
||||
* server-side page load we can redirect), com_installer's update.update and
|
||||
* manage.remove are POST actions with a CSRF token and a checked cid[] list —
|
||||
* so we intercept CLIENT-SIDE: capture the selection, go to the full-screen
|
||||
* backup page, and on return restore the selection and re-submit the real POST
|
||||
* form (fresh token) so the update/uninstall proceeds.
|
||||
*
|
||||
* The full-screen page arms the pre-action throttle on success (ajax.preupdate
|
||||
* Ack), so the server-side onExtensionBefore(Update|Uninstall) backup is then
|
||||
* skipped — no duplicate backup.
|
||||
*
|
||||
* Config: Joomla.getOptions('plg_system_mokosuitebackup.installer').
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var cfg = (window.Joomla && Joomla.getOptions)
|
||||
? Joomla.getOptions('plg_system_mokosuitebackup.installer', null)
|
||||
: null;
|
||||
|
||||
if (!cfg || !cfg.runbackupUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
var STORAGE_KEY = 'msbInstallerBackup';
|
||||
|
||||
/* Toolbar tasks to intercept, gated by which feature is enabled. */
|
||||
var TASKS = {};
|
||||
if (cfg.backupBeforeUpdate) { TASKS['update.update'] = true; }
|
||||
if (cfg.backupBeforeUninstall) { TASKS['manage.remove'] = true; }
|
||||
|
||||
function checkedCids() {
|
||||
var form = document.getElementById('adminForm');
|
||||
var cids = [];
|
||||
|
||||
if (form) {
|
||||
form.querySelectorAll('input[name="cid[]"]:checked').forEach(function (b) {
|
||||
cids.push(b.value);
|
||||
});
|
||||
}
|
||||
|
||||
return cids;
|
||||
}
|
||||
|
||||
/* On return from the backup screen: restore the selection and re-submit. */
|
||||
function resumeIfNeeded() {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
|
||||
if (params.get('msb_resume') !== '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
var raw = null;
|
||||
try { raw = sessionStorage.getItem(STORAGE_KEY); } catch (e) {}
|
||||
try { sessionStorage.removeItem(STORAGE_KEY); } catch (e) {}
|
||||
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data;
|
||||
try { data = JSON.parse(raw); } catch (e) { data = null; }
|
||||
|
||||
if (!data || !data.task) {
|
||||
return;
|
||||
}
|
||||
|
||||
var form = document.getElementById('adminForm');
|
||||
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
(data.cids || []).forEach(function (id) {
|
||||
var box = form.querySelector('input[name="cid[]"][value="' + id + '"]');
|
||||
if (box) { box.checked = true; }
|
||||
});
|
||||
|
||||
/* Re-run the original toolbar action; the wrapper lets it through
|
||||
because the resume flag is set. */
|
||||
window.__msbResuming = true;
|
||||
|
||||
if (window.Joomla && typeof Joomla.submitbutton === 'function') {
|
||||
Joomla.submitbutton(data.task);
|
||||
} else if (typeof window.submitbutton === 'function') {
|
||||
window.submitbutton(data.task);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrap Joomla.submitbutton to intercept the update/uninstall tasks. */
|
||||
function wrapSubmit() {
|
||||
var J = window.Joomla;
|
||||
|
||||
if (!J || typeof J.submitbutton !== 'function' || J.submitbutton.__msbWrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
var original = J.submitbutton;
|
||||
|
||||
var wrapped = function (task) {
|
||||
/* Skip when resuming after a backup, when a backup already ran this
|
||||
session (throttle window), or for tasks we don't handle. */
|
||||
if (window.__msbResuming || cfg.recentBackup || !TASKS[task]) {
|
||||
return original.apply(J, arguments);
|
||||
}
|
||||
|
||||
var cids = checkedCids();
|
||||
|
||||
/* Nothing selected — let Joomla show its own "please select" notice
|
||||
and don't run a pointless backup. */
|
||||
if (!cids.length) {
|
||||
return original.apply(J, arguments);
|
||||
}
|
||||
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ task: task, cids: cids }));
|
||||
} catch (e) {}
|
||||
|
||||
var ret = window.location.href;
|
||||
ret += (ret.indexOf('?') === -1 ? '?' : '&') + 'msb_resume=1';
|
||||
|
||||
window.location.href = cfg.runbackupUrl
|
||||
+ '&profile_id=' + encodeURIComponent(cfg.profileId)
|
||||
+ '&returnurl=' + encodeURIComponent(btoa(ret));
|
||||
};
|
||||
|
||||
wrapped.__msbWrapped = true;
|
||||
J.submitbutton = wrapped;
|
||||
}
|
||||
|
||||
/* Wrap as early as possible, and again once the DOM/core JS is ready. */
|
||||
wrapSubmit();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
wrapSubmit();
|
||||
resumeIfNeeded();
|
||||
});
|
||||
})();
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteBackup</name>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -29,6 +29,10 @@
|
||||
<language tag="en-GB">language/en-GB/plg_system_mokosuitebackup.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
<media destination="plg_system_mokosuitebackup" folder="media">
|
||||
<folder>js</folder>
|
||||
</media>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic">
|
||||
|
||||
@@ -13,8 +13,10 @@ namespace Joomla\Plugin\System\MokoSuiteBackup\Extension;
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Document\HtmlDocument;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RetentionManager;
|
||||
@@ -31,6 +33,7 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
|
||||
return [
|
||||
'onAfterInitialise' => 'onAfterInitialise',
|
||||
'onAfterRoute' => 'onAfterRoute',
|
||||
'onBeforeCompileHead' => 'onBeforeCompileHead',
|
||||
'onExtensionBeforeUpdate' => 'onExtensionBeforeUpdate',
|
||||
'onExtensionBeforeUninstall' => 'onExtensionBeforeUninstall',
|
||||
];
|
||||
@@ -146,16 +149,101 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Joomla core-update install through the full-screen backup page
|
||||
* BEFORE the update runs (Akeeba-style), so no backup runs synchronously
|
||||
* inside the update request (which is what white-screened large sites).
|
||||
* Inject the client-side interceptor on the Extensions → Update and
|
||||
* Extensions → Manage pages, so an EXTENSION update / uninstall also runs
|
||||
* through the full-screen backup page.
|
||||
*
|
||||
* Flow: user clicks "Install the update" (com_joomlaupdate&task=update.install)
|
||||
* → we redirect to com_mokosuitebackup&view=runbackup (full screen) with a
|
||||
* returnurl back to update.install&is_backed_up=1 → the backup runs on its
|
||||
* own page with real progress → on completion the browser returns to
|
||||
* update.install, we arm the throttle so onExtensionBeforeUpdate doesn't
|
||||
* duplicate the backup, and the update proceeds.
|
||||
* Core Joomla updates are handled server-side (see maybeRedirectForUpdate),
|
||||
* but com_installer's update.update / manage.remove are POST actions with a
|
||||
* CSRF token and a checked cid[] list — a server-side GET redirect would drop
|
||||
* the selection and fail the token check. So the JS wraps Joomla.submitbutton,
|
||||
* stores the selection, sends the browser to view=runbackup, and on return
|
||||
* restores the selection and re-submits the real POST form.
|
||||
*/
|
||||
public function onBeforeCompileHead(Event $event): void
|
||||
{
|
||||
$app = $this->getApplication();
|
||||
|
||||
if (!$app->isClient('administrator')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$input = $app->getInput();
|
||||
|
||||
if ($input->getCmd('option', '') !== 'com_installer') {
|
||||
return;
|
||||
}
|
||||
|
||||
$view = $input->getCmd('view', '');
|
||||
|
||||
if ($view !== 'update' && $view !== 'manage') {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = ComponentHelper::getParams('com_mokosuitebackup');
|
||||
$beforeUpdate = (int) $params->get('backup_before_update', 0);
|
||||
$beforeUninst = (int) $params->get('backup_before_uninstall', 0);
|
||||
|
||||
// Relevant feature for this view must be on.
|
||||
$active = ($view === 'update' && $beforeUpdate) || ($view === 'manage' && $beforeUninst);
|
||||
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Super Users only.
|
||||
$user = $app->getIdentity();
|
||||
|
||||
if (!$user || $user->guest || !$user->authorise('core.admin')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$doc = $app->getDocument();
|
||||
|
||||
if (!$doc instanceof HtmlDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already backed up this session (throttle window)? Then the JS should
|
||||
// let the action proceed without a fresh backup.
|
||||
$session = Factory::getSession();
|
||||
$throttleK = $view === 'update'
|
||||
? 'mokosuitebackup.preaction_backup_before_update'
|
||||
: 'mokosuitebackup.preaction_backup_before_uninstall';
|
||||
$lastRun = (int) $session->get($throttleK, 0);
|
||||
$recent = $lastRun > 0 && (time() - $lastRun) < 600;
|
||||
|
||||
$doc->getWebAssetManager()->useScript('core');
|
||||
|
||||
$doc->addScriptOptions('plg_system_mokosuitebackup.installer', [
|
||||
'runbackupUrl' => Route::_('index.php?option=com_mokosuitebackup&view=runbackup&tmpl=component&autostart=1', false),
|
||||
'profileId' => (int) $params->get('default_profile', 1),
|
||||
'backupBeforeUpdate' => $view === 'update' && (bool) $beforeUpdate,
|
||||
'backupBeforeUninstall' => $view === 'manage' && (bool) $beforeUninst,
|
||||
'recentBackup' => $recent,
|
||||
]);
|
||||
|
||||
$doc->addScript(Uri::root(true) . '/media/plg_system_mokosuitebackup/js/installer-backup.js', [], ['defer' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Joomla core update through the full-screen backup page BEFORE the
|
||||
* update applies (Akeeba-style), so no backup runs synchronously inside the
|
||||
* update request (which is what white-screened large sites).
|
||||
*
|
||||
* Interception point by Joomla version: on Joomla 4/5/6 the "Install the
|
||||
* update" flow server-side-redirects to the updating page `view=update`,
|
||||
* which *then* runs the file extraction from JavaScript (an AJAX call to
|
||||
* com_joomlaupdate/extract.php). So the `view=update` page LOAD is the last
|
||||
* moment before any files change — we intercept that. (Older releases used
|
||||
* `task=update.install`.) In onAfterRoute — before com_joomlaupdate renders
|
||||
* the page — we redirect to view=runbackup with a returnurl back to the same
|
||||
* page flagged `is_backed_up=1`; the backup runs on its own full-screen page,
|
||||
* then the browser returns to `view=update`, whose JS extraction then runs.
|
||||
* On return we arm the throttle so onExtensionBeforeUpdate won't duplicate it.
|
||||
*
|
||||
* Note: `update.finalise` is deliberately NOT intercepted — by then the files
|
||||
* are already extracted, so it is too late for a pre-update backup.
|
||||
*/
|
||||
private function maybeRedirectForUpdate(): void
|
||||
{
|
||||
@@ -167,9 +255,13 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
|
||||
|
||||
$app = $this->getApplication();
|
||||
$input = $app->getInput();
|
||||
$view = $input->getCmd('view', '');
|
||||
$task = $input->getCmd('task', '');
|
||||
|
||||
// Joomla 4/5/6 updating page (before its JS extracts), or the legacy
|
||||
// Joomla 3 install task.
|
||||
if ($input->getCmd('option', '') !== 'com_joomlaupdate'
|
||||
|| $input->getCmd('task', '') !== 'update.install') {
|
||||
|| ($view !== 'update' && $task !== 'update.install')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -200,10 +292,18 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
|
||||
$token = Session::getFormToken();
|
||||
$profileId = (int) $params->get('default_profile', 1);
|
||||
|
||||
// Where to send the browser after the backup: back to the update install.
|
||||
// Where to send the browser after the backup: back to the same update
|
||||
// entry point that fired (the updating page, or the legacy install task),
|
||||
// flagged so this doesn't loop.
|
||||
$returnUri = new Uri(Uri::base() . 'index.php');
|
||||
$returnUri->setVar('option', 'com_joomlaupdate');
|
||||
$returnUri->setVar('task', 'update.install');
|
||||
|
||||
if ($view === 'update') {
|
||||
$returnUri->setVar('view', 'update');
|
||||
} else {
|
||||
$returnUri->setVar('task', $task);
|
||||
}
|
||||
|
||||
$returnUri->setVar('is_backed_up', '1');
|
||||
$returnUri->setVar($token, '1');
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteBackup</name>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteBackup</name>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteBackup</name>
|
||||
<packagename>mokosuitebackup</packagename>
|
||||
<version>02.58.17</version>
|
||||
<version>02.58.20</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user