diff --git a/.mokogitea/ISSUE_TEMPLATE/waas_site_issue.md b/.mokogitea/ISSUE_TEMPLATE/waas_site_issue.md new file mode 100644 index 0000000..4a0c89b --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/waas_site_issue.md @@ -0,0 +1,77 @@ +--- +name: WaaS Client Site Issue +about: Report an issue with a WaaS client site (branding, deployment, media sync) +title: '[WAAS] ' +labels: 'waas, client-site' +assignees: '' + +--- + +## Site Issue Type +- [ ] Branding / CSS not applying +- [ ] Deployment failure +- [ ] Media sync issue +- [ ] Template override not working +- [ ] Module positioning issue +- [ ] Mobile / responsive layout +- [ ] Performance issue + +## Client Site +- **Client Org**: [e.g., ClarksvilleFurs] +- **Repo**: [e.g., client-waas-clarksvillefurs] +- **Environment**: [Dev / Production] +- **Site URL**: [dev or production URL — omit if private] + +## Issue Description +Describe the issue clearly. + +## Steps to Reproduce +1. Visit [page URL] +2. Look at [element] +3. See error + +## Expected Behavior +What the site should look like or how it should behave. + +## Actual Behavior +What is happening instead. + +## Screenshots +Attach screenshots showing the issue (desktop and mobile if relevant). + +## Deployment Status +- **Last deploy**: [date or "unknown"] +- **Deploy workflow**: [succeeded / failed / not run] +- **Branch**: [dev / main] + +## Media Sync +- [ ] Images missing after sync +- [ ] Sync direction: [dev-to-prod / prod-to-dev / bidirectional] +- [ ] Last sync: [date] + +## Template Details +- **Joomla Version**: [e.g., 5.x] +- **Template Name**: [e.g., clienttemplate] +- **MokoWaaS Plugin**: [Active / Inactive] +- **MokoOnyx Admin**: [Active / Inactive] + +## CSS Custom Properties +If branding issue, list the relevant CSS variables: +```css +:root { + --client-primary: #...; + --client-secondary: #...; +} +``` + +## Browser / Device +- **Browser**: [e.g., Chrome 120, Safari 17] +- **Device**: [Desktop / Tablet / Mobile] +- **Screen Width**: [e.g., 1920px, 768px, 375px] + +## Checklist +- [ ] I have cleared Joomla cache +- [ ] I have hard-refreshed the browser (Ctrl+Shift+R) +- [ ] I have checked the deploy workflow completed +- [ ] I have verified the change is on the correct branch +- [ ] No credentials or PII are included in this issue diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 46ce4b2..787b7a0 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -79,131 +79,37 @@ jobs: - name: Detect platform id: platform run: | - # Read platform from manifest.xml element; fallback to generic - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*//p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - echo "Platform detected: ${PLATFORM}" - # For packages: prefer pkg_*.xml in src/; fallback to any manifest - MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true) echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" - # -- STEP 1: Read version ----------------------------------------------- - - name: "Step 1: Read version from README.md" + - name: "Step 1: Read version" id: version run: | - VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path . 2>/dev/null) + VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .) if [ -z "$VERSION" ]; then - echo "No VERSION in README.md — skipping release" + echo "::error::No VERSION in README.md" echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi - # Derive major.minor for branch naming (patches update existing branch) - MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') - PATCH=$(echo "$VERSION" | awk -F. '{print $3}') - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" - echo "minor=$MINOR" >> "$GITHUB_OUTPUT" - echo "major=$MAJOR" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - echo "stability=stable" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" - if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then - echo "is_minor=true" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (first release for this minor — full pipeline)" - else - echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (patch — platform version + badges only)" - fi - - # -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------ - - name: "Step 1b: Bump minor version for stable release" - if: steps.version.outputs.skip != 'true' - id: bump - run: | - CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) - [ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; } - - MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1))) - MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2))) - - # Minor bump, reset patch. Rollover if minor > 99 - MINOR=$((MINOR + 1)) - if [ $MINOR -gt 99 ]; then - MINOR=0 - MAJOR=$((MAJOR + 1)) - fi - - VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR) - TODAY=$(date +%Y-%m-%d) - - echo "Stable bump: ${CURRENT} → ${VERSION} (minor)" - - # Update README.md - sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md - - # Update platform-specific manifest - PLATFORM="${{ steps.platform.outputs.platform }}" - MANIFEST="${{ steps.platform.outputs.manifest }}" - MOD_FILE="${{ steps.platform.outputs.mod_file }}" - case "$PLATFORM" in - joomla) - if [ -n "$MANIFEST" ]; then - MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) - [ -n "$MANIFEST_VER" ] && sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" - sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" - fi - # For packages: also bump version in all sub-extension manifests - if [ -d "src/packages" ]; then - for SUB_MANIFEST in $(find src/packages -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null); do - SUB_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$SUB_MANIFEST" | head -1) - if [ -n "$SUB_VER" ]; then - sed -i "s|${SUB_VER}|${VERSION}|" "$SUB_MANIFEST" - sed -i "s|[^<]*|${TODAY}|" "$SUB_MANIFEST" - echo " Bumped sub-extension: $(basename $SUB_MANIFEST) ${SUB_VER} → ${VERSION}" - fi - done - fi - ;; - dolibarr) - if [ -n "$MOD_FILE" ]; then - sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE" - fi - echo "${VERSION}" > update.txt - ;; - *) ;; - esac - - # Promote [Unreleased] section in CHANGELOG.md to new version - if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then - sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md - sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md - sed -i "2i ## [Unreleased]" CHANGELOG.md - sed -i "3i \\ " CHANGELOG.md - echo "CHANGELOG promoted to [${VERSION}]" - fi - - # Commit and push - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" - git push origin HEAD:main 2>&1 - } - - # Override version output for rest of pipeline + MAJOR=$(echo "$VERSION" | cut -d. -f1) echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT" + echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + + - name: "Step 1b: Bump version" + id: bump + if: steps.version.outputs.skip != 'true' + run: | + MOKO_API="/tmp/moko-platform-api/cli" + BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor) + VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true) + [ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .) + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Bumped to: ${VERSION}" - name: Check if already released if: steps.version.outputs.skip != 'true' @@ -353,166 +259,22 @@ jobs: # -- STEP 4: Update version badges ---------------------------------------- - name: "Step 4: Update version badges" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' + if: steps.version.outputs.skip != 'true' run: | VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do - if grep -q '\[VERSION:' "$f" 2>/dev/null; then - sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" - fi - done + php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true - # -- STEP 5: Write updates.xml (Joomla update server) --------------------- - name: "Step 5: Write update stream" - id: updates if: >- steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' + steps.platform.outputs.platform == 'joomla' run: | VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - REPO="${{ github.repository }}" + php /tmp/moko-platform-api/cli/updates_xml_build.php \ + --path . --version "${VERSION}" --stability stable \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + --github-output - # -- Parse extension metadata from XML manifest ---------------- - MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 2 -name "*.xml" ! -path "*/packages/*" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - # Extract fields using sed (portable — no grep -P) - EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) - EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) - EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) - PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) - - # If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini - if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then - INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) - [ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) - [ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME" - fi - - # Fallbacks - [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" - [ -z "$EXT_TYPE" ] && EXT_TYPE="component" - - # Derive element if not in manifest: - # 1. plugin="xxx" attribute (plugins) - # 2. module="xxx" attribute (modules) - # 3. XML filename (components, packages) - # 4. Repo name fallback (templates, anything else) - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - fi - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - fi - if [ -z "$EXT_ELEMENT" ]; then - FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - # If filename is generic (templateDetails, manifest), use repo name - case "$FNAME" in - templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; - *) EXT_ELEMENT="$FNAME" ;; - esac - fi - # Final fallback - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - - # Save for Steps 7, 8, 8b - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT" - echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT" - - # Build client tag: plugins and frontend modules need site - CLIENT_TAG="" - if [ -n "$EXT_CLIENT" ]; then - CLIENT_TAG="${EXT_CLIENT}" - elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then - CLIENT_TAG="site" - fi - - # Build folder tag for plugins (required for Joomla to match the update) - FOLDER_TAG="" - if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then - FOLDER_TAG="${EXT_FOLDER}" - fi - - # Build targetplatform (fallback to Joomla 5 if not in manifest) - if [ -z "$TARGET_PLATFORM" ]; then - TARGET_PLATFORM=$(printf '' "/") - fi - - # Build php_minimum tag - PHP_TAG="" - if [ -n "$PHP_MINIMUM" ]; then - PHP_TAG="${PHP_MINIMUM}" - fi - - # Build TYPE_PREFIX for download URL - TYPE_PREFIX="" - case "${EXT_TYPE}" in - plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; - module) TYPE_PREFIX="mod_" ;; - component) TYPE_PREFIX="com_" ;; - template) TYPE_PREFIX="tpl_" ;; - library) TYPE_PREFIX="lib_" ;; - package) TYPE_PREFIX="pkg_" ;; - esac - - DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" - INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable" - - # -- Build update entry for a given stability tag - build_entry() { - local TAG_NAME="$1" - printf '%s\n' ' ' - printf '%s\n' " ${EXT_NAME}" - printf '%s\n' " ${EXT_NAME} update" - printf '%s\n' " ${EXT_ELEMENT}" - printf '%s\n' " ${EXT_TYPE}" - printf '%s\n' " ${VERSION}" - [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" - [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" - printf '%s\n' " ${TAG_NAME}" - printf '%s\n' " ${INFO_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${DOWNLOAD_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${TARGET_PLATFORM}" - [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" - printf '%s\n' ' Moko Consulting' - printf '%s\n' ' https://mokoconsulting.tech' - printf '%s\n' ' ' - } - - # -- Write updates.xml with cascading channels - # Stable release updates ALL channels (development, alpha, beta, rc, stable) - { - printf '%s\n' "" - printf '%s\n' "" - printf '%s\n' "" - printf '%s\n' '' - build_entry "development" - build_entry "alpha" - build_entry "beta" - build_entry "rc" - build_entry "stable" - printf '%s\n' '' - } > updates.xml - - echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY - - # -- Commit all changes --------------------------------------------------- - name: Commit release changes if: >- steps.version.outputs.skip != 'true' && @@ -633,8 +395,7 @@ jobs: fi # Find extension element name from manifest - MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 2 -name "*.xml" ! -path "*/packages/*" -exec grep -l '/dev/null | head -1 || true) + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) [ -z "$MANIFEST" ] && exit 0 # Reuse element from Step 5, with same fallback chain @@ -663,44 +424,19 @@ jobs: # -- Build install packages from src/ ---------------------------- SOURCE_DIR="src" [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; } + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; } - EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" - - if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then - echo "=== Building Joomla PACKAGE (multi-extension) ===" - PKG_STAGE=$(mktemp -d) - - # ZIP each sub-extension - for ext_dir in "${SOURCE_DIR}"/packages/*/; do - [ ! -d "$ext_dir" ] && continue - SUB_NAME=$(basename "$ext_dir") - echo " Packaging sub-extension: ${SUB_NAME}" - (cd "$ext_dir" && zip -r "${PKG_STAGE}/${SUB_NAME}.zip" . -x $EXCLUDES) - done - - # Copy package-level files (manifest, script, etc.) - for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do - [ -f "$f" ] && cp "$f" "${PKG_STAGE}/" - done - - # Create ZIP and tar.gz from staged package - (cd "$PKG_STAGE" && zip -r "/tmp/${ZIP_NAME}" .) - tar -czf "/tmp/${TAR_NAME}" -C "$PKG_STAGE" . - - rm -rf "$PKG_STAGE" - echo "Package contents built with sub-extension ZIPs" - else - # Standard extension: flat ZIP from src/ - cd "$SOURCE_DIR" - zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES - cd .. - - tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ - --exclude='.ftpignore' --exclude='sftp-config*' \ - --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + # ZIP package (type-aware via moko-platform PHP API) + php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp + # Match the expected ZIP_NAME for upload + BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true) + if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then + mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}" fi + # tar.gz package (flat source archive) + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown") TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown") @@ -952,33 +688,14 @@ jobs: # -- Clean up lesser pre-releases (cascade) --------------------------------- # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev - name: "Delete lesser pre-release channels" - if: steps.version.outputs.skip != 'true' continue-on-error: true run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" + php /tmp/moko-platform-api/cli/release_cascade.php \ + --stability stable \ + --token "${{ secrets.GA_TOKEN }}" \ + --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + --gitea-url "${GITEA_URL}" 2>/dev/null || true - # Stable deletes all pre-release channels - TAGS_TO_DELETE="development alpha beta release-candidate" - - DELETED=0 - for TAG in $TAGS_TO_DELETE; do - RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/tags/${TAG}" 2>/dev/null || true - echo "Deleted: ${TAG} (id: ${RELEASE_ID})" - DELETED=$((DELETED + 1)) - fi - done - echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY - - # -- STEP 11: Reset dev branch from main ------------------------------------ - name: "Step 11: Delete and recreate dev branch from main" if: steps.version.outputs.skip != 'true' continue-on-error: true diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index 4dbb135..23b11a2 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# INGROUP: moko-platform.Maintenance +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/cascade-dev.yml.template # VERSION: 02.00.00 # BRIEF: Forward-merge main → all open branches after every push to main diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml index 3a81856..a890001 100644 --- a/.mokogitea/workflows/cleanup.yml +++ b/.mokogitea/workflows/cleanup.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# INGROUP: moko-platform.Maintenance +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.gitea/workflows/cleanup.yml # VERSION: 01.00.00 # BRIEF: Scheduled cleanup — delete merged branches and old workflow runs diff --git a/.mokogitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml index bb133ed..6429460 100644 --- a/.mokogitea/workflows/deploy-manual.yml +++ b/.mokogitea/workflows/deploy-manual.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Deploy -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API +# INGROUP: moko-platform.Deploy +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /templates/workflows/joomla/deploy-manual.yml.template # VERSION: 04.07.00 # BRIEF: Manual SFTP deploy to dev server for Joomla repos @@ -40,7 +40,7 @@ jobs: run: | php -v && composer --version - - name: Setup MokoStandards tools + - name: Setup moko-platform tools env: GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} @@ -48,10 +48,10 @@ jobs: COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' run: | git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ - /tmp/mokostandards-api 2>/dev/null || true - if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then - cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api 2>/dev/null || true + if [ -d "/tmp/moko-platform-api" ] && [ -f "/tmp/moko-platform-api/composer.json" ]; then + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true fi - name: Check FTP configuration @@ -101,15 +101,28 @@ jobs: DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) [ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote) - PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then - php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + PLATFORM=$(php /tmp/moko-platform-api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform-api/deploy/deploy-joomla.php" ]; then + php /tmp/moko-platform-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" else - php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + php /tmp/moko-platform-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" fi rm -f /tmp/deploy_key /tmp/sftp-config.json + + - name: Post-deploy health check + if: success() && steps.check.outputs.skip != 'true' + run: | + if [ -f "deploy/health-check.php" ]; then + SITE_URL="${{ vars.DEV_SITE_URL }}" + if [ -n "$SITE_URL" ]; then + php deploy/health-check.php --url "$SITE_URL" --checks http --timeout 30 || echo "::warning::Health check failed after deploy" + else + echo "DEV_SITE_URL not configured, skipping health check" + fi + fi + - name: Summary if: always() run: | diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml index 0c07612..e0fdd1d 100644 --- a/.mokogitea/workflows/gitleaks.yml +++ b/.mokogitea/workflows/gitleaks.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Security -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# INGROUP: moko-platform.Security +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/gitleaks.yml.template # VERSION: 01.00.00 # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml index 463a900..cde4541 100644 --- a/.mokogitea/workflows/notify.yml +++ b/.mokogitea/workflows/notify.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Notifications -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# INGROUP: moko-platform.Notifications +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.gitea/workflows/notify.yml # VERSION: 01.00.00 # BRIEF: Push notifications via ntfy on release success or workflow failure @@ -18,7 +18,6 @@ on: - "Joomla Build & Release" - "Joomla Extension CI" - "Deploy" - - "Cascade Main → Dev" types: - completed diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 9290a89..bc1a001 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# INGROUP: moko-platform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/universal/pr-check.yml.template # VERSION: 05.00.00 # BRIEF: PR gate — branch policy + code validation before merge @@ -108,8 +108,9 @@ jobs: - name: Detect platform id: platform run: | - # Parse manifest for platform detection - PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null) + # 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" @@ -193,32 +194,3 @@ jobs: 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; } - - # ── Changelog Gate ──────────────────────────────────────────────────── - changelog: - name: Changelog Updated - runs-on: ubuntu-latest - if: github.base_ref == 'main' - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check CHANGELOG.md was updated - run: | - BASE="${{ github.event.pull_request.base.sha }}" - HEAD="${{ github.event.pull_request.head.sha }}" - - if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then - echo "CHANGELOG.md updated" - else - # Allow [skip changelog] in PR title or body - PR_TITLE="${{ github.event.pull_request.title }}" - PR_BODY="${{ github.event.pull_request.body }}" - if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then - echo "::warning::Changelog skip requested via [skip changelog]" - exit 0 - fi - echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass." - exit 1 - fi diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 3ddd113..57d3380 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /templates/workflows/universal/pre-release.yml.template # VERSION: 05.00.00 # BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch @@ -49,27 +49,26 @@ jobs: 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 php-zip >/dev/null 2>&1 + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1 fi + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + git clone --depth 1 --branch main --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" /tmp/moko-platform-api + - name: Detect platform id: platform run: | - tr -d '[:space:]')| tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - # For packages: prefer pkg_*.xml in src/; fallback to any manifest - MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" - echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output - name: Resolve metadata id: meta run: | STABILITY="${{ inputs.stability }}" + MOKO_API="/tmp/moko-platform-api/cli" case "$STABILITY" in development) SUFFIX="-dev"; TAG="development" ;; @@ -78,66 +77,14 @@ jobs: release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; esac - # Read and bump patch version (with rollover) - CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) - [ -z "$CURRENT" ] && CURRENT="00.00.00" - - MAJOR=$(echo "$CURRENT" | cut -d. -f1) - MINOR=$(echo "$CURRENT" | cut -d. -f2) - PATCH=$(echo "$CURRENT" | cut -d. -f3) - - # Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major - NEW_PATCH=$((10#$PATCH + 1)) - NEW_MINOR=$((10#$MINOR)) - NEW_MAJOR=$((10#$MAJOR)) - - if [ $NEW_PATCH -gt 99 ]; then - NEW_PATCH=0 - NEW_MINOR=$((NEW_MINOR + 1)) - fi - if [ $NEW_MINOR -gt 99 ]; then - NEW_MINOR=0 - NEW_MAJOR=$((NEW_MAJOR + 1)) - fi - - VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH) - TODAY=$(date +%Y-%m-%d) - - echo "Bumping: ${CURRENT} → ${VERSION} (patch)" - - # Update README.md - sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md + # Bump patch version + BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .) + VERSION=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true) + [ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .) + echo "Version: ${VERSION}" # Update platform-specific manifest - PLATFORM="${{ steps.platform.outputs.platform }}" - MANIFEST="${{ steps.platform.outputs.manifest }}" - MOD_FILE="${{ steps.platform.outputs.mod_file }}" - case "$PLATFORM" in - joomla) - if [ -n "$MANIFEST" ]; then - MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) - sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" - sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" - fi - # For packages: also bump version in all sub-extension manifests - if [ -d "src/packages" ]; then - for SUB_MANIFEST in $(find src/packages -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null); do - SUB_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$SUB_MANIFEST" | head -1) - if [ -n "$SUB_VER" ]; then - sed -i "s|${SUB_VER}|${VERSION}|" "$SUB_MANIFEST" - sed -i "s|[^<]*|${TODAY}|" "$SUB_MANIFEST" - echo " Bumped sub-extension: $(basename $SUB_MANIFEST) ${SUB_VER} → ${VERSION}" - fi - done - fi - ;; - dolibarr) - if [ -n "$MOD_FILE" ]; then - sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE" - fi - ;; - *) ;; - esac + php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}" # Commit version bump git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" @@ -145,40 +92,22 @@ jobs: git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A git diff --cached --quiet || { - git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" + git commit -m "chore(version): bump to ${VERSION} [skip ci]" git push origin HEAD 2>&1 } - # Auto-detect element (platform-aware) - case "$PLATFORM" in - joomla) - MANIFEST="${{ steps.platform.outputs.manifest }}" - EXT_ELEMENT="" - if [ -n "$MANIFEST" ]; then - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - case "$EXT_ELEMENT" in - templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; - esac - fi - else - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - fi - ;; - dolibarr) - MOD_FILE="${{ steps.platform.outputs.mod_file }}" - if [ -n "$MOD_FILE" ]; then - MOD_BASENAME=$(basename "$MOD_FILE" .class.php) - EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]') - else - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - fi - ;; - *) - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - ;; - esac + # Detect element from Joomla/Dolibarr manifest + PLATFORM="${{ steps.platform.outputs.platform }}" + EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true) + # For Joomla, prefer tag + if [ "$PLATFORM" = "joomla" ]; then + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true) + if [ -n "$MANIFEST" ]; then + ELEM=$(grep -oP "\K[^<]+" "$MANIFEST" 2>/dev/null | head -1) + [ -n "$ELEM" ] && EXT_ELEMENT="$ELEM" + fi + fi + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" @@ -188,83 +117,42 @@ jobs: echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - name: Build package - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::error::No src/ or htdocs/ directory" - exit 1 - fi - - MANIFEST="${{ steps.meta.outputs.manifest }}" - EXT_TYPE="" - if [ -n "$MANIFEST" ]; then - EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - fi - - EXCLUDES="sftp-config* .ftpignore *.ppk *.pem *.key .env* *.local .build-trigger" - - mkdir -p build/package - - if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then - echo "=== Building Joomla PACKAGE (multi-extension) ===" - - # 1) ZIP each sub-extension in src/packages/ - for ext_dir in "${SOURCE_DIR}"/packages/*/; do - [ ! -d "$ext_dir" ] && continue - EXT_NAME=$(basename "$ext_dir") - echo " Packaging sub-extension: ${EXT_NAME}" - cd "$ext_dir" - zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES - cd "$OLDPWD" - done - - # 2) Copy package-level files (manifest, script, etc.) - for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do - [ -f "$f" ] && cp "$f" build/package/ - done - - echo "Package contents:" - ls -la build/package/ - else - echo "=== Building standard Joomla extension ===" - rsync -a \ - --exclude='sftp-config*' \ - --exclude='.ftpignore' \ - --exclude='*.ppk' \ - --exclude='*.pem' \ - --exclude='*.key' \ - --exclude='.env*' \ - --exclude='*.local' \ - --exclude='.build-trigger' \ - "${SOURCE_DIR}/" build/package/ - fi - - - name: Create ZIP id: zip run: | - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - cd build/package - zip -r "../${ZIP_NAME}" . - cd .. + VERSION="${{ steps.meta.outputs.version }}" + SUFFIX="${{ steps.meta.outputs.suffix }}" + PLATFORM="${{ steps.platform.outputs.platform }}" - SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) - echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" - echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)" + if [ "$PLATFORM" = "joomla" ]; then + php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --suffix "${SUFFIX}" --output build --github-output + else + # Generic build: zip src/ directory + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "::error::No src/ or htdocs/"; exit 1; } + EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" + ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + mkdir -p build + cd "$SOURCE_DIR" && zip -r "../build/${ZIP_NAME}" . && cd .. + SHA256=$(sha256sum "build/${ZIP_NAME}" | cut -d' ' -f1) + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "zip_path=build/${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + fi - name: Create or replace Gitea release id: release + continue-on-error: true run: | TAG="${{ steps.meta.outputs.tag }}" VERSION="${{ steps.meta.outputs.version }}" STABILITY="${{ steps.meta.outputs.stability }}" SHA256="${{ steps.zip.outputs.sha256 }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + ZIP_NAME="${{ steps.zip.outputs.zip_name }}" EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" TOKEN="${{ secrets.GA_TOKEN }}" API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" @@ -302,99 +190,29 @@ jobs: curl -sS -X POST -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/octet-stream" \ "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ - --data-binary "@build/${ZIP_NAME}" + --data-binary "@${{ steps.zip.outputs.zip_path }}" echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})" - - name: Update updates.xml + - name: "Update updates.xml" if: steps.platform.outputs.platform == 'joomla' run: | - STABILITY="${{ steps.meta.outputs.stability }}" VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" SHA256="${{ steps.zip.outputs.sha256 }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - TAG="${{ steps.meta.outputs.tag }}" - DATE=$(date +%Y-%m-%d) - - if [ ! -f "updates.xml" ]; then - echo "No updates.xml — skipping" - exit 0 - fi - - export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ - PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ - PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" - python3 << 'PYEOF' - import re, os - - stability = os.environ["PY_STABILITY"] - version = os.environ["PY_VERSION"] - sha256 = os.environ["PY_SHA256"] - zip_name = os.environ["PY_ZIP_NAME"] - tag = os.environ["PY_TAG"] - date = os.environ["PY_DATE"] - gitea_org = os.environ["PY_GITEA_ORG"] - gitea_repo = os.environ["PY_GITEA_REPO"] - download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" - - with open("updates.xml", "r") as f: - content = f.read() - - # Map stability to XML tag name - tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"} - xml_tag = tag_map.get(stability, stability) - - pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)" - match = re.search(pattern, content, re.DOTALL) - if match: - block = match.group(1) - updated = re.sub(r"[^<]*", f"{version}", block) - updated = re.sub(r"[^<]*", f"{date}", updated) - if "" in updated: - updated = re.sub(r"[^<]*", f"{sha256}", updated) - else: - updated = updated.replace("", f"\n {sha256}") - updated = re.sub(r"(]*>)[^<]*()", rf"\g<1>{download_url}\g<2>", updated) - content = content.replace(block, updated) - print(f"Updated {xml_tag} channel: version={version}") - else: - print(f"WARNING: No {xml_tag} block in updates.xml") - - with open("updates.xml", "w") as f: - f.write(content) - PYEOF - - # Commit and push to current branch + php /tmp/moko-platform-api/cli/updates_xml_build.php --path . --version "$VERSION" --stability "$STABILITY" --sha "$SHA256" --gitea-url "$GITEA_URL" --org "$GITEA_ORG" --repo "$GITEA_REPO" if ! git diff --quiet updates.xml 2>/dev/null; then git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" git add updates.xml - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git commit -m "chore: update $STABILITY channel $VERSION [skip ci]" git push origin HEAD 2>&1 || echo "WARNING: push failed" fi - name: "Sync updates.xml to all branches" if: steps.platform.outputs.platform == 'joomla' run: | - CURRENT_BRANCH="${{ github.ref_name }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - - # Sync updates.xml to main and dev (whichever isn't current) - for BRANCH in main dev; do - [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue - - echo "Syncing updates.xml → ${BRANCH}" - git fetch origin "${BRANCH}" 2>/dev/null || continue - git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue - git checkout "${CURRENT_BRANCH}" -- updates.xml - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" - git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" - fi - git checkout "${CURRENT_BRANCH}" 2>/dev/null - done + php /tmp/moko-platform-api/cli/updates_xml_sync.php --path . --current "${{ github.ref_name }}" --branches main,dev --version "${{ steps.meta.outputs.version }}" --token "${{ secrets.GA_TOKEN }}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" --gitea-url "${GITEA_URL}" - name: "Delete lesser pre-release channels (cascade)" continue-on-error: true diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index e5e1c73..d738ad7 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -7,18 +7,14 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# INGROUP: moko-platform.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/joomla/repo_health.yml.template # VERSION: 04.06.00 # BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. # ============================================================================ -name: "Joomla: Repo Health" - -concurrency: - group: repo-health-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: true +name: "Generic: Repo Health" defaults: run: @@ -288,7 +284,7 @@ jobs: exit 0 fi - IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}" + 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=() @@ -392,23 +388,27 @@ jobs: exit 0 fi - # Source directory: src/ or htdocs/ (either is valid) + 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 - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" - - missing_required=() - missing_optional=() - for item in "${required_artifacts[@]}"; do if printf '%s' "${item}" | grep -q '/$'; then d="${item%/}" @@ -450,12 +450,8 @@ jobs: fi done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') - if [ "${#dev_paths[@]}" -eq 0 ]; then - missing_required+=("dev/* branch (e.g. dev/01.00.00)") - fi - - if [ "${#dev_branches[@]}" -gt 0 ]; then - missing_required+=("invalid branch dev (must be dev/)") + if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then + missing_required+=("dev or dev/* branch") fi content_warnings=() @@ -481,26 +477,7 @@ jobs: export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - report_json="$(python3 - <<'PY' - import json - import os - - profile = os.environ.get('PROFILE_RAW') or 'all' - - missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else [] - missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else [] - content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else [] - - out = { - 'profile': profile, - 'missing_required': [x for x in missing_required if x], - 'missing_optional': [x for x in missing_optional if x], - 'content_warnings': [x for x in content_warnings if x], - } - - print(json.dumps(out, indent=2)) - PY - )" + 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' @@ -578,12 +555,14 @@ jobs: joomla_findings+=("updates.xml missing in root (required for Joomla update server)") fi - 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 + 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 { @@ -629,43 +608,29 @@ jobs: fi if [ -f "${DOCS_INDEX}" ]; then - missing_links="$(python3 - <<'PY' - import os - import re - - idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') - base = os.getcwd() - - bad = [] - pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') - - with open(idx, 'r', encoding='utf-8') as f: - for line in f: - for m in pat.findall(line): - link = m.strip() - if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): - continue - if link.startswith('/'): - rel = link.lstrip('/') - else: - rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) - rel = rel.split('#', 1)[0] - rel = rel.split('?', 1)[0] - if not rel: - continue - p = os.path.join(base, rel) - if not os.path.exists(p): - bad.append(rel) - - print('\n'.join(sorted(set(bad)))) - PY - )" + 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:' - while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" + for bl in ${missing_links}; do + printf '%s\n' "- ${bl}" + done printf '\n' } >> "${GITHUB_STEP_SUMMARY}" fi @@ -764,3 +729,41 @@ jobs: 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 + diff --git a/.mokogitea/workflows/security-audit.yml b/.mokogitea/workflows/security-audit.yml index 789325a..714d407 100644 --- a/.mokogitea/workflows/security-audit.yml +++ b/.mokogitea/workflows/security-audit.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Security -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# INGROUP: moko-platform.Security +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.gitea/workflows/security-audit.yml # VERSION: 01.00.00 # BRIEF: Dependency vulnerability scanning for composer and npm packages @@ -80,3 +80,19 @@ jobs: -H "Priority: high" \ -d "Security audit found vulnerabilities. Review dependency updates." \ "${NTFY_URL}/${NTFY_TOPIC}" || true + + + - name: Joomla version audit + if: always() + run: | + if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then + echo "$JOOMLA_SITES" > /tmp/sites.json + php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true + echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY + rm -f /tmp/sites.json + else + echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)" + fi + env: + JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }} + diff --git a/CHANGELOG.md b/CHANGELOG.md index 92f2bf0..6fa6c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,8 @@ # FILE INFORMATION DEFGROUP: MokoJoomTOS INGROUP: plg_system_mokojoomtos - REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS - VERSION: 04.01.00 + REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS + VERSION: 04.02.01 PATH: ./CHANGELOG.md BRIEF: Version history and release notes --> @@ -36,6 +36,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- enablePlugin() now called unconditionally in postflight() (#89) +- Replaced Table::addIncludePath() with bootComponent()->getMVCFactory() for Joomla 5 (#90) +- Added non-SEF URL fallback via Itemid matching (#91) +- enablePlugin() now fires on upgrade path (#92) +- Removed $_GET superglobal mutation (#93) +- Added params, metadata, attribs defaults to article creation (#94) +- Applied urldecode() to URI path before slug comparison (#95) +- Cast Registry return to array before iterating slugs (#96) +- Fixed MenuslugField separator `disable` to `disabled` property (#99) + +### Added + +- SEF disabled warning in MenuslugField dropdown (#97) +- Include Children toggle for offline-accessible menu items (defaults to Yes) + +### Fixed (Manifest) + +- Hardcode description in XML manifest (language variables don't resolve during install) + +### Changed + +- Stripped legacy mokojoomtos.php to minimal stub (#101) +- Converted script.php indentation from spaces to tabs (#102) +- Renamed installer class to PlgSystemMokojoomtosInstallerScript (#103) + ## [04.01.00] - 2026-05-16 ### Fixed @@ -55,7 +82,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Match against full menu route path instead of alias only (fixes nested routes like `/legal/terms-of-service`) - Plugin pretty name updated to Joomla convention: "System - Moko Terms of Service" -- Renamed `.mokogitea/` to `.gitea/` for standard Gitea compatibility - MenuslugField now stores and displays full route paths (e.g., `legal/terms-of-service`) ### Removed @@ -77,119 +103,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Added Gitea update server URL to plugin manifest (`updates.xml` on `main`) -- Removed obsolete `src/plugins/` legacy directory +- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute +- Component-only view now properly applied when site is offline ### Removed - Legacy duplicate manifest at `src/plugins/system/mokojoomtos/mokojoomtos.xml` -## [03.09.00] - 2026-02-28 - -### Changed - -- Updated version number to 03.09.00 across all files -- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute - -### Fixed - -- Template chrome (header, footer, modules) no longer loads when accessing TOS page in offline mode -- Component-only view now properly applied when site is offline - ## [1.0.0] - 2026-01-16 ### Added -- Initial template repository structure -- Comprehensive Makefile with 30+ build targets -- Complete README.md with usage documentation -- CONTRIBUTING.md with contribution guidelines -- SECURITY.md with security policy and best practices -- CHANGELOG.md for version tracking -- LICENSE file (GPL-3.0-or-later) -- EditorConfig for consistent code formatting -- Documentation structure in `docs/` directory -- Placeholders for component directories (`admin/`, `site/`, `media/`) -- MokoStandards compliance: - - File header standards with copyright and SPDX identifiers - - Joomla coding standards configuration - - PHPStan static analysis setup - - Dependency security auditing -- Makefile targets for: - - Dependency management (install-deps, update-deps) - - Code validation (lint, phpcs, phpstan) - - Testing (test, test-coverage) - - Building (build, build-assets) - - Development (dev-install, watch-assets) - - Release management (release, bump-version) - - Utility commands (clean, version, help) -- Documentation index files with MokoStandards metadata - -### Documentation - -- Comprehensive README with: - - Quick start guide - - Prerequisites and installation - - Usage examples - - Project structure overview - - Makefile command reference - - Standards compliance information -- Detailed CONTRIBUTING guide with: - - Commit message conventions - - DCO sign-off requirements - - Development setup instructions - - Code review process -- Security policy with: - - Vulnerability reporting procedures - - Security best practices - - Code examples for secure development -- Template structure for Joomla 4.x and 5.x compatibility - -## Version Guidelines - -### Version Numbering - -This template follows [Semantic Versioning](https://semver.org/): - -- **MAJOR** version for incompatible changes (e.g., 2.0.0) -- **MINOR** version for new features (e.g., 1.1.0) -- **PATCH** version for bug fixes (e.g., 1.0.1) - -### Change Categories - -- **Added**: New features -- **Changed**: Changes to existing functionality -- **Deprecated**: Soon-to-be removed features -- **Removed**: Removed features -- **Fixed**: Bug fixes -- **Security**: Security fixes - -## Release Notes - -### [1.0.0] Release Notes - -This is the initial release of the MokoJoomTOS plugin. It provides a production-ready Joomla system plugin for offline TOS access with: - -- **Complete build system** via comprehensive Makefile -- **MokoStandards compliance** out of the box -- **Developer-friendly** workflow with automation -- **Security-focused** with built-in best practices -- **Well-documented** with clear usage instructions - -This template is ready for use in creating new Joomla components that follow organizational coding standards and best practices. - -### Migration Guide - -**For New Projects**: Simply use this template to create your new component repository. - -**For Existing Projects**: Review the Makefile, documentation structure, and standards compliance files to gradually adopt features that benefit your project. +- Initial plugin release +- Offline mode bypass for configured menu slug +- Auto-provisioning installer (creates article, menu type, menu item) +- Component-only rendering (`tmpl=component`) +- Language files for en-GB and en-US +- MokoStandards compliance ## Links -- [Repository](https://github.com/mokoconsulting-tech/MokoJoomTOS) -- [Issues](https://github.com/mokoconsulting-tech/MokoJoomTOS/issues) -- [Pull Requests](https://github.com/mokoconsulting-tech/MokoJoomTOS/pulls) -- [MokoStandards](https://github.com/mokoconsulting-tech/MokoCodingDefaults) +- [Repository](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS) +- [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/issues) +- [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases) -[Unreleased]: https://github.com/mokoconsulting-tech/MokoJoomTOS/compare/v03.09.00...HEAD -[03.09.00]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v03.09.00 -[1.0.0]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v1.0.0 +[Unreleased]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/stable...dev +[04.01.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable +[04.00.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/v03.09.00...stable +[03.09.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/v03.09.00 +[1.0.0]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/v1.0.0 diff --git a/CLAUDE.md b/CLAUDE.md index 3d5ba17..424c6d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ MokoJoomTOS is a lightweight Joomla 4.x/5.x system plugin that allows Terms of S ``` / -├── .github/ # GitHub workflows, issue templates, copilot-instructions.md +├── .mokogitea/ # GitHub workflows, issue templates, copilot-instructions.md ├── docs/ # Detailed documentation (currently minimal with index.md) ├── scripts/ # Build and utility scripts (validate/, package scripts) ├── src/ # Plugin source code at root level (NOT nested under plugins/) @@ -469,6 +469,6 @@ This repository follows minimal documentation structure with essential docs in r 2. **SECURITY.md** - Security policy and vulnerability reporting procedures 3. **CODE_OF_CONDUCT.md** - Community standards and behavior expectations 4. **CHANGELOG.md** - Version history following Keep a Changelog format -5. **.github/copilot-instructions.md** - Comprehensive guidance for GitHub Copilot (includes all Joomla patterns) +5. **.mokogitea/copilot-instructions.md** - Comprehensive guidance for GitHub Copilot (includes all Joomla patterns) Currently no `docs/policy/` directory exists - all policy is in root-level markdown files. diff --git a/src/administrator/language/en-GB/plg_system_mokojoomtos.ini b/src/administrator/language/en-GB/plg_system_mokojoomtos.ini index d26179d..cc431a8 100644 --- a/src/administrator/language/en-GB/plg_system_mokojoomtos.ini +++ b/src/administrator/language/en-GB/plg_system_mokojoomtos.ini @@ -11,10 +11,16 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings" PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items" PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items." +PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items" +PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'." + ; Help PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin" PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="Step 1: Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).
Step 2: Create menu items pointing to those articles.
Step 3: Select the menu items above (hold Ctrl/Cmd to select multiple).
Step 4: When your site goes offline, visitors can still access the selected pages.

