Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8df658c5da | |||
| 6c79443be0 | |||
| 78d120d428 | |||
| aa2473e4ac | |||
| 78556d2775 | |||
| 46dabae61c | |||
| e21574a917 | |||
| 79a914f930 | |||
| fe90f0024b | |||
| e9eb94ccfd | |||
| 7e1a76a7cb | |||
| c48fd3b8eb | |||
| f5423bab5f | |||
| 4b17842496 | |||
| 85c0967e89 | |||
| d89812cbfb | |||
| 79efde483e | |||
| c567890bfa | |||
| ba070f004d | |||
| ce15cecf85 | |||
| 2b4af3ce28 | |||
| a1cba42f43 | |||
| 9eef484585 | |||
| ff14a1dcba | |||
| 925b27f2dc | |||
| 4e519b4e7c | |||
| 118c1cbe2f | |||
| 05aec9b315 | |||
| b345e42cd0 | |||
| f99e39efa3 | |||
| ae5e1f48dd | |||
| 2ddd6c27cd | |||
| 362e75a9a8 | |||
| cb4c227853 | |||
| b8e8487f6b | |||
| 7abe3ef035 | |||
| 344881809d | |||
| 47d4023108 | |||
| 373662b0d3 | |||
| c7ea43edcf | |||
| dbdb3a0e8a | |||
| 1127b23d1e | |||
| 1c41cdc03b | |||
| 79b23215eb | |||
| bd2effd89c | |||
| 1c646332e2 | |||
| 2deaa4faa4 | |||
| 629b8be470 | |||
| d2dd8a8da4 | |||
| ba90ce3365 | |||
| 2de8ab942b | |||
| 3ccad868bf | |||
| 4606f21a05 | |||
| aae23b46f0 | |||
| 08135beaf0 | |||
| 2cd83e0369 | |||
| a52ed25325 | |||
| 3c00fc2422 | |||
| c08015c324 | |||
| 90aa2cd58b | |||
| f17367456a | |||
| 92c4818410 | |||
| a5db8fba5b | |||
| a133d80f5f | |||
| 6da7ef0b69 | |||
| 65cf9fbf9f | |||
| 29e5a8c85b | |||
| cbc8627fb7 | |||
| f1dfc18f3d | |||
| 17ff3ce702 | |||
| 20fcd1c39c | |||
| 3fb82bc03c | |||
| 499436a8d0 | |||
| a004b9c0d9 | |||
| 5b1cf9ea8e | |||
| 86fe470158 | |||
| a6e196ffd0 | |||
| d43b9405c0 | |||
| 8506573898 | |||
| 6ad5046f82 | |||
| 20a760097b | |||
| b378794a05 | |||
| dc23f4dd5e | |||
| 9a5f4dbbb2 | |||
| 45066237b0 | |||
| 2296c2f754 | |||
| 7416c30925 | |||
| 186a75681a | |||
| 0a60436f3d | |||
| a2362d5400 | |||
| 6f1d270818 | |||
| 7aa1f2eb43 | |||
| 4339c223a1 | |||
| cb56430e37 | |||
| 67611604e2 | |||
| 00c1add0cd | |||
| 1e3f963beb | |||
| c280229b90 | |||
| fbf0a2de76 | |||
| 30b8e66eca | |||
| 83b261db02 | |||
| e0285f112f | |||
| f5538ade88 | |||
| 2d27385ac9 | |||
| a3eb713d81 | |||
| 7106ece393 | |||
| 394ad0d607 | |||
| f5bcc7c457 | |||
| 69c70d4480 | |||
| 89f1869120 | |||
| 0c845fe807 | |||
| a60f7ac032 | |||
| f62b1ec72e | |||
| 0562d3effd | |||
| 1c5c511c06 | |||
| 444cfa3fdb | |||
| 1e6d9ba078 | |||
| 9ee800b151 | |||
| 25991c474b | |||
| 059cbfe058 | |||
| d658019007 | |||
| 8237f85056 | |||
| 14cc00f93f | |||
| d1ccb8d419 | |||
| 8a783aa25a | |||
| d9d6ee9181 | |||
| f02c4d4c9d | |||
| 2df4b280eb | |||
| 3eb2562abd | |||
| 9a02b7d903 | |||
| d4fca16a13 | |||
| 90f08207ea | |||
| 3ef9bdaa48 | |||
| 7a7e804647 | |||
| 498f62e975 | |||
| f9c1c6b186 | |||
| d84ccfca31 | |||
| 8a428c6cbb | |||
| 4d476fde5d | |||
| 2c12fdc176 | |||
| 39495e5b12 | |||
| 44ed98d3c3 | |||
| 9aab873750 | |||
| 4bc8b47d05 | |||
| d6078ef528 | |||
| 3e4bb03165 | |||
| b894c95f6b | |||
| e3bd4a36ff | |||
| da9265e329 | |||
| b9c0ddf2b9 | |||
| 41e8bffd26 |
@@ -7,7 +7,7 @@ Full-site backup and restore for Joomla — database, files, and configuration.
|
|||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Package** | `pkg_mokosuitebackup` |
|
| **Package** | `pkg_mokosuitebackup` |
|
||||||
| **Language** | PHP 8.1+ |
|
| **Language** | PHP 8.3+ |
|
||||||
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
| **Wiki** | [MokoSuiteBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/wiki) |
|
| **Wiki** | [MokoSuiteBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/wiki) |
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ Joomla **package** with four sub-extensions:
|
|||||||
### com_mokosuitebackup (Component)
|
### com_mokosuitebackup (Component)
|
||||||
- Admin backend for managing backup profiles and records
|
- Admin backend for managing backup profiles and records
|
||||||
- Backup engine: `Engine/BackupEngine`, `Engine/DatabaseDumper`, `Engine/FileScanner`, `Engine/Archiver`
|
- Backup engine: `Engine/BackupEngine`, `Engine/DatabaseDumper`, `Engine/FileScanner`, `Engine/Archiver`
|
||||||
- Joomla 4/5 MVC: Controllers, Models, Views, Tables
|
- Joomla 6 MVC: Controllers, Models, Views, Tables
|
||||||
- Namespace: `Joomla\Component\MokoSuiteBackup\Administrator`
|
- Namespace: `Joomla\Component\MokoSuiteBackup\Administrator`
|
||||||
- DB tables: `#__mokosuitebackup_profiles`, `#__mokosuitebackup_records`
|
- DB tables: `#__mokosuitebackup_profiles`, `#__mokosuitebackup_records`
|
||||||
- CLI: `cli/mokosuitebackup.php` for cron-based backups
|
- CLI: `cli/mokosuitebackup.php` for cron-based backups
|
||||||
@@ -64,8 +64,8 @@ Joomla **package** with four sub-extensions:
|
|||||||
|
|
||||||
## Coding Standards
|
## Coding Standards
|
||||||
|
|
||||||
- PHP 8.1+ minimum
|
- PHP 8.3+ minimum (Joomla 6)
|
||||||
- Joomla 4/5 DI container pattern: `services/provider.php` → Extension class
|
- Joomla 6 DI container pattern: `services/provider.php` → Extension class
|
||||||
- Legacy stub `.php` file required for plugin loader but empty
|
- Legacy stub `.php` file required for plugin loader but empty
|
||||||
- `SubscriberInterface` for event subscription (not `on*` method naming)
|
- `SubscriberInterface` for event subscription (not `on*` method naming)
|
||||||
- `bind() → check() → store()` for Table operations (not `save()`)
|
- `bind() → check() → store()` for Table operations (not `save()`)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: MokoCLI.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
# VERSION: 09.02.00
|
# VERSION: 09.02.00
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
@@ -34,8 +34,7 @@ jobs:
|
|||||||
if: >-
|
if: >-
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||||
!startsWith(github.event.head_commit.message, 'Merge pull request') &&
|
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||||
!startsWith(github.event.repository.name, 'Template-')
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -44,7 +43,7 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup MokoCLI tools
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if ! command -v composer &> /dev/null; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
@@ -53,7 +52,7 @@ jobs:
|
|||||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" \
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||||
/tmp/mokocli
|
/tmp/mokocli
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: MokoCLI.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/auto-release.yml
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from MokoGitea repo metadata (API)
|
||||||
#
|
#
|
||||||
# +=======================================================================+
|
# +=======================================================================+
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
# +=======================================================================+
|
# +=======================================================================+
|
||||||
# | |
|
# | |
|
||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads MokoGitea repo metadata (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, type-prefixed packages |
|
# | joomla: XML manifest, type-prefixed packages |
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup MokoCLI tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokocli
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokocli
|
cd /tmp/mokocli
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
@@ -104,39 +104,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
php ${MOKO_CLI}/branch_rename.php \
|
||||||
AUTH="Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}"
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
FROM="${{ github.event.pull_request.head.ref || 'dev' }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
PR="${{ github.event.pull_request.number }}"
|
--api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
|
--pr "${{ github.event.pull_request.number }}"
|
||||||
# Resolve the source branch HEAD commit.
|
|
||||||
SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \
|
|
||||||
|| { echo "::error::Source branch ${FROM} not found"; exit 1; }
|
|
||||||
SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
|
|
||||||
[ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
|
|
||||||
|
|
||||||
# Point rc at the source commit. If rc already exists (a protected branch that
|
|
||||||
# cannot be deleted), force-update its ref in place instead of delete+recreate:
|
|
||||||
# deleting a protected branch fails, which then makes the recreate return HTTP 409.
|
|
||||||
if curl -sf -o /dev/null -H "$AUTH" "${API_BASE}/branches/rc"; then
|
|
||||||
echo "rc exists - force-updating to ${FROM} (${SRC_SHA})"
|
|
||||||
curl -sf -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/git/refs/heads/rc" -d "{\"sha\":\"${SRC_SHA}\",\"force\":true}" \
|
|
||||||
|| { echo "::error::Failed to force-update rc (CI token needs force-push on the protected rc branch)"; exit 1; }
|
|
||||||
else
|
|
||||||
echo "Creating rc from ${FROM}"
|
|
||||||
curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/branches" -d "{\"new_branch_name\":\"rc\",\"old_branch_name\":\"${FROM}\"}" \
|
|
||||||
|| { echo "::error::Failed to create rc from ${FROM}"; exit 1; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Repoint the PR at rc, then delete the old source branch (non-fatal).
|
|
||||||
if [ -n "$PR" ]; then
|
|
||||||
curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true
|
|
||||||
fi
|
|
||||||
curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true
|
|
||||||
echo "Renamed ${FROM} -> rc"
|
|
||||||
|
|
||||||
- name: Checkout rc and configure git
|
- name: Checkout rc and configure git
|
||||||
run: |
|
run: |
|
||||||
@@ -231,7 +203,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "No conflict markers found"
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup MokoCLI tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
@@ -246,7 +218,7 @@ jobs:
|
|||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokocli
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokocli
|
cd /tmp/mokocli
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
@@ -380,12 +352,9 @@ jobs:
|
|||||||
import sys
|
import sys
|
||||||
version, date = sys.argv[1], sys.argv[2]
|
version, date = sys.argv[1], sys.argv[2]
|
||||||
content = open('CHANGELOG.md').read()
|
content = open('CHANGELOG.md').read()
|
||||||
marker = f'## [{version}]'
|
old = '## [Unreleased]'
|
||||||
# Idempotent: only promote when this version header isn't already present,
|
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
||||||
# otherwise re-runs (or same-version builds) create empty duplicate headers.
|
content = content.replace(old, new, 1)
|
||||||
if marker not in content:
|
|
||||||
new = f'## [Unreleased]\n\n{marker} --- {date}'
|
|
||||||
content = content.replace('## [Unreleased]', new, 1)
|
|
||||||
open('CHANGELOG.md', 'w').write(content)
|
open('CHANGELOG.md', 'w').write(content)
|
||||||
" "$VERSION" "$DATE"
|
" "$VERSION" "$DATE"
|
||||||
git add CHANGELOG.md
|
git add CHANGELOG.md
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Universal
|
# INGROUP: MokoCLI.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Delete feature branches after PR merge
|
# BRIEF: Delete feature branches after PR merge
|
||||||
|
|||||||
@@ -1,106 +1,65 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Cascade
|
# INGROUP: MokoCLI.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11.
|
||||||
# PATH: /.mokogitea/workflows/cascade-dev.yml
|
#
|
||||||
# VERSION: 02.00.00
|
# +========================================================================+
|
||||||
# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify
|
# | CASCADE MAIN -> DEV |
|
||||||
|
# +========================================================================+
|
||||||
|
# | dev mirrors main and is reset on every release. |
|
||||||
|
# | delete+recreate cannot run against a protected branch, so this |
|
||||||
|
# | force-pushes main -> dev. The automation identity is force-push |
|
||||||
|
# | allowlisted on dev via branch-protection.yml. |
|
||||||
|
# | Runs AFTER the release workflow completes, so dev picks up the |
|
||||||
|
# | fully version-bumped + changelog-promoted main (not the pre-bump |
|
||||||
|
# | state). Force-push (not merge) => no version-file conflicts. |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
name: "Cascade Main -> Dev"
|
name: "Cascade Main -> Dev"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_run:
|
||||||
branches:
|
workflows: ["Universal: Build & Release"]
|
||||||
- main
|
types: [completed]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: cascade-dev-${{ github.repository }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
# ntfy destination is configured via repo or org variables (org vars are inherited).
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'gitea-releases' }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cascade:
|
cascade:
|
||||||
name: Cascade main -> dev
|
name: Reset dev to main
|
||||||
|
if: >-
|
||||||
|
!startsWith(github.event.repository.name, 'Template-') &&
|
||||||
|
(github.event_name == 'workflow_dispatch' ||
|
||||||
|
github.event.workflow_run.conclusion == 'success')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Open main -> dev PR (auto-merge if clean, else notify)
|
- name: Force dev to main
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
run: |
|
run: |
|
||||||
set -uo pipefail
|
set -euo pipefail
|
||||||
API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
|
git init -q sync && cd sync
|
||||||
AUTH="Authorization: token ${TOKEN}"
|
git config user.email "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||||
jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
|
git config user.name "mokogitea-actions[bot]"
|
||||||
|
git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git"
|
||||||
|
git fetch -q --depth=1 origin main
|
||||||
|
|
||||||
# 0. dev must exist
|
if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
|
||||||
if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
|
# dev exists -> force it to match main (dev is a mirror of main)
|
||||||
echo "No dev branch - nothing to cascade."; exit 0
|
git push --force origin FETCH_HEAD:refs/heads/dev
|
||||||
fi
|
echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
# 1. is main ahead of dev?
|
|
||||||
AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \
|
|
||||||
| python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0)
|
|
||||||
if [ "${AHEAD:-0}" -eq 0 ]; then
|
|
||||||
echo "dev already up to date with main."; exit 0
|
|
||||||
fi
|
|
||||||
echo "main is ${AHEAD} commit(s) ahead of dev."
|
|
||||||
|
|
||||||
# 2. reuse an open main->dev PR, else create one
|
|
||||||
PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \
|
|
||||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "")
|
|
||||||
if [ -z "$PR" ]; then
|
|
||||||
RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
|
|
||||||
-d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}')
|
|
||||||
PR=$(printf '%s' "$RESP" | jqnum number)
|
|
||||||
if [ -z "$PR" ]; then
|
|
||||||
echo "::warning::Could not open cascade PR: $RESP"; exit 0
|
|
||||||
fi
|
|
||||||
echo "Opened cascade PR #${PR}"
|
|
||||||
else
|
else
|
||||||
echo "Reusing open cascade PR #${PR}"
|
# dev missing (e.g. renamed to rc mid-cycle) -> recreate from main
|
||||||
fi
|
git push origin FETCH_HEAD:refs/heads/dev
|
||||||
|
echo "dev created from main" >> "$GITHUB_STEP_SUMMARY"
|
||||||
# 3. wait for MokoGitea to compute mergeability (conflict detection)
|
|
||||||
MERGEABLE=""
|
|
||||||
for _ in 1 2 3 4 5 6; do
|
|
||||||
MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqnum mergeable)
|
|
||||||
case "$MERGEABLE" in True|False) break ;; esac
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
echo "mergeable=${MERGEABLE}"
|
|
||||||
|
|
||||||
notify() {
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO}: dev cascade needs manual merge" \
|
|
||||||
-H "Tags: warning,twisted_rightwards_arrows" \
|
|
||||||
-H "Priority: high" \
|
|
||||||
-H "Click: ${MOKOGITEA_URL}/${REPO}/pulls/${PR}" \
|
|
||||||
-d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \
|
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# 4. auto-merge only if conflict-free; otherwise notify
|
|
||||||
if [ "$MERGEABLE" = "True" ]; then
|
|
||||||
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
|
|
||||||
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}')
|
|
||||||
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
|
|
||||||
echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
|
|
||||||
else
|
|
||||||
echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
|
|
||||||
notify "could not be auto-merged (HTTP ${CODE})."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification."
|
|
||||||
notify "has conflicts and cannot be merged automatically."
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: MokoCLI.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/ci-generic.yml
|
# PATH: /.mokogitea/workflows/ci-generic.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# INGROUP: MokoCLI.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
|
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||||
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
|
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||||
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
||||||
|
|
||||||
- name: Report CI failure
|
- name: Report CI failure
|
||||||
|
|||||||
+1321
-26
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Maintenance
|
# INGROUP: MokoCLI.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/cleanup.yml
|
# PATH: /.mokogitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: MokoCLI.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/gitleaks.yml
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 02.58.26
|
# VERSION: 02.64.02
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Notifications
|
# INGROUP: MokoCLI.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/notify.yml
|
# PATH: /.mokogitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.CI
|
# INGROUP: MokoCLI.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/pr-check.yml
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
@@ -47,15 +47,15 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
fix/*|bugfix/*)
|
fix/*|bugfix/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
patch/*)
|
patch/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
|
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
hotfix/*)
|
hotfix/*)
|
||||||
@@ -86,8 +86,7 @@ jobs:
|
|||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -97,80 +96,6 @@ jobs:
|
|||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Docs Update Gate (main PRs) ─────────────────────────────────────────
|
|
||||||
require-docs:
|
|
||||||
name: Require Docs Update
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
|
|
||||||
if: ${{ github.base_ref == 'main' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Require README.md and CHANGELOG.md in the PR diff
|
|
||||||
run: |
|
|
||||||
BASE="${{ github.event.pull_request.base.sha }}"
|
|
||||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
|
||||||
CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
|
|
||||||
if [ -z "$CHANGED" ]; then
|
|
||||||
git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
|
|
||||||
CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
echo "Changed files in PR:"
|
|
||||||
echo "$CHANGED"
|
|
||||||
MISSING=""
|
|
||||||
echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
|
|
||||||
echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
|
|
||||||
if [ -n "$MISSING" ]; then
|
|
||||||
echo "::error::PRs into main must update: ${MISSING}"
|
|
||||||
{
|
|
||||||
echo "## Docs Update Required"
|
|
||||||
echo ""
|
|
||||||
echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
|
|
||||||
echo ""
|
|
||||||
echo "Not updated in this PR: **${MISSING}**"
|
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Docs update present (README.md + CHANGELOG.md)"
|
|
||||||
echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|
||||||
# ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
|
|
||||||
wiki-reminder:
|
|
||||||
name: Wiki Update Reminder
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.base_ref == 'main' }}
|
|
||||||
steps:
|
|
||||||
- name: Remind to update the wiki
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
PR: ${{ github.event.pull_request.number }}
|
|
||||||
run: |
|
|
||||||
set -uo pipefail
|
|
||||||
{
|
|
||||||
echo "## Wiki Update Reminder"
|
|
||||||
echo ""
|
|
||||||
echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
|
|
||||||
echo ""
|
|
||||||
echo "- ${SERVER}/${REPO}/wiki"
|
|
||||||
echo ""
|
|
||||||
echo "_Non-blocking reminder._"
|
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
# Post a single PR comment (idempotent via hidden marker); best-effort, never fails.
|
|
||||||
API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
|
|
||||||
if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
|
|
||||||
existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)"
|
|
||||||
if [ "${existing:-0}" -eq 0 ]; then
|
|
||||||
curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \
|
|
||||||
-d '{"body":"<!-- wiki-reminder -->\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo "Wiki reminder emitted (non-blocking)."
|
|
||||||
|
|
||||||
# ── Secret Scanning ──────────────────────────────────────────────────
|
# ── Secret Scanning ──────────────────────────────────────────────────
|
||||||
gitleaks:
|
gitleaks:
|
||||||
name: Secret Scan
|
name: Secret Scan
|
||||||
@@ -350,7 +275,7 @@ jobs:
|
|||||||
joomla)
|
joomla)
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
if [ -z "$MANIFEST" ]; then
|
if [ -z "$MANIFEST" ]; then
|
||||||
echo "::warning::No Joomla manifest found (MokoSuite site)"
|
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "Manifest: ${MANIFEST}"
|
echo "Manifest: ${MANIFEST}"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Validation
|
# INGROUP: MokoCLI.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
|
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
@@ -20,7 +20,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
@@ -28,12 +28,14 @@ jobs:
|
|||||||
validate-metadata:
|
validate-metadata:
|
||||||
name: "Validate Joomla Metadata"
|
name: "Validate Joomla Metadata"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Skip on template repos (Template-*) — they have no real extension manifest to validate.
|
||||||
|
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup MokoCLI tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
@@ -55,14 +57,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate metadata against Joomla manifest
|
- name: Validate metadata against Joomla manifest
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
||||||
--path . \
|
--path . \
|
||||||
--token "${GITEA_TOKEN}" \
|
--token "${MOKOGITEA_TOKEN}" \
|
||||||
--org "${GITEA_ORG}" \
|
--org "${GITEA_ORG}" \
|
||||||
--repo "${GITEA_REPO}" \
|
--repo "${GITEA_REPO}" \
|
||||||
--api-base "${GITEA_URL}/api/v1" \
|
--api-base "${MOKOGITEA_URL}/api/v1" \
|
||||||
--ci
|
--ci
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: MokoCLI.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/pre-release.yml
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.02.00
|
# VERSION: 05.02.00
|
||||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
|
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup MokoCLI tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokocli
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# INGROUP: MokoCLI.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||||
@@ -25,8 +25,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request.merged == false &&
|
github.event.pull_request.merged == false &&
|
||||||
startsWith(github.event.pull_request.head.ref, 'rc/') &&
|
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||||
!startsWith(github.event.repository.name, 'Template-')
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Rename branch
|
- name: Rename branch
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Validation
|
# INGROUP: MokoCLI.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/repo-health.yml
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
|||||||
name: Sync Workflows to Repos
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- '.mokogitea/workflows/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout mokocli
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: MokoConsulting/MokoCLI
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/raw/branch/main/actions/setup-php@v1
|
|
||||||
with:
|
|
||||||
php-version: '8.1'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --no-dev --no-interaction
|
|
||||||
|
|
||||||
- name: Sync workflows to generic repos
|
|
||||||
run: php automation/bulk_sync.php --platform generic --org MokoConsulting --workflows-only --auto-merge --token "${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow.Template
|
# DEFGROUP: MokoGitea.Workflow.Template
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: MokoCLI.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
# PATH: /.mokogitea/workflows/version-set.yml
|
# PATH: /.mokogitea/workflows/version-set.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
@@ -34,7 +34,6 @@ jobs:
|
|||||||
set-version:
|
set-version:
|
||||||
name: Set Version to ${{ inputs.version }}
|
name: Set Version to ${{ inputs.version }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Validate version format
|
- name: Validate version format
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
|
||||||
# INGROUP: mokocli.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
||||||
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
|
||||||
# VERSION: 01.01.00
|
|
||||||
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
|
||||||
|
|
||||||
name: "Universal: Workflow Sync Trigger"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
name: Sync workflows to live repos
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
|
||||||
(github.event.pull_request.merged == true &&
|
|
||||||
!contains(github.event.pull_request.title, '[skip sync]')))
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Determine platform from repo name
|
|
||||||
id: platform
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
case "$REPO" in
|
|
||||||
Template-Joomla) PLATFORM="joomla" ;;
|
|
||||||
Template-Dolibarr) PLATFORM="dolibarr" ;;
|
|
||||||
Template-Go) PLATFORM="go" ;;
|
|
||||||
Template-MCP) PLATFORM="mcp" ;;
|
|
||||||
Template-Generic) PLATFORM="" ;;
|
|
||||||
*) PLATFORM="" ;;
|
|
||||||
esac
|
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Platform: ${PLATFORM:-all}"
|
|
||||||
|
|
||||||
- name: Clone mokocli
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
|
||||||
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
|
|
||||||
|
|
||||||
- name: Install PHP
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd /tmp/mokocli
|
|
||||||
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Run workflow sync
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
ARGS="--token ${MOKOGITEA_TOKEN}"
|
|
||||||
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
|
|
||||||
ARGS="${ARGS} --phase repos"
|
|
||||||
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
if [ -n "$PLATFORM" ]; then
|
|
||||||
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
|
|
||||||
+6
-73
@@ -1,81 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Fixed
|
## [02.64.00] --- 2026-07-14
|
||||||
- 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`.
|
|
||||||
|
|
||||||
### Added
|
## [02.64.00] --- 2026-07-14
|
||||||
- **Snapshot transfer + master→slave injection (#237):** download a content snapshot as a portable **`.msbsnap`** file (zip of `manifest.json` + `snapshot.json` + a sha256 integrity check); **import** one on another site via a new Import button on the Snapshots page (materialises a local snapshot record you can then restore); and a **direct injection API** — `POST /api/index.php/v1/mokosuitebackup/snapshot/inject` — so a **master** site can push a snapshot straight into a **slave**'s Web Services API. Two conflict modes ship now: **overwrite** (replace matching items by ID — master wins) and **create** (skip existing, add only new), selectable per inject call or defaulted in **Options → Snapshot Transfer**. A receiving site must opt in via the new *Allow Snapshot Injection* setting. A third **duplicate** mode inserts everything as brand-new records with **remapped IDs** (articles, categories, modules — plus their tags, custom-field values and featured flags) using Joomla's Table API so assets/UCM/nested-sets stay valid; each item is inserted defensively (a bad item is skipped and logged, never fatal).
|
|
||||||
- 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)
|
|
||||||
|
|
||||||
### Changed
|
## [02.63.00] --- 2026-07-13
|
||||||
- Consolidated backup plumbing into shared helpers (#230):
|
|
||||||
- New `RemoteUploaderFactory` replaces the `createUploaderFromParams()` copy that was duplicated in `BackupEngine` and `SteppedBackupEngine`.
|
|
||||||
- `RetentionManager` is now the single retention authority — it takes the global `max_age_days`/`max_backups` fallback and gained `pruneOrphans()`; the system plugin's hourly cleanup delegates to it and its duplicate `deleteBackupRecord()` logic is removed.
|
|
||||||
- The backend controller, Web Services API controller, and legacy `cli/mokosuitebackup.php` now run backups through the shared `BackupRunner` (gaining the normalized complete/warning/fail status) instead of instantiating `BackupEngine` directly.
|
|
||||||
|
|
||||||
### Fixed
|
## [02.62.00] --- 2026-07-13
|
||||||
- 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)
|
|
||||||
- Remote upload: the standalone restore script upload is no longer silent — its result is now checked and logged, and a failed restore-script upload marks the backup as `warning` (previously the result was discarded, so a missing restore script on the remote went unreported while the archive still showed success). (#226)
|
|
||||||
|
|
||||||
## [02.58.00] --- 2026-07-06
|
## [02.61.00] --- 2026-07-13
|
||||||
|
|
||||||
### Added
|
## [02.60.00] --- 2026-07-12
|
||||||
- SFTP destinations: **upload an SSH private key file** in the Add/Edit Destination modal instead of pasting it (reads the file into the key field client-side; pasting still works).
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Add/Edit Destination modal: wrap the content in a Bootstrap `.container-fluid` and give it a 1rem padding frame via the WAM stylesheet (Bootstrap's `.modal-content` has no padding of its own).
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Pre-update backup now shows an admin notification on **every** outcome (success / warning / failure) — previously a *successful* pre-update backup fired nothing, so it looked like the notification wasn't working. (#192)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Component admin CSS now loads via the Joomla **Web Asset Manager** (`media/com_mokosuitebackup/` + `joomla.asset.json`) instead of an inline `<style>` block.
|
|
||||||
- "Keep local copy" is configured **per remote destination** (in the Add/Edit Destination modal); the redundant profile-level field and its "Remote" tab are removed.
|
|
||||||
- Add/Edit Destination modal: cleaned up the cramped padding/margins.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Admin menu / duplicate-component fallout: (a) the orphaned `com_component-mokosuitebackup` is now removed in **preflight** too, so its "Backup" menu no longer collides on install ("The alias backup is already being used"); (b) `ensureSubmenuItems()` **recreates the top-level "Backup" menu** if it was deleted, instead of giving up — so Backup no longer disappears from the admin menu (and the cPanel module / MokoSuiteClient can find it again); (c) the component menu label now uses the short constant `COM_MOKOJOOMBACKUP_SHORT`. (#213 fallout)
|
|
||||||
- Installer no longer aborts on MySQL 8 with "This command is not supported in the prepared statement protocol yet" (error 1295). The legacy remote-column purge migration (`02.56.01.sql`) used `PREPARE`/`EXECUTE`/`DEALLOCATE`, which Joomla's installer rejects; the drop now runs in the package installer script via an INFORMATION_SCHEMA-gated plain `ALTER` (portable across MariaDB and MySQL 8). (#213 update path)
|
|
||||||
|
|
||||||
## [02.57.00] --- 2026-07-05
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- `BackupRunner` service — a single synchronous entry point for backups that unifies final status and notification gating across the pre-update, web-cron, pre-install, scheduled-task and CLI triggers (delegates to `BackupEngine`; no double-notify). Returns a normalized result including the real complete/warning/fail status. Dashboard "Backup Now" (AJAX) unchanged; installer progress popup deferred to #196. (#214)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Release automation: the changelog-promote step is now idempotent — it only inserts a `## [version]` header when that version isn't already present, preventing the empty, duplicated version headers that repeated/same-version builds were producing. (workflow)
|
|
||||||
|
|
||||||
## [02.56.11] --- 2026-07-05
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Package `postflight` now removes the orphaned `com_component-mokosuitebackup` registration (stray extension record, admin menu items, schema rows, and `administrator/` folder) left behind by the old "Type - Name" component `<name>` — self-healing on update so affected sites reconcile to the real `com_mokosuitebackup`. (#213 follow-up)
|
|
||||||
|
|
||||||
## [02.56.08] --- 2026-07-05
|
|
||||||
|
|
||||||
### 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
|
INGROUP: Template-Joomla.Documentation
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
PATH: /SECURITY.md
|
PATH: /SECURITY.md
|
||||||
VERSION: 02.58.26
|
VERSION: 02.64.02
|
||||||
BRIEF: Security vulnerability reporting and handling policy
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.3",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"ext-pdo_mysql": "*",
|
"ext-pdo_mysql": "*",
|
||||||
|
|||||||
Submodule source/packages/MokoSuiteClient updated: 246f7bb10c...050b05d6ef
@@ -349,6 +349,15 @@ COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_ANYWAY="Continue without backup"
|
|||||||
COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard"
|
COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard"
|
||||||
COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD="View backup record"
|
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_RUNBACKUP_LEAVE_WARNING="A backup is in progress. Leaving now will cancel it."
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_AUTOCONTINUE="Automatically continue the update when the backup finishes"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_DONT_CLOSE="Please do not close this window or switch to another window until the backup finishes."
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL="Cancel backup"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM1="Cancel the backup that is in progress?"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM2="Are you sure? The backup will be stopped and the update will NOT continue."
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCELLING="Cancelling…"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED_NO_UPDATE="Backup failed — the update was not started. Retry the backup, or go back without updating."
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_UPDATE="Continue the update"
|
||||||
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)"
|
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)"
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_ANYWAY="Continue without backup"
|
|||||||
COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard"
|
COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard"
|
||||||
COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD="View backup record"
|
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_RUNBACKUP_LEAVE_WARNING="A backup is in progress. Leaving now will cancel it."
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_AUTOCONTINUE="Automatically continue the update when the backup finishes"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_DONT_CLOSE="Please do not close this window or switch to another window until the backup finishes."
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL="Cancel backup"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM1="Cancel the backup that is in progress?"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM2="Are you sure? The backup will be stopped and the update will NOT continue."
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCELLING="Cancelling…"
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED_NO_UPDATE="Backup failed — the update was not started. Retry the backup, or go back without updating."
|
||||||
|
COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_UPDATE="Continue the update"
|
||||||
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)"
|
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)"
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* @package MokoSuiteBackup
|
||||||
|
* @subpackage com_mokosuitebackup
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*
|
||||||
|
* Full-screen backup progress screen (view=runbackup). Loaded via the Joomla
|
||||||
|
* Web Asset Manager (asset com_mokosuitebackup.runbackup).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Full-screen backup runs on a black backdrop. */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background-color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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.
|
||||||
|
The card's gray background comes from a Bootstrap utility class on the
|
||||||
|
element itself (see the template), not a rule here. */
|
||||||
|
.msb-runbackup .card {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msb-rb-progress {
|
||||||
|
height: 2.5rem;
|
||||||
|
background: var(--msb-track);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msb-rb-bar {
|
||||||
|
position: relative;
|
||||||
|
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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
"name": "com_mokosuitebackup.admin",
|
"name": "com_mokosuitebackup.admin",
|
||||||
"type": "style",
|
"type": "style",
|
||||||
"uri": "com_mokosuitebackup/css/admin.css"
|
"uri": "com_mokosuitebackup/css/admin.css"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "com_mokosuitebackup.runbackup",
|
||||||
|
"type": "style",
|
||||||
|
"uri": "com_mokosuitebackup/css/runbackup.css"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
display label there.
|
display label there.
|
||||||
-->
|
-->
|
||||||
<name>MokoSuiteBackup</name>
|
<name>MokoSuiteBackup</name>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -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 */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.59.01 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.59.02 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.59.03 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.59.04 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.59.05 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.59.06 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.59.07 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.59.08 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.59.09 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.60.00 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.60.01 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.60.02 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.60.03 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.60.04 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.60.05 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.60.06 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.61.00 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.61.01 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.61.04 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.61.05 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.61.06 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.62.00 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.62.01 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.62.02 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.62.03 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.62.04 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.62.05 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.63.00 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.63.01 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.63.02 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.63.05 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.63.06 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.63.07 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.63.09 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.63.10 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.63.11 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.64.00 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.64.01 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 02.64.02 — no schema changes */
|
||||||
@@ -89,11 +89,11 @@ class AjaxController extends BaseController
|
|||||||
*
|
*
|
||||||
* Called by the full-screen backup screen after a successful backup so the
|
* Called by the full-screen backup screen after a successful backup so the
|
||||||
* imminent server-side onExtensionBeforeUpdate / onExtensionBeforeUninstall
|
* imminent server-side onExtensionBeforeUpdate / onExtensionBeforeUninstall
|
||||||
* backup is skipped — it arms the same 10-minute throttle keys the system
|
* backup is skipped once — it sets a ONE-SHOT skip flag (keyed by the
|
||||||
* plugin checks, preventing a duplicate backup when the update / uninstall
|
* `action` the backup was fronting) that plg_system_mokosuitebackup
|
||||||
* then proceeds. Arms BOTH keys so a single ack covers whichever action the
|
* consumes on the re-fired update/uninstall request, so exactly that one
|
||||||
* full-screen backup was fronting.
|
* action is not double-backed-up while later, distinct actions still are.
|
||||||
* POST: task=ajax.preupdateAck
|
* POST: task=ajax.preupdateAck&action=update|uninstall
|
||||||
*/
|
*/
|
||||||
public function preupdateAck(): void
|
public function preupdateAck(): void
|
||||||
{
|
{
|
||||||
@@ -109,11 +109,21 @@ class AjaxController extends BaseController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same keys + semantics as plg_system_mokosuitebackup::runPreActionBackup().
|
/* Set a ONE-SHOT skip flag for the specific action the full-screen backup
|
||||||
|
was fronting. runPreActionBackup() consumes it on the re-fired
|
||||||
|
update/uninstall request (once), so the synchronous backup is skipped
|
||||||
|
that one time but the NEXT distinct action still backs up. Keyed by
|
||||||
|
action so an update ack never suppresses a later, unrelated uninstall.
|
||||||
|
For a core Joomla update (no action) neither flag is set — a core
|
||||||
|
update doesn't fire onExtensionBefore(Update|Uninstall). */
|
||||||
|
$action = $this->app->getInput()->getCmd('action', '');
|
||||||
$session = Factory::getSession();
|
$session = Factory::getSession();
|
||||||
$now = time();
|
|
||||||
$session->set('mokosuitebackup.preaction_backup_before_update', $now);
|
if ($action === 'update') {
|
||||||
$session->set('mokosuitebackup.preaction_backup_before_uninstall', $now);
|
$session->set('mokosuitebackup.skip_preaction_backup_before_update', true);
|
||||||
|
} elseif ($action === 'uninstall') {
|
||||||
|
$session->set('mokosuitebackup.skip_preaction_backup_before_uninstall', true);
|
||||||
|
}
|
||||||
|
|
||||||
$this->sendJson(['error' => false, 'message' => 'Pre-action backup acknowledged']);
|
$this->sendJson(['error' => false, 'message' => 'Pre-action backup acknowledged']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -480,8 +480,8 @@ function actionPreflight(): array
|
|||||||
$checks[] = [
|
$checks[] = [
|
||||||
'label' => 'PHP Version',
|
'label' => 'PHP Version',
|
||||||
'value' => PHP_VERSION,
|
'value' => PHP_VERSION,
|
||||||
'ok' => version_compare(PHP_VERSION, '8.1', '>='),
|
'ok' => version_compare(PHP_VERSION, '8.3', '>='),
|
||||||
'hint' => 'Joomla 4/5 requires PHP 8.1+',
|
'hint' => 'Joomla 6 requires PHP 8.3+',
|
||||||
];
|
];
|
||||||
|
|
||||||
$checks[] = [
|
$checks[] = [
|
||||||
|
|||||||
@@ -125,12 +125,24 @@ class SnapshotRestoreEngine
|
|||||||
$prefix = $db->getPrefix();
|
$prefix = $db->getPrefix();
|
||||||
$totalRows = 0;
|
$totalRows = 0;
|
||||||
|
|
||||||
try {
|
|
||||||
$db->transactionStart();
|
|
||||||
|
|
||||||
// Build list of tables to restore based on selected types
|
// Build list of tables to restore based on selected types
|
||||||
$tablesToRestore = $this->getTablesToRestore($restoreTypes);
|
$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') {
|
if ($mode === 'duplicate') {
|
||||||
$totalRows = $this->restoreDuplicate($data, $restoreTypes);
|
$totalRows = $this->restoreDuplicate($data, $restoreTypes);
|
||||||
} else {
|
} else {
|
||||||
@@ -156,7 +168,9 @@ class SnapshotRestoreEngine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($useTransaction) {
|
||||||
$db->transactionCommit();
|
$db->transactionCommit();
|
||||||
|
}
|
||||||
|
|
||||||
$this->log('Restore complete: ' . $totalRows . ' total rows');
|
$this->log('Restore complete: ' . $totalRows . ' total rows');
|
||||||
|
|
||||||
@@ -188,12 +202,16 @@ class SnapshotRestoreEngine
|
|||||||
'log' => implode("\n", $this->log),
|
'log' => implode("\n", $this->log),
|
||||||
];
|
];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
if ($useTransaction) {
|
||||||
try {
|
try {
|
||||||
$db->transactionRollback();
|
$db->transactionRollback();
|
||||||
$this->log('Transaction rolled back');
|
$this->log('Transaction rolled back');
|
||||||
} catch (\Exception $rollbackEx) {
|
} catch (\Exception $rollbackEx) {
|
||||||
$this->log('Rollback failed: ' . $rollbackEx->getMessage());
|
$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());
|
$this->log('FATAL: ' . $e->getMessage());
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class DashboardModel extends BaseDatabaseModel
|
|||||||
// PHP version
|
// PHP version
|
||||||
$checks[] = (object) [
|
$checks[] = (object) [
|
||||||
'label' => 'PHP Version',
|
'label' => 'PHP Version',
|
||||||
'status' => version_compare(PHP_VERSION, '8.1.0', '>='),
|
'status' => version_compare(PHP_VERSION, '8.3.0', '>='),
|
||||||
'detail' => PHP_VERSION,
|
'detail' => PHP_VERSION,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ class HtmlView extends BaseHtmlView
|
|||||||
|
|
||||||
public bool $autostart = true;
|
public bool $autostart = true;
|
||||||
|
|
||||||
|
/** Which action the backup fronts ('update' | 'uninstall' | ''); drives the completion ack skip flag. */
|
||||||
|
public string $action = '';
|
||||||
|
|
||||||
public function display($tpl = null): void
|
public function display($tpl = null): void
|
||||||
{
|
{
|
||||||
$input = Factory::getApplication()->getInput();
|
$input = Factory::getApplication()->getInput();
|
||||||
@@ -48,6 +51,7 @@ class HtmlView extends BaseHtmlView
|
|||||||
$this->description = $input->getString('description', '');
|
$this->description = $input->getString('description', '');
|
||||||
$this->returnUrl = $input->getRaw('returnurl', '');
|
$this->returnUrl = $input->getRaw('returnurl', '');
|
||||||
$this->autostart = (bool) $input->getInt('autostart', 1);
|
$this->autostart = (bool) $input->getInt('autostart', 1);
|
||||||
|
$this->action = $input->getCmd('msb_action', '');
|
||||||
|
|
||||||
if ($this->profileId <= 0) {
|
if ($this->profileId <= 0) {
|
||||||
$this->profileId = 1;
|
$this->profileId = 1;
|
||||||
|
|||||||
@@ -14,13 +14,18 @@ use Joomla\CMS\HTML\HTMLHelper;
|
|||||||
use Joomla\CMS\Language\Text;
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\Router\Route;
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Session\Session;
|
use Joomla\CMS\Session\Session;
|
||||||
|
use Joomla\CMS\Uri\Uri;
|
||||||
|
|
||||||
HTMLHelper::_('behavior.formvalidator');
|
HTMLHelper::_('behavior.formvalidator');
|
||||||
HTMLHelper::_('behavior.keepalive');
|
HTMLHelper::_('behavior.keepalive');
|
||||||
HTMLHelper::_('bootstrap.modal');
|
HTMLHelper::_('bootstrap.modal');
|
||||||
|
|
||||||
/* Component CSS via the Web Asset Manager (declared in media/joomla.asset.json) */
|
/* Component CSS. Registered in media/joomla.asset.json, but this component's WAM
|
||||||
|
styles don't emit on the admin document (asset resolves without error yet no
|
||||||
|
<link> renders), so attach the external stylesheet directly — reliably output
|
||||||
|
via <jdoc:include type="styles" />. */
|
||||||
$this->getDocument()->getWebAssetManager()->useStyle('com_mokosuitebackup.admin');
|
$this->getDocument()->getWebAssetManager()->useStyle('com_mokosuitebackup.admin');
|
||||||
|
$this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebackup/css/admin.css');
|
||||||
|
|
||||||
$profileId = (int) $this->item->id;
|
$profileId = (int) $this->item->id;
|
||||||
$token = Session::getFormToken();
|
$token = Session::getFormToken();
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ if ($this->returnUrl !== '') {
|
|||||||
$decoded = base64_decode($this->returnUrl, true);
|
$decoded = base64_decode($this->returnUrl, true);
|
||||||
$raw = ($decoded !== false && $decoded !== '') ? $decoded : $this->returnUrl;
|
$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);
|
$isAbsoluteHttp = (bool) preg_match('#^https?://#i', $raw);
|
||||||
$isRootRelative = isset($raw[0]) && $raw[0] === '/' && (!isset($raw[1]) || $raw[1] !== '/');
|
$isRootRelative = isset($raw[0]) && $raw[0] === '/' && (!isset($raw[1]) || $raw[1] !== '/');
|
||||||
|
|
||||||
@@ -63,6 +71,7 @@ $config = [
|
|||||||
'profileId' => $this->profileId,
|
'profileId' => $this->profileId,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'returnUrl' => $safeReturnUrl,
|
'returnUrl' => $safeReturnUrl,
|
||||||
|
'action' => $this->action,
|
||||||
'dashboardUrl' => $dashboardUrl,
|
'dashboardUrl' => $dashboardUrl,
|
||||||
'recordUrl' => Route::_('index.php?option=com_mokosuitebackup&view=backup&id=__MSBID__', false),
|
'recordUrl' => Route::_('index.php?option=com_mokosuitebackup&view=backup&id=__MSBID__', false),
|
||||||
'autostart' => $this->autostart,
|
'autostart' => $this->autostart,
|
||||||
@@ -77,12 +86,27 @@ $config = [
|
|||||||
'dashboard' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD'),
|
'dashboard' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD'),
|
||||||
'viewRecord' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD'),
|
'viewRecord' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD'),
|
||||||
'leaveWarn' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING'),
|
'leaveWarn' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING'),
|
||||||
|
'continueUpdate' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_UPDATE'),
|
||||||
|
'cancelConfirm1' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM1'),
|
||||||
|
'cancelConfirm2' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM2'),
|
||||||
|
'cancelling' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CANCELLING'),
|
||||||
|
'failedNoUpdate' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED_NO_UPDATE'),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Screen CSS lives in an external media stylesheet (media/com_mokosuitebackup/
|
||||||
|
// css/runbackup.css), not an inline <style> block. It is registered in
|
||||||
|
// media/joomla.asset.json, but this component's Web Asset Manager styles do not
|
||||||
|
// emit on the admin document (the asset resolves without error yet no <link> is
|
||||||
|
// produced — the same is true of com_mokosuitebackup.admin), so we attach the
|
||||||
|
// stylesheet directly to the document, which reliably renders via <jdoc:include
|
||||||
|
// type="styles" />.
|
||||||
|
$this->getDocument()->getWebAssetManager()->useStyle('com_mokosuitebackup.runbackup');
|
||||||
|
$this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebackup/css/runbackup.css');
|
||||||
?>
|
?>
|
||||||
<div class="msb-runbackup" style="max-width:640px;margin:3rem auto;padding:0 1rem;">
|
<div class="msb-runbackup">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm bg-body-secondary">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4 pb-3">
|
||||||
<h1 class="h4 mb-1" id="msb-rb-title">
|
<h1 class="h4 mb-1" id="msb-rb-title">
|
||||||
<span class="icon-archive" aria-hidden="true"></span>
|
<span class="icon-archive" aria-hidden="true"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_TITLE'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_TITLE'); ?>
|
||||||
@@ -97,20 +121,42 @@ $config = [
|
|||||||
); ?>
|
); ?>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="progress mb-3" style="height:1.75rem;">
|
<div id="msb-rb-phase" class="fw-bold mb-1"></div>
|
||||||
<div id="msb-rb-bar" class="progress-bar progress-bar-striped progress-bar-animated"
|
<div id="msb-rb-status" class="mb-0"><?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_STARTING'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||||
role="progressbar" style="width:0;" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">0%</div>
|
|
||||||
|
<div id="msb-rb-warn" class="alert alert-warning fw-bold mt-3 mb-0" role="alert">
|
||||||
|
<span class="icon-warning" aria-hidden="true"></span>
|
||||||
|
<?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_DONT_CLOSE'), ENT_QUOTES, 'UTF-8'); ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="msb-rb-phase" class="fw-bold mb-1"></div>
|
<?php if ($safeReturnUrl !== '') : ?>
|
||||||
<div id="msb-rb-status" class="text-muted small mb-3"><?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_STARTING'), ENT_QUOTES, 'UTF-8'); ?></div>
|
<!-- Pre-update/uninstall flow: opt in to auto-continue, or get a manual "Continue" button on completion. -->
|
||||||
|
<div class="form-check mt-3" id="msb-rb-autocontinue-wrap">
|
||||||
|
<input class="form-check-input" type="checkbox" id="msb-rb-autocontinue" checked>
|
||||||
|
<label class="form-check-label" for="msb-rb-autocontinue">
|
||||||
|
<?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_AUTOCONTINUE'), ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<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>
|
<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-record" class="btn btn-primary d-none" target="_blank" rel="noopener"></a>
|
||||||
<a href="#" id="msb-rb-continue" class="btn btn-warning 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>
|
<a href="<?php echo $dashboardUrl; ?>" id="msb-rb-dashboard" class="btn btn-secondary d-none"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Cancel is available while the backup is running; requires a double confirmation. -->
|
||||||
|
<div id="msb-rb-cancel-wrap" class="mt-3">
|
||||||
|
<button type="button" id="msb-rb-cancel" class="btn btn-outline-danger">
|
||||||
|
<?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL'), ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
</button>
|
||||||
|
</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">0%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,27 +168,61 @@ $config = [
|
|||||||
var CFG = <?php echo json_encode($config); ?>;
|
var CFG = <?php echo json_encode($config); ?>;
|
||||||
var L = CFG.labels || {};
|
var L = CFG.labels || {};
|
||||||
var running = false;
|
var running = false;
|
||||||
|
var cancelled = false;
|
||||||
var lastRecordId = 0;
|
var lastRecordId = 0;
|
||||||
|
|
||||||
var el = {
|
var el = {
|
||||||
bar: document.getElementById('msb-rb-bar'),
|
bar: document.getElementById('msb-rb-bar'),
|
||||||
|
track: document.querySelector('.msb-rb-progress'),
|
||||||
phase: document.getElementById('msb-rb-phase'),
|
phase: document.getElementById('msb-rb-phase'),
|
||||||
status: document.getElementById('msb-rb-status'),
|
status: document.getElementById('msb-rb-status'),
|
||||||
|
warn: document.getElementById('msb-rb-warn'),
|
||||||
title: document.getElementById('msb-rb-title'),
|
title: document.getElementById('msb-rb-title'),
|
||||||
actions: document.getElementById('msb-rb-actions'),
|
actions: document.getElementById('msb-rb-actions'),
|
||||||
retry: document.getElementById('msb-rb-retry'),
|
retry: document.getElementById('msb-rb-retry'),
|
||||||
record: document.getElementById('msb-rb-record'),
|
record: document.getElementById('msb-rb-record'),
|
||||||
continue: document.getElementById('msb-rb-continue'),
|
continue: document.getElementById('msb-rb-continue'),
|
||||||
dashboard: document.getElementById('msb-rb-dashboard')
|
dashboard: document.getElementById('msb-rb-dashboard'),
|
||||||
|
autocontinue: document.getElementById('msb-rb-autocontinue'),
|
||||||
|
autocontinueWrap: document.getElementById('msb-rb-autocontinue-wrap'),
|
||||||
|
cancel: document.getElementById('msb-rb-cancel'),
|
||||||
|
cancelWrap: document.getElementById('msb-rb-cancel-wrap')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function hideCancel() { if (el.cancelWrap) { el.cancelWrap.classList.add('d-none'); } }
|
||||||
|
|
||||||
|
/* Cancel the running backup — requires DOUBLE confirmation. Stops the step
|
||||||
|
loop, tells the server to cancel the record, and leaves to the dashboard
|
||||||
|
WITHOUT continuing any pending update. */
|
||||||
|
if (el.cancel) {
|
||||||
|
el.cancel.addEventListener('click', function () {
|
||||||
|
if (!running || cancelled) { return; }
|
||||||
|
if (!window.confirm(L.cancelConfirm1 || 'Cancel the backup that is in progress?')) { return; }
|
||||||
|
if (!window.confirm(L.cancelConfirm2 || 'Are you sure? The backup will be stopped and the update will NOT continue.')) { return; }
|
||||||
|
|
||||||
|
cancelled = true;
|
||||||
|
running = false; /* also disarms the leave-warning */
|
||||||
|
el.cancel.disabled = true;
|
||||||
|
if (el.warn) { el.warn.classList.add('d-none'); }
|
||||||
|
setStatus(L.cancelling || 'Cancelling…');
|
||||||
|
|
||||||
|
var go = function () { window.location.href = CFG.dashboardUrl; };
|
||||||
|
/* Best-effort server-side cancel of the record, then leave. */
|
||||||
|
if (lastRecordId > 0) {
|
||||||
|
post({ task: 'ajax.cancelBackup', id: lastRecordId }).then(go, go);
|
||||||
|
} else {
|
||||||
|
go();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setBar(pct, striped) {
|
function setBar(pct, striped) {
|
||||||
pct = Math.max(0, Math.min(100, parseInt(pct, 10) || 0));
|
pct = Math.max(0, Math.min(100, parseInt(pct, 10) || 0));
|
||||||
el.bar.style.width = pct + '%';
|
el.bar.style.width = pct + '%';
|
||||||
el.bar.textContent = pct + '%';
|
el.bar.textContent = pct + '%';
|
||||||
el.bar.setAttribute('aria-valuenow', pct);
|
if (el.track) { el.track.setAttribute('aria-valuenow', pct); }
|
||||||
if (!striped) {
|
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 || ''; }
|
function setPhase(t) { el.phase.textContent = t || ''; }
|
||||||
@@ -167,21 +247,44 @@ $config = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onComplete() {
|
async function onComplete() {
|
||||||
el.bar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
el.bar.classList.remove('is-animated');
|
||||||
el.bar.classList.add('bg-success');
|
el.bar.classList.add('is-success');
|
||||||
setBar(100, false);
|
setBar(100, false);
|
||||||
el.title.textContent = L.complete || 'Backup complete';
|
el.title.textContent = L.complete || 'Backup complete';
|
||||||
|
if (el.warn) { el.warn.classList.add('d-none'); }
|
||||||
|
hideCancel();
|
||||||
|
|
||||||
/* Arm the pre-action throttles so a following server-side onExtension
|
/* Set the one-shot skip flag for THIS action so the following
|
||||||
Before(Update|Uninstall) backup is skipped (no duplicate backup).
|
server-side onExtensionBefore(Update|Uninstall) backup is skipped once
|
||||||
|
(no duplicate backup) without suppressing later, distinct actions.
|
||||||
Awaited so it lands before we hand back to the update/uninstall. */
|
Awaited so it lands before we hand back to the update/uninstall. */
|
||||||
try { await post({ task: 'ajax.preupdateAck' }); } catch (e) {}
|
try { await post({ task: 'ajax.preupdateAck', action: CFG.action || '' }); } catch (e) {}
|
||||||
|
|
||||||
if (CFG.returnUrl) {
|
if (CFG.returnUrl) {
|
||||||
/* Pre-update / pre-uninstall flow: continue the original action.
|
/* Pre-update / pre-uninstall flow. If "auto-continue" is ticked
|
||||||
No "view record" button here — we're handing back to Joomla. */
|
(default) hand back to Joomla automatically; otherwise stop here
|
||||||
|
and offer a link to the new backup record plus a manual
|
||||||
|
"Continue the update" button. */
|
||||||
|
if (el.autocontinueWrap) { el.autocontinueWrap.classList.add('d-none'); }
|
||||||
|
|
||||||
|
var autoContinue = !el.autocontinue || el.autocontinue.checked;
|
||||||
|
|
||||||
|
if (autoContinue) {
|
||||||
setStatus(L.continuing || 'Continuing…');
|
setStatus(L.continuing || 'Continuing…');
|
||||||
window.setTimeout(function () { window.location.href = CFG.returnUrl; }, 1200);
|
window.setTimeout(function () { window.location.href = CFG.returnUrl; }, 1200);
|
||||||
|
} else {
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
el.continue.classList.remove('btn-warning');
|
||||||
|
el.continue.classList.add('btn-success');
|
||||||
|
showBtn(el.continue, L.continueUpdate || 'Continue the update', CFG.returnUrl);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
/* Manual "Backup Now": offer to view the record that was just made. */
|
/* Manual "Backup Now": offer to view the record that was just made. */
|
||||||
setStatus(L.complete || 'Backup complete');
|
setStatus(L.complete || 'Backup complete');
|
||||||
@@ -198,32 +301,36 @@ $config = [
|
|||||||
|
|
||||||
function onError(message) {
|
function onError(message) {
|
||||||
running = false;
|
running = false;
|
||||||
el.bar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
el.bar.classList.remove('is-animated');
|
||||||
el.bar.classList.add('bg-danger');
|
el.bar.classList.add('is-danger');
|
||||||
|
if (el.warn) { el.warn.classList.add('d-none'); }
|
||||||
el.title.textContent = L.failed || 'Backup failed';
|
el.title.textContent = L.failed || 'Backup failed';
|
||||||
setStatus(message || 'Backup failed');
|
/* Pre-update/uninstall flow: the backup failed, so the update must NOT
|
||||||
|
run. We deliberately do NOT offer a "continue without backup" option
|
||||||
|
here — the only ways forward are to retry the backup or go back. */
|
||||||
|
setStatus(CFG.returnUrl ? (L.failedNoUpdate || 'Backup failed — the update was not started. Retry the backup, or go back without updating.') : (message || 'Backup failed'));
|
||||||
|
hideCancel();
|
||||||
showActions();
|
showActions();
|
||||||
|
|
||||||
el.retry.textContent = L.retry || 'Retry';
|
el.retry.textContent = L.retry || 'Retry';
|
||||||
el.retry.classList.remove('d-none');
|
el.retry.classList.remove('d-none');
|
||||||
el.retry.onclick = function () {
|
el.retry.onclick = function () {
|
||||||
el.retry.classList.add('d-none');
|
el.retry.classList.add('d-none');
|
||||||
el.continue.classList.add('d-none');
|
el.bar.classList.remove('is-danger');
|
||||||
el.bar.classList.remove('bg-danger');
|
el.bar.classList.add('is-animated');
|
||||||
el.bar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
|
||||||
run();
|
run();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (CFG.returnUrl) {
|
|
||||||
showBtn(el.continue, L.continue || 'Continue without backup', CFG.returnUrl);
|
|
||||||
}
|
|
||||||
showBtn(el.dashboard, L.dashboard || 'Back to dashboard', CFG.dashboardUrl);
|
showBtn(el.dashboard, L.dashboard || 'Back to dashboard', CFG.dashboardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
if (running) { return; }
|
if (running) { return; }
|
||||||
running = true;
|
running = true;
|
||||||
|
cancelled = false;
|
||||||
el.actions.classList.add('d-none');
|
el.actions.classList.add('d-none');
|
||||||
|
if (el.cancelWrap) { el.cancelWrap.classList.remove('d-none'); }
|
||||||
|
if (el.cancel) { el.cancel.disabled = false; }
|
||||||
setBar(0, true);
|
setBar(0, true);
|
||||||
setPhase('');
|
setPhase('');
|
||||||
setStatus(L.starting || 'Starting backup…');
|
setStatus(L.starting || 'Starting backup…');
|
||||||
@@ -241,7 +348,7 @@ $config = [
|
|||||||
if (init.record_id) { lastRecordId = init.record_id; }
|
if (init.record_id) { lastRecordId = init.record_id; }
|
||||||
|
|
||||||
var done = false;
|
var done = false;
|
||||||
while (!done) {
|
while (!done && !cancelled) {
|
||||||
var step = await post({ task: 'ajax.step', session_id: init.session_id });
|
var step = await post({ task: 'ajax.step', session_id: init.session_id });
|
||||||
|
|
||||||
if (!step || step.error) {
|
if (!step || step.error) {
|
||||||
@@ -255,16 +362,27 @@ $config = [
|
|||||||
done = step.done || false;
|
done = step.done || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* User cancelled mid-flight — the click handler already stops the
|
||||||
|
loop and navigates away, so do not fall through to completion. */
|
||||||
|
if (cancelled) { return; }
|
||||||
|
|
||||||
running = false;
|
running = false;
|
||||||
await onComplete();
|
await onComplete();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (cancelled) { return; }
|
||||||
onError(err && err.message ? err.message : String(err));
|
onError(err && err.message ? err.message : String(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Warn before leaving while a backup is mid-flight. */
|
/* Warn before closing/leaving the window while a backup is mid-flight.
|
||||||
|
Return the message too — some browsers need it, others show a generic
|
||||||
|
prompt, but any non-empty value triggers the confirmation dialog. */
|
||||||
window.addEventListener('beforeunload', function (e) {
|
window.addEventListener('beforeunload', function (e) {
|
||||||
if (running) { e.preventDefault(); e.returnValue = L.leaveWarn || ''; }
|
if (!running) { return undefined; }
|
||||||
|
var msg = L.leaveWarn || 'A backup is in progress. Leaving now will cancel it.';
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = msg;
|
||||||
|
return msg;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (CFG.autostart) {
|
if (CFG.autostart) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="module" client="administrator" method="upgrade">
|
<extension type="module" client="administrator" method="upgrade">
|
||||||
<name>Module - MokoSuiteBackup - cPanel</name>
|
<name>Module - MokoSuiteBackup - cPanel</name>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-23</creationDate>
|
<creationDate>2026-06-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="actionlog" method="upgrade">
|
<extension type="plugin" group="actionlog" method="upgrade">
|
||||||
<name>Action Log - MokoSuiteBackup</name>
|
<name>Action Log - MokoSuiteBackup</name>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="console" method="upgrade">
|
<extension type="plugin" group="console" method="upgrade">
|
||||||
<name>Console - MokoSuiteBackup</name>
|
<name>Console - MokoSuiteBackup</name>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="content" method="upgrade">
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
<name>Content - MokoSuiteBackup</name>
|
<name>Content - MokoSuiteBackup</name>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="quickicon" method="upgrade">
|
<extension type="plugin" group="quickicon" method="upgrade">
|
||||||
<name>Quick Icon - MokoSuiteBackup</name>
|
<name>Quick Icon - MokoSuiteBackup</name>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
+1
@@ -14,3 +14,4 @@ PLG_SYSTEM_MOKOJOOMBACKUP_BACKING_UP="Backing up…"
|
|||||||
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update."
|
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update."
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed"
|
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed"
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…"
|
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…"
|
||||||
|
PLG_SYSTEM_MOKOSUITEBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…"
|
||||||
|
|||||||
+1
@@ -14,3 +14,4 @@ PLG_SYSTEM_MOKOJOOMBACKUP_BACKING_UP="Backing up…"
|
|||||||
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update."
|
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update."
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed"
|
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed"
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…"
|
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…"
|
||||||
|
PLG_SYSTEM_MOKOSUITEBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…"
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* MokoSuiteBackup — full-screen backup before an EXTENSION update / uninstall.
|
* MokoSuiteBackup — full-screen backup before an EXTENSION update / uninstall.
|
||||||
*
|
*
|
||||||
* Injected by plg_system_mokosuitebackup on the Extensions → Update and
|
* Injected by plg_system_mokosuitebackup on Extensions → Update and
|
||||||
* Extensions → Manage pages. Unlike the core Joomla update (which has a
|
* Extensions → Manage. Joomla 6 renders those toolbar buttons as web
|
||||||
* server-side page load we can redirect), com_installer's update.update and
|
* components (`<button task="update.update">`), NOT inline
|
||||||
* manage.remove are POST actions with a CSRF token and a checked cid[] list —
|
* onclick="Joomla.submitbutton(...)", so wrapping Joomla.submitbutton is
|
||||||
* so we intercept CLIENT-SIDE: capture the selection, go to the full-screen
|
* unreliable. Instead we add a CAPTURE-phase click listener on any element
|
||||||
* backup page, and on return restore the selection and re-submit the real POST
|
* carrying a matching `task` attribute — it runs before the toolbar web
|
||||||
* form (fresh token) so the update/uninstall proceeds.
|
* 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
|
* The full-screen page arms the pre-action throttle on success, so the
|
||||||
* Ack), so the server-side onExtensionBefore(Update|Uninstall) backup is then
|
* server-side onExtensionBefore(Update|Uninstall) backup is then skipped.
|
||||||
* skipped — no duplicate backup.
|
|
||||||
*
|
*
|
||||||
* Config: Joomla.getOptions('plg_system_mokosuitebackup.installer').
|
* Config: Joomla.getOptions('plg_system_mokosuitebackup.installer').
|
||||||
*/
|
*/
|
||||||
@@ -28,26 +28,106 @@
|
|||||||
|
|
||||||
var STORAGE_KEY = 'msbInstallerBackup';
|
var STORAGE_KEY = 'msbInstallerBackup';
|
||||||
|
|
||||||
/* Toolbar tasks to intercept, gated by which feature is enabled. */
|
|
||||||
var TASKS = {};
|
var TASKS = {};
|
||||||
if (cfg.backupBeforeUpdate) { TASKS['update.update'] = true; }
|
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() {
|
function checkedCids() {
|
||||||
var form = document.getElementById('adminForm');
|
var form = document.getElementById('adminForm');
|
||||||
var cids = [];
|
var cids = [];
|
||||||
|
|
||||||
if (form) {
|
if (form) {
|
||||||
form.querySelectorAll('input[name="cid[]"]:checked').forEach(function (b) {
|
form.querySelectorAll('input[name="cid[]"]:checked').forEach(function (b) { cids.push(b.value); });
|
||||||
cids.push(b.value);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cids;
|
return cids;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* On return from the backup screen: restore the selection and re-submit. */
|
/* Intercept the toolbar Update/Uninstall click in the CAPTURE phase, before
|
||||||
function resumeIfNeeded() {
|
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';
|
||||||
|
|
||||||
|
/* Tell the backup screen which action it is fronting so its completion
|
||||||
|
ack sets the matching one-shot skip flag (and not the other). */
|
||||||
|
var action = (task === 'update.update') ? 'update' : 'uninstall';
|
||||||
|
|
||||||
|
/* 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)
|
||||||
|
+ '&msb_action=' + action
|
||||||
|
+ '&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);
|
||||||
|
|
||||||
|
/* Full-screen "update is running" overlay shown while the re-fired update
|
||||||
|
POST processes, so the user knows it is working and not to close/switch. */
|
||||||
|
function showUpdateOverlay() {
|
||||||
|
if (document.getElementById('msb-update-overlay')) { return; }
|
||||||
|
|
||||||
|
var o = document.createElement('div');
|
||||||
|
o.id = 'msb-update-overlay';
|
||||||
|
o.setAttribute('role', 'status');
|
||||||
|
o.style.cssText = 'position:fixed;inset:0;z-index:2147483647;background:#000;color:#fff;'
|
||||||
|
+ 'display:flex;flex-direction:column;align-items:center;justify-content:center;'
|
||||||
|
+ 'text-align:center;padding:2rem;font-size:1.15rem;font-weight:600;';
|
||||||
|
|
||||||
|
var spin = document.createElement('span');
|
||||||
|
spin.className = 'spinner-border mb-3';
|
||||||
|
spin.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
var msg = document.createElement('div');
|
||||||
|
msg.textContent = (cfg && cfg.updateRunningText)
|
||||||
|
|| 'Backup complete. The update is now running — please wait and do not close this window…';
|
||||||
|
|
||||||
|
o.appendChild(spin);
|
||||||
|
o.appendChild(msg);
|
||||||
|
document.body.appendChild(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On return from the backup screen: restore the selection and re-fire. */
|
||||||
|
function resume() {
|
||||||
var params = new URLSearchParams(window.location.search);
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
if (params.get('msb_resume') !== '1') {
|
if (params.get('msb_resume') !== '1') {
|
||||||
@@ -55,15 +135,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var raw = null;
|
var raw = null;
|
||||||
try { raw = sessionStorage.getItem(STORAGE_KEY); } catch (e) {}
|
try { raw = sessionStorage.getItem(STORAGE_KEY); sessionStorage.removeItem(STORAGE_KEY); } catch (x) {}
|
||||||
try { sessionStorage.removeItem(STORAGE_KEY); } catch (e) {}
|
|
||||||
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var data;
|
var data;
|
||||||
try { data = JSON.parse(raw); } catch (e) { data = null; }
|
try { data = JSON.parse(raw); } catch (x) { data = null; }
|
||||||
|
|
||||||
if (!data || !data.task) {
|
if (!data || !data.task) {
|
||||||
return;
|
return;
|
||||||
@@ -71,72 +150,51 @@
|
|||||||
|
|
||||||
var form = document.getElementById('adminForm');
|
var form = document.getElementById('adminForm');
|
||||||
|
|
||||||
if (!form) {
|
if (form) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(data.cids || []).forEach(function (id) {
|
(data.cids || []).forEach(function (id) {
|
||||||
var box = form.querySelector('input[name="cid[]"][value="' + id + '"]');
|
var box = form.querySelector('input[name="cid[]"][value="' + id + '"]');
|
||||||
if (box) { box.checked = true; }
|
if (box) { box.checked = true; }
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Re-run the original toolbar action; the wrapper lets it through
|
/* Restore boxchecked AND fire its `change` event. Joomla's toolbar web
|
||||||
because the resume flag is set. */
|
component (joomla-toolbar-button) keeps a list-selection button
|
||||||
|
DISABLED until boxchecked changes via a change event — setting the
|
||||||
|
value alone leaves the button disabled, so executeTask() would no-op
|
||||||
|
and the update never submits (it just returns to the list). */
|
||||||
|
var bc = form.querySelector('input[name="boxchecked"]');
|
||||||
|
if (bc) {
|
||||||
|
bc.value = (data.cids || []).length;
|
||||||
|
bc.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.__msbResuming = true;
|
window.__msbResuming = true;
|
||||||
|
|
||||||
if (window.Joomla && typeof Joomla.submitbutton === 'function') {
|
/* Show a full-screen "update is running" overlay so the user gets clear
|
||||||
|
feedback that the update is proceeding while the re-fired POST runs.
|
||||||
|
It persists until the page navigates away (the update finishes and
|
||||||
|
redirects), matching the black full-screen backup screen. */
|
||||||
|
showUpdateOverlay();
|
||||||
|
|
||||||
|
/* Re-fire the toolbar action. The `task` attribute is on the OUTER
|
||||||
|
<joomla-toolbar-button>, but its click handler is bound to the INNER
|
||||||
|
<button>/<a>, so click that inner element (clicking the wrapper does
|
||||||
|
nothing). Fall back to Joomla.submitbutton if the button isn't found. */
|
||||||
|
window.setTimeout(function () {
|
||||||
|
var wc = document.querySelector('[task="' + data.task + '"]');
|
||||||
|
var inner = wc ? (wc.querySelector('button, a') || wc) : null;
|
||||||
|
|
||||||
|
if (inner) {
|
||||||
|
inner.click();
|
||||||
|
} else if (window.Joomla && typeof Joomla.submitbutton === 'function') {
|
||||||
Joomla.submitbutton(data.task);
|
Joomla.submitbutton(data.task);
|
||||||
} else if (typeof window.submitbutton === 'function') {
|
|
||||||
window.submitbutton(data.task);
|
|
||||||
}
|
}
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wrap Joomla.submitbutton to intercept the update/uninstall tasks. */
|
if (document.readyState === 'loading') {
|
||||||
function wrapSubmit() {
|
document.addEventListener('DOMContentLoaded', resume);
|
||||||
var J = window.Joomla;
|
} else {
|
||||||
|
resume();
|
||||||
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">
|
<extension type="plugin" group="system" method="upgrade">
|
||||||
<name>System - MokoSuiteBackup</name>
|
<name>System - MokoSuiteBackup</name>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ defined('_JEXEC') or die;
|
|||||||
use Joomla\CMS\Component\ComponentHelper;
|
use Joomla\CMS\Component\ComponentHelper;
|
||||||
use Joomla\CMS\Document\HtmlDocument;
|
use Joomla\CMS\Document\HtmlDocument;
|
||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
use Joomla\CMS\Plugin\CMSPlugin;
|
||||||
use Joomla\CMS\Router\Route;
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Session\Session;
|
use Joomla\CMS\Session\Session;
|
||||||
@@ -28,6 +29,12 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
|
|||||||
{
|
{
|
||||||
protected $autoloadLanguage = true;
|
protected $autoloadLanguage = true;
|
||||||
|
|
||||||
|
/** @var array<string,bool> Per-request guard: at most one pre-action backup per request (dedupes a batch update). */
|
||||||
|
private array $preactionRan = [];
|
||||||
|
|
||||||
|
/** @var array<string,bool> Per-request cache of the one-shot "client already backed up" skip flag. */
|
||||||
|
private array $preactionSkip = [];
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
public static function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -223,23 +230,21 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
|
|||||||
return;
|
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->getWebAssetManager()->useScript('core');
|
||||||
|
|
||||||
|
/* No time-based "recently backed up" suppression here: each distinct
|
||||||
|
Update/Uninstall click should get its own full-screen backup. The
|
||||||
|
re-fired click after a backup is guarded client-side by
|
||||||
|
window.__msbResuming, and the synchronous server-side fallback is
|
||||||
|
deduped per-request + by a one-shot skip flag (see runPreActionBackup),
|
||||||
|
so there is no double backup without blocking distinct actions. */
|
||||||
$doc->addScriptOptions('plg_system_mokosuitebackup.installer', [
|
$doc->addScriptOptions('plg_system_mokosuitebackup.installer', [
|
||||||
'runbackupUrl' => Route::_('index.php?option=com_mokosuitebackup&view=runbackup&tmpl=component&autostart=1', false),
|
'runbackupUrl' => Route::_('index.php?option=com_mokosuitebackup&view=runbackup&tmpl=component&autostart=1', false),
|
||||||
'profileId' => (int) $params->get('default_profile', 1),
|
'profileId' => (int) $params->get('default_profile', 1),
|
||||||
'backupBeforeUpdate' => $view === 'update' && (bool) $beforeUpdate,
|
'backupBeforeUpdate' => $view === 'update' && (bool) $beforeUpdate,
|
||||||
'backupBeforeUninstall' => $view === 'manage' && (bool) $beforeUninst,
|
'backupBeforeUninstall' => $view === 'manage' && (bool) $beforeUninst,
|
||||||
'recentBackup' => $recent,
|
'recentBackup' => false,
|
||||||
|
'updateRunningText' => Text::_('PLG_SYSTEM_MOKOSUITEBACKUP_UPDATE_RUNNING'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$doc->addScript(Uri::root(true) . '/media/plg_system_mokosuitebackup/js/installer-backup.js', [], ['defer' => true]);
|
$doc->addScript(Uri::root(true) . '/media/plg_system_mokosuitebackup/js/installer-backup.js', [], ['defer' => true]);
|
||||||
@@ -313,16 +318,20 @@ JS;
|
|||||||
|
|
||||||
$session = Factory::getSession();
|
$session = Factory::getSession();
|
||||||
|
|
||||||
// Returned from the backup screen: arm the throttle so the synchronous
|
// Returned from the backup screen: mark this core update backed up so we
|
||||||
// onExtensionBeforeUpdate backup doesn't run again, then let it proceed.
|
// don't redirect to the backup screen again on a page revisit, then let
|
||||||
|
// it proceed. Uses a DEDICATED core-update key — a core update does not
|
||||||
|
// fire onExtensionBefore(Update|Uninstall), so it must not touch the
|
||||||
|
// extension pre-action flags (which would suppress extension backups).
|
||||||
if ((int) $input->getInt('is_backed_up', 0) === 1) {
|
if ((int) $input->getInt('is_backed_up', 0) === 1) {
|
||||||
$session->set('mokosuitebackup.preaction_backup_before_update', time());
|
$session->set('mokosuitebackup.core_update_backed_up', time());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backed up recently already — let the update proceed.
|
// Backed up this core update recently already — let the update proceed
|
||||||
$lastRun = (int) $session->get('mokosuitebackup.preaction_backup_before_update', 0);
|
// without redirecting to the backup screen again.
|
||||||
|
$lastRun = (int) $session->get('mokosuitebackup.core_update_backed_up', 0);
|
||||||
|
|
||||||
if ($lastRun > 0 && (time() - $lastRun) < 600) {
|
if ($lastRun > 0 && (time() - $lastRun) < 600) {
|
||||||
return;
|
return;
|
||||||
@@ -525,17 +534,37 @@ JS;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throttle: only run once per 10 minutes to prevent duplicate
|
/* One backup per Update action — NOT a 10-minute time throttle. The old
|
||||||
// backups when multiple extensions are updated in a batch
|
600s window suppressed every extension's pre-action backup for 10
|
||||||
$session = Factory::getSession();
|
minutes after any single one ran (e.g. after updating this very
|
||||||
$sessionKey = 'mokosuitebackup.preaction_' . $paramName;
|
extension), so distinct updates silently went un-backed-up. Instead:
|
||||||
$lastRun = $session->get($sessionKey, 0);
|
|
||||||
|
|
||||||
if (time() - $lastRun < 600) {
|
(a) dedupe within a single request — a batch update fires this handler
|
||||||
|
once per extension, all in the same request, so back up only once;
|
||||||
|
(b) honour a one-shot session skip flag set by ajax.preupdateAck when
|
||||||
|
the client-side full-screen backup already ran and is re-firing the
|
||||||
|
real update/uninstall — consumed once, so the NEXT distinct action
|
||||||
|
backs up again. */
|
||||||
|
if (!empty($this->preactionRan[$paramName])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$session->set($sessionKey, time());
|
$session = Factory::getSession();
|
||||||
|
$skipKey = 'mokosuitebackup.skip_preaction_' . $paramName;
|
||||||
|
|
||||||
|
if (!array_key_exists($paramName, $this->preactionSkip)) {
|
||||||
|
$this->preactionSkip[$paramName] = (bool) $session->get($skipKey, false);
|
||||||
|
$session->set($skipKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->preactionSkip[$paramName]) {
|
||||||
|
// Client already backed up; skip this whole re-fired (possibly batch) request.
|
||||||
|
$this->preactionRan[$paramName] = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->preactionRan[$paramName] = true;
|
||||||
|
|
||||||
$profileId = (int) $params->get('default_profile', 1);
|
$profileId = (int) $params->get('default_profile', 1);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="task" method="upgrade">
|
<extension type="plugin" group="task" method="upgrade">
|
||||||
<name>Task - MokoSuiteBackup</name>
|
<name>Task - MokoSuiteBackup</name>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="webservices" method="upgrade">
|
<extension type="plugin" group="webservices" method="upgrade">
|
||||||
<name>Web Services - MokoSuiteBackup</name>
|
<name>Web Services - MokoSuiteBackup</name>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>Package - MokoSuiteBackup</name>
|
<name>Package - MokoSuiteBackup</name>
|
||||||
<packagename>mokosuitebackup</packagename>
|
<packagename>mokosuitebackup</packagename>
|
||||||
<version>02.58.26</version>
|
<version>02.64.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
Reference in New Issue
Block a user