7 Commits

Author SHA1 Message Date
Jonathan Miller be52bc048f feat: Joomla-styled standalone installer with provisioning wizard
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 2s
Complete rewrite of MokoRestore restore.php generator:
- 7-step wizard UI matching Joomla installer look and feel
- Pre-flight checks (PHP, extensions, disk space, backup detection)
- Archive extraction with password support and config pre-fill
- Database connection test and SQL import with error reporting
- Create or update configuration.php with fresh Joomla secret
- Super admin dropdown with bcrypt password reset
- Client provisioning: reset hits, clear sessions/cache/keys/tokens/logs
- Zero innerHTML — all DOM built with safe createElement/textContent
- Fully self-contained single PHP file, no external dependencies

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 12:48:42 -05:00
Jonathan Miller da9da3ef2c chore: move CLAUDE.md to .mokogitea/ directory
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Relocate CLAUDE.md from repo root to .mokogitea/ per project convention.
Content updated with focused, repo-specific architecture and rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:30:48 -05:00
Jonathan Miller a13f7ca6a6 chore: rename src/ to source/ per MokoStandards convention
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update all references in Makefile, manifest.xml, .gitignore, and CI
workflows (ci-joomla, pr-check, repo-health) to use source/ as the
primary directory with src/ as a fallback for compatibility.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 08:08:33 -05:00
Jonathan Miller 28fcc72ced fix: consolidate schema migrations to version within extension range
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Migrations 01.01.08 and 01.01.09 were never applied on upgrade because
their version numbers exceeded the extension version (01.01.07-dev).
Joomla skips migrations with version > extension version.

