Merge remote-tracking branch 'origin/main' into fix/remote-storage-cleanup
# Conflicts: # SECURITY.md # source/packages/MokoSuiteClient # source/packages/com_mokosuitebackup/mokosuitebackup.xml # source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml # source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml # source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml # source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml # source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml # source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml # source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml # source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml # source/pkg_mokosuitebackup.xml # source/script.php
This commit is contained in:
@@ -64,10 +64,14 @@ jobs:
|
||||
promote-rc:
|
||||
name: Promote to RC
|
||||
runs-on: release
|
||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
||||
if: >-
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
!startsWith(github.event.repository.name, 'Template-') &&
|
||||
(
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
)
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -164,9 +168,13 @@ jobs:
|
||||
release:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: release
|
||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
||||
if: >-
|
||||
github.event.pull_request.merged == true ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||
!startsWith(github.event.repository.name, 'Template-') &&
|
||||
(
|
||||
github.event.pull_request.merged == true ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||
)
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -33,7 +33,8 @@ jobs:
|
||||
run: |
|
||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||
# URL-encode the branch name's slashes (no PHP dependency on the runner)
|
||||
ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
|
||||
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.gitea/workflows/ci-generic.yml
|
||||
# PATH: /.mokogitea/workflows/ci-generic.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||
|
||||
@@ -32,6 +32,8 @@ jobs:
|
||||
lint:
|
||||
name: Lint & Validate
|
||||
runs-on: ubuntu-latest
|
||||
# Skip on template repos (Template-*) — they hold placeholder scaffolding, not buildable source.
|
||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -130,6 +132,9 @@ jobs:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
# Run only when lint succeeded; always() forces evaluation so a skipped
|
||||
# lint (e.g. template repos) skips this job cleanly instead of hanging.
|
||||
if: ${{ always() && needs.lint.result == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/cleanup.yml
|
||||
# PATH: /.mokogitea/workflows/cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/notify.yml
|
||||
# PATH: /.mokogitea/workflows/notify.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.CI
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# INGROUP: mokocli.CI
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: PR gate — branch policy + code validation before merge
|
||||
@@ -47,15 +47,15 @@ jobs:
|
||||
fi
|
||||
;;
|
||||
fix/*|bugfix/*)
|
||||
if [ "$BASE" != "dev" ]; then
|
||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||
REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
patch/*)
|
||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||
REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
hotfix/*)
|
||||
@@ -86,7 +86,8 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -126,6 +127,8 @@ jobs:
|
||||
validate:
|
||||
name: Validate PR
|
||||
runs-on: ubuntu-latest
|
||||
# Skip on template repos (Template-*) — no real manifest/source/changelog to validate.
|
||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -147,11 +150,12 @@ jobs:
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||
# Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used.
|
||||
API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata"
|
||||
PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)"
|
||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
echo "Detected platform: $PLATFORM"
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||
@@ -492,6 +496,9 @@ jobs:
|
||||
name: Build RC Package
|
||||
runs-on: ubuntu-latest
|
||||
needs: [branch-policy, validate]
|
||||
# Run only when both gates succeeded; always() forces evaluation so a skipped
|
||||
# validate (e.g. template repos) skips this job cleanly instead of hanging.
|
||||
if: ${{ always() && needs.branch-policy.result == 'success' && needs.validate.result == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Trigger RC pre-release
|
||||
|
||||
@@ -48,9 +48,13 @@ jobs:
|
||||
build:
|
||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||
runs-on: release
|
||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'push'
|
||||
!startsWith(github.event.repository.name, 'Template-') &&
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'push'
|
||||
)
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Sync Workflows to Repos
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.mokogitea/workflows/**'
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
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 }}
|
||||
+6
-47
@@ -1,55 +1,14 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
## [02.52.24] --- 2026-06-30
|
||||
## [02.55.00] --- 2026-07-04
|
||||
|
||||
## [02.55.00] --- 2026-07-04
|
||||
|
||||
## [02.52.24] --- 2026-06-30
|
||||
## [02.54.00] --- 2026-07-04
|
||||
|
||||
## [02.52.22] --- 2026-06-30
|
||||
## [02.54.00] --- 2026-07-04
|
||||
|
||||
### Added
|
||||
- Cancel Stalled toolbar button on Backup Records view to cancel backups stuck in "running" status
|
||||
- New ACL permission `mokosuitebackup.backup.cancel` for cancel stalled action
|
||||
- AJAX endpoint `ajax.cancelBackup` for programmatic/API cancel
|
||||
- Auto-timeout failsafe: preflight auto-cancels "running" backups older than 30 minutes
|
||||
- Pre-extension-update backup progress modal (Bootstrap 5 modal with stepped AJAX progress bar)
|
||||
- New `warning` backup status for records where archive succeeded but remote upload failed
|
||||
- Warning-status records are downloadable, browsable, restorable, and purgeable
|
||||
- Warning status filter option in Backup Records dropdown
|
||||
- Yellow "Warning" badge in backup list, detail view, and cpanel module
|
||||
## [02.54.00] --- 2026-07-04
|
||||
|
||||
### Fixed
|
||||
- Pre-update backup ran synchronously with no browser feedback — page hung until complete
|
||||
- Stalled backups permanently blocked future backups for the same profile
|
||||
- Preflight error message now directs users to Cancel Stalled action
|
||||
- Backups with failed remote uploads were marked as "complete", hiding the upload failure
|
||||
|
||||
## [02.52.18] --- 2026-06-30
|
||||
|
||||
## [01.45.00] --- 2026-06-28
|
||||
|
||||
## [01.43.35] --- 2026-06-28
|
||||
|
||||
### Added
|
||||
- Customizable restore script filename per backup profile (reduces discoverability on remote servers)
|
||||
- MokoRestore standalone mode: multi-ZIP selector when multiple backup archives are present
|
||||
- MokoRestore preflight: Joomla installation detection warning before overwriting an existing site
|
||||
- MokoRestore error handling: try/catch on fetch calls, HTTP status checks, JSON parse recovery
|
||||
- Download button on individual backup record detail toolbar
|
||||
- Profile column in backup records list links to the profile edit view
|
||||
|
||||
### Changed
|
||||
- Moved download, browse archive, and view log actions from backup list rows into the individual backup record view
|
||||
- Removed "Run Backup" / "Backup Now" buttons from profiles list, profile edit toolbar, and backup records view (backups are triggered from the dashboard only)
|
||||
- Removed ordering field from profiles; default sort is now by ID ascending
|
||||
- MokoRestore cleanup and security messages now reference the actual script filename instead of hardcoded "restore.php"
|
||||
|
||||
### Fixed
|
||||
- SSH key indicator detection and missing delete language key
|
||||
- Bootstrap 5 modal conversion for snapshots view (data-bs-dismiss, modal-footer, getOrCreateInstance)
|
||||
- ntfy default URL changed from ntfy.sh to ntfy.mokoconsulting.tech
|
||||
- Untranslated JFIELD_ORDERING_ASC / JFIELD_ORDERING_LABEL language keys replaced with component-specific keys
|
||||
- Options page title now shows "MokoSuiteBackup Options" instead of raw language key
|
||||
- Profile dropdown IDs in backup records and dashboard show "#ID — Title (type)" format
|
||||
- MokoRestore stalling: unhandled promise rejections from network errors or non-JSON responses left UI in loading state
|
||||
## [02.53.00] --- 2026-07-04
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 02.52.25
|
||||
VERSION: 02.55.00
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
Submodule source/packages/MokoSuiteClient updated: bb0b6ecac4...9df6bea4b7
@@ -5,6 +5,7 @@
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
||||
COM_MOKOJOOMBACKUP_SHORT="Backup"
|
||||
COM_MOKOJOOMBACKUP_CONFIGURATION="MokoSuiteBackup Options"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
||||
|
||||
@@ -22,7 +23,7 @@ COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE="Restore Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
|
||||
|
||||
; Dashboard view
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoSuiteBackup Dashboard"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
|
||||
@@ -44,14 +45,14 @@ COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND="Backup Trend (30 days)"
|
||||
; Backups view
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED="%d backup records deleted."
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED_1="%d backup record deleted."
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Records"
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records"
|
||||
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
||||
COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW="Backup Now"
|
||||
COM_MOKOJOOMBACKUP_DOWNLOAD="Download"
|
||||
|
||||
; Backup detail view
|
||||
COM_MOKOJOOMBACKUP_BACKUP_DETAIL="Backup Detail"
|
||||
COM_MOKOJOOMBACKUP_BACKUP_DETAIL="Detail"
|
||||
COM_MOKOJOOMBACKUP_VIEW_LOG="Backup Log"
|
||||
COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE="Browse Archive Contents"
|
||||
COM_MOKOJOOMBACKUP_BROWSE_COL_NAME="Name"
|
||||
@@ -75,7 +76,7 @@ COM_MOKOJOOMBACKUP_FIELD_DB_SIZE="DB Size"
|
||||
COM_MOKOJOOMBACKUP_FIELD_REMOTE="Remote Path"
|
||||
|
||||
; Profiles view
|
||||
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Backup Profiles"
|
||||
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Profiles"
|
||||
COM_MOKOJOOMBACKUP_PROFILES_TABLE_CAPTION="Table of backup profiles"
|
||||
COM_MOKOJOOMBACKUP_NO_PROFILES="No backup profiles found."
|
||||
COM_MOKOJOOMBACKUP_PROFILE_NEW="New Profile"
|
||||
@@ -250,9 +251,9 @@ COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE_DESC="Send an email when a backup fails.
|
||||
; Retention
|
||||
COM_MOKOJOOMBACKUP_FIELDSET_RETENTION="Retention"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS="Keep Backups (days)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS_DESC="Delete completed backups from this profile older than this many days. Set to 0 to use the global default from component options."
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS_DESC="Delete completed backups from this profile older than this many days. Set to 0 for unlimited (keep by age disabled)."
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT="Keep Backups (count)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT_DESC="Maximum number of completed backups to keep for this profile. Oldest are removed first. Set to 0 to use the global default from component options."
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT_DESC="Maximum number of completed backups to keep for this profile. Oldest are removed first. Set to 0 for unlimited (keep by count disabled)."
|
||||
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_SPACER_DESC="<strong>Push Notifications (ntfy)</strong> — Send instant push notifications to your phone or desktop via <a href='https://ntfy.sh' target='_blank'>ntfy.sh</a> or a self-hosted ntfy server."
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC="ntfy Topic"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
||||
COM_MOKOJOOMBACKUP_SHORT="Backup"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
||||
COM_MOKOJOOMBACKUP_SHORT="Backup"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||
@@ -18,7 +19,7 @@ COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD_DESC="Allows users in this group to d
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE="Restore Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
|
||||
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoSuiteBackup Dashboard"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
|
||||
@@ -30,8 +31,8 @@ COM_MOKOJOOMBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health"
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
||||
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Backup Profiles"
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Records"
|
||||
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Profiles"
|
||||
COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW="Backup Now"
|
||||
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
||||
COM_MOKOJOOMBACKUP_NO_PROFILES="No backup profiles found."
|
||||
@@ -133,3 +134,10 @@ COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED_1="%d backup record deleted."
|
||||
; ACL - Cancel
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files."
|
||||
|
||||
; Retention (per-profile)
|
||||
COM_MOKOJOOMBACKUP_FIELDSET_RETENTION="Retention"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS="Keep Backups (days)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_DAYS_DESC="Delete completed backups from this profile older than this many days. Set to 0 for unlimited (keep by age disabled)."
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT="Keep Backups (count)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RETENTION_COUNT_DESC="Maximum number of completed backups to keep for this profile. Oldest are removed first. Set to 0 for unlimited (keep by count disabled)."
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
||||
COM_MOKOJOOMBACKUP_SHORT="Backup"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>MokoSuiteBackup</name>
|
||||
<version>02.52.25</version>
|
||||
<name>Component - MokoSuiteBackup</name>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>COM_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
<description>Full-site backup and restore for Joomla — database, files, and configuration.</description>
|
||||
|
||||
<namespace path="src">Joomla\Component\MokoSuiteBackup</namespace>
|
||||
|
||||
@@ -37,20 +37,20 @@
|
||||
</update>
|
||||
|
||||
<administration>
|
||||
<menu img="class:archive">COM_MOKOJOOMBACKUP</menu>
|
||||
<menu img="class:archive">Backup</menu>
|
||||
<submenu>
|
||||
<menu link="option=com_mokosuitebackup&view=dashboard"
|
||||
img="class:home"
|
||||
alt="Dashboard">COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD</menu>
|
||||
alt="Dashboard">Dashboard</menu>
|
||||
<menu link="option=com_mokosuitebackup&view=backups"
|
||||
img="class:database"
|
||||
alt="Backups">COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS</menu>
|
||||
alt="Backups">Backup Records</menu>
|
||||
<menu link="option=com_mokosuitebackup&view=snapshots"
|
||||
img="class:camera"
|
||||
alt="Snapshots">COM_MOKOJOOMBACKUP_SUBMENU_SNAPSHOTS</menu>
|
||||
alt="Snapshots">Content Snapshots</menu>
|
||||
<menu link="option=com_mokosuitebackup&view=profiles"
|
||||
img="class:cog"
|
||||
alt="Profiles">COM_MOKOJOOMBACKUP_SUBMENU_PROFILES</menu>
|
||||
alt="Profiles">Backup Profiles</menu>
|
||||
</submenu>
|
||||
<files folder=".">
|
||||
<filename>access.xml</filename>
|
||||
|
||||
@@ -23,8 +23,8 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
|
||||
`notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs',
|
||||
`notify_on_success` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`notify_on_failure` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`retention_days` INT(11) NOT NULL DEFAULT 0 COMMENT '0 = use global default',
|
||||
`retention_count` INT(11) NOT NULL DEFAULT 0 COMMENT '0 = use global default',
|
||||
`retention_days` INT(11) NOT NULL DEFAULT 0 COMMENT 'Delete backups older than N days; 0 = unlimited',
|
||||
`retention_count` INT(11) NOT NULL DEFAULT 0 COMMENT 'Keep newest N backups; 0 = unlimited',
|
||||
`ntfy_topic` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy topic name',
|
||||
`ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL',
|
||||
`ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional)',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.52.27 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.53.00 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.54.00 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.55.00 — no schema changes */
|
||||
@@ -361,6 +361,17 @@ class BackupEngine
|
||||
NotificationSender::send($profile, $update, false, "Remote upload failed — see backup log for details.\n\n" . implode("\n", $this->log));
|
||||
}
|
||||
|
||||
// Enforce per-profile retention (age and/or copy count).
|
||||
try {
|
||||
$pruned = RetentionManager::prune($db, $profile);
|
||||
|
||||
if ($pruned > 0) {
|
||||
$this->log('Retention: pruned ' . $pruned . ' old backup(s)');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: retention pass failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Dispatch event for actionlog and other listeners
|
||||
$this->dispatchAfterRun(true, $recordId, $description, $profileId, $origin);
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
|
||||
|
||||
/**
|
||||
* Enforces per-profile backup retention.
|
||||
*
|
||||
* A profile may cap retained backups by age (retention_days) and/or by
|
||||
* number of copies (retention_count). A backup is pruned when EITHER rule
|
||||
* matches: it is older than retention_days OR it falls outside the newest
|
||||
* retention_count copies. Deleting a record also removes its archive and
|
||||
* log file, mirroring the Backup table's delete().
|
||||
*/
|
||||
final class RetentionManager
|
||||
{
|
||||
/**
|
||||
* Prune old backups for a profile according to its retention settings.
|
||||
*
|
||||
* Called after a backup completes. Only 'complete' and 'warning' records
|
||||
* are considered — pending/running/failed records are never pruned here.
|
||||
*
|
||||
* @param object $db Database driver
|
||||
* @param object $profile Profile row (needs id, retention_days, retention_count)
|
||||
*
|
||||
* @return int Number of backup records deleted
|
||||
*/
|
||||
public static function prune(object $db, object $profile): int
|
||||
{
|
||||
$days = (int) ($profile->retention_days ?? 0);
|
||||
$count = (int) ($profile->retention_count ?? 0);
|
||||
|
||||
// No retention configured — nothing to do.
|
||||
if ($days <= 0 && $count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Newest first, so the index is the copy's position from the top.
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName(['id', 'absolute_path', 'backupstart']))
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id)
|
||||
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')')
|
||||
->order($db->quoteName('backupstart') . ' DESC');
|
||||
$db->setQuery($query);
|
||||
$records = $db->loadObjectList() ?: [];
|
||||
|
||||
if (empty($records)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$cutoffTs = $days > 0 ? (time() - ($days * 86400)) : null;
|
||||
$deleted = 0;
|
||||
|
||||
foreach ($records as $index => $record) {
|
||||
$tooOld = $cutoffTs !== null && strtotime((string) $record->backupstart) < $cutoffTs;
|
||||
$overCount = $count > 0 && $index >= $count;
|
||||
|
||||
// Delete-if-either: prune when age OR count rule is exceeded.
|
||||
if (!$tooOld && !$overCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::deleteRecord($db, $record)) {
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single backup record and its on-disk archive + log file.
|
||||
*
|
||||
* The DB row is removed first; the files are only unlinked if that
|
||||
* succeeds, so a failed delete never orphans the record from its files.
|
||||
*/
|
||||
private static function deleteRecord(object $db, object $record): bool
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $record->id);
|
||||
$db->setQuery($query);
|
||||
|
||||
try {
|
||||
$db->execute();
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: retention could not delete record ' . $record->id . ': ' . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$archivePath = (string) ($record->absolute_path ?? '');
|
||||
|
||||
if ($archivePath !== '' && is_file($archivePath)) {
|
||||
@unlink($archivePath);
|
||||
|
||||
$logPath = BackupDirectory::logPathFromArchive($archivePath);
|
||||
|
||||
if (is_file($logPath)) {
|
||||
@unlink($logPath);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -602,6 +602,13 @@ class SteppedBackupEngine
|
||||
if ($uploadFailed) {
|
||||
NotificationSender::send($profile, $record, false, "Remote upload failed — see backup log for details.\n\n" . $logContent);
|
||||
}
|
||||
|
||||
// Enforce per-profile retention (age and/or copy count).
|
||||
$pruned = RetentionManager::prune($db, $profile);
|
||||
|
||||
if ($pruned > 0) {
|
||||
$session->log('Retention: pruned ' . $pruned . ' old backup(s)');
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: SteppedBackupEngine notification failed: ' . $e->getMessage());
|
||||
|
||||
@@ -37,7 +37,7 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
|
||||
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class HtmlView extends BaseHtmlView
|
||||
{
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
|
||||
|
||||
if ($user->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
|
||||
ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE', true);
|
||||
|
||||
@@ -52,7 +52,7 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_TITLE'), 'archive');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_TITLE'), 'archive');
|
||||
ToolbarHelper::preferences('com_mokosuitebackup');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class HtmlView extends BaseHtmlView
|
||||
? $user->authorise('core.create', 'com_mokosuitebackup')
|
||||
: $user->authorise('core.edit', 'com_mokosuitebackup');
|
||||
|
||||
ToolbarHelper::title(Text::_($title), 'cog');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_($title), 'cog');
|
||||
|
||||
if ($canSave) {
|
||||
ToolbarHelper::apply('profile.apply');
|
||||
|
||||
@@ -49,7 +49,7 @@ class HtmlView extends BaseHtmlView
|
||||
{
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_PROFILES_TITLE'), 'cog');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_PROFILES_TITLE'), 'cog');
|
||||
|
||||
if ($user->authorise('core.create', 'com_mokosuitebackup')) {
|
||||
ToolbarHelper::addNew('profile.add');
|
||||
|
||||
@@ -38,7 +38,7 @@ class HtmlView extends BaseHtmlView
|
||||
{
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SNAPSHOTS_TITLE'), 'camera');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_SNAPSHOTS_TITLE'), 'camera');
|
||||
|
||||
if ($user->authorise('mokosuitebackup.snapshot.manage', 'com_mokosuitebackup')) {
|
||||
ToolbarHelper::custom('snapshots.create', 'plus', '', 'COM_MOKOJOOMBACKUP_SNAPSHOT_CREATE', false);
|
||||
|
||||
@@ -684,19 +684,37 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
var PURGE_TOKEN = <?php echo json_encode($ajaxToken); ?>;
|
||||
var purgeCountTimer = null;
|
||||
|
||||
// Intercept Purge toolbar button to show the modal
|
||||
// Reset modal state and show it.
|
||||
function openPurgeModal() {
|
||||
document.getElementById('mb-purge-date').value = '';
|
||||
document.getElementById('mb-purge-count-wrapper').style.display = 'none';
|
||||
document.getElementById('mb-purge-none-wrapper').style.display = 'none';
|
||||
document.getElementById('mb-purge-submit').disabled = true;
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-purge-modal')).show();
|
||||
}
|
||||
|
||||
// Primary: wrap Joomla.submitbutton so the Purge toolbar button opens the
|
||||
// modal instead of submitting the no-op backups.purgeModal task. This is
|
||||
// resilient to how the Atum toolbar renders the button markup.
|
||||
if (window.Joomla && typeof Joomla.submitbutton === 'function') {
|
||||
var origSubmitbutton = Joomla.submitbutton;
|
||||
Joomla.submitbutton = function(task) {
|
||||
if (task === 'backups.purgeModal') {
|
||||
openPurgeModal();
|
||||
return false;
|
||||
}
|
||||
return origSubmitbutton.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Fallback: if the button still exposes an inline onclick, bind directly.
|
||||
var purgeBtn = document.querySelector('[onclick*="backups.purgeModal"], .button-trash');
|
||||
if (purgeBtn) {
|
||||
purgeBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Reset modal state
|
||||
document.getElementById('mb-purge-date').value = '';
|
||||
document.getElementById('mb-purge-count-wrapper').style.display = 'none';
|
||||
document.getElementById('mb-purge-none-wrapper').style.display = 'none';
|
||||
document.getElementById('mb-purge-submit').disabled = true;
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-purge-modal')).show();
|
||||
openPurgeModal();
|
||||
return false;
|
||||
}, true);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ $token = Session::getFormToken();
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<?php echo $this->form->renderFieldset('archive'); ?>
|
||||
<?php echo $this->form->renderFieldset('retention'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
-->
|
||||
<extension type="module" client="administrator" method="upgrade">
|
||||
<name>mod_mokosuitebackup_cpanel</name>
|
||||
<version>02.52.25</version>
|
||||
<name>Module - MokoSuiteBackup - cPanel</name>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>MOD_MOKOSUITEBACKUP_CPANEL_DESCRIPTION</description>
|
||||
<description>Displays backup status, Backup Now buttons, and quick links on the admin dashboard.</description>
|
||||
|
||||
<namespace path="src">Joomla\Module\MokoSuiteBackupCpanel</namespace>
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
-->
|
||||
<extension type="plugin" group="actionlog" method="upgrade">
|
||||
<name>Action Log - MokoSuiteBackup</name>
|
||||
<version>02.52.25</version>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
<description>Logs MokoSuiteBackup actions (backup, restore, profile changes) to User Action Logs.</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\Actionlog\MokoSuiteBackup</namespace>
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
-->
|
||||
<extension type="plugin" group="console" method="upgrade">
|
||||
<name>Console - MokoSuiteBackup</name>
|
||||
<version>02.52.25</version>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
<description>CLI commands for MokoSuiteBackup: run, list, profiles, restore, cleanup.</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\Console\MokoSuiteBackup</namespace>
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
-->
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoSuiteBackup</name>
|
||||
<version>02.52.25</version>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
<description>Automatically triggers a backup before extension installs or updates.</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\Content\MokoSuiteBackup</namespace>
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="quickicon" method="upgrade">
|
||||
<name>Quick Icon - MokoSuiteBackup</name>
|
||||
<version>02.52.25</version>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
<description>Shows backup status on the administrator dashboard.</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\Quickicon\MokoSuiteBackup</namespace>
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteBackup</name>
|
||||
<version>02.52.25</version>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
<description>Automatic cleanup of expired backup archives and scheduled backup triggers.</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\System\MokoSuiteBackup</namespace>
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteBackup</name>
|
||||
<version>02.52.25</version>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
<description>Scheduled task plugin for MokoSuiteBackup. Run backup profiles on a schedule via Joomla's Scheduled Tasks.</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\Task\MokoSuiteBackup</namespace>
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteBackup</name>
|
||||
<version>02.52.25</version>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
<description>REST API for remote backup management.</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\WebServices\MokoSuiteBackup</namespace>
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteBackup</name>
|
||||
<packagename>mokosuitebackup</packagename>
|
||||
<version>02.52.25</version>
|
||||
<version>02.55.00</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PKG_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
<description>Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API.</description>
|
||||
|
||||
<scriptfile>script.php</scriptfile>
|
||||
|
||||
|
||||
+57
-44
@@ -12,6 +12,7 @@ defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Installer\InstallerAdapter;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
class Pkg_MokoSuiteBackupInstallerScript
|
||||
@@ -73,22 +74,25 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
|
||||
/* Save download key before Joomla re-registers the update site */
|
||||
if ($type === 'update') {
|
||||
$this->preflight_saveKey();
|
||||
$this->backupDownloadKey();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before install/update to preserve the download key.
|
||||
*
|
||||
* Joomla re-registers update sites from the manifest on every update,
|
||||
* which can reset the extra_query (download key). We save it here
|
||||
* and restore it in postflight.
|
||||
* The download key cached during preflight so it survives an update.
|
||||
*/
|
||||
private ?string $savedDownloadKey = null;
|
||||
|
||||
public function preflight_saveKey(): void
|
||||
/**
|
||||
* Cache the existing download key from the update sites table before update runs.
|
||||
*
|
||||
* Joomla re-registers update sites from the manifest on every update, which
|
||||
* can reset the extra_query (download key). We save it here and restore it
|
||||
* in postflight.
|
||||
*/
|
||||
private function backupDownloadKey(): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
@@ -108,19 +112,16 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitebackup'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
||||
->setLimit(1);
|
||||
$db->setQuery($query);
|
||||
$key = $db->loadResult();
|
||||
|
||||
if (!empty($key)) {
|
||||
$this->savedDownloadKey = $key;
|
||||
$db->setQuery($query);
|
||||
$extraQuery = (string) $db->loadResult();
|
||||
|
||||
if (!empty($extraQuery)) {
|
||||
parse_str($extraQuery, $output);
|
||||
$this->savedDownloadKey = $output['dlid'] ?? $extraQuery;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: Could not save download key: ' . $e->getMessage());
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'MokoSuiteBackup could not preserve your download/license key before the update. '
|
||||
. 'Please verify your license key is still configured in System → Update Sites after this update completes.',
|
||||
'warning'
|
||||
);
|
||||
Log::add('MokoSuiteBackup: Could not backup download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +139,8 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
return;
|
||||
}
|
||||
|
||||
/* Restore download key if it was saved before update */
|
||||
if ($this->savedDownloadKey !== null) {
|
||||
/* Restore the download key preserved before the update re-registered the site */
|
||||
if ($type === 'update') {
|
||||
$this->restoreDownloadKey();
|
||||
}
|
||||
|
||||
@@ -168,14 +169,17 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
/* Sync submenu icons in #__menu (Joomla doesn't update icons on upgrades) */
|
||||
$this->syncMenuIcons();
|
||||
|
||||
/* Warn if no license key configured */
|
||||
$this->warnMissingLicenseKey();
|
||||
|
||||
/* Migrate profiles with old default backup_dir values to [DEFAULT_DIR] placeholder */
|
||||
$this->migrateDefaultBackupDir();
|
||||
|
||||
/* Remind user to review backup profile settings */
|
||||
/* Install completion notice (install and update) */
|
||||
$this->installSuccessful();
|
||||
|
||||
if ($type === 'install') {
|
||||
/* Fresh install never carries a download key — prompt for one */
|
||||
$this->warnMissingLicenseKey();
|
||||
|
||||
/* Remind user to review backup profile settings */
|
||||
$profileUrl = Route::_('index.php?option=com_mokosuitebackup&view=profiles');
|
||||
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
@@ -640,48 +644,57 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitebackup'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
||||
->setLimit(1);
|
||||
|
||||
$db->setQuery($query);
|
||||
$updateSiteId = (int) $db->loadResult();
|
||||
|
||||
if ($updateSiteId > 0) {
|
||||
if ($updateSiteId > 0 && !empty($this->savedDownloadKey)) {
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__update_sites'))
|
||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey))
|
||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $this->savedDownloadKey))
|
||||
->where($db->quoteName('update_site_id') . ' = ' . $updateSiteId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: Could not restore download key: ' . $e->getMessage());
|
||||
Log::add('MokoSuiteBackup: Could not restore download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'MokoSuiteBackup: Your download/license key could not be preserved during the update. '
|
||||
. 'Please re-enter it in the Update Sites configuration to continue receiving updates.',
|
||||
'<h4>MokoSuiteBackup</h4>'
|
||||
. '<p>Your download/license key could not be preserved during the update.</p>'
|
||||
. '<p>Please re-enter it in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]=pkg_mokosuitebackup">Update Sites</a> manager to continue receiving updates.</p>',
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show post-install license key prompt.
|
||||
*/
|
||||
private function warnMissingLicenseKey(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>MokoSuiteBackup License Key Required</h4>'
|
||||
. '<p>A download/license key (DLID) is required to receive updates.</p>'
|
||||
. '<p>Enter your key in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]=pkg_mokosuitebackup">Update Sites</a> manager '
|
||||
. 'or contact <a class="btn btn-sm btn-warning ms-2" href="https://mokoconsulting.tech/support" target="_blank" rel="noopener">Moko Consulting Support</a> to obtain one.</p>',
|
||||
'warning'
|
||||
);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show install successful prompt.
|
||||
*/
|
||||
private function installSuccessful(): void
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>MokoSuiteBackup installed successfully!</h4>',
|
||||
'info'
|
||||
);
|
||||
|
||||
// Show post-install license key prompt
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<strong>Moko Consulting License Key Required</strong><br>'
|
||||
. 'A download key (DLID) is required to receive updates. '
|
||||
. 'Enter your key in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]=moko">Update Sites</a> manager '
|
||||
. 'or contact <a class="btn btn-sm btn-warning ms-2" href="https://mokoconsulting.tech/support">Moko Consulting Support</a> to obtain one.',
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
error_log('MokoSuiteBackup: License key prompt failed: ' . $e->getMessage());
|
||||
}
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user