Tip: The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)." +; Warnings +PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active." + ; Errors PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s" diff --git a/src/administrator/language/en-US/plg_system_mokojoomtos.ini b/src/administrator/language/en-US/plg_system_mokojoomtos.ini index d26179d..cc431a8 100644 --- a/src/administrator/language/en-US/plg_system_mokojoomtos.ini +++ b/src/administrator/language/en-US/plg_system_mokojoomtos.ini @@ -11,10 +11,16 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings" PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items" PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items." +PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items" +PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'." + ; Help PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin" PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="Step 1: Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).
Step 2: Create menu items pointing to those articles.
Step 3: Select the menu items above (hold Ctrl/Cmd to select multiple).
Step 4: When your site goes offline, visitors can still access the selected pages.

Tip: The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)." +; Warnings +PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active." + ; Errors PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s" diff --git a/src/language/en-GB/plg_system_mokojoomtos.ini b/src/language/en-GB/plg_system_mokojoomtos.ini index 3155c1c..7e09ab1 100644 --- a/src/language/en-GB/plg_system_mokojoomtos.ini +++ b/src/language/en-GB/plg_system_mokojoomtos.ini @@ -11,6 +11,9 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings" PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items" PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items." +PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items" +PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'." + ; Help PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin" PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="Step 1: Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).
Step 2: Create menu items pointing to those articles.
Step 3: Select the menu items above (hold Ctrl/Cmd to select multiple).
Step 4: When your site goes offline, visitors can still access the selected pages.