Consolidated into 01.01.02.sql which falls between 01.01.01 and
01.01.07-dev, ensuring existing installs receive the ALTER TABLE
statements for notify_user_groups and archive_name_format.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:36:05 -05:00
jmiller fdd004b345 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-06-06 12:32:27 +00:00
Jonathan Miller 2755ef709f feat: interactive directory tree browser for exclude filters
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Replace plain text ExcludeList with DirectoryFilter field that provides
a browsable server directory tree with checkboxes, removable pills, and
manual path entry. Backward compatible storage format.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:24:33 -05:00
Jonathan Miller df59fd7303 fix: dashboard tiles use onclick navigation, last backup links to record detail
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:13:26 -05:00
210 changed files with 1698 additions and 633 deletions
+1 -1
View File
@@ -151,7 +151,7 @@ package-lock.json
# PHP / Composer tooling
# ============================================================
vendor/
!src/media/vendor/
!source/media/vendor/
composer.lock
*.phar
codeception.phar
+19 -30
View File
@@ -1,45 +1,37 @@
# CLAUDE.md
# MokoJoomBackup
This file provides guidance to Claude Code when working with this repository.
Full-site backup and restore for Joomla — database, files, and configuration. Replaces Akeeba Backup Pro.
## Project Overview
**MokoJoomBackup** -- Full-site backup and restore for Joomla — database, files, and configuration
## Quick Reference
| Field | Value |
|---|---|
| **Platform** | joomla |
| **Language** | PHP |
| **Default branch** | main |
| **License** | GPL-3.0-or-later |
| **Package** | `pkg_mokobackup` |
| **Language** | PHP 8.1+ |
| **Branch** | develop on `dev`, merge to `main` (protected) |
| **Wiki** | [MokoJoomBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/wiki) |
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
## Common Commands
## Commands
```bash
make build # Build the project
make build # Build package ZIP
make lint # Run linters
make validate # Validate structure
make release # Full release pipeline
make minify # Minify CSS/JS assets
make clean # Clean build artifacts
```
```bash
composer install # Install PHP dependencies
```
## Architecture
This is a Joomla **package** extension (`pkg_mokobackup`) containing three sub-extensions:
Joomla **package** with four sub-extensions:
### com_mokobackup (Component)
- Admin backend for managing backup profiles and backup records
- Admin backend for managing backup profiles and records
- Backup engine: `Engine/BackupEngine`, `Engine/DatabaseDumper`, `Engine/FileScanner`, `Engine/Archiver`
- Joomla 4/5 MVC: Controllers, Models, Views, Tables
- Namespace: `Joomla\Component\MokoBackup\Administrator`
- Database tables: `#__mokobackup_profiles`, `#__mokobackup_records`
- DB tables: `#__mokobackup_profiles`, `#__mokobackup_records`
- CLI: `cli/mokobackup.php` for cron-based backups
### plg_system_mokobackup (System Plugin)
@@ -49,36 +41,33 @@ This is a Joomla **package** extension (`pkg_mokobackup`) containing three sub-e
### plg_task_mokobackup (Task Plugin)
- Integrates with Joomla's Scheduled Tasks (com_scheduler)
- Registers "Run Backup Profile" task type
- Each scheduled task selects a backup profile — create multiple tasks for different schedules
- Namespace: `Joomla\Plugin\Task\MokoBackup`
### plg_webservices_mokobackup (WebServices Plugin)
- REST API for remote backup management
- Wire-compatible with existing mcp_mokobackup MCP server
- REST API for remote backup management (wire-compatible with mcp_mokobackup)
- Endpoints: backup, backups, profiles, download, delete
- Namespace: `Joomla\Plugin\WebServices\MokoBackup`
### Database Schema
Two tables:
- `#__mokobackup_profiles` — backup profiles (name, description, config JSON, filters JSON)
- `#__mokobackup_records` — backup records (profile_id, status, origin, archive path, sizes, timestamps)
## Rules
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
- **Attribution**: use `Authored-by: Moko Consulting` in commits
- **Branch strategy**: develop on `dev`, merge to `main` for release
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
- **Attribution**: `Authored-by: Moko Consulting`
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Minification**: handled at build time (CI)
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
## Coding Standards
- PHP 8.1+ minimum
- Joomla 4/5 DI container pattern: `services/provider.php` > Extension class
- Joomla 4/5 DI container pattern: `services/provider.php` Extension class
- Legacy stub `.php` file required for plugin loader but empty
- `SubscriberInterface` for event subscription (not `on*` method naming)
- `bind() > check() > store()` for Table operations (not `save()`)
- `bind() check() store()` for Table operations (not `save()`)
- Language file placement: site (no `folder`) vs admin (`folder="administrator"`)
- SPDX license headers on all PHP files
+1 -1
View File
@@ -16,6 +16,6 @@
<build>
<language>PHP</language>
<package-type>joomla-extension</package-type>
<entry-point>src/</entry-point>
<entry-point>source/</entry-point>
</build>
</moko-platform>
+6 -6
View File
@@ -67,7 +67,7 @@ jobs:
- name: PHP syntax check
run: |
ERRORS=0
for DIR in src/ htdocs/; do
for DIR in source/ src/ htdocs/; do
if [ -d "$DIR" ]; then
FOUND=1
while IFS= read -r -d '' FILE; do
@@ -207,7 +207,7 @@ jobs:
echo "### Language Directory Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
for DIR in src/ htdocs/; do
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] || continue
# Find all language directories
while IFS= read -r -d '' LANG_DIR; do
@@ -239,7 +239,7 @@ jobs:
MISSING=0
CHECKED=0
for DIR in src/ htdocs/; do
for DIR in source/ src/ htdocs/; do
if [ -d "$DIR" ]; then
while IFS= read -r -d '' SUBDIR; do
CHECKED=$((CHECKED + 1))
@@ -252,7 +252,7 @@ jobs:
done
if [ "${CHECKED}" -eq 0 ]; then
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
echo "No source/, src/, or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
elif [ "${MISSING}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
@@ -450,7 +450,7 @@ jobs:
# Determine source directory
SRC_DIR=""
for DIR in src/ htdocs/ lib/; do
for DIR in source/ src/ htdocs/ lib/; do
if [ -d "$DIR" ]; then
SRC_DIR="$DIR"
break
@@ -458,7 +458,7 @@ jobs:
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
echo "No source directory found (source/, src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
exit 0
fi
+5 -4
View File
@@ -159,11 +159,11 @@ jobs:
echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
done < <(find . -name "*.php" \( -path "*/source/*" -o -path "*/src/*" \) -not -path "./.git/*" -not -path "./vendor/*" -print0)
if [ "$ERRORS" -gt 0 ]; then
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) in source/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "JEXEC guard: OK"
@@ -451,10 +451,11 @@ jobs:
- name: Verify package source
run: |
SOURCE_DIR="src"
SOURCE_DIR="source"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
echo "::warning::No source/, src/, or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
+82 -65
View File
@@ -7,7 +7,7 @@
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 09.23.00
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
@@ -17,6 +17,10 @@ on:
types: [closed]
branches:
- dev
pull_request_target:
types: [synchronize, opened, reopened]
branches:
- main
workflow_dispatch:
inputs:
stability:
@@ -43,7 +47,8 @@ jobs:
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps:
- name: Checkout
@@ -51,22 +56,29 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
if [ -f “/opt/moko-platform/cli/version_bump.php” ] && [ -f “/opt/moko-platform/vendor/autoload.php” ]; then
echo “Using pre-installed /opt/moko-platform”
echo “MOKO_CLI=/opt/moko-platform/cli” >> “$GITHUB_ENV”
else
echo “Falling back to fresh clone”
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
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
- name: Detect platform
id: platform
@@ -76,24 +88,40 @@ jobs:
- name: Resolve metadata and bump version
id: meta
run: |
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
STABILITY="release-candidate"
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) TAG="development" ;;
alpha) TAG="alpha" ;;
beta) TAG="beta" ;;
release-candidate) TAG="release-candidate" ;;
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Set stability suffix, bump preserves it, fix consistency
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \
--branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Read final version (includes suffix, e.g. 01.02.15-dev)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
@@ -118,11 +146,12 @@ jobs:
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ==="
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
@@ -135,6 +164,41 @@ jobs:
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
run: |
@@ -146,55 +210,8 @@ jobs:
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml -> ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
# updates.xml is generated dynamically by MokoGitea license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
+6 -4
View File
@@ -396,17 +396,19 @@ jobs:
missing_required=()
missing_optional=()
# Source directory: src/ or htdocs/ (either is valid for extension repos)
# Source directory: source/, src/, or htdocs/ (any is valid for extension repos)
SOURCE_DIR=""
if [ -d "src" ]; then
if [ -d "source" ]; then
SOURCE_DIR="source"
elif [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need src/
# Platform/tooling repos don't need source/
SOURCE_DIR=""
else
missing_required+=("src/ or htdocs/ (source directory required)")
missing_required+=("source/ or htdocs/ (source directory required)")
fi
for item in "${required_artifacts[@]}"; do
+1 -1
View File
@@ -15,7 +15,7 @@
EXTENSION_NAME := mokobackup
EXTENSION_TYPE := package
SRC_DIR := src
SRC_DIR := source
# Gitea
GITEA_URL := https://git.mokoconsulting.tech
@@ -124,7 +124,7 @@
<fieldset name="filters" label="COM_MOKOBACKUP_FIELDSET_FILTERS">
<field
name="exclude_dirs"
type="ExcludeList"
type="DirectoryFilter"
label="COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS"
description="COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS_DESC"
filter="raw"
@@ -102,7 +102,10 @@ COM_MOKOBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="Include MokoRestore (standalone r
; Exclusion filter fields
COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS="Exclude Directories"
COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS_DESC="One directory path per line (relative to Joomla root). These directories will be skipped during file backup."
COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS_DESC="Browse and check directories to exclude from file backup. You can also type paths manually."
COM_MOKOBACKUP_FILTER_EXCLUDED="Excluded"
COM_MOKOBACKUP_FILTER_INCLUDED="Included"
COM_MOKOBACKUP_FILTER_ADD_MANUAL="Add Path"
COM_MOKOBACKUP_FIELD_EXCLUDE_FILES="Exclude Files"
COM_MOKOBACKUP_FIELD_EXCLUDE_FILES_DESC="One filename or pattern per line. Supports wildcards (e.g. *.bak, *.tmp)."
COM_MOKOBACKUP_FIELD_EXCLUDE_TABLES="Exclude Tables"
@@ -1,7 +1,12 @@
-- MokoJoomBackup 01.01.08
-- MokoJoomBackup 01.01.02
-- Consolidated schema updates: NULL defaults, notifications, archive name format
-- Fix: allow NULL defaults for manifest and log columns
ALTER TABLE `#__mokobackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL;
ALTER TABLE `#__mokobackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL;
-- Add user group notifications column to profiles
ALTER TABLE `#__mokobackup_profiles` ADD COLUMN `notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs' AFTER `notify_email`;
-- Add archive_name_format column with placeholder support
ALTER TABLE `#__mokobackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,259 @@
<?php
/**
* @package MokoJoomBackup
* @subpackage com_mokobackup
* @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
*
* Interactive directory tree field with checkboxes for exclude/include filtering.
* Loads the directory tree from the server via AJAX (browseDir endpoint).
*/
namespace Joomla\Component\MokoBackup\Administrator\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Language\Text;
class DirectoryFilterField extends FormField
{
protected $type = 'DirectoryFilter';
protected function getInput(): string
{
$id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8');
$name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
$mode = htmlspecialchars((string) ($this->element['mode'] ?? 'exclude'), ENT_QUOTES, 'UTF-8');
// Parse current values (newline-separated)
$items = [];
if (!empty($this->value)) {
$items = array_values(array_filter(array_map('trim', explode("\n", str_replace("\r", '', $this->value)))));
}
$itemsJson = json_encode($items);
$jRoot = json_encode(JPATH_ROOT);
$labelExclude = Text::_('COM_MOKOBACKUP_FILTER_EXCLUDED');
$labelInclude = Text::_('COM_MOKOBACKUP_FILTER_INCLUDED');
$labelManual = Text::_('COM_MOKOBACKUP_FILTER_ADD_MANUAL');
$addLabel = Text::_('JGLOBAL_FIELD_ADD');
$placeholder = htmlspecialchars((string) ($this->element['hint'] ?? 'path/to/directory'), ENT_QUOTES, 'UTF-8');
return <<<HTML
<div id="{$id}_wrap">
<input type="hidden" name="{$name}" id="{$id}" value="" />
<!-- Manual entry row -->
<div class="input-group input-group-sm mb-2">
<input type="text" class="form-control" id="{$id}_manual" placeholder="{$placeholder}" />
<button type="button" class="btn btn-outline-success" id="{$id}_addBtn">
<span class="icon-plus" aria-hidden="true"></span> {$addLabel}
</button>
</div>
<!-- Selected items (pills) -->
<div id="{$id}_pills" class="mb-2 d-flex flex-wrap gap-1"></div>
<!-- Browsable tree -->
<div class="card">
<div class="card-header py-1 px-2 d-flex justify-content-between align-items-center">
<small class="fw-bold text-muted" id="{$id}_cwd"></small>
<button type="button" class="btn btn-sm btn-link p-0" id="{$id}_upBtn" style="display:none;">
<span class="icon-arrow-up-4" aria-hidden="true"></span> ..
</button>
</div>
<div id="{$id}_tree" class="list-group list-group-flush" style="max-height:300px; overflow-y:auto;"></div>
</div>
</div>
<style>
#{$id}_wrap .mb-dir-pill {
display: inline-flex; align-items: center; gap: 0.3rem;
padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;
font-family: monospace; cursor: default;
}
#{$id}_wrap .mb-dir-pill.excluded { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
#{$id}_wrap .mb-dir-pill.included { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
#{$id}_wrap .mb-dir-pill .btn-close { font-size: 0.6rem; }
#{$id}_wrap .mb-dir-row { display: flex; align-items: center; padding: 0.35rem 0.75rem; gap: 0.5rem; border-bottom: 1px solid #eee; }
#{$id}_wrap .mb-dir-row:hover { background: #f8f9fa; }
#{$id}_wrap .mb-dir-row .mb-dir-name { cursor: pointer; flex: 1; font-size: 0.9rem; }
#{$id}_wrap .mb-dir-row .mb-dir-name:hover { color: #0d6efd; text-decoration: underline; }
#{$id}_wrap .mb-dir-check { width: 1rem; height: 1rem; cursor: pointer; }
</style>
<script>
(function() {
const id = '{$id}';
const hidden = document.getElementById(id);
const pills = document.getElementById(id + '_pills');
const tree = document.getElementById(id + '_tree');
const cwdEl = document.getElementById(id + '_cwd');
const upBtn = document.getElementById(id + '_upBtn');
const manualInput = document.getElementById(id + '_manual');
const addBtn = document.getElementById(id + '_addBtn');
const jRoot = {$jRoot};
let selected = new Set({$itemsJson});
let currentPath = jRoot;
let parentPath = null;
function sync() {
hidden.value = Array.from(selected).join('\\n');
renderPills();
}
function renderPills() {
while (pills.firstChild) pills.removeChild(pills.firstChild);
selected.forEach(function(path) {
const pill = document.createElement('span');
pill.className = 'mb-dir-pill excluded';
const icon = document.createElement('span');
icon.className = 'icon-folder';
icon.setAttribute('aria-hidden', 'true');
pill.appendChild(icon);
pill.appendChild(document.createTextNode(' ' + path + ' '));
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'btn-close btn-close-sm';
closeBtn.setAttribute('aria-label', 'Remove');
closeBtn.addEventListener('click', function() {
selected.delete(path);
sync();
refreshTree();
});
pill.appendChild(closeBtn);
pills.appendChild(pill);
});
}
function toRelative(absPath) {
if (absPath.indexOf(jRoot) === 0) {
let rel = absPath.substring(jRoot.length);
if (rel.charAt(0) === '/') rel = rel.substring(1);
return rel;
}
return absPath;
}
function setTreeMessage(text, cls) {
while (tree.firstChild) tree.removeChild(tree.firstChild);
const msg = document.createElement('div');
msg.className = 'p-2 ' + cls;
msg.textContent = text;
tree.appendChild(msg);
}
function loadDir(path) {
setTreeMessage('Loading...', 'text-muted');
currentPath = path;
const form = new URLSearchParams();
form.append('task', 'ajax.browseDir');
form.append('path', path);
const tokenName = Joomla.getOptions('csrf.token') || '';
if (tokenName) form.append(tokenName, '1');
fetch('index.php?option=com_mokobackup&format=json', {
method: 'POST', body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
setTreeMessage(data.message || 'Error', 'text-danger');
return;
}
parentPath = data.parent || null;
cwdEl.textContent = data.current || path;
upBtn.style.display = parentPath ? '' : 'none';
renderTree(data.dirs || []);
})
.catch(function(err) {
setTreeMessage('Error: ' + err.message, 'text-danger');
});
}
function refreshTree() {
loadDir(currentPath);
}
function renderTree(dirs) {
while (tree.firstChild) tree.removeChild(tree.firstChild);
if (dirs.length === 0) {
setTreeMessage('(empty)', 'text-muted');
return;
}
dirs.forEach(function(dir) {
const rel = toRelative(dir.path);
const isExcluded = selected.has(rel);
const row = document.createElement('div');
row.className = 'mb-dir-row' + (isExcluded ? ' bg-danger bg-opacity-10' : '');
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'mb-dir-check form-check-input';
cb.checked = isExcluded;
cb.title = isExcluded ? 'Excluded — uncheck to include' : 'Check to exclude';
cb.addEventListener('change', function() {
if (cb.checked) {
selected.add(rel);
} else {
selected.delete(rel);
}
sync();
refreshTree();
});
const icon = document.createElement('span');
icon.className = isExcluded ? 'icon-unpublish text-danger' : 'icon-folder text-warning';
icon.setAttribute('aria-hidden', 'true');
const nameEl = document.createElement('span');
nameEl.className = 'mb-dir-name';
nameEl.textContent = dir.name;
nameEl.addEventListener('click', function() { loadDir(dir.path); });
row.appendChild(cb);
row.appendChild(icon);
row.appendChild(nameEl);
tree.appendChild(row);
});
}
upBtn.addEventListener('click', function() {
if (parentPath) loadDir(parentPath);
});
addBtn.addEventListener('click', function() {
const val = manualInput.value.trim();
if (val && !selected.has(val)) {
selected.add(val);
manualInput.value = '';
sync();
refreshTree();
}
});
manualInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); }
});
sync();
loadDir(jRoot);
})();
</script>
HTML;
}
}

Some files were not shown because too many files have changed in this diff Show More