Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a02b7d903 | |||
| d4fca16a13 | |||
| 90f08207ea | |||
| 3ef9bdaa48 | |||
| 7a7e804647 | |||
| 498f62e975 | |||
| f9c1c6b186 | |||
| d84ccfca31 | |||
| 8a428c6cbb | |||
| 4d476fde5d | |||
| 2c12fdc176 | |||
| 39495e5b12 | |||
| 44ed98d3c3 | |||
| 9aab873750 | |||
| 4bc8b47d05 | |||
| d6078ef528 | |||
| 3e4bb03165 | |||
| b894c95f6b | |||
| e3bd4a36ff | |||
| da9265e329 | |||
| b9c0ddf2b9 | |||
| 41e8bffd26 |
@@ -380,13 +380,10 @@ jobs:
|
||||
import sys
|
||||
version, date = sys.argv[1], sys.argv[2]
|
||||
content = open('CHANGELOG.md').read()
|
||||
marker = f'## [{version}]'
|
||||
# Idempotent: only promote when this version header isn't already present,
|
||||
# otherwise re-runs (or same-version builds) create empty duplicate headers.
|
||||
if marker not in content:
|
||||
new = f'## [Unreleased]\n\n{marker} --- {date}'
|
||||
content = content.replace('## [Unreleased]', new, 1)
|
||||
open('CHANGELOG.md', 'w').write(content)
|
||||
old = '## [Unreleased]'
|
||||
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
||||
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1)
|
||||
open('CHANGELOG.md', 'w').write(content)
|
||||
" "$VERSION" "$DATE"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
|
||||
|
||||
@@ -131,10 +131,11 @@ jobs:
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
# Run only when lint succeeded; always() forces evaluation so a skipped
|
||||
# lint (e.g. template repos) skips this job cleanly instead of hanging.
|
||||
if: ${{ always() && needs.lint.result == 'success' }}
|
||||
# Independent job (no `needs: lint`): the Gitea Actions scheduler does not
|
||||
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
|
||||
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
|
||||
# directly (same condition lint uses) instead of gating on lint's result.
|
||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 02.58.28
|
||||
# VERSION: 02.59.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found in source files"
|
||||
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/pre-release.yml
|
||||
# VERSION: 05.02.00
|
||||
# VERSION: 05.02.01
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
@@ -162,7 +162,13 @@ jobs:
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1
|
||||
# Push the bump commit, but do NOT fail the release if the target branch
|
||||
# is protected and the release identity is not on the push allowlist.
|
||||
# The build proceeds from the in-tree bumped version regardless; if the
|
||||
# push is rejected, the next run simply re-bumps from the same base.
|
||||
if ! git push origin HEAD 2>&1; then
|
||||
echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway."
|
||||
fi
|
||||
}
|
||||
|
||||
# Auto-detect element via manifest_element.php
|
||||
@@ -274,4 +280,4 @@ jobs:
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
+3
-16
@@ -1,7 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [02.59.00] --- 2026-07-12
|
||||
|
||||
### Fixed
|
||||
- Pre-update backup on the **Joomla Update** page is now **seamless** (Akeeba-style): after the full-screen backup runs, the plugin auto-ticks Joomla's "I have taken a backup" checkbox and clicks Install, so the update continues automatically instead of stopping for a second manual click.
|
||||
- Pre-update full-screen backup now actually fires on the **Joomla Update** page. The "Install the update" button posts to `option=com_joomlaupdate&layout=update` (Joomla's confirm/updating page) — carrying **only `layout=update`**, no `view`/`task` — so the previous match on `view=update`/`update.install` never triggered. The plugin now intercepts `layout=update` (as well as `view=update` and the legacy `update.install`) and returns the browser there flagged `is_backed_up=1`.
|
||||
@@ -64,18 +66,3 @@
|
||||
|
||||
### Fixed
|
||||
- Component manifest `<name>` reverted to the element-safe `MokoSuiteBackup`. The "Type - Name" convention derived element `com_component-mokosuitebackup`, registering a duplicate component and leaving the real `com_mokosuitebackup` orphaned on the old version. (#213)
|
||||
|
||||
## [02.56.07] --- 2026-07-05
|
||||
|
||||
### Security
|
||||
- Mask the FTP password in `AjaxController::maskSecrets()`/`mergeExistingSecrets()` — stored FTP remotes no longer leak their password via `listRemotes`. (#169)
|
||||
- Stop the API single-item view (`Backups/JsonapiView`) leaking the server `absolute_path`. (#187)
|
||||
- SFTP uploader uses `StrictHostKeyChecking=accept-new` instead of `no`. (#182)
|
||||
|
||||
### Fixed
|
||||
- `DatabaseDumper` writes a real newline after `DROP TABLE` (was a literal `\n`). (#179)
|
||||
- Profile-picker forms order by `id` instead of the dropped `ordering` column. (#180)
|
||||
- `uninstall.mysql.sql` now drops the snapshots table. (#181)
|
||||
- CLI snapshot delete uses the `data_file` column (was non-existent `file_path`). (#184)
|
||||
- Remove the duplicate pre-update backup (content plugin no longer subscribes to `onExtensionBeforeUpdate`). (#186)
|
||||
- `DatabaseImporter` collects non-fatal statement errors (`getErrors()`/`hasErrors()`). (#188)
|
||||
|
||||
+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.28
|
||||
VERSION: 02.59.00
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
display label there.
|
||||
-->
|
||||
<name>MokoSuiteBackup</name>
|
||||
<version>02.58.28</version>
|
||||
<version>02.59.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/* 02.58.28 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.29 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.30 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.31 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.32 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.35 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.36 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.37 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.59.00 — no schema changes */
|
||||
@@ -125,11 +125,23 @@ class SnapshotRestoreEngine
|
||||
$prefix = $db->getPrefix();
|
||||
$totalRows = 0;
|
||||
|
||||
try {
|
||||
$db->transactionStart();
|
||||
// Build list of tables to restore based on selected types
|
||||
$tablesToRestore = $this->getTablesToRestore($restoreTypes);
|
||||
|
||||
// Build list of tables to restore based on selected types
|
||||
$tablesToRestore = $this->getTablesToRestore($restoreTypes);
|
||||
/* Duplicate mode inserts brand-new records through Joomla's Table API,
|
||||
whose Nested tables (categories, tags) issue LOCK TABLES — which
|
||||
implicitly COMMITS any open transaction in MySQL/InnoDB. Wrapping it
|
||||
in transactionStart()/Rollback() would be a false atomicity promise:
|
||||
the first category/tag store would commit and defeat the rollback.
|
||||
Duplicate mode is therefore best-effort and per-item defensive (a bad
|
||||
item is skipped and logged, never fatal) rather than all-or-nothing.
|
||||
The raw-DB modes (replace/create/merge) remain fully transactional. */
|
||||
$useTransaction = ($mode !== 'duplicate');
|
||||
|
||||
try {
|
||||
if ($useTransaction) {
|
||||
$db->transactionStart();
|
||||
}
|
||||
|
||||
if ($mode === 'duplicate') {
|
||||
$totalRows = $this->restoreDuplicate($data, $restoreTypes);
|
||||
@@ -156,7 +168,9 @@ class SnapshotRestoreEngine
|
||||
}
|
||||
}
|
||||
|
||||
$db->transactionCommit();
|
||||
if ($useTransaction) {
|
||||
$db->transactionCommit();
|
||||
}
|
||||
|
||||
$this->log('Restore complete: ' . $totalRows . ' total rows');
|
||||
|
||||
@@ -188,11 +202,15 @@ class SnapshotRestoreEngine
|
||||
'log' => implode("\n", $this->log),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
$db->transactionRollback();
|
||||
$this->log('Transaction rolled back');
|
||||
} catch (\Exception $rollbackEx) {
|
||||
$this->log('Rollback failed: ' . $rollbackEx->getMessage());
|
||||
if ($useTransaction) {
|
||||
try {
|
||||
$db->transactionRollback();
|
||||
$this->log('Transaction rolled back');
|
||||
} catch (\Exception $rollbackEx) {
|
||||
$this->log('Rollback failed: ' . $rollbackEx->getMessage());
|
||||
}
|
||||
} else {
|
||||
$this->log('Duplicate mode is not transactional; records already inserted are left in place.');
|
||||
}
|
||||
|
||||
$this->log('FATAL: ' . $e->getMessage());
|
||||
|
||||
@@ -34,6 +34,14 @@ if ($this->returnUrl !== '') {
|
||||
$decoded = base64_decode($this->returnUrl, true);
|
||||
$raw = ($decoded !== false && $decoded !== '') ? $decoded : $this->returnUrl;
|
||||
|
||||
/* Reject any backslash or control char outright: legitimate Joomla admin
|
||||
return URLs never contain them, and a browser normalises a leading "/\"
|
||||
to "//", turning a would-be root-relative path into a protocol-relative
|
||||
open redirect (e.g. "/\evil.com" → "//evil.com"). */
|
||||
if (preg_match('/[\x00-\x1f\x7f\\\\]/', $raw)) {
|
||||
$raw = '';
|
||||
}
|
||||
|
||||
$isAbsoluteHttp = (bool) preg_match('#^https?://#i', $raw);
|
||||
$isRootRelative = isset($raw[0]) && $raw[0] === '/' && (!isset($raw[1]) || $raw[1] !== '/');
|
||||
|
||||
@@ -80,9 +88,55 @@ $config = [
|
||||
],
|
||||
];
|
||||
?>
|
||||
<div class="msb-runbackup" style="max-width:640px;margin:3rem auto;padding:0 1rem;">
|
||||
<style>
|
||||
/* Colours are pulled from the Atum admin template's own CSS custom
|
||||
properties so the screen matches whatever theme the site runs, with
|
||||
Bootstrap and then a literal as progressive fallbacks. */
|
||||
.msb-runbackup {
|
||||
--msb-accent: var(--template-link-color, var(--bs-link-color, #2a69b8));
|
||||
--msb-accent2: var(--template-special-color, var(--msb-accent));
|
||||
--msb-success: var(--template-success-color, var(--bs-success, #2f7d32));
|
||||
--msb-danger: var(--template-danger-color, var(--bs-danger, #c52128));
|
||||
--msb-track: var(--template-bg-light, var(--bs-gray-200, #edf0f5));
|
||||
padding: 0 1rem;
|
||||
}
|
||||
/* overflow:hidden clips the full-bleed bar to the card's rounded corners. */
|
||||
.msb-runbackup .card { overflow: hidden; }
|
||||
.msb-rb-progress {
|
||||
height: 2.5rem;
|
||||
background: var(--msb-track);
|
||||
overflow: hidden;
|
||||
}
|
||||
.msb-rb-bar {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: .95rem;
|
||||
white-space: nowrap;
|
||||
background-color: var(--msb-accent);
|
||||
background-image: linear-gradient(90deg, var(--msb-accent), var(--msb-accent2));
|
||||
transition: width .35s ease;
|
||||
}
|
||||
.msb-rb-bar.is-animated::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: linear-gradient(45deg, rgba(255,255,255,.18) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.18) 50%, rgba(255,255,255,.18) 75%, transparent 75%, transparent);
|
||||
background-size: 1rem 1rem;
|
||||
animation: msb-rb-stripes 1s linear infinite;
|
||||
}
|
||||
.msb-rb-bar { position: relative; }
|
||||
.msb-rb-bar.is-success { background-image: none; background-color: var(--msb-success); }
|
||||
.msb-rb-bar.is-danger { background-image: none; background-color: var(--msb-danger); }
|
||||
@keyframes msb-rb-stripes { from { background-position: 1rem 0; } to { background-position: 0 0; } }
|
||||
</style>
|
||||
<div class="msb-runbackup">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="card-body p-4 pb-3">
|
||||
<h1 class="h4 mb-1" id="msb-rb-title">
|
||||
<span class="icon-archive" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_TITLE'); ?>
|
||||
@@ -97,21 +151,21 @@ $config = [
|
||||
); ?>
|
||||
</p>
|
||||
|
||||
<div class="progress mb-3" style="height:1.75rem;">
|
||||
<div id="msb-rb-bar" class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width:0;" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">0%</div>
|
||||
</div>
|
||||
|
||||
<div id="msb-rb-phase" class="fw-bold mb-1"></div>
|
||||
<div id="msb-rb-status" class="text-muted small mb-3"><?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_STARTING'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<div id="msb-rb-status" class="text-muted small mb-0"><?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_STARTING'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
|
||||
<div id="msb-rb-actions" class="d-none">
|
||||
<div id="msb-rb-actions" class="d-none mt-3">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Full-bleed progress bar, edge-to-edge along the bottom of the card. -->
|
||||
<div class="msb-rb-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
|
||||
<div id="msb-rb-bar" class="msb-rb-bar is-animated" style="width:0;">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,6 +180,7 @@ $config = [
|
||||
|
||||
var el = {
|
||||
bar: document.getElementById('msb-rb-bar'),
|
||||
track: document.querySelector('.msb-rb-progress'),
|
||||
phase: document.getElementById('msb-rb-phase'),
|
||||
status: document.getElementById('msb-rb-status'),
|
||||
title: document.getElementById('msb-rb-title'),
|
||||
@@ -140,9 +195,9 @@ $config = [
|
||||
pct = Math.max(0, Math.min(100, parseInt(pct, 10) || 0));
|
||||
el.bar.style.width = pct + '%';
|
||||
el.bar.textContent = pct + '%';
|
||||
el.bar.setAttribute('aria-valuenow', pct);
|
||||
if (el.track) { el.track.setAttribute('aria-valuenow', pct); }
|
||||
if (!striped) {
|
||||
el.bar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
||||
el.bar.classList.remove('is-animated');
|
||||
}
|
||||
}
|
||||
function setPhase(t) { el.phase.textContent = t || ''; }
|
||||
@@ -167,8 +222,8 @@ $config = [
|
||||
}
|
||||
|
||||
async function onComplete() {
|
||||
el.bar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
||||
el.bar.classList.add('bg-success');
|
||||
el.bar.classList.remove('is-animated');
|
||||
el.bar.classList.add('is-success');
|
||||
setBar(100, false);
|
||||
el.title.textContent = L.complete || 'Backup complete';
|
||||
|
||||
@@ -198,8 +253,8 @@ $config = [
|
||||
|
||||
function onError(message) {
|
||||
running = false;
|
||||
el.bar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
||||
el.bar.classList.add('bg-danger');
|
||||
el.bar.classList.remove('is-animated');
|
||||
el.bar.classList.add('is-danger');
|
||||
el.title.textContent = L.failed || 'Backup failed';
|
||||
setStatus(message || 'Backup failed');
|
||||
showActions();
|
||||
@@ -209,8 +264,8 @@ $config = [
|
||||
el.retry.onclick = function () {
|
||||
el.retry.classList.add('d-none');
|
||||
el.continue.classList.add('d-none');
|
||||
el.bar.classList.remove('bg-danger');
|
||||
el.bar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||
el.bar.classList.remove('is-danger');
|
||||
el.bar.classList.add('is-animated');
|
||||
run();
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="module" client="administrator" method="upgrade">
|
||||
<name>Module - MokoSuiteBackup - cPanel</name>
|
||||
<version>02.58.28</version>
|
||||
<version>02.59.00</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.28</version>
|
||||
<version>02.59.00</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.28</version>
|
||||
<version>02.59.00</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.28</version>
|
||||
<version>02.59.00</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.28</version>
|
||||
<version>02.59.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/**
|
||||
* 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.
|
||||
* Injected by plg_system_mokosuitebackup on Extensions → Update and
|
||||
* Extensions → Manage. Joomla 6 renders those toolbar buttons as web
|
||||
* components (`<button task="update.update">`), NOT inline
|
||||
* onclick="Joomla.submitbutton(...)", so wrapping Joomla.submitbutton is
|
||||
* unreliable. Instead we add a CAPTURE-phase click listener on any element
|
||||
* carrying a matching `task` attribute — it runs before the toolbar web
|
||||
* component, so we can stop it, run the full-screen backup, and on return
|
||||
* restore the checked cid[] selection and re-fire the button.
|
||||
*
|
||||
* 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.
|
||||
* The full-screen page arms the pre-action throttle on success, so the
|
||||
* server-side onExtensionBefore(Update|Uninstall) backup is then skipped.
|
||||
*
|
||||
* Config: Joomla.getOptions('plg_system_mokosuitebackup.installer').
|
||||
*/
|
||||
@@ -28,26 +28,76 @@
|
||||
|
||||
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; }
|
||||
if (cfg.backupBeforeUninstall) { TASKS['manage.remove'] = true; TASKS['manage.uninstall'] = 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);
|
||||
});
|
||||
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() {
|
||||
/* Intercept the toolbar Update/Uninstall click in the CAPTURE phase, before
|
||||
the toolbar web component's own handler runs. */
|
||||
document.addEventListener('click', function (e) {
|
||||
if (window.__msbResuming || cfg.recentBackup) {
|
||||
return;
|
||||
}
|
||||
|
||||
var node = e.target;
|
||||
var btn = (node && node.closest) ? node.closest('[task]') : null;
|
||||
|
||||
if (!btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
var task = btn.getAttribute('task');
|
||||
|
||||
if (!TASKS[task]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cids = checkedCids();
|
||||
|
||||
/* Nothing selected — let Joomla show its own "please select" notice. */
|
||||
if (!cids.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ret = window.location.href;
|
||||
ret += (ret.indexOf('?') === -1 ? '?' : '&') + 'msb_resume=1';
|
||||
|
||||
/* Build the redirect BEFORE we swallow the click. btoa() throws on
|
||||
non-Latin1 input (e.g. a UTF-8 filter value in the query string), so
|
||||
encode as UTF-8 first; if it still fails, bail WITHOUT preventing the
|
||||
click so Joomla's own update/uninstall proceeds normally. */
|
||||
var target;
|
||||
try {
|
||||
target = cfg.runbackupUrl
|
||||
+ '&profile_id=' + encodeURIComponent(cfg.profileId)
|
||||
+ '&returnurl=' + encodeURIComponent(btoa(unescape(encodeURIComponent(ret))));
|
||||
} catch (x) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ task: task, cids: cids }));
|
||||
} catch (x) {}
|
||||
|
||||
window.location.href = target;
|
||||
}, true);
|
||||
|
||||
/* On return from the backup screen: restore the selection and re-fire. */
|
||||
function resume() {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
|
||||
if (params.get('msb_resume') !== '1') {
|
||||
@@ -55,15 +105,14 @@
|
||||
}
|
||||
|
||||
var raw = null;
|
||||
try { raw = sessionStorage.getItem(STORAGE_KEY); } catch (e) {}
|
||||
try { sessionStorage.removeItem(STORAGE_KEY); } catch (e) {}
|
||||
try { raw = sessionStorage.getItem(STORAGE_KEY); sessionStorage.removeItem(STORAGE_KEY); } catch (x) {}
|
||||
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data;
|
||||
try { data = JSON.parse(raw); } catch (e) { data = null; }
|
||||
try { data = JSON.parse(raw); } catch (x) { data = null; }
|
||||
|
||||
if (!data || !data.task) {
|
||||
return;
|
||||
@@ -71,72 +120,31 @@
|
||||
|
||||
var form = document.getElementById('adminForm');
|
||||
|
||||
if (!form) {
|
||||
return;
|
||||
if (form) {
|
||||
(data.cids || []).forEach(function (id) {
|
||||
var box = form.querySelector('input[name="cid[]"][value="' + id + '"]');
|
||||
if (box) { box.checked = true; }
|
||||
});
|
||||
|
||||
/* Joomla's list-check reads boxchecked — set it so the re-fire passes. */
|
||||
var bc = form.querySelector('input[name="boxchecked"]');
|
||||
if (bc) { bc.value = (data.cids || []).length; }
|
||||
}
|
||||
|
||||
(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') {
|
||||
var btn = document.querySelector('[task="' + data.task + '"]');
|
||||
|
||||
if (btn) {
|
||||
window.setTimeout(function () { btn.click(); }, 300);
|
||||
} else 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;
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', resume);
|
||||
} else {
|
||||
resume();
|
||||
}
|
||||
|
||||
/* 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.28</version>
|
||||
<version>02.59.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteBackup</name>
|
||||
<version>02.58.28</version>
|
||||
<version>02.59.00</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.28</version>
|
||||
<version>02.59.00</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.28</version>
|
||||
<version>02.59.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user