Tip: The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)." diff --git a/src/language/en-US/plg_system_mokojoomtos.ini b/src/language/en-US/plg_system_mokojoomtos.ini index 3155c1c..7e09ab1 100644 --- a/src/language/en-US/plg_system_mokojoomtos.ini +++ b/src/language/en-US/plg_system_mokojoomtos.ini @@ -11,6 +11,9 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings" PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items" PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items." +PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items" +PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'." + ; Help PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin" PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="Step 1: Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).
Step 2: Create menu items pointing to those articles.
Step 3: Select the menu items above (hold Ctrl/Cmd to select multiple).
Step 4: When your site goes offline, visitors can still access the selected pages.

Tip: The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)." diff --git a/src/mokojoomtos.php b/src/mokojoomtos.php index f7e64a0..45f95f5 100644 --- a/src/mokojoomtos.php +++ b/src/mokojoomtos.php @@ -9,106 +9,16 @@ defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\CMS\Uri\Uri; /** - * MokoJoomTOS Offline Mode Bypass Plugin (Legacy) + * MokoJoomTOS Legacy Entry Point * - * Allows configured menu items to remain accessible when the site - * is in offline mode. + * This file is required by Joomla's plugin loader () + * but is NOT executed under Joomla 5's DI container. The actual plugin logic lives + * in src/Extension/MokoJoomTOS.php, bootstrapped via services/provider.php. * * @since 1.0.0 */ class PlgSystemMokojoomtos extends CMSPlugin { - /** - * Load the language file on instantiation. - * - * @var boolean - * @since 1.0.0 - */ - protected $autoloadLanguage = true; - - /** - * Application object - * - * @var \Joomla\CMS\Application\CMSApplication - * @since 1.0.0 - */ - protected $app; - - /** - * After route event handler - * - * @return void - * - * @since 04.00.00 - */ - public function onAfterRoute() - { - // Only process for site application - if (!$this->app->isClient('site')) - { - return; - } - - // Get the global configuration - $config = $this->app->getConfig(); - - // Only proceed if site is offline - if (!$config->get('offline')) - { - return; - } - - // Get the configured slugs (stored as array for multi-select) - $slugs = $this->params->get('tos_slug', []); - - // Handle legacy single-value string format - if (is_string($slugs)) - { - $slugs = array_filter([trim($slugs)]); - } - - if (empty($slugs)) - { - return; - } - - // Get the current URI path - $uri = Uri::getInstance(); - $path = trim($uri->getPath(), '/'); - - // Remove the base path if present - $base = trim(Uri::base(true), '/'); - if (!empty($base) && strpos($path, $base) === 0) - { - $path = trim(substr($path, strlen($base)), '/'); - } - - // Check if the path matches any configured slug - foreach ($slugs as $slug) - { - $slug = trim($slug); - if (empty($slug)) - { - continue; - } - - if ($path === $slug || strpos($path, $slug . '/') === 0) - { - // Temporarily disable offline mode for this request - $config->set('offline', 0); - - // Set component-only view (no template chrome) - $input = $this->app->input; - $input->set('tmpl', 'component'); - - // Also set in GET superglobal to ensure recognition - $_GET['tmpl'] = 'component'; - - return; - } - } - } } diff --git a/src/mokojoomtos.xml b/src/mokojoomtos.xml index 82b91ec..c512ce7 100644 --- a/src/mokojoomtos.xml +++ b/src/mokojoomtos.xml @@ -38,7 +38,7 @@ hello@mokoconsulting.tech https://mokoconsulting.tech 04.02.01 - PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION + Allows Terms of Service to be accessible via menu slug when site is offline Joomla\Plugin\System\MokoJoomTOS @@ -71,6 +71,18 @@ description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC" multiple="true" /> + + + + + ' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '

