diff --git a/.mokogitea/CLAUDE.md b/.mokogitea/CLAUDE.md index 62e02cc..c3ba57e 100644 --- a/.mokogitea/CLAUDE.md +++ b/.mokogitea/CLAUDE.md @@ -1,4 +1,4 @@ -# MokoJoomBackup +# MokoSuiteBackup Full-site backup and restore for Joomla — database, files, and configuration. Replaces Akeeba Backup Pro. @@ -6,10 +6,10 @@ Full-site backup and restore for Joomla — database, files, and configuration. | Field | Value | |---|---| -| **Package** | `pkg_mokojoombackup` | +| **Package** | `pkg_mokosuitebackup` | | **Language** | PHP 8.1+ | | **Branch** | develop on `dev`, merge to `main` (protected) | -| **Wiki** | [MokoJoomBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/wiki) | +| **Wiki** | [MokoSuiteBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/wiki) | ## Commands @@ -26,32 +26,32 @@ composer install # Install PHP dependencies Joomla **package** with four sub-extensions: -### com_mokojoombackup (Component) +### com_mokosuitebackup (Component) - 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\MokoJoomBackup\Administrator` -- DB tables: `#__mokojoombackup_profiles`, `#__mokojoombackup_records` -- CLI: `cli/mokojoombackup.php` for cron-based backups +- Namespace: `Joomla\Component\MokoSuiteBackup\Administrator` +- DB tables: `#__mokosuitebackup_profiles`, `#__mokosuitebackup_records` +- CLI: `cli/mokosuitebackup.php` for cron-based backups -### plg_system_mokojoombackup (System Plugin) +### plg_system_mokosuitebackup (System Plugin) - Cleanup of expired backup archives (age + count limits) -- Namespace: `Joomla\Plugin\System\MokoJoomBackup` +- Namespace: `Joomla\Plugin\System\MokoSuiteBackup` -### plg_task_mokojoombackup (Task Plugin) +### plg_task_mokosuitebackup (Task Plugin) - Integrates with Joomla's Scheduled Tasks (com_scheduler) - Registers "Run Backup Profile" task type -- Namespace: `Joomla\Plugin\Task\MokoJoomBackup` +- Namespace: `Joomla\Plugin\Task\MokoSuiteBackup` -### plg_webservices_mokojoombackup (WebServices Plugin) -- REST API for remote backup management (wire-compatible with mcp_mokojoombackup) +### plg_webservices_mokosuitebackup (WebServices Plugin) +- REST API for remote backup management (wire-compatible with mcp_mokosuitebackup) - Endpoints: backup, backups, profiles, download, delete -- Namespace: `Joomla\Plugin\WebServices\MokoJoomBackup` +- Namespace: `Joomla\Plugin\WebServices\MokoSuiteBackup` ### Database Schema -- `#__mokojoombackup_profiles` — backup profiles (name, description, config JSON, filters JSON) -- `#__mokojoombackup_records` — backup records (profile_id, status, origin, archive path, sizes, timestamps) +- `#__mokosuitebackup_profiles` — backup profiles (name, description, config JSON, filters JSON) +- `#__mokosuitebackup_records` — backup records (profile_id, status, origin, archive path, sizes, timestamps) ## Rules diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 1c5fa27..47e26c9 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -1,11 +1,11 @@ - MokoJoomBackup - Package - MokoJoomBackup + MokoSuiteBackup + Package - MokoSuiteBackup MokoConsulting Full-site backup and restore for Joomla — database, files, and configuration - 01.08.00-dev + 01.20.00-dev GNU General Public License v3 diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index fb9dc82..34953b1 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -1,66 +1,66 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /.mokogitea/workflows/auto-bump.yml -# VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) - -name: "Universal: Auto Version Bump" - -on: - push: - branches: - - dev - - rc - - 'feature/**' - - 'patch/**' - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -permissions: - contents: write - -jobs: - bump: - name: Version Bump - runs-on: release - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip bump]') && - !startsWith(github.event.head_commit.message, 'Merge pull request') - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - run: | - 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 - if [ -d "/opt/moko-platform/cli" ]; then - echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/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" - fi - - - name: Bump version - run: | - php ${MOKO_CLI}/version_auto_bump.php \ - --path . --branch "${GITHUB_REF_NAME}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/auto-bump.yml +# VERSION: 09.02.00 +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + - rc + - 'feature/**' + - 'patch/**' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +permissions: + contents: write + +jobs: + bump: + name: Version Bump + runs-on: release + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip bump]') && + !startsWith(github.event.head_commit.message, 'Merge pull request') + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + run: | + 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 + if [ -d "/opt/moko-platform/cli" ]; then + echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/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" + fi + + - name: Bump version + run: | + php ${MOKO_CLI}/version_auto_bump.php \ + --path . --branch "${GITHUB_REF_NAME}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index c2b02a6..525f995 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Automation -# VERSION: 01.00.00 +# INGROUP: mokoplatform.Automation +# VERSION: 01.20.00 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" @@ -28,7 +28,7 @@ jobs: steps: - name: Create branch and comment run: | - TOKEN="${{ secrets.GA_TOKEN }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_TITLE="${{ github.event.issue.title }}" diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 4d78d7a..6625857 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -1,508 +1,508 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/pr-check.yml.template -# VERSION: 09.23.00 -# BRIEF: PR gate — branch policy + code validation before merge - -name: "Universal: PR Check" - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - # ── Branch Policy ────────────────────────────────────────────────────── - branch-policy: - name: Branch Policy - runs-on: ubuntu-latest - steps: - - name: Check branch merge target - run: | - HEAD="${{ github.head_ref }}" - BASE="${{ github.base_ref }}" - - echo "PR: ${HEAD} → ${BASE}" - - ALLOWED=true - REASON="" - - case "$HEAD" in - feature/*|feat/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Feature branches must target 'dev', not '${BASE}'" - fi - ;; - fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" - fi - ;; - patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then - ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - rc) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="RC branch can only merge into 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY - - # ── Code Validation ──────────────────────────────────────────────────── - validate: - name: Validate PR - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found in source files" - echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Detect platform - id: platform - run: | - # Read platform from XML manifest ( tag) or plain text fallback - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - - - name: Setup PHP - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - - - name: PHP syntax check - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) - echo "PHP lint: ${ERRORS} error(s)" - [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } - - - name: Joomla JEXEC guard check - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - # Skip vendor, node_modules, and index.html stub files - case "$file" in ./vendor/*|./node_modules/*) continue ;; esac - # Check first 10 lines for JEXEC or JPATH guard - if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then - 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) - 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 - exit 1 - fi - echo "JEXEC guard: OK" - - - name: Joomla directory listing protection - if: steps.platform.outputs.platform == 'joomla' - run: | - MISSING=0 - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && exit 0 - while IFS= read -r dir; do - if [ ! -f "${dir}/index.html" ]; then - echo "::warning::Missing index.html in ${dir} (directory listing protection)" - MISSING=$((MISSING + 1)) - fi - done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") - if [ "$MISSING" -gt 0 ]; then - echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY - echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY - fi - echo "Directory protection: ${MISSING} missing (advisory)" - - - name: Joomla script file and asset checks - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && exit 0 - MANIFEST_DIR=$(dirname "$MANIFEST") - - # Check scriptfile exists if declared - SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) - if [ -n "$SCRIPTFILE" ]; then - if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then - echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" - ERRORS=$((ERRORS + 1)) - else - echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" - fi - fi - - # Require joomla.asset.json and validate it - ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$ASSET_JSON" ]; then - echo "::error::joomla.asset.json not found — Joomla asset system is required" - ERRORS=$((ERRORS + 1)) - else - if command -v php &> /dev/null; then - php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { - echo "::error::joomla.asset.json is not valid JSON" - ERRORS=$((ERRORS + 1)) - } - fi - echo "joomla.asset.json: valid" - fi - - # Validate all XML files in src/ are well-formed - XML_ERRORS=0 - if command -v php &> /dev/null; then - while IFS= read -r -d '' xmlfile; do - if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then - XML_ERRORS=$((XML_ERRORS + 1)) - fi - done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) - fi - if [ "$XML_ERRORS" -gt 0 ]; then - echo "::error::${XML_ERRORS} XML file(s) are malformed" - ERRORS=$((ERRORS + 1)) - else - echo "XML well-formedness: OK" - fi - - [ "$ERRORS" -gt 0 ] && exit 1 - echo "Joomla asset checks: OK" - - - name: Validate platform manifest - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "::warning::No Joomla manifest found (WaaS site)" - exit 0 - fi - echo "Manifest: ${MANIFEST}" - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } - fi - for ELEMENT in name version description; do - grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } - done - # Block legacy raw/branch update server URLs on MokoGitea - RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) - if [ -n "$RAW_URLS" ]; then - echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" - echo "$RAW_URLS" - exit 1 - fi - echo "Joomla manifest valid" - ;; - dolibarr) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - if [ -z "$MOD_FILE" ]; then - echo "::error::No mod*.class.php found" - exit 1 - fi - echo "Dolibarr module: ${MOD_FILE}" - ;; - *) - echo "Generic platform — no manifest validation" - ;; - esac - - - name: Check update stream format - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - if [ -f "updates.xml" ]; then - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } - fi - echo "updates.xml valid" - fi - ;; - dolibarr) - [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" - ;; - esac - - - name: Validate Joomla language files - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - WARNINGS=0 - - # Require both en-GB and en-US language directories - LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$LANG_ROOT" ]; then - echo "No language/ directory found — skipping" - exit 0 - fi - - if [ ! -d "$LANG_ROOT/en-GB" ]; then - echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" - ERRORS=$((ERRORS + 1)) - fi - if [ ! -d "$LANG_ROOT/en-US" ]; then - echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" - ERRORS=$((ERRORS + 1)) - fi - - # Check that en-GB and en-US have matching .ini files - if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then - for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do - [ ! -f "$GB_INI" ] && continue - US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" - if [ ! -f "$US_INI" ]; then - echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" - ERRORS=$((ERRORS + 1)) - fi - done - for US_INI in "$LANG_ROOT/en-US"/*.ini; do - [ ! -f "$US_INI" ] && continue - GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" - if [ ! -f "$GB_INI" ]; then - echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" - ERRORS=$((ERRORS + 1)) - fi - done - fi - - # Find all .ini language files - INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) - if [ -z "$INI_FILES" ]; then - echo "No .ini language files found" - [ "$ERRORS" -gt 0 ] && exit 1 - exit 0 - fi - - echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" - - for FILE in $INI_FILES; do - FNAME=$(basename "$FILE") - LINENUM=0 - SEEN_KEYS="" - - while IFS= read -r line || [ -n "$line" ]; do - LINENUM=$((LINENUM + 1)) - - # Skip empty lines and comments - [ -z "$line" ] && continue - echo "$line" | grep -qE '^\s*;' && continue - echo "$line" | grep -qE '^\s*$' && continue - - # Must match KEY="VALUE" format - if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then - echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" - ERRORS=$((ERRORS + 1)) - continue - fi - - # Extract key and check for duplicates - KEY=$(echo "$line" | sed 's/=.*//') - if echo "$SEEN_KEYS" | grep -qx "$KEY"; then - echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" - ERRORS=$((ERRORS + 1)) - fi - SEEN_KEYS="${SEEN_KEYS} - ${KEY}" - done < "$FILE" - - echo " ${FILE}: checked ${LINENUM} lines" - done - - # Cross-check en-GB vs en-US key consistency - GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) - US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) - - if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then - for GB_FILE in "$GB_DIR"/*.ini; do - [ ! -f "$GB_FILE" ] && continue - FNAME=$(basename "$GB_FILE") - US_FILE="$US_DIR/$FNAME" - [ ! -f "$US_FILE" ] && continue - - GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) - US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) - - # Keys in en-GB but not en-US - MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_US" ]; then - echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" - echo "$MISSING_US" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - - # Keys in en-US but not en-GB - MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_GB" ]; then - echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" - echo "$MISSING_GB" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - done - fi - - { - echo "### Language File Validation" - echo "| Metric | Count |" - echo "|---|---|" - echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" - echo "| Errors | ${ERRORS} |" - echo "| Warnings | ${WARNINGS} |" - } >> $GITHUB_STEP_SUMMARY - - if [ "$ERRORS" -gt 0 ]; then - echo "::error::Language validation failed with ${ERRORS} error(s)" - exit 1 - fi - echo "Language files: OK (${WARNINGS} warning(s))" - - - name: Check changelog has unreleased entry - run: | - if [ ! -f "CHANGELOG.md" ]; then - echo "::warning::No CHANGELOG.md found" - exit 0 - fi - # Check for content under [Unreleased] section - if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then - echo "::error::CHANGELOG.md missing [Unreleased] section" - exit 1 - fi - # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased - UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) - if [ "$UNRELEASED_CONTENT" -eq 0 ]; then - echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." - echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY - echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" - - - name: Verify package source - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" - exit 0 - fi - FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) - echo "Source: ${FILE_COUNT} files" - [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } - - # ── Pre-Release RC Build ───────────────────────────────────────────────── - pre-release: - name: Build RC Package - runs-on: ubuntu-latest - needs: [branch-policy, validate] - - steps: - - name: Trigger RC pre-release - env: - GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" - echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY - echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY - - # ── Issue Reporter ────────────────────────────────────────────────────── - report-issues: - name: Report Issues - runs-on: ubuntu-latest - needs: [branch-policy, validate] - if: >- - always() && - needs.validate.result == 'failure' - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - sparse-checkout: automation/ci-issue-reporter.sh - sparse-checkout-cone-mode: false - - - name: "File issue for PR validation failure" - env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - chmod +x automation/ci-issue-reporter.sh - ./automation/ci-issue-reporter.sh \ - --gate "PR Validation" \ - --workflow "PR Check" \ - --severity error \ - --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 09.23.00 +# BRIEF: PR gate — branch policy + code validation before merge + +name: "Universal: PR Check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Branch Policy ────────────────────────────────────────────────────── + branch-policy: + name: Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch merge target + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + rc) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="RC branch can only merge into 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + + # ── Code Validation ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found in source files" + echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) + echo "PHP lint: ${ERRORS} error(s)" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: Joomla JEXEC guard check + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + # Skip vendor, node_modules, and index.html stub files + case "$file" in ./vendor/*|./node_modules/*) continue ;; esac + # Check first 10 lines for JEXEC or JPATH guard + if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then + 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) + 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 + exit 1 + fi + echo "JEXEC guard: OK" + + - name: Joomla directory listing protection + if: steps.platform.outputs.platform == 'joomla' + run: | + MISSING=0 + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && exit 0 + while IFS= read -r dir; do + if [ ! -f "${dir}/index.html" ]; then + echo "::warning::Missing index.html in ${dir} (directory listing protection)" + MISSING=$((MISSING + 1)) + fi + done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") + if [ "$MISSING" -gt 0 ]; then + echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY + echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY + fi + echo "Directory protection: ${MISSING} missing (advisory)" + + - name: Joomla script file and asset checks + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + [ -z "$MANIFEST" ] && exit 0 + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check scriptfile exists if declared + SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) + if [ -n "$SCRIPTFILE" ]; then + if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then + echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" + ERRORS=$((ERRORS + 1)) + else + echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" + fi + fi + + # Require joomla.asset.json and validate it + ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ASSET_JSON" ]; then + echo "::error::joomla.asset.json not found — Joomla asset system is required" + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { + echo "::error::joomla.asset.json is not valid JSON" + ERRORS=$((ERRORS + 1)) + } + fi + echo "joomla.asset.json: valid" + fi + + # Validate all XML files in src/ are well-formed + XML_ERRORS=0 + if command -v php &> /dev/null; then + while IFS= read -r -d '' xmlfile; do + if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then + XML_ERRORS=$((XML_ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) + fi + if [ "$XML_ERRORS" -gt 0 ]; then + echo "::error::${XML_ERRORS} XML file(s) are malformed" + ERRORS=$((ERRORS + 1)) + else + echo "XML well-formedness: OK" + fi + + [ "$ERRORS" -gt 0 ] && exit 1 + echo "Joomla asset checks: OK" + + - name: Validate platform manifest + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla manifest found (WaaS site)" + exit 0 + fi + echo "Manifest: ${MANIFEST}" + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } + fi + for ELEMENT in name version description; do + grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } + done + # Block legacy raw/branch update server URLs on MokoGitea + RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) + if [ -n "$RAW_URLS" ]; then + echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" + echo "$RAW_URLS" + exit 1 + fi + echo "Joomla manifest valid" + ;; + dolibarr) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + if [ -z "$MOD_FILE" ]; then + echo "::error::No mod*.class.php found" + exit 1 + fi + echo "Dolibarr module: ${MOD_FILE}" + ;; + *) + echo "Generic platform — no manifest validation" + ;; + esac + + - name: Check update stream format + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + if [ -f "updates.xml" ]; then + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } + fi + echo "updates.xml valid" + fi + ;; + dolibarr) + [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" + ;; + esac + + - name: Validate Joomla language files + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + WARNINGS=0 + + # Require both en-GB and en-US language directories + LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$LANG_ROOT" ]; then + echo "No language/ directory found — skipping" + exit 0 + fi + + if [ ! -d "$LANG_ROOT/en-GB" ]; then + echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" + ERRORS=$((ERRORS + 1)) + fi + if [ ! -d "$LANG_ROOT/en-US" ]; then + echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" + ERRORS=$((ERRORS + 1)) + fi + + # Check that en-GB and en-US have matching .ini files + if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then + for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do + [ ! -f "$GB_INI" ] && continue + US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" + if [ ! -f "$US_INI" ]; then + echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" + ERRORS=$((ERRORS + 1)) + fi + done + for US_INI in "$LANG_ROOT/en-US"/*.ini; do + [ ! -f "$US_INI" ] && continue + GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" + if [ ! -f "$GB_INI" ]; then + echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" + ERRORS=$((ERRORS + 1)) + fi + done + fi + + # Find all .ini language files + INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) + if [ -z "$INI_FILES" ]; then + echo "No .ini language files found" + [ "$ERRORS" -gt 0 ] && exit 1 + exit 0 + fi + + echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" + + for FILE in $INI_FILES; do + FNAME=$(basename "$FILE") + LINENUM=0 + SEEN_KEYS="" + + while IFS= read -r line || [ -n "$line" ]; do + LINENUM=$((LINENUM + 1)) + + # Skip empty lines and comments + [ -z "$line" ] && continue + echo "$line" | grep -qE '^\s*;' && continue + echo "$line" | grep -qE '^\s*$' && continue + + # Must match KEY="VALUE" format + if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then + echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Extract key and check for duplicates + KEY=$(echo "$line" | sed 's/=.*//') + if echo "$SEEN_KEYS" | grep -qx "$KEY"; then + echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" + ERRORS=$((ERRORS + 1)) + fi + SEEN_KEYS="${SEEN_KEYS} + ${KEY}" + done < "$FILE" + + echo " ${FILE}: checked ${LINENUM} lines" + done + + # Cross-check en-GB vs en-US key consistency + GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) + US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) + + if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then + for GB_FILE in "$GB_DIR"/*.ini; do + [ ! -f "$GB_FILE" ] && continue + FNAME=$(basename "$GB_FILE") + US_FILE="$US_DIR/$FNAME" + [ ! -f "$US_FILE" ] && continue + + GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) + US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) + + # Keys in en-GB but not en-US + MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_US" ]; then + echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" + echo "$MISSING_US" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + + # Keys in en-US but not en-GB + MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_GB" ]; then + echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" + echo "$MISSING_GB" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + done + fi + + { + echo "### Language File Validation" + echo "| Metric | Count |" + echo "|---|---|" + echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" + echo "| Errors | ${ERRORS} |" + echo "| Warnings | ${WARNINGS} |" + } >> $GITHUB_STEP_SUMMARY + + if [ "$ERRORS" -gt 0 ]; then + echo "::error::Language validation failed with ${ERRORS} error(s)" + exit 1 + fi + echo "Language files: OK (${WARNINGS} warning(s))" + + - name: Check changelog has unreleased entry + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::No CHANGELOG.md found" + exit 0 + fi + # Check for content under [Unreleased] section + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md missing [Unreleased] section" + exit 1 + fi + # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased + UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) + if [ "$UNRELEASED_CONTENT" -eq 0 ]; then + echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." + echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY + echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" + + - name: Verify package source + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source: ${FILE_COUNT} files" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + + steps: + - name: Trigger RC pre-release + env: + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY + + # ── Issue Reporter ────────────────────────────────────────────────────── + report-issues: + name: Report Issues + runs-on: ubuntu-latest + needs: [branch-policy, validate] + if: >- + always() && + needs.validate.result == 'failure' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issue for PR validation failure" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + ./automation/ci-issue-reporter.sh \ + --gate "PR Validation" \ + --workflow "PR Check" \ + --severity error \ + --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index bc53b7f..24c47e0 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -8,4 +8,245 @@ # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /templates/workflows/universal/pre-release.yml.template # VERSION: 05.01.00 -# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches \ No newline at end of file +# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches + +name: "Universal: Pre-Release" + +on: + push: + branches: + - dev + - 'fix/**' + - 'patch/**' + - 'hotfix/**' + - 'bugfix/**' + - 'chore/**' + - alpha + - beta + - rc + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + github.event_name == 'push' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + ref: ${{ github.ref_name }} + + - 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/cli/manifest_element.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 2>&1; 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 + rm -rf /tmp/moko-platform-api + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git + git clone --depth 1 --branch main --quiet $CLONE_URL /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 + fi + + - name: Detect platform + id: platform + run: | + # Auto-detect and update platform if not set in manifest + php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + # Auto-detect stability from branch name on push, or use input on dispatch + if [ "${{ github.event_name }}" = "push" ]; then + case "${{ github.ref_name }}" in + rc) STABILITY="release-candidate" ;; + alpha) STABILITY="alpha" ;; + beta) STABILITY="beta" ;; + *) STABILITY="development" ;; + esac + else + STABILITY="${{ inputs.stability || 'development' }}" + fi + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # 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 "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml + php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true + + # 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" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + 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}${SUFFIX} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --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: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + # 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 + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/rc-revert.yml b/.mokogitea/workflows/rc-revert.yml new file mode 100644 index 0000000..f54b184 --- /dev/null +++ b/.mokogitea/workflows/rc-revert.yml @@ -0,0 +1,66 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoPlatform.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/rc-revert.yml +# VERSION: 09.23.00 +# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge + +name: "RC Revert" + +on: + pull_request: + types: [closed] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + revert: + name: Rename rc/ back to dev/ + runs-on: ubuntu-latest + if: >- + github.event.pull_request.merged == false && + startsWith(github.event.pull_request.head.ref, 'rc/') + + steps: + - name: Rename branch + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + SUFFIX="${BRANCH#rc/}" + DEV_BRANCH="dev/${SUFFIX}" + API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Create dev/ branch from rc/ branch + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \ + "${API}" 2>/dev/null || true) + + if [ "$STATUS" = "201" ]; then + echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY + else + echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})" + exit 1 + fi + + # Delete rc/ branch + ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: token ${TOKEN}" \ + "${API}/${ENCODED}" 2>/dev/null || true) + + if [ "$STATUS" = "204" ]; then + echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + else + echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})" + fi + + echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY + echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index 8d57aaf..d0538d5 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -1,711 +1,711 @@ -# ============================================================================ -# Copyright (C) 2025 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/joomla/repo_health.yml.template -# VERSION: 09.23.00 -# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. -# ============================================================================ - -name: "Generic: Repo Health" - -defaults: - run: - shell: bash - -on: - workflow_dispatch: - inputs: - profile: - description: 'Validation profile: all, scripts, or repo' - required: true - default: all - type: choice - options: - - all - - scripts - - repo - pull_request: - push: - -permissions: - contents: read - -env: - # Scripts governance policy - SCRIPTS_REQUIRED_DIRS: - SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate - - # Repo health policy - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ - REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ - REPO_DISALLOWED_DIRS: - REPO_DISALLOWED_FILES: TODO.md,todo.md - - # Extended checks toggles - EXTENDED_CHECKS: "true" - - # File / directory variables - DOCS_INDEX: docs/docs-index.md - SCRIPT_DIR: scripts - WORKFLOWS_DIR: .mokogitea/workflows - SHELLCHECK_PATTERN: '*.sh' - SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - access_check: - name: Access control - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - - outputs: - allowed: ${{ steps.perm.outputs.allowed }} - permission: ${{ steps.perm.outputs.permission }} - - steps: - - name: Check actor permission (admin only) - id: perm - env: - TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} - REPO: ${{ github.repository }} - ACTOR: ${{ github.actor }} - run: | - set -euo pipefail - ALLOWED=false - PERMISSION=unknown - METHOD="" - - # Hardcoded authorized users — always allowed - case "$ACTOR" in - jmiller|gitea-actions[bot]) - ALLOWED=true - PERMISSION=admin - METHOD="hardcoded allowlist" - ;; - *) - # Detect platform and check permissions via API - API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" - RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') - PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") - if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then - ALLOWED=true - fi - METHOD="collaborator API" - ;; - esac - - echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" - echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" - - { - echo "## Access Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${ALLOWED} |" - echo "" - if [ "$ALLOWED" = "true" ]; then - echo "${ACTOR} authorized (${METHOD})" - else - echo "${ACTOR} is NOT authorized. Requires admin or maintain role." - fi - } >> "${GITHUB_STEP_SUMMARY}" - - - name: Deny execution when not permitted - if: ${{ steps.perm.outputs.allowed != 'true' }} - run: | - set -euo pipefail - printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" - exit 1 - - scripts_governance: - name: Scripts governance - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Scripts folder checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes scripts governance' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ ! -d "${SCRIPT_DIR}" ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' 'Status: OK (advisory)' - printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi - IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" - - missing_dirs=() - unapproved_dirs=() - - for d in "${required_dirs[@]}"; do - req="${d%/}" - [ ! -d "${req}" ] && missing_dirs+=("${req}/") - done - - while IFS= read -r d; do - allowed=false - for a in "${allowed_dirs[@]}"; do - a_norm="${a%/}" - [ "${d%/}" = "${a_norm}" ] && allowed=true - done - [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") - done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') - - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Area | Status | Notes |' - printf '%s\n' '|---|---|---|' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Required directories | Warning | Missing required subfolders |' - else - printf '%s\n' '| Required directories | OK | All required subfolders present |' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' - else - printf '%s\n' '| Directory policy | OK | No unapproved directories |' - fi - - printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' - printf '\n' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Missing required script directories:' - for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Missing required script directories: none.' - printf '\n' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Unapproved script directories detected:' - for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Unapproved script directories detected: none.' - printf '\n' - fi - - printf '%s\n' 'Scripts governance completed in advisory mode.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - repo_health: - name: Repository health - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Repository health checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ]; then - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes repository health' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" - - missing_required=() - missing_optional=() - - # Source directory: src/ or htdocs/ (either is valid for extension repos) - SOURCE_DIR="" - if [ -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/ - SOURCE_DIR="" - else - missing_required+=("src/ or htdocs/ (source directory required)") - fi - - for item in "${required_artifacts[@]}"; do - if printf '%s' "${item}" | grep -q '/$'; then - d="${item%/}" - [ ! -d "${d}" ] && missing_required+=("${item}") - else - [ ! -f "${item}" ] && missing_required+=("${item}") - fi - done - - for f in "${optional_files[@]}"; do - if printf '%s' "${f}" | grep -q '/$'; then - d="${f%/}" - [ ! -d "${d}" ] && missing_optional+=("${f}") - else - [ ! -f "${f}" ] && missing_optional+=("${f}") - fi - done - - for d in "${disallowed_dirs[@]}"; do - d_norm="${d%/}" - [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") - done - - for f in "${disallowed_files[@]}"; do - [ -f "${f}" ] && missing_required+=("${f} (disallowed)") - done - - git fetch origin --prune - - dev_paths=() - dev_branches=() - - while IFS= read -r b; do - name="${b#origin/}" - if [ "${name}" = 'dev' ]; then - dev_branches+=("${name}") - else - dev_paths+=("${name}") - fi - done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') - - if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then - missing_required+=("dev or dev/* branch") - fi - - content_warnings=() - - if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md missing '# Changelog' header") - fi - - if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") - fi - - if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then - content_warnings+=("LICENSE does not look like a GPL text") - fi - - if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then - content_warnings+=("README.md missing expected brand keyword") - fi - - export PROFILE_RAW="${profile}" - export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" - export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" - export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - - report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") - - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Metric | Value |' - printf '%s\n' '|---|---|' - printf '%s\n' "| Missing required | ${#missing_required[@]} |" - printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" - printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" - printf '\n' - - printf '%s\n' '### Guardrails report (JSON)' - printf '%s\n' '```json' - printf '%s\n' "${report_json}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_required[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repo artifacts' - for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repo artifacts' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#content_warnings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Repo content warnings' - for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - # -- Joomla-specific checks -- - joomla_findings=() - - MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" - if [ -z "${MANIFEST}" ]; then - joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") - else - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then - joomla_findings+=("XML manifest: type attribute missing or invalid") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP ' missing (required for Joomla 5+)") - fi - fi - - INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" - if [ "${INI_COUNT}" -eq 0 ]; then - joomla_findings+=("No .ini language files found") - fi - - if [ ! -f 'updates.xml' ]; then - joomla_findings+=("updates.xml missing in root (required for Joomla update server)") - fi - - if [ -n "${SOURCE_DIR}" ]; then - INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") - for dir in "${INDEX_DIRS[@]}"; do - if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then - joomla_findings+=("${dir}/index.html missing (directory listing protection)") - fi - done - fi - - if [ "${#joomla_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' '| Check | Status |' - printf '%s\n' '|---|---|' - for f in "${joomla_findings[@]}"; do - printf '%s\n' "| ${f} | Warning |" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - else - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' 'All Joomla-specific checks passed.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - extended_enabled="${EXTENDED_CHECKS:-true}" - extended_findings=() - - if [ "${extended_enabled}" = 'true' ]; then - if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then - : - else - extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") - fi - - if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then - bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" - if [ -n "${bad_refs}" ]; then - extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") - { - printf '%s\n' '### Workflow pinning advisory' - printf '%s\n' 'Found uses: entries pinned to main/master:' - printf '%s\n' '```' - printf '%s\n' "${bad_refs}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -f "${DOCS_INDEX}" ]; then - missing_links="" - while IFS= read -r docline; do - for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do - case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac - linkpath="${link%%#*}" - linkpath="${linkpath%%\?*}" - [ -z "$linkpath" ] && continue - if [ "${linkpath:0:1}" = "/" ]; then - testpath="${linkpath#/}" - else - testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" - fi - [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " - done - done < "${DOCS_INDEX}" - if [ -n "${missing_links}" ]; then - extended_findings+=("docs/docs-index.md contains broken relative links") - { - printf '%s\n' '### Docs index link integrity' - printf '%s\n' 'Broken relative links:' - for bl in ${missing_links}; do - printf '%s\n' "- ${bl}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -d "${SCRIPT_DIR}" ]; then - if ! command -v shellcheck >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y shellcheck >/dev/null - fi - - sc_out='' - while IFS= read -r shf; do - [ -z "${shf}" ] && continue - out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" - if [ -n "${out_one}" ]; then - sc_out="${sc_out}${out_one}\n" - fi - done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) - - if [ -n "${sc_out}" ]; then - extended_findings+=("ShellCheck warnings detected (advisory)") - sc_head="$(printf '%s' "${sc_out}" | head -n 200)" - { - printf '%s\n' '### ShellCheck (advisory)' - printf '%s\n' '```' - printf '%s\n' "${sc_head}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - spdx_missing=() - IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" - spdx_args=() - for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done - - while IFS= read -r f; do - [ -z "${f}" ] && continue - if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then - spdx_missing+=("${f}") - fi - done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) - - if [ "${#spdx_missing[@]}" -gt 0 ]; then - extended_findings+=("SPDX header missing in some tracked files (advisory)") - { - printf '%s\n' '### SPDX header advisory' - printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' - for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - stale_cutoff_days=180 - stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" - if [ -n "${stale_branches}" ]; then - extended_findings+=("Stale remote branches detected (advisory)") - { - printf '%s\n' '### Git hygiene advisory' - printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" - while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - { - printf '%s\n' '### Guardrails coverage matrix' - printf '%s\n' '| Domain | Status | Notes |' - printf '%s\n' '|---|---|---|' - printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' - printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' - printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' - printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' - if [ "${extended_enabled}" = 'true' ]; then - if [ "${#extended_findings[@]}" -gt 0 ]; then - printf '%s\n' '| Extended checks | Warning | See extended findings below |' - else - printf '%s\n' '| Extended checks | OK | No findings |' - fi - else - printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' - fi - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Extended findings (advisory)' - for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" - - - site-health: - name: Site Health - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - - name: Uptime check - if: env.URLS != '' - run: | - echo "$URLS" > /tmp/urls.txt - php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" - rm -f /tmp/urls.txt - env: - URLS: ${{ vars.MONITORED_URLS }} - - - name: SSL certificate check - if: env.DOMAINS != '' - run: | - echo "$DOMAINS" > /tmp/domains.txt - php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" - rm -f /tmp/domains.txt - env: - DOMAINS: ${{ vars.MONITORED_DOMAINS }} - - - name: Summary - if: always() - run: | - echo "### Site Health" >> $GITHUB_STEP_SUMMARY - echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY - - # ═══════════════════════════════════════════════════════════════════════ - # Issue Reporter — file issues for failed gates - # ═══════════════════════════════════════════════════════════════════════ - report-issues: - name: "Report Issues" - runs-on: ubuntu-latest - needs: [access_check, scripts_governance, repo_health] - if: >- - always() && - (needs.scripts_governance.result == 'failure' || - needs.repo_health.result == 'failure') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - sparse-checkout: automation/ci-issue-reporter.sh - sparse-checkout-cone-mode: false - - - name: "File issues for failed gates" - env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - chmod +x automation/ci-issue-reporter.sh - REPORTER="./automation/ci-issue-reporter.sh" - WF="Repo Health" - - report_gate() { - local gate="$1" result="$2" details="$3" - if [ "$result" = "failure" ]; then - "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error - fi - } - - report_gate "Scripts Governance" \ - "${{ needs.scripts_governance.result }}" \ - "Scripts directory policy violations detected. Review required and allowed directories." - - report_gate "Repository Health" \ - "${{ needs.repo_health.result }}" \ - "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." +# ============================================================================ +# Copyright (C) 2025 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 09.23.00 +# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: "Generic: Repo Health" + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - scripts + - repo + pull_request: + push: + +permissions: + contents: read + +env: + # Scripts governance policy + SCRIPTS_REQUIRED_DIRS: + SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate + + # Repo health policy + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ + REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ + REPO_DISALLOWED_DIRS: + REPO_DISALLOWED_FILES: TODO.md,todo.md + + # Extended checks toggles + EXTENDED_CHECKS: "true" + + # File / directory variables + DOCS_INDEX: docs/docs-index.md + SCRIPT_DIR: scripts + WORKFLOWS_DIR: .mokogitea/workflows + SHELLCHECK_PATTERN: '*.sh' + SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + access_check: + name: Access control + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + outputs: + allowed: ${{ steps.perm.outputs.allowed }} + permission: ${{ steps.perm.outputs.permission }} + + steps: + - name: Check actor permission (admin only) + id: perm + env: + TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + ALLOWED=false + PERMISSION=unknown + METHOD="" + + # Hardcoded authorized users — always allowed + case "$ACTOR" in + jmiller|gitea-actions[bot]) + ALLOWED=true + PERMISSION=admin + METHOD="hardcoded allowlist" + ;; + *) + # Detect platform and check permissions via API + API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" + RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') + PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") + if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then + ALLOWED=true + fi + METHOD="collaborator API" + ;; + esac + + echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" + echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" + + { + echo "## Access Authorization" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| **Actor** | \`${ACTOR}\` |" + echo "| **Repository** | \`${REPO}\` |" + echo "| **Permission** | \`${PERMISSION}\` |" + echo "| **Method** | ${METHOD} |" + echo "| **Authorized** | ${ALLOWED} |" + echo "" + if [ "$ALLOWED" = "true" ]; then + echo "${ACTOR} authorized (${METHOD})" + else + echo "${ACTOR} is NOT authorized. Requires admin or maintain role." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Deny execution when not permitted + if: ${{ steps.perm.outputs.allowed != 'true' }} + run: | + set -euo pipefail + printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" + exit 1 + + scripts_governance: + name: Scripts governance + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Scripts folder checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes scripts governance' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ ! -d "${SCRIPT_DIR}" ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' 'Status: OK (advisory)' + printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi + IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" + + missing_dirs=() + unapproved_dirs=() + + for d in "${required_dirs[@]}"; do + req="${d%/}" + [ ! -d "${req}" ] && missing_dirs+=("${req}/") + done + + while IFS= read -r d; do + allowed=false + for a in "${allowed_dirs[@]}"; do + a_norm="${a%/}" + [ "${d%/}" = "${a_norm}" ] && allowed=true + done + [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") + done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') + + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Area | Status | Notes |' + printf '%s\n' '|---|---|---|' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Required directories | Warning | Missing required subfolders |' + else + printf '%s\n' '| Required directories | OK | All required subfolders present |' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' + else + printf '%s\n' '| Directory policy | OK | No unapproved directories |' + fi + + printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' + printf '\n' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Missing required script directories:' + for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Missing required script directories: none.' + printf '\n' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Unapproved script directories detected:' + for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Unapproved script directories detected: none.' + printf '\n' + fi + + printf '%s\n' 'Scripts governance completed in advisory mode.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + repo_health: + name: Repository health + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Repository health checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ]; then + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes repository health' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" + + missing_required=() + missing_optional=() + + # Source directory: src/ or htdocs/ (either is valid for extension repos) + SOURCE_DIR="" + if [ -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/ + SOURCE_DIR="" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + for item in "${required_artifacts[@]}"; do + if printf '%s' "${item}" | grep -q '/$'; then + d="${item%/}" + [ ! -d "${d}" ] && missing_required+=("${item}") + else + [ ! -f "${item}" ] && missing_required+=("${item}") + fi + done + + for f in "${optional_files[@]}"; do + if printf '%s' "${f}" | grep -q '/$'; then + d="${f%/}" + [ ! -d "${d}" ] && missing_optional+=("${f}") + else + [ ! -f "${f}" ] && missing_optional+=("${f}") + fi + done + + for d in "${disallowed_dirs[@]}"; do + d_norm="${d%/}" + [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") + done + + for f in "${disallowed_files[@]}"; do + [ -f "${f}" ] && missing_required+=("${f} (disallowed)") + done + + git fetch origin --prune + + dev_paths=() + dev_branches=() + + while IFS= read -r b; do + name="${b#origin/}" + if [ "${name}" = 'dev' ]; then + dev_branches+=("${name}") + else + dev_paths+=("${name}") + fi + done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') + + if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then + missing_required+=("dev or dev/* branch") + fi + + content_warnings=() + + if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md missing '# Changelog' header") + fi + + if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") + fi + + if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then + content_warnings+=("LICENSE does not look like a GPL text") + fi + + if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then + content_warnings+=("README.md missing expected brand keyword") + fi + + export PROFILE_RAW="${profile}" + export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" + export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" + export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" + + report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") + + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Metric | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Missing required | ${#missing_required[@]} |" + printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" + printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" + printf '\n' + + printf '%s\n' '### Guardrails report (JSON)' + printf '%s\n' '```json' + printf '%s\n' "${report_json}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_required[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repo artifacts' + for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repo artifacts' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#content_warnings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Repo content warnings' + for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # -- Joomla-specific checks -- + joomla_findings=() + + MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" + if [ -z "${MANIFEST}" ]; then + joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") + else + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP ' missing (required for Joomla 5+)") + fi + fi + + INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" + if [ "${INI_COUNT}" -eq 0 ]; then + joomla_findings+=("No .ini language files found") + fi + + if [ ! -f 'updates.xml' ]; then + joomla_findings+=("updates.xml missing in root (required for Joomla update server)") + fi + + if [ -n "${SOURCE_DIR}" ]; then + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + fi + + if [ "${#joomla_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' '| Check | Status |' + printf '%s\n' '|---|---|' + for f in "${joomla_findings[@]}"; do + printf '%s\n' "| ${f} | Warning |" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' 'All Joomla-specific checks passed.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + extended_enabled="${EXTENDED_CHECKS:-true}" + extended_findings=() + + if [ "${extended_enabled}" = 'true' ]; then + if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then + : + else + extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") + fi + + if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then + bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" + if [ -n "${bad_refs}" ]; then + extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") + { + printf '%s\n' '### Workflow pinning advisory' + printf '%s\n' 'Found uses: entries pinned to main/master:' + printf '%s\n' '```' + printf '%s\n' "${bad_refs}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -f "${DOCS_INDEX}" ]; then + missing_links="" + while IFS= read -r docline; do + for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do + case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac + linkpath="${link%%#*}" + linkpath="${linkpath%%\?*}" + [ -z "$linkpath" ] && continue + if [ "${linkpath:0:1}" = "/" ]; then + testpath="${linkpath#/}" + else + testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" + fi + [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " + done + done < "${DOCS_INDEX}" + if [ -n "${missing_links}" ]; then + extended_findings+=("docs/docs-index.md contains broken relative links") + { + printf '%s\n' '### Docs index link integrity' + printf '%s\n' 'Broken relative links:' + for bl in ${missing_links}; do + printf '%s\n' "- ${bl}" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -d "${SCRIPT_DIR}" ]; then + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y shellcheck >/dev/null + fi + + sc_out='' + while IFS= read -r shf; do + [ -z "${shf}" ] && continue + out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" + if [ -n "${out_one}" ]; then + sc_out="${sc_out}${out_one}\n" + fi + done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) + + if [ -n "${sc_out}" ]; then + extended_findings+=("ShellCheck warnings detected (advisory)") + sc_head="$(printf '%s' "${sc_out}" | head -n 200)" + { + printf '%s\n' '### ShellCheck (advisory)' + printf '%s\n' '```' + printf '%s\n' "${sc_head}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + spdx_missing=() + IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" + spdx_args=() + for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done + + while IFS= read -r f; do + [ -z "${f}" ] && continue + if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then + spdx_missing+=("${f}") + fi + done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) + + if [ "${#spdx_missing[@]}" -gt 0 ]; then + extended_findings+=("SPDX header missing in some tracked files (advisory)") + { + printf '%s\n' '### SPDX header advisory' + printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' + for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + stale_cutoff_days=180 + stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" + if [ -n "${stale_branches}" ]; then + extended_findings+=("Stale remote branches detected (advisory)") + { + printf '%s\n' '### Git hygiene advisory' + printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" + while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + { + printf '%s\n' '### Guardrails coverage matrix' + printf '%s\n' '| Domain | Status | Notes |' + printf '%s\n' '|---|---|---|' + printf '%s\n' '| Access control | OK | Admin-only execution gate |' + printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' + printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' + printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' + printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' + if [ "${extended_enabled}" = 'true' ]; then + if [ "${#extended_findings[@]}" -gt 0 ]; then + printf '%s\n' '| Extended checks | Warning | See extended findings below |' + else + printf '%s\n' '| Extended checks | OK | No findings |' + fi + else + printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' + fi + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Extended findings (advisory)' + for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" + + + site-health: + name: Site Health + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - name: Uptime check + if: env.URLS != '' + run: | + echo "$URLS" > /tmp/urls.txt + php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" + rm -f /tmp/urls.txt + env: + URLS: ${{ vars.MONITORED_URLS }} + + - name: SSL certificate check + if: env.DOMAINS != '' + run: | + echo "$DOMAINS" > /tmp/domains.txt + php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" + rm -f /tmp/domains.txt + env: + DOMAINS: ${{ vars.MONITORED_DOMAINS }} + + - name: Summary + if: always() + run: | + echo "### Site Health" >> $GITHUB_STEP_SUMMARY + echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════════════ + # Issue Reporter — file issues for failed gates + # ═══════════════════════════════════════════════════════════════════════ + report-issues: + name: "Report Issues" + runs-on: ubuntu-latest + needs: [access_check, scripts_governance, repo_health] + if: >- + always() && + (needs.scripts_governance.result == 'failure' || + needs.repo_health.result == 'failure') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issues for failed gates" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + REPORTER="./automation/ci-issue-reporter.sh" + WF="Repo Health" + + report_gate() { + local gate="$1" result="$2" details="$3" + if [ "$result" = "failure" ]; then + "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error + fi + } + + report_gate "Scripts Governance" \ + "${{ needs.scripts_governance.result }}" \ + "Scripts directory policy violations detected. Review required and allowed directories." + + report_gate "Repository Health" \ + "${{ needs.repo_health.result }}" \ + "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." diff --git a/CHANGELOG.md b/CHANGELOG.md index 709e7a8..63f16b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog ## [Unreleased] +### Fixed +- Admin submenu items (Dashboard, Backups, Profiles) not created on install — `` block in manifest was empty +- Submenu items not created on update — added `ensureSubmenuItems()` using Joomla's `MenuTable` API with proper nested set positioning +- Submenu icons not rendering in Joomla 6 — set `menu_icon` param for level 2+ items (Atum only renders `img` column icons for level 1) +- CSS selector `#menu` → `.main-nav` for icon injection (Joomla 6 uses dynamic `id="menu{moduleId}"`) +- Use `margin-inline-end` instead of `margin-right` for RTL layout support + ## [01.08.00] --- 2026-06-07 ## [01.07.00] --- 2026-06-07 @@ -12,13 +19,13 @@ ### Added - Dashboard submenu entry as default landing page with `class:home` icon -- `[DEFAULT_DIR]` placeholder for portable backup directory configuration — resolves to `administrator/components/com_mokojoombackup/backups` at runtime +- `[DEFAULT_DIR]` placeholder for portable backup directory configuration — resolves to `administrator/components/com_mokosuitebackup/backups` at runtime - Live AJAX directory validation on backup_dir field — checks existence, writability, and placeholder resolution as user types (debounced 400ms) - `checkDir` AJAX endpoint for real-time directory permission checking - Web-accessible warning badge on backup download buttons when archive is inside web root - Inline security warning in FolderPicker when default directory is selected - Auto `.htaccess` and `index.html` protection for web-accessible backup directories on profile save and at backup time -- Font Awesome 6 submenu icons via CSS injection in `MokoJoomBackupComponent::boot()` +- Font Awesome 6 submenu icons via CSS injection in `MokoSuiteBackupComponent::boot()` - `syncMenuIcons()` installer postflight — syncs icon classes to `#__menu` on install and update - `encryptionPassword` property on `SteppedSession` for upcoming stepped backup encryption support diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 791100e..9c88371 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to MokoJoomBackup +# Contributing to MokoSuiteBackup -Thank you for your interest in contributing to MokoJoomBackup. +Thank you for your interest in contributing to MokoSuiteBackup. ## Getting Started @@ -27,7 +27,7 @@ Thank you for your interest in contributing to MokoJoomBackup. ## Reporting Issues -Report bugs and feature requests via [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/issues). +Report bugs and feature requests via [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/issues). ## License diff --git a/Makefile b/Makefile index bafba81..3ab13af 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # Copyright (C) 2026 Moko Consulting # SPDX-License-Identifier: GPL-3.0-or-later # -# MokoJoomBackup — Full-site backup and restore for Joomla +# MokoSuiteBackup — Full-site backup and restore for Joomla # # Builds and releases are handled by CI workflows (pre-release.yml, # auto-release.yml). This Makefile provides local validation helpers @@ -12,7 +12,7 @@ # CONFIGURATION # ============================================================================== -EXTENSION_NAME := mokojoombackup +EXTENSION_NAME := mokosuitebackup EXTENSION_TYPE := package SRC_DIR := source @@ -20,7 +20,7 @@ SRC_DIR := source # Gitea GITEA_URL := https://git.mokoconsulting.tech GITEA_ORG := MokoConsulting -GITEA_REPO := MokoJoomBackup +GITEA_REPO := MokoSuiteBackup # Tools PHP := php @@ -44,7 +44,7 @@ COLOR_RED := \033[31m .PHONY: help help: ## Show this help message @echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)" - @echo "$(COLOR_BLUE)║ MokoJoomBackup Makefile ║$(COLOR_RESET)" + @echo "$(COLOR_BLUE)║ MokoSuiteBackup Makefile ║$(COLOR_RESET)" @echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)" @echo "" @echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)" @@ -158,7 +158,7 @@ release-rc: validate validate-xml ## Trigger release-candidate build via CI work .PHONY: version version: ## Display version from package manifest - @VERSION=$$(grep '' $(SRC_DIR)/pkg_mokojoombackup.xml | sed 's/.*\(.*\)<\/version>.*/\1/'); \ + @VERSION=$$(grep '' $(SRC_DIR)/pkg_mokosuitebackup.xml | sed 's/.*\(.*\)<\/version>.*/\1/'); \ echo "$(COLOR_BLUE)$(EXTENSION_NAME)$(COLOR_RESET) v$$VERSION ($(EXTENSION_TYPE))" # Default target diff --git a/README.md b/README.md index 27143f0..8f93b6c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# MokoJoomBackup +# MokoSuiteBackup - + Full-site backup and restore for Joomla — database, files, and configuration. ## Overview -MokoJoomBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It creates complete site backups including the database, files, and configuration, packaged into downloadable ZIP archives. Supports multiple backup profiles, scheduled backups via CLI/cron, and a REST API for remote management. +MokoSuiteBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It creates complete site backups including the database, files, and configuration, packaged into downloadable ZIP archives. Supports multiple backup profiles, scheduled backups via CLI/cron, and a REST API for remote management. ## Features @@ -25,13 +25,13 @@ MokoJoomBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It cre ## Installation -1. Download `pkg_mokobackup-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases) +1. Download `pkg_mokobackup-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/releases) 2. Joomla Administrator > Extensions > Install 3. System plugin enabled automatically on install ## Configuration -- **Component**: Administrator > Components > MokoJoomBackup +- **Component**: Administrator > Components > MokoSuiteBackup - **Profiles**: Create backup profiles with different file/database filters - **System Plugin**: Configure scheduled backup triggers and notifications - **CLI**: `php cli/mokobackup.php --profile=1` for cron-based backups diff --git a/mokosuitebackup.php b/mokosuitebackup.php new file mode 100644 index 0000000..2954662 --- /dev/null +++ b/mokosuitebackup.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; diff --git a/source/packages/plg_webservices_mokojoombackup/mokojoombackup.xml b/mokosuitebackup.xml similarity index 63% rename from source/packages/plg_webservices_mokojoombackup/mokojoombackup.xml rename to mokosuitebackup.xml index 96c008b..29eca03 100644 --- a/source/packages/plg_webservices_mokojoombackup/mokojoombackup.xml +++ b/mokosuitebackup.xml @@ -1,14 +1,13 @@ - plg_webservices_mokojoombackup - 01.08.00 + Web Services - MokoSuiteBackup + 01.10.00-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech @@ -17,16 +16,16 @@ GPL-3.0-or-later PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION - Joomla\Plugin\WebServices\MokoJoomBackup + Joomla\Plugin\WebServices\MokoSuiteBackup - mokojoombackup.php + mokosuitebackup.php services src - language/en-GB/plg_webservices_mokojoombackup.ini - language/en-GB/plg_webservices_mokojoombackup.sys.ini + language/en-GB/plg_webservices_mokosuitebackup.ini + language/en-GB/plg_webservices_mokosuitebackup.sys.ini diff --git a/source/packages/com_mokojoombackup/api/index.html b/services/index.html similarity index 100% rename from source/packages/com_mokojoombackup/api/index.html rename to services/index.html diff --git a/services/provider.php b/services/provider.php new file mode 100644 index 0000000..18e5ea9 --- /dev/null +++ b/services/provider.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\WebServices\MokoSuiteBackup\Extension\MokoSuiteBackupWebServices; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoSuiteBackupWebServices( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('webservices', 'mokosuitebackup') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/language/en-GB/pkg_mokojoombackup.sys.ini b/source/language/en-GB/pkg_mokojoombackup.sys.ini deleted file mode 100644 index bf27f12..0000000 --- a/source/language/en-GB/pkg_mokojoombackup.sys.ini +++ /dev/null @@ -1,10 +0,0 @@ -; MokoJoomBackup — Package language file (en-GB) -; @package MokoJoomBackup -; @author Moko Consulting -; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. -; @license GPL-3.0-or-later - -PKG_MOKOJOOMBACKUP="Package - MokoJoomBackup" -PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API." -PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later." -PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your Update Site to receive automatic updates." diff --git a/source/language/en-GB/pkg_mokosuitebackup.sys.ini b/source/language/en-GB/pkg_mokosuitebackup.sys.ini new file mode 100644 index 0000000..8868c02 --- /dev/null +++ b/source/language/en-GB/pkg_mokosuitebackup.sys.ini @@ -0,0 +1,10 @@ +; MokoSuiteBackup — Package language file (en-GB) +; @package MokoSuiteBackup +; @author Moko Consulting +; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. +; @license GPL-3.0-or-later + +PKG_MOKOJOOMBACKUP="Package - MokoSuiteBackup" +PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API." +PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoSuiteBackup requires PHP %s or later." +PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your Update Site to receive automatic updates." diff --git a/source/language/en-US/pkg_mokojoombackup.sys.ini b/source/language/en-US/pkg_mokojoombackup.sys.ini deleted file mode 100644 index 07d63b6..0000000 --- a/source/language/en-US/pkg_mokojoombackup.sys.ini +++ /dev/null @@ -1,10 +0,0 @@ -; MokoJoomBackup — Package language file (en-US) -; @package MokoJoomBackup -; @author Moko Consulting -; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. -; @license GPL-3.0-or-later - -PKG_MOKOJOOMBACKUP="Package - MokoJoomBackup" -PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API." -PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later." -PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your Update Site to receive automatic updates." diff --git a/source/language/en-US/pkg_mokosuitebackup.sys.ini b/source/language/en-US/pkg_mokosuitebackup.sys.ini new file mode 100644 index 0000000..a0db20e --- /dev/null +++ b/source/language/en-US/pkg_mokosuitebackup.sys.ini @@ -0,0 +1,10 @@ +; MokoSuiteBackup — Package language file (en-US) +; @package MokoSuiteBackup +; @author Moko Consulting +; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. +; @license GPL-3.0-or-later + +PKG_MOKOJOOMBACKUP="Package - MokoSuiteBackup" +PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API." +PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoSuiteBackup requires PHP %s or later." +PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your Update Site to receive automatic updates." diff --git a/source/packages/com_mokojoombackup/language/en-GB/com_mokojoombackup.sys.ini b/source/packages/com_mokojoombackup/language/en-GB/com_mokojoombackup.sys.ini deleted file mode 100644 index 7a80b72..0000000 --- a/source/packages/com_mokojoombackup/language/en-GB/com_mokojoombackup.sys.ini +++ /dev/null @@ -1,11 +0,0 @@ -; MokoJoomBackup — Component system language file (en-GB) -; @package MokoJoomBackup -; @author Moko Consulting -; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. -; @license GPL-3.0-or-later - -COM_MOKOJOOMBACKUP="MokoJoomBackup" -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" -COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles" diff --git a/source/packages/com_mokojoombackup/language/en-US/com_mokojoombackup.sys.ini b/source/packages/com_mokojoombackup/language/en-US/com_mokojoombackup.sys.ini deleted file mode 100644 index 337fd6a..0000000 --- a/source/packages/com_mokojoombackup/language/en-US/com_mokojoombackup.sys.ini +++ /dev/null @@ -1,11 +0,0 @@ -; MokoJoomBackup — Component system language file (en-US) -; @package MokoJoomBackup -; @author Moko Consulting -; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. -; @license GPL-3.0-or-later - -COM_MOKOJOOMBACKUP="MokoJoomBackup" -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" -COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles" diff --git a/source/packages/com_mokojoombackup/sql/uninstall.mysql.sql b/source/packages/com_mokojoombackup/sql/uninstall.mysql.sql deleted file mode 100644 index 974f591..0000000 --- a/source/packages/com_mokojoombackup/sql/uninstall.mysql.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE IF EXISTS `#__mokojoombackup_records`; -DROP TABLE IF EXISTS `#__mokojoombackup_profiles`; diff --git a/source/packages/com_mokojoombackup/sql/updates/mysql/01.01.01.sql b/source/packages/com_mokojoombackup/sql/updates/mysql/01.01.01.sql deleted file mode 100644 index ec8fb68..0000000 --- a/source/packages/com_mokojoombackup/sql/updates/mysql/01.01.01.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `#__mokojoombackup_profiles` CHANGE `include_kickstart` `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive'; diff --git a/source/packages/com_mokojoombackup/sql/updates/mysql/01.01.02.sql b/source/packages/com_mokojoombackup/sql/updates/mysql/01.01.02.sql deleted file mode 100644 index 8b86fb7..0000000 --- a/source/packages/com_mokojoombackup/sql/updates/mysql/01.01.02.sql +++ /dev/null @@ -1,12 +0,0 @@ --- MokoJoomBackup 01.01.02 --- Consolidated schema updates: NULL defaults, notifications, archive name format - --- Fix: allow NULL defaults for manifest and log columns -ALTER TABLE `#__mokojoombackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL; -ALTER TABLE `#__mokojoombackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL; - --- Add user group notifications column to profiles -ALTER TABLE `#__mokojoombackup_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 `#__mokojoombackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`; diff --git a/source/packages/com_mokojoombackup/src/Extension/MokoJoomBackupComponent.php b/source/packages/com_mokojoombackup/src/Extension/MokoJoomBackupComponent.php deleted file mode 100644 index 10cd7f0..0000000 --- a/source/packages/com_mokojoombackup/src/Extension/MokoJoomBackupComponent.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. - * @license GNU General Public License version 3 or later; see LICENSE - */ - -namespace Joomla\Component\MokoJoomBackup\Administrator\Extension; - -defined('_JEXEC') or die; - -use Joomla\CMS\Extension\MVCComponent; -use Joomla\CMS\Factory; - -class MokoJoomBackupComponent extends MVCComponent -{ - public function boot(): void - { - parent::boot(); - - try { - $app = Factory::getApplication(); - - if (!$app->isClient('administrator')) { - return; - } - - $wa = $app->getDocument()->getWebAssetManager(); - $wa->addInlineStyle( - '#menu a[href*="com_mokojoombackup"][href*="view=dashboard"] .sidebar-item-title::before,' - . ' #menu a[href*="com_mokojoombackup"][href*="view=backups"] .sidebar-item-title::before,' - . ' #menu a[href*="com_mokojoombackup"][href*="view=profiles"] .sidebar-item-title::before' - . ' { font-family: "Font Awesome 6 Free"; font-weight: 900; margin-right: .5em; }' - . ' #menu a[href*="com_mokojoombackup"][href*="view=dashboard"] .sidebar-item-title::before { content: "\f015"; }' - . ' #menu a[href*="com_mokojoombackup"][href*="view=backups"] .sidebar-item-title::before { content: "\f1c0"; }' - . ' #menu a[href*="com_mokojoombackup"][href*="view=profiles"] .sidebar-item-title::before { content: "\f013"; }' - ); - } catch (\Throwable $e) { - error_log('MokoJoomBackup: boot() CSS injection failed: ' . $e->getMessage()); - } - } -} diff --git a/source/packages/com_mokojoombackup/src/Utility/BackupDirectory.php b/source/packages/com_mokojoombackup/src/Utility/BackupDirectory.php deleted file mode 100644 index 9048f19..0000000 --- a/source/packages/com_mokojoombackup/src/Utility/BackupDirectory.php +++ /dev/null @@ -1,153 +0,0 @@ - - * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. - * @license GNU General Public License version 3 or later; see LICENSE - */ - -namespace Joomla\Component\MokoJoomBackup\Administrator\Utility; - -defined('_JEXEC') or die; - -class BackupDirectory -{ - public const DEFAULT_RELATIVE = 'administrator/components/com_mokojoombackup/backups'; - - public const PLACEHOLDER = '[DEFAULT_DIR]'; - - private const HTACCESS_CONTENT = <<<'HTACCESS' -# Apache 2.4+ - - Require all denied - -# Apache 2.2 - - Order deny,allow - Deny from all - -HTACCESS; - - private const INDEX_CONTENT = ''; - - /** - * Get the absolute default backup directory path. - */ - public static function getDefaultAbsolute(): string - { - return JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups'; - } - - /** - * Resolve a backup directory path. Replaces [DEFAULT_DIR] placeholder, - * then resolves relative paths from JPATH_ROOT. - * - * @param string $dir Raw directory value from profile - * - * @return string Absolute path (may still contain other placeholders) - */ - public static function resolve(string $dir): string - { - if ($dir === '' || $dir === self::PLACEHOLDER) { - $dir = self::getDefaultAbsolute(); - } else { - $dir = str_replace(self::PLACEHOLDER, self::getDefaultAbsolute(), $dir); - } - - if ($dir !== '' && ($dir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $dir))) { - return rtrim($dir, '/\\'); - } - - return JPATH_ROOT . '/' . $dir; - } - - /** - * Check whether a resolved path still contains unresolved placeholders. - */ - public static function hasPlaceholders(string $path): bool - { - return (bool) preg_match('/\[.+\]/', $path); - } - - /** - * Check whether a resolved absolute path is inside the web root. - */ - public static function isWebAccessible(string $absolutePath): bool - { - $jRoot = realpath(JPATH_ROOT) ?: JPATH_ROOT; - $realDir = realpath($absolutePath) ?: $absolutePath; - - return strpos($realDir, $jRoot) === 0; - } - - /** - * Create .htaccess and index.html protection files in a directory. - * Only creates files if they don't already exist. - */ - public static function protect(string $dir): void - { - if (!is_dir($dir)) { - return; - } - - $htaccess = $dir . '/.htaccess'; - - if (!is_file($htaccess)) { - if (@file_put_contents($htaccess, self::HTACCESS_CONTENT . "\n") === false) { - error_log('MokoJoomBackup: Could not create .htaccess in: ' . $dir); - } - } - - $index = $dir . '/index.html'; - - if (!is_file($index)) { - if (@file_put_contents($index, self::INDEX_CONTENT) === false) { - error_log('MokoJoomBackup: Could not create index.html in: ' . $dir); - } - } - } - - /** - * Ensure the backup directory exists, create it if needed, - * and apply web protection if it's inside the web root. - * - * @return bool True if directory exists and is usable - */ - public static function ensureReady(string $dir): bool - { - if (!is_dir($dir)) { - if (!@mkdir($dir, 0755, true)) { - return false; - } - } - - self::protect($dir); - - return true; - } - - /** - * Parse a newline-separated text field into an array of trimmed, non-empty strings. - */ - public static function parseNewlineList(string $text): array - { - if (empty($text)) { - return []; - } - - return array_values(array_filter( - array_map('trim', explode("\n", str_replace("\r", '', $text))), - fn($line) => $line !== '' - )); - } - - /** - * Derive the log file path from an archive path. - */ - public static function logPathFromArchive(string $archivePath): string - { - return preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath); - } -} diff --git a/source/packages/com_mokosuitebackup/access.xml b/source/packages/com_mokosuitebackup/access.xml new file mode 100644 index 0000000..c7e6f78 --- /dev/null +++ b/source/packages/com_mokosuitebackup/access.xml @@ -0,0 +1,15 @@ + + +
+ + + + + + + + + + +
+
diff --git a/source/packages/com_mokojoombackup/api/src/Controller/index.html b/source/packages/com_mokosuitebackup/api/index.html similarity index 100% rename from source/packages/com_mokojoombackup/api/src/Controller/index.html rename to source/packages/com_mokosuitebackup/api/index.html diff --git a/source/packages/com_mokojoombackup/api/src/Controller/BackupsController.php b/source/packages/com_mokosuitebackup/api/src/Controller/BackupsController.php similarity index 51% rename from source/packages/com_mokojoombackup/api/src/Controller/BackupsController.php rename to source/packages/com_mokosuitebackup/api/src/Controller/BackupsController.php index 1f5b829..a471b48 100644 --- a/source/packages/com_mokojoombackup/api/src/Controller/BackupsController.php +++ b/source/packages/com_mokosuitebackup/api/src/Controller/BackupsController.php @@ -1,19 +1,19 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Api\Controller; +namespace Joomla\Component\MokoSuiteBackup\Api\Controller; defined('_JEXEC') or die; use Joomla\CMS\MVC\Controller\ApiController; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; class BackupsController extends ApiController { @@ -21,10 +21,18 @@ class BackupsController extends ApiController protected $default_view = 'backups'; /** - * Start a new backup (POST /api/index.php/v1/mokojoombackup/backup) + * Start a new backup (POST /api/index.php/v1/mokosuitebackup/backup) */ public function backup(): static { + if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) { + $this->app->setHeader('status', 403); + echo json_encode(['errors' => [['title' => 'Access denied']]]); + $this->app->close(); + + return $this; + } + $data = json_decode($this->input->json->getRaw(), true) ?: []; $profileId = (int) ($data['profile'] ?? 1); @@ -47,10 +55,18 @@ class BackupsController extends ApiController } /** - * Download a backup archive (GET /api/index.php/v1/mokojoombackup/backup/:id/download) + * Download a backup archive (GET /api/index.php/v1/mokosuitebackup/backup/:id/download) */ public function download(): static { + if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')) { + $this->app->setHeader('status', 403); + echo json_encode(['errors' => [['title' => 'Access denied']]]); + $this->app->close(); + + return $this; + } + $id = $this->input->getInt('id', 0); $model = $this->getModel('Backup', 'Administrator'); @@ -64,20 +80,42 @@ class BackupsController extends ApiController return $this; } - $content = base64_encode(file_get_contents($item->absolute_path)); + // Stream as binary download instead of base64 to avoid memory exhaustion + while (@ob_end_clean()) { + // clear all buffers + } + + $filename = basename($item->archivename ?? $item->absolute_path); + $filesize = filesize($item->absolute_path); + $contentType = str_ends_with($filename, '.tar.gz') + ? 'application/gzip' + : 'application/zip'; + + header('Content-Type: ' . $contentType); + header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename)); + header('Content-Length: ' . $filesize); + header('Cache-Control: no-cache, must-revalidate'); + + readfile($item->absolute_path); - $this->app->setHeader('status', 200); - echo json_encode(['data' => $content]); $this->app->close(); return $this; } /** - * List backup profiles (GET /api/index.php/v1/mokojoombackup/profiles) + * List backup profiles (GET /api/index.php/v1/mokosuitebackup/profiles) */ public function profiles(): static { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) { + $this->app->setHeader('status', 403); + echo json_encode(['errors' => [['title' => 'Access denied']]]); + $this->app->close(); + + return $this; + } + $model = $this->getModel('Profiles', 'Administrator'); $items = $model->getItems(); diff --git a/source/packages/com_mokojoombackup/api/src/View/Backups/index.html b/source/packages/com_mokosuitebackup/api/src/Controller/index.html similarity index 100% rename from source/packages/com_mokojoombackup/api/src/View/Backups/index.html rename to source/packages/com_mokosuitebackup/api/src/Controller/index.html diff --git a/source/packages/com_mokojoombackup/api/src/View/Backups/JsonapiView.php b/source/packages/com_mokosuitebackup/api/src/View/Backups/JsonapiView.php similarity index 87% rename from source/packages/com_mokojoombackup/api/src/View/Backups/JsonapiView.php rename to source/packages/com_mokosuitebackup/api/src/View/Backups/JsonapiView.php index f39925f..147fe92 100644 --- a/source/packages/com_mokojoombackup/api/src/View/Backups/JsonapiView.php +++ b/source/packages/com_mokosuitebackup/api/src/View/Backups/JsonapiView.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Api\View\Backups; +namespace Joomla\Component\MokoSuiteBackup\Api\View\Backups; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/api/src/View/index.html b/source/packages/com_mokosuitebackup/api/src/View/Backups/index.html similarity index 100% rename from source/packages/com_mokojoombackup/api/src/View/index.html rename to source/packages/com_mokosuitebackup/api/src/View/Backups/index.html diff --git a/source/packages/com_mokojoombackup/api/src/index.html b/source/packages/com_mokosuitebackup/api/src/View/index.html similarity index 100% rename from source/packages/com_mokojoombackup/api/src/index.html rename to source/packages/com_mokosuitebackup/api/src/View/index.html diff --git a/source/packages/com_mokojoombackup/cli/index.html b/source/packages/com_mokosuitebackup/api/src/index.html similarity index 100% rename from source/packages/com_mokojoombackup/cli/index.html rename to source/packages/com_mokosuitebackup/api/src/index.html diff --git a/source/packages/com_mokojoombackup/forms/index.html b/source/packages/com_mokosuitebackup/cli/index.html similarity index 100% rename from source/packages/com_mokojoombackup/forms/index.html rename to source/packages/com_mokosuitebackup/cli/index.html diff --git a/source/packages/com_mokojoombackup/cli/mokojoombackup.php b/source/packages/com_mokosuitebackup/cli/mokosuitebackup.php similarity index 85% rename from source/packages/com_mokojoombackup/cli/mokojoombackup.php rename to source/packages/com_mokosuitebackup/cli/mokosuitebackup.php index 9135706..29f1c93 100644 --- a/source/packages/com_mokojoombackup/cli/mokojoombackup.php +++ b/source/packages/com_mokosuitebackup/cli/mokosuitebackup.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -10,7 +10,7 @@ * CLI backup script for cron/scheduled use. * * Usage: - * php cli/mokojoombackup.php --profile=1 --description="Scheduled backup" + * php cli/mokosuitebackup.php --profile=1 --description="Scheduled backup" * * Must be run from the Joomla root directory. */ @@ -30,7 +30,7 @@ if (!defined('JPATH_BASE')) { require_once JPATH_BASE . '/includes/framework.php'; use Joomla\CMS\Factory; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; // Parse CLI arguments $profileId = 1; @@ -51,7 +51,7 @@ if (empty($description)) { // Boot the application $app = Factory::getApplication('administrator'); -echo "MokoJoomBackup CLI\n"; +echo "MokoSuiteBackup CLI\n"; echo "Profile: {$profileId}\n"; echo "Description: {$description}\n"; echo "Starting backup...\n\n"; diff --git a/source/packages/com_mokojoombackup/config.xml b/source/packages/com_mokosuitebackup/config.xml similarity index 76% rename from source/packages/com_mokojoombackup/config.xml rename to source/packages/com_mokosuitebackup/config.xml index 98c5a0d..5b93f7f 100644 --- a/source/packages/com_mokojoombackup/config.xml +++ b/source/packages/com_mokosuitebackup/config.xml @@ -1,7 +1,7 @@ - com_mokojoombackup - 01.08.00 + MokoSuiteBackup + 01.20.00-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech @@ -17,7 +16,7 @@ GPL-3.0-or-later COM_MOKOJOOMBACKUP_DESCRIPTION - Joomla\Component\MokoJoomBackup + Joomla\Component\MokoSuiteBackup @@ -40,11 +39,19 @@ COM_MOKOJOOMBACKUP - COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD - COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS - COM_MOKOJOOMBACKUP_SUBMENU_PROFILES + COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD + COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS + COM_MOKOJOOMBACKUP_SUBMENU_PROFILES + access.xml + config.xml cli forms services @@ -53,8 +60,8 @@ tmpl - en-GB/com_mokojoombackup.ini - en-GB/com_mokojoombackup.sys.ini + en-GB/com_mokosuitebackup.ini + en-GB/com_mokosuitebackup.sys.ini diff --git a/source/packages/com_mokojoombackup/sql/index.html b/source/packages/com_mokosuitebackup/services/index.html similarity index 100% rename from source/packages/com_mokojoombackup/sql/index.html rename to source/packages/com_mokosuitebackup/services/index.html diff --git a/source/packages/com_mokojoombackup/services/provider.php b/source/packages/com_mokosuitebackup/services/provider.php similarity index 79% rename from source/packages/com_mokojoombackup/services/provider.php rename to source/packages/com_mokosuitebackup/services/provider.php index af40114..58a3949 100644 --- a/source/packages/com_mokojoombackup/services/provider.php +++ b/source/packages/com_mokosuitebackup/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -15,20 +15,20 @@ use Joomla\CMS\Extension\ComponentInterface; use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory; use Joomla\CMS\Extension\Service\Provider\MVCFactory; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; -use Joomla\Component\MokoJoomBackup\Administrator\Extension\MokoJoomBackupComponent; +use Joomla\Component\MokoSuiteBackup\Administrator\Extension\MokoSuiteBackupComponent; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; return new class () implements ServiceProviderInterface { public function register(Container $container): void { - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\MokoJoomBackup')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\MokoJoomBackup')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\MokoSuiteBackup')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\MokoSuiteBackup')); $container->set( ComponentInterface::class, function (Container $container) { - $component = new MokoJoomBackupComponent( + $component = new MokoSuiteBackupComponent( $container->get(ComponentDispatcherFactoryInterface::class) ); $component->setMVCFactory($container->get(MVCFactoryInterface::class)); diff --git a/source/packages/com_mokojoombackup/sql/mysql/index.html b/source/packages/com_mokosuitebackup/sql/index.html similarity index 100% rename from source/packages/com_mokojoombackup/sql/mysql/index.html rename to source/packages/com_mokosuitebackup/sql/index.html diff --git a/source/packages/com_mokojoombackup/sql/install.mysql.sql b/source/packages/com_mokosuitebackup/sql/install.mysql.sql similarity index 89% rename from source/packages/com_mokojoombackup/sql/install.mysql.sql rename to source/packages/com_mokosuitebackup/sql/install.mysql.sql index 47a5427..8929203 100644 --- a/source/packages/com_mokojoombackup/sql/install.mysql.sql +++ b/source/packages/com_mokosuitebackup/sql/install.mysql.sql @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` ( +CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `title` VARCHAR(255) NOT NULL DEFAULT '', `description` TEXT NOT NULL, @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` ( `archive_format` VARCHAR(10) NOT NULL DEFAULT 'zip', `compression_level` TINYINT(1) UNSIGNED NOT NULL DEFAULT 5 COMMENT '0=none, 9=max', `split_size` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '0=no split, otherwise MB per part', - `backup_dir` VARCHAR(512) NOT NULL DEFAULT 'administrator/components/com_mokojoombackup/backups', + `backup_dir` VARCHAR(512) NOT NULL DEFAULT '[DEFAULT_DIR]', `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders', `exclude_dirs` TEXT NOT NULL COMMENT 'Newline-separated directory paths to exclude', `exclude_files` TEXT NOT NULL COMMENT 'Newline-separated filename patterns to exclude', @@ -36,6 +36,9 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_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, + `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)', `published` TINYINT(1) NOT NULL DEFAULT 1, `ordering` INT(11) NOT NULL DEFAULT 0, `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', @@ -44,7 +47,7 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` ( KEY `idx_published` (`published`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS `#__mokojoombackup_records` ( +CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_records` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `profile_id` INT(11) UNSIGNED NOT NULL DEFAULT 1, `description` VARCHAR(255) NOT NULL DEFAULT '', @@ -74,15 +77,15 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_records` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Insert default backup profile (IGNORE prevents duplicate key error on update) -INSERT IGNORE INTO `#__mokojoombackup_profiles` ( +INSERT IGNORE INTO `#__mokosuitebackup_profiles` ( `id`, `title`, `description`, `backup_type`, `archive_format`, `compression_level`, `split_size`, `backup_dir`, `exclude_dirs`, `exclude_files`, `exclude_tables`, `published`, `ordering`, `created`, `modified` ) VALUES ( 1, 'Default Backup Profile', 'Full site backup with default settings', 'full', - 'zip', 5, 0, 'administrator/components/com_mokojoombackup/backups', - 'administrator/components/com_mokojoombackup/backups\ntmp\ncache\nlogs\nadministrator/logs', + 'zip', 5, 0, '[DEFAULT_DIR]', + 'administrator/components/com_mokosuitebackup/backups\ntmp\ncache\nlogs\nadministrator/logs', '.gitignore\n.htaccess.bak', '#__session', 1, 1, NOW(), NOW() diff --git a/source/packages/com_mokojoombackup/sql/updates/index.html b/source/packages/com_mokosuitebackup/sql/mysql/index.html similarity index 100% rename from source/packages/com_mokojoombackup/sql/updates/index.html rename to source/packages/com_mokosuitebackup/sql/mysql/index.html diff --git a/source/packages/com_mokosuitebackup/sql/uninstall.mysql.sql b/source/packages/com_mokosuitebackup/sql/uninstall.mysql.sql new file mode 100644 index 0000000..241bd9b --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/uninstall.mysql.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS `#__mokosuitebackup_records`; +DROP TABLE IF EXISTS `#__mokosuitebackup_profiles`; diff --git a/source/packages/com_mokojoombackup/sql/updates/mysql/index.html b/source/packages/com_mokosuitebackup/sql/updates/index.html similarity index 100% rename from source/packages/com_mokojoombackup/sql/updates/mysql/index.html rename to source/packages/com_mokosuitebackup/sql/updates/index.html diff --git a/source/packages/com_mokojoombackup/sql/updates/mysql/01.00.00.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.00.00.sql similarity index 100% rename from source/packages/com_mokojoombackup/sql/updates/mysql/01.00.00.sql rename to source/packages/com_mokosuitebackup/sql/updates/mysql/01.00.00.sql diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.01.01.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.01.01.sql new file mode 100644 index 0000000..0c6a473 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.01.01.sql @@ -0,0 +1 @@ +ALTER TABLE `#__mokosuitebackup_profiles` CHANGE `include_kickstart` `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive'; diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.01.02.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.01.02.sql new file mode 100644 index 0000000..9cc869f --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.01.02.sql @@ -0,0 +1,12 @@ +-- MokoSuiteBackup 01.01.02 +-- Consolidated schema updates: NULL defaults, notifications, archive name format + +-- Fix: allow NULL defaults for manifest and log columns +ALTER TABLE `#__mokosuitebackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL; +ALTER TABLE `#__mokosuitebackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL; + +-- Add user group notifications column to profiles +ALTER TABLE `#__mokosuitebackup_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 `#__mokosuitebackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`; diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.18.00.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.18.00.sql new file mode 100644 index 0000000..ef693b2 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.18.00.sql @@ -0,0 +1,5 @@ +-- Add ntfy push notification fields to backup profiles +ALTER TABLE `#__mokosuitebackup_profiles` + ADD COLUMN `ntfy_topic` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy topic name' AFTER `notify_on_failure`, + ADD COLUMN `ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL' AFTER `ntfy_topic`, + ADD COLUMN `ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional, for private topics)' AFTER `ntfy_server`; diff --git a/source/packages/com_mokojoombackup/src/Controller/index.html b/source/packages/com_mokosuitebackup/sql/updates/mysql/index.html similarity index 100% rename from source/packages/com_mokojoombackup/src/Controller/index.html rename to source/packages/com_mokosuitebackup/sql/updates/mysql/index.html diff --git a/source/packages/com_mokojoombackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php similarity index 72% rename from source/packages/com_mokojoombackup/src/Controller/AjaxController.php rename to source/packages/com_mokosuitebackup/src/Controller/AjaxController.php index 6b3c3e6..2cdbbb7 100644 --- a/source/packages/com_mokojoombackup/src/Controller/AjaxController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -11,14 +11,14 @@ * Handles init and step requests from the admin UI JavaScript. */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller; defined('_JEXEC') or die; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Session\Session; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\SteppedBackupEngine; -use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\SteppedBackupEngine; +use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory; class AjaxController extends BaseController { @@ -29,7 +29,13 @@ class AjaxController extends BaseController public function init(): void { if (!Session::checkToken('get') && !Session::checkToken('post')) { - $this->sendJson(['error' => true, 'message' => 'Invalid token']); + $this->sendJson(['error' => true, 'message' => 'Invalid token'], 403); + + return; + } + + if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) { + $this->sendJson(['error' => true, 'message' => 'Access denied'], 403); return; } @@ -50,7 +56,13 @@ class AjaxController extends BaseController public function step(): void { if (!Session::checkToken('get') && !Session::checkToken('post')) { - $this->sendJson(['error' => true, 'message' => 'Invalid token']); + $this->sendJson(['error' => true, 'message' => 'Invalid token'], 403); + + return; + } + + if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) { + $this->sendJson(['error' => true, 'message' => 'Access denied'], 403); return; } @@ -76,17 +88,26 @@ class AjaxController extends BaseController public function browseDir(): void { if (!Session::checkToken('get') && !Session::checkToken('post')) { - $this->sendJson(['error' => true, 'message' => 'Invalid token']); + $this->sendJson(['error' => true, 'message' => 'Invalid token'], 403); + + return; + } + + if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) { + $this->sendJson(['error' => true, 'message' => 'Access denied'], 403); return; } $requestPath = $this->input->getString('path', JPATH_ROOT); - $path = realpath($requestPath) ?: $requestPath; + + // Resolve placeholders and relative paths before permission check + $resolved = BackupDirectory::resolve($requestPath); + $path = realpath($resolved) ?: $resolved; // Security: restrict browsing to site root and current user's home $jRoot = realpath(JPATH_ROOT); - $homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ''); + $homeDir = BackupDirectory::getHomeDirectory(); $allowed = false; if ($jRoot !== false && strpos($path, $jRoot) === 0) { @@ -143,7 +164,7 @@ class AjaxController extends BaseController if ($parent !== $path) { if ($jRoot !== false && strpos($parent, $jRoot) === 0) { $parentAllowed = true; - } elseif ($homeDir !== '' && strpos($parent, $homeDir) === 0) { + } elseif ($homeDir !== '' && (strpos($parent, $homeDir) === 0 || $parent === \dirname($homeDir))) { $parentAllowed = true; } } @@ -169,7 +190,13 @@ class AjaxController extends BaseController public function viewLog(): void { if (!Session::checkToken('get') && !Session::checkToken('post')) { - $this->sendJson(['error' => true, 'message' => 'Invalid token']); + $this->sendJson(['error' => true, 'message' => 'Invalid token'], 403); + + return; + } + + if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) { + $this->sendJson(['error' => true, 'message' => 'Access denied'], 403); return; } @@ -182,16 +209,23 @@ class AjaxController extends BaseController return; } - $db = \Joomla\CMS\Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['absolute_path', 'log'])) - ->from($db->quoteName('#__mokojoombackup_records')) - ->where($db->quoteName('id') . ' = ' . (int) $id); - $db->setQuery($query); - $record = $db->loadObject(); + try { + $db = \Joomla\CMS\Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName(['absolute_path', 'log'])) + ->from($db->quoteName('#__mokosuitebackup_records')) + ->where($db->quoteName('id') . ' = ' . (int) $id); + $db->setQuery($query); + $record = $db->loadObject(); + } catch (\Exception $e) { + error_log('MokoSuiteBackup: viewLog() DB error for record ' . $id . ': ' . $e->getMessage()); + $this->sendJson(['error' => true, 'message' => 'Failed to load backup record'], 500); + + return; + } if (!$record) { - $this->sendJson(['error' => true, 'message' => 'Record not found']); + $this->sendJson(['error' => true, 'message' => 'Record not found'], 404); return; } @@ -199,18 +233,26 @@ class AjaxController extends BaseController // Try to load log from file alongside the archive $logPath = BackupDirectory::logPathFromArchive($record->absolute_path); $logContent = ''; + $source = 'none'; if (is_file($logPath)) { - $logContent = file_get_contents($logPath); + $content = file_get_contents($logPath); + + if ($content !== false) { + $logContent = $content; + $source = 'file'; + } else { + $source = 'file (read error)'; + } } elseif (!empty($record->log)) { - // Fall back to database-stored log $logContent = $record->log; + $source = 'database'; } $this->sendJson([ 'error' => false, 'log' => $logContent ?: '(no log available)', - 'source' => is_file($logPath) ? 'file' : 'database', + 'source' => $source, ]); } @@ -221,13 +263,13 @@ class AjaxController extends BaseController public function checkDir(): void { if (!Session::checkToken('get') && !Session::checkToken('post')) { - $this->sendJson(['error' => true, 'message' => 'Invalid token']); + $this->sendJson(['error' => true, 'message' => 'Invalid token'], 403); return; } - if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokojoombackup')) { - $this->sendJson(['error' => true, 'message' => 'Access denied']); + if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) { + $this->sendJson(['error' => true, 'message' => 'Access denied'], 403); return; } @@ -269,9 +311,10 @@ class AjaxController extends BaseController /** * Send a JSON response and close the application. */ - private function sendJson(array $data): void + private function sendJson(array $data, int $status = 200): void { $app = $this->app; + $app->setHeader('status', $status); $app->setHeader('Content-Type', 'application/json; charset=utf-8'); $app->setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); $app->sendHeaders(); diff --git a/source/packages/com_mokojoombackup/src/Controller/BackupController.php b/source/packages/com_mokosuitebackup/src/Controller/BackupController.php similarity index 74% rename from source/packages/com_mokojoombackup/src/Controller/BackupController.php rename to source/packages/com_mokosuitebackup/src/Controller/BackupController.php index 7844749..0b315de 100644 --- a/source/packages/com_mokojoombackup/src/Controller/BackupController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/BackupController.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/src/Controller/BackupsController.php b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php similarity index 53% rename from source/packages/com_mokojoombackup/src/Controller/BackupsController.php rename to source/packages/com_mokosuitebackup/src/Controller/BackupsController.php index c1a99ca..a15527e 100644 --- a/source/packages/com_mokojoombackup/src/Controller/BackupsController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php @@ -1,21 +1,22 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller; defined('_JEXEC') or die; +use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\AdminController; use Joomla\CMS\Router\Route; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\RestoreEngine; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RestoreEngine; class BackupsController extends AdminController { @@ -35,6 +36,13 @@ class BackupsController extends AdminController { $this->checkToken(); + if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); + + return; + } + $profileId = $this->input->getInt('profile_id', 1); $description = $this->input->getString('description', ''); @@ -47,7 +55,7 @@ class BackupsController extends AdminController $this->setMessage($result['message'], 'error'); } - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); } /** @@ -57,13 +65,22 @@ class BackupsController extends AdminController */ public function download(): void { + $this->checkToken('get'); + + if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); + + return; + } + $id = $this->input->getInt('id', 0); $model = $this->getModel('Backup'); $item = $model->getItem($id); if (!$item || !$item->id || !$item->filesexist || !is_file($item->absolute_path)) { - $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND', 'error'); - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); return; } @@ -82,7 +99,7 @@ class BackupsController extends AdminController : 'application/zip'; header('Content-Type: ' . $contentType); - header('Content-Disposition: attachment; filename="' . $filename . '"'); + header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename)); header('Content-Length: ' . $filesize); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: no-cache'); @@ -101,6 +118,13 @@ class BackupsController extends AdminController { $this->checkToken(); + if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); + + return; + } + $id = $this->input->getInt('id', 0); $restoreFiles = (bool) $this->input->getInt('restore_files', 1); $restoreDb = (bool) $this->input->getInt('restore_db', 1); @@ -108,8 +132,8 @@ class BackupsController extends AdminController $password = $this->input->getString('encryption_password', ''); if (!$id) { - $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error'); - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); return; } @@ -123,7 +147,7 @@ class BackupsController extends AdminController $this->setMessage($result['message'], 'error'); } - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); } /** @@ -133,12 +157,19 @@ class BackupsController extends AdminController { $this->checkToken(); + if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); + + return; + } + $cid = $this->input->get('cid', [], 'array'); $id = !empty($cid) ? (int) $cid[0] : $this->input->getInt('id', 0); if (!$id) { - $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error'); - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); return; } @@ -147,22 +178,22 @@ class BackupsController extends AdminController $item = $model->getItem($id); if (!$item || !$item->id) { - $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error'); - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); return; } if (!is_file($item->absolute_path)) { - $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND', 'error'); - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); return; } if (empty($item->checksum)) { - $this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM', 'warning'); - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM'), 'warning'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); return; } @@ -170,11 +201,11 @@ class BackupsController extends AdminController $currentHash = hash_file('sha256', $item->absolute_path); if ($currentHash === $item->checksum) { - $this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_OK'); + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_VERIFY_OK')); } else { - $this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_FAILED', 'error'); + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_VERIFY_FAILED'), 'error'); } - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); } } diff --git a/source/packages/com_mokojoombackup/src/Controller/DisplayController.php b/source/packages/com_mokosuitebackup/src/Controller/DisplayController.php similarity index 74% rename from source/packages/com_mokojoombackup/src/Controller/DisplayController.php rename to source/packages/com_mokosuitebackup/src/Controller/DisplayController.php index cac40fd..d4a90e0 100644 --- a/source/packages/com_mokojoombackup/src/Controller/DisplayController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/DisplayController.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/src/Controller/ProfileController.php b/source/packages/com_mokosuitebackup/src/Controller/ProfileController.php similarity index 74% rename from source/packages/com_mokojoombackup/src/Controller/ProfileController.php rename to source/packages/com_mokosuitebackup/src/Controller/ProfileController.php index d7540e8..1186db4 100644 --- a/source/packages/com_mokojoombackup/src/Controller/ProfileController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/ProfileController.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/src/Controller/ProfilesController.php b/source/packages/com_mokosuitebackup/src/Controller/ProfilesController.php similarity index 81% rename from source/packages/com_mokojoombackup/src/Controller/ProfilesController.php rename to source/packages/com_mokosuitebackup/src/Controller/ProfilesController.php index fa0ad47..061feec 100644 --- a/source/packages/com_mokojoombackup/src/Controller/ProfilesController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/ProfilesController.php @@ -1,21 +1,22 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller; defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\AdminController; use Joomla\CMS\Router\Route; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\AkeebaImporter; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\AkeebaImporter; class ProfilesController extends AdminController { @@ -33,6 +34,13 @@ class ProfilesController extends AdminController { $this->checkToken(); + if (!$this->app->getIdentity()->authorise('core.create', 'com_mokosuitebackup')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=profiles', false)); + + return; + } + $importHistory = (bool) $this->input->getInt('import_history', 1); $importer = new AkeebaImporter(); @@ -40,7 +48,7 @@ class ProfilesController extends AdminController if (!$detection['profiles']) { $this->setMessage('COM_MOKOJOOMBACKUP_AKEEBA_NOT_FOUND', 'error'); - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=profiles', false)); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=profiles', false)); return; } @@ -55,7 +63,7 @@ class ProfilesController extends AdminController $this->setMessage($result['message'], 'error'); } - $this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=profiles', false)); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=profiles', false)); } /** diff --git a/source/packages/com_mokojoombackup/src/Engine/index.html b/source/packages/com_mokosuitebackup/src/Controller/index.html similarity index 100% rename from source/packages/com_mokojoombackup/src/Engine/index.html rename to source/packages/com_mokosuitebackup/src/Controller/index.html diff --git a/source/packages/com_mokojoombackup/src/Engine/AkeebaImporter.php b/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php similarity index 95% rename from source/packages/com_mokojoombackup/src/Engine/AkeebaImporter.php rename to source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php index 6861c78..5e32b37 100644 --- a/source/packages/com_mokojoombackup/src/Engine/AkeebaImporter.php +++ b/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php @@ -1,16 +1,16 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE * - * Imports Akeeba Backup Pro profiles and backup history into MokoJoomBackup. + * Imports Akeeba Backup Pro profiles and backup history into MokoSuiteBackup. * * Reads from #__ak_profiles and #__ak_stats, maps Akeeba's configuration - * format to MokoJoomBackup's individual column format. + * format to MokoSuiteBackup's individual column format. * * Akeeba config format: * INI-style with dot-notation keys, e.g.: @@ -25,12 +25,12 @@ * "databases": {"include": {...}, "exclude": {...}}} */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; use Joomla\CMS\Factory; -use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; +use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory; class AkeebaImporter { @@ -90,7 +90,7 @@ class AkeebaImporter } /** - * Import all Akeeba profiles into MokoJoomBackup. + * Import all Akeeba profiles into MokoSuiteBackup. * * @param bool $importHistory Also import backup history from #__ak_stats * @@ -120,7 +120,7 @@ class AkeebaImporter $akProfiles = $db->loadObjectList(); $profilesImported = 0; - $profileIdMap = []; // akeeba_id => mokojoombackup_id + $profileIdMap = []; // akeeba_id => mokosuitebackup_id foreach ($akProfiles as $akProfile) { $config = $this->parseAkeebaConfig($akProfile->configuration ?? ''); @@ -128,11 +128,11 @@ class AkeebaImporter $mokoProfile = $this->mapToMokoProfile($akProfile, $config, $filters); - $db->insertObject('#__mokojoombackup_profiles', $mokoProfile, 'id'); + $db->insertObject('#__mokosuitebackup_profiles', $mokoProfile, 'id'); $profileIdMap[$akProfile->id] = $mokoProfile->id; $profilesImported++; - $this->log('Imported profile: "' . $akProfile->description . '" (Akeeba #' . $akProfile->id . ' → MokoJoomBackup #' . $mokoProfile->id . ')'); + $this->log('Imported profile: "' . $akProfile->description . '" (Akeeba #' . $akProfile->id . ' → MokoSuiteBackup #' . $mokoProfile->id . ')'); } // Import backup history @@ -201,7 +201,7 @@ class AkeebaImporter 'log' => 'Imported from Akeeba Backup record #' . $stat->id, ]; - $db->insertObject('#__mokojoombackup_records', $record, 'id'); + $db->insertObject('#__mokosuitebackup_records', $record, 'id'); $imported++; } @@ -211,7 +211,7 @@ class AkeebaImporter } /** - * Map an Akeeba profile to a MokoJoomBackup profile object. + * Map an Akeeba profile to a MokoSuiteBackup profile object. */ private function mapToMokoProfile(object $akProfile, array $config, array $filters): object { diff --git a/source/packages/com_mokojoombackup/src/Engine/ArchiverInterface.php b/source/packages/com_mokosuitebackup/src/Engine/ArchiverInterface.php similarity index 86% rename from source/packages/com_mokojoombackup/src/Engine/ArchiverInterface.php rename to source/packages/com_mokosuitebackup/src/Engine/ArchiverInterface.php index b496059..282f1a8 100644 --- a/source/packages/com_mokojoombackup/src/Engine/ArchiverInterface.php +++ b/source/packages/com_mokosuitebackup/src/Engine/ArchiverInterface.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php similarity index 82% rename from source/packages/com_mokojoombackup/src/Engine/BackupEngine.php rename to source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php index d9863cf..bf6544d 100644 --- a/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php @@ -1,19 +1,19 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; use Joomla\CMS\Factory; -use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; +use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory; use Joomla\Event\Event; class BackupEngine @@ -47,7 +47,7 @@ class BackupEngine // Load profile $query = $db->getQuery(true) ->select('*') - ->from($db->quoteName('#__mokojoombackup_profiles')) + ->from($db->quoteName('#__mokosuitebackup_profiles')) ->where($db->quoteName('id') . ' = ' . $profileId); $db->setQuery($query); $profile = $db->loadObject(); @@ -105,7 +105,7 @@ class BackupEngine 'log' => '', ]; - $db->insertObject('#__mokojoombackup_records', $record, 'id'); + $db->insertObject('#__mokosuitebackup_records', $record, 'id'); $recordId = $record->id; try { @@ -161,10 +161,20 @@ class BackupEngine foreach ($filesToBackup as $relativePath) { $fullPath = JPATH_ROOT . '/' . $relativePath; - if (is_file($fullPath) && is_readable($fullPath)) { - $archiver->addFile($fullPath, $relativePath); - } else { + if (!is_file($fullPath) || !is_readable($fullPath)) { $skippedFiles++; + continue; + } + + // Store configuration.php as .bak with credentials stripped. + // The restore process rebuilds a fresh configuration.php + // from user input + non-sensitive values from the .bak. + if ($relativePath === 'configuration.php') { + $sanitized = self::sanitizeConfiguration($fullPath); + $archiver->addFromString('configuration.php.bak', $sanitized); + $this->log('configuration.php saved as .bak (credentials stripped)'); + } else { + $archiver->addFile($fullPath, $relativePath); } } @@ -213,11 +223,16 @@ class BackupEngine MokoRestore::wrap($archivePath, $mokoRestorePath); // Replace the original archive with the wrapped one - @unlink($archivePath); + if (is_file($archivePath) && !unlink($archivePath)) { + $this->log('WARNING: Could not remove pre-wrap archive'); + } rename($mokoRestorePath, $archivePath); $totalSize = filesize($archivePath); $sizeHuman = number_format($totalSize / 1048576, 2) . ' MB'; + // Recompute checksum for the final wrapped archive + $checksum = hash_file('sha256', $archivePath); $this->log('MokoRestore archive created: ' . $sizeHuman); + $this->log('SHA-256 (wrapped): ' . $checksum); } $remoteFilename = ''; @@ -249,7 +264,7 @@ class BackupEngine $logContent = implode("\n", $this->log); $logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath); if (@file_put_contents($logPath, $logContent) === false) { - error_log('MokoJoomBackup: Could not write log file: ' . $logPath); + error_log('MokoSuiteBackup: Could not write log file: ' . $logPath); } // Final record update @@ -268,7 +283,7 @@ class BackupEngine 'log' => $logContent, ]; - $db->updateObject('#__mokojoombackup_records', $update, 'id'); + $db->updateObject('#__mokosuitebackup_records', $update, 'id'); // Send success notification NotificationSender::send($profile, $update, true, implode("\n", $this->log)); @@ -296,7 +311,7 @@ class BackupEngine 'log' => implode("\n", $this->log), ]; - $db->updateObject('#__mokojoombackup_records', $update, 'id'); + $db->updateObject('#__mokosuitebackup_records', $update, 'id'); // Send failure notification NotificationSender::send($profile, $update, false, implode("\n", $this->log)); @@ -416,7 +431,7 @@ class BackupEngine { $query = $db->getQuery(true) ->select($db->quoteName('manifest')) - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('profile_id') . ' = ' . $profileId) ->where($db->quoteName('status') . ' = ' . $db->quote('complete')) ->where($db->quoteName('manifest') . ' != ' . $db->quote('')) @@ -472,14 +487,14 @@ class BackupEngine } /** - * Dispatch the onMokoJoomBackupAfterRun event so plugins (actionlog, etc.) can react. + * Dispatch the onMokoSuiteBackupAfterRun event so plugins (actionlog, etc.) can react. */ private function dispatchAfterRun(bool $success, int $recordId, string $description, int $profileId, string $origin): void { try { $app = Factory::getApplication(); - $event = new Event('onMokoJoomBackupAfterRun', [ + $event = new Event('onMokoSuiteBackupAfterRun', [ 'success' => $success, 'record_id' => $recordId, 'description' => $description, @@ -487,13 +502,67 @@ class BackupEngine 'origin' => $origin, ]); - $app->getDispatcher()->dispatch('onMokoJoomBackupAfterRun', $event); + $app->getDispatcher()->dispatch('onMokoSuiteBackupAfterRun', $event); } catch (\Throwable $e) { // Never let a listener failure break the backup result, but log it - error_log('MokoJoomBackup: onAfterRun listener error: ' . $e->getMessage()); + error_log('MokoSuiteBackup: onAfterRun listener error: ' . $e->getMessage()); } } + /** + * Sanitize configuration.php by replacing sensitive field values with + * [SANITIZED:fieldname] placeholders. Non-sensitive fields (sitename, + * debug, cache, SEF, etc.) are preserved as-is. + * + * @param string $path Absolute path to configuration.php + * + * @return string Sanitized file contents + */ + public static function sanitizeConfiguration(string $path): string + { + $content = file_get_contents($path); + + if ($content === false) { + error_log('MokoSuiteBackup: sanitizeConfiguration() failed to read: ' . $path); + + return ''; + } + + // Fields whose values must be replaced with placeholders. + // Grouped by category for maintainability. + $sensitiveFields = [ + // Database + 'host', 'user', 'password', 'db', + // Security + 'secret', + // SMTP + 'smtpuser', 'smtppass', 'smtphost', + // Proxy + 'proxy_user', 'proxy_pass', + // Redis + 'redis_server_auth', 'session_redis_server_auth', + // Database TLS + 'dbsslkey', 'dbsslcert', 'dbsslca', + ]; + + foreach ($sensitiveFields as $field) { + // Match: public $field = 'value'; (single-quoted) + $content = preg_replace( + '/^(\s*public\s+\$' . preg_quote($field, '/') . '\s*=\s*\').*?(\';)/m', + '$1[SANITIZED:' . $field . ']$2', + $content + ); + // Match: public $field = "value"; (double-quoted) + $content = preg_replace( + '/^(\s*public\s+\$' . preg_quote($field, '/') . '\s*=\s*").*?("\s*;)/m', + '$1[SANITIZED:' . $field . ']$2', + $content + ); + } + + return $content; + } + private function log(string $message): void { $this->log[] = '[' . date('H:i:s') . '] ' . $message; diff --git a/source/packages/com_mokojoombackup/src/Engine/DatabaseDumper.php b/source/packages/com_mokosuitebackup/src/Engine/DatabaseDumper.php similarity index 96% rename from source/packages/com_mokojoombackup/src/Engine/DatabaseDumper.php rename to source/packages/com_mokosuitebackup/src/Engine/DatabaseDumper.php index f4bf538..bf49ec7 100644 --- a/source/packages/com_mokojoombackup/src/Engine/DatabaseDumper.php +++ b/source/packages/com_mokosuitebackup/src/Engine/DatabaseDumper.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; @@ -56,7 +56,7 @@ class DatabaseDumper $prefix = $db->getPrefix(); $output = []; - $output[] = '-- MokoJoomBackup Database Dump'; + $output[] = '-- MokoSuiteBackup Database Dump'; $output[] = '-- Generated: ' . date('Y-m-d H:i:s'); $output[] = '-- Server: ' . $db->getServerType(); $output[] = '-- Database: ' . $db->getName(); diff --git a/source/packages/com_mokojoombackup/src/Engine/DatabaseImporter.php b/source/packages/com_mokosuitebackup/src/Engine/DatabaseImporter.php similarity index 91% rename from source/packages/com_mokojoombackup/src/Engine/DatabaseImporter.php rename to source/packages/com_mokosuitebackup/src/Engine/DatabaseImporter.php index 4e178de..717dce8 100644 --- a/source/packages/com_mokojoombackup/src/Engine/DatabaseImporter.php +++ b/source/packages/com_mokosuitebackup/src/Engine/DatabaseImporter.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -12,7 +12,7 @@ * and DROP TABLE before CREATE TABLE for clean restores. */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; @@ -101,7 +101,7 @@ class DatabaseImporter // Log but don't abort — some statements may fail on // different MySQL versions (e.g. charset differences) // but the overall restore should continue. - error_log('MokoJoomBackup SQL import warning: ' . $e->getMessage()); + error_log('MokoSuiteBackup SQL import warning: ' . $e->getMessage()); } } } @@ -115,7 +115,7 @@ class DatabaseImporter $db->execute(); $statementsExecuted++; } catch (\Exception $e) { - error_log('MokoJoomBackup SQL import warning (final): ' . $e->getMessage()); + error_log('MokoSuiteBackup SQL import warning (final): ' . $e->getMessage()); } } } finally { diff --git a/source/packages/com_mokojoombackup/src/Engine/DifferentialScanner.php b/source/packages/com_mokosuitebackup/src/Engine/DifferentialScanner.php similarity index 94% rename from source/packages/com_mokojoombackup/src/Engine/DifferentialScanner.php rename to source/packages/com_mokosuitebackup/src/Engine/DifferentialScanner.php index 0996b9b..0409b17 100644 --- a/source/packages/com_mokojoombackup/src/Engine/DifferentialScanner.php +++ b/source/packages/com_mokosuitebackup/src/Engine/DifferentialScanner.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -15,7 +15,7 @@ * {"path/to/file": {"size": 1234, "mtime": 1717350000}, ...} */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/src/Engine/FileRestorer.php b/source/packages/com_mokosuitebackup/src/Engine/FileRestorer.php similarity index 92% rename from source/packages/com_mokojoombackup/src/Engine/FileRestorer.php rename to source/packages/com_mokosuitebackup/src/Engine/FileRestorer.php index 0bad937..5ffbbc3 100644 --- a/source/packages/com_mokojoombackup/src/Engine/FileRestorer.php +++ b/source/packages/com_mokosuitebackup/src/Engine/FileRestorer.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -11,7 +11,7 @@ * Skips database.sql and sensitive files that should not be overwritten. */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; @@ -22,10 +22,11 @@ class FileRestorer /** * Files that should never be overwritten during restore. - * configuration.php is handled separately by the RestoreEngine. + * configuration.php is rebuilt from .bak + user input by RestoreEngine. */ private const SKIP_FILES = [ 'configuration.php', + 'configuration.php.bak', '.htaccess', 'web.config', ]; diff --git a/source/packages/com_mokojoombackup/src/Engine/FileScanner.php b/source/packages/com_mokosuitebackup/src/Engine/FileScanner.php similarity index 94% rename from source/packages/com_mokojoombackup/src/Engine/FileScanner.php rename to source/packages/com_mokosuitebackup/src/Engine/FileScanner.php index f64884a..9ff7526 100644 --- a/source/packages/com_mokojoombackup/src/Engine/FileScanner.php +++ b/source/packages/com_mokosuitebackup/src/Engine/FileScanner.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/src/Engine/FtpUploader.php b/source/packages/com_mokosuitebackup/src/Engine/FtpUploader.php similarity index 96% rename from source/packages/com_mokojoombackup/src/Engine/FtpUploader.php rename to source/packages/com_mokosuitebackup/src/Engine/FtpUploader.php index 9d585b3..465f034 100644 --- a/source/packages/com_mokojoombackup/src/Engine/FtpUploader.php +++ b/source/packages/com_mokosuitebackup/src/Engine/FtpUploader.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/src/Engine/GoogleDriveUploader.php b/source/packages/com_mokosuitebackup/src/Engine/GoogleDriveUploader.php similarity index 98% rename from source/packages/com_mokojoombackup/src/Engine/GoogleDriveUploader.php rename to source/packages/com_mokosuitebackup/src/Engine/GoogleDriveUploader.php index 3cf9c80..7d1d83d 100644 --- a/source/packages/com_mokojoombackup/src/Engine/GoogleDriveUploader.php +++ b/source/packages/com_mokosuitebackup/src/Engine/GoogleDriveUploader.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -12,7 +12,7 @@ * No SDK dependency — pure PHP with cURL. */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/src/Engine/JpaUnarchiver.php b/source/packages/com_mokosuitebackup/src/Engine/JpaUnarchiver.php similarity index 97% rename from source/packages/com_mokojoombackup/src/Engine/JpaUnarchiver.php rename to source/packages/com_mokosuitebackup/src/Engine/JpaUnarchiver.php index 768cff8..0bb1aae 100644 --- a/source/packages/com_mokojoombackup/src/Engine/JpaUnarchiver.php +++ b/source/packages/com_mokosuitebackup/src/Engine/JpaUnarchiver.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -19,7 +19,7 @@ * The RestoreEngine can then restore from the extracted files. */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; diff --git a/source/packages/com_mokojoombackup/src/Engine/MokoRestore.php b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php similarity index 76% rename from source/packages/com_mokojoombackup/src/Engine/MokoRestore.php rename to source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php index 4781699..30f1944 100644 --- a/source/packages/com_mokojoombackup/src/Engine/MokoRestore.php +++ b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -21,7 +21,7 @@ * with a Joomla-styled wizard interface. */ -namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; +namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; @@ -89,7 +89,7 @@ class MokoRestore * * DELETE THIS FILE AFTER INSTALLATION IS COMPLETE. * - * @package MokoJoomBackup + * @package MokoSuiteBackup * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GPL-3.0-or-later */ @@ -258,31 +258,34 @@ function actionExtract(array $data): array $count = $zip->numFiles; $zip->close(); - // Try to read existing configuration.php for pre-filling + // Pre-fill from configuration.php.bak (sanitized backup) or + // configuration.php (legacy/unsanitized backup). Skip [SANITIZED:] values. $existingConfig = []; - $configFile = RESTORE_DIR . '/configuration.php'; + $configFile = RESTORE_DIR . '/configuration.php.bak'; + + if (!is_file($configFile)) { + $configFile = RESTORE_DIR . '/configuration.php'; + } if (is_file($configFile)) { $content = file_get_contents($configFile); - if (preg_match('/\$host\s*=\s*\'([^\']*)\'/', $content, $m)) { - $existingConfig['db_host'] = $m[1]; - } + $fieldMap = [ + 'host' => 'db_host', + 'db' => 'db_name', + 'user' => 'db_user', + 'dbprefix' => 'db_prefix', + 'sitename' => 'sitename', + 'smtphost' => 'smtp_host', + 'smtpuser' => 'smtp_user', + ]; - if (preg_match('/\$db\s*=\s*\'([^\']*)\'/', $content, $m)) { - $existingConfig['db_name'] = $m[1]; - } - - if (preg_match('/\$user\s*=\s*\'([^\']*)\'/', $content, $m)) { - $existingConfig['db_user'] = $m[1]; - } - - if (preg_match('/\$dbprefix\s*=\s*\'([^\']*)\'/', $content, $m)) { - $existingConfig['db_prefix'] = $m[1]; - } - - if (preg_match('/\$sitename\s*=\s*\'([^\']*)\'/', $content, $m)) { - $existingConfig['sitename'] = $m[1]; + foreach ($fieldMap as $phpField => $configKey) { + if (preg_match('/\$' . preg_quote($phpField, '/') . '\s*=\s*\'([^\']*)\'/', $content, $m)) { + if (strpos($m[1], '[SANITIZED:') === false) { + $existingConfig[$configKey] = $m[1]; + } + } } } @@ -390,42 +393,104 @@ function actionConfig(array $data): array $prefix = $data['db_prefix'] ?? 'moko_'; $sitename = $data['sitename'] ?? 'Joomla Site'; $livesite = $data['live_site'] ?? ''; + $smtpHost = $data['smtp_host'] ?? ''; + $smtpUser = $data['smtp_user'] ?? ''; + $smtpPass = $data['smtp_pass'] ?? ''; $tmpPath = RESTORE_DIR . '/tmp'; $logPath = RESTORE_DIR . '/administrator/logs'; - $configFile = RESTORE_DIR . '/configuration.php'; + $configPath = RESTORE_DIR . '/configuration.php'; + $bakPath = RESTORE_DIR . '/configuration.php.bak'; - if (is_file($configFile)) { - // Update existing configuration.php - $config = file_get_contents($configFile); + // Use .bak as the base template (preserves non-sensitive settings like + // debug, cache, SEF, editor, etc.). Fall back to existing config + // for legacy/unsanitized backups, or build from scratch if neither exists. + $basePath = is_file($bakPath) ? $bakPath : (is_file($configPath) ? $configPath : null); + + if ($basePath !== null) { + $config = file_get_contents($basePath); + + // Replace all credential and server-specific fields with user input + // Escape all user input for safe interpolation into PHP string literals + $eHost = addcslashes($host, "'\\"); + $eDbName = addcslashes($dbName, "'\\"); + $eDbUser = addcslashes($dbUser, "'\\"); + $eDbPass = addcslashes($dbPass, "'\\"); + $ePrefix = addcslashes($prefix, "'\\"); + $eSite = addcslashes($sitename, "'\\"); + $eLive = addcslashes($livesite, "'\\"); + $eSmtpH = addcslashes($smtpHost, "'\\"); + $eSmtpU = addcslashes($smtpUser, "'\\"); + $eSmtpP = addcslashes($smtpPass, "'\\"); $replacements = [ - '/\$host\s*=\s*\'[^\']*\'/' => "\$host = '{$host}'", - '/\$db\s*=\s*\'[^\']*\'/' => "\$db = '{$dbName}'", - '/\$user\s*=\s*\'[^\']*\'/' => "\$user = '{$dbUser}'", - '/\$password\s*=\s*\'[^\']*\'/' => "\$password = '" . addcslashes($dbPass, "'\\") . "'", - '/\$dbprefix\s*=\s*\'[^\']*\'/' => "\$dbprefix = '{$prefix}'", + '/\$host\s*=\s*\'[^\']*\'/' => "\$host = '{$eHost}'", + '/\$db\s*=\s*\'[^\']*\'/' => "\$db = '{$eDbName}'", + '/\$user\s*=\s*\'[^\']*\'/' => "\$user = '{$eDbUser}'", + '/\$password\s*=\s*\'[^\']*\'/' => "\$password = '{$eDbPass}'", + '/\$dbprefix\s*=\s*\'[^\']*\'/' => "\$dbprefix = '{$ePrefix}'", '/\$tmp_path\s*=\s*\'[^\']*\'/' => "\$tmp_path = '{$tmpPath}'", '/\$log_path\s*=\s*\'[^\']*\'/' => "\$log_path = '{$logPath}'", - '/\$sitename\s*=\s*\'[^\']*\'/' => "\$sitename = '" . addcslashes($sitename, "'\\") . "'", + '/\$sitename\s*=\s*\'[^\']*\'/' => "\$sitename = '{$eSite}'", '/\$secret\s*=\s*\'[^\']*\'/' => "\$secret = '" . bin2hex(random_bytes(16)) . "'", ]; if ($livesite !== '') { - $replacements['/\$live_site\s*=\s*\'[^\']*\'/'] = "\$live_site = '{$livesite}'"; + $replacements['/\$live_site\s*=\s*\'[^\']*\'/'] = "\$live_site = '{$eLive}'"; } + // SMTP — always replace (clears sanitized placeholders even if blank) + $replacements['/\$smtphost\s*=\s*\'[^\']*\'/'] = "\$smtphost = '{$eSmtpH}'"; + $replacements['/\$smtpuser\s*=\s*\'[^\']*\'/'] = "\$smtpuser = '{$eSmtpU}'"; + $replacements['/\$smtppass\s*=\s*\'[^\']*\'/'] = "\$smtppass = '{$eSmtpP}'"; + + // Clear remaining sanitized placeholders (proxy, Redis, DB TLS) + $replacements['/\$proxy_user\s*=\s*\'[^\']*\'/'] = "\$proxy_user = ''"; + $replacements['/\$proxy_pass\s*=\s*\'[^\']*\'/'] = "\$proxy_pass = ''"; + $replacements['/\$redis_server_auth\s*=\s*\'[^\']*\'/'] = "\$redis_server_auth = ''"; + $replacements['/\$session_redis_server_auth\s*=\s*\'[^\']*\'/'] = "\$session_redis_server_auth = ''"; + $replacements['/\$dbsslkey\s*=\s*\'[^\']*\'/'] = "\$dbsslkey = ''"; + $replacements['/\$dbsslcert\s*=\s*\'[^\']*\'/'] = "\$dbsslcert = ''"; + $replacements['/\$dbsslca\s*=\s*\'[^\']*\'/'] = "\$dbsslca = ''"; + foreach ($replacements as $pattern => $replacement) { $config = preg_replace($pattern, $replacement, $config); } - file_put_contents($configFile, $config); + if (file_put_contents($configPath, $config) === false) { + return ['success' => false, 'message' => 'Failed to write Joomla config file — check directory permissions']; + } - return ['success' => true, 'message' => 'configuration.php updated with new settings and fresh secret']; + // Remove .bak after successful rebuild + if (is_file($bakPath)) { + @unlink($bakPath); + } + + // Reset .htaccess to Joomla defaults if requested + $htWarn = ''; + + if (($data['reset_htaccess'] ?? '0') === '1') { + $htWarn = writeDefaultHtaccess(RESTORE_DIR); + } + + $msg = 'Joomla configuration rebuilt with fresh credentials and secret'; + + if ($htWarn !== '') { + $msg .= ' (Warning: ' . $htWarn . ')'; + } + + return ['success' => true, 'message' => $msg]; } - // Create new configuration.php from scratch - $secret = bin2hex(random_bytes(16)); + // Create new configuration.php from scratch — use escaped values + $eHost = addcslashes($host, "'\\"); + $eDbName = addcslashes($dbName, "'\\"); + $eDbUser = addcslashes($dbUser, "'\\"); + $eDbPass = addcslashes($dbPass, "'\\"); + $ePrefix = addcslashes($prefix, "'\\"); + $eSite = addcslashes($sitename, "'\\"); + $eLive = addcslashes($livesite, "'\\"); + $secret = bin2hex(random_bytes(16)); $newConfig = << false, 'message' => 'Failed to write Joomla config file — check directory permissions']; + } // Ensure directories exist @mkdir($tmpPath, 0755, true); @mkdir($logPath, 0755, true); - return ['success' => true, 'message' => 'configuration.php created from scratch with fresh secret']; + // Reset .htaccess to Joomla defaults if requested + $htWarn = ''; + + if (($data['reset_htaccess'] ?? '0') === '1') { + $htWarn = writeDefaultHtaccess(RESTORE_DIR); + } + + $msg = 'Joomla configuration created from scratch with fresh secret'; + + if ($htWarn !== '') { + $msg .= ' (Warning: ' . $htWarn . ')'; + } + + return ['success' => true, 'message' => $msg]; +} + +/** + * Write a clean Joomla default .htaccess file. + * Backs up the existing one as .htaccess.bak first. + */ +function writeDefaultHtaccess(string $siteRoot): string +{ + $htaccess = $siteRoot . '/.htaccess'; + + // Backup existing .htaccess before overwriting + if (is_file($htaccess)) { + if (!copy($htaccess, $htaccess . '.bak')) { + return 'Could not back up existing .htaccess — reset skipped for safety'; + } + } + + $default = <<<'HTACCESS' +## +# @package Joomla +# @copyright (C) 2005 Open Source Matters, Inc. +# @license GNU General Public License version 2 or later; see LICENSE.txt +## + +## +# READ THIS COMPLETELY IF YOU CHOOSE TO USE THIS FILE! +# +# The line 'Options +FollowSymLinks' may cause problems with some server +# configurations. It is required for the use of Apache mod_rewrite, but +# it may have already been set by your server administrator in a way that +# disallows changing it in this .htaccess file. If using it causes your +# server to report an error, comment it out, reload your site in your +# browser and test your SEF URLs. If they work, then it has been set by +# your server administrator and you do not need to set it here. +## + +## No directory listings + + IndexIgnore * + + +## Suppress mime type detection in browsers for unknown types + + Header always set X-Content-Type-Options "nosniff" + + +## Can be commented out if causes errors, see notes above. +Options +FollowSymLinks +Options -Indexes + +## Disable inline JavaScript when directly opening SVG files or embedding them with the object-tag + + + Header always set Content-Security-Policy "script-src 'none'" + + + +## Mod_rewrite in use. +RewriteEngine On + +## Begin - Rewrite rules to block out some common exploits. +# If you experience problems on your site then comment out the operations listed +# below by adding a # to the beginning of the line. +# This attempts to block the most common type of exploit `attempts` on Joomla! +# +# Block any script trying to base64_encode data within the URL. +RewriteCond %{QUERY_STRING} base64_encode[^(]*\([^)]*\) [OR] +# Block any script that includes a diff --git a/source/packages/com_mokojoombackup/tmpl/profile/index.html b/source/packages/com_mokosuitebackup/tmpl/index.html similarity index 100% rename from source/packages/com_mokojoombackup/tmpl/profile/index.html rename to source/packages/com_mokosuitebackup/tmpl/index.html diff --git a/source/packages/com_mokojoombackup/tmpl/profile/edit.php b/source/packages/com_mokosuitebackup/tmpl/profile/edit.php similarity index 92% rename from source/packages/com_mokojoombackup/tmpl/profile/edit.php rename to source/packages/com_mokosuitebackup/tmpl/profile/edit.php index f2ea845..ce77be9 100644 --- a/source/packages/com_mokojoombackup/tmpl/profile/edit.php +++ b/source/packages/com_mokosuitebackup/tmpl/profile/edit.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Router\Route; HTMLHelper::_('behavior.formvalidator'); HTMLHelper::_('behavior.keepalive'); ?> -
diff --git a/source/packages/com_mokojoombackup/tmpl/profiles/index.html b/source/packages/com_mokosuitebackup/tmpl/profile/index.html similarity index 100% rename from source/packages/com_mokojoombackup/tmpl/profiles/index.html rename to source/packages/com_mokosuitebackup/tmpl/profile/index.html diff --git a/source/packages/com_mokojoombackup/tmpl/profiles/default.php b/source/packages/com_mokosuitebackup/tmpl/profiles/default.php similarity index 92% rename from source/packages/com_mokojoombackup/tmpl/profiles/default.php rename to source/packages/com_mokosuitebackup/tmpl/profiles/default.php index 9eb5d13..7d048f1 100644 --- a/source/packages/com_mokojoombackup/tmpl/profiles/default.php +++ b/source/packages/com_mokosuitebackup/tmpl/profiles/default.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -20,7 +20,7 @@ HTMLHelper::_('behavior.multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?> - +
@@ -60,7 +60,7 @@ $listDirn = $this->escape($this->state->get('list.direction')); id); ?> - + escape($item->title); ?> description)) : ?> diff --git a/source/packages/plg_quickicon_mokojoombackup/index.html b/source/packages/com_mokosuitebackup/tmpl/profiles/index.html similarity index 100% rename from source/packages/plg_quickicon_mokojoombackup/index.html rename to source/packages/com_mokosuitebackup/tmpl/profiles/index.html diff --git a/source/packages/plg_actionlog_mokojoombackup/language/en-GB/plg_actionlog_mokojoombackup.sys.ini b/source/packages/plg_actionlog_mokojoombackup/language/en-GB/plg_actionlog_mokojoombackup.sys.ini deleted file mode 100644 index 61b9070..0000000 --- a/source/packages/plg_actionlog_mokojoombackup/language/en-GB/plg_actionlog_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Actionlog Plugin system language file (en-GB) -PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoJoomBackup" -PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." diff --git a/source/packages/plg_actionlog_mokojoombackup/language/en-US/plg_actionlog_mokojoombackup.sys.ini b/source/packages/plg_actionlog_mokojoombackup/language/en-US/plg_actionlog_mokojoombackup.sys.ini deleted file mode 100644 index 26c0927..0000000 --- a/source/packages/plg_actionlog_mokojoombackup/language/en-US/plg_actionlog_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Actionlog Plugin system language file (en-US) -PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoJoomBackup" -PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." diff --git a/source/packages/plg_actionlog_mokojoombackup/language/en-GB/plg_actionlog_mokojoombackup.ini b/source/packages/plg_actionlog_mokosuitebackup/language/en-GB/plg_actionlog_mokosuitebackup.ini similarity index 74% rename from source/packages/plg_actionlog_mokojoombackup/language/en-GB/plg_actionlog_mokojoombackup.ini rename to source/packages/plg_actionlog_mokosuitebackup/language/en-GB/plg_actionlog_mokosuitebackup.ini index 75b37b6..9b0e1f9 100644 --- a/source/packages/plg_actionlog_mokojoombackup/language/en-GB/plg_actionlog_mokojoombackup.ini +++ b/source/packages/plg_actionlog_mokosuitebackup/language/en-GB/plg_actionlog_mokosuitebackup.ini @@ -1,6 +1,6 @@ -; MokoJoomBackup — Actionlog Plugin language file (en-GB) -PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoJoomBackup" -PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." +; MokoSuiteBackup — Actionlog Plugin language file (en-GB) +PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoSuiteBackup" +PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoSuiteBackup actions (backup, restore, profile changes) to User Action Logs." PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})" PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})" PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})" diff --git a/source/packages/plg_actionlog_mokosuitebackup/language/en-GB/plg_actionlog_mokosuitebackup.sys.ini b/source/packages/plg_actionlog_mokosuitebackup/language/en-GB/plg_actionlog_mokosuitebackup.sys.ini new file mode 100644 index 0000000..18d9fb4 --- /dev/null +++ b/source/packages/plg_actionlog_mokosuitebackup/language/en-GB/plg_actionlog_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Actionlog Plugin system language file (en-GB) +PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoSuiteBackup" +PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoSuiteBackup actions (backup, restore, profile changes) to User Action Logs." diff --git a/source/packages/plg_actionlog_mokojoombackup/language/en-US/plg_actionlog_mokojoombackup.ini b/source/packages/plg_actionlog_mokosuitebackup/language/en-US/plg_actionlog_mokosuitebackup.ini similarity index 74% rename from source/packages/plg_actionlog_mokojoombackup/language/en-US/plg_actionlog_mokojoombackup.ini rename to source/packages/plg_actionlog_mokosuitebackup/language/en-US/plg_actionlog_mokosuitebackup.ini index 9baa0c5..9dd2041 100644 --- a/source/packages/plg_actionlog_mokojoombackup/language/en-US/plg_actionlog_mokojoombackup.ini +++ b/source/packages/plg_actionlog_mokosuitebackup/language/en-US/plg_actionlog_mokosuitebackup.ini @@ -1,6 +1,6 @@ -; MokoJoomBackup — Actionlog Plugin language file (en-US) -PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoJoomBackup" -PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." +; MokoSuiteBackup — Actionlog Plugin language file (en-US) +PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoSuiteBackup" +PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoSuiteBackup actions (backup, restore, profile changes) to User Action Logs." PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})" PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})" PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})" diff --git a/source/packages/plg_actionlog_mokosuitebackup/language/en-US/plg_actionlog_mokosuitebackup.sys.ini b/source/packages/plg_actionlog_mokosuitebackup/language/en-US/plg_actionlog_mokosuitebackup.sys.ini new file mode 100644 index 0000000..d2327fd --- /dev/null +++ b/source/packages/plg_actionlog_mokosuitebackup/language/en-US/plg_actionlog_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Actionlog Plugin system language file (en-US) +PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoSuiteBackup" +PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoSuiteBackup actions (backup, restore, profile changes) to User Action Logs." diff --git a/source/packages/plg_system_mokojoombackup/mokojoombackup.php b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.php similarity index 76% rename from source/packages/plg_system_mokojoombackup/mokojoombackup.php rename to source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.php index b53cca7..fcdfa92 100644 --- a/source/packages/plg_system_mokojoombackup/mokojoombackup.php +++ b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_actionlog_mokojoombackup/mokojoombackup.xml b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml similarity index 63% rename from source/packages/plg_actionlog_mokojoombackup/mokojoombackup.xml rename to source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml index 7bf5acf..5b3d40a 100644 --- a/source/packages/plg_actionlog_mokojoombackup/mokojoombackup.xml +++ b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml @@ -1,14 +1,13 @@ - plg_actionlog_mokojoombackup - 01.08.00 + Action Log - MokoSuiteBackup + 01.20.00-rc 2026-06-04 Moko Consulting hello@mokoconsulting.tech @@ -17,16 +16,16 @@ GPL-3.0-or-later PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION - Joomla\Plugin\Actionlog\MokoJoomBackup + Joomla\Plugin\Actionlog\MokoSuiteBackup - mokojoombackup.php + mokosuitebackup.php services src - language/en-GB/plg_actionlog_mokojoombackup.ini - language/en-GB/plg_actionlog_mokojoombackup.sys.ini + language/en-GB/plg_actionlog_mokosuitebackup.ini + language/en-GB/plg_actionlog_mokosuitebackup.sys.ini diff --git a/source/packages/plg_actionlog_mokojoombackup/services/provider.php b/source/packages/plg_actionlog_mokosuitebackup/services/provider.php similarity index 74% rename from source/packages/plg_actionlog_mokojoombackup/services/provider.php rename to source/packages/plg_actionlog_mokosuitebackup/services/provider.php index 572a2a9..d956ff8 100644 --- a/source/packages/plg_actionlog_mokojoombackup/services/provider.php +++ b/source/packages/plg_actionlog_mokosuitebackup/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -16,7 +16,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\Actionlog\MokoJoomBackup\Extension\MokoJoomBackupActionlog; +use Joomla\Plugin\Actionlog\MokoSuiteBackup\Extension\MokoSuiteBackupActionlog; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -24,9 +24,9 @@ return new class () implements ServiceProviderInterface { $container->set( PluginInterface::class, function (Container $container) { - $plugin = new MokoJoomBackupActionlog( + $plugin = new MokoSuiteBackupActionlog( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('actionlog', 'mokojoombackup') + (array) PluginHelper::getPlugin('actionlog', 'mokosuitebackup') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_actionlog_mokojoombackup/src/Extension/MokoJoomBackupActionlog.php b/source/packages/plg_actionlog_mokosuitebackup/src/Extension/MokoSuiteBackupActionlog.php similarity index 85% rename from source/packages/plg_actionlog_mokojoombackup/src/Extension/MokoJoomBackupActionlog.php rename to source/packages/plg_actionlog_mokosuitebackup/src/Extension/MokoSuiteBackupActionlog.php index 7b02ddc..5084bb6 100644 --- a/source/packages/plg_actionlog_mokojoombackup/src/Extension/MokoJoomBackupActionlog.php +++ b/source/packages/plg_actionlog_mokosuitebackup/src/Extension/MokoSuiteBackupActionlog.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Plugin\Actionlog\MokoJoomBackup\Extension; +namespace Joomla\Plugin\Actionlog\MokoSuiteBackup\Extension; defined('_JEXEC') or die; @@ -18,7 +18,7 @@ use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; -final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInterface +final class MokoSuiteBackupActionlog extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; @@ -27,7 +27,7 @@ final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInter return [ 'onContentAfterSave' => 'onContentAfterSave', 'onContentAfterDelete' => 'onContentAfterDelete', - 'onMokoJoomBackupAfterRun' => 'onMokoJoomBackupAfterRun', + 'onMokoSuiteBackupAfterRun' => 'onMokoSuiteBackupAfterRun', ]; } @@ -38,7 +38,7 @@ final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInter { [$context, $table, $isNew] = array_values($event->getArguments()); - if ($context !== 'com_mokojoombackup.profile') { + if ($context !== 'com_mokosuitebackup.profile') { return; } @@ -55,7 +55,7 @@ final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInter 'username' => $this->getCurrentUserName(), ], $messageKey, - 'com_mokojoombackup.profile', + 'com_mokosuitebackup.profile', $this->getCurrentUserId() ); } @@ -67,7 +67,7 @@ final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInter { [$context, $table] = array_values($event->getArguments()); - if ($context === 'com_mokojoombackup.profile') { + if ($context === 'com_mokosuitebackup.profile') { $this->addLog( [ 'PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_DELETED', @@ -77,10 +77,10 @@ final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInter 'username' => $this->getCurrentUserName(), ], 'PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_DELETED', - 'com_mokojoombackup.profile', + 'com_mokosuitebackup.profile', $this->getCurrentUserId() ); - } elseif ($context === 'com_mokojoombackup.backup') { + } elseif ($context === 'com_mokosuitebackup.backup') { $this->addLog( [ 'PLG_ACTIONLOG_MOKOJOOMBACKUP_RECORD_DELETED', @@ -90,7 +90,7 @@ final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInter 'username' => $this->getCurrentUserName(), ], 'PLG_ACTIONLOG_MOKOJOOMBACKUP_RECORD_DELETED', - 'com_mokojoombackup.backup', + 'com_mokosuitebackup.backup', $this->getCurrentUserId() ); } @@ -100,7 +100,7 @@ final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInter * Log when a backup completes or fails. * This event should be dispatched from BackupEngine. */ - public function onMokoJoomBackupAfterRun(Event $event): void + public function onMokoSuiteBackupAfterRun(Event $event): void { $args = $event->getArguments(); @@ -125,7 +125,7 @@ final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInter 'username' => $this->getCurrentUserName(), ], $messageKey, - 'com_mokojoombackup.backup', + 'com_mokosuitebackup.backup', $this->getCurrentUserId() ); } @@ -139,7 +139,7 @@ final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInter 'message_language_key' => $messageLanguageKey, 'message' => json_encode($message), 'date' => date('Y-m-d H:i:s'), - 'extension' => 'com_mokojoombackup', + 'extension' => 'com_mokosuitebackup', 'user_id' => $userId, 'ip_address' => Factory::getApplication()->input->server->getString('REMOTE_ADDR', ''), 'item_id' => $message['id'] ?? 0, diff --git a/source/packages/plg_console_mokojoombackup/language/en-GB/plg_console_mokojoombackup.ini b/source/packages/plg_console_mokojoombackup/language/en-GB/plg_console_mokojoombackup.ini deleted file mode 100644 index 311e8d5..0000000 --- a/source/packages/plg_console_mokojoombackup/language/en-GB/plg_console_mokojoombackup.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Console Plugin language file (en-GB) -PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoJoomBackup" -PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/source/packages/plg_console_mokojoombackup/language/en-GB/plg_console_mokojoombackup.sys.ini b/source/packages/plg_console_mokojoombackup/language/en-GB/plg_console_mokojoombackup.sys.ini deleted file mode 100644 index f56b212..0000000 --- a/source/packages/plg_console_mokojoombackup/language/en-GB/plg_console_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Console Plugin system language file (en-GB) -PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoJoomBackup" -PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/source/packages/plg_console_mokojoombackup/language/en-US/plg_console_mokojoombackup.ini b/source/packages/plg_console_mokojoombackup/language/en-US/plg_console_mokojoombackup.ini deleted file mode 100644 index 2de8315..0000000 --- a/source/packages/plg_console_mokojoombackup/language/en-US/plg_console_mokojoombackup.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Console Plugin language file (en-US) -PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoJoomBackup" -PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/source/packages/plg_console_mokojoombackup/language/en-US/plg_console_mokojoombackup.sys.ini b/source/packages/plg_console_mokojoombackup/language/en-US/plg_console_mokojoombackup.sys.ini deleted file mode 100644 index d229b02..0000000 --- a/source/packages/plg_console_mokojoombackup/language/en-US/plg_console_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Console Plugin system language file (en-US) -PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoJoomBackup" -PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/source/packages/plg_console_mokosuitebackup/language/en-GB/plg_console_mokosuitebackup.ini b/source/packages/plg_console_mokosuitebackup/language/en-GB/plg_console_mokosuitebackup.ini new file mode 100644 index 0000000..7c7943c --- /dev/null +++ b/source/packages/plg_console_mokosuitebackup/language/en-GB/plg_console_mokosuitebackup.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Console Plugin language file (en-GB) +PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoSuiteBackup" +PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoSuiteBackup: run, list, profiles, restore, cleanup." diff --git a/source/packages/plg_console_mokosuitebackup/language/en-GB/plg_console_mokosuitebackup.sys.ini b/source/packages/plg_console_mokosuitebackup/language/en-GB/plg_console_mokosuitebackup.sys.ini new file mode 100644 index 0000000..bf02aca --- /dev/null +++ b/source/packages/plg_console_mokosuitebackup/language/en-GB/plg_console_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Console Plugin system language file (en-GB) +PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoSuiteBackup" +PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoSuiteBackup: run, list, profiles, restore, cleanup." diff --git a/source/packages/plg_console_mokosuitebackup/language/en-US/plg_console_mokosuitebackup.ini b/source/packages/plg_console_mokosuitebackup/language/en-US/plg_console_mokosuitebackup.ini new file mode 100644 index 0000000..aca5ed8 --- /dev/null +++ b/source/packages/plg_console_mokosuitebackup/language/en-US/plg_console_mokosuitebackup.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Console Plugin language file (en-US) +PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoSuiteBackup" +PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoSuiteBackup: run, list, profiles, restore, cleanup." diff --git a/source/packages/plg_console_mokosuitebackup/language/en-US/plg_console_mokosuitebackup.sys.ini b/source/packages/plg_console_mokosuitebackup/language/en-US/plg_console_mokosuitebackup.sys.ini new file mode 100644 index 0000000..f053ee7 --- /dev/null +++ b/source/packages/plg_console_mokosuitebackup/language/en-US/plg_console_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Console Plugin system language file (en-US) +PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoSuiteBackup" +PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoSuiteBackup: run, list, profiles, restore, cleanup." diff --git a/source/packages/plg_actionlog_mokojoombackup/mokojoombackup.php b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.php similarity index 76% rename from source/packages/plg_actionlog_mokojoombackup/mokojoombackup.php rename to source/packages/plg_console_mokosuitebackup/mokosuitebackup.php index 13e2b3b..deda959 100644 --- a/source/packages/plg_actionlog_mokojoombackup/mokojoombackup.php +++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_console_mokojoombackup/mokojoombackup.xml b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml similarity index 63% rename from source/packages/plg_console_mokojoombackup/mokojoombackup.xml rename to source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml index cac6df2..f08c514 100644 --- a/source/packages/plg_console_mokojoombackup/mokojoombackup.xml +++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml @@ -1,14 +1,13 @@ - plg_console_mokojoombackup - 01.08.00 + Console - MokoSuiteBackup + 01.20.00-rc 2026-06-04 Moko Consulting hello@mokoconsulting.tech @@ -17,16 +16,16 @@ GPL-3.0-or-later PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION - Joomla\Plugin\Console\MokoJoomBackup + Joomla\Plugin\Console\MokoSuiteBackup - mokojoombackup.php + mokosuitebackup.php services src - language/en-GB/plg_console_mokojoombackup.ini - language/en-GB/plg_console_mokojoombackup.sys.ini + language/en-GB/plg_console_mokosuitebackup.ini + language/en-GB/plg_console_mokosuitebackup.sys.ini diff --git a/source/packages/plg_console_mokojoombackup/services/provider.php b/source/packages/plg_console_mokosuitebackup/services/provider.php similarity index 75% rename from source/packages/plg_console_mokojoombackup/services/provider.php rename to source/packages/plg_console_mokosuitebackup/services/provider.php index e80cfa1..977c537 100644 --- a/source/packages/plg_console_mokojoombackup/services/provider.php +++ b/source/packages/plg_console_mokosuitebackup/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -16,7 +16,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\Console\MokoJoomBackup\Extension\MokoJoomBackupConsole; +use Joomla\Plugin\Console\MokoSuiteBackup\Extension\MokoSuiteBackupConsole; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -24,9 +24,9 @@ return new class () implements ServiceProviderInterface { $container->set( PluginInterface::class, function (Container $container) { - $plugin = new MokoJoomBackupConsole( + $plugin = new MokoSuiteBackupConsole( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('console', 'mokojoombackup') + (array) PluginHelper::getPlugin('console', 'mokosuitebackup') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_console_mokojoombackup/src/Command/CleanupCommand.php b/source/packages/plg_console_mokosuitebackup/src/Command/CleanupCommand.php similarity index 86% rename from source/packages/plg_console_mokojoombackup/src/Command/CleanupCommand.php rename to source/packages/plg_console_mokosuitebackup/src/Command/CleanupCommand.php index 68615b1..d3a67ff 100644 --- a/source/packages/plg_console_mokojoombackup/src/Command/CleanupCommand.php +++ b/source/packages/plg_console_mokosuitebackup/src/Command/CleanupCommand.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Plugin\Console\MokoJoomBackup\Command; +namespace Joomla\Plugin\Console\MokoSuiteBackup\Command; defined('_JEXEC') or die; @@ -21,7 +21,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; class CleanupCommand extends AbstractCommand { - protected static $defaultName = 'mokojoombackup:cleanup'; + protected static $defaultName = 'mokosuitebackup:cleanup'; protected function configure(): void { @@ -38,7 +38,7 @@ class CleanupCommand extends AbstractCommand $maxCount = (int) $input->getOption('max-count'); $dryRun = $input->getOption('dry-run'); - $io->title('MokoJoomBackup — Cleanup'); + $io->title('MokoSuiteBackup — Cleanup'); if ($dryRun) { $io->note('Dry run — no files will be deleted.'); @@ -51,7 +51,7 @@ class CleanupCommand extends AbstractCommand $cutoff = date('Y-m-d H:i:s', strtotime("-{$maxAge} days")); $query = $db->getQuery(true) ->select('id, absolute_path, description, backupstart') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)) ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); $db->setQuery($query); @@ -67,7 +67,7 @@ class CleanupCommand extends AbstractCommand $db->setQuery( $db->getQuery(true) - ->delete($db->quoteName('#__mokojoombackup_records')) + ->delete($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('id') . ' = ' . (int) $record->id) ); $db->execute(); @@ -79,7 +79,7 @@ class CleanupCommand extends AbstractCommand // Enforce max count $query = $db->getQuery(true) ->select('COUNT(*)') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); $db->setQuery($query); $totalCount = (int) $db->loadResult(); @@ -88,7 +88,7 @@ class CleanupCommand extends AbstractCommand $excess = $totalCount - $maxCount; $query = $db->getQuery(true) ->select('id, absolute_path, description, backupstart') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('status') . ' = ' . $db->quote('complete')) ->order($db->quoteName('backupstart') . ' ASC'); $db->setQuery($query, 0, $excess); @@ -104,7 +104,7 @@ class CleanupCommand extends AbstractCommand $db->setQuery( $db->getQuery(true) - ->delete($db->quoteName('#__mokojoombackup_records')) + ->delete($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('id') . ' = ' . (int) $record->id) ); $db->execute(); diff --git a/source/packages/plg_console_mokojoombackup/src/Command/ListCommand.php b/source/packages/plg_console_mokosuitebackup/src/Command/ListCommand.php similarity index 83% rename from source/packages/plg_console_mokojoombackup/src/Command/ListCommand.php rename to source/packages/plg_console_mokosuitebackup/src/Command/ListCommand.php index b8aebe7..9a730ea 100644 --- a/source/packages/plg_console_mokojoombackup/src/Command/ListCommand.php +++ b/source/packages/plg_console_mokosuitebackup/src/Command/ListCommand.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Plugin\Console\MokoJoomBackup\Command; +namespace Joomla\Plugin\Console\MokoSuiteBackup\Command; defined('_JEXEC') or die; @@ -21,7 +21,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; class ListCommand extends AbstractCommand { - protected static $defaultName = 'mokojoombackup:list'; + protected static $defaultName = 'mokosuitebackup:list'; protected function configure(): void { @@ -36,14 +36,14 @@ class ListCommand extends AbstractCommand $limit = (int) $input->getOption('limit'); $status = $input->getOption('status'); - $io->title('MokoJoomBackup — Backup Records'); + $io->title('MokoSuiteBackup — Backup Records'); $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('r.id, r.description, r.status, r.origin, r.backup_type, r.total_size, r.backupstart, r.backupend') ->select($db->quoteName('p.title', 'profile_title')) - ->from($db->quoteName('#__mokojoombackup_records', 'r')) - ->join('LEFT', $db->quoteName('#__mokojoombackup_profiles', 'p') . ' ON p.id = r.profile_id') + ->from($db->quoteName('#__mokosuitebackup_records', 'r')) + ->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id') ->order($db->quoteName('r.backupstart') . ' DESC'); if ($status) { diff --git a/source/packages/plg_console_mokojoombackup/src/Command/ProfilesCommand.php b/source/packages/plg_console_mokosuitebackup/src/Command/ProfilesCommand.php similarity index 81% rename from source/packages/plg_console_mokojoombackup/src/Command/ProfilesCommand.php rename to source/packages/plg_console_mokosuitebackup/src/Command/ProfilesCommand.php index 22e8723..3999a4d 100644 --- a/source/packages/plg_console_mokojoombackup/src/Command/ProfilesCommand.php +++ b/source/packages/plg_console_mokosuitebackup/src/Command/ProfilesCommand.php @@ -1,14 +1,14 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Plugin\Console\MokoJoomBackup\Command; +namespace Joomla\Plugin\Console\MokoSuiteBackup\Command; defined('_JEXEC') or die; @@ -20,7 +20,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; class ProfilesCommand extends AbstractCommand { - protected static $defaultName = 'mokojoombackup:profiles'; + protected static $defaultName = 'mokosuitebackup:profiles'; protected function configure(): void { @@ -31,12 +31,12 @@ class ProfilesCommand extends AbstractCommand { $io = new SymfonyStyle($input, $output); - $io->title('MokoJoomBackup — Backup Profiles'); + $io->title('MokoSuiteBackup — Backup Profiles'); $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('id, title, backup_type, published, ordering') - ->from($db->quoteName('#__mokojoombackup_profiles')) + ->from($db->quoteName('#__mokosuitebackup_profiles')) ->order($db->quoteName('ordering') . ' ASC'); $db->setQuery($query); $profiles = $db->loadObjectList(); diff --git a/source/packages/plg_console_mokojoombackup/src/Command/RestoreCommand.php b/source/packages/plg_console_mokosuitebackup/src/Command/RestoreCommand.php similarity index 82% rename from source/packages/plg_console_mokojoombackup/src/Command/RestoreCommand.php rename to source/packages/plg_console_mokosuitebackup/src/Command/RestoreCommand.php index cccc480..7ed26d7 100644 --- a/source/packages/plg_console_mokojoombackup/src/Command/RestoreCommand.php +++ b/source/packages/plg_console_mokosuitebackup/src/Command/RestoreCommand.php @@ -1,19 +1,19 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Plugin\Console\MokoJoomBackup\Command; +namespace Joomla\Plugin\Console\MokoSuiteBackup\Command; defined('_JEXEC') or die; use Joomla\CMS\Factory; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\RestoreEngine; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RestoreEngine; use Joomla\Console\Command\AbstractCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -22,7 +22,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; class RestoreCommand extends AbstractCommand { - protected static $defaultName = 'mokojoombackup:restore'; + protected static $defaultName = 'mokosuitebackup:restore'; protected function configure(): void { @@ -35,12 +35,12 @@ class RestoreCommand extends AbstractCommand $io = new SymfonyStyle($input, $output); $recordId = (int) $input->getArgument('id'); - $io->title('MokoJoomBackup — Restore Backup'); + $io->title('MokoSuiteBackup — Restore Backup'); $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('*') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('id') . ' = ' . $recordId); $db->setQuery($query); $record = $db->loadObject(); @@ -73,7 +73,7 @@ class RestoreCommand extends AbstractCommand return 0; } - $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/src/Engine/RestoreEngine.php'; + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/RestoreEngine.php'; if (!file_exists($engineFile)) { $io->error('RestoreEngine not found. Is the component fully installed?'); diff --git a/source/packages/plg_console_mokojoombackup/src/Command/RunCommand.php b/source/packages/plg_console_mokosuitebackup/src/Command/RunCommand.php similarity index 76% rename from source/packages/plg_console_mokojoombackup/src/Command/RunCommand.php rename to source/packages/plg_console_mokosuitebackup/src/Command/RunCommand.php index 78991af..f75ea1d 100644 --- a/source/packages/plg_console_mokojoombackup/src/Command/RunCommand.php +++ b/source/packages/plg_console_mokosuitebackup/src/Command/RunCommand.php @@ -1,19 +1,19 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Plugin\Console\MokoJoomBackup\Command; +namespace Joomla\Plugin\Console\MokoSuiteBackup\Command; defined('_JEXEC') or die; use Joomla\CMS\Factory; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; use Joomla\Console\Command\AbstractCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -22,7 +22,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; class RunCommand extends AbstractCommand { - protected static $defaultName = 'mokojoombackup:run'; + protected static $defaultName = 'mokosuitebackup:run'; protected function configure(): void { @@ -37,13 +37,13 @@ class RunCommand extends AbstractCommand $profileId = (int) $input->getOption('profile'); $desc = $input->getOption('description') ?: ''; - $io->title('MokoJoomBackup — Run Backup'); + $io->title('MokoSuiteBackup — Run Backup'); $io->text('Profile ID: ' . $profileId); - $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/src/Engine/BackupEngine.php'; + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php'; if (!file_exists($engineFile)) { - $io->error('MokoJoomBackup component not installed.'); + $io->error('MokoSuiteBackup component not installed.'); return 1; } diff --git a/source/packages/plg_console_mokojoombackup/src/Extension/MokoJoomBackupConsole.php b/source/packages/plg_console_mokosuitebackup/src/Extension/MokoSuiteBackupConsole.php similarity index 61% rename from source/packages/plg_console_mokojoombackup/src/Extension/MokoJoomBackupConsole.php rename to source/packages/plg_console_mokosuitebackup/src/Extension/MokoSuiteBackupConsole.php index 2c04833..ce6eae4 100644 --- a/source/packages/plg_console_mokojoombackup/src/Extension/MokoJoomBackupConsole.php +++ b/source/packages/plg_console_mokosuitebackup/src/Extension/MokoSuiteBackupConsole.php @@ -1,27 +1,27 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Plugin\Console\MokoJoomBackup\Extension; +namespace Joomla\Plugin\Console\MokoSuiteBackup\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; -use Joomla\Plugin\Console\MokoJoomBackup\Command\CleanupCommand; -use Joomla\Plugin\Console\MokoJoomBackup\Command\ListCommand; -use Joomla\Plugin\Console\MokoJoomBackup\Command\ProfilesCommand; -use Joomla\Plugin\Console\MokoJoomBackup\Command\RestoreCommand; -use Joomla\Plugin\Console\MokoJoomBackup\Command\RunCommand; +use Joomla\Plugin\Console\MokoSuiteBackup\Command\CleanupCommand; +use Joomla\Plugin\Console\MokoSuiteBackup\Command\ListCommand; +use Joomla\Plugin\Console\MokoSuiteBackup\Command\ProfilesCommand; +use Joomla\Plugin\Console\MokoSuiteBackup\Command\RestoreCommand; +use Joomla\Plugin\Console\MokoSuiteBackup\Command\RunCommand; -final class MokoJoomBackupConsole extends CMSPlugin implements SubscriberInterface +final class MokoSuiteBackupConsole extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; diff --git a/source/packages/plg_content_mokojoombackup/language/en-GB/plg_content_mokojoombackup.sys.ini b/source/packages/plg_content_mokojoombackup/language/en-GB/plg_content_mokojoombackup.sys.ini deleted file mode 100644 index bb42dfd..0000000 --- a/source/packages/plg_content_mokojoombackup/language/en-GB/plg_content_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Content Plugin system language file (en-GB) -PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoJoomBackup" -PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." diff --git a/source/packages/plg_content_mokojoombackup/language/en-US/plg_content_mokojoombackup.sys.ini b/source/packages/plg_content_mokojoombackup/language/en-US/plg_content_mokojoombackup.sys.ini deleted file mode 100644 index 4c443cf..0000000 --- a/source/packages/plg_content_mokojoombackup/language/en-US/plg_content_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Content Plugin system language file (en-US) -PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoJoomBackup" -PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." diff --git a/source/packages/plg_content_mokojoombackup/language/en-GB/plg_content_mokojoombackup.ini b/source/packages/plg_content_mokosuitebackup/language/en-GB/plg_content_mokosuitebackup.ini similarity index 84% rename from source/packages/plg_content_mokojoombackup/language/en-GB/plg_content_mokojoombackup.ini rename to source/packages/plg_content_mokosuitebackup/language/en-GB/plg_content_mokosuitebackup.ini index b7e9375..126f36d 100644 --- a/source/packages/plg_content_mokojoombackup/language/en-GB/plg_content_mokojoombackup.ini +++ b/source/packages/plg_content_mokosuitebackup/language/en-GB/plg_content_mokosuitebackup.ini @@ -1,5 +1,5 @@ -; MokoJoomBackup — Content Plugin language file (en-GB) -PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoJoomBackup" +; MokoSuiteBackup — Content Plugin language file (en-GB) +PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoSuiteBackup" PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install" PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed." diff --git a/source/packages/plg_content_mokosuitebackup/language/en-GB/plg_content_mokosuitebackup.sys.ini b/source/packages/plg_content_mokosuitebackup/language/en-GB/plg_content_mokosuitebackup.sys.ini new file mode 100644 index 0000000..4d54f0a --- /dev/null +++ b/source/packages/plg_content_mokosuitebackup/language/en-GB/plg_content_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Content Plugin system language file (en-GB) +PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoSuiteBackup" +PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." diff --git a/source/packages/plg_content_mokojoombackup/language/en-US/plg_content_mokojoombackup.ini b/source/packages/plg_content_mokosuitebackup/language/en-US/plg_content_mokosuitebackup.ini similarity index 84% rename from source/packages/plg_content_mokojoombackup/language/en-US/plg_content_mokojoombackup.ini rename to source/packages/plg_content_mokosuitebackup/language/en-US/plg_content_mokosuitebackup.ini index 0765605..89f37dc 100644 --- a/source/packages/plg_content_mokojoombackup/language/en-US/plg_content_mokojoombackup.ini +++ b/source/packages/plg_content_mokosuitebackup/language/en-US/plg_content_mokosuitebackup.ini @@ -1,5 +1,5 @@ -; MokoJoomBackup — Content Plugin language file (en-US) -PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoJoomBackup" +; MokoSuiteBackup — Content Plugin language file (en-US) +PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoSuiteBackup" PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install" PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed." diff --git a/source/packages/plg_content_mokosuitebackup/language/en-US/plg_content_mokosuitebackup.sys.ini b/source/packages/plg_content_mokosuitebackup/language/en-US/plg_content_mokosuitebackup.sys.ini new file mode 100644 index 0000000..235bb7e --- /dev/null +++ b/source/packages/plg_content_mokosuitebackup/language/en-US/plg_content_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Content Plugin system language file (en-US) +PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoSuiteBackup" +PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." diff --git a/source/packages/plg_content_mokojoombackup/mokojoombackup.php b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.php similarity index 76% rename from source/packages/plg_content_mokojoombackup/mokojoombackup.php rename to source/packages/plg_content_mokosuitebackup/mokosuitebackup.php index c59a8b8..49dac86 100644 --- a/source/packages/plg_content_mokojoombackup/mokojoombackup.php +++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_content_mokojoombackup/mokojoombackup.xml b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml similarity index 76% rename from source/packages/plg_content_mokojoombackup/mokojoombackup.xml rename to source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml index c7d3e90..38537fe 100644 --- a/source/packages/plg_content_mokojoombackup/mokojoombackup.xml +++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml @@ -1,14 +1,13 @@ - plg_content_mokojoombackup - 01.08.00 + Content - MokoSuiteBackup + 01.20.00-rc 2026-06-04 Moko Consulting hello@mokoconsulting.tech @@ -17,17 +16,17 @@ GPL-3.0-or-later PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION - Joomla\Plugin\Content\MokoJoomBackup + Joomla\Plugin\Content\MokoSuiteBackup - mokojoombackup.php + mokosuitebackup.php services src - language/en-GB/plg_content_mokojoombackup.ini - language/en-GB/plg_content_mokojoombackup.sys.ini + language/en-GB/plg_content_mokosuitebackup.ini + language/en-GB/plg_content_mokosuitebackup.sys.ini @@ -60,7 +59,7 @@ type="sql" label="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE" description="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE_DESC" - query="SELECT id AS value, title AS text FROM #__mokojoombackup_profiles WHERE published = 1 ORDER BY ordering ASC" + query="SELECT id AS value, title AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY ordering ASC" default="1" > diff --git a/source/packages/plg_content_mokojoombackup/services/provider.php b/source/packages/plg_content_mokosuitebackup/services/provider.php similarity index 75% rename from source/packages/plg_content_mokojoombackup/services/provider.php rename to source/packages/plg_content_mokosuitebackup/services/provider.php index afcd020..e2b2c1b 100644 --- a/source/packages/plg_content_mokojoombackup/services/provider.php +++ b/source/packages/plg_content_mokosuitebackup/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -16,7 +16,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\Content\MokoJoomBackup\Extension\MokoJoomBackupContent; +use Joomla\Plugin\Content\MokoSuiteBackup\Extension\MokoSuiteBackupContent; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -24,9 +24,9 @@ return new class () implements ServiceProviderInterface { $container->set( PluginInterface::class, function (Container $container) { - $plugin = new MokoJoomBackupContent( + $plugin = new MokoSuiteBackupContent( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('content', 'mokojoombackup') + (array) PluginHelper::getPlugin('content', 'mokosuitebackup') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_content_mokojoombackup/src/Extension/MokoJoomBackupContent.php b/source/packages/plg_content_mokosuitebackup/src/Extension/MokoSuiteBackupContent.php similarity index 75% rename from source/packages/plg_content_mokojoombackup/src/Extension/MokoJoomBackupContent.php rename to source/packages/plg_content_mokosuitebackup/src/Extension/MokoSuiteBackupContent.php index ae65490..f3f98b0 100644 --- a/source/packages/plg_content_mokojoombackup/src/Extension/MokoJoomBackupContent.php +++ b/source/packages/plg_content_mokosuitebackup/src/Extension/MokoSuiteBackupContent.php @@ -1,24 +1,24 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Plugin\Content\MokoJoomBackup\Extension; +namespace Joomla\Plugin\Content\MokoSuiteBackup\Extension; defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; -final class MokoJoomBackupContent extends CMSPlugin implements SubscriberInterface +final class MokoSuiteBackupContent extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; @@ -63,15 +63,15 @@ final class MokoJoomBackupContent extends CMSPlugin implements SubscriberInterfa // Throttle: only one auto-backup per hour via session $session = Factory::getSession(); - $lastRun = $session->get('mokojoombackup.content_last_autobackup', 0); + $lastRun = $session->get('mokosuitebackup.content_last_autobackup', 0); if (time() - $lastRun < 3600) { return; } - $session->set('mokojoombackup.content_last_autobackup', time()); + $session->set('mokosuitebackup.content_last_autobackup', time()); - $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/src/Engine/BackupEngine.php'; + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php'; if (!file_exists($engineFile)) { return; @@ -87,7 +87,7 @@ final class MokoJoomBackupContent extends CMSPlugin implements SubscriberInterfa } catch (\Throwable $e) { // Non-fatal — log and continue with the install/update Factory::getApplication()->enqueueMessage( - 'MokoJoomBackup auto-backup failed: ' . $e->getMessage(), + 'MokoSuiteBackup auto-backup failed: ' . $e->getMessage(), 'warning' ); } diff --git a/source/packages/plg_quickicon_mokojoombackup/language/en-GB/index.html b/source/packages/plg_quickicon_mokosuitebackup/index.html similarity index 100% rename from source/packages/plg_quickicon_mokojoombackup/language/en-GB/index.html rename to source/packages/plg_quickicon_mokosuitebackup/index.html diff --git a/source/packages/plg_quickicon_mokojoombackup/language/en-US/index.html b/source/packages/plg_quickicon_mokosuitebackup/language/en-GB/index.html similarity index 100% rename from source/packages/plg_quickicon_mokojoombackup/language/en-US/index.html rename to source/packages/plg_quickicon_mokosuitebackup/language/en-GB/index.html diff --git a/source/packages/plg_quickicon_mokojoombackup/language/en-GB/plg_quickicon_mokojoombackup.ini b/source/packages/plg_quickicon_mokosuitebackup/language/en-GB/plg_quickicon_mokosuitebackup.ini similarity index 85% rename from source/packages/plg_quickicon_mokojoombackup/language/en-GB/plg_quickicon_mokojoombackup.ini rename to source/packages/plg_quickicon_mokosuitebackup/language/en-GB/plg_quickicon_mokosuitebackup.ini index cc0a4e5..c099061 100644 --- a/source/packages/plg_quickicon_mokojoombackup/language/en-GB/plg_quickicon_mokojoombackup.ini +++ b/source/packages/plg_quickicon_mokosuitebackup/language/en-GB/plg_quickicon_mokosuitebackup.ini @@ -1,4 +1,4 @@ -PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoJoomBackup" +PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoSuiteBackup" PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION="Shows backup status on the administrator dashboard." PLG_QUICKICON_MOKOJOOMBACKUP_OK="Backups: OK" PLG_QUICKICON_MOKOJOOMBACKUP_NO_BACKUPS="Backups: No backups yet!" diff --git a/source/packages/plg_quickicon_mokojoombackup/language/en-GB/plg_quickicon_mokojoombackup.sys.ini b/source/packages/plg_quickicon_mokosuitebackup/language/en-GB/plg_quickicon_mokosuitebackup.sys.ini similarity index 61% rename from source/packages/plg_quickicon_mokojoombackup/language/en-GB/plg_quickicon_mokojoombackup.sys.ini rename to source/packages/plg_quickicon_mokosuitebackup/language/en-GB/plg_quickicon_mokosuitebackup.sys.ini index d432f08..4b4ba26 100644 --- a/source/packages/plg_quickicon_mokojoombackup/language/en-GB/plg_quickicon_mokojoombackup.sys.ini +++ b/source/packages/plg_quickicon_mokosuitebackup/language/en-GB/plg_quickicon_mokosuitebackup.sys.ini @@ -1,2 +1,2 @@ -PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoJoomBackup" +PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoSuiteBackup" PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION="Shows backup status on the administrator dashboard." diff --git a/source/packages/plg_quickicon_mokojoombackup/language/index.html b/source/packages/plg_quickicon_mokosuitebackup/language/en-US/index.html similarity index 100% rename from source/packages/plg_quickicon_mokojoombackup/language/index.html rename to source/packages/plg_quickicon_mokosuitebackup/language/en-US/index.html diff --git a/source/packages/plg_quickicon_mokojoombackup/language/en-US/plg_quickicon_mokojoombackup.ini b/source/packages/plg_quickicon_mokosuitebackup/language/en-US/plg_quickicon_mokosuitebackup.ini similarity index 85% rename from source/packages/plg_quickicon_mokojoombackup/language/en-US/plg_quickicon_mokojoombackup.ini rename to source/packages/plg_quickicon_mokosuitebackup/language/en-US/plg_quickicon_mokosuitebackup.ini index cc0a4e5..c099061 100644 --- a/source/packages/plg_quickicon_mokojoombackup/language/en-US/plg_quickicon_mokojoombackup.ini +++ b/source/packages/plg_quickicon_mokosuitebackup/language/en-US/plg_quickicon_mokosuitebackup.ini @@ -1,4 +1,4 @@ -PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoJoomBackup" +PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoSuiteBackup" PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION="Shows backup status on the administrator dashboard." PLG_QUICKICON_MOKOJOOMBACKUP_OK="Backups: OK" PLG_QUICKICON_MOKOJOOMBACKUP_NO_BACKUPS="Backups: No backups yet!" diff --git a/source/packages/plg_quickicon_mokojoombackup/language/en-US/plg_quickicon_mokojoombackup.sys.ini b/source/packages/plg_quickicon_mokosuitebackup/language/en-US/plg_quickicon_mokosuitebackup.sys.ini similarity index 61% rename from source/packages/plg_quickicon_mokojoombackup/language/en-US/plg_quickicon_mokojoombackup.sys.ini rename to source/packages/plg_quickicon_mokosuitebackup/language/en-US/plg_quickicon_mokosuitebackup.sys.ini index d432f08..4b4ba26 100644 --- a/source/packages/plg_quickicon_mokojoombackup/language/en-US/plg_quickicon_mokojoombackup.sys.ini +++ b/source/packages/plg_quickicon_mokosuitebackup/language/en-US/plg_quickicon_mokosuitebackup.sys.ini @@ -1,2 +1,2 @@ -PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoJoomBackup" +PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoSuiteBackup" PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION="Shows backup status on the administrator dashboard." diff --git a/source/packages/plg_quickicon_mokojoombackup/services/index.html b/source/packages/plg_quickicon_mokosuitebackup/language/index.html similarity index 100% rename from source/packages/plg_quickicon_mokojoombackup/services/index.html rename to source/packages/plg_quickicon_mokosuitebackup/language/index.html diff --git a/source/packages/plg_quickicon_mokojoombackup/mokojoombackup.php b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.php similarity index 100% rename from source/packages/plg_quickicon_mokojoombackup/mokojoombackup.php rename to source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.php diff --git a/source/packages/plg_quickicon_mokojoombackup/mokojoombackup.xml b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml similarity index 59% rename from source/packages/plg_quickicon_mokojoombackup/mokojoombackup.xml rename to source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml index 3848797..f4b4268 100644 --- a/source/packages/plg_quickicon_mokojoombackup/mokojoombackup.xml +++ b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml @@ -1,7 +1,7 @@ - plg_quickicon_mokojoombackup - 01.08.00 + Quick Icon - MokoSuiteBackup + 01.20.00-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech @@ -10,16 +10,16 @@ GPL-3.0-or-later PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION - Joomla\Plugin\Quickicon\MokoJoomBackup + Joomla\Plugin\Quickicon\MokoSuiteBackup - mokojoombackup.php + mokosuitebackup.php services src - language/en-GB/plg_quickicon_mokojoombackup.ini - language/en-GB/plg_quickicon_mokojoombackup.sys.ini + language/en-GB/plg_quickicon_mokosuitebackup.ini + language/en-GB/plg_quickicon_mokosuitebackup.sys.ini diff --git a/source/packages/plg_quickicon_mokojoombackup/src/Extension/index.html b/source/packages/plg_quickicon_mokosuitebackup/services/index.html similarity index 100% rename from source/packages/plg_quickicon_mokojoombackup/src/Extension/index.html rename to source/packages/plg_quickicon_mokosuitebackup/services/index.html diff --git a/source/packages/plg_quickicon_mokojoombackup/services/provider.php b/source/packages/plg_quickicon_mokosuitebackup/services/provider.php similarity index 75% rename from source/packages/plg_quickicon_mokojoombackup/services/provider.php rename to source/packages/plg_quickicon_mokosuitebackup/services/provider.php index 4af1c9f..48ad6c2 100644 --- a/source/packages/plg_quickicon_mokojoombackup/services/provider.php +++ b/source/packages/plg_quickicon_mokosuitebackup/services/provider.php @@ -8,7 +8,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\Quickicon\MokoJoomBackup\Extension\MokoJoomBackupQuickicon; +use Joomla\Plugin\Quickicon\MokoSuiteBackup\Extension\MokoSuiteBackupQuickicon; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -16,9 +16,9 @@ return new class () implements ServiceProviderInterface { $container->set( PluginInterface::class, function (Container $container) { - $plugin = new MokoJoomBackupQuickicon( + $plugin = new MokoSuiteBackupQuickicon( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('quickicon', 'mokojoombackup') + (array) PluginHelper::getPlugin('quickicon', 'mokosuitebackup') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_quickicon_mokojoombackup/src/Extension/MokoJoomBackupQuickicon.php b/source/packages/plg_quickicon_mokosuitebackup/src/Extension/MokoSuiteBackupQuickicon.php similarity index 86% rename from source/packages/plg_quickicon_mokojoombackup/src/Extension/MokoJoomBackupQuickicon.php rename to source/packages/plg_quickicon_mokosuitebackup/src/Extension/MokoSuiteBackupQuickicon.php index b17baee..1972b97 100644 --- a/source/packages/plg_quickicon_mokojoombackup/src/Extension/MokoJoomBackupQuickicon.php +++ b/source/packages/plg_quickicon_mokosuitebackup/src/Extension/MokoSuiteBackupQuickicon.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -10,7 +10,7 @@ * Dashboard quickicon widget showing backup status at a glance. */ -namespace Joomla\Plugin\Quickicon\MokoJoomBackup\Extension; +namespace Joomla\Plugin\Quickicon\MokoSuiteBackup\Extension; defined('_JEXEC') or die; @@ -20,7 +20,7 @@ use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; -final class MokoJoomBackupQuickicon extends CMSPlugin implements SubscriberInterface +final class MokoSuiteBackupQuickicon extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; @@ -44,7 +44,7 @@ final class MokoJoomBackupQuickicon extends CMSPlugin implements SubscriberInter // Get last completed backup $query = $db->getQuery(true) ->select('*') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('status') . ' = ' . $db->quote('complete')) ->order($db->quoteName('backupstart') . ' DESC'); $db->setQuery($query, 0, 1); @@ -53,7 +53,7 @@ final class MokoJoomBackupQuickicon extends CMSPlugin implements SubscriberInter // Get total count and storage $query = $db->getQuery(true) ->select('COUNT(*) AS total, COALESCE(SUM(total_size), 0) AS total_size') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); $db->setQuery($query); $stats = $db->loadObject(); @@ -61,7 +61,7 @@ final class MokoJoomBackupQuickicon extends CMSPlugin implements SubscriberInter // Check for recent failures $query = $db->getQuery(true) ->select('COUNT(*)') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('status') . ' = ' . $db->quote('fail')) ->where($db->quoteName('backupstart') . ' > DATE_SUB(NOW(), INTERVAL 7 DAY)'); $db->setQuery($query); @@ -94,12 +94,12 @@ final class MokoJoomBackupQuickicon extends CMSPlugin implements SubscriberInter $result = $event->getArgument('result', []); $result[] = [ [ - 'link' => 'index.php?option=com_mokojoombackup&view=backups', + 'link' => 'index.php?option=com_mokosuitebackup&view=backups', 'image' => $warning ? 'icon-warning' : 'icon-database', 'icon' => $warning ? 'icon-warning' : 'icon-database', 'text' => Text::_($text), 'linkadd' => $subtitle ? '
' . htmlspecialchars($subtitle) . '' : '', - 'id' => 'plg_quickicon_mokojoombackup', + 'id' => 'plg_quickicon_mokosuitebackup', 'group' => 'MOD_QUICKICON_MAINTENANCE', ], ]; diff --git a/source/packages/plg_quickicon_mokojoombackup/src/index.html b/source/packages/plg_quickicon_mokosuitebackup/src/Extension/index.html similarity index 100% rename from source/packages/plg_quickicon_mokojoombackup/src/index.html rename to source/packages/plg_quickicon_mokosuitebackup/src/Extension/index.html diff --git a/source/packages/plg_system_mokojoombackup/index.html b/source/packages/plg_quickicon_mokosuitebackup/src/index.html similarity index 100% rename from source/packages/plg_system_mokojoombackup/index.html rename to source/packages/plg_quickicon_mokosuitebackup/src/index.html diff --git a/source/packages/plg_system_mokojoombackup/language/en-GB/plg_system_mokojoombackup.sys.ini b/source/packages/plg_system_mokojoombackup/language/en-GB/plg_system_mokojoombackup.sys.ini deleted file mode 100644 index 1fce9d3..0000000 --- a/source/packages/plg_system_mokojoombackup/language/en-GB/plg_system_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — System Plugin system language file (en-GB) -PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoJoomBackup" -PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers." diff --git a/source/packages/plg_system_mokojoombackup/language/en-US/plg_system_mokojoombackup.sys.ini b/source/packages/plg_system_mokojoombackup/language/en-US/plg_system_mokojoombackup.sys.ini deleted file mode 100644 index 9c4fd2e..0000000 --- a/source/packages/plg_system_mokojoombackup/language/en-US/plg_system_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — System Plugin system language file (en-US) -PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoJoomBackup" -PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers." diff --git a/source/packages/plg_system_mokojoombackup/language/en-GB/index.html b/source/packages/plg_system_mokosuitebackup/index.html similarity index 100% rename from source/packages/plg_system_mokojoombackup/language/en-GB/index.html rename to source/packages/plg_system_mokosuitebackup/index.html diff --git a/source/packages/plg_system_mokojoombackup/language/en-US/index.html b/source/packages/plg_system_mokosuitebackup/language/en-GB/index.html similarity index 100% rename from source/packages/plg_system_mokojoombackup/language/en-US/index.html rename to source/packages/plg_system_mokosuitebackup/language/en-GB/index.html diff --git a/source/packages/plg_system_mokojoombackup/language/en-GB/plg_system_mokojoombackup.ini b/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini similarity index 84% rename from source/packages/plg_system_mokojoombackup/language/en-GB/plg_system_mokojoombackup.ini rename to source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini index 246611b..d0804cd 100644 --- a/source/packages/plg_system_mokojoombackup/language/en-GB/plg_system_mokojoombackup.ini +++ b/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini @@ -1,5 +1,5 @@ -; MokoJoomBackup — System Plugin language file (en-GB) -PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoJoomBackup" +; MokoSuiteBackup — System Plugin language file (en-GB) +PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoSuiteBackup" PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers." PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_AUTO_CLEANUP="Auto Cleanup" PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_AUTO_CLEANUP_DESC="Automatically remove old backup archives based on age and count limits." diff --git a/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.sys.ini b/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.sys.ini new file mode 100644 index 0000000..563477a --- /dev/null +++ b/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — System Plugin system language file (en-GB) +PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoSuiteBackup" +PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers." diff --git a/source/packages/plg_system_mokojoombackup/language/index.html b/source/packages/plg_system_mokosuitebackup/language/en-US/index.html similarity index 100% rename from source/packages/plg_system_mokojoombackup/language/index.html rename to source/packages/plg_system_mokosuitebackup/language/en-US/index.html diff --git a/source/packages/plg_system_mokojoombackup/language/en-US/plg_system_mokojoombackup.ini b/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini similarity index 84% rename from source/packages/plg_system_mokojoombackup/language/en-US/plg_system_mokojoombackup.ini rename to source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini index 8d8e47f..15103fb 100644 --- a/source/packages/plg_system_mokojoombackup/language/en-US/plg_system_mokojoombackup.ini +++ b/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini @@ -1,5 +1,5 @@ -; MokoJoomBackup — System Plugin language file (en-US) -PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoJoomBackup" +; MokoSuiteBackup — System Plugin language file (en-US) +PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoSuiteBackup" PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers." PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_AUTO_CLEANUP="Auto Cleanup" PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_AUTO_CLEANUP_DESC="Automatically remove old backup archives based on age and count limits." diff --git a/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.sys.ini b/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.sys.ini new file mode 100644 index 0000000..66e5ee4 --- /dev/null +++ b/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — System Plugin system language file (en-US) +PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoSuiteBackup" +PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers." diff --git a/source/packages/plg_system_mokojoombackup/services/index.html b/source/packages/plg_system_mokosuitebackup/language/index.html similarity index 100% rename from source/packages/plg_system_mokojoombackup/services/index.html rename to source/packages/plg_system_mokosuitebackup/language/index.html diff --git a/source/packages/plg_console_mokojoombackup/mokojoombackup.php b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.php similarity index 76% rename from source/packages/plg_console_mokojoombackup/mokojoombackup.php rename to source/packages/plg_system_mokosuitebackup/mokosuitebackup.php index bb0f01c..0bd3eb5 100644 --- a/source/packages/plg_console_mokojoombackup/mokojoombackup.php +++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_system_mokojoombackup/mokojoombackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml similarity index 78% rename from source/packages/plg_system_mokojoombackup/mokojoombackup.xml rename to source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml index 3475147..5895d60 100644 --- a/source/packages/plg_system_mokojoombackup/mokojoombackup.xml +++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml @@ -1,14 +1,13 @@ - plg_system_mokojoombackup - 01.08.00 + System - MokoSuiteBackup + 01.20.00-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech @@ -17,17 +16,17 @@ GPL-3.0-or-later PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION - Joomla\Plugin\System\MokoJoomBackup + Joomla\Plugin\System\MokoSuiteBackup - mokojoombackup.php + mokosuitebackup.php services src - language/en-GB/plg_system_mokojoombackup.ini - language/en-GB/plg_system_mokojoombackup.sys.ini + language/en-GB/plg_system_mokosuitebackup.ini + language/en-GB/plg_system_mokosuitebackup.sys.ini diff --git a/source/packages/plg_system_mokojoombackup/src/Extension/index.html b/source/packages/plg_system_mokosuitebackup/services/index.html similarity index 100% rename from source/packages/plg_system_mokojoombackup/src/Extension/index.html rename to source/packages/plg_system_mokosuitebackup/services/index.html diff --git a/source/packages/plg_system_mokojoombackup/services/provider.php b/source/packages/plg_system_mokosuitebackup/services/provider.php similarity index 76% rename from source/packages/plg_system_mokojoombackup/services/provider.php rename to source/packages/plg_system_mokosuitebackup/services/provider.php index fb7f9d4..f4c0ed5 100644 --- a/source/packages/plg_system_mokojoombackup/services/provider.php +++ b/source/packages/plg_system_mokosuitebackup/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -16,7 +16,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\System\MokoJoomBackup\Extension\MokoJoomBackup; +use Joomla\Plugin\System\MokoSuiteBackup\Extension\MokoSuiteBackup; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -24,9 +24,9 @@ return new class () implements ServiceProviderInterface { $container->set( PluginInterface::class, function (Container $container) { - $plugin = new MokoJoomBackup( + $plugin = new MokoSuiteBackup( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('system', 'mokojoombackup') + (array) PluginHelper::getPlugin('system', 'mokosuitebackup') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_system_mokojoombackup/src/Extension/MokoJoomBackup.php b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php similarity index 65% rename from source/packages/plg_system_mokojoombackup/src/Extension/MokoJoomBackup.php rename to source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php index e459e3f..b0d8d38 100644 --- a/source/packages/plg_system_mokojoombackup/src/Extension/MokoJoomBackup.php +++ b/source/packages/plg_system_mokosuitebackup/src/Extension/MokoSuiteBackup.php @@ -1,40 +1,42 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ -namespace Joomla\Plugin\System\MokoJoomBackup\Extension; +namespace Joomla\Plugin\System\MokoSuiteBackup\Extension; defined('_JEXEC') or die; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine; +use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; -final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface +final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; public static function getSubscribedEvents(): array { return [ - 'onAfterInitialise' => 'onAfterInitialise', - 'onAfterRoute' => 'onAfterRoute', + 'onAfterInitialise' => 'onAfterInitialise', + 'onAfterRoute' => 'onAfterRoute', + 'onExtensionBeforeUpdate' => 'onExtensionBeforeUpdate', + 'onExtensionBeforeUninstall' => 'onExtensionBeforeUninstall', ]; } /** * Web cron trigger — runs before routing so no authentication is needed. * - * URL: index.php?mokojoombackup_cron=SECRET&profile_id=1 + * URL: index.php?mokosuitebackup_cron=SECRET&profile_id=1 * * External cron services (cron-job.org, UptimeRobot, etc.) can call this * URL on a schedule to trigger backups on shared hosting without crontab. @@ -42,14 +44,14 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface public function onAfterInitialise(Event $event): void { $app = $this->getApplication(); - $secret = $app->input->getString('mokojoombackup_cron', ''); + $secret = $app->input->getString('mokosuitebackup_cron', ''); if ($secret === '') { return; } // Load component params - $params = ComponentHelper::getParams('com_mokojoombackup'); + $params = ComponentHelper::getParams('com_mokosuitebackup'); $enabled = (int) $params->get('webcron_enabled', 0); $configSecret = trim($params->get('webcron_secret', '')); $ipWhitelist = trim($params->get('webcron_ip_whitelist', '')); @@ -104,7 +106,7 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface $app = $this->getApplication(); // Skip if this is a web cron request (already handled) - if ($app->input->getString('mokojoombackup_cron', '') !== '') { + if ($app->input->getString('mokosuitebackup_cron', '') !== '') { return; } @@ -119,13 +121,13 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface // Throttle: only check once per hour via session flag $session = Factory::getSession(); - $lastCheck = $session->get('mokojoombackup.last_cleanup', 0); + $lastCheck = $session->get('mokosuitebackup.last_cleanup', 0); if (time() - $lastCheck < 3600) { return; } - $session->set('mokojoombackup.last_cleanup', time()); + $session->set('mokosuitebackup.last_cleanup', time()); $this->cleanupOldBackups(); } @@ -143,7 +145,7 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface $cutoff = date('Y-m-d H:i:s', strtotime("-{$maxAge} days")); $query = $db->getQuery(true) ->select('id, absolute_path') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)) ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); $db->setQuery($query); @@ -158,7 +160,7 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface $db->setQuery( $db->getQuery(true) - ->delete($db->quoteName('#__mokojoombackup_records')) + ->delete($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('id') . ' = ' . (int) $record->id) ); $db->execute(); @@ -167,7 +169,7 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface // Enforce max backups count (keep newest) $query = $db->getQuery(true) ->select('COUNT(*)') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); $db->setQuery($query); $totalCount = (int) $db->loadResult(); @@ -176,7 +178,7 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface $excess = $totalCount - $maxBackups; $query = $db->getQuery(true) ->select('id, absolute_path') - ->from($db->quoteName('#__mokojoombackup_records')) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('status') . ' = ' . $db->quote('complete')) ->order($db->quoteName('backupstart') . ' ASC'); $db->setQuery($query, 0, $excess); @@ -191,7 +193,7 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface $db->setQuery( $db->getQuery(true) - ->delete($db->quoteName('#__mokojoombackup_records')) + ->delete($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('id') . ' = ' . (int) $record->id) ); $db->execute(); @@ -199,6 +201,68 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface } } + /** + * Run a backup before any extension is updated. + */ + public function onExtensionBeforeUpdate(Event $event): void + { + $this->runPreActionBackup('backup_before_update', 'Pre-update backup'); + } + + /** + * Run a backup before any extension is uninstalled. + */ + public function onExtensionBeforeUninstall(Event $event): void + { + $this->runPreActionBackup('backup_before_uninstall', 'Pre-uninstall backup'); + } + + /** + * Run a pre-action backup if the option is enabled and not already + * done in this session (throttled to once per 10 minutes to avoid + * duplicate backups during batch updates). + */ + private function runPreActionBackup(string $paramName, string $description): void + { + $params = ComponentHelper::getParams('com_mokosuitebackup'); + + if (!(int) $params->get($paramName, 0)) { + return; + } + + // Throttle: only run once per 10 minutes to prevent duplicate + // backups when multiple extensions are updated in a batch + $session = Factory::getSession(); + $sessionKey = 'mokosuitebackup.preaction_' . $paramName; + $lastRun = $session->get($sessionKey, 0); + + if (time() - $lastRun < 600) { + return; + } + + $session->set($sessionKey, time()); + + $profileId = (int) $params->get('default_profile', 1); + + try { + $engine = new BackupEngine(); + $result = $engine->run($profileId, $description, 'preaction'); + + if (!$result['success']) { + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup: ' . $description . ' failed — ' . $result['message'], + 'warning' + ); + } + } catch (\Exception $e) { + error_log('MokoSuiteBackup: ' . $description . ' failed: ' . $e->getMessage()); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup: ' . $description . ' failed — ' . $e->getMessage(), + 'warning' + ); + } + } + /** * Send a JSON response and terminate — used by web cron handler. */ diff --git a/source/packages/plg_system_mokojoombackup/src/index.html b/source/packages/plg_system_mokosuitebackup/src/Extension/index.html similarity index 100% rename from source/packages/plg_system_mokojoombackup/src/index.html rename to source/packages/plg_system_mokosuitebackup/src/Extension/index.html diff --git a/source/packages/plg_task_mokojoombackup/forms/index.html b/source/packages/plg_system_mokosuitebackup/src/index.html similarity index 100% rename from source/packages/plg_task_mokojoombackup/forms/index.html rename to source/packages/plg_system_mokosuitebackup/src/index.html diff --git a/source/packages/plg_task_mokojoombackup/language/en-GB/plg_task_mokojoombackup.ini b/source/packages/plg_task_mokojoombackup/language/en-GB/plg_task_mokojoombackup.ini deleted file mode 100644 index ef0115d..0000000 --- a/source/packages/plg_task_mokojoombackup/language/en-GB/plg_task_mokojoombackup.ini +++ /dev/null @@ -1,12 +0,0 @@ -; MokoJoomBackup — Task Plugin language file (en-GB) -PLG_TASK_MOKOJOOMBACKUP="Task - MokoJoomBackup" -PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoJoomBackup. Allows running backup profiles on a schedule via Joomla's Scheduled Tasks system." - -; Task type -PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_TITLE="MokoJoomBackup: Run Backup Profile" -PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_DESC="Run a MokoJoomBackup backup using the selected profile. Create multiple tasks with different profiles for different backup schedules (e.g. daily full backup, hourly database-only backup)." - -; Task form fields -PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE="Backup Profile" -PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE_DESC="Select which backup profile to run. Each profile defines backup type (full/database/files), exclusion filters, and storage settings." -PLG_TASK_MOKOJOOMBACKUP_SELECT_PROFILE="- Select Profile -" diff --git a/source/packages/plg_task_mokojoombackup/language/en-GB/plg_task_mokojoombackup.sys.ini b/source/packages/plg_task_mokojoombackup/language/en-GB/plg_task_mokojoombackup.sys.ini deleted file mode 100644 index cc26e23..0000000 --- a/source/packages/plg_task_mokojoombackup/language/en-GB/plg_task_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Task Plugin system language file (en-GB) -PLG_TASK_MOKOJOOMBACKUP="Task - MokoJoomBackup" -PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoJoomBackup. Run backup profiles on a schedule via Joomla's Scheduled Tasks." diff --git a/source/packages/plg_task_mokojoombackup/language/en-US/plg_task_mokojoombackup.ini b/source/packages/plg_task_mokojoombackup/language/en-US/plg_task_mokojoombackup.ini deleted file mode 100644 index 7deadad..0000000 --- a/source/packages/plg_task_mokojoombackup/language/en-US/plg_task_mokojoombackup.ini +++ /dev/null @@ -1,8 +0,0 @@ -; MokoJoomBackup — Task Plugin language file (en-US) -PLG_TASK_MOKOJOOMBACKUP="Task - MokoJoomBackup" -PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoJoomBackup. Allows running backup profiles on a schedule via Joomla's Scheduled Tasks system." -PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_TITLE="MokoJoomBackup: Run Backup Profile" -PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_DESC="Run a MokoJoomBackup backup using the selected profile. Create multiple tasks with different profiles for different backup schedules." -PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE="Backup Profile" -PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE_DESC="Select which backup profile to run." -PLG_TASK_MOKOJOOMBACKUP_SELECT_PROFILE="- Select Profile -" diff --git a/source/packages/plg_task_mokojoombackup/language/en-US/plg_task_mokojoombackup.sys.ini b/source/packages/plg_task_mokojoombackup/language/en-US/plg_task_mokojoombackup.sys.ini deleted file mode 100644 index 4850f90..0000000 --- a/source/packages/plg_task_mokojoombackup/language/en-US/plg_task_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — Task Plugin system language file (en-US) -PLG_TASK_MOKOJOOMBACKUP="Task - MokoJoomBackup" -PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoJoomBackup. Run backup profiles on a schedule via Joomla's Scheduled Tasks." diff --git a/source/packages/plg_task_mokojoombackup/index.html b/source/packages/plg_task_mokosuitebackup/forms/index.html similarity index 100% rename from source/packages/plg_task_mokojoombackup/index.html rename to source/packages/plg_task_mokosuitebackup/forms/index.html diff --git a/source/packages/plg_task_mokojoombackup/forms/run_profile.xml b/source/packages/plg_task_mokosuitebackup/forms/run_profile.xml similarity index 74% rename from source/packages/plg_task_mokojoombackup/forms/run_profile.xml rename to source/packages/plg_task_mokosuitebackup/forms/run_profile.xml index 57ee2f4..cf1fc9c 100644 --- a/source/packages/plg_task_mokojoombackup/forms/run_profile.xml +++ b/source/packages/plg_task_mokosuitebackup/forms/run_profile.xml @@ -2,7 +2,7 @@
@@ -11,7 +11,7 @@ type="sql" label="PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE" description="PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE_DESC" - query="SELECT id AS value, title AS text FROM #__mokojoombackup_profiles WHERE published = 1 ORDER BY ordering ASC" + query="SELECT id AS value, title AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY ordering ASC" default="1" required="true" > diff --git a/source/packages/plg_task_mokojoombackup/language/en-GB/index.html b/source/packages/plg_task_mokosuitebackup/index.html similarity index 100% rename from source/packages/plg_task_mokojoombackup/language/en-GB/index.html rename to source/packages/plg_task_mokosuitebackup/index.html diff --git a/source/packages/plg_task_mokojoombackup/language/en-US/index.html b/source/packages/plg_task_mokosuitebackup/language/en-GB/index.html similarity index 100% rename from source/packages/plg_task_mokojoombackup/language/en-US/index.html rename to source/packages/plg_task_mokosuitebackup/language/en-GB/index.html diff --git a/source/packages/plg_task_mokosuitebackup/language/en-GB/plg_task_mokosuitebackup.ini b/source/packages/plg_task_mokosuitebackup/language/en-GB/plg_task_mokosuitebackup.ini new file mode 100644 index 0000000..d2a490c --- /dev/null +++ b/source/packages/plg_task_mokosuitebackup/language/en-GB/plg_task_mokosuitebackup.ini @@ -0,0 +1,12 @@ +; MokoSuiteBackup — Task Plugin language file (en-GB) +PLG_TASK_MOKOJOOMBACKUP="Task - MokoSuiteBackup" +PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoSuiteBackup. Allows running backup profiles on a schedule via Joomla's Scheduled Tasks system." + +; Task type +PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_TITLE="MokoSuiteBackup: Run Backup Profile" +PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_DESC="Run a MokoSuiteBackup backup using the selected profile. Create multiple tasks with different profiles for different backup schedules (e.g. daily full backup, hourly database-only backup)." + +; Task form fields +PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE="Backup Profile" +PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE_DESC="Select which backup profile to run. Each profile defines backup type (full/database/files), exclusion filters, and storage settings." +PLG_TASK_MOKOJOOMBACKUP_SELECT_PROFILE="- Select Profile -" diff --git a/source/packages/plg_task_mokosuitebackup/language/en-GB/plg_task_mokosuitebackup.sys.ini b/source/packages/plg_task_mokosuitebackup/language/en-GB/plg_task_mokosuitebackup.sys.ini new file mode 100644 index 0000000..1dc1a26 --- /dev/null +++ b/source/packages/plg_task_mokosuitebackup/language/en-GB/plg_task_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Task Plugin system language file (en-GB) +PLG_TASK_MOKOJOOMBACKUP="Task - MokoSuiteBackup" +PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoSuiteBackup. Run backup profiles on a schedule via Joomla's Scheduled Tasks." diff --git a/source/packages/plg_task_mokojoombackup/language/index.html b/source/packages/plg_task_mokosuitebackup/language/en-US/index.html similarity index 100% rename from source/packages/plg_task_mokojoombackup/language/index.html rename to source/packages/plg_task_mokosuitebackup/language/en-US/index.html diff --git a/source/packages/plg_task_mokosuitebackup/language/en-US/plg_task_mokosuitebackup.ini b/source/packages/plg_task_mokosuitebackup/language/en-US/plg_task_mokosuitebackup.ini new file mode 100644 index 0000000..5283b83 --- /dev/null +++ b/source/packages/plg_task_mokosuitebackup/language/en-US/plg_task_mokosuitebackup.ini @@ -0,0 +1,8 @@ +; MokoSuiteBackup — Task Plugin language file (en-US) +PLG_TASK_MOKOJOOMBACKUP="Task - MokoSuiteBackup" +PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoSuiteBackup. Allows running backup profiles on a schedule via Joomla's Scheduled Tasks system." +PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_TITLE="MokoSuiteBackup: Run Backup Profile" +PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_DESC="Run a MokoSuiteBackup backup using the selected profile. Create multiple tasks with different profiles for different backup schedules." +PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE="Backup Profile" +PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE_DESC="Select which backup profile to run." +PLG_TASK_MOKOJOOMBACKUP_SELECT_PROFILE="- Select Profile -" diff --git a/source/packages/plg_task_mokosuitebackup/language/en-US/plg_task_mokosuitebackup.sys.ini b/source/packages/plg_task_mokosuitebackup/language/en-US/plg_task_mokosuitebackup.sys.ini new file mode 100644 index 0000000..23039fb --- /dev/null +++ b/source/packages/plg_task_mokosuitebackup/language/en-US/plg_task_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — Task Plugin system language file (en-US) +PLG_TASK_MOKOJOOMBACKUP="Task - MokoSuiteBackup" +PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoSuiteBackup. Run backup profiles on a schedule via Joomla's Scheduled Tasks." diff --git a/source/packages/plg_task_mokojoombackup/services/index.html b/source/packages/plg_task_mokosuitebackup/language/index.html similarity index 100% rename from source/packages/plg_task_mokojoombackup/services/index.html rename to source/packages/plg_task_mokosuitebackup/language/index.html diff --git a/source/packages/plg_task_mokojoombackup/mokojoombackup.php b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.php similarity index 77% rename from source/packages/plg_task_mokojoombackup/mokojoombackup.php rename to source/packages/plg_task_mokosuitebackup/mokosuitebackup.php index 4cc8120..e76f1ef 100644 --- a/source/packages/plg_task_mokojoombackup/mokojoombackup.php +++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_task_mokojoombackup/mokojoombackup.xml b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml similarity index 65% rename from source/packages/plg_task_mokojoombackup/mokojoombackup.xml rename to source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml index 5249aab..cf0831b 100644 --- a/source/packages/plg_task_mokojoombackup/mokojoombackup.xml +++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml @@ -1,14 +1,13 @@ - plg_task_mokojoombackup - 01.08.00 + Task - MokoSuiteBackup + 01.20.00-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech @@ -17,17 +16,17 @@ GPL-3.0-or-later PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION - Joomla\Plugin\Task\MokoJoomBackup + Joomla\Plugin\Task\MokoSuiteBackup - mokojoombackup.php + mokosuitebackup.php services src forms - language/en-GB/plg_task_mokojoombackup.ini - language/en-GB/plg_task_mokojoombackup.sys.ini + language/en-GB/plg_task_mokosuitebackup.ini + language/en-GB/plg_task_mokosuitebackup.sys.ini diff --git a/source/packages/plg_task_mokojoombackup/src/Extension/index.html b/source/packages/plg_task_mokosuitebackup/services/index.html similarity index 100% rename from source/packages/plg_task_mokojoombackup/src/Extension/index.html rename to source/packages/plg_task_mokosuitebackup/services/index.html diff --git a/source/packages/plg_task_mokojoombackup/services/provider.php b/source/packages/plg_task_mokosuitebackup/services/provider.php similarity index 76% rename from source/packages/plg_task_mokojoombackup/services/provider.php rename to source/packages/plg_task_mokosuitebackup/services/provider.php index f48dbc4..3680436 100644 --- a/source/packages/plg_task_mokojoombackup/services/provider.php +++ b/source/packages/plg_task_mokosuitebackup/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -16,7 +16,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\Task\MokoJoomBackup\Extension\MokoJoomBackupTask; +use Joomla\Plugin\Task\MokoSuiteBackup\Extension\MokoSuiteBackupTask; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -24,9 +24,9 @@ return new class () implements ServiceProviderInterface { $container->set( PluginInterface::class, function (Container $container) { - $plugin = new MokoJoomBackupTask( + $plugin = new MokoSuiteBackupTask( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('task', 'mokojoombackup') + (array) PluginHelper::getPlugin('task', 'mokosuitebackup') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_task_mokojoombackup/src/Extension/MokoJoomBackupTask.php b/source/packages/plg_task_mokosuitebackup/src/Extension/MokoSuiteBackupTask.php similarity index 78% rename from source/packages/plg_task_mokojoombackup/src/Extension/MokoJoomBackupTask.php rename to source/packages/plg_task_mokosuitebackup/src/Extension/MokoSuiteBackupTask.php index 4023778..4a0e020 100644 --- a/source/packages/plg_task_mokojoombackup/src/Extension/MokoJoomBackupTask.php +++ b/source/packages/plg_task_mokosuitebackup/src/Extension/MokoSuiteBackupTask.php @@ -1,20 +1,20 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE * - * Joomla Scheduled Task plugin for MokoJoomBackup. + * Joomla Scheduled Task plugin for MokoSuiteBackup. * * Registers a "Run Backup Profile" task type with com_scheduler. * Admins can create multiple scheduled tasks in System > Scheduled Tasks, * each pointing to a different backup profile — just like Akeeba Backup Pro. */ -namespace Joomla\Plugin\Task\MokoJoomBackup\Extension; +namespace Joomla\Plugin\Task\MokoSuiteBackup\Extension; defined('_JEXEC') or die; @@ -25,7 +25,7 @@ use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; -final class MokoJoomBackupTask extends CMSPlugin implements SubscriberInterface +final class MokoSuiteBackupTask extends CMSPlugin implements SubscriberInterface { use TaskPluginTrait; @@ -38,7 +38,7 @@ final class MokoJoomBackupTask extends CMSPlugin implements SubscriberInterface * so different backup profiles run on different schedules. */ protected const TASKS_MAP = [ - 'mokojoombackup.run_profile' => [ + 'mokosuitebackup.run_profile' => [ 'langConstPrefix' => 'PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE', 'method' => 'runBackupProfile', 'form' => 'run_profile', @@ -67,20 +67,20 @@ final class MokoJoomBackupTask extends CMSPlugin implements SubscriberInterface $profileId = (int) ($params->profile_id ?? 1); // Load the backup engine from the component - $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/src/Engine/BackupEngine.php'; + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php'; if (!file_exists($engineFile)) { - $this->logTask('MokoJoomBackup component not installed — cannot run backup.'); + $this->logTask('MokoSuiteBackup component not installed — cannot run backup.'); return Status::KNOCKOUT; } // The autoloader should handle this via namespace, but ensure class is available - if (!class_exists('\\Joomla\\Component\\MokoJoomBackup\\Administrator\\Engine\\BackupEngine')) { + if (!class_exists('\\Joomla\\Component\\MokoSuiteBackup\\Administrator\\Engine\\BackupEngine')) { require_once $engineFile; } - $engine = new \Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine(); + $engine = new \Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine(); $result = $engine->run($profileId, 'Scheduled task backup', 'scheduled'); if ($result['success']) { diff --git a/source/packages/plg_task_mokojoombackup/src/index.html b/source/packages/plg_task_mokosuitebackup/src/Extension/index.html similarity index 100% rename from source/packages/plg_task_mokojoombackup/src/index.html rename to source/packages/plg_task_mokosuitebackup/src/Extension/index.html diff --git a/source/packages/plg_webservices_mokojoombackup/index.html b/source/packages/plg_task_mokosuitebackup/src/index.html similarity index 100% rename from source/packages/plg_webservices_mokojoombackup/index.html rename to source/packages/plg_task_mokosuitebackup/src/index.html diff --git a/source/packages/plg_webservices_mokojoombackup/language/en-GB/plg_webservices_mokojoombackup.sys.ini b/source/packages/plg_webservices_mokojoombackup/language/en-GB/plg_webservices_mokojoombackup.sys.ini deleted file mode 100644 index 575a5a7..0000000 --- a/source/packages/plg_webservices_mokojoombackup/language/en-GB/plg_webservices_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — WebServices Plugin system language file (en-GB) -PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoJoomBackup" -PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management." diff --git a/source/packages/plg_webservices_mokojoombackup/language/en-US/plg_webservices_mokojoombackup.sys.ini b/source/packages/plg_webservices_mokojoombackup/language/en-US/plg_webservices_mokojoombackup.sys.ini deleted file mode 100644 index 0d83f11..0000000 --- a/source/packages/plg_webservices_mokojoombackup/language/en-US/plg_webservices_mokojoombackup.sys.ini +++ /dev/null @@ -1,3 +0,0 @@ -; MokoJoomBackup — WebServices Plugin system language file (en-US) -PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoJoomBackup" -PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management." diff --git a/source/packages/plg_webservices_mokojoombackup/mokojoombackup.php b/source/packages/plg_webservices_mokojoombackup/mokojoombackup.php deleted file mode 100644 index d3162d7..0000000 --- a/source/packages/plg_webservices_mokojoombackup/mokojoombackup.php +++ /dev/null @@ -1,11 +0,0 @@ - - * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. - * @license GNU General Public License version 3 or later; see LICENSE - */ - -defined('_JEXEC') or die; diff --git a/source/packages/plg_webservices_mokojoombackup/services/provider.php b/source/packages/plg_webservices_mokojoombackup/services/provider.php deleted file mode 100644 index b96697f..0000000 --- a/source/packages/plg_webservices_mokojoombackup/services/provider.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. - * @license GNU General Public License version 3 or later; see LICENSE - */ - -defined('_JEXEC') or die; - -use Joomla\CMS\Extension\PluginInterface; -use Joomla\CMS\Factory; -use Joomla\CMS\Plugin\PluginHelper; -use Joomla\DI\Container; -use Joomla\DI\ServiceProviderInterface; -use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\WebServices\MokoJoomBackup\Extension\MokoJoomBackupWebServices; - -return new class () implements ServiceProviderInterface { - public function register(Container $container): void - { - $container->set( - PluginInterface::class, - function (Container $container) { - $plugin = new MokoJoomBackupWebServices( - $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('webservices', 'mokojoombackup') - ); - $plugin->setApplication(Factory::getApplication()); - - return $plugin; - } - ); - } -}; diff --git a/source/packages/plg_webservices_mokojoombackup/language/en-GB/index.html b/source/packages/plg_webservices_mokosuitebackup/index.html similarity index 100% rename from source/packages/plg_webservices_mokojoombackup/language/en-GB/index.html rename to source/packages/plg_webservices_mokosuitebackup/index.html diff --git a/source/packages/plg_webservices_mokojoombackup/language/en-US/index.html b/source/packages/plg_webservices_mokosuitebackup/language/en-GB/index.html similarity index 100% rename from source/packages/plg_webservices_mokojoombackup/language/en-US/index.html rename to source/packages/plg_webservices_mokosuitebackup/language/en-GB/index.html diff --git a/source/packages/plg_webservices_mokojoombackup/language/en-GB/plg_webservices_mokojoombackup.ini b/source/packages/plg_webservices_mokosuitebackup/language/en-GB/plg_webservices_mokosuitebackup.ini similarity index 53% rename from source/packages/plg_webservices_mokojoombackup/language/en-GB/plg_webservices_mokojoombackup.ini rename to source/packages/plg_webservices_mokosuitebackup/language/en-GB/plg_webservices_mokosuitebackup.ini index b712a7f..830309c 100644 --- a/source/packages/plg_webservices_mokojoombackup/language/en-GB/plg_webservices_mokojoombackup.ini +++ b/source/packages/plg_webservices_mokosuitebackup/language/en-GB/plg_webservices_mokosuitebackup.ini @@ -1,3 +1,3 @@ -; MokoJoomBackup — WebServices Plugin language file (en-GB) -PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoJoomBackup" +; MokoSuiteBackup — WebServices Plugin language file (en-GB) +PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoSuiteBackup" PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management. Provides endpoints to start, list, download, and delete backups." diff --git a/source/packages/plg_webservices_mokosuitebackup/language/en-GB/plg_webservices_mokosuitebackup.sys.ini b/source/packages/plg_webservices_mokosuitebackup/language/en-GB/plg_webservices_mokosuitebackup.sys.ini new file mode 100644 index 0000000..7b7c461 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitebackup/language/en-GB/plg_webservices_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — WebServices Plugin system language file (en-GB) +PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoSuiteBackup" +PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management." diff --git a/source/packages/plg_webservices_mokojoombackup/language/index.html b/source/packages/plg_webservices_mokosuitebackup/language/en-US/index.html similarity index 100% rename from source/packages/plg_webservices_mokojoombackup/language/index.html rename to source/packages/plg_webservices_mokosuitebackup/language/en-US/index.html diff --git a/source/packages/plg_webservices_mokojoombackup/language/en-US/plg_webservices_mokojoombackup.ini b/source/packages/plg_webservices_mokosuitebackup/language/en-US/plg_webservices_mokosuitebackup.ini similarity index 53% rename from source/packages/plg_webservices_mokojoombackup/language/en-US/plg_webservices_mokojoombackup.ini rename to source/packages/plg_webservices_mokosuitebackup/language/en-US/plg_webservices_mokosuitebackup.ini index 6fbd792..b4a0e10 100644 --- a/source/packages/plg_webservices_mokojoombackup/language/en-US/plg_webservices_mokojoombackup.ini +++ b/source/packages/plg_webservices_mokosuitebackup/language/en-US/plg_webservices_mokosuitebackup.ini @@ -1,3 +1,3 @@ -; MokoJoomBackup — WebServices Plugin language file (en-US) -PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoJoomBackup" +; MokoSuiteBackup — WebServices Plugin language file (en-US) +PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoSuiteBackup" PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management. Provides endpoints to start, list, download, and delete backups." diff --git a/source/packages/plg_webservices_mokosuitebackup/language/en-US/plg_webservices_mokosuitebackup.sys.ini b/source/packages/plg_webservices_mokosuitebackup/language/en-US/plg_webservices_mokosuitebackup.sys.ini new file mode 100644 index 0000000..84ea6b3 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitebackup/language/en-US/plg_webservices_mokosuitebackup.sys.ini @@ -0,0 +1,3 @@ +; MokoSuiteBackup — WebServices Plugin system language file (en-US) +PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoSuiteBackup" +PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management." diff --git a/source/packages/plg_webservices_mokojoombackup/services/index.html b/source/packages/plg_webservices_mokosuitebackup/language/index.html similarity index 100% rename from source/packages/plg_webservices_mokojoombackup/services/index.html rename to source/packages/plg_webservices_mokosuitebackup/language/index.html diff --git a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.php b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.php new file mode 100644 index 0000000..2954662 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; diff --git a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml new file mode 100644 index 0000000..ff28d7f --- /dev/null +++ b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml @@ -0,0 +1,31 @@ + + + + Web Services - MokoSuiteBackup + 01.20.00-rc + 2026-06-02 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION + + Joomla\Plugin\WebServices\MokoSuiteBackup + + + mokosuitebackup.php + services + src + + + + language/en-GB/plg_webservices_mokosuitebackup.ini + language/en-GB/plg_webservices_mokosuitebackup.sys.ini + + diff --git a/source/packages/plg_webservices_mokojoombackup/src/Extension/index.html b/source/packages/plg_webservices_mokosuitebackup/services/index.html similarity index 100% rename from source/packages/plg_webservices_mokojoombackup/src/Extension/index.html rename to source/packages/plg_webservices_mokosuitebackup/services/index.html diff --git a/source/packages/plg_webservices_mokosuitebackup/services/provider.php b/source/packages/plg_webservices_mokosuitebackup/services/provider.php new file mode 100644 index 0000000..18e5ea9 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitebackup/services/provider.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\WebServices\MokoSuiteBackup\Extension\MokoSuiteBackupWebServices; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoSuiteBackupWebServices( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('webservices', 'mokosuitebackup') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_webservices_mokojoombackup/src/Extension/MokoJoomBackupWebServices.php b/source/packages/plg_webservices_mokosuitebackup/src/Extension/MokoSuiteBackupWebServices.php similarity index 58% rename from source/packages/plg_webservices_mokojoombackup/src/Extension/MokoJoomBackupWebServices.php rename to source/packages/plg_webservices_mokosuitebackup/src/Extension/MokoSuiteBackupWebServices.php index 8a67e20..b56c0d9 100644 --- a/source/packages/plg_webservices_mokojoombackup/src/Extension/MokoJoomBackupWebServices.php +++ b/source/packages/plg_webservices_mokosuitebackup/src/Extension/MokoSuiteBackupWebServices.php @@ -1,23 +1,23 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE * - * REST API endpoints — wire-compatible with the mcp_mokojoombackup MCP server. + * REST API endpoints — wire-compatible with the mcp_mokosuitebackup MCP server. * * Akeeba-compatible routes: - * POST /api/index.php/v1/mokojoombackup/backup — Start backup - * GET /api/index.php/v1/mokojoombackup/backups — List records - * DELETE /api/index.php/v1/mokojoombackup/backup/:id — Delete record - * GET /api/index.php/v1/mokojoombackup/backup/:id/download — Download archive - * GET /api/index.php/v1/mokojoombackup/profiles — List profiles + * POST /api/index.php/v1/mokosuitebackup/backup — Start backup + * GET /api/index.php/v1/mokosuitebackup/backups — List records + * DELETE /api/index.php/v1/mokosuitebackup/backup/:id — Delete record + * GET /api/index.php/v1/mokosuitebackup/backup/:id/download — Download archive + * GET /api/index.php/v1/mokosuitebackup/profiles — List profiles */ -namespace Joomla\Plugin\WebServices\MokoJoomBackup\Extension; +namespace Joomla\Plugin\WebServices\MokoSuiteBackup\Extension; defined('_JEXEC') or die; @@ -27,7 +27,7 @@ use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; use Joomla\Router\Route; -final class MokoJoomBackupWebServices extends CMSPlugin implements SubscriberInterface +final class MokoSuiteBackupWebServices extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; @@ -44,18 +44,18 @@ final class MokoJoomBackupWebServices extends CMSPlugin implements SubscriberInt [$router] = array_values($event->getArguments()); $defaults = [ - 'component' => 'com_mokojoombackup', + 'component' => 'com_mokosuitebackup', 'public' => false, ]; // Standard CRUD for backup records - $router->createCRUDRoutes('v1/mokojoombackup/backups', 'backups', $defaults); + $router->createCRUDRoutes('v1/mokosuitebackup/backups', 'backups', $defaults); // Start a backup (POST) $router->addRoute( new Route( ['POST'], - 'v1/mokojoombackup/backup', + 'v1/mokosuitebackup/backup', 'backups.backup', [], $defaults @@ -66,7 +66,7 @@ final class MokoJoomBackupWebServices extends CMSPlugin implements SubscriberInt $router->addRoute( new Route( ['DELETE'], - 'v1/mokojoombackup/backup/:id', + 'v1/mokosuitebackup/backup/:id', 'backups.delete', ['id' => '(\d+)'], $defaults @@ -77,7 +77,7 @@ final class MokoJoomBackupWebServices extends CMSPlugin implements SubscriberInt $router->addRoute( new Route( ['GET'], - 'v1/mokojoombackup/backup/:id/download', + 'v1/mokosuitebackup/backup/:id/download', 'backups.download', ['id' => '(\d+)'], $defaults @@ -88,7 +88,7 @@ final class MokoJoomBackupWebServices extends CMSPlugin implements SubscriberInt $router->addRoute( new Route( ['GET'], - 'v1/mokojoombackup/profiles', + 'v1/mokosuitebackup/profiles', 'backups.profiles', [], $defaults diff --git a/source/packages/plg_webservices_mokojoombackup/src/index.html b/source/packages/plg_webservices_mokosuitebackup/src/Extension/index.html similarity index 100% rename from source/packages/plg_webservices_mokojoombackup/src/index.html rename to source/packages/plg_webservices_mokosuitebackup/src/Extension/index.html diff --git a/source/packages/plg_webservices_mokosuitebackup/src/index.html b/source/packages/plg_webservices_mokosuitebackup/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_webservices_mokosuitebackup/src/index.html @@ -0,0 +1 @@ + diff --git a/source/pkg_mokojoombackup.xml b/source/pkg_mokojoombackup.xml deleted file mode 100644 index 9b63361..0000000 --- a/source/pkg_mokojoombackup.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - Package - MokoJoomBackup - mokojoombackup - 01.08.00 - 2026-06-02 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GPL-3.0-or-later - PKG_MOKOJOOMBACKUP_DESCRIPTION - - script.php - - - com_mokojoombackup.zip - plg_system_mokojoombackup.zip - plg_task_mokojoombackup.zip - plg_quickicon_mokojoombackup.zip - plg_webservices_mokojoombackup.zip - plg_console_mokojoombackup.zip - plg_content_mokojoombackup.zip - plg_actionlog_mokojoombackup.zip - - - - language/en-GB/pkg_mokojoombackup.sys.ini - - - - https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/updates.xml - - - true - diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml new file mode 100644 index 0000000..5188f17 --- /dev/null +++ b/source/pkg_mokosuitebackup.xml @@ -0,0 +1,42 @@ + + + + Package - MokoSuiteBackup + mokosuitebackup + 01.20.00-rc + 2026-06-02 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PKG_MOKOJOOMBACKUP_DESCRIPTION + + script.php + + + com_mokosuitebackup.zip + plg_system_mokosuitebackup.zip + plg_task_mokosuitebackup.zip + plg_quickicon_mokosuitebackup.zip + plg_webservices_mokosuitebackup.zip + plg_console_mokosuitebackup.zip + plg_content_mokosuitebackup.zip + plg_actionlog_mokosuitebackup.zip + + + + language/en-GB/pkg_mokosuitebackup.sys.ini + + + + https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/updates.xml + + + true + diff --git a/source/script.php b/source/script.php index a4fdb3f..f194511 100644 --- a/source/script.php +++ b/source/script.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -14,7 +14,7 @@ use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; -class Pkg_MokoJoomBackupInstallerScript +class Pkg_MokoSuiteBackupInstallerScript { /** * Minimum Joomla version required @@ -40,6 +40,15 @@ class Pkg_MokoJoomBackupInstallerScript */ public function preflight(string $type, InstallerAdapter $parent): bool { + if (version_compare(JVERSION, $this->minimumJoomla, '<')) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('PKG_MOKOJOOMBACKUP_JOOMLA_VERSION_ERROR', $this->minimumJoomla), + 'error' + ); + + return false; + } + if (version_compare(PHP_VERSION, $this->minimumPhp, '<')) { Factory::getApplication()->enqueueMessage( Text::sprintf('PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR', $this->minimumPhp), @@ -57,14 +66,6 @@ class Pkg_MokoJoomBackupInstallerScript return true; } - /** - * Called after install/update. - * - * @param string $type Action type - * @param InstallerAdapter $parent Installer adapter - * - * @return void - */ /** * Called before install/update to preserve the download key. * @@ -91,7 +92,7 @@ class Pkg_MokoJoomBackupInstallerScript $db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') ) - ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokojoombackup')) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitebackup')) ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) ->setLimit(1); $db->setQuery($query); @@ -100,125 +101,53 @@ class Pkg_MokoJoomBackupInstallerScript if (!empty($key)) { $this->savedDownloadKey = $key; } - } catch (\Throwable $e) { - error_log('MokoJoomBackup: Could not save download key: ' . $e->getMessage()); + } 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' + ); } } + /** + * Called after install/update/uninstall. + * + * @param string $type Action type (install, update, uninstall) + * @param InstallerAdapter $parent Installer adapter + * + * @return void + */ public function postflight(string $type, InstallerAdapter $parent): void { + if ($type === 'uninstall') { + return; + } + // Restore download key if it was saved before update if ($this->savedDownloadKey !== null) { $this->restoreDownloadKey(); } if ($type === 'install') { - // Enable the system plugin automatically on fresh install - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup')); + // Enable all bundled plugins on fresh install + $this->enableBundledPlugins(); - $db->setQuery($query); - $db->execute(); + // Create default backup directory in site root + $this->createBackupDirectory(); - // Enable the quickicon plugin automatically - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('quickicon')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup')); - - $db->setQuery($query); - $db->execute(); - - // Enable the task plugin automatically - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('task')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup')); - - $db->setQuery($query); - $db->execute(); - - // Enable the webservices plugin automatically - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('webservices')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup')); - - $db->setQuery($query); - $db->execute(); - - // Enable the console plugin automatically - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('console')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup')); - - $db->setQuery($query); - $db->execute(); - - // Enable the content plugin automatically - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('content')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup')); - - $db->setQuery($query); - $db->execute(); - - // Enable the actionlog plugin automatically - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup')); - - $db->setQuery($query); - $db->execute(); - - // Create and protect default backup directory - $backupDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups'; - - if (!is_dir($backupDir)) { - mkdir($backupDir, 0755, true); - } - - if (is_dir($backupDir)) { - $htaccess = $backupDir . '/.htaccess'; - - if (!is_file($htaccess)) { - file_put_contents($htaccess, "# Apache 2.4+\n\n Require all denied\n\n# Apache 2.2\n\n Order deny,allow\n Deny from all\n\n"); - } - - $index = $backupDir . '/index.html'; - - if (!is_file($index)) { - file_put_contents($index, ''); - } - } - - // Create default scheduled task — every 30 days, profile 1 + // Create default scheduled task for backup automation $this->createDefaultScheduledTask(); } - if ($type === 'uninstall') { - return; - } + // Ensure submenu items exist and are up to date + // (Joomla may not add new submenu entries or update params on upgrades) + $this->ensureSubmenuItems(); + + // Fix package client_id — packages must be client_id=0 (site) for + // Joomla's updater to match the site in updates.xml + $this->fixPackageClientId(); // Sync submenu icons in #__menu (Joomla doesn't update icons on upgrades) $this->syncMenuIcons(); @@ -226,12 +155,12 @@ class Pkg_MokoJoomBackupInstallerScript // Warn if no license key configured $this->warnMissingLicenseKey(); - // Warn if any profile still uses the default backup directory - $this->warnDefaultBackupDir(); + // Migrate profiles with old default backup_dir values to [DEFAULT_DIR] placeholder + $this->migrateDefaultBackupDir(); // Remind user to review backup profile settings if ($type === 'install') { - $profileUrl = Route::_('index.php?option=com_mokojoombackup&view=profiles'); + $profileUrl = Route::_('index.php?option=com_mokosuitebackup&view=profiles'); Factory::getApplication()->enqueueMessage( 'Review Your Backup Settings — ' @@ -243,33 +172,119 @@ class Pkg_MokoJoomBackupInstallerScript } } - private function warnDefaultBackupDir(): void + private function enableBundledPlugins(): void + { + $folders = ['system', 'quickicon', 'task', 'webservices', 'console', 'content', 'actionlog']; + $db = Factory::getDbo(); + + foreach ($folders as $folder) { + try { + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote($folder)) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup')); + $db->setQuery($query); + $db->execute(); + } catch (\Exception $e) { + error_log('MokoSuiteBackup: Failed to enable ' . $folder . ' plugin: ' . $e->getMessage()); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup: Could not enable the ' . $folder . ' plugin. ' + . 'Please enable it manually in Extensions → Plugins.', + 'warning' + ); + } + } + } + + private function createBackupDirectory(): void + { + $backupDir = JPATH_ROOT . '/backups'; + + if (is_dir($backupDir)) { + return; + } + + if (!mkdir($backupDir, 0755, true)) { + error_log('MokoSuiteBackup: Failed to create default backup directory: ' . $backupDir); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup could not create the default backup directory at ' + . htmlspecialchars($backupDir) . '. ' + . 'Please create it manually and ensure the web server has write permissions.', + 'warning' + ); + + return; + } + + // Protect directory from direct web access + $htaccess = $backupDir . '/.htaccess'; + + if (!file_exists($htaccess)) { + if (file_put_contents($htaccess, "Order Deny,Allow\nDeny from all\n") === false) { + error_log('MokoSuiteBackup: Failed to write .htaccess to ' . $backupDir); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup created the backup directory but could not write an .htaccess file to protect it. ' + . 'Please manually create ' . htmlspecialchars($htaccess) . ' with "Deny from all" to prevent direct web access.', + 'warning' + ); + } + } + + $indexHtml = $backupDir . '/index.html'; + + if (!file_exists($indexHtml)) { + if (file_put_contents($indexHtml, '') === false) { + error_log('MokoSuiteBackup: Failed to write index.html to ' . $backupDir); + } + } + } + + private function migrateDefaultBackupDir(): void { try { $db = Factory::getDbo(); + $oldDefaults = [ + 'administrator/components/com_mokosuitebackup/backups', + 'administrator/components/com_mokojoombackup/backups', + './backups', + 'backups', + ]; $query = $db->getQuery(true) ->select('COUNT(*)') - ->from($db->quoteName('#__mokojoombackup_profiles')) + ->from($db->quoteName('#__mokosuitebackup_profiles')) ->where($db->quoteName('published') . ' = 1') - ->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote('administrator/components/com_mokojoombackup/backups') - . ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]') - . ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('') + ->where('(' . $db->quoteName('backup_dir') . ' IN (' + . implode(',', array_map([$db, 'quote'], $oldDefaults)) + . ') OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('') . ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)'); $db->setQuery($query); if ((int) $db->loadResult() > 0) { - $profileUrl = Route::_('index.php?option=com_mokojoombackup&view=profiles'); + $update = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitebackup_profiles')) + ->set($db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]')) + ->where('(' . $db->quoteName('backup_dir') . ' IN (' + . implode(',', array_map([$db, 'quote'], $oldDefaults)) + . ') OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('') + . ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)'); + $db->setQuery($update); + $db->execute(); - Factory::getApplication()->enqueueMessage( - 'Backup Directory Warning — ' - . 'One or more profiles store backups in the default directory inside the web root. ' - . 'For better security, configure a backup directory outside the web root. ' - . 'Edit Profiles', - 'warning' - ); + $migrated = $db->getAffectedRows(); + + if ($migrated > 0) { + error_log('MokoSuiteBackup: Migrated ' . $migrated . ' profile(s) from legacy backup_dir to [DEFAULT_DIR]'); + } } - } catch (\Throwable $e) { - error_log('MokoJoomBackup: warnDefaultBackupDir() failed: ' . $e->getMessage()); + } catch (\Exception $e) { + error_log('MokoSuiteBackup: migrateDefaultBackupDir() failed: ' . $e->getMessage()); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup could not automatically migrate backup directory settings in your profiles. ' + . 'Please review your backup profiles and ensure the backup directory is set correctly.', + 'warning' + ); } } @@ -278,11 +293,11 @@ class Pkg_MokoJoomBackupInstallerScript try { $db = Factory::getDbo(); - // Check if a MokoJoomBackup task already exists + // Check if a MokoSuiteBackup task already exists $query = $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__scheduler_tasks')) - ->where($db->quoteName('type') . ' = ' . $db->quote('mokojoombackup.run_profile')); + ->where($db->quoteName('type') . ' = ' . $db->quote('mokosuitebackup.run_profile')); $db->setQuery($query); if ((int) $db->loadResult() > 0) { @@ -292,8 +307,8 @@ class Pkg_MokoJoomBackupInstallerScript $now = date('Y-m-d H:i:s'); $task = (object) [ - 'title' => 'MokoJoomBackup — Monthly Full Backup', - 'type' => 'mokojoombackup.run_profile', + 'title' => 'MokoSuiteBackup — Monthly Full Backup', + 'type' => 'mokosuitebackup.run_profile', 'execution_rules' => json_encode([ 'rule-type' => 'interval-days', 'interval-days' => '30', @@ -323,13 +338,169 @@ class Pkg_MokoJoomBackupInstallerScript 'cli_exclusive' => 0, 'note' => '', 'created' => $now, - 'created_by' => Factory::getApplication()->getIdentity()->id ?? 0, + 'created_by' => Factory::getApplication()->getIdentity()?->id ?? 0, 'next_execution' => date('Y-m-d 03:00:00', strtotime('+1 day')), ]; $db->insertObject('#__scheduler_tasks', $task); - } catch (\Throwable $e) { - error_log('MokoJoomBackup: createDefaultScheduledTask() failed: ' . $e->getMessage()); + } catch (\Exception $e) { + error_log('MokoSuiteBackup: createDefaultScheduledTask() failed: ' . $e->getMessage()); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup could not create the default scheduled backup task. ' + . 'Please create a scheduled task manually in System → Scheduled Tasks to enable automated backups.', + 'warning' + ); + } + } + + /** + * Ensure admin submenu items exist in #__menu. + * + * On updates Joomla may not add new submenu entries or update params, + * so we manually create missing items using MenuTable for correct + * nested set positioning (lft/rgt values). + */ + private function ensureSubmenuItems(): void + { + $submenus = [ + [ + 'link' => 'index.php?option=com_mokosuitebackup&view=dashboard', + 'title' => 'COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD', + 'img' => 'class:home', + 'menu_icon' => 'icon-home', + ], + [ + 'link' => 'index.php?option=com_mokosuitebackup&view=backups', + 'title' => 'COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS', + 'img' => 'class:database', + 'menu_icon' => 'icon-database', + ], + [ + 'link' => 'index.php?option=com_mokosuitebackup&view=profiles', + 'title' => 'COM_MOKOJOOMBACKUP_SUBMENU_PROFILES', + 'img' => 'class:cog', + 'menu_icon' => 'icon-cog', + ], + ]; + + try { + $db = Factory::getDbo(); + + // Find the parent menu item for our component + $query = $db->getQuery(true) + ->select([$db->quoteName('id'), $db->quoteName('menutype')]) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('client_id') . ' = 1') + ->where($db->quoteName('level') . ' = 1') + ->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup%')) + ->setLimit(1); + $db->setQuery($query); + $parent = $db->loadObject(); + + if (!$parent) { + error_log('MokoSuiteBackup: ensureSubmenuItems() — parent menu item not found'); + return; + } + + // Get the component extension_id + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitebackup')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->setLimit(1); + $db->setQuery($query); + $componentId = (int) $db->loadResult(); + + if (!$componentId) { + error_log('MokoSuiteBackup: ensureSubmenuItems() — component extension_id not found'); + return; + } + + foreach ($submenus as $submenu) { + // Check if this submenu item already exists + $query = $db->getQuery(true) + ->select([$db->quoteName('id'), $db->quoteName('params')]) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('client_id') . ' = 1') + ->where($db->quoteName('link') . ' = ' . $db->quote($submenu['link'])) + ->setLimit(1); + $db->setQuery($query); + $existing = $db->loadObject(); + + if ($existing) { + // Merge menu_icon into existing params to preserve other settings + $existingParams = json_decode($existing->params ?? '{}', true) ?: []; + $existingParams['menu_icon'] = $submenu['menu_icon']; + $mergedParams = json_encode($existingParams); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('params') . ' = ' . $db->quote($mergedParams)) + ->where($db->quoteName('id') . ' = ' . (int) $existing->id); + $db->setQuery($query); + $db->execute(); + continue; + } + + // Use Joomla's MenuTable to create the item properly + $table = Factory::getApplication() + ->bootComponent('com_menus') + ->getMVCFactory() + ->createTable('Menu', 'Administrator'); + + $params = json_encode(['menu_icon' => $submenu['menu_icon']]); + + $table->menutype = $parent->menutype; + $table->title = $submenu['title']; + $table->alias = strtolower(str_replace(' ', '-', $submenu['title'])); + $table->link = $submenu['link']; + $table->type = 'component'; + $table->published = 1; + $table->parent_id = $parent->id; + $table->level = 2; + $table->component_id = $componentId; + $table->client_id = 1; + $table->img = $submenu['img']; + $table->params = $params; + $table->language = '*'; + $table->access = 1; + + $table->setLocation($parent->id, 'last-child'); + + if (!$table->check() || !$table->store()) { + error_log('MokoSuiteBackup: Failed to create submenu "' . $submenu['title'] . '": ' . $table->getError()); + } + } + } catch (\Exception $e) { + error_log('MokoSuiteBackup: ensureSubmenuItems() failed: ' . $e->getMessage()); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup could not create or update sidebar menu items. ' + . 'If submenu entries are missing, try reinstalling the component.', + 'warning' + ); + } + } + + private function fixPackageClientId(): void + { + try { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('client_id') . ' = 0') + ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokosuitebackup')) + ->where($db->quoteName('type') . ' = ' . $db->quote('package')); + $db->setQuery($query); + $db->execute(); + } catch (\Exception $e) { + error_log('MokoSuiteBackup: fixPackageClientId() failed: ' . $e->getMessage()); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup could not correct the package registration. ' + . 'Automatic updates may not appear. If you do not see update notifications, ' + . 'please reinstall the package.', + 'warning' + ); } } @@ -349,22 +520,25 @@ class Pkg_MokoJoomBackupInstallerScript ->update($db->quoteName('#__menu')) ->set($db->quoteName('img') . ' = ' . $db->quote($icon)) ->where($db->quoteName('client_id') . ' = 1') - ->where($db->quoteName('link') . ' LIKE ' . $db->quote('%com_mokojoombackup%' . $linkFragment . '%')); + ->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup%' . $linkFragment . '%')); $db->setQuery($query); $db->execute(); } - // Set top-level component menu icon $query = $db->getQuery(true) ->update($db->quoteName('#__menu')) ->set($db->quoteName('img') . ' = ' . $db->quote('class:archive')) ->where($db->quoteName('client_id') . ' = 1') - ->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokojoombackup')) + ->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup')) ->where($db->quoteName('level') . ' = 1'); $db->setQuery($query); $db->execute(); - } catch (\Throwable $e) { - error_log('MokoJoomBackup: syncMenuIcons() failed: ' . $e->getMessage()); + } catch (\Exception $e) { + error_log('MokoSuiteBackup: syncMenuIcons() failed: ' . $e->getMessage()); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup could not update sidebar menu icons. This is cosmetic and does not affect functionality.', + 'notice' + ); } } @@ -388,7 +562,7 @@ class Pkg_MokoJoomBackupInstallerScript $db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') ) - ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokojoombackup')) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitebackup')) ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) ->setLimit(1); $db->setQuery($query); @@ -402,8 +576,13 @@ class Pkg_MokoJoomBackupInstallerScript $db->setQuery($query); $db->execute(); } - } catch (\Throwable $e) { - error_log('MokoJoomBackup: Could not restore download key: ' . $e->getMessage()); + } catch (\Exception $e) { + error_log('MokoSuiteBackup: Could not restore download key: ' . $e->getMessage()); + 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.', + 'warning' + ); } } @@ -416,7 +595,7 @@ class Pkg_MokoJoomBackupInstallerScript $db->getQuery(true) ->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')]) ->from($db->quoteName('#__update_sites')) - ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoJoomBackup%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoJoomBackup%') . ')') + ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteBackup%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteBackup%') . ')') ->setLimit(1) ); $site = $db->loadObject(); @@ -439,8 +618,13 @@ class Pkg_MokoJoomBackupInstallerScript 'warning' ); } - catch (\Throwable $e) { - error_log('MokoJoomBackup: License key check failed: ' . $e->getMessage()); + catch (\Exception $e) { + error_log('MokoSuiteBackup: License key check failed: ' . $e->getMessage()); + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup could not verify your license key status. ' + . 'Please check System → Update Sites to ensure a valid license key is configured.', + 'warning' + ); } } } diff --git a/src/Extension/MokoSuiteBackupWebServices.php b/src/Extension/MokoSuiteBackupWebServices.php new file mode 100644 index 0000000..b56c0d9 --- /dev/null +++ b/src/Extension/MokoSuiteBackupWebServices.php @@ -0,0 +1,98 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * + * REST API endpoints — wire-compatible with the mcp_mokosuitebackup MCP server. + * + * Akeeba-compatible routes: + * POST /api/index.php/v1/mokosuitebackup/backup — Start backup + * GET /api/index.php/v1/mokosuitebackup/backups — List records + * DELETE /api/index.php/v1/mokosuitebackup/backup/:id — Delete record + * GET /api/index.php/v1/mokosuitebackup/backup/:id/download — Download archive + * GET /api/index.php/v1/mokosuitebackup/profiles — List profiles + */ + +namespace Joomla\Plugin\WebServices\MokoSuiteBackup\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Router\ApiRouter; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; +use Joomla\Router\Route; + +final class MokoSuiteBackupWebServices extends CMSPlugin implements SubscriberInterface +{ + protected $autoloadLanguage = true; + + public static function getSubscribedEvents(): array + { + return [ + 'onBeforeApiRoute' => 'onBeforeApiRoute', + ]; + } + + public function onBeforeApiRoute(Event $event): void + { + /** @var ApiRouter $router */ + [$router] = array_values($event->getArguments()); + + $defaults = [ + 'component' => 'com_mokosuitebackup', + 'public' => false, + ]; + + // Standard CRUD for backup records + $router->createCRUDRoutes('v1/mokosuitebackup/backups', 'backups', $defaults); + + // Start a backup (POST) + $router->addRoute( + new Route( + ['POST'], + 'v1/mokosuitebackup/backup', + 'backups.backup', + [], + $defaults + ) + ); + + // Delete a backup (DELETE) + $router->addRoute( + new Route( + ['DELETE'], + 'v1/mokosuitebackup/backup/:id', + 'backups.delete', + ['id' => '(\d+)'], + $defaults + ) + ); + + // Download a backup archive (GET) + $router->addRoute( + new Route( + ['GET'], + 'v1/mokosuitebackup/backup/:id/download', + 'backups.download', + ['id' => '(\d+)'], + $defaults + ) + ); + + // List backup profiles (GET) + $router->addRoute( + new Route( + ['GET'], + 'v1/mokosuitebackup/profiles', + 'backups.profiles', + [], + $defaults + ) + ); + } +} diff --git a/src/Extension/index.html b/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/src/index.html @@ -0,0 +1 @@ +