'; - } + /** + * Function called after plugin installation + * + * @param InstallerAdapter $parent Parent installer adapter + * + * @return void + * + * @since 1.0.0 + */ + public function install($parent) + { + echo '

' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '

'; + } - /** - * Function called after plugin update - * - * @param InstallerAdapter $parent Parent installer adapter - * - * @return void - * - * @since 1.0.0 - */ - public function update($parent) - { - echo '

' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '

'; - } + /** + * Function called after plugin update + * + * @param InstallerAdapter $parent Parent installer adapter + * + * @return void + * + * @since 1.0.0 + */ + public function update($parent) + { + echo '

' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '

'; + } - /** - * Function called after plugin uninstallation - * - * @param InstallerAdapter $parent Parent installer adapter - * - * @return void - * - * @since 1.0.0 - */ - public function uninstall($parent) - { - echo '

' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '

'; - } + /** + * Function called after plugin uninstallation + * + * @param InstallerAdapter $parent Parent installer adapter + * + * @return void + * + * @since 1.0.0 + */ + public function uninstall($parent) + { + echo '

' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '

'; + } - /** - * Function called after extension installation/update/discover_install - * - * @param string $type Installation type (install, update, discover_install) - * @param InstallerAdapter $parent Parent installer adapter - * - * @return void - * - * @since 1.0.0 - */ - public function postflight($type, $parent) - { - if ($type === 'install' || $type === 'discover_install') { - // Create Terms of Service article and menu item - $this->createTermsOfServiceSetup(); - - echo '
'; - echo '

' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '

'; - echo '

' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '

'; - echo '
'; - } - } + /** + * Function called after extension installation/update/discover_install + * + * Fixes #89: enablePlugin() is now called unconditionally for both + * install and upgrade paths. + * Fixes #92: enablePlugin() is called on upgrade to re-enable if disabled. + * + * @param string $type Installation type (install, update, discover_install) + * @param InstallerAdapter $parent Parent installer adapter + * + * @return void + * + * @since 1.0.0 + */ + public function postflight($type, $parent) + { + // Always enable the plugin on install or upgrade + $this->enablePlugin(); - /** - * Create Terms of Service article and menu item - * - * @return void - * - * @since 1.0.0 - */ - private function createTermsOfServiceSetup() - { - try { - $db = Factory::getDbo(); - - // Check if Terms of Service article already exists - $query = $db->getQuery(true) - ->select('id') - ->from($db->quoteName('#__content')) - ->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service')); - $db->setQuery($query); - $articleId = $db->loadResult(); - - // Create article if it doesn't exist - if (!$articleId) { - $articleId = $this->createTermsArticle(); - } - - if ($articleId) { - // Check if menu item already exists - $query = $db->getQuery(true) - ->select('id') - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service')) - ->where($db->quoteName('published') . ' >= 0'); - $db->setQuery($query); - $menuId = $db->loadResult(); - - // Create menu item if it doesn't exist - if (!$menuId) { - $this->createTermsMenuItem($articleId); - } - } - } catch (\Exception $e) { - Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror'); - } - } + if ($type === 'install' || $type === 'discover_install') { + $this->createTermsOfServiceSetup(); - /** - * Create Terms of Service article - * - * @return int|null Article ID or null on failure - * - * @since 1.0.0 - */ - private function createTermsArticle() - { - try { - $db = Factory::getDbo(); + echo '
'; + echo '

' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '

'; + echo '

' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '

'; + echo '
'; + } + } - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_content/tables'); - $table = Table::getInstance('Content', 'Joomla\\Component\\Content\\Administrator\\Table\\'); + /** + * Create Terms of Service article and menu item + * + * @return void + * + * @since 1.0.0 + */ + private function createTermsOfServiceSetup() + { + try { + $db = Factory::getDbo(); - if (!$table) { - Log::add('Failed to get Content table instance', Log::WARNING, 'jerror'); - return null; - } + // Check if Terms of Service article already exists (with catid filter) + $query = $db->getQuery(true) + ->select('id') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service')) + ->where($db->quoteName('state') . ' >= 0'); + $db->setQuery($query); + $articleId = $db->loadResult(); - // Get Uncategorised category ID dynamically - $query = $db->getQuery(true) - ->select('id') - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('extension') . ' = ' . $db->quote('com_content')) - ->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised')) - ->where($db->quoteName('published') . ' = 1'); - $db->setQuery($query); - $catId = (int) $db->loadResult(); + if (!$articleId) { + $articleId = $this->createTermsArticle(); + } - if (!$catId) { - Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror'); - return null; - } + if ($articleId) { + $query = $db->getQuery(true) + ->select('id') + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service')) + ->where($db->quoteName('published') . ' >= 0'); + $db->setQuery($query); + $menuId = $db->loadResult(); - // Get current user ID for article ownership - $createdBy = Factory::getApplication()->getIdentity()->id ?: 0; + if (!$menuId) { + $this->createTermsMenuItem($articleId); + } + } + } catch (\Exception $e) { + Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror'); + } + } - $data = [ - 'title' => 'Terms of Service', - 'alias' => 'terms-of-service', - 'introtext' => '

Terms of Service

Welcome to our Terms of Service page.

This page will remain accessible even when the site is in offline/maintenance mode.

', - 'fulltext' => '', - 'state' => 1, - 'catid' => $catId, - 'created' => Factory::getDate()->toSql(), - 'created_by' => $createdBy, - 'language' => '*', - 'access' => 1, // Public - ]; - - // Bind data to table object first - if (!$table->bind($data)) { - Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror'); - return null; - } - - // Check data validity - if (!$table->check()) { - Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror'); - return null; - } - - // Save the table - if (!$table->store()) { - Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror'); - return null; - } - - echo '

✓ Created Terms of Service article

'; - return $table->id; - } catch (\Exception $e) { - Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror'); - } - - return null; - } + /** + * Create Terms of Service article using Joomla 5 MVCFactory + * + * Fixes #90: Uses bootComponent()->getMVCFactory() instead of + * the removed Table::addIncludePath() / Table::getInstance(). + * Fixes #94: Includes params, metadata, and attribs defaults. + * + * @return int|null Article ID or null on failure + * + * @since 1.0.0 + */ + private function createTermsArticle() + { + try { + $db = Factory::getDbo(); + $app = Factory::getApplication(); - /** - * Create Terms of Service menu item - * - * @param int $articleId The article ID to link to - * - * @return void - * - * @since 1.0.0 - */ - private function createTermsMenuItem($articleId) - { - try { - $db = Factory::getDbo(); - - // Check if "Legal" menu type exists - $query = $db->getQuery(true) - ->select('id') - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' = ' . $db->quote('legal')); - $db->setQuery($query); - $legalMenuExists = $db->loadResult(); - - // Create "Legal" menu type if it doesn't exist - if (!$legalMenuExists) { - $this->createLegalMenuType(); - } - - // Get com_content component ID dynamically - $query = $db->getQuery(true) - ->select('extension_id') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')) - ->where($db->quoteName('element') . ' = ' . $db->quote('com_content')); - $db->setQuery($query); - $componentId = (int) $db->loadResult(); + // Get content table via MVCFactory (Joomla 4/5 compatible) + $table = $app->bootComponent('com_content') + ->getMVCFactory() + ->createTable('Article', 'Administrator'); - if (!$componentId) { - Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror'); - return; - } - - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables'); - $table = Table::getInstance('Menu', 'Joomla\\Component\\Menus\\Administrator\\Table\\'); - - if (!$table) { - Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror'); - return; - } - - $data = [ - 'menutype' => 'legal', - 'title' => 'Terms of Service', - 'alias' => 'terms-of-service', - 'link' => 'index.php?option=com_content&view=article&id=' . $articleId, - 'type' => 'component', - 'published' => 1, - 'parent_id' => 1, - 'component_id' => $componentId, - 'level' => 1, - 'language' => '*', - 'access' => 1, // Public - 'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}', - ]; - - // Set the location in the menu tree - $table->setLocation($data['parent_id'], 'last-child'); - - // Bind data to table object - if (!$table->bind($data)) { - Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror'); - return; - } - - // Check data validity - if (!$table->check()) { - Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror'); - return; - } - - // Save the menu item - if (!$table->store()) { - Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror'); - return; - } - - echo '

✓ Created Terms of Service menu item in Legal menu with slug: terms-of-service

'; - - // Enable the plugin - $this->enablePlugin(); - } catch (\Exception $e) { - Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror'); - } - } + if (!$table) { + Log::add('Failed to get Content table instance', Log::WARNING, 'jerror'); + return null; + } - /** - * Create Legal menu type - * - * @return void - * - * @since 1.0.0 - */ - private function createLegalMenuType() - { - try { - $db = Factory::getDbo(); - - // Insert the Legal menu type - $query = $db->getQuery(true) - ->insert($db->quoteName('#__menu_types')) - ->columns($db->quoteName(['menutype', 'title', 'description'])) - ->values( - $db->quote('legal') . ', ' . - $db->quote('Legal') . ', ' . - $db->quote('Legal documents and policies menu') - ); - $db->setQuery($query); - $db->execute(); - - echo '

✓ Created Legal menu type

'; - } catch (\Exception $e) { - Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror'); - } - } + // Get Uncategorised category ID dynamically + $query = $db->getQuery(true) + ->select('id') + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('extension') . ' = ' . $db->quote('com_content')) + ->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised')) + ->where($db->quoteName('published') . ' = 1'); + $db->setQuery($query); + $catId = (int) $db->loadResult(); - /** - * Enable the plugin after installation - * - * @return void - * - * @since 1.0.0 - */ - private function enablePlugin() - { - try { - $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('mokojoomtos')); - $db->setQuery($query); - $db->execute(); - - echo '

✓ Plugin enabled automatically

'; - } catch (\Exception $e) { - Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror'); - } - } + if (!$catId) { + Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror'); + return null; + } + + $createdBy = $app->getIdentity()->id ?: 0; + + $data = [ + 'title' => 'Terms of Service', + 'alias' => 'terms-of-service', + 'introtext' => '

Terms of Service

Welcome to our Terms of Service page.

This page will remain accessible even when the site is in offline/maintenance mode.

', + 'fulltext' => '', + 'state' => 1, + 'catid' => $catId, + 'created' => Factory::getDate()->toSql(), + 'created_by' => $createdBy, + 'language' => '*', + 'access' => 1, + 'params' => '{}', + 'metadata' => '{"robots":"","author":"","rights":""}', + 'attribs' => '{}', + ]; + + if (!$table->bind($data)) { + Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror'); + return null; + } + + if (!$table->check()) { + Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror'); + return null; + } + + if (!$table->store()) { + Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror'); + return null; + } + + echo '

Created Terms of Service article

'; + return $table->id; + } catch (\Exception $e) { + Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror'); + } + + return null; + } + + /** + * Create Terms of Service menu item using Joomla 5 MVCFactory + * + * Fixes #90: Uses bootComponent()->getMVCFactory() instead of + * the removed Table::addIncludePath() / Table::getInstance(). + * + * @param int $articleId The article ID to link to + * + * @return void + * + * @since 1.0.0 + */ + private function createTermsMenuItem($articleId) + { + try { + $db = Factory::getDbo(); + $app = Factory::getApplication(); + + // Check if "Legal" menu type exists + $query = $db->getQuery(true) + ->select('id') + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' = ' . $db->quote('legal')); + $db->setQuery($query); + $legalMenuExists = $db->loadResult(); + + if (!$legalMenuExists) { + $this->createLegalMenuType(); + } + + // Get com_content component ID dynamically + $query = $db->getQuery(true) + ->select('extension_id') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_content')); + $db->setQuery($query); + $componentId = (int) $db->loadResult(); + + if (!$componentId) { + Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror'); + return; + } + + // Get menu table via MVCFactory (Joomla 4/5 compatible) + $table = $app->bootComponent('com_menus') + ->getMVCFactory() + ->createTable('Menu', 'Administrator'); + + if (!$table) { + Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror'); + return; + } + + $data = [ + 'menutype' => 'legal', + 'title' => 'Terms of Service', + 'alias' => 'terms-of-service', + 'link' => 'index.php?option=com_content&view=article&id=' . $articleId, + 'type' => 'component', + 'published' => 1, + 'parent_id' => 1, + 'component_id' => $componentId, + 'level' => 1, + 'language' => '*', + 'access' => 1, + 'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}', + ]; + + $table->setLocation($data['parent_id'], 'last-child'); + + if (!$table->bind($data)) { + Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror'); + return; + } + + if (!$table->check()) { + Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror'); + return; + } + + if (!$table->store()) { + Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror'); + return; + } + + echo '

Created Terms of Service menu item in Legal menu

'; + } catch (\Exception $e) { + Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror'); + } + } + + /** + * Create Legal menu type + * + * @return void + * + * @since 1.0.0 + */ + private function createLegalMenuType() + { + try { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->insert($db->quoteName('#__menu_types')) + ->columns($db->quoteName(['menutype', 'title', 'description'])) + ->values( + $db->quote('legal') . ', ' . + $db->quote('Legal') . ', ' . + $db->quote('Legal documents and policies menu') + ); + $db->setQuery($query); + $db->execute(); + + echo '

Created Legal menu type

'; + } catch (\Exception $e) { + // Duplicate key is expected if race condition — safe to ignore + Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror'); + } + } + + /** + * Enable the plugin after installation + * + * @return void + * + * @since 1.0.0 + */ + private function enablePlugin() + { + try { + $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('mokojoomtos')); + $db->setQuery($query); + $db->execute(); + } catch (\Exception $e) { + Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror'); + } + } } diff --git a/src/src/Extension/MokoJoomTOS.php b/src/src/Extension/MokoJoomTOS.php index adfe3d3..f5bb099 100644 --- a/src/src/Extension/MokoJoomTOS.php +++ b/src/src/Extension/MokoJoomTOS.php @@ -10,6 +10,7 @@ namespace Joomla\Plugin\System\MokoJoomTOS\Extension; defined('_JEXEC') or die; +use Joomla\CMS\Factory; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Uri\Uri; use Joomla\Event\SubscriberInterface; @@ -53,23 +54,21 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface * the site is in offline mode. If both conditions are met, temporarily * disables offline mode and sets component-only view for this request. * - * This event fires after routing but before template selection, making it - * the correct place to set tmpl=component to prevent template chrome loading. - * * @return void * * @since 1.0.0 */ public function onAfterRoute() { + $app = $this->getApplication(); + // Only process for site application - if (!$this->getApplication()->isClient('site')) + if (!$app->isClient('site')) { return; } - // Get the global configuration - $config = $this->getApplication()->getConfig(); + $config = $app->getConfig(); // Only proceed if site is offline if (!$config->get('offline')) @@ -77,7 +76,7 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface return; } - // Get the configured slugs (stored as array for multi-select) + // Get the configured slugs — cast to array to handle stdClass from Registry (#96) $slugs = $this->params->get('tos_slug', []); // Handle legacy single-value string format @@ -85,46 +84,161 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface { $slugs = array_filter([trim($slugs)]); } + else + { + $slugs = (array) $slugs; + } if (empty($slugs)) { return; } - // Get the current URI path - $uri = Uri::getInstance(); - $path = trim($uri->getPath(), '/'); + $includeChildren = (int) $this->params->get('include_children', 1); - // Remove the base path if present + // Try SEF path matching first, then fall back to Itemid matching (#91) + if ($this->matchByPath($slugs, $config, $app, $includeChildren)) + { + return; + } + + $this->matchByItemId($slugs, $config, $app, $includeChildren); + } + + /** + * Match the current request path against configured slugs (SEF mode) + * + * @param array $slugs Configured slug values + * @param object $config Joomla configuration object + * @param object $app Application instance + * @param integer $includeChildren Whether to include child menu items + * + * @return boolean True if a match was found and offline mode was bypassed + * + * @since 4.1.0 + */ + private function matchByPath(array $slugs, $config, $app, int $includeChildren = 1): bool + { + $uri = Uri::getInstance(); + $path = urldecode(trim($uri->getPath(), '/')); + + // Remove the base path if present (subdirectory installs) $base = trim(Uri::base(true), '/'); if (!empty($base) && strpos($path, $base) === 0) { $path = trim(substr($path, strlen($base)), '/'); } - // Check if the path matches any configured slug + // Skip if path is empty or just index.php (non-SEF) + if (empty($path) || $path === 'index.php') + { + return false; + } + foreach ($slugs as $slug) { - $slug = trim($slug); + $slug = trim((string) $slug); if (empty($slug)) { continue; } - if ($path === $slug || strpos($path, $slug . '/') === 0) + $isMatch = ($path === $slug) + || ($includeChildren && strpos($path, $slug . '/') === 0); + + if ($isMatch) { - // Temporarily disable offline mode for this request - $config->set('offline', 0); - - // Set component-only view (no template chrome) - $input = $this->getApplication()->input; - $input->set('tmpl', 'component'); - - // Also set in GET superglobal to ensure recognition - $_GET['tmpl'] = 'component'; - - return; + $this->bypassOffline($config, $app); + return true; } } + + return false; + } + + /** + * Match the current request Itemid against menu items for configured slugs (non-SEF fallback) + * + * When SEF URLs are disabled, the path is just index.php so we match by + * checking if the requested Itemid belongs to a menu item whose path + * matches a configured slug. + * + * @param array $slugs Configured slug values + * @param object $config Joomla configuration object + * @param object $app Application instance + * @param integer $includeChildren Whether to include child menu items + * + * @return boolean True if a match was found and offline mode was bypassed + * + * @since 4.1.0 + */ + private function matchByItemId(array $slugs, $config, $app, int $includeChildren = 1): bool + { + $itemId = (int) $app->input->getInt('Itemid', 0); + + if (!$itemId) + { + return false; + } + + try + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('path')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = ' . $itemId) + ->where($db->quoteName('published') . ' = 1') + ->where($db->quoteName('client_id') . ' = 0'); + $db->setQuery($query); + $menuPath = $db->loadResult(); + + if (!$menuPath) + { + return false; + } + + $menuPath = trim($menuPath, '/'); + + foreach ($slugs as $slug) + { + $slug = trim((string) $slug); + if (empty($slug)) + { + continue; + } + + $isMatch = ($menuPath === $slug) + || ($includeChildren && strpos($menuPath, $slug . '/') === 0); + + if ($isMatch) + { + $this->bypassOffline($config, $app); + return true; + } + } + } + catch (\Exception $e) + { + // Silently fail — do not bypass offline mode on error + } + + return false; + } + + /** + * Bypass offline mode and set component-only view for this request + * + * @param object $config Joomla configuration object + * @param object $app Application instance + * + * @return void + * + * @since 4.1.0 + */ + private function bypassOffline($config, $app): void + { + $config->set('offline', 0); + $app->input->set('tmpl', 'component'); } } diff --git a/src/src/Field/MenuslugField.php b/src/src/Field/MenuslugField.php index 03e295a..4ed6697 100644 --- a/src/src/Field/MenuslugField.php +++ b/src/src/Field/MenuslugField.php @@ -42,6 +42,24 @@ class MenuslugField extends ListField { $options = parent::getOptions(); + // Warn if SEF URLs are disabled (#97) + try + { + $sef = Factory::getApplication()->get('sef', true); + if (!$sef) + { + $options[] = (object) [ + 'value' => '', + 'text' => Text::_('PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING'), + 'disabled' => true + ]; + } + } + catch (\Exception $e) + { + // Ignore — field still works without the warning + } + try { $db = Factory::getDbo(); @@ -70,7 +88,7 @@ class MenuslugField extends ListField $options[] = (object) [ 'value' => '', 'text' => '──────────────', - 'disable' => true + 'disabled' => true ]; } $lastMenuType = $item->menutype; diff --git a/updates.xml b/updates.xml index ea95a6e..9f89552 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,13 +10,13 @@ System - Moko Terms of Service update mokojoomtos plugin - 04.02.01 + 04.01.00 site system development - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/dev + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/dev/plg_system_mokojoomtos-04.02.01.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.01.00.zip Moko Consulting