MokoOnyx v01.00.00 — initial release (successor to MokoCassiopeia)
Some checks failed
Standards Compliance / Secret Scanning (push) Successful in 3s
Standards Compliance / License Header Validation (push) Successful in 4s
Standards Compliance / Repository Structure Validation (push) Successful in 5s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Version Consistency Check (push) Successful in 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 2s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 4s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 3s
Standards Compliance / Code Duplication Detection (push) Successful in 4s
Standards Compliance / Dead Code Detection (push) Successful in 3s
Standards Compliance / File Size Limits (push) Successful in 2s
CodeQL Security Scanning / Analyze (javascript) (push) Failing after 1m9s
Standards Compliance / Binary File Detection (push) Successful in 4s
CodeQL Security Scanning / Analyze (actions) (push) Failing after 1m11s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 5s
Standards Compliance / Broken Link Detection (push) Successful in 5s
Standards Compliance / Unused Dependencies Check (push) Successful in 7s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Enterprise Readiness Check (push) Successful in 3s
Standards Compliance / Repository Health Check (push) Successful in 4s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
CodeQL Security Scanning / Security Scan Summary (push) Successful in 1s
Standards Compliance / Compliance Summary (push) Successful in 1s
Repo Health / Access control (push) Successful in 1s
Auto-Update SHA Hash / Update SHA-256 Hash in updates.xml (release) Successful in 4s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s

All files renamed from mokocassiopeia to mokoonyx.
Update server points to MokoOnyx repo.
Bridge migration removed (clean standalone template).
Version reset to 01.00.00.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-04-19 17:19:03 -05:00
parent 3ba2214614
commit 8258ed804a
238 changed files with 85443 additions and 2 deletions

41
.editorconfig Normal file
View File

@@ -0,0 +1,41 @@
# EditorConfig helps maintain consistent coding styles across different editors and IDEs
# https://editorconfig.org/
root = true
# Default settings — Tabs preferred, width = 2 spaces
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
tab_width = 2
# PowerShell scripts — tabs, 2-space visual width
[*.ps1]
indent_style = tab
tab_width = 2
end_of_line = crlf
# Markdown files — keep trailing whitespace for line breaks
[*.md]
trim_trailing_whitespace = false
# JSON / YAML files — tabs, 2-space visual width
[*.{json,yml,yaml}]
indent_style = tab
tab_width = 2
# Makefiles — always tabs, default width
[Makefile]
indent_style = tab
tab_width = 2
# Windows batch scripts — keep CRLF endings
[*.{bat,cmd}]
end_of_line = crlf
# Shell scripts — ensure LF endings
[*.sh]
end_of_line = lf

17
.gitattributes vendored Normal file
View File

@@ -0,0 +1,17 @@
# Force LF line endings for all text files
* text=auto eol=lf
# Explicitly mark binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg text eol=lf
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.zip binary
*.gz binary
*.tar binary

1
.github/.mokostandards vendored Normal file
View File

@@ -0,0 +1 @@
platform: waas-component

304
.github/CLAUDE.md vendored Normal file
View File

@@ -0,0 +1,304 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: MokoStandards.Templates.GitHub
INGROUP: MokoStandards.Templates
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
PATH: /templates/github/copilot-instructions.joomla.md.template
VERSION: XX.YY.ZZ
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
Tokens replaced at sync time: MokoOnyx, https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx, {{EXTENSION_NAME}},
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
-->
> [!IMPORTANT]
> **🔧 AI Self-Update Required on First Use**
>
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
> this repository — or this is a brand-new repo.
>
> **On your very first interaction in this repository, before answering any other question:**
>
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
> 2. If they do, locate the values as described in the table below and replace every occurrence
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
> through the closing `> ---` line) in both files.
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
> values — all set."*
>
> | Placeholder | Where to find the value |
> |---|---|
> | `MokoOnyx` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
> | `https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx` | Full GitHub URL, e.g. `https://git.mokoconsulting.tech/MokoConsulting/<repo-name>` |
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
>
> ---
# MokoOnyx — GitHub Copilot Custom Instructions
## What This Repo Is
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
Repository URL: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
Extension name: **{{EXTENSION_NAME}}**
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
Platform: **Joomla 4.x / MokoWaaS**
---
## Primary Language
**PHP** (≥ 7.4) is the primary language for this Joomla extension. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
---
## File Header — Always Required on New Files
Every new file needs a copyright header as its first content.
**PHP:**
```php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoOnyx.{{EXTENSION_TYPE}}
* INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /path/to/file.php
* VERSION: XX.YY.ZZ
* BRIEF: One-line description of purpose
*/
defined('_JEXEC') or die;
```
**Markdown:**
```markdown
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: MokoOnyx.Documentation
INGROUP: MokoOnyx
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
PATH: /docs/file.md
VERSION: XX.YY.ZZ
BRIEF: One-line description
-->
```
**YAML / Shell / XML:** Use the appropriate comment syntax with the same fields. JSON files are exempt.
---
## Version Management
**`README.md` is the single source of truth for the repository version.**
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03``01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references.
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
### Joomla Version Alignment
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
```xml
<!-- In manifest.xml — must match README.md version -->
<version>01.02.04</version>
<!-- In updates.xml — prepend a new <update> block for every release.
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
in the XML attribute value. Joomla's update server treats the value as a
regular expression, so \. matches a literal dot. -->
<updates>
<update>
<name>{{EXTENSION_NAME}}</name>
<version>01.02.04</version>
<downloads>
<downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="4\.[0-9]+" />
</update>
<!-- … older entries preserved below … -->
</updates>
```
---
## Joomla Extension Structure
```
MokoOnyx/
├── manifest.xml # Joomla installer manifest (root — required)
├── updates.xml # Update server manifest (root — required, see below)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ └── views/
├── admin/ # Backend (admin) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ ├── views/
│ └── sql/
├── language/ # Language INI files
├── media/ # CSS, JS, images (deployed to /media/{{EXTENSION_ELEMENT}}/)
├── docs/ # Technical documentation
├── tests/ # Test suite
├── .github/
│ ├── workflows/
│ ├── copilot-instructions.md # This file
│ └── CLAUDE.md
├── README.md # Version source of truth
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE # GPL-3.0-or-later
└── Makefile # Build automation
```
---
## updates.xml — Required in Repo Root
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
The `manifest.xml` must reference it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/main/updates.xml
</server>
</updateservers>
```
**Rules:**
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
---
## manifest.xml Rules
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
- Must include `<files folder="site">` and `<administration>` sections.
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
---
## GitHub Actions — Token Usage
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
```yaml
# ✅ Correct
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
```
```yaml
# ❌ Wrong — never use these in workflows
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
```
---
## MokoStandards Reference
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). Authoritative policies:
| Document | Purpose |
|----------|---------|
| [file-header-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
| [coding-style-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
| [branching-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
| [changelog-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
---
## Naming Conventions
| Context | Convention | Example |
|---------|-----------|---------|
| PHP class | `PascalCase` | `MyController` |
| PHP method / function | `camelCase` | `getItems()` |
| PHP variable | `$snake_case` | `$item_id` |
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
| PHP class file | `PascalCase.php` | `ItemModel.php` |
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` |
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
---
## Commit Messages
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
---
## Branch Naming
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
---
## Keeping Documentation Current
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
---
## Key Constraints
- Never commit directly to `main` — all changes go via PR, squash-merged
- Never skip the FILE INFORMATION block on a new file
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync

55
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,55 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# CODEOWNERS — require approval from jmiller-moko for protected paths
# Synced from MokoStandards. Do not edit manually.
#
# Changes to these paths require review from the listed owners before merge.
# Combined with branch protection (require PR reviews), this prevents
# unauthorized modifications to workflows, configs, and governance files.
# ── Synced workflows (managed by MokoStandards — do not edit manually) ────
/.github/workflows/deploy-dev.yml @jmiller-moko
/.github/workflows/deploy-demo.yml @jmiller-moko
/.github/workflows/deploy-manual.yml @jmiller-moko
/.github/workflows/auto-release.yml @jmiller-moko
/.github/workflows/auto-dev-issue.yml @jmiller-moko
/.github/workflows/auto-assign.yml @jmiller-moko
/.github/workflows/sync-version-on-merge.yml @jmiller-moko
/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko
/.github/workflows/repository-cleanup.yml @jmiller-moko
/.github/workflows/standards-compliance.yml @jmiller-moko
/.github/workflows/codeql-analysis.yml @jmiller-moko
/.github/workflows/repo_health.yml @jmiller-moko
/.github/workflows/ci-joomla.yml @jmiller-moko
/.github/workflows/update-server.yml @jmiller-moko
/.github/workflows/deploy-manual.yml @jmiller-moko
/.github/workflows/ci-dolibarr.yml @jmiller-moko
/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko
/.github/workflows/changelog-validation.yml @jmiller-moko
/.github/workflows/branch-freeze.yml @jmiller-moko
# Custom workflows in .github/workflows/ not listed above are repo-owned.
# ── GitHub configuration ─────────────────────────────────────────────────
/.github/ISSUE_TEMPLATE/ @jmiller-moko
/.github/CODEOWNERS @jmiller-moko
/.github/copilot.yml @jmiller-moko
/.github/copilot-instructions.md @jmiller-moko
/.github/CLAUDE.md @jmiller-moko
/.github/.mokostandards @jmiller-moko
# ── Build and config files ───────────────────────────────────────────────
/composer.json @jmiller-moko
/phpstan.neon @jmiller-moko
/Makefile @jmiller-moko
/.ftpignore @jmiller-moko
/.gitignore @jmiller-moko
/.gitattributes @jmiller-moko
/.editorconfig @jmiller-moko
# ── Governance documents ─────────────────────────────────────────────────
/LICENSE @jmiller-moko
/CONTRIBUTING.md @jmiller-moko
/SECURITY.md @jmiller-moko
/GOVERNANCE.md @jmiller-moko
/CODE_OF_CONDUCT.md @jmiller-moko

110
.github/ISSUE_TEMPLATE/adr.md vendored Normal file
View File

@@ -0,0 +1,110 @@
---
name: Architecture Decision Record (ADR)
about: Propose or document an architectural decision
title: '[ADR] '
labels: 'architecture, decision'
assignees: ''
---
## ADR Number
ADR-XXXX
## Status
- [ ] Proposed
- [ ] Accepted
- [ ] Deprecated
- [ ] Superseded by ADR-XXXX
## Context
Describe the issue or problem that motivates this decision.
## Decision
State the architecture decision and provide rationale.
## Consequences
### Positive
- List positive consequences
### Negative
- List negative consequences or trade-offs
### Neutral
- List neutral aspects
## Alternatives Considered
### Alternative 1
- Description
- Pros
- Cons
- Why not chosen
### Alternative 2
- Description
- Pros
- Cons
- Why not chosen
## Implementation Plan
1. Step 1
2. Step 2
3. Step 3
## Stakeholders
- **Decision Makers**: @user1, @user2
- **Consulted**: @user3, @user4
- **Informed**: team-name
## Technical Details
### Architecture Diagram
```
[Add diagram or link]
```
### Dependencies
- Dependency 1
- Dependency 2
### Impact Analysis
- **Performance**: [Impact description]
- **Security**: [Impact description]
- **Scalability**: [Impact description]
- **Maintainability**: [Impact description]
## Testing Strategy
- [ ] Unit tests
- [ ] Integration tests
- [ ] Performance tests
- [ ] Security tests
## Documentation
- [ ] Architecture documentation updated
- [ ] API documentation updated
- [ ] Developer guide updated
- [ ] Runbook created
## Migration Path
Describe how to migrate from current state to new architecture.
## Rollback Plan
Describe how to rollback if issues occur.
## Timeline
- **Proposal Date**:
- **Decision Date**:
- **Implementation Start**:
- **Expected Completion**:
## References
- Related ADRs:
- External resources:
- RFCs:
## Review Checklist
- [ ] Aligns with enterprise architecture principles
- [ ] Security implications reviewed
- [ ] Performance implications reviewed
- [ ] Cost implications reviewed
- [ ] Compliance requirements met
- [ ] Team consensus achieved

48
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,48 @@
---
name: Bug Report
about: Report a bug or issue with the project
title: '[BUG] '
labels: 'bug'
assignees: ''
---
## Bug Description
A clear and concise description of what the bug is.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
## Expected Behavior
A clear and concise description of what you expected to happen.
## Actual Behavior
A clear and concise description of what actually happened.
## Screenshots
If applicable, add screenshots to help explain your problem.
## Environment
- **Project**: [e.g., MokoDoliTools, moko-cassiopeia]
- **Version**: [e.g., 1.2.3]
- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0]
- **PHP Version**: [e.g., 8.1]
- **Database**: [e.g., MySQL 8.0, PostgreSQL 14]
- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121]
- **OS**: [e.g., Ubuntu 22.04, Windows 11]
## Additional Context
Add any other context about the problem here.
## Possible Solution
If you have suggestions on how to fix the issue, please describe them here.
## Checklist
- [ ] I have searched for similar issues before creating this one
- [ ] I have provided all the requested information
- [ ] I have tested this on the latest stable version
- [ ] I have checked the documentation and couldn't find a solution

18
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
---
blank_issues_enabled: true
contact_links:
- name: 💼 Enterprise Support
url: https://mokoconsulting.tech/enterprise
about: Enterprise-level support and consultation services
- name: 💬 Ask a Question
url: https://mokoconsulting.tech/
about: Get help or ask questions through our website
- name: 📚 MokoStandards Documentation
url: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
about: View our coding standards and best practices
- name: 🔒 Report a Security Vulnerability
url: https://git.mokoconsulting.tech/MokoConsulting/.github-private/security/advisories/new
about: Report security vulnerabilities privately (for critical issues)
- name: 💡 Community Discussions
url: https://github.com/orgs/mokoconsulting-tech/discussions
about: Join community discussions and Q&A

52
.github/ISSUE_TEMPLATE/documentation.md vendored Normal file
View File

@@ -0,0 +1,52 @@
---
name: Documentation Issue
about: Report an issue with documentation
title: '[DOCS] '
labels: 'documentation'
assignees: ''
---
## Documentation Issue
**Location**:
<!-- Specify the file, page, or section with the issue -->
## Issue Type
<!-- Mark the relevant option with an "x" -->
- [ ] Typo or grammar error
- [ ] Outdated information
- [ ] Missing documentation
- [ ] Unclear explanation
- [ ] Broken links
- [ ] Missing examples
- [ ] Other (specify below)
## Description
<!-- Clearly describe the documentation issue -->
## Current Content
<!-- Quote or describe the current documentation (if applicable) -->
```
Current text here
```
## Suggested Improvement
<!-- Provide your suggestion for how to improve the documentation -->
```
Suggested text here
```
## Additional Context
<!-- Add any other context, screenshots, or references -->
## Standards Alignment
- [ ] Follows MokoStandards documentation guidelines
- [ ] Uses en_US/en_GB localization
- [ ] Includes proper SPDX headers where applicable
## Checklist
- [ ] I have searched for similar documentation issues
- [ ] I have provided a clear description
- [ ] I have suggested an improvement (if applicable)

View File

@@ -0,0 +1,85 @@
---
name: Enterprise Support Request
about: Request enterprise-level support or consultation
title: '[ENTERPRISE] '
labels: 'enterprise, support'
assignees: ''
---
## Support Request Type
- [ ] Critical Production Issue
- [ ] Performance Optimization
- [ ] Security Audit
- [ ] Architecture Review
- [ ] Custom Development
- [ ] Migration Support
- [ ] Training & Onboarding
- [ ] Other (please specify)
## Priority Level
- [ ] P0 - Critical (Production Down)
- [ ] P1 - High (Major Feature Broken)
- [ ] P2 - Medium (Non-Critical Issue)
- [ ] P3 - Low (Enhancement/Question)
## Organization Details
- **Company Name**:
- **Contact Person**:
- **Email**:
- **Phone** (for P0/P1 issues):
- **Timezone**:
## Issue Description
Provide a clear and detailed description of your request or issue.
## Business Impact
Describe the impact on your business operations:
- Number of users affected:
- Revenue impact (if applicable):
- Deadline/SLA requirements:
## Environment Details
- **Deployment Type**: [On-Premise / Cloud / Hybrid]
- **Platform**: [Joomla / Dolibarr / Custom]
- **Version**:
- **Infrastructure**: [AWS / Azure / GCP / Other]
- **Scale**: [Users / Transactions / Data Volume]
## Current Configuration
```yaml
# Paste relevant configuration (sanitize sensitive data)
```
## Logs and Diagnostics
```
# Paste relevant logs (sanitize sensitive data)
```
## Attempted Solutions
Describe any troubleshooting steps already taken.
## Expected Resolution
Describe your expected outcome or resolution.
## Additional Resources
- **Documentation Links**:
- **Related Issues**:
- **Screenshots/Videos**:
## Enterprise SLA
- [ ] Standard Support (initial response within 13 weeks)
- [ ] Premium Support (initial response within 5 business days)
- [ ] Critical Support (initial response within 72 hours)
- [ ] Custom SLA (specify):
## Compliance Requirements
- [ ] GDPR
- [ ] HIPAA
- [ ] SOC 2
- [ ] ISO 27001
- [ ] Other (specify):
---
**Note**: Enterprise support requests require an active support contract. If you don't have one, please contact us at enterprise@mokoconsulting.tech

View File

@@ -0,0 +1,51 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: '[FEATURE] '
labels: 'enhancement'
assignees: ''
---
## Feature Description
A clear and concise description of the feature you'd like to see.
## Problem or Use Case
Describe the problem this feature would solve or the use case it addresses.
Ex. I'm always frustrated when [...]
## Proposed Solution
A clear and concise description of what you want to happen.
## Alternative Solutions
A clear and concise description of any alternative solutions or features you've considered.
## Benefits
Describe how this feature would benefit users:
- Who would use this feature?
- What problems does it solve?
- What value does it add?
## Implementation Details (Optional)
If you have ideas about how this could be implemented, share them here:
- Technical approach
- Files/components that might need changes
- Any concerns or challenges you foresee
## Additional Context
Add any other context, mockups, or screenshots about the feature request here.
## Relevant Standards
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] Accessibility (WCAG 2.1 AA)
- [ ] Localization (en_US/en_GB)
- [ ] Security best practices
- [ ] Code quality standards
- [ ] Other: [specify]
## Checklist
- [ ] I have searched for similar feature requests before creating this one
- [ ] I have clearly described the use case and benefits
- [ ] I have considered alternative solutions
- [ ] This feature aligns with the project's goals and scope

View File

@@ -0,0 +1,190 @@
---
name: Firewall Request
about: Request firewall rule changes or access to external resources
title: '[FIREWALL] [Resource Name] - [Brief Description]'
labels: ['firewall-request', 'infrastructure', 'security']
assignees: []
---
## Firewall Request
### Request Type
- [ ] Allow outbound access to external service/API
- [ ] Allow inbound access from external source
- [ ] Modify existing firewall rule
- [ ] Remove/revoke firewall rule
- [ ] Other (specify):
### Resource Information
**Service/Domain Name**:
**IP Address(es)**:
**Port(s)**:
**Protocol**:
- [ ] HTTP (80)
- [ ] HTTPS (443)
- [ ] SSH (22)
- [ ] FTP (21)
- [ ] SFTP (22)
- [ ] Custom (specify): _______________
### Requestor Information
**Name**:
**GitHub Username**: @
**Email**: @mokoconsulting.tech
**Team/Department**:
**Manager**: @
### Business Justification
**Why is this access needed?**
**Which project(s) require this access?**
**What functionality will break without this access?**
**Is there an alternative solution?**
- [ ] Yes (explain):
- [ ] No
### Security Considerations
**Data Classification**:
- [ ] Public
- [ ] Internal
- [ ] Confidential
- [ ] Restricted
**Sensitive Data Transmission**:
- [ ] No sensitive data will be transmitted
- [ ] Sensitive data will be transmitted (encryption required)
- [ ] Authentication credentials will be transmitted (secure storage required)
**Third-Party Service**:
- [ ] This is a trusted/verified third-party service
- [ ] This is a new/unverified service (security review required)
**Service Documentation**:
(Provide link to service documentation or API specs)
### Access Scope
**Affected Systems**:
- [ ] Development environment only
- [ ] Staging environment only
- [ ] Production environment
- [ ] All environments
**Access Duration**:
- [ ] Permanent (ongoing business need)
- [ ] Temporary (specify end date): _______________
- [ ] Testing only (specify duration): _______________
### Technical Details
**Source System(s)**:
(Which internal systems need access?)
**Destination System(s)**:
(Which external systems need to be accessed?)
**Expected Traffic Volume**:
(e.g., requests per hour/day)
**Traffic Pattern**:
- [ ] Continuous
- [ ] Periodic (specify frequency): _______________
- [ ] On-demand/manual
- [ ] Scheduled (specify schedule): _______________
### Testing Requirements
**Pre-Production Testing**:
- [ ] Request includes dev/staging access for testing
- [ ] Testing can be done with production access only
- [ ] No testing required (modify existing rule)
**Testing Plan**:
**Rollback Plan**:
(What happens if access needs to be revoked?)
### Compliance & Audit
**Compliance Requirements**:
- [ ] GDPR considerations
- [ ] SOC 2 compliance required
- [ ] PCI DSS considerations
- [ ] Other regulatory requirements: _______________
- [ ] No specific compliance requirements
**Audit/Logging Requirements**:
- [ ] Standard logging sufficient
- [ ] Enhanced logging/monitoring required
- [ ] Real-time alerting required
### Urgency
- [ ] Critical (production down, immediate access needed)
- [ ] High (needed within 24 hours)
- [ ] Normal (needed within 1 week)
- [ ] Low priority (needed within 1 month)
**If critical/high urgency, explain why:**
### Approvals
**Manager Approval**:
- [ ] Manager has been notified and approves this request
**Security Team Review Required**:
- [ ] Yes (new external service, sensitive data)
- [ ] No (minor change, established service)
### Additional Information
**Related Documentation**:
(Links to relevant docs, RFCs, tickets, etc.)
**Dependencies**:
(Other systems or changes this depends on)
**Comments/Questions**:
---
## For Infrastructure/Security Team Use Only
**Do not edit below this line**
### Security Review
- [ ] Security team review completed
- [ ] Risk assessment: Low / Medium / High
- [ ] Encryption required: Yes / No
- [ ] VPN required: Yes / No
- [ ] Additional security controls: _______________
**Reviewed By**: @_______________
**Review Date**: _______________
**Review Notes**:
### Implementation
- [ ] Firewall rule created/modified
- [ ] Rule tested in dev/staging
- [ ] Rule deployed to production
- [ ] Monitoring/alerting configured
- [ ] Documentation updated
**Firewall Rule ID**: _______________
**Implementation Date**: _______________
**Implemented By**: @_______________
**Configuration Details**:
```
Source:
Destination:
Port/Protocol:
Action: Allow/Deny
```
### Verification
- [ ] Requestor confirmed access working
- [ ] Logs reviewed (no anomalies)
- [ ] Security scan completed (if applicable)
**Verification Date**: _______________
**Verified By**: @_______________
### Notes

87
.github/ISSUE_TEMPLATE/joomla_issue.md vendored Normal file
View File

@@ -0,0 +1,87 @@
---
name: Joomla Extension Issue
about: Report an issue with a Joomla extension
title: '[JOOMLA] '
labels: 'joomla'
assignees: ''
---
## Issue Type
- [ ] Component Issue
- [ ] Module Issue
- [ ] Plugin Issue
- [ ] Template Issue
## Extension Details
- **Extension Name**: [e.g., moko-cassiopeia]
- **Extension Version**: [e.g., 1.2.3]
- **Extension Type**: [Component / Module / Plugin / Template]
## Joomla Environment
- **Joomla Version**: [e.g., 4.4.0, 5.0.0]
- **PHP Version**: [e.g., 8.1.0]
- **Database**: [MySQL / PostgreSQL / MariaDB]
- **Database Version**: [e.g., 8.0]
- **Server**: [Apache / Nginx / IIS]
- **Hosting**: [Shared / VPS / Dedicated / Cloud]
## Issue Description
Provide a clear and detailed description of the issue.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Configure '...'
4. See error
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened.
## Error Messages
```
# Paste any error messages from Joomla error logs
# Location: administrator/logs/error.php
```
## Browser Console Errors
```javascript
// Paste any JavaScript console errors (F12 in browser)
```
## Screenshots
Add screenshots to help explain the issue.
## Configuration
```ini
# Paste extension configuration (sanitize sensitive data)
```
## Installed Extensions
List other installed extensions that might conflict:
- Extension 1 (version)
- Extension 2 (version)
## Template Overrides
- [ ] Using template overrides
- [ ] Custom CSS
- [ ] Custom JavaScript
## Additional Context
- **Multilingual Site**: [Yes / No]
- **Cache Enabled**: [Yes / No]
- **Debug Mode**: [Yes / No]
- **SEF URLs**: [Yes / No]
## Checklist
- [ ] I have cleared Joomla cache
- [ ] I have disabled other extensions to test for conflicts
- [ ] I have checked Joomla error logs
- [ ] I have tested with a default Joomla template
- [ ] I have checked browser console for JavaScript errors
- [ ] I have searched for similar issues
- [ ] I am using a supported Joomla version

82
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,82 @@
---
name: Question
about: Ask a question about usage, features, or best practices
title: '[QUESTION] '
labels: ['question']
assignees: []
---
## Question
**Your question:**
## Context
**What are you trying to accomplish?**
**What have you already tried?**
**Category**:
- [ ] Script usage
- [ ] Configuration
- [ ] Workflow setup
- [ ] Documentation interpretation
- [ ] Best practices
- [ ] Integration
- [ ] Other: __________
## Environment (if relevant)
**Your setup**:
- Operating System:
- Version:
## What You've Researched
**Documentation reviewed**:
- [ ] README.md
- [ ] Project documentation
- [ ] Other (specify): __________
**Similar issues/questions found**:
- #
- #
## Expected Outcome
**What result are you hoping for?**
## Code/Configuration Samples
**Relevant code or configuration** (if applicable):
```bash
# Your code here
```
## Additional Context
**Any other relevant information:**
**Screenshots** (if helpful):
## Urgency
- [ ] Urgent (blocking work)
- [ ] Normal (can work on other things meanwhile)
- [ ] Low priority (just curious)
## Checklist
- [ ] I have searched existing issues and discussions
- [ ] I have reviewed relevant documentation
- [ ] I have provided sufficient context
- [ ] I have included code/configuration samples if relevant
- [ ] This is a genuine question (not a bug report or feature request)

View File

@@ -0,0 +1,107 @@
---
name: License Request
about: Request an organization license for Sublime Text
title: '[LICENSE REQUEST] Sublime Text - [Your Name]'
labels: ['license-request', 'admin']
assignees: []
---
## License Request
### Tool Information
**Tool Name**: Sublime Text
**License Type Requested**: Organization Pool
**Personal Purchase**:
- [ ] I prefer to purchase my own license ($99 USD - recommended, immediate access)
- [ ] I prefer an organization license (1-2 business days, organization use only)
- [ ] I have already purchased my own license (registration only for support)
### Requestor Information
**Name**:
**GitHub Username**: @
**Email**: @mokoconsulting.tech
**Team/Department**:
**Manager**: @
### Justification
**Why do you need this license?**
**Primary use case**:
- [ ] Remote development (SFTP to servers)
- [ ] Local development
- [ ] Code review
- [ ] Documentation editing
- [ ] Other (specify):
**Which projects/repositories will you work on?**
**Have you evaluated the free trial?**
- [ ] Yes, I've used the trial and Sublime Text meets my needs
- [ ] No, requesting license before trial
**Alternative tools considered**:
- [ ] VS Code (free alternative)
- [ ] Vim/Neovim (free, terminal-based)
- [ ] Other: _______________
### Platform
- [ ] Windows
- [ ] macOS
- [ ] Linux (distribution: ________)
### Urgency
- [ ] Urgent (needed within 24 hours - please justify)
- [ ] Normal (1-2 business days)
- [ ] Low priority (when available)
**If urgent, please explain why:**
### SFTP Plugin
**Note**: Sublime SFTP plugin ($16 USD) is a **separate personal purchase** and is NOT provided by the organization.
- [ ] I understand SFTP plugin requires separate personal purchase
- [ ] I have already purchased SFTP plugin
- [ ] I will purchase SFTP plugin if needed for my work
- [ ] I don't need SFTP plugin (local development only)
### Acknowledgments
- [ ] I have read the License Management Policy (/docs/github-private/LICENSE_MANAGEMENT.md)
- [ ] I understand organization licenses are for work use only
- [ ] I understand organization licenses must be returned upon leaving
- [ ] I understand personal purchases ($99) are an alternative with lifetime access
- [ ] I understand SFTP plugin ($16) requires separate personal purchase
- [ ] I agree to the terms of use
### Additional Information
**Expected daily usage hours**: _____ hours/day
**Duration of need**:
- [ ] Permanent (ongoing role)
- [ ] Temporary project (_____ months)
- [ ] Trial/Evaluation (_____ weeks)
**Comments/Questions**:
---
## For Admin Use Only
**Do not edit below this line**
- [ ] Manager approval received (@manager-username)
- [ ] License available in pool (current: __/20)
- [ ] License type confirmed (Organization / Personal registration)
- [ ] License key sent via encrypted email
- [ ] Activation confirmed by user
- [ ] Added to license tracking sheet
- [ ] User notified of SFTP plugin requirement
**License Key ID**: _____________
**Date Issued**: _____________
**Issued By**: @_____________
**Notes**:

126
.github/ISSUE_TEMPLATE/rfc.md vendored Normal file
View File

@@ -0,0 +1,126 @@
---
name: Request for Comments (RFC)
about: Propose a significant change for community discussion
title: '[RFC] '
labels: 'rfc, discussion'
assignees: ''
---
## RFC Summary
One-paragraph summary of the proposal.
## Motivation
Why are we doing this? What use cases does it support? What is the expected outcome?
## Detailed Design
### Overview
Provide a detailed explanation of the proposed change.
### API Changes (if applicable)
```php
// Before
function oldApi($param1) { }
// After
function newApi($param1, $param2) { }
```
### User Experience Changes
Describe how users will interact with this change.
### Implementation Approach
High-level implementation strategy.
## Drawbacks
Why should we *not* do this?
## Alternatives
What other designs have been considered? What is the impact of not doing this?
### Alternative 1
- Description
- Trade-offs
### Alternative 2
- Description
- Trade-offs
## Adoption Strategy
How will existing users adopt this? Is this a breaking change?
### Migration Guide
```bash
# Steps to migrate
```
### Deprecation Timeline
- **Announcement**:
- **Deprecation**:
- **Removal**:
## Unresolved Questions
- Question 1
- Question 2
## Future Possibilities
What future work does this enable?
## Impact Assessment
### Performance
Expected performance impact.
### Security
Security considerations and implications.
### Compatibility
- **Backward Compatible**: [Yes / No]
- **Breaking Changes**: [List]
### Maintenance
Long-term maintenance considerations.
## Community Input
### Stakeholders
- [ ] Core team
- [ ] Module developers
- [ ] End users
- [ ] Enterprise customers
### Feedback Period
**Duration**: [e.g., 2 weeks]
**Deadline**: [date]
## Implementation Timeline
### Phase 1: Design
- [ ] RFC discussion
- [ ] Design finalization
- [ ] Approval
### Phase 2: Implementation
- [ ] Core implementation
- [ ] Tests
- [ ] Documentation
### Phase 3: Release
- [ ] Beta release
- [ ] Feedback collection
- [ ] Stable release
## Success Metrics
How will we measure success?
- Metric 1
- Metric 2
## References
- Related RFCs:
- External documentation:
- Prior art:
## Open Questions for Community
1. Question 1?
2. Question 2?
---
**Note**: This RFC is open for community discussion. Please provide feedback in the comments below.

51
.github/ISSUE_TEMPLATE/security.md vendored Normal file
View File

@@ -0,0 +1,51 @@
---
name: Security Vulnerability Report
about: Report a security vulnerability (use only for non-critical issues)
title: '[SECURITY] '
labels: 'security'
assignees: ''
---
## ⚠️ IMPORTANT: Private Disclosure Required
**For critical security vulnerabilities, DO NOT use this template.**
Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure.
Use this template only for:
- Security improvements
- Non-critical security suggestions
- Security documentation updates
---
## Security Issue
**Severity**:
<!-- Low, Medium, or informational only -->
## Description
<!-- Describe the security concern or improvement suggestion -->
## Affected Components
<!-- List the affected files, features, or components -->
## Suggested Mitigation
<!-- Describe how this could be addressed -->
## Standards Reference
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] SPDX license identifiers
- [ ] Secret management
- [ ] Dependency security
- [ ] Access control
- [ ] Other: [specify]
## Additional Context
<!-- Add any other context about the security concern -->
## Checklist
- [ ] This is NOT a critical vulnerability requiring private disclosure
- [ ] I have reviewed the SECURITY.md policy
- [ ] I have provided sufficient detail for evaluation

304
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,304 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: MokoStandards.Templates.GitHub
INGROUP: MokoStandards.Templates
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
PATH: /templates/github/copilot-instructions.joomla.md.template
VERSION: XX.YY.ZZ
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
Tokens replaced at sync time: MokoOnyx, https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx, {{EXTENSION_NAME}},
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
-->
> [!IMPORTANT]
> **🔧 AI Self-Update Required on First Use**
>
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
> this repository — or this is a brand-new repo.
>
> **On your very first interaction in this repository, before answering any other question:**
>
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
> 2. If they do, locate the values as described in the table below and replace every occurrence
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
> through the closing `> ---` line) in both files.
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
> values — all set."*
>
> | Placeholder | Where to find the value |
> |---|---|
> | `MokoOnyx` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
> | `https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx` | Full GitHub URL, e.g. `https://git.mokoconsulting.tech/MokoConsulting/<repo-name>` |
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
>
> ---
# MokoOnyx — GitHub Copilot Custom Instructions
## What This Repo Is
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
Repository URL: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
Extension name: **{{EXTENSION_NAME}}**
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
Platform: **Joomla 4.x / MokoWaaS**
---
## Primary Language
**PHP** (≥ 7.4) is the primary language for this Joomla extension. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
---
## File Header — Always Required on New Files
Every new file needs a copyright header as its first content.
**PHP:**
```php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoOnyx.{{EXTENSION_TYPE}}
* INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /path/to/file.php
* VERSION: XX.YY.ZZ
* BRIEF: One-line description of purpose
*/
defined('_JEXEC') or die;
```
**Markdown:**
```markdown
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: MokoOnyx.Documentation
INGROUP: MokoOnyx
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
PATH: /docs/file.md
VERSION: XX.YY.ZZ
BRIEF: One-line description
-->
```
**YAML / Shell / XML:** Use the appropriate comment syntax with the same fields. JSON files are exempt.
---
## Version Management
**`README.md` is the single source of truth for the repository version.**
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03``01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references.
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
### Joomla Version Alignment
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
```xml
<!-- In manifest.xml — must match README.md version -->
<version>01.02.04</version>
<!-- In updates.xml — prepend a new <update> block for every release.
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
in the XML attribute value. Joomla's update server treats the value as a
regular expression, so \. matches a literal dot. -->
<updates>
<update>
<name>{{EXTENSION_NAME}}</name>
<version>01.02.04</version>
<downloads>
<downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="4\.[0-9]+" />
</update>
<!-- … older entries preserved below … -->
</updates>
```
---
## Joomla Extension Structure
```
MokoOnyx/
├── manifest.xml # Joomla installer manifest (root — required)
├── updates.xml # Update server manifest (root — required, see below)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ └── views/
├── admin/ # Backend (admin) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ ├── views/
│ └── sql/
├── language/ # Language INI files
├── media/ # CSS, JS, images (deployed to /media/{{EXTENSION_ELEMENT}}/)
├── docs/ # Technical documentation
├── tests/ # Test suite
├── .github/
│ ├── workflows/
│ ├── copilot-instructions.md # This file
│ └── CLAUDE.md
├── README.md # Version source of truth
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE # GPL-3.0-or-later
└── Makefile # Build automation
```
---
## updates.xml — Required in Repo Root
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
The `manifest.xml` must reference it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/main/updates.xml
</server>
</updateservers>
```
**Rules:**
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
---
## manifest.xml Rules
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
- Must include `<files folder="site">` and `<administration>` sections.
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
---
## GitHub Actions — Token Usage
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
```yaml
# ✅ Correct
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
```
```yaml
# ❌ Wrong — never use these in workflows
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
```
---
## MokoStandards Reference
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). Authoritative policies:
| Document | Purpose |
|----------|---------|
| [file-header-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
| [coding-style-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
| [branching-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
| [changelog-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
---
## Naming Conventions
| Context | Convention | Example |
|---------|-----------|---------|
| PHP class | `PascalCase` | `MyController` |
| PHP method / function | `camelCase` | `getItems()` |
| PHP variable | `$snake_case` | `$item_id` |
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
| PHP class file | `PascalCase.php` | `ItemModel.php` |
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` |
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
---
## Commit Messages
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
---
## Branch Naming
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
---
## Keeping Documentation Current
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
---
## Key Constraints
- Never commit directly to `main` — all changes go via PR, squash-merged
- Never skip the FILE INFORMATION block on a new file
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync

137
.github/copilot.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
# GitHub Copilot Configuration
# This file configures GitHub Copilot settings for the repository
# Allowed domains for Copilot to access
# These domains are trusted sources that Copilot can fetch information from
allowed_domains:
# Standard license providers
- "www.gnu.org" # GNU licenses (GPL, LGPL, AGPL)
- "opensource.org" # Open Source Initiative
- "choosealicense.com" # GitHub's license chooser
- "spdx.org" # Software Package Data Exchange
- "creativecommons.org" # Creative Commons licenses
- "apache.org" # Apache Software Foundation
- "fsf.org" # Free Software Foundation
# Documentation and standards
- "semver.org" # Semantic Versioning
- "keepachangelog.com" # Changelog standards
- "conventionalcommits.org" # Commit message standards
# GitHub and related
- "github.com" # GitHub main site
- "docs.github.com" # GitHub documentation
- "raw.githubusercontent.com" # GitHub raw content
# Package managers and registries
- "npmjs.com" # npm registry
- "pypi.org" # Python Package Index
- "packagist.org" # PHP Composer packages
- "rubygems.org" # Ruby gems
# Standards and specifications
- "json-schema.org" # JSON Schema
- "w3.org" # W3C standards
- "ietf.org" # IETF RFCs and standards
# PHP and Joomla specific
- "joomla.org" # Joomla CMS
- "docs.joomla.org" # Joomla documentation
- "downloads.joomla.org" # Joomla core downloads
- "php.net" # PHP documentation
- "getcomposer.org" # Composer dependency manager
- "packagist.org" # Composer package registry (also listed under packages)
# Dolibarr specific
- "dolibarr.org" # Dolibarr ERP/CRM
- "wiki.dolibarr.org" # Dolibarr wiki
- "docs.dolibarr.org" # Dolibarr developer documentation
# Moko Consulting
- "mokoconsulting.tech" # Moko Consulting main site
- "*.mokoconsulting.tech" # All Moko Consulting subdomains (API, docs, CDN, etc.)
# Google services
- "drive.google.com" # Google Drive (file sharing and assets)
- "docs.google.com" # Google Docs
- "sheets.google.com" # Google Sheets
- "accounts.google.com" # Google authentication
- "storage.googleapis.com" # Google Cloud Storage
- "*.googleapis.com" # Google APIs (Maps, Fonts, etc.)
- "*.googleusercontent.com" # Google user-uploaded content and CDN
- "fonts.googleapis.com" # Google Fonts CSS
- "fonts.gstatic.com" # Google Fonts static assets
# GitHub extended
- "api.github.com" # GitHub REST API
- "upload.github.com" # GitHub file uploads
- "objects.githubusercontent.com" # GitHub release assets and LFS
- "user-images.githubusercontent.com" # GitHub issue/PR image attachments
- "codeload.github.com" # GitHub archive downloads
- "ghcr.io" # GitHub Container Registry
- "pkg.github.com" # GitHub Packages
# Developer reference
- "developer.mozilla.org" # MDN Web Docs
- "stackoverflow.com" # Stack Overflow
- "git-scm.com" # Git documentation
# CDN and infrastructure
- "cdn.jsdelivr.net" # jsDelivr CDN
- "unpkg.com" # unpkg CDN
- "cdnjs.cloudflare.com" # Cloudflare CDN
- "img.shields.io" # Shields.io badge images
- "shields.io" # Shields.io badge service
# Container registries
- "hub.docker.com" # Docker Hub
- "registry-1.docker.io" # Docker registry pulls
- "index.docker.io" # Docker index
# CI / code quality
- "codecov.io" # Code coverage reporting
- "coveralls.io" # Coveralls coverage service
- "sonarcloud.io" # SonarCloud static analysis
# Terraform / infrastructure
- "registry.terraform.io" # Terraform provider registry
- "releases.hashicorp.com" # HashiCorp release downloads
- "checkpoint-api.hashicorp.com" # HashiCorp update checks
# Settings for code generation and suggestions
copilot:
# Enable Copilot for this repository
enabled: true
# File patterns to include for Copilot suggestions
include:
- "**/*.py"
- "**/*.js"
- "**/*.php"
- "**/*.md"
- "**/*.yml"
- "**/*.yaml"
- "**/*.json"
- "**/*.xml"
- "**/*.sh"
# File patterns to exclude from Copilot suggestions
exclude:
- "**/node_modules/**"
- "**/vendor/**"
- "**/build/**"
- "**/dist/**"
- "**/.git/**"
- "**/LICENSE"
- "**/CHANGELOG.md"
# Notes:
# ------
# - This configuration allows GitHub Copilot to fetch information from trusted sources
# - License providers are included to help with license text and compliance information
# - Package registries help with dependency management and version checking
# - Standards organizations provide authoritative specifications
# - Platform-specific sites (Joomla, Dolibarr, PHP) support our technology stack
# - All domains listed are well-known, reputable sources in their respective domains
# - This list focuses on read-only access to public information
# - No authentication credentials should be used with these domains

107
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,107 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: GitHub.Dependabot
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.github/dependabot.yml
# VERSION: 03.09.03
# BRIEF: Dependabot configuration for automated dependency updates and security patches
# NOTE: Monitors GitHub Actions for vulnerabilities and keeps ecosystem secure
version: 2
updates:
# Monitor GitHub Actions for security updates
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 1
labels:
- "dependencies"
- "security"
- "automated"
commit-message:
prefix: "chore(deps)"
include: "scope"
reviewers:
- "mokoconsulting-tech/maintainers"
assignees:
- "jmiller-moko"
# Group all updates together
groups:
github-actions:
patterns:
- "*"
# Monitor Python dependencies for security updates
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 1
labels:
- "dependencies"
- "security"
- "automated"
- "python"
commit-message:
prefix: "chore(deps)"
include: "scope"
reviewers:
- "mokoconsulting-tech/maintainers"
assignees:
- "jmiller-moko"
# Group all updates together
groups:
python-dependencies:
patterns:
- "*"
# Monitor npm dependencies for security updates
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 1
labels:
- "dependencies"
- "security"
- "automated"
- "javascript"
commit-message:
prefix: "chore(deps)"
include: "scope"
reviewers:
- "mokoconsulting-tech/maintainers"
assignees:
- "jmiller-moko"
# Group all updates together
groups:
npm-dependencies:
patterns:
- "*"
# Monitor Composer dependencies for security updates
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 1
labels:
- "dependencies"
- "security"
- "automated"
- "php"
commit-message:
prefix: "chore(deps)"
include: "scope"
reviewers:
- "mokoconsulting-tech/maintainers"
assignees:
- "jmiller-moko"
# Group all updates together
groups:
composer-dependencies:
patterns:
- "*"

20
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,20 @@
# Pull Request
## Purpose
## Change Summary
## Testing Evidence
## Risk and Rollback
## Checklist
- [ ] Follows Conventional Commits
- [ ] Tests added or updated
- [ ] Documentation updated if required
- [ ] License header present where applicable
- [ ] Linked issue(s) referenced
## Reviewer Notes
## Review and Approval

76
.github/workflows/auto-assign.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Workflows.Shared
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.github/workflows/auto-assign.yml
# VERSION: 04.06.00
# BRIEF: Auto-assign jmiller to unassigned issues and PRs every 15 minutes
name: Auto-Assign Issues & PRs
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
schedule:
- cron: '0 */12 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
auto-assign:
name: Assign unassigned issues and PRs
runs-on: ubuntu-latest
steps:
- name: Assign unassigned issues
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
ASSIGNEE="jmiller"
echo "## 🏷️ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
ASSIGNED_ISSUES=0
ASSIGNED_PRS=0
# Assign unassigned open issues
ISSUES=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=100&assignee=none" | jq -r '.[].number' 2>/dev/null || true)
for NUM in $ISSUES; do
# Skip PRs (the issues endpoint returns PRs too)
IS_PR=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/$NUM" | jq -r '.pull_request // empty' 2>/dev/null || true)
if [ -z "$IS_PR" ]; then
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/$NUM/assignees" 2>/dev/null -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && {
ASSIGNED_ISSUES=$((ASSIGNED_ISSUES + 1))
echo " Assigned issue #$NUM"
} || true
fi
done
# Assign unassigned open PRs
PRS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/pulls?state=open&per_page=100" | jq -r '.[] | select(.assignees | length == 0) | .number' 2>/dev/null || true)
for NUM in $PRS; do
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/$NUM/assignees" 2>/dev/null -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && {
ASSIGNED_PRS=$((ASSIGNED_PRS + 1))
echo " Assigned PR #$NUM"
} || true
done
echo "| Type | Assigned |" >> $GITHUB_STEP_SUMMARY
echo "|------|----------|" >> $GITHUB_STEP_SUMMARY
echo "| Issues | $ASSIGNED_ISSUES |" >> $GITHUB_STEP_SUMMARY
echo "| Pull Requests | $ASSIGNED_PRS |" >> $GITHUB_STEP_SUMMARY
if [ "$ASSIGNED_ISSUES" -eq 0 ] && [ "$ASSIGNED_PRS" -eq 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ All issues and PRs already have assignees" >> $GITHUB_STEP_SUMMARY
fi

207
.github/workflows/auto-dev-issue.yml vendored Normal file
View File

@@ -0,0 +1,207 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/shared/auto-dev-issue.yml.template
# VERSION: 04.06.00
# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow
# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos.
name: Dev/RC Branch Issue
on:
# Auto-create on RC branch creation
create:
# Manual trigger for dev branches
workflow_dispatch:
inputs:
branch:
description: 'Branch name (e.g., dev/my-feature or dev/04.06)'
required: true
type: string
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
issues: write
jobs:
create-issue:
name: Create version tracking issue
runs-on: ubuntu-latest
if: >-
(github.event_name == 'workflow_dispatch') ||
(github.event.ref_type == 'branch' &&
(startsWith(github.event.ref, 'rc/') ||
startsWith(github.event.ref, 'alpha/') ||
startsWith(github.event.ref, 'beta/')))
steps:
- name: Create tracking issue and sub-issues
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
# For manual dispatch, use input; for auto, use event ref
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
BRANCH="${{ inputs.branch }}"
else
BRANCH="${{ github.event.ref }}"
fi
REPO="${{ github.repository }}"
ACTOR="${{ github.actor }}"
NOW=$(date -u '+%Y-%m-%d %H:%M UTC')
# Determine branch type and version
if [[ "$BRANCH" == rc/* ]]; then
VERSION="${BRANCH#rc/}"
BRANCH_TYPE="Release Candidate"
LABEL_TYPE="type: release"
TITLE_PREFIX="rc"
elif [[ "$BRANCH" == beta/* ]]; then
VERSION="${BRANCH#beta/}"
BRANCH_TYPE="Beta"
LABEL_TYPE="type: release"
TITLE_PREFIX="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
VERSION="${BRANCH#alpha/}"
BRANCH_TYPE="Alpha"
LABEL_TYPE="type: release"
TITLE_PREFIX="alpha"
else
VERSION="${BRANCH#dev/}"
BRANCH_TYPE="Development"
LABEL_TYPE="type: feature"
TITLE_PREFIX="feat"
fi
TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}"
# Check for existing issue with same title prefix
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=10" 2>/dev/null \
| jq -r ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1)
if [ -n "$EXISTING" ]; then
echo " Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# ── Define sub-issues for the workflow ─────────────────────────
if [[ "$BRANCH" == rc/* ]]; then
SUB_ISSUES=(
"RC Testing|Verify all features work on rc branch|type: test,release-candidate"
"Regression Testing|Run full regression suite before merge|type: test,release-candidate"
"Version Bump|Bump version in README.md and all headers|type: version,release-candidate"
"Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate"
"Merge to Version Branch|Create PR to version/XX|type: release,needs-review"
)
elif [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
SUB_ISSUES=(
"Testing|Verify features on ${BRANCH_TYPE} branch|type: test,status: in-progress"
"Bug Fixes|Fix issues found during ${BRANCH_TYPE} testing|type: bug,status: pending"
"Promote to Next Stage|Create PR to promote to next release stage|type: release,needs-review"
)
else
SUB_ISSUES=(
"Development|Implement feature/fix on dev branch|type: feature,status: in-progress"
"Unit Testing|Write and pass unit tests|type: test,status: pending"
"Code Review|Request and complete code review|needs-review,status: pending"
"Version Bump|Bump version in README.md and all headers|type: version,status: pending"
"Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending"
"Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending"
"Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending"
)
fi
# ── Create sub-issues first ───────────────────────────────────────
SUB_LIST=""
SUB_NUMBERS=""
for SUB in "${SUB_ISSUES[@]}"; do
IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \
"$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH")
SUB_URL=$(gh issue create \
--repo "$REPO" \
--title "$SUB_FULL_TITLE" \
--body "$SUB_BODY" \
--label "${SUB_LABELS}" \
--assignee "jmiller" 2>&1)
SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$')
if [ -n "$SUB_NUM" ]; then
SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})"
SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}"
fi
sleep 0.3
done
# ── Create parent tracking issue ──────────────────────────────────
PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \
"$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST")
PARENT_URL=$(gh issue create \
--repo "$REPO" \
--title "$TITLE" \
--body "$PARENT_BODY" \
--label "${LABEL_TYPE},version" \
--assignee "jmiller" 2>&1)
PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$')
# ── Link sub-issues back to parent ────────────────────────────────
if [ -n "$PARENT_NUM" ]; then
for SUB in "${SUB_ISSUES[@]}"; do
IFS='|' read -r SUB_TITLE _ _ <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_NUM=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=20" 2>/dev/null \
| jq -r ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1)
if [ -n "$SUB_NUM" ]; then
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" 2>/dev/null -X PATCH \
-f body="$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" | jq -r '.body' 2>/dev/null)
> **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true
fi
sleep 0.2
done
fi
# ── Create or update prerelease for alpha/beta/rc ────────────────
if [[ "$BRANCH" == rc/* ]] || [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
case "$BRANCH_TYPE" in
Alpha) RELEASE_TAG="alpha" ;;
Beta) RELEASE_TAG="beta" ;;
"Release Candidate") RELEASE_TAG="release-candidate" ;;
esac
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--title "${RELEASE_TAG} (${VERSION})" \
--notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
--prerelease \
--target main 2>/dev/null || true
echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
gh release edit "$RELEASE_TAG" \
--title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true
echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
fi
fi
# ── Summary ───────────────────────────────────────────────────────
echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY
echo "|------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY
echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY

563
.github/workflows/auto-release.yml vendored Normal file
View File

@@ -0,0 +1,563 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/joomla/auto-release.yml.template
# VERSION: 04.06.00
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
#
# +========================================================================+
# | BUILD & RELEASE PIPELINE (JOOMLA) |
# +========================================================================+
# | |
# | Triggers on push to main (skips bot commits + [skip ci]): |
# | |
# | Every push: |
# | 1. Read version from README.md |
# | 3. Set platform version (Joomla <version>) |
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
# | 5. Write updates.xml (Joomla update server XML) |
# | 6. Create git tag vXX.YY.ZZ |
# | 7a. Patch: update existing GitHub Release for this minor |
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
# | |
# | Every version change: archives main -> version/XX.YY branch |
# | Patch 00 = development (no release). First release = patch 01. |
# | First release only (patch == 01): |
# | 7b. Create new GitHub Release |
# | |
# +========================================================================+
name: Build & Release
on:
pull_request:
types: [closed]
branches:
- main
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
jobs:
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN || github.token }}
fetch-depth: 0
- name: Set authenticated push URL
run: git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Setup MokoStandards tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch version/04 --quiet \
"https://x-access-token:${GH_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoStandards-API.git" \
/tmp/mokostandards-api
cd /tmp/mokostandards-api
composer install --no-dev --no-interaction --quiet
# -- STEP 1: Read version -----------------------------------------------
- name: "Step 1: Read version from README.md"
id: version
run: |
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null)
if [ -z "$VERSION" ]; then
echo "No VERSION in README.md — skipping release"
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=v${MAJOR}" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch 00 = development — skipping release)"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "01" ]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (first release — full pipeline)"
else
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch — platform version + badges only)"
fi
fi
- name: Check if already released
if: steps.version.outputs.skip != 'true'
id: check
run: |
TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
TAG_EXISTS=false
BRANCH_EXISTS=false
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
if [ "$TAG_EXISTS" = "true" ] && [ "$BRANCH_EXISTS" = "true" ]; then
echo "already_released=true" >> "$GITHUB_OUTPUT"
else
echo "already_released=false" >> "$GITHUB_OUTPUT"
fi
# -- SANITY CHECKS -------------------------------------------------------
- name: "Sanity: Pre-release validation"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
ERRORS=0
echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# -- Version drift check (must pass before release) --------
README_VER=$(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)
if [ "$README_VER" != "$VERSION" ]; then
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check CHANGELOG version matches
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
# Check composer.json version if present
if [ -f "composer.json" ]; then
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
fi
# Common checks
if [ ! -f "LICENSE" ]; then
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
fi
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
else
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
fi
# -- Joomla: manifest version drift --------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
fi
# -- Joomla: XML manifest existence --------
if [ -z "$MANIFEST" ]; then
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# -- Joomla: extension type check --------
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
else
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
# Always runs — every version change on main archives to version/XX.YY
- name: "Step 2: Version archive branch"
if: steps.check.outputs.already_released != 'true'
run: |
BRANCH="${{ steps.version.outputs.branch }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}"
PATCH="${{ steps.version.outputs.version }}"
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
# Check if branch exists
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
git push origin HEAD:"$BRANCH" --force
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
else
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
git push origin "$BRANCH" --force
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 3: Set platform version ----------------------------------------
- name: "Step 3: Set platform version"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
php /tmp/mokostandards-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
# -- STEP 4: Update version badges ----------------------------------------
- name: "Step 4: Update version badges"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
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
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
- name: "Step 5: Write updates.xml"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
REPO="${{ github.repository }}"
# -- Parse extension metadata from XML manifest ----------------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/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>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Templates/modules don't have <element> — derive from <name> (lowercased)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
fi
# Build client tag: plugins and frontend modules need <client>site</client>
CLIENT_TAG=""
if [ -n "$EXT_CLIENT" ]; then
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
CLIENT_TAG="<client>site</client>"
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="<folder>${EXT_FOLDER}</folder>"
fi
# Build targetplatform (fallback to Joomla 5 if not in manifest)
if [ -z "$TARGET_PLATFORM" ]; then
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
fi
# Build php_minimum tag
PHP_TAG=""
if [ -n "$PHP_MINIMUM" ]; then
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
fi
DOWNLOAD_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/tag/v${VERSION}"
# -- Build stable entry to temp file ─────────────────────────
{
printf '%s\n' ' <update>'
printf '%s\n' " <name>${EXT_NAME}</name>"
printf '%s\n' " <description>${EXT_NAME} update</description>"
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
printf '%s\n' " <type>${EXT_TYPE}</type>"
printf '%s\n' " <version>${VERSION}</version>"
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
printf '%s\n' ' <tags>'
printf '%s\n' ' <tag>stable</tag>'
printf '%s\n' ' </tags>'
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
printf '%s\n' ' <downloads>'
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
printf '%s\n' ' </downloads>'
printf '%s\n' " ${TARGET_PLATFORM}"
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
printf '%s\n' ' </update>'
} > /tmp/stable_entry.xml
# -- Write updates.xml preserving dev/rc entries ──────────────
# Extract existing entries for other stability levels
# Order reflects release workflow: development → alpha → beta → rc → stable
if [ -f "updates.xml" ]; then
printf 'import re, sys\n' > /tmp/extract.py
printf 'with open("updates.xml") as f: c = f.read()\n' >> /tmp/extract.py
printf 'tag = sys.argv[1]\n' >> /tmp/extract.py
printf 'm = re.search(r"( <update>.*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)", c, re.DOTALL)\n' >> /tmp/extract.py
printf 'if m: print(m.group(1))\n' >> /tmp/extract.py
fi
DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true)
ALPHA_ENTRY=$(python3 /tmp/extract.py alpha 2>/dev/null || true)
BETA_ENTRY=$(python3 /tmp/extract.py beta 2>/dev/null || true)
RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true)
{
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' '<updates>'
[ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
[ -n "$ALPHA_ENTRY" ] && echo "$ALPHA_ENTRY"
[ -n "$BETA_ENTRY" ] && echo "$BETA_ENTRY"
[ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
cat /tmp/stable_entry.xml
printf '%s\n' '</updates>'
} > updates.xml
echo "updates.xml: ${VERSION} (stable + rc/dev preserved)" >> $GITHUB_STEP_SUMMARY
# -- Commit all changes ---------------------------------------------------
- name: Commit release changes
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
if git diff --quiet && git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
VERSION="${{ steps.version.outputs.version }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add -A
git commit -m "chore(release): build ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
# -- STEP 6: Create tag ---------------------------------------------------
- name: "Step 6: Create git tag"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true' &&
steps.version.outputs.is_minor == 'true'
run: |
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
# Only create the major release tag if it doesn't exist yet
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
git tag "$RELEASE_TAG"
git push origin "$RELEASE_TAG"
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
fi
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 7: Create or update GitHub Release ------------------------------
- name: "Step 7: GitHub Release"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true'
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
MAJOR="${{ steps.version.outputs.major }}"
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
# Check if the major release already exists
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
if [ -z "$EXISTING" ]; then
# First release for this major
gh release create "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH"
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
else
# Append version notes to existing major release
CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true)
{
echo "$CURRENT_NOTES"
echo ""
echo "---"
echo "### ${VERSION}"
echo ""
cat /tmp/release_notes.md
} > /tmp/updated_notes.md
gh release edit "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/updated_notes.md
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
# Every patch builds an install-ready ZIP and uploads it to the minor release.
# Result: one Release per minor version with a ZIP for each patch.
- name: "Step 8: Build Joomla package and update checksum"
if: >-
steps.version.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}"
# All ZIPs upload to the major release tag (vXX)
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || {
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
exit 0
}
# Find extension element name from manifest
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
[ -z "$MANIFEST" ] && exit 0
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${EXT_ELEMENT}-${VERSION}.tar.gz"
# -- 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; }
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
# ZIP package
cd "$SOURCE_DIR"
zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
cd ..
# tar.gz package
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")
# -- Calculate SHA-256 for both ----------------------------------
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
# -- Upload both to release tag ----------------------------------
gh release upload "$RELEASE_TAG" "/tmp/${ZIP_NAME}" --clobber 2>/dev/null || true
gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
# -- Update updates.xml with both download formats ---------------
if [ -f "updates.xml" ]; then
ZIP_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
TAR_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
# Replace downloads block with both formats + SHA
sed -i "s|<downloads>.*</downloads>|<downloads>\n <downloadurl type=\"full\" format=\"zip\">${ZIP_URL}</downloadurl>\n <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n </downloads>|" updates.xml 2>/dev/null || true
if grep -q '<sha256>' updates.xml; then
sed -i "s|<sha256>.*</sha256>|<sha256>${SHA256_ZIP}</sha256>|" updates.xml
else
sed -i "s|</downloads>|</downloads>\n <sha256>${SHA256_ZIP}</sha256>|" updates.xml
fi
git add updates.xml
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true
git push || true
fi
echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [${PACKAGE_NAME}](https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.version.outputs.version }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi

144
.github/workflows/auto-update-sha.yml vendored Normal file
View File

@@ -0,0 +1,144 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoOnyx.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
# PATH: /.github/workflows/auto-update-sha.yml
# VERSION: 03.09.03
# BRIEF: Automatically update SHA-256 hash in updates.xml after release
# NOTE: Ensures updates.xml stays synchronized with release packages
name: Auto-Update SHA Hash
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to update SHA for (e.g., 03.08.03)'
required: true
type: string
permissions:
contents: write
jobs:
update-sha:
name: Update SHA-256 Hash in updates.xml
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Get release tag
id: tag
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${{ github.event.release.tag_name }}"
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Processing release: ${TAG}"
- name: Download release package
run: |
TAG="${{ steps.tag.outputs.tag }}"
PACKAGE_NAME="mokoonyx-src-${TAG}.zip"
DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/${PACKAGE_NAME}"
echo "Downloading: ${DOWNLOAD_URL}"
curl -L -o "${PACKAGE_NAME}" "${DOWNLOAD_URL}"
if [ ! -f "${PACKAGE_NAME}" ]; then
echo "Error: Failed to download package"
exit 1
fi
echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV
- name: Calculate SHA-256 hash
id: sha
run: |
SHA256_HASH=$(sha256sum "${PACKAGE_NAME}" | cut -d' ' -f1)
echo "sha256=${SHA256_HASH}" >> $GITHUB_OUTPUT
echo "SHA-256 Hash: ${SHA256_HASH}"
- name: Update updates.xml
run: |
TAG="${{ steps.tag.outputs.tag }}"
SHA256="${{ steps.sha.outputs.sha256 }}"
DATE=$(date +%Y-%m-%d)
# Update version
sed -i "s|<version>.*</version>|<version>${TAG}</version>|" updates.xml
# Update creation date
sed -i "s|<creationDate>.*</creationDate>|<creationDate>${DATE}</creationDate>|" updates.xml
# Update download URL
sed -i "s|<downloadurl type='full' format='zip'>.*</downloadurl>|<downloadurl type='full' format='zip'>https://github.com/${{ github.repository }}/releases/download/${TAG}/mokoonyx-src-${TAG}.zip</downloadurl>|" updates.xml
# Update or add SHA-256 hash
if grep -q "<sha256>" updates.xml; then
sed -i "s|<sha256>.*</sha256>|<sha256>sha256:${SHA256}</sha256>|" updates.xml
else
# Add SHA-256 after downloadurl
sed -i "/<\/downloadurl>/a\ <sha256>sha256:${SHA256}<\/sha256>" updates.xml
fi
echo "Updated updates.xml with:"
echo " Version: ${TAG}"
echo " Date: ${DATE}"
echo " SHA-256: ${SHA256}"
- name: Check for changes
id: changes
run: |
if git diff --quiet updates.xml; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No changes to updates.xml"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "Changes detected in updates.xml"
git diff updates.xml
fi
- name: Commit and push changes
if: steps.changes.outputs.has_changes == 'true'
run: |
TAG="${{ steps.tag.outputs.tag }}"
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 SHA-256 hash for release ${TAG} - SHA: ${{ steps.sha.outputs.sha256 }}"
git push origin main
echo "Successfully updated updates.xml with SHA-256 hash for release ${TAG}"
- name: Summary
if: steps.changes.outputs.has_changes == 'true'
run: |
echo "### SHA-256 Hash Updated Successfully" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Release: ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
echo "- SHA-256: \`${{ steps.sha.outputs.sha256 }}\`" >> $GITHUB_STEP_SUMMARY
echo "- File: updates.xml" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The Joomla update server will now provide the correct package hash." >> $GITHUB_STEP_SUMMARY
- name: Summary (no changes)
if: steps.changes.outputs.has_changes == 'false'
run: |
echo "### No Updates Needed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "updates.xml already contains the correct SHA-256 hash for release ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY

114
.github/workflows/branch-freeze.yml vendored Normal file
View File

@@ -0,0 +1,114 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/shared/branch-freeze.yml.template
# VERSION: 04.06.00
# BRIEF: Freeze or unfreeze any branch via ruleset — manual workflow_dispatch
name: Branch Freeze
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to freeze/unfreeze (e.g., version/04, dev/feature)'
required: true
type: string
action:
description: 'Action to perform'
required: true
type: choice
options:
- freeze
- unfreeze
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
manage-freeze:
name: "${{ inputs.action }} branch: ${{ inputs.branch }}"
runs-on: ubuntu-latest
steps:
- name: Check permissions
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}/permission" 2>/dev/null \
2>/dev/null | jq -r '.permission' || echo "read")
if [ "$PERMISSION" != "admin" ]; then
echo "Denied: only admins can freeze/unfreeze branches (${ACTOR} has ${PERMISSION})"
exit 1
fi
- name: "${{ inputs.action }} branch"
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
BRANCH="${{ inputs.branch }}"
ACTION="${{ inputs.action }}"
REPO="${{ github.repository }}"
RULESET_NAME="FROZEN: ${BRANCH}"
echo "## Branch Freeze" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$ACTION" = "freeze" ]; then
# Check if ruleset already exists
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \
| jq -r ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
if [ -n "$EXISTING" ]; then
echo "Branch \`${BRANCH}\` is already frozen (ruleset #${EXISTING})" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Create freeze ruleset — blocks all updates except admin bypass
printf '{"name":"%s","target":"branch","enforcement":"active",' "${RULESET_NAME}" > /tmp/ruleset.json
printf '"bypass_actors":[{"actor_id":5,"actor_type":"RepositoryRole","bypass_mode":"always"}],' >> /tmp/ruleset.json
printf '"conditions":{"ref_name":{"include":["refs/heads/%s"],"exclude":[]}},' "${BRANCH}" >> /tmp/ruleset.json
printf '"rules":[{"type":"update"},{"type":"deletion"},{"type":"non_fast_forward"}]}' >> /tmp/ruleset.json
RESULT=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null -X POST -d @/tmp/ruleset.json 2>&1 | jq -r '.id') || true
if echo "$RESULT" | grep -qE '^[0-9]+$'; then
echo "Frozen \`${BRANCH}\` — ruleset #${RESULT}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${BRANCH}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Ruleset | #${RESULT} |" >> $GITHUB_STEP_SUMMARY
echo "| Rules | No updates, no deletion, no force push |" >> $GITHUB_STEP_SUMMARY
echo "| Bypass | Repository admins only |" >> $GITHUB_STEP_SUMMARY
else
echo "Failed to freeze: ${RESULT}" >> $GITHUB_STEP_SUMMARY
exit 1
fi
elif [ "$ACTION" = "unfreeze" ]; then
# Find and delete the freeze ruleset
RULESET_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \
| jq -r ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
if [ -z "$RULESET_ID" ]; then
echo "Branch \`${BRANCH}\` is not frozen (no ruleset found)" >> $GITHUB_STEP_SUMMARY
exit 0
fi
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets/${RULESET_ID}" 2>/dev/null -X DELETE --silent 2>/dev/null
echo "Unfrozen \`${BRANCH}\` — ruleset #${RULESET_ID} deleted" >> $GITHUB_STEP_SUMMARY
fi
rm -f /tmp/ruleset.json

View File

@@ -0,0 +1,99 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/shared/changelog-validation.yml.template
# VERSION: 04.06.00
# BRIEF: Validates CHANGELOG.md format and version consistency
# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos.
name: Changelog Validation
on:
pull_request:
branches:
- main
- 'dev/**'
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
validate-changelog:
name: Validate CHANGELOG.md
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Check CHANGELOG.md exists
run: |
echo "### Changelog Validation" >> $GITHUB_STEP_SUMMARY
if [ ! -f "CHANGELOG.md" ]; then
echo "CHANGELOG.md not found in repository root." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "CHANGELOG.md exists." >> $GITHUB_STEP_SUMMARY
- name: Check VERSION header matches README.md
run: |
# Extract version from README.md FILE INFORMATION block
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
if [ -z "$README_VERSION" ]; then
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
exit 1
fi
# Check that CHANGELOG.md has a matching version header
CHANGELOG_VERSION=$(grep -oP '^\#\#\s*\[\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' CHANGELOG.md | head -1)
if [ -z "$CHANGELOG_VERSION" ]; then
echo "No version header found in CHANGELOG.md (expected \`## [XX.YY.ZZ] - YYYY-MM-DD\`)." >> $GITHUB_STEP_SUMMARY
exit 1
fi
if [ "$CHANGELOG_VERSION" != "$README_VERSION" ]; then
echo "CHANGELOG latest version \`${CHANGELOG_VERSION}\` does not match README VERSION \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "CHANGELOG version \`${CHANGELOG_VERSION}\` matches README VERSION." >> $GITHUB_STEP_SUMMARY
- name: Validate conventional changelog format
run: |
ERRORS=0
# Check that version entries follow ## [XX.YY.ZZ] - YYYY-MM-DD format
while IFS= read -r LINE; do
if ! echo "$LINE" | grep -qP '^\#\#\s*\[[0-9]{2}\.[0-9]{2}\.[0-9]{2}\]\s*-\s*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then
echo "Malformed version header: \`${LINE}\`" >> $GITHUB_STEP_SUMMARY
echo " Expected format: \`## [XX.YY.ZZ] - YYYY-MM-DD\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done < <(grep -P '^\#\#\s*\[' CHANGELOG.md)
ENTRY_COUNT=$(grep -cP '^\#\#\s*\[' CHANGELOG.md || echo "0")
if [ "$ENTRY_COUNT" -eq 0 ]; then
echo "No version entries found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found ${ENTRY_COUNT} version entr(ies) in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} format issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Changelog format validation passed.**" >> $GITHUB_STEP_SUMMARY
fi

376
.github/workflows/ci-joomla.yml vendored Normal file
View File

@@ -0,0 +1,376 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
# VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos.
name: Joomla Extension CI
on:
pull_request:
branches:
- main
- 'dev/**'
workflow_dispatch:
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint-and-validate:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Clone MokoStandards
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
git clone --depth 1 --branch version/04 --quiet \
"https://x-access-token:${GH_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoStandards.git" \
/tmp/mokostandards
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: PHP syntax check
run: |
ERRORS=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
FOUND=1
while IFS= read -r -d '' FILE; do
OUTPUT=$(php -l "$FILE" 2>&1)
if echo "$OUTPUT" | grep -q "Parse error"; then
echo "::error file=${FILE}::${OUTPUT}"
ERRORS=$((ERRORS + 1))
fi
done < <(find "$DIR" -name "*.php" -print0)
fi
done
echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
fi
- name: XML manifest validation
run: |
echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find the extension manifest (XML with <extension tag)
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No Joomla extension manifest found (XML file with \`<extension\` tag)." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Validate well-formed XML
php -r "
\$xml = @simplexml_load_file('$MANIFEST');
if (\$xml === false) {
echo 'INVALID';
exit(1);
}
echo 'VALID';
" > /tmp/xml_result 2>&1
XML_RESULT=$(cat /tmp/xml_result)
if [ "$XML_RESULT" != "VALID" ]; then
echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
fi
# Check required tags: name, version, author, namespace (Joomla 5+)
for TAG in name version author namespace; do
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
fi
done
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check language files referenced in manifest
run: |
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -n "$MANIFEST" ]; then
# Extract language file references from manifest
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
if [ -z "$LANG_FILES" ]; then
echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
else
while IFS= read -r LANG_FILE; do
LANG_FILE=$(echo "$LANG_FILE" | xargs)
if [ -z "$LANG_FILE" ]; then
continue
fi
# Check in common locations
FOUND=0
for BASE in "." "src" "htdocs"; do
if [ -f "${BASE}/${LANG_FILE}" ]; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 0 ]; then
echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
fi
done <<< "$LANG_FILES"
fi
else
echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check index.html files in directories
run: |
echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
MISSING=0
CHECKED=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
while IFS= read -r -d '' SUBDIR; do
CHECKED=$((CHECKED + 1))
if [ ! -f "${SUBDIR}/index.html" ]; then
echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
MISSING=$((MISSING + 1))
fi
done < <(find "$DIR" -type d -print0)
fi
done
if [ "${CHECKED}" -eq 0 ]; then
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
elif [ "${MISSING}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
fi
release-readiness:
name: Release Readiness Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.base_ref == 'main'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Validate release readiness
run: |
echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Extract version from README.md
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
if [ -z "$README_VERSION" ]; then
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Find the extension manifest
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Check <version> matches README VERSION
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
if [ -z "$MANIFEST_VERSION" ]; then
echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check extension type, element, client attributes
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ -z "$EXT_TYPE" ]; then
echo "Missing \`type\` attribute on \`<extension>\` tag." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
fi
# Element check (component/module/plugin name)
HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_ELEMENT" -eq 0 ]; then
echo "Missing \`<element>\` or \`<name>\` in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Client attribute for site/admin modules and plugins
if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
HAS_CLIENT=$(grep -cP '<extension[^>]*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_CLIENT" -eq 0 ]; then
echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
fi
fi
# Check updates.xml exists
if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
echo "Update XML present." >> $GITHUB_STEP_SUMMARY
else
echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Check CHANGELOG.md exists
if [ -f "CHANGELOG.md" ]; then
echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
else
echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ $ERRORS -gt 0 ]; then
echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
fi
test:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
needs: lint-and-validate
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP ${{ matrix.php }}
run: |
php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: Run tests
run: |
echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
else
echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
else
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
fi

108
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,108 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/generic/codeql-analysis.yml.template
# VERSION: 03.09.03
# BRIEF: CodeQL security scanning workflow (generic — all repo types)
# NOTE: Deployed to .github/workflows/codeql-analysis.yml in governed repos.
# CodeQL does not support PHP directly; JavaScript scans JSON/YAML/shell.
# For PHP-specific security scanning see standards-compliance.yml.
name: CodeQL Security Scanning
on:
push:
branches:
- main
- version/*
schedule:
# Weekly on Monday at 06:00 UTC
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
actions: read
contents: read
security-events: write
pull-requests: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 360
strategy:
fail-fast: false
matrix:
# CodeQL does not support PHP. Use 'javascript' to scan JSON, YAML,
# and shell scripts. Add 'actions' to scan GitHub Actions workflows.
language: ['javascript', 'actions']
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-extended,security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
upload: true
output: sarif-results
wait-for-processing: true
- name: Upload SARIF results
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.5.0
with:
name: codeql-results-${{ matrix.language }}
path: sarif-results
retention-days: 30
- name: Step summary
if: always()
run: |
echo "### 🔍 CodeQL — ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
URL="https://github.com/${{ github.repository }}/security/code-scanning"
echo "See the [Security tab]($URL) for findings." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Severity | SLA |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-----|" >> $GITHUB_STEP_SUMMARY
echo "| Critical | 7 days |" >> $GITHUB_STEP_SUMMARY
echo "| High | 14 days |" >> $GITHUB_STEP_SUMMARY
echo "| Medium | 30 days |" >> $GITHUB_STEP_SUMMARY
echo "| Low | 60 days / next release |" >> $GITHUB_STEP_SUMMARY
summary:
name: Security Scan Summary
runs-on: ubuntu-latest
needs: analyze
if: always()
steps:
- name: Summary
run: |
echo "### 🛡️ CodeQL Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
SECURITY_URL="https://github.com/${{ github.repository }}/security"
echo "" >> $GITHUB_STEP_SUMMARY
echo "📊 [View all security alerts]($SECURITY_URL)" >> $GITHUB_STEP_SUMMARY

128
.github/workflows/deploy-manual.yml vendored Normal file
View File

@@ -0,0 +1,128 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.06.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
# NOTE: Joomla repos use update.xml for distribution. This is for manual
# dev server testing only — triggered via workflow_dispatch.
name: Deploy to Dev (Manual)
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: release
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch version/04 --quiet \
"https://x-access-token:${GH_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoStandards.git" \
/tmp/mokostandards 2>/dev/null || true
if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then
cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured — cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
[ -n "$SUFFIX" ] && REMOTE="${REMOTE}/${SUFFIX#/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
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[@]}"
else
php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped — FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi

View File

@@ -0,0 +1,758 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Firewall
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template
# VERSION: 04.06.00
# BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server
# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules.
name: Enterprise Firewall Configuration
# This workflow provides firewall configuration guidance for enterprise-ready sites
# It generates firewall rules for allowing outbound access to trusted domains
# including license providers, documentation sources, package registries,
# and the SFTP deployment server (DEV_FTP_HOST / DEV_FTP_PORT).
#
# Runs automatically when:
# - Coding agent workflows are triggered (pull requests with copilot/ prefix)
# - Manual workflow dispatch for custom configurations
on:
workflow_dispatch:
inputs:
firewall_type:
description: 'Target firewall type'
required: true
type: choice
options:
- 'iptables'
- 'ufw'
- 'firewalld'
- 'aws-security-group'
- 'azure-nsg'
- 'gcp-firewall'
- 'cloudflare'
- 'all'
default: 'all'
output_format:
description: 'Output format'
required: true
type: choice
options:
- 'shell-script'
- 'json'
- 'yaml'
- 'markdown'
- 'all'
default: 'markdown'
# Auto-run when coding agent creates or updates PRs
pull_request:
branches:
- 'copilot/**'
- 'agent/**'
types: [opened, synchronize, reopened]
# Auto-run on push to coding agent branches
push:
branches:
- 'copilot/**'
- 'agent/**'
permissions:
contents: read
actions: read
jobs:
generate-firewall-rules:
name: Generate Firewall Rules
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Apply Firewall Rules to Runner (Auto-run only)
if: github.event_name != 'workflow_dispatch'
env:
DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }}
run: |
echo "🔥 Applying firewall rules for coding agent environment..."
echo ""
echo "This step ensures the GitHub Actions runner can access trusted domains"
echo "including license providers, package registries, and documentation sources."
echo ""
# Note: GitHub Actions runners are ephemeral and run in controlled environments
# This step documents what domains are being accessed during the workflow
# Actual firewall configuration is managed by GitHub
cat > /tmp/trusted-domains.txt << 'EOF'
# Trusted domains for coding agent environment
# License Providers
www.gnu.org
opensource.org
choosealicense.com
spdx.org
creativecommons.org
apache.org
fsf.org
# Documentation & Standards
semver.org
keepachangelog.com
conventionalcommits.org
# GitHub & Related
github.com
api.github.com
docs.github.com
raw.githubusercontent.com
ghcr.io
# Package Registries
npmjs.com
registry.npmjs.org
pypi.org
files.pythonhosted.org
packagist.org
repo.packagist.org
rubygems.org
# Platform-Specific
joomla.org
downloads.joomla.org
docs.joomla.org
php.net
getcomposer.org
dolibarr.org
wiki.dolibarr.org
docs.dolibarr.org
# Moko Consulting
mokoconsulting.tech
# SFTP Deployment Server (DEV_FTP_HOST)
${DEV_FTP_HOST:-<not configured>}
# Google Services
drive.google.com
docs.google.com
sheets.google.com
accounts.google.com
storage.googleapis.com
fonts.googleapis.com
fonts.gstatic.com
# GitHub Extended
upload.github.com
objects.githubusercontent.com
user-images.githubusercontent.com
codeload.github.com
pkg.github.com
# Developer Reference
developer.mozilla.org
stackoverflow.com
git-scm.com
# CDN & Infrastructure
cdn.jsdelivr.net
unpkg.com
cdnjs.cloudflare.com
img.shields.io
# Container Registries
hub.docker.com
registry-1.docker.io
# CI & Code Quality
codecov.io
sonarcloud.io
# Terraform & Infrastructure
registry.terraform.io
releases.hashicorp.com
checkpoint-api.hashicorp.com
EOF
echo "✓ Trusted domains documented for this runner"
echo "✓ GitHub Actions runners have network access to these domains"
echo ""
# Test connectivity to key domains
echo "Testing connectivity to key domains..."
for domain in "github.com" "www.gnu.org" "npmjs.com" "pypi.org"; do
if curl -s --max-time 3 -o /dev/null -w "%{http_code}" "https://$domain" | grep -q "200\|301\|302"; then
echo " ✓ $domain is accessible"
else
echo " ⚠️ $domain connectivity check failed (may be expected)"
fi
done
# Test SFTP server connectivity (TCP port check)
SFTP_HOST="${DEV_FTP_HOST:-}"
SFTP_PORT="${DEV_FTP_PORT:-22}"
if [ -n "$SFTP_HOST" ]; then
# Strip any embedded :port suffix
SFTP_HOST="${SFTP_HOST%%:*}"
echo ""
echo "Testing SFTP deployment server connectivity..."
if timeout 5 bash -c "echo >/dev/tcp/${SFTP_HOST}/${SFTP_PORT}" 2>/dev/null; then
echo " ✓ SFTP server ${SFTP_HOST}:${SFTP_PORT} is reachable"
else
echo " ⚠️ SFTP server ${SFTP_HOST}:${SFTP_PORT} is not reachable from runner (firewall rule needed)"
fi
else
echo ""
echo " DEV_FTP_HOST not configured — skipping SFTP connectivity check"
fi
- name: Generate Firewall Configuration
id: generate
env:
DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }}
run: |
cat > generate_firewall_config.py << 'PYTHON_EOF'
#!/usr/bin/env python3
"""
Enterprise Firewall Configuration Generator
Generates firewall rules for enterprise-ready deployments allowing
access to trusted domains including license providers, documentation
sources, package registries, and platform-specific sites.
"""
import json
import os
import yaml
import sys
from typing import List, Dict
# SFTP deployment server from org variables
_sftp_host_raw = os.environ.get("DEV_FTP_HOST", "").strip()
_sftp_port = os.environ.get("DEV_FTP_PORT", "").strip() or "22"
# Strip embedded :port suffix if present
_sftp_host = _sftp_host_raw.split(":")[0] if _sftp_host_raw else ""
if ":" in _sftp_host_raw and not _sftp_port:
_sftp_port = _sftp_host_raw.split(":")[1]
SFTP_HOST = _sftp_host
SFTP_PORT = int(_sftp_port) if _sftp_port.isdigit() else 22
# Trusted domains from .github/copilot.yml
TRUSTED_DOMAINS = {
"license_providers": [
"www.gnu.org",
"opensource.org",
"choosealicense.com",
"spdx.org",
"creativecommons.org",
"apache.org",
"fsf.org",
],
"documentation_standards": [
"semver.org",
"keepachangelog.com",
"conventionalcommits.org",
],
"github_related": [
"github.com",
"api.github.com",
"docs.github.com",
"raw.githubusercontent.com",
"ghcr.io",
],
"package_registries": [
"npmjs.com",
"registry.npmjs.org",
"pypi.org",
"files.pythonhosted.org",
"packagist.org",
"repo.packagist.org",
"rubygems.org",
],
"standards_organizations": [
"json-schema.org",
"w3.org",
"ietf.org",
],
"platform_specific": [
"joomla.org",
"downloads.joomla.org",
"docs.joomla.org",
"php.net",
"getcomposer.org",
"dolibarr.org",
"wiki.dolibarr.org",
"docs.dolibarr.org",
],
"moko_consulting": [
"mokoconsulting.tech",
],
"google_services": [
"drive.google.com",
"docs.google.com",
"sheets.google.com",
"accounts.google.com",
"storage.googleapis.com",
"fonts.googleapis.com",
"fonts.gstatic.com",
],
"github_extended": [
"upload.github.com",
"objects.githubusercontent.com",
"user-images.githubusercontent.com",
"codeload.github.com",
"pkg.github.com",
],
"developer_reference": [
"developer.mozilla.org",
"stackoverflow.com",
"git-scm.com",
],
"cdn_and_infrastructure": [
"cdn.jsdelivr.net",
"unpkg.com",
"cdnjs.cloudflare.com",
"img.shields.io",
],
"container_registries": [
"hub.docker.com",
"registry-1.docker.io",
],
"ci_code_quality": [
"codecov.io",
"sonarcloud.io",
],
"terraform_infrastructure": [
"registry.terraform.io",
"releases.hashicorp.com",
"checkpoint-api.hashicorp.com",
],
}
# Inject SFTP deployment server as a separate category (port 22, not 443)
if SFTP_HOST:
TRUSTED_DOMAINS["sftp_deployment_server"] = [SFTP_HOST]
print(f" SFTP deployment server: {SFTP_HOST}:{SFTP_PORT}")
def generate_sftp_iptables_rules(host: str, port: int) -> str:
"""Generate iptables rules specifically for SFTP egress"""
return (
f"# Allow SFTP to deployment server {host}:{port}\n"
f"iptables -A OUTPUT -p tcp -d $(dig +short {host} | head -1)"
f" --dport {port} -j ACCEPT # SFTP deploy\n"
)
def generate_sftp_ufw_rules(host: str, port: int) -> str:
"""Generate UFW rules for SFTP egress"""
return (
f"# Allow SFTP to deployment server\n"
f"ufw allow out to $(dig +short {host} | head -1)"
f" port {port} proto tcp comment 'SFTP deploy to {host}'\n"
)
def generate_sftp_firewalld_rules(host: str, port: int) -> str:
"""Generate firewalld rules for SFTP egress"""
return (
f"# Allow SFTP to deployment server\n"
f"firewall-cmd --permanent --add-rich-rule='"
f"rule family=ipv4 destination address=$(dig +short {host} | head -1)"
f" port port={port} protocol=tcp accept' # SFTP deploy\n"
)
def generate_iptables_rules(domains: List[str]) -> str:
"""Generate iptables firewall rules"""
rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - iptables", ""]
rules.append("# Allow outbound HTTPS to trusted domains")
rules.append("")
for domain in domains:
rules.append(f"# Allow {domain}")
rules.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {domain} | head -1) --dport 443 -j ACCEPT")
rules.append("")
rules.append("# Allow DNS lookups")
rules.append("iptables -A OUTPUT -p udp --dport 53 -j ACCEPT")
rules.append("iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT")
return "\n".join(rules)
def generate_ufw_rules(domains: List[str]) -> str:
"""Generate UFW firewall rules"""
rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - UFW", ""]
rules.append("# Allow outbound HTTPS to trusted domains")
rules.append("")
for domain in domains:
rules.append(f"# Allow {domain}")
rules.append(f"ufw allow out to $(dig +short {domain} | head -1) port 443 proto tcp comment 'Allow {domain}'")
rules.append("")
rules.append("# Allow DNS")
rules.append("ufw allow out 53/udp comment 'Allow DNS UDP'")
rules.append("ufw allow out 53/tcp comment 'Allow DNS TCP'")
return "\n".join(rules)
def generate_firewalld_rules(domains: List[str]) -> str:
"""Generate firewalld rules"""
rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - firewalld", ""]
rules.append("# Add trusted domains to firewall")
rules.append("")
for domain in domains:
rules.append(f"# Allow {domain}")
rules.append(f"firewall-cmd --permanent --add-rich-rule='rule family=ipv4 destination address=$(dig +short {domain} | head -1) port port=443 protocol=tcp accept'")
rules.append("")
rules.append("# Reload firewall")
rules.append("firewall-cmd --reload")
return "\n".join(rules)
def generate_aws_security_group(domains: List[str]) -> Dict:
"""Generate AWS Security Group rules (JSON format)"""
rules = {
"SecurityGroupRules": {
"Egress": []
}
}
for domain in domains:
rules["SecurityGroupRules"]["Egress"].append({
"Description": f"Allow HTTPS to {domain}",
"IpProtocol": "tcp",
"FromPort": 443,
"ToPort": 443,
"CidrIp": "0.0.0.0/0", # In practice, resolve to specific IPs
"Tags": [{
"Key": "Domain",
"Value": domain
}]
})
# Add DNS
rules["SecurityGroupRules"]["Egress"].append({
"Description": "Allow DNS",
"IpProtocol": "udp",
"FromPort": 53,
"ToPort": 53,
"CidrIp": "0.0.0.0/0"
})
return rules
def generate_markdown_documentation(domains_by_category: Dict[str, List[str]]) -> str:
"""Generate markdown documentation"""
md = ["# Enterprise Firewall Configuration Guide", ""]
md.append("## Overview")
md.append("")
md.append("This document provides firewall configuration guidance for enterprise-ready deployments.")
md.append("It lists trusted domains that should be whitelisted for outbound access to ensure")
md.append("proper functionality of license validation, package management, and documentation access.")
md.append("")
md.append("## Trusted Domains by Category")
md.append("")
all_domains = []
for category, domains in domains_by_category.items():
category_name = category.replace("_", " ").title()
md.append(f"### {category_name}")
md.append("")
md.append("| Domain | Purpose |")
md.append("|--------|---------|")
for domain in domains:
all_domains.append(domain)
purpose = get_domain_purpose(domain)
md.append(f"| `{domain}` | {purpose} |")
md.append("")
md.append("## Implementation Examples")
md.append("")
md.append("### iptables Example")
md.append("")
md.append("```bash")
md.append("# Allow HTTPS to trusted domain")
md.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {all_domains[0]}) --dport 443 -j ACCEPT")
md.append("```")
md.append("")
md.append("### UFW Example")
md.append("")
md.append("```bash")
md.append("# Allow HTTPS to trusted domain")
md.append(f"ufw allow out to {all_domains[0]} port 443 proto tcp")
md.append("```")
md.append("")
md.append("### AWS Security Group Example")
md.append("")
md.append("```json")
md.append("{")
md.append(' "IpPermissions": [{')
md.append(' "IpProtocol": "tcp",')
md.append(' "FromPort": 443,')
md.append(' "ToPort": 443,')
md.append(' "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS to trusted domains"}]')
md.append(" }]")
md.append("}")
md.append("```")
md.append("")
md.append("## Ports Required")
md.append("")
md.append("| Port | Protocol | Purpose |")
md.append("|------|----------|---------|")
md.append("| 443 | TCP | HTTPS (secure web access) |")
md.append("| 80 | TCP | HTTP (redirects to HTTPS) |")
md.append("| 53 | UDP/TCP | DNS resolution |")
md.append("")
md.append("## Security Considerations")
md.append("")
md.append("1. **DNS Resolution**: Ensure DNS queries are allowed (port 53 UDP/TCP)")
md.append("2. **Certificate Validation**: HTTPS requires ability to reach certificate authorities")
md.append("3. **Dynamic IPs**: Some domains use CDNs with dynamic IPs - consider using FQDNs in rules")
md.append("4. **Regular Updates**: Review and update whitelist as services change")
md.append("5. **Logging**: Enable logging for blocked connections to identify missing rules")
md.append("")
md.append("## Compliance Notes")
md.append("")
md.append("- All listed domains provide read-only access to public information")
md.append("- License providers enable GPL compliance verification")
md.append("- Package registries support dependency security scanning")
md.append("- No authentication credentials are transmitted to these domains")
md.append("")
return "\n".join(md)
def get_domain_purpose(domain: str) -> str:
"""Get human-readable purpose for a domain"""
purposes = {
"www.gnu.org": "GNU licenses and documentation",
"opensource.org": "Open Source Initiative resources",
"choosealicense.com": "GitHub license selection tool",
"spdx.org": "Software Package Data Exchange identifiers",
"creativecommons.org": "Creative Commons licenses",
"apache.org": "Apache Software Foundation licenses",
"fsf.org": "Free Software Foundation resources",
"semver.org": "Semantic versioning specification",
"keepachangelog.com": "Changelog format standards",
"conventionalcommits.org": "Commit message conventions",
"github.com": "GitHub platform access",
"api.github.com": "GitHub API access",
"docs.github.com": "GitHub documentation",
"raw.githubusercontent.com": "GitHub raw content access",
"npmjs.com": "npm package registry",
"pypi.org": "Python Package Index",
"packagist.org": "PHP Composer package registry",
"rubygems.org": "Ruby gems registry",
"joomla.org": "Joomla CMS platform",
"php.net": "PHP documentation and downloads",
"dolibarr.org": "Dolibarr ERP/CRM platform",
}
return purposes.get(domain, "Trusted resource")
def main():
# Use inputs if provided (manual dispatch), otherwise use defaults (auto-run)
firewall_type = "${{ github.event.inputs.firewall_type }}" or "all"
output_format = "${{ github.event.inputs.output_format }}" or "markdown"
print(f"Running in {'manual' if '${{ github.event.inputs.firewall_type }}' else 'automatic'} mode")
print(f"Firewall type: {firewall_type}")
print(f"Output format: {output_format}")
print("")
# Collect all domains
all_domains = []
for domains in TRUSTED_DOMAINS.values():
all_domains.extend(domains)
# Remove duplicates and sort
all_domains = sorted(set(all_domains))
print(f"Generating firewall rules for {len(all_domains)} trusted domains...")
print("")
# Exclude SFTP server from HTTPS rule generation (different port)
https_domains = [d for d in all_domains if d != SFTP_HOST]
# Generate based on firewall type
if firewall_type in ["iptables", "all"]:
rules = generate_iptables_rules(https_domains)
if SFTP_HOST:
rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n"
rules += generate_sftp_iptables_rules(SFTP_HOST, SFTP_PORT)
with open("firewall-rules-iptables.sh", "w") as f:
f.write(rules)
print("✓ Generated iptables rules: firewall-rules-iptables.sh")
if firewall_type in ["ufw", "all"]:
rules = generate_ufw_rules(https_domains)
if SFTP_HOST:
rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n"
rules += generate_sftp_ufw_rules(SFTP_HOST, SFTP_PORT)
with open("firewall-rules-ufw.sh", "w") as f:
f.write(rules)
print("✓ Generated UFW rules: firewall-rules-ufw.sh")
if firewall_type in ["firewalld", "all"]:
rules = generate_firewalld_rules(https_domains)
if SFTP_HOST:
rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n"
rules += generate_sftp_firewalld_rules(SFTP_HOST, SFTP_PORT)
with open("firewall-rules-firewalld.sh", "w") as f:
f.write(rules)
print("✓ Generated firewalld rules: firewall-rules-firewalld.sh")
if firewall_type in ["aws-security-group", "all"]:
rules = generate_aws_security_group(all_domains)
with open("firewall-rules-aws-sg.json", "w") as f:
json.dump(rules, f, indent=2)
print("✓ Generated AWS Security Group rules: firewall-rules-aws-sg.json")
if output_format in ["yaml", "all"]:
with open("trusted-domains.yml", "w") as f:
yaml.dump(TRUSTED_DOMAINS, f, default_flow_style=False)
print("✓ Generated YAML domain list: trusted-domains.yml")
if output_format in ["json", "all"]:
with open("trusted-domains.json", "w") as f:
json.dump(TRUSTED_DOMAINS, f, indent=2)
print("✓ Generated JSON domain list: trusted-domains.json")
if output_format in ["markdown", "all"]:
md = generate_markdown_documentation(TRUSTED_DOMAINS)
with open("FIREWALL_CONFIGURATION.md", "w") as f:
f.write(md)
print("✓ Generated documentation: FIREWALL_CONFIGURATION.md")
print("")
print("Domain Categories:")
for category, domains in TRUSTED_DOMAINS.items():
print(f" - {category}: {len(domains)} domains")
print("")
print("Total unique domains: ", len(all_domains))
if __name__ == "__main__":
main()
PYTHON_EOF
chmod +x generate_firewall_config.py
pip install PyYAML
python3 generate_firewall_config.py
- name: Upload Firewall Configuration Artifacts
uses: actions/upload-artifact@v6
with:
name: firewall-configurations
path: |
firewall-rules-*.sh
firewall-rules-*.json
trusted-domains.*
FIREWALL_CONFIGURATION.md
retention-days: 90
- name: Display Summary
run: |
echo "## Firewall Configuration" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "**Mode**: Manual Execution" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Firewall rules have been generated for enterprise-ready deployments." >> $GITHUB_STEP_SUMMARY
else
echo "**Mode**: Automatic Execution (Coding Agent Active)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "This workflow ran automatically because a coding agent (GitHub Copilot) is active." >> $GITHUB_STEP_SUMMARY
echo "Firewall configuration has been validated for the coding agent environment." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Files Generated" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if ls firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null; then
ls -lh firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null | awk '{print "- " $9 " (" $5 ")"}' >> $GITHUB_STEP_SUMMARY
else
echo "- Documentation generated" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "### Download Artifacts" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Download the generated firewall configurations from the workflow artifacts." >> $GITHUB_STEP_SUMMARY
else
echo "### Trusted Domains Active" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The coding agent has access to:" >> $GITHUB_STEP_SUMMARY
echo "- License providers (GPL, OSI, SPDX, Apache, etc.)" >> $GITHUB_STEP_SUMMARY
echo "- Package registries (npm, PyPI, Packagist, RubyGems)" >> $GITHUB_STEP_SUMMARY
echo "- Documentation sources (GitHub, Joomla, Dolibarr, PHP)" >> $GITHUB_STEP_SUMMARY
echo "- Standards organizations (W3C, IETF, JSON Schema)" >> $GITHUB_STEP_SUMMARY
fi
# Usage Instructions:
#
# This workflow runs in two modes:
#
# 1. AUTOMATIC MODE (Coding Agent):
# - Triggers when coding agent branches (copilot/**, agent/**) are pushed or PR'd
# - Validates firewall configuration for the coding agent environment
# - Documents accessible domains for compliance
# - Ensures license sources and package registries are available
#
# 2. MANUAL MODE (Enterprise Configuration):
# - Manually trigger from the Actions tab
# - Select desired firewall type and output format
# - Download generated artifacts
# - Apply firewall rules to your enterprise environment
#
# Configuration:
# - Trusted domains are sourced from .github/copilot.yml
# - Modify copilot.yml to add/remove trusted domains
# - Changes automatically propagate to firewall rules
#
# Important Notes:
# - Review generated rules before applying to production
# - Some domains may use CDNs with dynamic IPs
# - Consider using FQDN-based rules where supported
# - Test thoroughly in staging environment first
# - Monitor logs for blocked connections
# - Update rules as domains/services change

446
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,446 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoOnyx.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
# PATH: /.github/workflows/release.yml
# VERSION: 03.09.16
# BRIEF: Joomla release — build ZIP, publish to Gitea, mirror to GitHub
name: Create Release
on:
push:
tags:
- '[0-9][0-9].[0-9][0-9].[0-9][0-9]'
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., 03.09.16)'
required: true
type: string
prerelease:
description: 'Mark as pre-release'
required: false
type: boolean
default: false
stability:
description: 'Stability tag (development, alpha, beta, rc, stable)'
required: false
type: string
default: 'development'
permissions:
contents: write
env:
GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting
GITEA_REPO: MokoOnyx
EXT_ELEMENT: mokoonyx
jobs:
build:
name: Build Release Package
runs-on: release
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PHP
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
php -v
composer --version
- name: Get version and stability
id: meta
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
STABILITY="${{ inputs.stability }}"
PRERELEASE="${{ inputs.prerelease }}"
else
VERSION=${GITHUB_REF#refs/tags/}
STABILITY="stable"
PRERELEASE="false"
fi
# Derive suffix and tag from stability
case "$STABILITY" in
development) SUFFIX="-dev"; TAG_NAME="development" ;;
alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;;
beta) SUFFIX="-beta"; TAG_NAME="beta" ;;
rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;;
stable) SUFFIX=""; TAG_NAME="v${VERSION%%.*}" ;;
*) SUFFIX="-dev"; TAG_NAME="development" ;;
esac
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "Building: ${ZIP_NAME} (${STABILITY})"
- name: Install dependencies
env:
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-dev --optimize-autoloader --no-interaction
fi
- name: Create package
run: |
mkdir -p build/package
rsync -av \
--exclude='sftp-config*' \
--exclude='.ftpignore' \
--exclude='*.ppk' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='.env*' \
--exclude='*.local' \
src/ build/package/
- name: Build ZIP
id: zip
run: |
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
cd build/package
zip -r "../${ZIP_NAME}" .
cd ..
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
SIZE=$(stat -c%s "${ZIP_NAME}")
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
echo "size=${SIZE}" >> "$GITHUB_OUTPUT"
echo "SHA-256: ${SHA256}"
echo "Size: ${SIZE} bytes"
# ── Gitea Release (PRIMARY) ──────────────────────────────────────
- name: "Gitea: Delete existing release"
run: |
TAG="${{ steps.meta.outputs.tag_name }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Find and delete existing release by tag (may not exist — ignore 404)
RELEASE_ID=$(curl -s -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty')
if [ -n "$RELEASE_ID" ]; then
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/releases/${RELEASE_ID}" || true
echo "Deleted existing release id=${RELEASE_ID}"
fi
# Delete existing tag
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" 2>/dev/null || true
- name: "Gitea: Create release"
id: gitea_release
run: |
TAG="${{ steps.meta.outputs.tag_name }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
PRERELEASE="${{ steps.meta.outputs.prerelease }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Build release body
BODY="## ${EXT_ELEMENT} ${VERSION} (${STABILITY})
### SHA-256
\`${SHA256}\`"
# Extract changelog if available
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md)
if [ -n "$NOTES" ]; then
BODY="## ${EXT_ELEMENT} ${VERSION} (${STABILITY})
${NOTES}
### SHA-256
\`${SHA256}\`"
fi
fi
IS_PRE="true"
if [ "$STABILITY" = "stable" ]; then
IS_PRE="false"
fi
RESULT=$(curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "${{ github.ref_name }}" \
--arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^}" \
--arg body "$BODY" \
--argjson pre "$IS_PRE" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}'
)")
RELEASE_ID=$(echo "$RESULT" | jq -r '.id')
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
echo "Gitea release created: id=${RELEASE_ID}, tag=${TAG}"
- name: "Gitea: Upload ZIP"
run: |
RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
curl -sf -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}"
echo "Uploaded ${ZIP_NAME} to Gitea release ${RELEASE_ID}"
# ── GitHub Mirror (BACKUP) ───────────────────────────────────────
- name: "GitHub: Mirror release (stable/rc only)"
if: ${{ steps.meta.outputs.stability == 'stable' || steps.meta.outputs.stability == 'rc' }}
continue-on-error: true
run: |
TAG="${{ steps.meta.outputs.tag_name }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TOKEN="${{ secrets.GH_TOKEN }}"
GH_REPO="mokoconsulting-tech/${GITEA_REPO}"
GH_API="https://api.github.com/repos/${GH_REPO}"
IS_PRE="true"
[ "$STABILITY" = "stable" ] && IS_PRE="false"
# Delete existing release by tag
EXISTING=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${GH_API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty')
if [ -n "$EXISTING" ]; then
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${GH_API}/releases/${EXISTING}" || true
fi
# Delete tag
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${GH_API}/git/refs/tags/${TAG}" 2>/dev/null || true
# Create release
RELEASE_ID=$(curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${GH_API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "${{ github.sha }}" \
--arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \
--arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \
--argjson pre "$IS_PRE" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre, draft: false}'
)" | jq -r '.id')
# Upload ZIP
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ]; then
curl -sf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"https://uploads.github.com/repos/${GH_REPO}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@build/${ZIP_NAME}"
echo "GitHub mirror: uploaded ${ZIP_NAME}"
fi
# ── Update updates.xml ──────────────────────────────────────────
- name: "Update updates.xml for this channel"
run: |
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.meta.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag_name }}"
DATE=$(date +%Y-%m-%d)
if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then
echo "No updates.xml or no SHA — 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"]
# Map stability to the <tag> value in updates.xml
tag_map = {
"development": "development",
"alpha": "alpha",
"beta": "beta",
"rc": "rc",
"stable": "stable",
}
xml_tag = tag_map.get(stability, "development")
with open("updates.xml", "r") as f:
content = f.read()
# Build regex to find the specific <update> block for this stability tag
# Use negative lookahead to avoid matching across multiple <update> blocks
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
match = re.search(block_pattern, content, re.DOTALL)
if not match:
print(f"No <update> block found for <tag>{xml_tag}</tag>")
exit(0)
block = match.group(1)
original_block = block
# Update version
block = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
# Update creation date
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
# Update SHA-256
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", block)
# Update Gitea download URL
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
block = re.sub(
r"(<downloadurl[^>]*>)https://git\.mokoconsulting\.tech/[^<]*(</downloadurl>)",
rf"\g<1>{gitea_url}\g<2>",
block
)
# Update GitHub download URL only for RC and stable (others are Gitea-only)
if stability in ("rc", "stable"):
gh_url = f"https://github.com/mokoconsulting-tech/{gitea_repo}/releases/download/{tag}/{zip_name}"
block = re.sub(
r"(<downloadurl[^>]*>)https://github\.com/[^<]*(</downloadurl>)",
rf"\g<1>{gh_url}\g<2>",
block
)
else:
# Remove any GitHub download URL for dev/alpha/beta
block = re.sub(
r"\n\s*<downloadurl[^>]*>https://github\.com/[^<]*</downloadurl>",
"",
block
)
content = content.replace(original_block, block)
with open("updates.xml", "w") as f:
f.write(content)
print(f"Updated {xml_tag} channel: version={version}, sha={sha256[:16]}..., date={date}")
PYEOF
- name: "Commit updates.xml to current branch and main"
run: |
if git diff --quiet updates.xml 2>/dev/null; then
echo "No changes to updates.xml"
exit 0
fi
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.meta.outputs.version }}"
CURRENT_BRANCH="${{ github.ref_name }}"
TOKEN="${{ secrets.GA_TOKEN }}"
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} SHA-256 for ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
# Set push URL with GA_TOKEN for authenticated pushes (branch protection requires jmiller)
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
# Push to current branch
git push || true
# Also update updates.xml on main via Gitea API (git push blocked by branch protection)
if [ "$CURRENT_BRANCH" != "main" ]; then
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# Get current file SHA on main (required for update)
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
if [ -n "$FILE_SHA" ]; then
# Base64-encode the updates.xml content from working tree (has updated SHA)
CONTENT=$(base64 -w0 updates.xml)
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/contents/updates.xml" \
-d "$(jq -n \
--arg content "$CONTENT" \
--arg sha "$FILE_SHA" \
--arg msg "chore: update ${STABILITY} channel to ${VERSION} on main [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
echo "updates.xml synced to main via API (HTTP ${HTTP_CODE})"
else
echo "WARNING: failed to sync updates.xml to main (HTTP ${HTTP_CODE})"
echo "$RESPONSE" | head -5
fi
else
echo "WARNING: could not get file SHA for updates.xml on main"
fi
fi
- name: Summary
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TAG="${{ steps.meta.outputs.tag_name }}"
echo "### Release Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY

787
.github/workflows/repo_health.yml vendored Normal file
View File

@@ -0,0 +1,787 @@
# ============================================================================
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.github/workflows/repo_health.yml
# VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# NOTE: Field is user-managed.
# ============================================================================
name: Repo Health
concurrency:
group: repo-health-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
on:
workflow_dispatch:
inputs:
profile:
description: 'Validation profile: all, release, scripts, or repo'
required: true
default: all
type: choice
options:
- all
- release
- scripts
- repo
pull_request:
push:
permissions:
contents: read
env:
# Release policy - Repository Variables Only
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy
# Note: directories listed without a trailing slash.
SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy
# Files are listed as-is; directories must end with a trailing slash.
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md
# Extended checks toggles
EXTENDED_CHECKS: "true"
# File / directory variables (moved to top-level env)
DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts
WORKFLOWS_DIR: .github/workflows
SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
access_check:
name: Access control
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
allowed: ${{ steps.perm.outputs.allowed }}
permission: ${{ steps.perm.outputs.permission }}
steps:
- name: Check actor permission (admin only)
id: perm
run: |
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
TOKEN="${{ secrets.GA_TOKEN }}"
GITEA_API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1"
PERMISSION="unknown"
ALLOWED="false"
METHOD=""
# Hardcoded authorized users
if [ "$ACTOR" = "jmiller" ] || [ "$ACTOR" = "gitea-actions[bot]" ]; then
PERMISSION="admin"
ALLOWED="true"
METHOD="hardcoded allowlist"
else
# Check via Gitea API
RESULT=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${GITEA_API}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
PERMISSION=$(echo "$RESULT" | jq -r '.permission // "unknown"')
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "owner" ] || [ "$PERMISSION" = "maintain" ]; then
ALLOWED="true"
fi
METHOD="Gitea collaborator API"
fi
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
{
echo "## 🔐 Access Authorization"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| **Actor** | \`${ACTOR}\` |"
echo "| **Repository** | \`${REPO}\` |"
echo "| **Permission** | \`${PERMISSION}\` |"
echo "| **Method** | ${METHOD} |"
echo "| **Authorized** | ${ALLOWED} |"
echo "| **Trigger** | \`${{ github.event_name }}\` |"
echo "| **Branch** | \`${GITHUB_REF#refs/heads/}\` |"
echo ""
if [ "$ALLOWED" = "true" ]; then
echo "✅ ${ACTOR} authorized (${METHOD})"
else
echo "❌ ${ACTOR} is NOT authorized. Requires admin or maintain role."
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Deny execution when not permitted
if: ${{ steps.perm.outputs.allowed != 'true' }}
run: |
set -euo pipefail
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
exit 1
release_config:
name: Release configuration
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Guardrails release vars
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes release validation'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
missing=()
missing_optional=()
for k in "${required[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing+=("${k}")
done
for k in "${optional[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing_optional+=("${k}")
done
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Variable | Status |'
printf '%s\n' '|---|---|'
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repository variables'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#missing[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repository variables'
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
{
printf '%s\n' '### Repository variables validation result'
printf '%s\n' 'Status: OK'
printf '%s\n' 'All required repository variables present.'
printf '%s\n' ''
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
scripts_governance:
name: Scripts governance
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Scripts folder checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes scripts governance'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
if [ ! -d "${SCRIPT_DIR}" ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' 'Status: OK (advisory)'
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=()
unapproved_dirs=()
for d in "${required_dirs[@]}"; do
req="${d%/}"
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
done
while IFS= read -r d; do
allowed=false
for a in "${allowed_dirs[@]}"; do
a_norm="${a%/}"
[ "${d%/}" = "${a_norm}" ] && allowed=true
done
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Area | Status | Notes |'
printf '%s\n' '|---|---|---|'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
else
printf '%s\n' '| Required directories | OK | All required subfolders present |'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
else
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
fi
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
printf '\n'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Missing required script directories:'
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Missing required script directories: none.'
printf '\n'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Unapproved script directories detected:'
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Unapproved script directories detected: none.'
printf '\n'
fi
printf '%s\n' 'Scripts governance completed in advisory mode.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
repo_health:
name: Repository health
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Repository health checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes repository health'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
# Source directory: src/ or htdocs/ (either is valid)
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
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%/}"
[ ! -d "${d}" ] && missing_required+=("${item}")
else
[ ! -f "${item}" ] && missing_required+=("${item}")
fi
done
# Optional entries: handle files and directories (trailing slash indicates dir)
for f in "${optional_files[@]}"; do
if printf '%s' "${f}" | grep -q '/$'; then
d="${f%/}"
[ ! -d "${d}" ] && missing_optional+=("${f}")
else
[ ! -f "${f}" ] && missing_optional+=("${f}")
fi
done
for d in "${disallowed_dirs[@]}"; do
d_norm="${d%/}"
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
done
for f in "${disallowed_files[@]}"; do
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
done
git fetch origin --prune
dev_paths=()
dev_branches=()
# Look for remote branches matching origin/dev*.
# A plain origin/dev is considered invalid; we require dev/<something> branches.
while IFS= read -r b; do
name="${b#origin/}"
if [ "${name}" = 'dev' ]; then
dev_branches+=("${name}")
else
dev_paths+=("${name}")
fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
# If there are no dev/* branches, fail the guardrail.
if [ "${#dev_paths[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
fi
# If a plain dev branch exists (origin/dev), flag it as invalid.
if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)")
fi
content_warnings=()
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
fi
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
fi
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
content_warnings+=("LICENSE does not look like a GPL text")
fi
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
content_warnings+=("README.md missing expected brand keyword")
fi
export PROFILE_RAW="${profile}"
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
report_json="$(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
)"
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Metric | Value |'
printf '%s\n' '|---|---|'
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
printf '\n'
printf '%s\n' '### Guardrails report (JSON)'
printf '%s\n' '```json'
printf '%s\n' "${report_json}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_required[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repo artifacts'
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repo artifacts'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#content_warnings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Repo content warnings'
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
# ── Joomla-specific checks ───────────────────────────────────────
joomla_findings=()
# XML manifest: find any XML file containing <extension
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
if [ -z "${MANIFEST}" ]; then
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
else
# Check <version> tag exists
if ! grep -qP '<version>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <version> tag missing")
fi
# Check extension type attribute
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
joomla_findings+=("XML manifest: type attribute missing or invalid")
fi
# Check <name> tag
if ! grep -qP '<name>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <name> tag missing")
fi
# Check <author> tag
if ! grep -qP '<author>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <author> tag missing")
fi
# Check <namespace> for Joomla 5+
if ! grep -qP '<namespace' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
fi
fi
# Language files: check for at least one .ini file
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
if [ "${INI_COUNT}" -eq 0 ]; then
joomla_findings+=("No .ini language files found")
fi
# updates.xml must exist in root (Joomla update server)
if [ ! -f 'updates.xml' ]; then
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi
# index.html files for directory listing protection
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 [ "${#joomla_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' '| Check | Status |'
printf '%s\n' '|---|---|'
for f in "${joomla_findings[@]}"; do
printf '%s\n' "| ${f} | Warning |"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
else
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' 'All Joomla-specific checks passed.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
extended_enabled="${EXTENDED_CHECKS:-true}"
extended_findings=()
if [ "${extended_enabled}" = 'true' ]; then
# CODEOWNERS presence
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
:
else
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
fi
# Workflow pinning advisory: flag uses @main/@master
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
if [ -n "${bad_refs}" ]; then
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
{
printf '%s\n' '### Workflow pinning advisory'
printf '%s\n' 'Found uses: entries pinned to main/master:'
printf '%s\n' '```'
printf '%s\n' "${bad_refs}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
# Docs index link integrity (docs/docs-index.md)
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
)"
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}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
# ShellCheck advisory
if [ -d "${SCRIPT_DIR}" ]; then
if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y shellcheck >/dev/null
fi
sc_out=''
while IFS= read -r shf; do
[ -z "${shf}" ] && continue
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
if [ -n "${out_one}" ]; then
sc_out="${sc_out}${out_one}\n"
fi
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
if [ -n "${sc_out}" ]; then
extended_findings+=("ShellCheck warnings detected (advisory)")
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
{
printf '%s\n' '### ShellCheck (advisory)'
printf '%s\n' '```'
printf '%s\n' "${sc_head}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
# SPDX header advisory for common source types
spdx_missing=()
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
spdx_args=()
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
while IFS= read -r f; do
[ -z "${f}" ] && continue
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
spdx_missing+=("${f}")
fi
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
if [ "${#spdx_missing[@]}" -gt 0 ]; then
extended_findings+=("SPDX header missing in some tracked files (advisory)")
{
printf '%s\n' '### SPDX header advisory'
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
# Git hygiene advisory: branches older than 180 days (remote)
stale_cutoff_days=180
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 [...]
if [ -n "${stale_branches}" ]; then
extended_findings+=("Stale remote branches detected (advisory)")
{
printf '%s\n' '### Git hygiene advisory'
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
{
printf '%s\n' '### Guardrails coverage matrix'
printf '%s\n' '| Domain | Status | Notes |'
printf '%s\n' '|---|---|---|'
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
printf '%s\n' '| Release variables | OK | Repository variables validation |'
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
if [ "${extended_enabled}" = 'true' ]; then
if [ "${#extended_findings[@]}" -gt 0 ]; then
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
else
printf '%s\n' '| Extended checks | OK | No findings |'
fi
else
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
fi
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Extended findings (advisory)'
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"

525
.github/workflows/repository-cleanup.yml vendored Normal file
View File

@@ -0,0 +1,525 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/shared/repository-cleanup.yml.template
# VERSION: 04.06.00
# BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes
# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos.
# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch.
name: Repository Cleanup
on:
schedule:
- cron: '0 6 1,15 * *'
workflow_dispatch:
inputs:
reset_labels:
description: 'Delete ALL existing labels and recreate the standard set'
type: boolean
default: false
clean_branches:
description: 'Delete old chore/sync-mokostandards-* branches'
type: boolean
default: true
clean_workflows:
description: 'Delete orphaned workflow runs (cancelled, stale)'
type: boolean
default: true
clean_logs:
description: 'Delete workflow run logs older than 30 days'
type: boolean
default: true
fix_templates:
description: 'Strip copyright comment blocks from issue templates'
type: boolean
default: true
rebuild_indexes:
description: 'Rebuild docs/ index files'
type: boolean
default: true
delete_closed_issues:
description: 'Delete issues that have been closed for more than 30 days'
type: boolean
default: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
issues: write
actions: write
jobs:
cleanup:
name: Repository Maintenance
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN || github.token }}
fetch-depth: 0
- name: Check actor permission
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
ACTOR="${{ github.actor }}"
# Schedule triggers use gitea-actions[bot]
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "✅ Scheduled run — authorized"
exit 0
fi
AUTHORIZED_USERS="jmiller gitea-actions[bot]"
for user in $AUTHORIZED_USERS; do
if [ "$ACTOR" = "$user" ]; then
echo "✅ ${ACTOR} authorized"
exit 0
fi
done
PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" \
2>/dev/null | jq -r '.permission')
case "$PERMISSION" in
admin|maintain) echo "✅ ${ACTOR} has ${PERMISSION}" ;;
*) echo "❌ Admin or maintain required"; exit 1 ;;
esac
# ── Determine which tasks to run ─────────────────────────────────────
# On schedule: run all tasks with safe defaults (labels NOT reset)
# On dispatch: use input toggles
- name: Set task flags
id: tasks
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "reset_labels=false" >> $GITHUB_OUTPUT
echo "clean_branches=true" >> $GITHUB_OUTPUT
echo "clean_workflows=true" >> $GITHUB_OUTPUT
echo "clean_logs=true" >> $GITHUB_OUTPUT
echo "fix_templates=true" >> $GITHUB_OUTPUT
echo "rebuild_indexes=true" >> $GITHUB_OUTPUT
echo "delete_closed_issues=false" >> $GITHUB_OUTPUT
else
echo "reset_labels=${{ inputs.reset_labels }}" >> $GITHUB_OUTPUT
echo "clean_branches=${{ inputs.clean_branches }}" >> $GITHUB_OUTPUT
echo "clean_workflows=${{ inputs.clean_workflows }}" >> $GITHUB_OUTPUT
echo "clean_logs=${{ inputs.clean_logs }}" >> $GITHUB_OUTPUT
echo "fix_templates=${{ inputs.fix_templates }}" >> $GITHUB_OUTPUT
echo "rebuild_indexes=${{ inputs.rebuild_indexes }}" >> $GITHUB_OUTPUT
echo "delete_closed_issues=${{ inputs.delete_closed_issues }}" >> $GITHUB_OUTPUT
fi
# ── DELETE RETIRED WORKFLOWS (always runs) ────────────────────────────
- name: Delete retired workflow files
run: |
echo "## 🗑️ Retired Workflow Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
RETIRED=(
".github/workflows/build.yml"
".github/workflows/code-quality.yml"
".github/workflows/release-cycle.yml"
".github/workflows/release-pipeline.yml"
".github/workflows/branch-cleanup.yml"
".github/workflows/auto-update-changelog.yml"
".github/workflows/enterprise-issue-manager.yml"
".github/workflows/flush-actions-cache.yml"
".github/workflows/mokostandards-script-runner.yml"
".github/workflows/unified-ci.yml"
".github/workflows/unified-platform-testing.yml"
".github/workflows/reusable-build.yml"
".github/workflows/reusable-ci-validation.yml"
".github/workflows/reusable-deploy.yml"
".github/workflows/reusable-php-quality.yml"
".github/workflows/reusable-platform-testing.yml"
".github/workflows/reusable-project-detector.yml"
".github/workflows/reusable-release.yml"
".github/workflows/reusable-script-executor.yml"
".github/workflows/rebuild-docs-indexes.yml"
".github/workflows/setup-project-v2.yml"
".github/workflows/sync-docs-to-project.yml"
".github/workflows/release.yml"
".github/workflows/sync-changelogs.yml"
".github/workflows/version_branch.yml"
"update.json"
".github/workflows/auto-version-branch.yml"
".github/workflows/publish-to-mokodolibarr.yml"
".github/workflows/ci.yml"
".github/workflows/deploy-rs.yml"
"sftp-config.json"
"sftp-config.json.template"
"scripts/sftp-config"
)
DELETED=0
for wf in "${RETIRED[@]}"; do
if [ -f "$wf" ]; then
git rm "$wf" 2>/dev/null || rm -f "$wf"
echo " Deleted: \`$(basename $wf)\`" >> $GITHUB_STEP_SUMMARY
DELETED=$((DELETED+1))
fi
done
if [ "$DELETED" -gt 0 ]; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add -A
git commit -m "chore: delete ${DELETED} retired workflow file(s) [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
echo "✅ ${DELETED} retired workflow(s) deleted" >> $GITHUB_STEP_SUMMARY
else
echo "✅ No retired workflows found" >> $GITHUB_STEP_SUMMARY
fi
# ── LABEL RESET ──────────────────────────────────────────────────────
- name: Reset labels to standard set
if: steps.tasks.outputs.reset_labels == 'true'
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
echo "## 🏷️ Label Reset" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/labels?per_page=100" 2>/dev/null | jq -r '.[].name' | while read -r label; do
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$label', safe=''))")
gh api -X DELETE "repos/${REPO}/labels/${ENCODED}" --silent 2>/dev/null || true
done
while IFS='|' read -r name color description; do
[ -z "$name" ] && continue
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/labels" 2>/dev/null \
-f name="$name" -f color="$color" -f description="$description" \
--silent 2>/dev/null || true
done << 'LABELS'
joomla|7F52FF|Joomla extension or component
dolibarr|FF6B6B|Dolibarr module or extension
generic|808080|Generic project or library
php|4F5D95|PHP code changes
javascript|F7DF1E|JavaScript code changes
typescript|3178C6|TypeScript code changes
python|3776AB|Python code changes
css|1572B6|CSS/styling changes
html|E34F26|HTML template changes
documentation|0075CA|Documentation changes
ci-cd|000000|CI/CD pipeline changes
docker|2496ED|Docker configuration changes
tests|00FF00|Test suite changes
security|FF0000|Security-related changes
dependencies|0366D6|Dependency updates
config|F9D0C4|Configuration file changes
build|FFA500|Build system changes
automation|8B4513|Automated processes or scripts
mokostandards|B60205|MokoStandards compliance
needs-review|FBCA04|Awaiting code review
work-in-progress|D93F0B|Work in progress, not ready for merge
breaking-change|D73A4A|Breaking API or functionality change
priority: critical|B60205|Critical priority, must be addressed immediately
priority: high|D93F0B|High priority
priority: medium|FBCA04|Medium priority
priority: low|0E8A16|Low priority
type: bug|D73A4A|Something isn't working
type: feature|A2EEEF|New feature or request
type: enhancement|84B6EB|Enhancement to existing feature
type: refactor|F9D0C4|Code refactoring
type: chore|FEF2C0|Maintenance tasks
type: version|0E8A16|Version-related change
status: pending|FBCA04|Pending action or decision
status: in-progress|0E8A16|Currently being worked on
status: blocked|B60205|Blocked by another issue or dependency
status: on-hold|D4C5F9|Temporarily on hold
status: wontfix|FFFFFF|This will not be worked on
size/xs|C5DEF5|Extra small change (1-10 lines)
size/s|6FD1E2|Small change (11-30 lines)
size/m|F9DD72|Medium change (31-100 lines)
size/l|FFA07A|Large change (101-300 lines)
size/xl|FF6B6B|Extra large change (301-1000 lines)
size/xxl|B60205|Extremely large change (1000+ lines)
health: excellent|0E8A16|Health score 90-100
health: good|FBCA04|Health score 70-89
health: fair|FFA500|Health score 50-69
health: poor|FF6B6B|Health score below 50
standards-update|B60205|MokoStandards sync update
standards-drift|FBCA04|Repository drifted from MokoStandards
sync-report|0075CA|Bulk sync run report
sync-failure|D73A4A|Bulk sync failure requiring attention
push-failure|D73A4A|File push failure requiring attention
health-check|0E8A16|Repository health check results
version-drift|FFA500|Version mismatch detected
deploy-failure|CC0000|Automated deploy failure tracking
template-validation-failure|D73A4A|Template workflow validation failure
version|0E8A16|Version bump or release
LABELS
echo "✅ Standard labels created" >> $GITHUB_STEP_SUMMARY
# ── BRANCH CLEANUP ───────────────────────────────────────────────────
- name: Delete old sync branches
if: steps.tasks.outputs.clean_branches == 'true'
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
CURRENT="chore/sync-mokostandards-v04.05"
echo "## 🌿 Branch Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
FOUND=false
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/branches?per_page=100" | jq -r '.[].name' 2>/dev/null | \
grep "^chore/sync-mokostandards" | \
grep -v "^${CURRENT}$" | while read -r branch; do
gh pr list --repo "$REPO" --head "$branch" --state open --json number 2>/dev/null | jq -r '.[].number' | while read -r pr; do
gh pr close "$pr" --repo "$REPO" --comment "Superseded by \`${CURRENT}\`" 2>/dev/null || true
echo " Closed PR #${pr}" >> $GITHUB_STEP_SUMMARY
done
gh api -X DELETE "repos/${REPO}/git/refs/heads/${branch}" --silent 2>/dev/null || true
echo " Deleted: \`${branch}\`" >> $GITHUB_STEP_SUMMARY
FOUND=true
done
if [ "$FOUND" != "true" ]; then
echo "✅ No old sync branches found" >> $GITHUB_STEP_SUMMARY
fi
# ── WORKFLOW RUN CLEANUP ─────────────────────────────────────────────
- name: Clean up workflow runs
if: steps.tasks.outputs.clean_workflows == 'true'
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
echo "## 🔄 Workflow Run Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
DELETED=0
# Delete cancelled and stale workflow runs
for status in cancelled stale; do
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/actions/runs?status=${status}&per_page=100" 2>/dev/null \
2>/dev/null | jq -r '.workflow_runs[].id' | while read -r run_id; do
gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}" --silent 2>/dev/null || true
DELETED=$((DELETED+1))
done
done
echo "✅ Cleaned cancelled/stale workflow runs" >> $GITHUB_STEP_SUMMARY
# ── LOG CLEANUP ──────────────────────────────────────────────────────
- name: Delete old workflow run logs
if: steps.tasks.outputs.clean_logs == 'true'
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
echo "## 📋 Log Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Deleting logs older than: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY
DELETED=0
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/actions/runs?created=<${CUTOFF}&per_page=100" 2>/dev/null \
2>/dev/null | jq -r '.workflow_runs[].id' | while read -r run_id; do
gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}/logs" --silent 2>/dev/null || true
DELETED=$((DELETED+1))
done
echo "✅ Cleaned old workflow run logs" >> $GITHUB_STEP_SUMMARY
# ── ISSUE TEMPLATE FIX ──────────────────────────────────────────────
- name: Strip copyright headers from issue templates
if: steps.tasks.outputs.fix_templates == 'true'
run: |
echo "## 📋 Issue Template Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
FIXED=0
for f in .github/ISSUE_TEMPLATE/*.md; do
[ -f "$f" ] || continue
if grep -q '^<!--$' "$f"; then
sed -i '/^<!--$/,/^-->$/d' "$f"
echo " Cleaned: \`$(basename $f)\`" >> $GITHUB_STEP_SUMMARY
FIXED=$((FIXED+1))
fi
done
if [ "$FIXED" -gt 0 ]; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add .github/ISSUE_TEMPLATE/
git commit -m "fix: strip copyright comment blocks from issue templates [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
echo "✅ ${FIXED} template(s) cleaned and committed" >> $GITHUB_STEP_SUMMARY
else
echo "✅ No templates need cleaning" >> $GITHUB_STEP_SUMMARY
fi
# ── REBUILD DOC INDEXES ─────────────────────────────────────────────
- name: Rebuild docs/ index files
if: steps.tasks.outputs.rebuild_indexes == 'true'
run: |
echo "## 📚 Documentation Index Rebuild" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ ! -d "docs" ]; then
echo "⏭️ No docs/ directory — skipping" >> $GITHUB_STEP_SUMMARY
exit 0
fi
UPDATED=0
# Generate index.md for each docs/ subdirectory
find docs -type d | while read -r dir; do
INDEX="${dir}/index.md"
FILES=$(find "$dir" -maxdepth 1 -name "*.md" ! -name "index.md" -printf "- [%f](./%f)\n" 2>/dev/null | sort)
if [ -z "$FILES" ]; then
continue
fi
cat > "$INDEX" << INDEXEOF
# $(basename "$dir")
## Documents
${FILES}
---
*Auto-generated by repository-cleanup workflow*
INDEXEOF
# Dedent
sed -i 's/^ //' "$INDEX"
UPDATED=$((UPDATED+1))
done
if [ "$UPDATED" -gt 0 ]; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add docs/
if ! git diff --cached --quiet; then
git commit -m "docs: rebuild documentation indexes [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
echo "✅ ${UPDATED} index file(s) rebuilt and committed" >> $GITHUB_STEP_SUMMARY
else
echo "✅ All indexes already up to date" >> $GITHUB_STEP_SUMMARY
fi
else
echo "✅ No indexes to rebuild" >> $GITHUB_STEP_SUMMARY
fi
# ── VERSION DRIFT DETECTION ──────────────────────────────────────────
- name: Check for version drift
run: |
echo "## 📦 Version Drift Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ ! -f "README.md" ]; then
echo "⏭️ No README.md — skipping" >> $GITHUB_STEP_SUMMARY
exit 0
fi
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md 2>/dev/null | head -1)
if [ -z "$README_VERSION" ]; then
echo "⚠️ No VERSION found in README.md FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY
exit 0
fi
echo "**README version:** \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
DRIFT=0
CHECKED=0
# Check all files with FILE INFORMATION blocks
while IFS= read -r -d '' file; do
FILE_VERSION=$(grep -oP '^\s*\*?\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' "$file" 2>/dev/null | head -1)
[ -z "$FILE_VERSION" ] && continue
CHECKED=$((CHECKED+1))
if [ "$FILE_VERSION" != "$README_VERSION" ]; then
echo " ⚠️ \`${file}\`: \`${FILE_VERSION}\` (expected \`${README_VERSION}\`)" >> $GITHUB_STEP_SUMMARY
DRIFT=$((DRIFT+1))
fi
done < <(find . -maxdepth 4 -type f \( -name "*.php" -o -name "*.md" -o -name "*.yml" \) ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -print0 2>/dev/null)
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$DRIFT" -gt 0 ]; then
echo "⚠️ **${DRIFT}** file(s) out of ${CHECKED} have version drift" >> $GITHUB_STEP_SUMMARY
echo "Run \`sync-version-on-merge\` workflow or update manually" >> $GITHUB_STEP_SUMMARY
else
echo "✅ All ${CHECKED} file(s) match README version \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# ── PROTECT CUSTOM WORKFLOWS ────────────────────────────────────────
- name: Ensure custom workflow directory exists
run: |
echo "## 🔧 Custom Workflows" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ ! -d ".github/workflows/custom" ]; then
mkdir -p .github/workflows/custom
cat > .github/workflows/custom/README.md << 'CWEOF'
# Custom Workflows
Place repo-specific workflows here. Files in this directory are:
- **Never overwritten** by MokoStandards bulk sync
- **Never deleted** by the repository-cleanup workflow
- Safe for custom CI, notifications, or repo-specific automation
Synced workflows live in `.github/workflows/` (parent directory).
CWEOF
sed -i 's/^ //' .github/workflows/custom/README.md
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add .github/workflows/custom/
if ! git diff --cached --quiet; then
git commit -m "chore: create .github/workflows/custom/ for repo-specific workflows [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
echo "✅ Created \`.github/workflows/custom/\` directory" >> $GITHUB_STEP_SUMMARY
fi
else
CUSTOM_COUNT=$(find .github/workflows/custom -name "*.yml" -o -name "*.yaml" 2>/dev/null | wc -l)
echo "✅ Custom workflow directory exists (${CUSTOM_COUNT} workflow(s))" >> $GITHUB_STEP_SUMMARY
fi
# ── DELETE CLOSED ISSUES ──────────────────────────────────────────────
- name: Delete old closed issues
if: steps.tasks.outputs.delete_closed_issues == 'true'
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
echo "## 🗑️ Closed Issue Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Deleting issues closed before: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY
DELETED=0
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" 2>/dev/null \
| jq -r ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do
# Lock and close with "not_planned" to mark as cleaned up
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${num}/lock" 2>/dev/null -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true
echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY
DELETED=$((DELETED+1))
done
if [ "$DELETED" -eq 0 ] 2>/dev/null; then
echo "✅ No old closed issues found" >> $GITHUB_STEP_SUMMARY
else
echo "✅ Locked ${DELETED} old closed issue(s)" >> $GITHUB_STEP_SUMMARY
fi
- name: Summary
if: always()
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "*Run by @${{ github.actor }} — trigger: ${{ github.event_name }}*" >> $GITHUB_STEP_SUMMARY

2598
.github/workflows/standards-compliance.yml vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template
# VERSION: 04.06.00
# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers
# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos.
# README.md is the single source of truth for the repository version.
name: Sync Version from README
on:
pull_request:
types: [closed]
branches:
- main
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (preview only, no commit)'
type: boolean
default: false
permissions:
contents: write
issues: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync-version:
name: Propagate README version
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN || github.token }}
fetch-depth: 0
- name: Set up PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch version/04 --quiet \
"https://x-access-token:${GH_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoStandards.git" \
/tmp/mokostandards
cd /tmp/mokostandards
composer install --no-dev --no-interaction --quiet
- name: Auto-bump patch version
if: ${{ github.event_name != 'workflow_dispatch' && github.actor != 'gitea-actions[bot]' }}
run: |
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then
echo "README.md changed in this push — skipping auto-bump"
exit 0
fi
RESULT=$(php /tmp/mokostandards/api/cli/version_bump.php --path .) || {
echo "⚠️ Could not bump version — skipping"
exit 0
}
echo "Auto-bumping patch: $RESULT"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add README.md
git commit -m "chore(version): auto-bump patch ${RESULT} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
- name: Extract version from README.md
id: readme_version
run: |
git pull --ff-only 2>/dev/null || true
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null)
if [ -z "$VERSION" ]; then
echo "⚠️ No VERSION in README.md — skipping propagation"
echo "skip=true" >> $GITHUB_OUTPUT
exit 0
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "skip=false" >> $GITHUB_OUTPUT
echo "✅ README.md version: $VERSION"
- name: Run version sync
if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }}
run: |
php /tmp/mokostandards/api/maintenance/update_version_from_readme.php \
--path . \
--create-issue \
--repo "${{ github.repository }}"
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
- name: Commit updated files
if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }}
run: |
git pull --ff-only 2>/dev/null || true
if git diff --quiet; then
echo " No version changes needed — already up to date"
exit 0
fi
VERSION="${{ steps.readme_version.outputs.version }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add -A
git commit -m "chore(version): sync badges and headers to ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
- name: Summary
run: |
VERSION="${{ steps.readme_version.outputs.version }}"
echo "## 📦 Version Sync — ${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Source:** \`README.md\` FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY
echo "**Version:** \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY

346
.github/workflows/update-server.yml vendored Normal file
View File

@@ -0,0 +1,346 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Joomla
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/joomla/update-server.yml.template
# VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple <update> entries:
# - <tag>stable</tag> on push to main (from auto-release)
# - <tag>rc</tag> on push to rc/**
# - <tag>development</tag> on push to dev/**
#
# Joomla filters by user's "Minimum Stability" setting.
name: Update Joomla Update Server XML Feed
on:
pull_request:
types: [closed]
branches:
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
jobs:
update-xml:
name: Update updates.xml
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN || github.token }}
fetch-depth: 0
- name: Setup MokoStandards tools
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch version/04 --quiet \
"https://x-access-token:${GH_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoStandards.git" \
/tmp/mokostandards 2>/dev/null || true
if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then
cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Generate updates.xml entry
run: |
BRANCH="${{ github.ref_name }}"
REPO="${{ github.repository }}"
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Auto-bump patch on alpha/beta/rc branches (not dev — dev bumps manually)
if [[ "$BRANCH" != dev/* ]]; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
BUMPED=$(php /tmp/mokostandards/api/cli/version_bump.php --path . 2>/dev/null || true)
if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
git push 2>/dev/null || true
fi
fi
# Determine stability from branch or input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
elif [[ "$BRANCH" == dev/* ]]; then
STABILITY="development"
else
STABILITY="stable"
fi
# Parse manifest (portable — no grep -P)
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "No Joomla manifest found — skipping"
exit 0
fi
# Extract fields using sed (works on all runners)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Templates and modules don't have <element> — derive from <name>
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
fi
# Use manifest version if README version is empty
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
CLIENT_TAG=""
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
FOLDER_TAG=""
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
PHP_TAG=""
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
# Version suffix for non-stable
DISPLAY_VERSION="$VERSION"
case "$STABILITY" in
development) DISPLAY_VERSION="${VERSION}-dev" ;;
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
esac
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
# Each stability level has its own release tag
case "$STABILITY" in
development) RELEASE_TAG="development" ;;
alpha) RELEASE_TAG="alpha" ;;
beta) RELEASE_TAG="beta" ;;
rc) RELEASE_TAG="release-candidate" ;;
*) RELEASE_TAG="v${MAJOR}" ;;
esac
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
DOWNLOAD_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
INFO_URL="https://github.com/${REPO}"
# ── Build install packages (ZIP + tar.gz) ───────────────────
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_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*' .
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
# Ensure release exists
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || \
gh release create "$RELEASE_TAG" --title "${RELEASE_TAG} (${DISPLAY_VERSION})" --notes "${STABILITY} release" --prerelease --target main 2>/dev/null || true
# Upload both formats
gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true
gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
else
SHA256=""
fi
# ── Build the new entry ───────────────────────────────────────
NEW_ENTRY=""
NEW_ENTRY="${NEW_ENTRY} <update>\n"
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} (${STABILITY})</description>\n"
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
NEW_ENTRY="${NEW_ENTRY} <version>${DISPLAY_VERSION}</version>\n"
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <tags>\n"
NEW_ENTRY="${NEW_ENTRY} <tag>${STABILITY}</tag>\n"
NEW_ENTRY="${NEW_ENTRY} </tags>\n"
NEW_ENTRY="${NEW_ENTRY} <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
TAR_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>sha256:${SHA256}</sha256>\n"
NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n"
[ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
NEW_ENTRY="${NEW_ENTRY} </update>"
# ── Write new entry to temp file ───────────────────────────────
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
# ── Merge into updates.xml ─────────────────────────────────────
if [ ! -f "updates.xml" ]; then
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>' > updates.xml
printf '%s\n' '<updates>' >> updates.xml
cat /tmp/new_entry.xml >> updates.xml
printf '\n%s\n' '</updates>' >> updates.xml
else
# Remove existing entry for this stability, insert new one
printf 'import re\nstability = "%s"\n' "${STABILITY}" > /tmp/merge_xml.py
printf 'with open("updates.xml") as f: content = f.read()\n' >> /tmp/merge_xml.py
printf 'with open("/tmp/new_entry.xml") as f: new_entry = f.read()\n' >> /tmp/merge_xml.py
printf 'pattern = r" <update>.*?<tag>" + re.escape(stability) + r"</tag>.*?</update>\\n?"\n' >> /tmp/merge_xml.py
printf 'content = re.sub(pattern, "", content, flags=re.DOTALL)\n' >> /tmp/merge_xml.py
printf 'content = content.replace("</updates>", new_entry + "\\n</updates>")\n' >> /tmp/merge_xml.py
printf 'content = re.sub(r"\\n{3,}", "\\n\\n", content)\n' >> /tmp/merge_xml.py
printf 'with open("updates.xml", "w") as f: f.write(content)\n' >> /tmp/merge_xml.py
python3 /tmp/merge_xml.py 2>/dev/null || {
# Fallback: rebuild keeping other stability entries
{
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' '<updates>'
for TAG in stable rc development; do
[ "$TAG" = "${STABILITY}" ] && continue
if grep -q "<tag>${TAG}</tag>" updates.xml 2>/dev/null; then
sed -n "/<update>/,/<\/update>/{ /<tag>${TAG}<\/tag>/p; }" updates.xml
fi
done
cat /tmp/new_entry.xml
printf '\n%s\n' '</updates>'
} > /tmp/updates_new.xml
mv /tmp/updates_new.xml updates.xml
}
fi
# Commit
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/')
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: |
# ── Permission check: admin or maintain role required ──────
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}/permission" 2>/dev/null \
2>/dev/null | jq -r '.permission' || \
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}" 2>/dev/null \
2>/dev/null | jq -r '.role' || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
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 --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards/api/deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards/api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY

207
.gitignore vendored Normal file
View File

@@ -0,0 +1,207 @@
# ============================================================
# Local task tracking (not version controlled)
# ============================================================
TODO.md
# ============================================================
# Environment and secrets
# ============================================================
.env
.env.local
.env.*.local
*.local.php
*.secret.php
configuration.php
configuration.*.php
configuration.local.php
conf/conf.php
conf/conf*.php
secrets/
*.secrets.*
# ============================================================
# Logs, dumps and databases
# ============================================================
*.db
*.db-journal
*.dump
*.log
*.pid
*.seed
# ============================================================
# OS / Editor / IDE cruft
# ============================================================
.DS_Store
Thumbs.db
desktop.ini
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
$RECYCLE.BIN/
System Volume Information/
*.lnk
Icon?
.idea/
.settings/
.claude/
.vscode/*
!.vscode/tasks.json
!.vscode/settings.json.example
!.vscode/extensions.json
*.code-workspace
*.sublime*
.project
.buildpath
.classpath
*.bak
*.swp
*.swo
*.tmp
*.old
*.orig
# ============================================================
# Dev scripts and scratch
# ============================================================
TODO.md
todo*
*ffs*
# ============================================================
# SFTP / sync tools
# ============================================================
sftp-config*.json
sftp-config.json.template
sftp-settings.json
# ============================================================
# Sublime SFTP / FTP sync
# ============================================================
*.sublime-project
*.sublime-workspace
*.sublime-settings
.libsass.json
*.ffs*
# ============================================================
# Replit / cloud IDE
# ============================================================
.replit
replit.md
# ============================================================
# Archives / release artifacts
# ============================================================
*.7z
*.rar
*.tar
*.tar.gz
*.tgz
*.zip
artifacts/
release/
releases/
# ============================================================
# Build outputs and site generators
# ============================================================
.mkdocs-build/
.cache/
.parcel-cache/
build/
dist/
out/
site/
*.map
*.css.map
*.js.map
*.tsbuildinfo
# ============================================================
# CI / test artifacts
# ============================================================
.coverage
.coverage.*
coverage/
coverage.xml
htmlcov/
junit.xml
reports/
test-results/
tests/_output/
.github/local/
.github/workflows/*.log
# ============================================================
# Node / JavaScript
# ============================================================
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.pnpm-store/
.yarn/
.npmrc
.eslintcache
package-lock.json
# ============================================================
# PHP / Composer tooling
# ============================================================
composer.lock
*.phar
codeception.phar
.phpunit.result.cache
.php_cs.cache
.php-cs-fixer.cache
.phpstan.cache
.phplint-cache
phpmd-cache/
.psalm/
.rector/
# ============================================================
# Python
# ============================================================
__pycache__/
*.py[cod]
*.pyc
*$py.class
*.so
.Python
.eggs/
*.egg
*.egg-info/
.installed.cfg
MANIFEST
develop-eggs/
downloads/
eggs/
parts/
sdist/
var/
wheels/
ENV/
env/
.venv/
venv/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.pyright/
.tox/
.nox/
*.cover
*.coverage
hypothesis/
# Custom theme palettes (site-specific, not version controlled)
# Note: src/templates/*.custom.css are STARTER templates (tracked)
src/media/css/theme/*.custom.css
src/media/css/theme/*.custom.min.css
templates/*.custom.css
update.xml
.moko-standards

630
CHANGELOG.md Normal file
View File

@@ -0,0 +1,630 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation
PATH: ./CHANGELOG.md
VERSION: 03.09.03
BRIEF: Changelog file documenting version history of MokoOnyx
-->
# Changelog — MokoOnyx (VERSION: 03.09.03)
All notable changes to the MokoOnyx Joomla template are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [03.10.00] - 2026-04-18 — Bridge Release (MokoOnyx → MokoOnyx)
### Important
- **Template Rename** — MokoOnyx is being renamed to **MokoOnyx**. This bridge release automatically migrates your template settings, menu assignments, and files to the new name. MokoOnyx can be safely uninstalled after this update.
### Added
- **Offline page redesign** — Full-viewport background from Joomla offline_image or header background, glass card overlay, centered logo with glow, login accordion, copyright footer
- **CSS variable click-to-copy** — Text containing `--variable-name` patterns is wrapped in clickable chips that copy to clipboard with toast notification
- **Brand-aside 3-column layout** — Flex columns like top-a with card style
- **mod_stats table layout** — Converted from definition list to semantic table
- **Favicon multi-format support** — Now handles PNG, JPEG, GIF, WebP, BMP (not just PNG)
- **Theme variables** — `--theme-fab-bg`, `--theme-fab-color`, `--theme-fab-btn-bg`, `--theme-fab-border`, `--offline-card-bg`
- **Footer CSS variables** — Added to CSS Variables reference tab
- **Bridge migration script** — `helper/bridge.php` handles automatic MokoOnyx → MokoOnyx migration
- **Dedicated release runner** — Release workflows run on isolated `release` label runner
- **Runner fleet** — 3 CI + 1 release runner (12 concurrent jobs)
### Changed
- **Gitea-primary CI/CD** — All workflows use Gitea API, GitHub is backup for stable/RC only
- **Theme switcher** — Larger, bordered, theme-aware colors (off-white on dark, primary on light)
- **Auto switch** — Red when off, green when on
- **A11y toolbar** — Theme-aware colors for dark mode visibility
- **Search button border** — Matches input border (`--input-border-color`)
- **Offline message** — 0=hidden, 1=custom message, 2=system language string
- **Light theme fonts** — Fixed trailing `)` syntax error, normalized quote style to match dark
- **`--accent-color-secondary`** — Unified to `#6fb3ff` across both themes
- **`--alert-color`** — Set to `#000` in light theme
### Removed
- Brand showcase tab (redundant with theme preview)
- Position selectors for a11y/theme FAB (forced to bottom-right)
- Custom theme CSS from git tracking (site-specific, gitignored)
### Fixed
- SHA-256 checksum format — Removed `sha256:` prefix (Joomla expects raw hex)
- Favicon path resolution — Strips `#joomlaImage://` fragment, tries multiple path candidates
- `REQUIRE_SIGNIN_VIEW` — Set to `false` for public release downloads
- Release workflow — Uses Gitea API to update `updates.xml` on main (bypasses branch protection)
- Language loading on offline page — `com_users` and core language files loaded explicitly
---
## [Unreleased] - 2026-04-02
### Added
- **Favicon configuration** — New "Favicon" tab in template config; upload a PNG and all favicon sizes are auto-generated via PHP GD (ICO, Apple Touch Icon 180px, Android Chrome 192/512px, site.webmanifest)
- **Module overrides** — 11 new `default.php` layout overrides for Joomla core modules: `mod_custom`, `mod_articles_latest`, `mod_articles_popular`, `mod_articles_news`, `mod_articles_category`, `mod_breadcrumbs`, `mod_footer`, `mod_login`, `mod_finder`, `mod_tags_popular`, `mod_tags_similar`, `mod_related_items`
- **Module title support** — All module overrides respect `$module->showtitle`, `header_tag`, `header_class`, and `moduleclass_sfx` parameters
- **Module CSS** — BEM-scoped styles for module titles, article lists, tag badges, search forms, login forms, breadcrumbs, and footer content
- **Hero card variables** — Full variable-driven hero system: `--hero-card-bg`, `--hero-card-color`, `--hero-card-overlay`, `--hero-card-border-radius`, `--hero-card-padding-x/y`, `--hero-card-max-width`, plus `--hero-alt-card-*` for secondary variant
- **Hero mobile breakpoint** — Photo background hidden on mobile (≤767.98px), hero card becomes full-bleed (100dvh, no border-radius)
- **CSS fallback values** — 1365 `var()` calls in template.css now include inline fallback values
- **Card border-radius** — `.card` now has `.25rem` fallback on `var(--card-border-radius)`
- **Usage section in README** — Added missing "Usage" section required by MokoStandards
### Changed
- **Button backgrounds** — `--btn-bg: transparent` changed to `var(--body-bg)` in dark and light themes
- **Offcanvas close button** — `.offcanvas-header .btn-close` now gets `background-color` from `--offcanvas-bg`
- **Custom template sync** — Both `dark.custom.css` and `light.custom.css` now contain all variables from their standard counterparts (was missing 223 variables)
- **Overlay layer** — Added `--hero-overlay-bg-position` and `--hero-overlay-bg-size` variables
- **Legacy CSS cleanup** — Removed vendor prefixes (`-webkit-box`, `-ms-flexbox`) from `.overlay` rules, replaced with modern flexbox
### Removed
- **FILE INFORMATION headers** — Stripped DEFGROUP/INGROUP/PATH/VERSION/BRIEF metadata from all PHP, CSS, JS, INI, and HTML files (kept in XML and README per policy)
- **Mobile overrides** — Deleted 26 `mobile.php` layout files and their empty parent directories
- **Joomla-specific gitignore entries** — Removed ~700 lines of Joomla CMS core paths from `.gitignore` (not applicable to a template repository)
### Fixed
- **CI: composer install** — Workflow `standards-compliance.yml` now conditionally runs `composer install` only when `composer.json` exists
- **CI: YAML syntax** — Fixed invalid YAML in `auto-update-sha.yml` caused by multiline commit message in run block
---
## [03.09.02] - 2026-03-26
### Added - Hero Variant System & Block Color System
#### Hero Variants
- **`.hero#primary`** and **`.hero#secondary`** CSS variant system for visually distinct hero treatments
- Shared `.hero` base class with `background-size: cover`, `border-radius: .5rem`, and `overflow: hidden`
- Six new CSS variables (`--hero-primary-bg-color`, `--hero-primary-overlay`, `--hero-primary-color`, and secondary equivalents)
- Light and dark mode defaults in custom palette templates
#### Block Color System
- Automatic `:nth-child()` slot palette for `top-a`, `top-b`, `bottom-a`, `bottom-b` module positions
- Four color slots (`--block-color-1` through `--block-color-4`) with matching text variables
- Named per-module overrides: `#block-highlight`, `#block-cta`, `#block-alert`
- ID specificity wins over `:nth-child()` — no `!important` needed
#### Files Modified
- `src/media/css/template.css` — hero variant rules, block color `:nth-child()` rules, named override rules
- `src/media/css/theme/light.standard.css` — hero and block color variables (light standard)
- `src/media/css/theme/dark.standard.css` — hero and block color variables (dark standard)
- `src/templates/light.custom.css` — hero and block color variables (light custom starter)
- `src/templates/dark.custom.css` — hero and block color variables (dark custom starter)
- `src/templateDetails.xml` — Theme Preview tab, hero/block note fields, scriptfile registration, version bump to 03.09.02
- `src/language/en-GB/tpl_mokoonyx.ini` — language strings for new admin fields (British English)
- `src/language/en-US/tpl_mokoonyx.ini` — language strings for new admin fields (American English)
- `docs/CSS_VARIABLES.md` — full variable reference for both systems, sync script documentation
- `CHANGELOG.md` — this entry
#### Files Added
- `src/templates/theme-test.html` — Bootstrap-style test page with branded showcase, CSS variable swatches, hero demos, block color demos, and color test image
- `src/script.php` — Joomla install/update lifecycle script (runs CSS variable sync on upgrade, checks PHP/Joomla minimum versions)
- `src/sync_custom_vars.php` — CLI/library utility that detects missing CSS variables in user custom palettes and injects them
- `src/templates/brand-showcase.html` — Interactive color system gradients with hover pixel sampler, Bootstrap component showcase
#### Variable Audit
- All 20 hero/block variables confirmed present in all 4 theme files (light/dark standard + custom)
- No duplicate variable declarations found across any theme file
- `--gutter-x` references in template.css are self-scoped to grid containers (standard Bootstrap 5 behavior, not a `:root` variable)
---
## [03.08.03] - 2026-02-27
### Added - Main Menu Collapsible Dropdown Override
**New feature**: Added responsive "Main Menu" mod_menu override with Bootstrap 5 collapsible dropdown functionality.
#### What's New
- **Main Menu module override** with full Bootstrap 5 responsive navbar
- Collapsible hamburger menu for mobile devices
- Multi-level dropdown support with hover on desktop, tap on mobile
- WCAG 2.1 compliant touch targets (48px on mobile, 44px on desktop)
- BEM naming convention: `.mod-menu-main__*`
- **Appears as "Mainmenu" layout option** in Joomla admin module settings
#### Files Added
- `src/templates/html/mod_menu/mainmenu.php` - Main layout with Bootstrap navbar
- `src/templates/html/mod_menu/mainmenu_component.php` - Component menu items
- `src/templates/html/mod_menu/mainmenu_heading.php` - Heading menu items
- `src/templates/html/mod_menu/mainmenu_separator.php` - Separator menu items
- `src/templates/html/mod_menu/mainmenu_url.php` - URL menu items
- `src/templates/html/mod_menu/index.html` - Security file
#### Features
- **Bootstrap 5 Navbar**: Uses Bootstrap's native navbar-nav structure
- **Collapsible on Mobile**: Hamburger menu with smooth collapse animation
- **Dropdown Menus**: Multi-level dropdown support with caret indicators
- **Responsive Breakpoints**: Mobile-first design adapting at 768px and 992px
- **Touch-Friendly**: 48px minimum touch targets on mobile
- **Accessible**: ARIA labels and keyboard navigation support
- **Active States**: Visual indicators for current and active menu items
- **Alternative Layout**: Named `mainmenu.php` (not `default.php`) to appear as selectable layout option in Joomla admin
#### CSS Architecture
- 200+ lines of responsive CSS in template.css
- BEM naming: `.mod-menu-main`, `.mod-menu-main__list`, `.mod-menu-main__link`
- CSS variables integration for colors and borders
- Hover effects on desktop, tap effects on mobile
- Smooth transitions and animations
#### Module Count Update
- **Before**: 16 module overrides
- **After**: 17 module overrides (added mod_menu "Main Menu")
- **Component overrides**: Still 7 (unchanged)
### Removed - mod_search Override
**Cassiopeia approach**: Removed mod_search override to align with Cassiopeia template philosophy of not overriding standard Joomla modules.
#### Reason for Removal
- mod_search is a standard Joomla core module
- Following Cassiopeia template approach: use core layouts for standard modules
- Prevents potential language loading issues
- Ensures compatibility with future Joomla updates
- Core mod_search already includes responsive design and accessibility features
#### Files Removed
- `src/templates/html/mod_search/default.php` - Custom search module layout
- `src/templates/html/mod_search/index.html` - Security file
#### Module Count Update (After Removal)
- **Before**: 17 module overrides
- **After**: 16 module overrides (removed mod_search)
- **Component overrides**: Still 7 (unchanged)
### Removed - Documentation Cleanup
**Documentation policy**: Removed all markdown files from `src/templates/html/` directory. All documentation belongs in `docs/` folder only.
#### Files Removed (9 markdown files)
- `src/templates/html/STANDARD_MODULES_README.md`
- `src/templates/html/INDUSTRY_MODULES_README.md`
- `src/templates/html/VIRTUEMART_MODULES_README.md`
- `src/templates/html/mod_virtuemart_cart/README.md`
- `src/templates/html/mod_virtuemart_category/README.md`
- `src/templates/html/mod_virtuemart_currencies/README.md`
- `src/templates/html/mod_virtuemart_manufacturer/README.md`
- `src/templates/html/mod_virtuemart_product/README.md`
- `src/templates/html/mod_search/README.md`
**Note**: All module override documentation is consolidated in `docs/MODULE_OVERRIDES.md`. The `src/templates/html/` directory now contains only PHP override files and `index.html` security files.
**Note**: Unlike the previously removed mod_menu override (v03.08.01), this new "Main Menu" override is properly structured based on Joomla core layouts and Bootstrap 5, ensuring language strings load correctly and menu functionality works as expected. The layout is named `mainmenu.php` (not `default.php`) to appear as an alternative layout option "Mainmenu" in the Joomla admin module dropdown selector, preserving Joomla's core default menu layout.
## [03.08.02] - 2026-02-27
### Removed - Fix Language Loading in All Module Overrides
**Critical fix**: Removed standard Joomla module overrides to fix language string loading issues. Following Cassiopeia template approach.
#### Problem
- Default language strings not loading in module overrides (mod_breadcrumbs, mod_login, mod_articles_latest)
- Language constants displayed instead of translated text (e.g., "MOD_LOGIN_VALUE_USERNAME" instead of "Username")
- Custom overrides interfered with Joomla's module initialization and language loading process
#### Solution - Cassiopeia Approach
- **Removed** standard Joomla module overrides:
- `src/templates/html/mod_breadcrumbs/` (2 files)
- `src/templates/html/mod_login/` (2 files)
- `src/templates/html/mod_articles_latest/` (2 files)
- Template now uses Joomla's core module layouts for standard modules
- Language files load automatically via Joomla's module system
- Custom styling can still be applied via CSS using module-specific classes
- **Retained** third-party extension overrides where they add mobile-responsive value:
- VirtueMart modules (5): mod_virtuemart_cart, _category, _currencies, _manufacturer, _product
- Community Builder modules (2): mod_cblogin, mod_comprofilerOnline
- Other extensions (9): mod_acymailing, mod_hikashop_cart, mod_k2_content, mod_kunena*, mod_osmembership, mod_search
#### Cassiopeia Template Philosophy
- Cassiopeia (Joomla's default template) does NOT override standard module layouts
- It relies on core Joomla module files and applies styling via CSS
- Overrides are only created when structural changes are absolutely necessary
- This ensures compatibility, automatic language loading, and easier maintenance
#### Module Count Update
- **Before**: 19 module overrides
- **After**: 16 module overrides
- **Removed**: 3 standard Joomla modules (breadcrumbs, login, articles_latest)
- **Component overrides**: Still 7 (unchanged)
#### Files Removed
- `src/templates/html/mod_breadcrumbs/default.php`
- `src/templates/html/mod_breadcrumbs/index.html`
- `src/templates/html/mod_login/default.php`
- `src/templates/html/mod_login/index.html`
- `src/templates/html/mod_articles_latest/default.php`
- `src/templates/html/mod_articles_latest/index.html`
**Note**: This follows Joomla best practices by using core layouts for standard modules. Styling is handled via CSS. Third-party extension overrides remain for mobile responsiveness.
## [03.08.01] - 2026-02-27
### Removed - Fix Breaking Overrides
**Critical fix**: Removed mod_menu override that was causing menu links to break and language strings not to load.
#### Problem
- mod_menu override files (default.php, default_component.php, default_url.php) were attempting to load menu-specific layouts that don't exist in the template
- This broke Joomla's core menu rendering system
- Menu links were not functional
- Language strings were not loading properly in menus
#### Solution
- **Removed** entire `src/templates/html/mod_menu/` directory (4 files)
- Template now uses Joomla's default menu rendering
- Custom styling can still be applied via CSS using `.mod-menu` class
- All menu functionality restored to standard Joomla behavior
#### Documentation Updates
- Updated MODULE_OVERRIDES.md: Changed count from 20 to 19 module overrides, removed mod_menu section, added note about removal
- Updated STANDARD_MODULES_README.md: Removed mod_menu documentation, renumbered remaining modules, updated file structure
- Updated testing checklists to remove mod_menu references
- **Added clarification**: MokoOnyx is a standalone template extension (not a package)
- Updated updates.xml to version 03.08.01
#### Files Removed
- `src/templates/html/mod_menu/default.php`
- `src/templates/html/mod_menu/default_component.php`
- `src/templates/html/mod_menu/default_url.php`
- `src/templates/html/mod_menu/index.html`
**Note**: This is a patch release that removes problematic overrides to restore core functionality. Menu styling via CSS remains intact. MokoOnyx remains a standalone Joomla template extension (type="template"), not bundled as a package.
## [03.08.00] - 2026-02-22
### Added - Community Builder Component Overrides
Minor version bump adding **4 Community Builder component view overrides** to complement the existing CB module overrides (mod_cblogin, mod_comprofilerOnline).
#### Community Builder Components (4 views)
- **com_comprofiler/userprofile**: User profile display with avatar, tabs, and custom fields in responsive layout
- **com_comprofiler/userslist**: User directory with search functionality and responsive grid (1-3 columns)
- **com_comprofiler/registers**: User registration form with multi-step fieldsets, validation, captcha support
- **com_comprofiler/login**: Login page with remember me checkbox, registration and password recovery links
#### CSS Architecture (600+ lines)
- Mobile-first responsive design with Bootstrap breakpoints (576px, 768px, 992px)
- BEM naming convention (`.cb-profile__`, `.cb-userslist__`, `.cb-register__`, `.cb-login__`)
- Integrated with template CSS variables for consistent theming
- 48px touch targets on mobile, 44px on desktop (WCAG 2.1 Level AA)
- 16px input font size on mobile to prevent iOS zoom
- Responsive grids adapting from 1 column (mobile) to 2-3 columns (desktop)
#### Accessibility Features
- Full ARIA labels and descriptions for screen readers
- Semantic HTML5 structure with proper landmarks
- Keyboard navigation support throughout
- Required field indicators with visually-hidden labels
- Focus states with visible outlines
#### Security Best Practices
- Proper output escaping with htmlspecialchars() and ENT_QUOTES
- _JEXEC security checks in all PHP files
- index.html protection files in all directories (6 files)
- CSRF token support in forms
- Input validation and error display
### Technical Details
- **Files Added**: 11 (4 component view files + 6 index.html + 1 root index.html)
- **CSS Lines Added**: 600+ lines of responsive styles
- **PHP Validation**: All files pass syntax validation
- **Component Views**: userprofile, userslist, registers, login
- **Documentation**: Ready for MODULE_OVERRIDES.md update
## [03.07.00] - 2026-02-22
### Added - Mobile-Responsive Module & Component Overrides
This major release introduces **20 mobile-responsive module overrides** and **3 component overrides** designed to enhance the mobile user experience across standard Joomla, VirtueMart, Community Builder, and popular third-party extensions.
#### Search Module
- **mod_search**: Mobile-responsive search with multiple button positions (left, right, top, bottom), 48px touch targets, 16px input font to prevent iOS zoom
#### VirtueMart E-Commerce Modules (5 modules)
- **mod_virtuemart_cart**: Shopping cart with responsive product cards, remove buttons, price display
- **mod_virtuemart_product**: Product showcase with responsive grid (1-4 columns), hover effects, ratings
- **mod_virtuemart_currencies**: Currency selector dropdown with accessible styling
- **mod_virtuemart_category**: Category navigation with hierarchical display, product counts
- **mod_virtuemart_manufacturer**: Manufacturer/brand display with responsive grid (2-4 columns)
- **VIRTUEMART_MODULES_README.md**: Comprehensive master documentation for all VirtueMart overrides
#### Standard Joomla & Community Builder Modules (6 modules)
- **mod_menu**: Main navigation with multiple layout files (default, component, URL), responsive horizontal/vertical layouts
- **mod_breadcrumbs**: Breadcrumb navigation with Schema.org markup for SEO
- **mod_login**: User login/logout form with 2FA support, remember me checkbox
- **mod_articles_latest**: Latest articles with responsive cards, metadata, featured badges
- **mod_cblogin**: Community Builder login with avatar display, profile links
- **mod_comprofilerOnline**: CB online users with avatar grid, online status indicators
- **STANDARD_MODULES_README.md**: Comprehensive master documentation for standard module overrides
#### Industry Extension Modules (8 modules + 2 components)
- **mod_k2_content**: K2 content display with responsive grid (1-3 columns), featured images, metadata
- **mod_acymailing**: Newsletter subscription form with validation, GDPR compliance
- **mod_hikashop_cart**: HikaShop shopping cart with product list, quantity adjustment
- **mod_kunenalatest**: Kunena forum latest posts with excerpts, avatars, reply counts
- **mod_kunenalogin**: Kunena forum login with user avatar, statistics, quick login
- **mod_kunenasearch**: Kunena forum search with multiple button positions
- **mod_kunenastats**: Kunena forum statistics with visual cards, member/topic counts
- **mod_osmembership**: OS Membership Pro plans with pricing cards, feature lists, badges
- **com_kunena/category**: Kunena forum category list component view
- **com_osmembership/plans**: OS Membership Pro responsive pricing table component view
- **INDUSTRY_MODULES_README.md**: Comprehensive master documentation for industry extensions
#### CSS & Styling
- Added **2,000+ lines** of mobile-responsive CSS to `src/media/css/template.css`
- Four dedicated CSS sections for organized styling:
- MOD_SEARCH MOBILE RESPONSIVE STYLES
- VIRTUEMART MODULE MOBILE RESPONSIVE STYLES
- STANDARD JOOMLA & COMMUNITY BUILDER MODULE STYLES
- INDUSTRY EXTENSION MODULE STYLES
- ADDITIONAL KUNENA & MEMBERSHIP PRO MODULE STYLES
- BEM naming convention for all CSS classes (`.mod-search__button`, `.mod-vm-product__grid`, etc.)
- Integration with existing template CSS variables for seamless theming
- Responsive grids with Bootstrap-aligned breakpoints (sm, md, lg, xl, xxl)
#### Documentation
- **docs/MODULE_OVERRIDES.md**: Comprehensive guide covering all 23 overrides
- Feature descriptions and specifications
- CSS architecture and customization guide
- Accessibility features documentation
- Troubleshooting guide
- Best practices and usage examples
- Individual README.md files for VirtueMart module groups (5 modules)
- Master README files for each category (VirtueMart, Standard, Industry)
- Security index.html files in all override directories (23 files)
### Key Features Across All Overrides
#### Mobile-First Responsive Design
- Touch targets: 48px on mobile, 44px on desktop (WCAG 2.1 compliant)
- 16px minimum input font size on mobile (prevents iOS zoom)
- Responsive layouts: 1-4 columns based on screen size
- Mobile-first CSS with progressive enhancement
- Bootstrap-aligned breakpoints: 576px, 768px, 992px, 1200px, 1400px
#### Accessibility
- Full ARIA labels and descriptions on all interactive elements
- Keyboard navigation support throughout
- Screen reader compatible with semantic HTML5
- WCAG 2.1 Level AA compliance
- Proper heading hierarchy and focus management
- Alternative text for images and icons
#### Security
- Proper output escaping with Joomla escapeHtml()
- _JEXEC security checks in all PHP files
- index.html protection files in all directories
- Input validation where applicable
- CSRF token support in forms
#### Maintainability
- BEM naming convention for CSS classes
- Consistent code structure across all overrides
- Comprehensive inline documentation
- Modular, reusable components
- Integration with template CSS variables
### Changed
- **Version**: Updated to 03.07.00 across all files
### Technical Details
- **Total Files**: 66 new files created
- 42 PHP override files
- 23 index.html security files
- 1 comprehensive MODULE_OVERRIDES.md documentation
- **CSS Added**: 2,000+ lines of responsive styles
- **Documentation**: 15,000+ words across all README files
### Migration Notes
- All overrides are opt-in and non-breaking
- Existing sites will continue to work without changes
- Overrides automatically apply when modules are used
- No database changes or migration required
- Custom overrides can coexist with template overrides
### Testing
- All PHP syntax validated
- Code review completed (all issues resolved)
- CodeQL security scan passed
- Responsive design tested across breakpoints
- Accessibility validated with ARIA compliance
---
## [03.06.03] - 2026-01-30
### Added
- **Templates Directory**: Created `/templates/` directory with ready-to-use color palette templates
- `colors_custom_light.css` - Comprehensive light mode color template with all available variables
- `colors_custom_dark.css` - Comprehensive dark mode color template with all available variables
- **CSS Variables Documentation**: Added complete CSS variables reference guide (`docs/CSS_VARIABLES.md`)
- Complete list of all customizable CSS variables
- Organized by category (colors, typography, borders, etc.)
- Usage examples and tips for customization
- Light and dark mode variable differences documented
### Changed
- **README**: Updated title to "README - MokoOnyx (VERSION: 03.09.03)"
- **README**: Fixed custom color variables instructions with correct file paths
- **README**: Updated example CSS variables to use actual template variable names (e.g., `--color-link` instead of `--cassiopeia-color-link`)
- **README**: Added note that custom color files are excluded from version control via `.gitignore`
- **README**: Enhanced Custom Color Palettes section with step-by-step instructions
- **README**: Added link to CSS Variables documentation for complete reference
- **TOC CSS**: Updated bootstrap-toc.css to use template color variables for proper theme integration
- **Version**: Updated version to 03.06.03 across all files
### Documentation
- **docs/README.md**: Added CSS Variables Reference to developer documentation section
- **docs/README.md**: Updated project structure to include `/templates/` directory
- **docs/README.md**: Updated version to 03.06.03
- Clarified that `colors_custom.css` files are gitignored to prevent fork-specific customizations from being committed
## [03.06.02] - 2026-01-30
### Major Rebrand
This release includes a complete rebrand from "Moko-Cassiopeia" (hyphenated) to "MokoOnyx" (camelCase).
### Changed
- **Naming Convention**: Changed template identifier from `moko-cassiopeia` to `mokoonyx` across all files
- **Display Name**: Updated from "Moko-Cassiopeia" to "MokoOnyx" in all documentation and language files
- **Language Constants**: Renamed all language keys from `TPL_MOKO-CASSIOPEIA_*` to `TPL_MOKOONYX_*`
- **Language Files**: Renamed from `tpl_moko-cassiopeia.*` to `tpl_mokoonyx.*` (4 files)
- **Media Paths**: Updated from `media/templates/site/moko-cassiopeia/` to `media/templates/site/mokoonyx/`
- **Repository URLs**: Updated all references to use `MokoOnyx` casing
- **Template Element**: Changed Joomla extension element name from `moko-cassiopeia` to `mokoonyx`
- **Documentation**: Updated all markdown files, XML manifests, and code comments
### Removed
- **Default Assets**: Removed `logo.svg` and `favicon.ico` to allow clean installations
- **Template Overrides**: Removed all template override files (48 files, ~4,500 lines)
- Removed `src/templates/html/` folder entirely
- Removed overrides for: com_content, com_contact, com_engage, mod_menu, mod_custom, mod_gabble, layouts/chromes
- Template now inherits all rendering from Joomla Cassiopeia defaults
- Updated `templateDetails.xml` to remove html folder reference
### Breaking Changes
⚠️ **Important**: This release contains breaking changes:
- Existing installations will see template name change in Joomla admin
- Custom code referencing old language constants (`TPL_MOKO-CASSIOPEIA_*`) will need updates
- Custom code referencing old media paths will need updates
- Sites relying on custom template overrides will revert to Cassiopeia defaults
- Extension element name changed (may require reinstallation in some cases)
### Migration Notes
- Backup your site before upgrading
- Review any custom code for references to old naming convention
- Test thoroughly after upgrade, especially if using custom overrides
## [03.06.00] - 2026-01-28
### Changed
- Updated version to 03.06.00 across all files
- Standardized version numbering format
## [03.05.01] - 2026-01-09
### Added
- Added `dependency-review.yml` workflow for dependency vulnerability scanning
- Added `standards-compliance.yml` workflow for MokoStandards validation
- Added `.github/dependabot.yml` configuration for automated security updates
- Added `docs/README.md` as documentation index
### Changed
- Removed custom `codeql-analysis.yml` workflow (repository uses GitHub's default CodeQL setup)
- Enforced repository compliance with MokoStandards requirements
- Improved security posture with automated scanning and dependency management
## [03.05.00] - 2026-01-04
### Added
- Created `.github/workflows` directory structure
### Changed
- Replaced `./CODE_OF_CONDUCT.md` from `MokoStandards`
- Replaced `./CONTRIBUTING.md` from `MokoStandards`
- TODO split to own file
## [03.01.00] - 2025-12-16
### Added
- Created `.github/workflows/` directory for GitHub Actions
## [03.00.00] - 2025-12-09
### Changed
- Copyright Headers updated to MokoCodingDefaults standards
- Fixed `./templates/mokoonyx/index.php` color style injection
- Upgraded Font Awesome 6 to Font Awesome 7 Free
- Added Font Awesome 7 Free style fallback
### Removed
- Removed `./CODE_OF_CONDUCT.md` (replaced with MokoStandards version)
- Removed `./CONTRIBUTING.md` (replaced with MokoStandards version)
## [02.01.05] - 2025-09-04
### Changed
- Repaired template.css and colors_standard.css
### Removed
- Removed vmbasic.css
## [02.00.00] - 2025-08-30
### Added - Dark Mode Toggle
- Frontend toggle switch included in template
- JavaScript handles switching between light/dark modes
- Dark mode CSS rules applied across template styles
- Automatic persistence of user choice (via localStorage)
- Admins can override default mode in template settings
### Added - Header Parameters Update
- Added logo parameter support in template settings
- Updated metadata & copyright header
### Added - Expanded TOC (Table of Contents)
- Automatic TOC injection when enabled
- User selects placement via article > options > layout (`toc-left` or `toc-right`)
### Changed
- Cleaned up `index.php` by removing skip-to-content duplicate calls
- Consolidated JavaScript asset loading (ensuring dark-mode script is loaded correctly from external JS file)
- Streamlined CSS for toggle switch, ensuring it inherits Bootstrap/Cassiopeia defaults
- General accessibility refinements in typography and color contrast
- Fixed missing logo param in header output
- Corrected stylesheet inconsistencies between Bootstrap 5 helpers and template overrides
- Patched redundant calls in script includes
## [01.00.00] - 2025-01-01
### Added - Initial Public Release
- Font Awesome 6 integration (later upgraded to FA7)
- Bootstrap 5 helpers (grid, utility classes)
- Automatic Table of Contents (TOC) utility
- Moko Expansions: Google Tag Manager / GA4 hooks
- Built on top of Joomla's default Cassiopeia template
- Minimal core template overrides for maximum upgrade compatibility
---
## Links
- **Full Roadmap**: [MokoOnyx Roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap)
- **Repository**: [GitHub](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
## Version Format
This project uses semantic versioning: `MAJOR.MINOR.PATCH`
- **MAJOR**: Incompatible API changes or major overhauls
- **MINOR**: New features, backwards-compatible
- **PATCH**: Bug fixes, backwards-compatible

98
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,98 @@
<!--
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template
INGROUP: MokoOnyx.Governance
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: CODE_OF_CONDUCT.md
VERSION: 03.09.03
BRIEF: Contributor code of conduct for the MokoOnyx project.
PATH: /CODE_OF_CONDUCT.md
NOTE: This document defines behavioral expectations and enforcement processes.
-->
## Code of Conduct
This Code of Conduct establishes expectations for behavior within the MokoOnyx project community. The objective is to maintain a professional, inclusive, and respectful environment aligned with open source governance best practices.
## Scope
This Code of Conduct applies to all project spaces, including:
* GitHub repositories, issues, pull requests, discussions, and security advisories.
* Project documentation, workflows, and release processes.
* Any communication channels officially associated with the project.
## Our Standards
Participants are expected to:
* Communicate professionally and respectfully.
* Provide constructive feedback focused on technical merit and project objectives.
* Respect differing viewpoints, experience levels, and backgrounds.
* Follow documented contribution, security, and governance policies.
Unacceptable behavior includes:
* Harassment, discrimination, or exclusionary conduct.
* Personal attacks, insults, or inflammatory comments.
* Publishing private information without consent.
* Disruptive behavior that materially interferes with project operations.
## Enforcement Responsibilities
Project maintainers are responsible for:
* Clarifying standards when questions arise.
* Taking appropriate and proportionate corrective action when violations occur.
* Maintaining confidentiality to the extent practical during investigations.
## Reporting
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported through:
* Email: `hello@mokoconsulting.tech` with subject `CODE OF CONDUCT: MokoOnyx`.
Reports should include relevant context, links, screenshots, or other supporting information.
## Enforcement Guidelines
Corrective actions may include, but are not limited to:
* Private warning or request for corrective action.
* Temporary or permanent restriction from project participation.
* Removal of content that violates this Code of Conduct.
Decisions are made based on impact, severity, and pattern of behavior.
## No Retaliation
Retaliation against individuals who report concerns in good faith is not tolerated. Any retaliatory behavior will be treated as a separate violation.
## Jurisdiction
This project is managed from Tennessee, USA. This statement is informational and does not constitute legal advice.
---
## Metadata
* **Document:** CODE_OF_CONDUCT.md
* **Repository:** [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* **Path:** /CODE_OF_CONDUCT.md
* **Owner:** Moko Consulting
* **Version:** 03.06.00
* **Status:** Active
* **Effective Date:** 2025-12-18
* **Last Reviewed:** 2025-12-18
## Revision History
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------------------------------- | --------------- |
| 2025-12-18 | Initial publication of contributor conduct standards and enforcement process. | Moko Consulting |

145
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,145 @@
<!--
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template
INGROUP: MokoOnyx.Governance
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: CONTRIBUTING.md
VERSION: 03.09.03
BRIEF: Contribution guidelines for the MokoOnyx project.
PATH: /CONTRIBUTING.md
NOTE: This document defines contribution workflow, standards, and governance alignment.
-->
## Contributing
This document defines how to contribute to the MokoOnyx project. The goal is to ensure changes are reviewable, auditable, and aligned with project governance and release processes.
## Scope
These guidelines apply to all contributions, including:
* Source code changes
* Documentation updates
* Bug reports and enhancement proposals
## Prerequisites
Contributors are expected to:
* Have a working understanding of Joomla template structure.
* Be familiar with Git and GitHub pull request workflows.
* Review repository governance documents prior to submitting changes.
* Set up the development environment using the provided tools.
### Quick Setup
For first-time contributors:
```bash
# Clone the repository
git clone https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
cd MokoOnyx
```
See [docs/QUICK_START.md](./docs/QUICK_START.md) for detailed setup instructions.
## Development Tools
The repository provides several tools to streamline development:
* **Pre-commit Hooks**: Automatic local validation before commits
## Contribution Workflow
1. Fork the repository.
2. Create a branch from the active development branch.
3. Make focused, minimal changes that address a single concern.
4. Submit a pull request with a clear description of intent and impact.
Direct commits to protected branches are not permitted.
## Branching and Versioning
* Development work occurs on designated development branches.
* Releases are produced from versioned branches following repository standards.
* Contributors should not bump version numbers unless explicitly requested.
## Coding and Formatting Standards
All contributions must:
* Follow Joomla coding standards where applicable.
* Conform to Moko Consulting repository standards for headers, metadata, and file structure.
* Avoid introducing tabs, inconsistent path separators, or non portable assumptions.
Automated checks may reject changes that do not meet these requirements.
## Documentation Standards
Documentation changes must:
* Include required metadata and revision history sections.
* Avoid embedding version numbers in revision history tables.
* Preserve existing structure unless a structural change is explicitly proposed.
## Commit Messages
Commit messages should:
* Be concise and descriptive.
* Focus on what changed and why.
* Avoid referencing internal issue trackers unless required.
## Reporting Issues
Bug reports and enhancement requests should be filed as GitHub issues and include:
* Clear reproduction steps or use cases.
* Expected versus actual behavior.
* Relevant environment details.
Security related issues must follow the process defined in SECURITY.md and must not be reported publicly.
## Review Process
All pull requests are subject to review. Review criteria include:
* Technical correctness
* Alignment with project goals
* Maintainability and clarity
* Risk introduced to release and update processes
Maintainers may request changes prior to approval.
## License
By contributing, you agree that your contributions will be licensed under GPL-3.0-or-later, consistent with the rest of the project.
## Code of Conduct
Participation in this project is governed by the Code of Conduct. Unacceptable behavior may result in contribution restrictions.
---
## Metadata
* **Document:** CONTRIBUTING.md
* **Repository:** [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* **Path:** /CONTRIBUTING.md
* **Owner:** Moko Consulting
* **Version:** 03.06.00
* **Status:** Active
* **Effective Date:** 2025-12-18
* **Last Reviewed:** 2025-12-18
## Revision History
| Date | Change Summary | Author |
| ---------- | ------------------------------------------------------------------------- | --------------- |
| 2025-12-18 | Initial publication of contribution guidelines and workflow expectations. | Moko Consulting |

115
GOVERNANCE.md Normal file
View File

@@ -0,0 +1,115 @@
<!--
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template
INGROUP: MokoOnyx.Governance
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: GOVERNANCE.md
VERSION: 03.09.03
BRIEF: Project governance model, roles, and decision processes for MokoOnyx.
PATH: /GOVERNANCE.md
NOTE: This document defines authority, decision making, and escalation paths.
-->
## Governance Overview
This document defines the governance framework for the MokoOnyx project. The objective is to ensure clear ownership, predictable decision making, and accountable stewardship across development, releases, and community interaction.
## Project Ownership
MokoOnyx is owned and maintained by **Moko Consulting**. Final authority for project direction, releases, and policy enforcement resides with the project owner.
## Roles and Responsibilities
### Maintainers
Maintainers are responsible for:
* Setting technical direction and release priorities.
* Reviewing and approving pull requests.
* Managing releases and distribution artifacts.
* Enforcing repository policies, including security and conduct requirements.
### Contributors
Contributors may:
* Submit pull requests and issues.
* Propose enhancements and report defects.
* Participate in technical discussions.
Contributors do not have merge authority unless explicitly granted.
## Decision Making
Decisions are made using a maintainers led model:
* Routine changes are approved through pull request review.
* Material changes affecting architecture, branding, licensing, or release processes require maintainer consensus.
* The project owner retains final decision authority if consensus cannot be reached.
## Change Management
Significant changes should:
* Be documented through issues or pull requests with clear rationale.
* Consider backward compatibility and upgrade impact.
* Include documentation updates when behavior or usage changes.
## Release Authority
Only maintainers may:
* Cut releases and publish artifacts.
* Update version numbers and manifests.
* Publish update metadata or advisories.
Release processes follow documented workflows and automation standards.
## Security Governance
Security issues are governed by the SECURITY.md policy. Maintainers are responsible for confidential handling, coordinated disclosure, and publication of advisories when appropriate.
## Conduct Enforcement
Behavior within the project is governed by CODE_OF_CONDUCT.md. Maintainers are responsible for enforcement actions and escalation handling.
## Conflict Resolution
Conflicts are handled through:
* Direct discussion between involved parties when appropriate.
* Maintainer mediation when necessary.
* Final determination by the project owner if required.
## External Dependencies
The project depends on Joomla core and other third party components. Governance of upstream projects remains outside the scope of this repository, but upstream changes may influence project decisions.
## Jurisdiction
This project is managed from Tennessee, USA. This statement is informational and does not constitute legal advice.
---
## Metadata
* **Document:** GOVERNANCE.md
* **Repository:** [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* **Path:** /GOVERNANCE.md
* **Owner:** Moko Consulting
* **Version:** 03.06.00
* **Status:** Active
* **Effective Date:** 2025-12-18
* **Last Reviewed:** 2025-12-18
## Revision History
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------------------------- | --------------- |
| 2025-12-18 | Initial publication of governance model, roles, and decision processes. | Moko Consulting |

696
LICENSE Normal file
View File

@@ -0,0 +1,696 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License (./LICENSE).
# FILE INFORMATION
DEFGROUP: MokoStandards
INGROUP: MokoStandards.Documentation
REPO: https://github.com/mokoconsulting-tech/MokoStandards/
VERSION: 05.00.00
PATH: ./LICENSE
BRIEF: Project license (GPL-3.0-or-later)
NOTE: Exact text fetched from gnu.org
-->
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

199
Makefile Normal file
View File

@@ -0,0 +1,199 @@
# Makefile for MokoOnyx Joomla Template
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Build and validation powered by MokoStandards Enterprise API
# Install: composer install
# ==============================================================================
# CONFIGURATION
# ==============================================================================
EXTENSION_NAME := mokoonyx
EXTENSION_TYPE := template
EXTENSION_VERSION := $(shell grep -oP 'VERSION:\s*\K[0-9.]+' README.md 2>/dev/null || echo "0.0.0")
SRC_DIR := src
BUILD_DIR := build
DIST_DIR := dist
PHP := php
COMPOSER := composer
MOKO := vendor/bin/moko
# Colors
COLOR_RESET := \033[0m
COLOR_GREEN := \033[32m
COLOR_YELLOW := \033[33m
COLOR_BLUE := \033[34m
COLOR_RED := \033[31m
# ==============================================================================
# TARGETS
# ==============================================================================
.PHONY: help
help: ## Show this help message
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
@echo "$(COLOR_BLUE)║ MokoOnyx Template Build ║$(COLOR_RESET)"
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
@echo ""
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
@echo "Powered by: MokoStandards Enterprise API"
@echo ""
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
@echo ""
# ── Dependencies ──────────────────────────────────────────────────────────────
.PHONY: install-deps
install-deps: ## Install Composer dependencies (includes MokoStandards API)
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
@$(COMPOSER) install
@echo "$(COLOR_GREEN)✓ Dependencies installed$(COLOR_RESET)"
.PHONY: update-deps
update-deps: ## Update Composer dependencies
@echo "$(COLOR_BLUE)Updating dependencies...$(COLOR_RESET)"
@$(COMPOSER) update
@echo "$(COLOR_GREEN)✓ Dependencies updated$(COLOR_RESET)"
# ── Validation (MokoStandards API) ────────────────────────────────────────────
.PHONY: check-moko
check-moko:
@if [ ! -f "$(MOKO)" ]; then \
echo "$(COLOR_RED)✗ MokoStandards CLI not found. Run: make install-deps$(COLOR_RESET)"; \
exit 1; \
fi
.PHONY: lint
lint: check-moko ## PHP syntax check via MokoStandards
@echo "$(COLOR_BLUE)Running PHP syntax check...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:syntax -- --path .
@echo "$(COLOR_GREEN)✓ PHP syntax OK$(COLOR_RESET)"
.PHONY: check-joomla
check-joomla: check-moko ## Validate Joomla manifest via MokoStandards
@echo "$(COLOR_BLUE)Validating Joomla manifest...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:joomla -- --path .
@echo "$(COLOR_GREEN)✓ Joomla manifest valid$(COLOR_RESET)"
.PHONY: check-version
check-version: check-moko ## Verify version consistency across files
@echo "$(COLOR_BLUE)Checking version consistency...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:version -- --path .
@echo "$(COLOR_GREEN)✓ Versions consistent$(COLOR_RESET)"
.PHONY: check-headers
check-headers: check-moko ## Check license headers on source files
@echo "$(COLOR_BLUE)Checking license headers...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:headers -- --path .
@echo "$(COLOR_GREEN)✓ Headers OK$(COLOR_RESET)"
.PHONY: check-secrets
check-secrets: check-moko ## Scan for leaked credentials
@echo "$(COLOR_BLUE)Scanning for secrets...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:secrets -- --path .
@echo "$(COLOR_GREEN)✓ No secrets found$(COLOR_RESET)"
.PHONY: check-xml
check-xml: check-moko ## Validate XML files are well-formed
@echo "$(COLOR_BLUE)Checking XML files...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:xml -- --path .
@echo "$(COLOR_GREEN)✓ XML well-formed$(COLOR_RESET)"
.PHONY: validate
validate: lint check-joomla check-version check-xml check-headers check-secrets ## Run all MokoStandards validation checks
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
.PHONY: health
health: check-moko ## Full repository health check via MokoStandards
@echo "$(COLOR_BLUE)Running full health check...$(COLOR_RESET)"
@$(PHP) $(MOKO) health -- --path .
# ── Build ─────────────────────────────────────────────────────────────────────
.PHONY: clean
clean: ## Clean build artifacts
@echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)"
@rm -rf $(BUILD_DIR) $(DIST_DIR)
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
.PHONY: build
build: clean ## Build template installable ZIP from src/
@echo "$(COLOR_BLUE)Building $(EXTENSION_NAME) v$(EXTENSION_VERSION)...$(COLOR_RESET)"
@mkdir -p $(BUILD_DIR)/package $(DIST_DIR)
@cp -r $(SRC_DIR)/* $(BUILD_DIR)/package/
@cd $(BUILD_DIR)/package && \
if command -v zip >/dev/null 2>&1; then \
zip -r "../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" .; \
elif command -v pwsh >/dev/null 2>&1; then \
pwsh -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip' -Force"; \
elif command -v powershell >/dev/null 2>&1; then \
powershell -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip' -Force"; \
else \
echo "$(COLOR_RED)✗ No zip tool found (zip, pwsh, powershell)$(COLOR_RESET)"; \
exit 1; \
fi
@echo "$(COLOR_GREEN)✓ Package: $(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
.PHONY: build-beta
build-beta: clean ## Build beta release ZIP
@echo "$(COLOR_BLUE)Building $(EXTENSION_NAME) v$(EXTENSION_VERSION)-beta...$(COLOR_RESET)"
@mkdir -p $(BUILD_DIR)/package $(DIST_DIR)
@cp -r $(SRC_DIR)/* $(BUILD_DIR)/package/
@cd $(BUILD_DIR)/package && \
if command -v zip >/dev/null 2>&1; then \
zip -r "../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip" .; \
elif command -v pwsh >/dev/null 2>&1; then \
pwsh -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip' -Force"; \
elif command -v powershell >/dev/null 2>&1; then \
powershell -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip' -Force"; \
else \
echo "$(COLOR_RED)✗ No zip tool found$(COLOR_RESET)"; \
exit 1; \
fi
@echo "$(COLOR_GREEN)✓ Package: $(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip$(COLOR_RESET)"
.PHONY: checksum
checksum: ## Generate SHA-256 checksums for dist packages
@echo "$(COLOR_BLUE)Generating checksums...$(COLOR_RESET)"
@for f in $(DIST_DIR)/*.zip; do \
sha256sum "$$f" | tee "$${f}.sha256"; \
done
@echo "$(COLOR_GREEN)✓ Checksums generated$(COLOR_RESET)"
# ── Release ───────────────────────────────────────────────────────────────────
.PHONY: release
release: validate build checksum ## Full release pipeline (validate + build + checksum)
@echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
@echo ""
@echo "$(COLOR_BLUE)Next steps:$(COLOR_RESET)"
@echo " 1. Tag: git tag $(EXTENSION_VERSION)"
@echo " 2. Push: git push origin --tags"
@echo " 3. Create Gitea release and attach $(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip"
@echo ""
# ── Info ──────────────────────────────────────────────────────────────────────
.PHONY: version
version: ## Display version and extension info
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
@echo " Name: $(EXTENSION_NAME)"
@echo " Type: $(EXTENSION_TYPE)"
@echo " Version: $(EXTENSION_VERSION)"
.PHONY: security-check
security-check: ## Run Composer security audit
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
@$(COMPOSER) audit
@echo "$(COLOR_GREEN)✓ Security check complete$(COLOR_RESET)"
.PHONY: all
all: install-deps validate build checksum ## Full pipeline: deps → validate → build → checksum
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
.DEFAULT_GOAL := help

521
README.md
View File

@@ -1,3 +1,520 @@
# MokoOnyx
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
MokoOnyx <20> Joomla site template (successor to MokoCassiopeia)
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: ./README.md
VERSION: 01.00.00
BRIEF: Documentation for MokoOnyx template
-->
# MokoOnyx → MokoOnyx
> **This template is being renamed to MokoOnyx.** Version 01.00.00 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoOnyx can be safely uninstalled.
**A Modern, Lightweight Joomla Template Based on Cassiopeia**
[![Version](https://img.shields.io/badge/version-03.09.07-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/v03)
[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE)
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net)
MokoOnyx is a modern, lightweight enhancement layer built on top of Joomla's Cassiopeia template. It adds **Font Awesome 7**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, advanced **Dark Mode** theming, and optional integrations for **Google Tag Manager** and **Google Analytics (GA4)**—all while maintaining minimal core template overrides for maximum upgrade compatibility.
---
## 📑 Table of Contents
- [Features](#-features)
- [Requirements](#-requirements)
- [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Configuration](#-configuration)
- [Theme System](#-theme-system)
- [Development](#-development)
- [Documentation](#-documentation)
- [Changelog](#-changelog)
- [Support](#-support)
- [Contributing](#-contributing)
- [Included Libraries](#-included-libraries)
- [License](#-license)
---
## ✨ Features
### Core Enhancements
- **Built on Cassiopeia**: Extends Joomla's default template with minimal overrides
- **Font Awesome 7**: Fully integrated into Joomla's asset manager with 2,000+ icons
- **Bootstrap 5**: Extended utility classes and responsive grid system
- **No Template Overrides**: Clean installation that inherits all Cassiopeia defaults
- **Upgrade-Friendly**: Minimal modifications ensure smooth Joomla updates
### Advanced Theming
- **Dark Mode Support**: Built-in light/dark mode toggle with system preference detection
- **Color Palettes**: Standard, Alternative, and Custom color schemes
- **Theme Persistence**: User preferences saved via localStorage
- **Theme Control Options**: Switch, radio buttons, or hidden controls
- **Auto Dark Mode**: Optional automatic dark mode based on time/system settings
- **Meta Tags**: Automatic color-scheme and theme-color meta tags
### Developer Features
- **Custom Code Injection**: Add custom HTML to `<head>` start/end
- **Drawer Sidebars**: Configurable left/right drawer positions with custom icons
- **Font Options**: Local and web fonts (Roboto, Fira Sans, Noto Sans)
- **Sticky Header**: Optional sticky navigation
- **Back to Top**: Floating back-to-top button
### Analytics & Tracking
- **Google Tag Manager**: Optional GTM integration with container ID configuration
- **Google Analytics**: Optional GA4 integration with measurement ID
- **Privacy-Friendly**: All tracking features are optional and easily disabled
### Content Features
- **Table of Contents**: Automatic TOC generation for long articles
- Placement options: `toc-left` or `toc-right` layouts
- Automatic heading extraction and navigation
- Responsive sidebar positioning
---
## 📋 Requirements
- **Joomla**: 4.4.x or 5.x
- **PHP**: 8.0 or higher
- **Database**: MySQL 5.7+ / MariaDB 10.2+ / PostgreSQL 11+
- **Browser Support**: Modern browsers (Chrome, Firefox, Safari, Edge)
---
## 📦 Installation
**Note**: MokoOnyx is a **standalone Joomla template extension** (not bundled as a package). Install it directly via Joomla's Extension Manager.
### Via Joomla Extension Manager
1. Download the latest `mokoonyx-{version}.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases)
2. In Joomla admin, navigate to **System → Install → Extensions**
3. Upload the ZIP file and click **Upload & Install**
4. Navigate to **System → Site Templates**
5. Set **MokoOnyx** as your default template
### Via Git (Development)
```bash
git clone https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
cd MokoOnyx
```
See [Development Guide](./docs/JOOMLA_DEVELOPMENT.md) for development setup.
---
## 🚀 Quick Start
### 1. Install the Template
Install `mokoonyx.zip` via Joomla's Extension Manager.
### 2. Set as Default
Navigate to **System → Site Templates** and set **MokoOnyx** as default.
### 3. Configure Template Options
Go to **System → Site Templates → MokoOnyx** to configure:
- **Branding**: Upload logo, set site title/description
- **Theme**: Configure color schemes and dark mode
- **Layout**: Set container type (static/fluid), sticky header
- **Analytics**: Add GTM/GA4 tracking codes (optional)
- **Custom Code**: Inject custom HTML/CSS/JS
### 4. Test Dark Mode
The template includes a dark mode toggle. Test it by:
- Using the floating theme toggle button (bottom-right by default)
- Checking theme persistence across page loads
- Verifying system preference detection
---
## Usage
Once installed and set as the default site template, MokoOnyx works out of the box with Joomla's standard content and module system. Key usage points:
- **Template Options** — Configure via **System → Site Templates → MokoOnyx** (theme colours, layout, analytics, favicon, drawers)
- **Custom Colour Schemes** — Copy `templates/mokoonyx/templates/light.custom.css` or `dark.custom.css` to `media/templates/site/mokoonyx/css/theme/` and select "Custom" in the Theme tab
- **Custom CSS/JS** — Create `media/templates/site/mokoonyx/css/user.css` or `js/user.js` for site-specific overrides that survive template updates
- **Module Overrides** — The template includes overrides for common Joomla modules with consistent title rendering, Bootstrap 5 styling, and Font Awesome 7 icons
- **Dark Mode** — Enabled by default with a floating toggle button; respects system preference and persists via localStorage
See [Configuration](#-configuration) below for detailed parameter reference.
---
## ⚙️ Configuration
### Global Parameters
Access template configuration via **System → Site Templates → MokoOnyx**.
#### Theme Tab
**General Settings:**
- **Theme Enabled**: Enable/disable theme system
- **Theme Control Type**: Switch (Light↔Dark), Radios (Light/Dark/System), or None
- **Default Choice**: System, Light, or Dark
- **Auto Dark Mode**: Automatic dark mode based on time
- **Meta Tags**: Enable color-scheme and theme-color meta tags
- **Bridge Bootstrap ARIA**: Sync theme with Bootstrap's data-bs-theme
**Variables & Palettes:**
- **Light Mode Palette**: Standard, Alternative, or Custom
- **Dark Mode Palette**: Standard, Alternative, or Custom
**Typography:**
- **Font Scheme**: Local (Roboto) or Web fonts (Fira Sans, Roboto+Noto Sans)
**Branding & Icons:**
- **Brand**: Enable/disable site branding
- **Logo File**: Upload custom logo (no default logo included)
- **Site Title**: Custom site title
- **Site Description**: Tagline/description
- **Font Awesome Kit**: Optional FA Pro kit code
**Header & Navigation:**
- **Sticky Header**: Enable fixed header on scroll
- **Back to Top**: Enable floating back-to-top button
**Theme Toggle UI:**
- **FAB Enabled**: Enable floating action button toggle
- **FAB Position**: Bottom-right, Bottom-left, Top-right, or Top-left
#### Advanced Tab
- **Layout**: Static or Fluid container
#### Google Tab
- **Google Tag Manager**: Enable and configure GTM container ID
- **Google Analytics**: Enable and configure GA4 measurement ID
#### Custom Code Tab
- **Custom Head Start**: HTML injected at start of `<head>`
- **Custom Head End**: HTML injected at end of `<head>`
#### Drawers Tab
- **Left Drawer Icon**: Font Awesome icon class (e.g., `fa-solid fa-chevron-right`)
- **Right Drawer Icon**: Font Awesome icon class (e.g., `fa-solid fa-chevron-left`)
### Custom Theme Palettes
MokoOnyx supports custom theme schemes:
1. **Copy template files** from `/templates/` directory:
- `light.custom.css``media/templates/site/mokoonyx/css/theme/light.custom.css`
- `dark.custom.css``media/templates/site/mokoonyx/css/theme/dark.custom.css`
2. **Customize** the CSS variables to match your brand colors
3. **Enable in Joomla**: System → Site Templates → MokoOnyx → Theme tab → Set palette to "Custom"
4. **Save** and view your site with custom colors
**Note:** Custom color files are excluded from version control (`.gitignore`) to prevent fork-specific customizations from being committed.
**Quick Example:**
```css
:root[data-bs-theme="light"] {
--color-primary: #1e40af;
--color-link: #2563eb;
--color-hover: #1d4ed8;
--body-color: #1f2937;
--body-bg: #ffffff;
}
```
**Complete Reference:** See [CSS Variables Documentation](./docs/CSS_VARIABLES.md) for all available variables and detailed usage examples.
### Table of Contents
Enable automatic TOC for articles:
1. Edit an article in Joomla admin
2. Navigate to **Options → Layout**
3. Select **toc-left** or **toc-right**
4. Save the article
The TOC will automatically generate from article headings (H2, H3, etc.) and appear as a sidebar.
---
## 🎨 Theme System
### Dark Mode Features
- **Automatic Detection**: Respects user's system preferences
- **Manual Toggle**: Floating button or radio controls
- **Persistence**: Saves preference in localStorage
- **Smooth Transitions**: Animated theme switching
- **Comprehensive Support**: All components themed for dark mode
### Theme Control Types
1. **Switch**: Simple light/dark toggle button
2. **Radios**: Three options - Light, Dark, System
3. **None**: No visible control (respects system only)
### Meta Tags
When enabled, the template adds:
```html
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#1e3a8a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
```
---
## 🛠 Development
### For Contributors
**New to the project?** See [Quick Start Guide](./docs/QUICK_START.md) for a 5-minute setup.
### Development Resources
- **[Quick Start Guide](./docs/QUICK_START.md)** - Setup and first steps
- **[Joomla Development Guide](./docs/JOOMLA_DEVELOPMENT.md)** - Testing, quality checks, deployment
- **[Workflow Guide](./docs/WORKFLOW_GUIDE.md)** - Git workflow and branching
- **[Contributing Guide](./CONTRIBUTING.md)** - Contribution guidelines
- **[Roadmap](./docs/ROADMAP.md)** - Feature roadmap and planning
### Development Tools
- **Pre-commit Hooks**: Automatic validation before commits
- **PHP CodeSniffer**: Code style validation (Joomla standards)
- **PHPStan**: Static analysis for PHP code
- **Codeception**: Testing framework
### Quick Development Setup
```bash
# Clone repository
git clone https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
cd MokoOnyx
# Install development dependencies (if using Composer)
composer install --dev
# Run code quality checks
make validate # or manual commands
```
### Building Template Package
See [Joomla Development Guide](./docs/JOOMLA_DEVELOPMENT.md) for packaging instructions.
---
## 📚 Documentation
### User Documentation
- **[README](./README.md)** - This file (overview and features)
- **[CHANGELOG](./CHANGELOG.md)** - Version history and changes
- **[Roadmap](./docs/ROADMAP.md)** - Planned features and timeline
### Developer Documentation
- **[Quick Start](./docs/QUICK_START.md)** - 5-minute developer setup
- **[Development Guide](./docs/JOOMLA_DEVELOPMENT.md)** - Comprehensive development guide
- **[Workflow Guide](./docs/WORKFLOW_GUIDE.md)** - Git workflow and processes
- **[CSS Variables Reference](./docs/CSS_VARIABLES.md)** - Complete CSS customization guide
- **[Documentation Index](./docs/README.md)** - All documentation links
### Governance
- **[Contributing](./CONTRIBUTING.md)** - How to contribute
- **[Code of Conduct](./CODE_OF_CONDUCT.md)** - Community standards
- **[Governance](./GOVERNANCE.md)** - Project governance model
- **[Security Policy](./SECURITY.md)** - Security reporting and procedures
---
## 📖 Changelog
See the [CHANGELOG.md](./CHANGELOG.md) for detailed version history.
### Recent Releases
- **[03.06.03]** (2026-01-30) - README updates and TOC color variable improvements
- **[03.06.02]** (2026-01-30) - Complete rebrand to MokoOnyx, removed all overrides
- **[03.06.00]** (2026-01-28) - Version standardization
- **[03.05.01]** (2026-01-09) - Security and compliance improvements
- **[02.00.00]** (2025-08-30) - Dark mode toggle and improved theming
---
## 💬 Support
### Getting Help
- **Documentation**: Check this README and [docs folder](./docs/)
- **Issues**: Report bugs via [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Discussions**: Ask questions in [GitHub Discussions](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/discussions)
- **Roadmap**: View planned features in [Roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap)
### Reporting Bugs
Please include:
- Joomla version
- PHP version
- Template version
- Steps to reproduce
- Expected vs actual behavior
- Screenshots (if applicable)
### Security Issues
**Do not** report security vulnerabilities via public issues. See [SECURITY.md](./SECURITY.md) for reporting procedures.
---
## 🤝 Contributing
We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
### How to Contribute
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Run quality checks
5. Commit your changes (`git commit -m 'Add amazing feature'`)
6. Push to the branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request
### Development Workflow
See [Workflow Guide](./docs/WORKFLOW_GUIDE.md) for detailed Git workflow.
### Customizations
For template customizations, use Joomla's built-in template settings (System → Site Templates → MokoOnyx → Custom Code tab) for HTML/CSS/JS customizations.
---
## 📦 Included Libraries
MokoOnyx includes the following third-party libraries to provide enhanced functionality:
### Bootstrap TOC
- **Version**: 1.0.1
- **Author**: Aidan Feldman
- **License**: MIT License
- **Source**: [GitHub Repository](https://github.com/afeld/bootstrap-toc)
- **Release**: [v1.0.1 Release](https://github.com/afeld/bootstrap-toc/releases/tag/v1.0.1)
- **Purpose**: Automatically generates a table of contents from article headings with scrollspy support
- **Location**: `src/media/vendor/bootstrap-toc/`
- **Integration**: Registered in `joomla.asset.json` as `vendor.bootstrap-toc` (CSS) and `vendor.bootstrap-toc.js` (JavaScript)
- **Usage**: Activated when using `toc-left` or `toc-right` article layouts
- **Features**:
- Automatic TOC generation from H1-H6 headings
- Hierarchical nested navigation
- Active state highlighting with scrollspy
- Responsive design (collapses on mobile)
- Smooth scrolling to sections
- Automatic unique ID generation for headings
- **Customizations**: CSS adapted to use Cassiopeia CSS variables for theme compatibility
### Font Awesome 7 Free
- **Version**: 7.0 (Free)
- **License**: Font Awesome Free License
- **Source**: [Font Awesome](https://fontawesome.com)
- **Purpose**: Provides 2,000+ vector icons for interface elements
- **Location**: `src/media/vendor/fa7free/`
- **Integration**: Fully integrated into Joomla's asset manager
- **Styles Available**: Solid, Regular, Brands
### Bootstrap 5
- **Version**: 5.x (via Joomla)
- **License**: MIT License
- **Source**: [Bootstrap](https://getbootstrap.com)
- **Purpose**: Provides responsive grid system and utility classes
- **Integration**: Inherited from Joomla's Cassiopeia template, extended with additional helpers
- **Components Used**: Grid, utilities, modal, dropdown, collapse, offcanvas, tooltip, popover, scrollspy
### Integration Method
All third-party libraries are:
- ✅ Properly licensed and attributed
- ✅ Registered in Joomla's Web Asset Manager (`joomla.asset.json`)
- ✅ Loaded on-demand to optimize performance
- ✅ Versioned and documented for maintenance
- ✅ Compatible with Joomla 4.4.x and 5.x
---
## 📄 License
This project is licensed under the **GNU General Public License v3.0** - see the [LICENSE](./LICENSE) file for details.
### Third-Party Licenses
- **Joomla! CMS**: GPL-2.0-or-later
- **Cassiopeia Template**: GPL-2.0-or-later (Joomla Project)
- **Font Awesome 7 Free**: Font Awesome Free License
- **Bootstrap 5**: MIT License
- **Bootstrap TOC**: MIT License (A. Feld)
All third-party libraries and assets remain the property of their respective authors and are credited in source files.
---
## 🔗 Links
- **Repository**: [GitHub](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Discussions**: [GitHub Discussions](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/discussions)
- **Roadmap**: [Full Roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap)
- **Moko Consulting**: [Website](https://mokoconsulting.tech)
---
## 📊 Metadata
- **Maintainer**: Moko Consulting Engineering
- **Author**: Jonathan Miller (@jmiller-moko)
- **Repository**: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
- **License**: GPL-3.0-or-later
- **Classification**: Public Open Source Standards
## 📝 Revision History
| Date | Version | Change Summary | Author |
| ---------- | -------- | ------------------------------------------------------------------------- | ------------------------------- |
| 2026-01-30 | 03.06.03 | Updated README title, fixed custom color variables instructions, improved TOC color scheme integration | Copilot Agent |
| 2026-01-30 | 03.06.02 | Regenerated README with comprehensive documentation and updated structure | Copilot Agent |
| 2026-01-30 | 03.06.02 | Complete rebrand to MokoOnyx, removed overrides | Copilot Agent |
| 2026-01-05 | 03.00.00 | Initial publication of template documentation and feature overview | Moko Consulting |
| 2026-01-05 | 03.00.00 | Fixed malformed markdown table formatting in revision history | Jonathan Miller (@jmiller-moko) |
---
**Made with ❤️ by [Moko Consulting](https://mokoconsulting.tech)**

185
SECURITY.md Normal file
View File

@@ -0,0 +1,185 @@
<!--
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template
INGROUP: MokoOnyx.Governance
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: SECURITY.md
VERSION: 03.09.03
BRIEF: Security policy and vulnerability reporting process for MokoOnyx.
PATH: /SECURITY.md
NOTE: This policy is process oriented and does not replace secure engineering practices.
-->
## Security Policy
This document defines how MokoOnyx handles vulnerability intake, triage, remediation, and disclosure. The objective is to reduce risk, protect downstream users, and preserve operational continuity with a verifiable audit trail.
## Scope
This policy applies to:
* Repository source code, workflows, scripts, and build artifacts.
* Release packaging (ZIP outputs) generated from the repository.
* Configuration and metadata used for distribution (for example manifests and update metadata).
Out of scope:
* Vulnerabilities in upstream Joomla core, third party extensions, or external infrastructure not controlled by this repository.
* Issues that require physical access to a host, compromised administrator credentials, or a compromised hosting provider, unless the repository materially increases impact.
## Supported Versions
Security fixes are prioritized for:
* The latest released version.
* The current development line when it is actively used for release engineering.
Backports may be provided based on impact, deployment footprint, and engineering capacity.
## Reporting a Vulnerability
Use one of the following channels:
* GitHub Security Advisories (preferred): use the repository security tab to submit a private report.
* Email: send details to `hello@mokoconsulting.tech` with subject `SECURITY: MokoOnyx vulnerability report`.
Do not file a public GitHub issue for suspected security vulnerabilities.
### What to include
Provide enough detail to reproduce and triage:
* A clear description of the vulnerability and expected impact.
* A minimal proof of concept or reproduction steps.
* Affected versions, configuration assumptions, and environment details.
* Any proposed mitigation or patch.
* Your preferred contact details for follow up.
## Triage and Response Targets
The project operates with response targets aligned to practical delivery realities:
* **Acknowledgement:** within 3 business days.
* **Initial triage:** within 10 business days.
* **Fix plan:** communicated once severity is confirmed.
These targets are not guarantees. Complex issues, supply chain considerations, and coordination with upstream vendors may extend timelines.
## Severity Assessment
Issues are triaged based on business impact and technical exploitability, including:
* Remote exploitability and required privileges.
* Data confidentiality, integrity, and availability impact.
* Likelihood of exploitation in typical Joomla deployments.
* Exposure surface (public endpoints, administrator area, installation flows, and update mechanisms).
When appropriate, industry standard scoring such as CVSS may be used for internal prioritization.
## Coordinated Disclosure
The project follows coordinated vulnerability disclosure:
* Reports are treated as confidential until remediation is available.
* A public advisory may be published once a fix is released.
* A reasonable embargo period is expected to enable patch distribution.
If you believe disclosure is time sensitive due to active exploitation, include that assessment and any supporting indicators.
## Security Updates and Advisories
Security updates are distributed through:
* GitHub releases for the repository.
* GitHub Security Advisories when applicable.
Advisories may include:
* Affected versions and fixed versions.
* Mitigations and workarounds when a fix is not immediately available.
* Upgrade guidance.
## Dependencies and Supply Chain Controls
The project aims to manage supply chain risk through:
* Pinning and review of workflow dependencies where feasible.
* Minimizing privileged GitHub token permissions.
* Validating build inputs prior to packaging releases.
If you identify a supply chain issue (for example compromised action, dependency confusion, or malicious upstream artifact), report it as a vulnerability.
## Secure Development and CI Expectations
Security posture is reinforced through operational controls:
* CI validation for packaging inputs and manifest integrity.
* Consistent path normalization and whitespace hygiene checks where required for release correctness.
* Least privilege for GitHub Actions permissions.
### Template Security Features
**Custom Head Content Injection**
The template provides Custom Head Code fields (`custom_head_start` and `custom_head_end`) that allow administrators to inject custom HTML, CSS, and JavaScript code. This is an intentional feature for:
* Adding analytics scripts (Google Analytics, Google Tag Manager)
* Custom meta tags
* Third-party integrations
* Custom styling
**Security Considerations:**
* These fields use `filter="raw"` to allow HTML/JS injection
* **Access is restricted to Joomla administrators only** via template configuration
* This is not an XSS vulnerability as it requires administrator privileges
* Administrators should only add trusted code from verified sources
* Regular security audits should review custom head content
This policy does not guarantee that all vulnerabilities will be prevented. It defines how risk is managed when issues are discovered.
## Safe Harbor
The project supports good faith security research. When you:
* Avoid privacy violations, data destruction, and service disruption.
* Limit testing to systems you own or have explicit permission to test.
* Provide a reasonable window for coordinated disclosure.
Then the project will treat your report as a constructive security contribution.
Jurisdiction note: this repository is managed from Tennessee, USA. This note is informational only and does not constitute legal advice.
## Public Communications
Only maintainers will publish security advisories or public statements for confirmed vulnerabilities. Public communication will focus on actionable remediation and operational risk reduction.
## Acknowledgements
If you want credit, include the name or handle to list in an advisory. If you prefer anonymity, state that explicitly.
---
## Metadata
* **Document:** SECURITY.md
* **Repository:** [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* **Path:** /SECURITY.md
* **Owner:** Moko Consulting
* **Version:** 03.06.00
* **Status:** Active
* **Effective Date:** 2025-12-18
* **Last Reviewed:** 2025-12-18
## Revision History
| Date | Change Summary | Author |
| ---------- | ------------------------------------------------------------------------------------------------ | --------------- |
| 2026-01-30 | Added Template Security Features section documenting custom head content injection controls. | Copilot Agent |
| 2025-12-18 | Initial publication of security policy, intake channels, triage targets, and disclosure process. | Moko Consulting |

34
codeception.yml Normal file
View File

@@ -0,0 +1,34 @@
namespace: Tests
paths:
tests: tests
output: tests/_output
data: tests/_data
support: tests/_support
envs: tests/_envs
settings:
shuffle: false
lint: true
colors: true
memory_limit: 1024M
coverage:
enabled: true
include:
- src/*
exclude:
- src/vendor/*
- src/media/*
- src/language/*
extensions:
enabled:
- Codeception\Extension\RunFailed
params:
- env
modules:
config:
Db:
dsn: 'mysql:host=localhost;dbname=joomla_test'
user: 'root'
password: ''
dump: tests/_data/dump.sql
populate: true
cleanup: true

30
composer.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "mokoconsulting/mokoonyx",
"description": "MokoOnyx \u00e2\u20ac\u201d Joomla site template based on Cassiopeia",
"type": "joomla-template",
"license": "GPL-3.0-or-later",
"authors": [
{
"name": "Jonathan Miller",
"email": "hello@mokoconsulting.tech"
}
],
"require": {
"php": ">=8.1",
"ext-zip": "*"
},
"require-dev": {
"mokoconsulting-tech/enterprise": "^4.0"
},
"config": {
"sort-packages": true,
"optimize-autoloader": true,
"preferred-install": "dist"
},
"repositories": [
{
"type": "composer",
"url": "https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer"
}
]
}

1460
docs/CSS_VARIABLES.md Normal file

File diff suppressed because it is too large Load Diff

376
docs/JOOMLA_DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,376 @@
# Joomla Development Workflows and Scripts
This document describes the Joomla-aware development workflows and scripts available in this repository.
## Table of Contents
- [Overview](#overview)
- [Requirements](#requirements)
- [Scripts](#scripts)
- [GitHub Actions Workflows](#github-actions-workflows)
- [Testing](#testing)
- [Code Quality](#code-quality)
- [Deployment](#deployment)
## Overview
This repository includes comprehensive Joomla development workflows and scripts for:
1. **Extension Packaging** - Create distributable ZIP packages
2. **Joomla Testing** - Automated testing with multiple Joomla versions
3. **Code Quality** - PHPStan, PHP_CodeSniffer, and compatibility checks
4. **Deployment** - Staging and production deployment workflows
## Requirements
### Local Development
- PHP 8.0 or higher
- Composer (for PHPStan and PHP_CodeSniffer)
- Node.js 18+ (for some workflows)
- MySQL/MariaDB (for Joomla testing)
### CI/CD (GitHub Actions)
All requirements are automatically installed in CI/CD pipelines.
## Scripts
### Extension Packaging
Package the Joomla template as a distributable ZIP file:
```bash
make package
```
This creates a ZIP file in the `dist` directory with all necessary template files, excluding development files.
## GitHub Actions Workflows
### 1. PHP Code Quality (`php_quality.yml`)
Runs on every push and pull request to main branches.
**Jobs:**
- **PHP_CodeSniffer** - Checks code style and standards
- **PHPStan** - Static analysis at level 5
- **PHP Compatibility** - Ensures PHP 8.0+ compatibility
**Matrix Testing:**
- PHP versions: 8.0, 8.1, 8.2, 8.3
**Trigger:**
```bash
# Automatically runs on push/PR
git push origin dev/3.5.0
```
### 2. Joomla Testing (`joomla_testing.yml`)
Tests template with multiple Joomla and PHP versions.
**Jobs:**
- **Joomla Setup** - Installs Joomla CMS
- **Template Installation** - Installs template into Joomla
- **Validation** - Validates template functionality
- **Codeception** - Runs test framework
**Matrix Testing:**
- PHP versions: 8.0, 8.1, 8.2, 8.3
- Joomla versions: 4.4 (LTS), 5.0, 5.1
- MySQL version: 8.0
**Example:**
```bash
# Automatically runs on push to main branches
git push origin main
```
### 3. Deploy to Staging (`deploy_staging.yml`)
Manual deployment to staging/development environments.
**Parameters:**
- `environment`: Target environment (staging, development, preview)
- `version`: Version to deploy (optional, defaults to latest)
**Usage:**
1. Go to Actions → Deploy to Staging
2. Click "Run workflow"
3. Select environment and version
4. Click "Run workflow"
**Required Secrets:**
For staging deployment, configure these repository secrets:
- `STAGING_HOST` - SFTP server hostname
- `STAGING_USER` - SFTP username
- `STAGING_KEY` - SSH private key (recommended) or use `STAGING_PASSWORD`
- `STAGING_PATH` - Remote path for deployment
- `STAGING_PORT` - SSH port (optional, default: 22)
## Testing
### Codeception Framework
The repository is configured with Codeception for acceptance and unit testing.
#### Running Tests Locally
1. Install Codeception:
```bash
composer global require "codeception/codeception" --with-all-dependencies
```
2. Run tests:
```bash
# Run all tests
codecept run
# Run acceptance tests only
codecept run acceptance
# Run unit tests only
codecept run unit
# Run with verbose output
codecept run --debug
```
#### Test Structure
```
tests/
├── _data/ # Test data and fixtures
├── _output/ # Test reports and screenshots
├── _support/ # Helper classes
├── acceptance/ # Acceptance tests
│ └── TemplateInstallationCest.php
├── unit/ # Unit tests
│ └── TemplateConfigurationTest.php
├── acceptance.suite.yml
└── unit.suite.yml
```
#### Writing Tests
**Unit Test Example:**
```php
<?php
namespace Tests\Unit;
use Codeception\Test\Unit;
class MyTemplateTest extends Unit
{
public function testSomething()
{
$this->assertTrue(true);
}
}
```
**Acceptance Test Example:**
```php
<?php
namespace Tests\Acceptance;
use Tests\Support\AcceptanceTester;
class MyAcceptanceCest
{
public function testPageLoad(AcceptanceTester $I)
{
$I->amOnPage('/');
$I->see('Welcome');
}
}
```
## Code Quality
### PHPStan
Static analysis configuration in `phpstan.neon`:
```bash
# Run PHPStan locally
phpstan analyse --configuration=phpstan.neon
```
**Configuration:**
- Analysis level: 5
- Target paths: `src/`
- PHP version: 8.0+
### PHP_CodeSniffer
Coding standards configuration in `phpcs.xml`:
```bash
# Check code style
phpcs --standard=phpcs.xml
# Fix auto-fixable issues
phpcbf --standard=phpcs.xml
```
**Standards:**
- PSR-12 as base
- PHP 8.0+ compatibility checks
- Joomla coding conventions (when available)
### Running Quality Checks Locally
1. Install tools:
```bash
composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
composer global require "phpstan/phpstan:^1.0" --with-all-dependencies
composer global require "phpcompatibility/php-compatibility:^9.0" --with-all-dependencies
```
2. Configure PHPCompatibility:
```bash
phpcs --config-set installed_paths ~/.composer/vendor/phpcompatibility/php-compatibility
```
3. Run checks:
```bash
# PHP syntax check
make validate-required
# CodeSniffer
phpcs --standard=phpcs.xml src/
# PHPStan
phpstan analyse --configuration=phpstan.neon
# PHP Compatibility
phpcs --standard=PHPCompatibility --runtime-set testVersion 8.0- src/
```
## Deployment
### Manual Deployment
Use the package script to create a distribution:
```bash
# Create package
make package
# Upload to server
scp dist/moko-cassiopeia-3.5.0-template.zip user@server:/path/to/joomla/
```
### Automated Deployment
Use the GitHub Actions workflow:
1. **Staging Deployment:**
- Go to Actions → Deploy to Staging
- Select "staging" environment
- Click "Run workflow"
2. **Development Testing:**
- Select "development" environment
- Useful for quick testing without affecting staging
3. **Preview Deployment:**
- Select "preview" environment
- For showcasing features before staging
### Post-Deployment Steps
After deployment to Joomla:
1. Log in to Joomla administrator
2. Go to System → Extensions → Discover
3. Click "Discover" to find the template
4. Click "Install" to complete installation
5. Go to System → Site Templates
6. Configure template settings
7. Set as default template if desired
## CI/CD Pipeline Details
### Build Process
1. **Validation** - All scripts validate before packaging
2. **Packaging** - Create ZIP with proper structure
3. **Testing** - Run on multiple PHP/Joomla versions
4. **Quality** - PHPStan and PHPCS analysis
5. **Deployment** - SFTP upload to target environment
### Matrix Testing Strategy
- **PHP Versions:** 8.0, 8.1, 8.2, 8.3
- **Joomla Versions:** 4.4 LTS, 5.0, 5.1
- **Exclusions:** PHP 8.3 not tested with Joomla 4.4 (incompatible)
## Troubleshooting
### Common Issues
**Issue: PHP_CodeSniffer not found**
```bash
composer global require "squizlabs/php_codesniffer:^3.0"
export PATH="$PATH:$HOME/.composer/vendor/bin"
```
**Issue: PHPStan errors**
```bash
# Increase analysis memory
php -d memory_limit=1G $(which phpstan) analyse
```
**Issue: Joomla installation fails in CI**
- Check MySQL service is running
- Verify database credentials
- Ensure PHP extensions are installed
**Issue: SFTP deployment fails**
- Verify SSH key is correctly formatted
- Check firewall allows port 22
- Ensure STAGING_PATH exists on server
## Contributing
When adding new workflows or scripts:
1. Follow existing script structure
2. Add proper error handling
3. Include usage documentation
4. Test with multiple PHP versions
5. Update this documentation
## Support
For issues or questions:
- Open an issue on GitHub
- Check existing workflow runs for examples
- Review test output in Actions tab
## License
All scripts and workflows are licensed under GPL-3.0-or-later, same as the main project.
---
## Metadata
* Document: docs/JOOMLA_DEVELOPMENT.md
* Repository: [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* Path: /docs/JOOMLA_DEVELOPMENT.md
* Owner: Moko Consulting
* Version: 03.06.03
* Status: Active
* Effective Date: 2026-01-30
* Classification: Public Open Source Documentation
## Revision History
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------- | --------------- |
| 2026-01-30 | Updated metadata to MokoStandards format | GitHub Copilot |
| 2025-01-04 | Initial Joomla development guide created | GitHub Copilot |

310
docs/MANUAL_DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,310 @@
# Manual Deployment Guide - MokoOnyx
This guide explains how to manually deploy the MokoOnyx template from the `src` directory to a Joomla installation without using the build/packaging process.
## Table of Contents
- [Overview](#overview)
- [Understanding the Structure](#understanding-the-structure)
- [Manual Deployment Methods](#manual-deployment-methods)
- [Troubleshooting](#troubleshooting)
- [When to Use Manual Deployment](#when-to-use-manual-deployment)
## Overview
**Important**: The `src` directory in this repository is the development source, not a ready-to-install package. For production use, we recommend using the packaged ZIP file from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases).
However, for development or testing purposes, you can manually deploy files from the `src` directory to your Joomla installation.
## Understanding the Structure
### Repository Structure
The `src/` directory contains:
```
src/
├── component.php # Template file
├── error.php # Template file
├── index.php # Main template file
├── offline.php # Template file
├── templateDetails.xml # Template manifest
├── joomla.asset.json # Asset registration
├── html/ # Module & component overrides
├── language/ # Frontend language files
├── administrator/ # Backend language files
│ └── language/
└── media/ # Assets (CSS, JS, images, fonts)
├── css/
├── js/
├── images/
└── fonts/
```
### Joomla Installation Structure
Joomla expects template files in these locations:
```
YOUR_JOOMLA_ROOT/
├── templates/
│ └── mokoonyx/ # Template files go here
│ ├── component.php
│ ├── error.php
│ ├── index.php
│ ├── offline.php
│ ├── templateDetails.xml
│ ├── joomla.asset.json
│ ├── html/
│ ├── language/
│ └── administrator/
└── media/
└── templates/
└── site/
└── mokoonyx/ # Media files go here
├── css/
├── js/
├── images/
└── fonts/
```
**Key Point**: Template files and media files go to **different locations** in Joomla!
## Manual Deployment Methods
### Method 1: Recommended - Upload as ZIP (Still Manual)
This method mimics what Joomla's installer does automatically.
1. **Prepare the template directory**:
```bash
# From the repository root
cd src
# Copy all files EXCEPT media to a temp directory
mkdir -p /tmp/mokoonyx
cp component.php /tmp/mokoonyx/
cp error.php /tmp/mokoonyx/
cp index.php /tmp/mokoonyx/
cp offline.php /tmp/mokoonyx/
cp templateDetails.xml /tmp/mokoonyx/
cp joomla.asset.json /tmp/mokoonyx/
cp -r html /tmp/mokoonyx/
cp -r language /tmp/mokoonyx/
cp -r administrator /tmp/mokoonyx/
# Copy media to a separate temp directory
mkdir -p /tmp/mokoonyx_media
cp -r media/* /tmp/mokoonyx_media/
```
2. **Upload to Joomla via FTP/SFTP**:
```bash
# Upload template files
# Replace with your actual Joomla path
scp -r /tmp/mokoonyx/* user@yourserver:/path/to/joomla/templates/mokoonyx/
# Upload media files
scp -r /tmp/mokoonyx_media/* user@yourserver:/path/to/joomla/media/templates/site/mokoonyx/
```
3. **Set proper permissions**:
```bash
# On your server
cd /path/to/joomla
chmod 755 templates/mokoonyx
chmod 644 templates/mokoonyx/*
chmod 755 templates/mokoonyx/html
chmod 755 media/templates/site/mokoonyx
```
### Method 2: Direct Copy to Existing Installation
If you have direct filesystem access (e.g., local development):
1. **Copy template files** (excluding media):
```bash
# From repository root
cd src
# Copy to Joomla templates directory
cp component.php /path/to/joomla/templates/mokoonyx/
cp error.php /path/to/joomla/templates/mokoonyx/
cp index.php /path/to/joomla/templates/mokoonyx/
cp offline.php /path/to/joomla/templates/mokoonyx/
cp templateDetails.xml /path/to/joomla/templates/mokoonyx/
cp joomla.asset.json /path/to/joomla/templates/mokoonyx/
# Copy directories
cp -r html /path/to/joomla/templates/mokoonyx/
cp -r language /path/to/joomla/templates/mokoonyx/
cp -r administrator /path/to/joomla/templates/mokoonyx/
```
2. **Copy media files separately**:
```bash
# Copy media to the media directory
cp -r media/* /path/to/joomla/media/templates/site/mokoonyx/
```
3. **Clear Joomla cache**:
- In Joomla admin: **System → Clear Cache**
- Or delete: `/path/to/joomla/cache/*` and `/path/to/joomla/administrator/cache/*`
### Method 3: Symlink for Development (Linux/Mac only)
For active development where you want changes to immediately reflect:
1. **Create symlinks**:
```bash
# Remove existing directory if present
rm -rf /path/to/joomla/templates/mokoonyx
rm -rf /path/to/joomla/media/templates/site/mokoonyx
# Create parent directories if needed
mkdir -p /path/to/joomla/templates
mkdir -p /path/to/joomla/media/templates/site
# Symlink template files
ln -s /path/to/MokoOnyx/src /path/to/joomla/templates/mokoonyx
# Symlink media files
ln -s /path/to/MokoOnyx/src/media /path/to/joomla/media/templates/site/mokoonyx
```
2. **Note**: This won't work as-is because the src directory includes the media folder. You'll need to:
```bash
# Better approach for symlinks:
# Link everything except media at template root
cd /path/to/joomla/templates
mkdir -p mokoonyx
cd mokoonyx
ln -s /path/to/MokoOnyx/src/component.php
ln -s /path/to/MokoOnyx/src/error.php
ln -s /path/to/MokoOnyx/src/index.php
ln -s /path/to/MokoOnyx/src/offline.php
ln -s /path/to/MokoOnyx/src/templateDetails.xml
ln -s /path/to/MokoOnyx/src/joomla.asset.json
ln -s /path/to/MokoOnyx/src/html
ln -s /path/to/MokoOnyx/src/language
ln -s /path/to/MokoOnyx/src/administrator
# Link media separately
ln -s /path/to/MokoOnyx/src/media /path/to/joomla/media/templates/site/mokoonyx
```
## Troubleshooting
### Language Files Not Loading
**Problem**: Language strings appear as language keys (e.g., `TPL_MOKOONYX_LABEL`)
**Solution**: Ensure the `language` and `administrator` folders are present in your template directory:
```bash
# Check if folders exist
ls -la /path/to/joomla/templates/mokoonyx/language
ls -la /path/to/joomla/templates/mokoonyx/administrator
```
The `templateDetails.xml` should contain (lines 54-55):
```xml
<files>
<!-- ... other files ... -->
<folder>language</folder>
<folder>administrator</folder>
</files>
```
### CSS/JS Not Loading
**Problem**: Styles or scripts don't apply
**Solution**: Verify media files are in the correct location:
```bash
# Check media directory structure
ls -la /path/to/joomla/media/templates/site/mokoonyx/
# Should show: css/, js/, images/, fonts/
```
Clear Joomla cache:
- Admin: **System → Clear Cache**
- Check browser developer console for 404 errors
### Template Not Appearing in Template Manager
**Problem**: MokoOnyx doesn't show in **System → Site Templates**
**Solution**:
1. Verify `templateDetails.xml` is present in `/path/to/joomla/templates/mokoonyx/`
2. Check file permissions (should be readable by web server)
3. Verify XML is well-formed:
```bash
xmllint --noout /path/to/joomla/templates/mokoonyx/templateDetails.xml
```
4. Check Joomla's error logs for XML parsing errors
### File Permission Issues
**Problem**: "Permission denied" or template files not readable
**Solution**:
```bash
# Set proper ownership (replace www-data with your web server user)
chown -R www-data:www-data /path/to/joomla/templates/mokoonyx
chown -R www-data:www-data /path/to/joomla/media/templates/site/mokoonyx
# Set proper permissions
find /path/to/joomla/templates/mokoonyx -type d -exec chmod 755 {} \;
find /path/to/joomla/templates/mokoonyx -type f -exec chmod 644 {} \;
find /path/to/joomla/media/templates/site/mokoonyx -type d -exec chmod 755 {} \;
find /path/to/joomla/media/templates/site/mokoonyx -type f -exec chmod 644 {} \;
```
## When to Use Manual Deployment
### ✅ Use Manual Deployment For:
- **Active Development**: Testing changes immediately without rebuilding packages
- **Local Development**: Working on a local Joomla instance
- **Quick Fixes**: Making emergency hotfixes directly on a development server
- **Learning**: Understanding the template structure and Joomla's file organization
### ❌ Don't Use Manual Deployment For:
- **Production Sites**: Always use packaged ZIP files from releases
- **Client Sites**: Use proper Joomla extension installation
- **Version Control**: Can lead to inconsistent deployments
- **Staging Environments**: Use CI/CD or release packages
## Best Practices
1. **Always Test Locally First**: Don't deploy untested changes to production
2. **Keep Backups**: Back up both template and media directories before updating
3. **Use Version Control**: Track your customizations separately from manual deployments
4. **Document Changes**: Note any manual file modifications
5. **Clear Cache**: Always clear Joomla cache after manual file updates
6. **Verify Permissions**: Ensure web server can read all files
## Related Documentation
- **[Quick Start Guide](QUICK_START.md)** - Development environment setup
- **[Joomla Development Guide](JOOMLA_DEVELOPMENT.md)** - Complete development workflows
- **[Release Process](RELEASE_PROCESS.md)** - How to create proper release packages
## Support
If you encounter issues with manual deployment:
1. Check this troubleshooting guide first
2. Review [Joomla's template documentation](https://docs.joomla.org/J4.x:Creating_a_Simple_Template)
3. Open an issue on [GitHub](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
4. Contact: hello@mokoconsulting.tech
---
**Document Version**: 1.0.0
**Last Updated**: 2026-03-01
**Status**: Active

725
docs/MODULE_OVERRIDES.md Normal file
View File

@@ -0,0 +1,725 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: docs/MODULE_OVERRIDES.md
VERSION: 03.09.03
BRIEF: Comprehensive guide to MokoOnyx mobile-responsive module overrides
PATH: /docs/MODULE_OVERRIDES.md
-->
# Module & Component Overrides — MokoOnyx
This document provides a comprehensive guide to all mobile-responsive module and component overrides included in MokoOnyx.
## Overview
MokoOnyx includes **16 mobile-responsive module overrides** and **12 component view overrides** designed to enhance the mobile user experience for third-party extensions and the Main Menu navigation.
**Important**: Following Cassiopeia template best practices, MokoOnyx avoids overriding standard Joomla core modules (such as mod_search, mod_login, mod_breadcrumbs) to ensure proper language loading and compatibility. **Exception**: mod_menu "Main Menu" override provides essential Bootstrap 5 collapsible dropdown functionality.
### Alternative Layouts, Not Replacements
**All MokoOnyx overrides use alternative layout names (`mobile.php`) instead of replacing default layouts (`default.php`).** This means:
- ✅ Default Joomla layouts continue to work unchanged
- ✅ You must explicitly select the "mobile" layout in module/menu item settings
- ✅ Joomla core updates don't break your site
- ✅ Full control over which modules use enhanced layouts
**📖 See [OVERRIDE_PHILOSOPHY.md](OVERRIDE_PHILOSOPHY.md) for complete details on how to activate and use these alternative layouts.**
### Key Features
All module overrides share these characteristics:
- **Mobile-First Design**: Optimized for mobile devices with responsive breakpoints
- **Touch Targets**: 48px on mobile, 44px on desktop (WCAG 2.1 compliant)
- **Input Font Size**: 16px minimum on mobile (prevents iOS zoom)
- **Accessibility**: Full ARIA labels, keyboard navigation, semantic HTML
- **BEM Naming**: Consistent CSS class naming convention
- **CSS Variables**: Integration with template color schemes
- **Security**: Proper escaping, _JEXEC checks, index.html protection
- **Documentation**: Each override includes comprehensive README
## Module Categories
### 1. VirtueMart E-Commerce Modules
Five comprehensive overrides for VirtueMart shopping functionality.
**Master Documentation**: [VIRTUEMART_MODULES_README.md](../src/html/VIRTUEMART_MODULES_README.md)
#### mod_virtuemart_cart
**Location**: `src/html/mod_virtuemart_cart/`
Shopping cart display with product list and checkout button.
**Features**:
- Responsive product cards
- Remove item buttons with confirmations
- Price display with currency formatting
- Checkout button with prominent styling
#### mod_virtuemart_product
**Location**: `src/html/mod_virtuemart_product/`
Product showcase with grid layouts.
**Features**:
- Responsive grid: 1-4 columns based on screen size
- Product images with hover effects
- Price display and "Add to Cart" buttons
- Rating display support
#### mod_virtuemart_currencies
**Location**: `src/html/mod_virtuemart_currencies/`
Currency selector dropdown for multi-currency stores.
**Features**:
- Accessible dropdown with proper labels
- Currency symbol and name display
- Responsive button styling
#### mod_virtuemart_category
**Location**: `src/html/mod_virtuemart_category/`
Category navigation with hierarchical display.
**Features**:
- Expandable subcategories
- Product count display
- Hierarchical indentation
- Active category highlighting
#### mod_virtuemart_manufacturer
**Location**: `src/html/mod_virtuemart_manufacturer/`
Manufacturer/brand display with grid layout.
**Features**:
- Responsive grid: 2-4 columns
- Logo display support
- Product count per manufacturer
---
### 2. Main Menu & Community Builder Modules
Three essential Community Builder and navigation module overrides.
#### mod_menu (Main Menu)
**Location**: `src/html/mod_menu/`
Bootstrap 5 responsive navigation menu with collapsible dropdown functionality.
**Files**:
- `mainmenu.php` - Main layout with Bootstrap navbar
- `mainmenu_component.php` - Component menu items
- `mainmenu_heading.php` - Heading menu items
- `mainmenu_separator.php` - Separator menu items
- `mainmenu_url.php` - URL menu items
**Features**:
- Bootstrap 5 navbar structure with collapsible hamburger menu
- Multi-level dropdown support (hover on desktop, tap on mobile)
- WCAG 2.1 compliant touch targets (48px mobile, 44px desktop)
- BEM naming convention: `.mod-menu-main__*`
- Active state indicators for current menu items
- ARIA labels and keyboard navigation support
- Alternative layout named `mainmenu.php` (not `default.php`)
**Activation**: Select "Mainmenu" layout in Joomla Administrator → Modules → Menu Module → Advanced Tab → Alternative Layout
**Note**: Unlike the broken mod_menu override removed in v03.08.01, this v03.08.03 version is properly structured based on Joomla core layouts and Bootstrap 5, ensuring language strings load correctly and menu functionality works as expected.
#### mod_cblogin
**Location**: `src/html/mod_cblogin/`
Community Builder login with avatar display.
**Features**:
- User avatar when logged in
- CB-specific login form
- Profile link
- Logout button
#### mod_comprofilerOnline
**Location**: `src/html/mod_comprofilerOnline/`
Community Builder online users display.
**Features**:
- User count display
- Avatar grid layout
- Username display
- Online status indicators
---
### 3. Industry Extension Modules
Eight popular third-party extension module overrides plus component views.
#### K2 Content Extension
##### mod_k2_content
**Location**: `src/html/mod_k2_content/`
K2 content display with advanced layouts.
**Features**:
- Responsive grid: 1-3 columns
- Featured images with lazy loading
- Category, author, date metadata
- Excerpt support
- Tag display
#### AcyMailing Newsletter
##### mod_acymailing
**Location**: `src/html/mod_acymailing/`
Newsletter subscription form.
**Features**:
- Email validation
- Privacy checkbox
- Success/error messaging
- GDPR compliance fields
#### HikaShop E-Commerce
##### mod_hikashop_cart
**Location**: `src/html/mod_hikashop_cart/`
HikaShop shopping cart module.
**Features**:
- Product list with images
- Quantity adjustment
- Price totals
- Checkout button
#### Kunena Forum
Four comprehensive forum modules plus component view.
##### mod_kunenalatest
**Location**: `src/html/mod_kunenalatest/`
Latest forum posts display.
**Features**:
- Post excerpts
- Author avatars
- Reply count
- Post date
##### mod_kunenalogin
**Location**: `src/html/mod_kunenalogin/`
Forum-specific login module.
**Features**:
- User avatar display
- Forum statistics
- Quick login form
- Profile link
##### mod_kunenasearch
**Location**: `src/html/mod_kunenasearch/`
Forum search with button positions.
**Features**:
- Multiple button positions (left, right, top)
- Search placeholder text
- Icon support
- 48px touch targets
##### mod_kunenastats
**Location**: `src/html/mod_kunenastats/`
Forum statistics display.
**Features**:
- Visual stat cards
- Member count
- Topic/post totals
- Latest member
- Responsive grid layout
##### com_kunena (Component)
**Location**: `src/html/com_kunena/`
Forum category list view.
**Views**:
- `category/default.php` - Category listing with icons
#### OS Membership Pro
Module and component overrides for membership management.
##### mod_osmembership
**Location**: `src/html/mod_osmembership/`
Membership plans module.
**Features**:
- Plan cards with pricing
- Feature lists
- Signup buttons
- Badge displays (popular, featured)
##### com_osmembership (Component)
**Location**: `src/html/com_osmembership/`
Membership pricing tables.
**Views**:
- `plans/default.php` - Responsive pricing table with comparison features
---
### 4. Community Builder Components
Four comprehensive component view overrides for Community Builder user management.
#### com_comprofiler
**Location**: `src/html/com_comprofiler/`
Mobile-responsive views for Community Builder user profiles, registration, and login.
##### userprofile
User profile display with tabbed interface.
**Features**:
- Large avatar display (150px)
- Tabbed interface for profile sections
- Custom field display with labels
- Online status indicator
- Responsive layout: vertical mobile → horizontal desktop
##### userslist
User directory with search and grid layout.
**Features**:
- Search functionality with accessible form
- Responsive grid: 1 column mobile → 2-3 columns desktop
- User cards with avatars (80px)
- Custom field display
- Profile view buttons
- Pagination support
##### registers
Multi-step registration form with validation.
**Features**:
- Fieldset organization with legends
- Required field indicators (*)
- Input validation and error display
- Captcha support section
- Terms & conditions checkbox
- GDPR-compliant design
- 16px input font on mobile
##### login
Login page with remember me and helper links.
**Features**:
- Centered login container (max-width: 450px)
- Username/password fields with autocomplete
- Remember me checkbox
- Registration and password recovery links
- CSRF token support
- Responsive padding adjustments
### 5. JEM (Joomla Event Manager) Components
Five comprehensive component view overrides for JEM event management.
#### com_jem
**Location**: `src/html/com_jem/`
Mobile-responsive views for JEM event listings, details, calendar, venues, and categories.
##### eventslist
Event listing with card-based layout.
**Features**:
- Event cards with date, time, and venue
- Category badges with color coding
- Responsive event grid layout
- Event description excerpts
- Read more buttons with clear calls-to-action
- Pagination support
- Empty state messaging
##### event
Single event details view with comprehensive information.
**Features**:
- Large event image display (responsive)
- Date and time with structured data
- Venue information with maps link
- Event description with full content
- Category display with badges
- Registration information (if enabled)
- Contact information display
- Back to events navigation
- Meta information with icons
##### calendar
Monthly calendar view with event indicators.
**Features**:
- Month navigation (previous/next)
- Calendar grid with weekday headers
- Event indicators on dates with events
- Responsive calendar layout
- Today highlighting
- Event list for selected month
- Event count per day display
- Touch-friendly navigation buttons
##### venue
Venue details with location and upcoming events.
**Features**:
- Venue image display
- Complete address information
- Website link (external)
- Google Maps integration
- Venue description
- Upcoming events at venue
- Location coordinates display
- Back navigation
##### categories
Event category listing with descriptions.
**Features**:
- Category cards with images
- Category descriptions
- Event count per category
- View category buttons
- Responsive grid layout
- Empty state messaging
- Pagination support
---
## CSS Architecture
All module styles are located in `src/media/css/template.css` with dedicated sections:
### CSS Sections
1. **MOD_SEARCH MOBILE RESPONSIVE STYLES** (Lines ~18400+)
- Search box layouts
- Button position variants
- Input styling
2. **VIRTUEMART MODULE MOBILE RESPONSIVE STYLES** (Lines ~18500+)
- Cart product cards
- Product grids
- Currency selector
- Category navigation
- Manufacturer displays
3. **STANDARD JOOMLA & COMMUNITY BUILDER MODULE STYLES** (Lines ~19300+)
- Menu navigation
- Breadcrumbs
- Login forms
- Article displays
- CB module components
4. **INDUSTRY EXTENSION MODULE STYLES** (Lines ~19800+)
- K2 content grids
- AcyMailing forms
- HikaShop cart
- Kunena forum modules
- OS Membership pricing
5. **COMMUNITY BUILDER COMPONENT STYLES** (Lines ~21000+)
- User profile layouts
- Users list grids
- Registration forms
- Login pages
- Tab interfaces
6. **JEM COMPONENT STYLES** (Lines ~22000+)
- Event list cards
- Event details layout
- Calendar grid
- Venue information
- Category displays
### CSS Variables Integration
All modules integrate with template CSS variables:
```css
/* Common Variables Used */
--body-color /* Text color */
--link-color /* Link color */
--link-hover-color /* Link hover color */
--border-color /* Border color */
--secondary-bg /* Background color */
--border-radius /* Border radius */
--input-bg /* Input background */
--input-border-color /* Input border */
--btn-primary-bg /* Primary button */
--btn-primary-hover-bg /* Button hover */
```
See [CSS_VARIABLES.md](CSS_VARIABLES.md) for complete reference.
---
## Responsive Breakpoints
All modules use Bootstrap-aligned breakpoints:
| Breakpoint | Size | Typical Changes |
|------------|-----------|-----------------------------------|
| `xs` | < 576px | Single column, stacked layouts |
| `sm` | ≥ 576px | 2 columns for grids |
| `md` | ≥ 768px | 3 columns, horizontal layouts |
| `lg` | ≥ 992px | 4 columns, expanded spacing |
| `xl` | ≥ 1200px | Maximum width, optimal spacing |
| `xxl` | ≥ 1400px | Extra spacing |
---
## Accessibility Features
All overrides implement comprehensive accessibility:
### ARIA Labels
- Descriptive labels for all interactive elements
- `aria-label` for icon-only buttons
- `aria-describedby` for form fields
- `aria-live` for dynamic content
### Keyboard Navigation
- Proper tab order
- Focus states on all interactive elements
- Keyboard-accessible dropdowns
- Skip links where appropriate
### Screen Readers
- Semantic HTML5 elements
- Hidden text for icon-only elements
- Proper heading hierarchy
- Alternative text for images
### WCAG 2.1 Compliance
- Touch targets: 48px minimum on mobile
- Color contrast ratios meet AA standards
- Text resizable to 200% without loss
- No content relies on color alone
---
## Customization Guide
### Override Customization
Each module can be customized in two ways:
#### 1. CSS Customization
Edit `src/media/css/user.css` to add custom styles:
```css
/* Example: Change product grid columns */
@media (min-width: 768px) {
.mod-vm-product__grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Example: Customize cart button */
.mod-vm-cart__checkout-button {
background-color: #28a745;
}
```
#### 2. Template Override Customization
Copy the entire module directory and modify:
```bash
# Keep original override as reference
cp -r src/html/mod_virtuemart_cart src/html/mod_virtuemart_cart_original
# Modify your version
# Edit src/html/mod_virtuemart_cart/default.php
```
### CSS Variables Override
Override CSS variables in your custom color scheme:
```css
/* src/media/css/theme/light.custom.css */
:root {
--vm-price-color: #28a745;
--vm-cart-bg: #f8f9fa;
--vm-button-primary: #007bff;
}
```
---
## Best Practices
### When Using Overrides
1. **Test Across Devices**: Always test on actual mobile devices
2. **Maintain Accessibility**: Don't remove ARIA labels or keyboard navigation
3. **Keep BEM Naming**: Use established class naming patterns
4. **Security First**: Always escape output and validate input
5. **Document Changes**: Comment your customizations
### When Updating
1. **Backup First**: Always backup your site before updating
2. **Review Changes**: Check CHANGELOG.md for breaking changes
3. **Test Thoroughly**: Test all modules after updates
4. **Custom Overrides**: May need adjustments after template updates
---
## Troubleshooting
### Common Issues
#### Module Not Displaying Correctly
1. Clear Joomla cache (System → Clear Cache)
2. Check module is published and assigned to correct position
3. Verify template is assigned to menu items
4. Check browser console for JavaScript errors
#### Styles Not Applying
1. Clear browser cache (Ctrl+F5 / Cmd+Shift+R)
2. Verify `template.css` is loading
3. Check CSS specificity conflicts
4. Review custom CSS in `user.css`
#### Mobile View Issues
1. Test with browser dev tools responsive mode
2. Check viewport meta tag in template
3. Verify breakpoint media queries
4. Test on actual devices when possible
#### Accessibility Issues
1. Run WAVE or axe DevTools accessibility check
2. Test with keyboard navigation only
3. Verify screen reader compatibility
4. Check color contrast ratios
### Getting Help
- **Documentation**: Check module-specific README files
- **GitHub Issues**: [Report issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Support**: hello@mokoconsulting.tech
---
## How to Activate Alternative Layouts
All MokoOnyx overrides are **alternative layouts** that must be explicitly activated. They do not automatically replace default layouts.
### Quick Start: Enable Mobile Layout
1. **Go to Joomla Administrator** → Extensions → Modules
2. **Open the module** you want to enhance (e.g., VirtueMart Cart)
3. **Navigate to Advanced tab**
4. **Find "Alternative Layout" field**
5. **Select "MokoOnyx - mobile"** from dropdown
6. **Save & Close**
### For Menu Items (Component Views)
1. **Go to Menus** → Select your menu
2. **Open the menu item** (e.g., Events List)
3. **Navigate to Advanced Options or Page Display tab**
4. **Find "Alternative Layout" field**
5. **Select "MokoOnyx - mobile"** from dropdown
6. **Save & Close**
### Apply to All Modules in a Position
In your template's `index.php`, specify layout for entire module position:
```php
<jdoc:include type="modules" name="sidebar-left" style="none" layout="mobile" />
```
**📖 For complete documentation, see [OVERRIDE_PHILOSOPHY.md](OVERRIDE_PHILOSOPHY.md)**
---
## Version History
| Version | Date | Changes |
|----------|------------|--------------------------------------------------|
| 03.08.04 | 2026-02-27 | Added alternative layout activation instructions, JEM overrides |
| 03.08.03 | 2026-02-25 | Removed mod_search override per Cassiopeia philosophy |
| 03.08.00 | 2026-02-22 | Added Community Builder component overrides |
| 03.07.00 | 2026-02-22 | Initial release of all mobile-responsive overrides |
---
## Additional Resources
- **Override Philosophy**: [OVERRIDE_PHILOSOPHY.md](OVERRIDE_PHILOSOPHY.md) ⭐ **Start here**
- **Main README**: [README.md](../README.md)
- **Changelog**: [CHANGELOG.md](../CHANGELOG.md)
- **CSS Variables**: [CSS_VARIABLES.md](CSS_VARIABLES.md)
- **Repository**: [GitHub](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
---
## Metadata
* Document: docs/MODULE_OVERRIDES.md
* Repository: [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* Path: /docs/MODULE_OVERRIDES.md
* Owner: Moko Consulting
* Version: 03.07.00
* Status: Active
* Effective Date: 2026-02-22
* Classification: Public Open Source Documentation
## Revision History
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------- | --------------- |
| 2026-02-22 | Initial creation with comprehensive module override documentation | GitHub Copilot |

332
docs/OVERRIDE_PHILOSOPHY.md Normal file
View File

@@ -0,0 +1,332 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: docs/OVERRIDE_PHILOSOPHY.md
VERSION: 03.09.03
BRIEF: Philosophy and implementation of non-replacing alternative layouts
PATH: /docs/OVERRIDE_PHILOSOPHY.md
-->
# Override Philosophy — MokoOnyx
## Core Principle: Add-On, Not Replacement
**MokoOnyx overrides are designed as alternative layouts, not replacements of default Joomla layouts.**
This means:
- ✅ Default Joomla layouts continue to work unchanged
- ✅ Site administrators can choose when to use our enhanced layouts
- ✅ Updates to Joomla core layouts don't break the site
- ✅ Compatibility with other extensions is maintained
- ✅ Users have control over which layouts to use
---
## Technical Implementation
### Layout Naming Convention
All MokoOnyx overrides use **`mobile.php`** naming instead of **`default.php`**:
```
❌ BAD (Replaces default):
src/html/mod_virtuemart_cart/default.php
✅ GOOD (Alternative layout):
src/html/mod_virtuemart_cart/mobile.php
```
### How Joomla Handles Layouts
When a module or component looks for a layout, Joomla searches in this order:
1. **Template override with specified layout name**: `templates/mokoonyx/html/{extension}/{view}/{layout}.php`
2. **Extension's specified layout**: `{extension}/tmpl/{view}/{layout}.php`
3. **Template override for default layout**: `templates/mokoonyx/html/{extension}/{view}/default.php`
4. **Extension's default layout**: `{extension}/tmpl/{view}/default.php`
By naming our overrides `mobile.php` instead of `default.php`, they become **step 1** alternatives that must be explicitly selected, rather than **step 3** replacements that are automatically used.
---
## How to Use Alternative Layouts
### Method 1: Module/Menu Item Settings
When editing a module or menu item in Joomla administrator:
1. Open the module/menu item for editing
2. Navigate to the **Advanced** tab
3. Find the **Alternative Layout** field
4. Select **MokoOnyx - mobile** from the dropdown
5. Save
### Method 2: Override in Module Position
If you want all modules in a specific position to use the mobile layout:
```php
<!-- In your index.php template file -->
<?php if ($this->countModules('sidebar-left')) : ?>
<jdoc:include type="modules" name="sidebar-left" style="none" layout="mobile" />
<?php endif; ?>
```
### Method 3: Module Chrome (Advanced)
Create a custom module chrome in `templates/mokoonyx/html/layouts/chromes/` that automatically applies the mobile layout.
---
## Exception: Main Menu
**The only exception** to this philosophy is `mod_menu` with the "Main Menu" module type.
The template includes files like:
- `src/html/mod_menu/mainmenu.php`
- `src/html/mod_menu/mainmenu_component.php`
- `src/html/mod_menu/mainmenu_heading.php`
- `src/html/mod_menu/mainmenu_url.php`
- `src/html/mod_menu/mainmenu_separator.php`
These use a **custom layout name** (`mainmenu`) instead of replacing `default.php`, which allows the site to:
- Use the enhanced Bootstrap 5 collapsible menu for main navigation
- Keep standard Joomla menus working in other positions
- Provide better mobile navigation without breaking existing menus
To use this layout, set the module's **Alternative Layout** to **MokoOnyx - mainmenu**.
---
## Override Inventory
### Module Overrides (16 total)
All use `mobile.php` naming (alternative layout):
**VirtueMart (5)**:
- `mod_virtuemart_cart/mobile.php`
- `mod_virtuemart_product/mobile.php`
- `mod_virtuemart_currencies/mobile.php`
- `mod_virtuemart_category/mobile.php`
- `mod_virtuemart_manufacturer/mobile.php`
**Community Builder (2)**:
- `mod_cblogin/mobile.php`
- `mod_comprofilerOnline/mobile.php`
**Main Menu (1)**:
- `mod_menu/mainmenu.php` (custom layout name)
**Industry Extensions (8)**:
- `mod_k2_content/mobile.php`
- `mod_acymailing/mobile.php`
- `mod_hikashop_cart/mobile.php`
- `mod_kunenalatest/mobile.php`
- `mod_kunenalogin/mobile.php`
- `mod_kunenasearch/mobile.php`
- `mod_kunenastats/mobile.php`
- `mod_osmembership/mobile.php`
### Component View Overrides (12 total)
All use `mobile.php` naming (alternative layout):
**Community Builder (4)**:
- `com_comprofiler/userprofile/mobile.php`
- `com_comprofiler/userslist/mobile.php`
- `com_comprofiler/registers/mobile.php`
- `com_comprofiler/login/mobile.php`
**JEM - Joomla Event Manager (5)**:
- `com_jem/eventslist/mobile.php`
- `com_jem/event/mobile.php`
- `com_jem/calendar/mobile.php`
- `com_jem/venue/mobile.php`
- `com_jem/categories/mobile.php`
**Kunena Forum (1)**:
- `com_kunena/category/mobile.php`
**OSMembership (2)**:
- `com_osmembership/plan/mobile.php`
- `com_osmembership/plans/mobile.php`
**Joomla Core (2)**:
- `com_content/article/toc-left.php` (custom layout name)
- `com_content/article/toc-right.php` (custom layout name)
---
## Benefits of This Approach
### 1. **Zero Breaking Changes**
Existing sites continue to work exactly as before. No layouts are forcibly changed.
### 2. **Gradual Adoption**
Site administrators can:
- Test mobile layouts on specific modules first
- Roll out changes module-by-module
- Keep some modules using default layouts if needed
- Easily revert by changing the Alternative Layout setting
### 3. **Extension Compatibility**
Third-party extensions' default layouts remain untouched, preventing conflicts with:
- Extension updates
- Other templates
- Custom development
### 4. **Joomla Core Updates**
When Joomla core updates:
- Default layouts get new features/bug fixes automatically
- Mobile layouts remain stable and tested
- No emergency fixes needed after Joomla updates
### 5. **Multi-Language Support**
Joomla's language system loads extension language files properly because:
- Extensions aren't hijacked by template overrides
- Language strings come from the correct source
- Translations work as expected
---
## Standards Not Overridden
Following Cassiopeia template best practices, MokoOnyx **does not override** standard Joomla core modules:
-`mod_breadcrumbs` - Use Joomla core layout
-`mod_login` - Use Joomla core layout
-`mod_articles_latest` - Use Joomla core layout
-`mod_articles_category` - Use Joomla core layout
-`mod_articles_news` - Use Joomla core layout
-`mod_search` - Use Joomla core layout (removed in v03.08.03)
**Reason**: These modules have robust core layouts with proper language loading, accessibility, and ongoing Joomla maintenance.
---
## Developer Guidelines
When adding new overrides to MokoOnyx:
### ✅ DO:
1. Name files `mobile.php` or use descriptive custom names (`mainmenu.php`, `toc-left.php`)
2. Document the alternative layout in MODULE_OVERRIDES.md
3. Add CSS with BEM naming: `.{extension}-{view}__element`
4. Test that default layouts still work
5. Provide clear instructions for selecting the layout
### ❌ DON'T:
1. Create `default.php` files that replace core layouts
2. Override standard Joomla core modules without strong justification
3. Break backward compatibility
4. Assume users will automatically get your layout
5. Forget to document how to enable the alternative layout
---
## Migration from Replacing Overrides
If you're migrating from a template that used `default.php` overrides:
### Step 1: Identify Replaced Layouts
```bash
find templates/oldtemplate/html -name "default.php"
```
### Step 2: Rename to Alternative Layouts
```bash
# For each default.php found:
mv default.php mobile.php
```
### Step 3: Update Module Settings
For each module using the old override:
1. Edit module in administrator
2. Advanced tab → Alternative Layout
3. Select "mobile" from dropdown
4. Save
### Step 4: Test
- Verify module displays correctly
- Check that other modules still use default layouts
- Confirm language strings load properly
---
## Troubleshooting
### My Alternative Layout Doesn't Appear in Dropdown
**Check:**
1. File is in correct location: `templates/mokoonyx/html/{extension}/{view}/`
2. File has `.php` extension
3. File is not named `default.php`
4. Cache is cleared (System → Clear Cache)
### Module Still Uses Default Layout
**Check:**
1. Module's Alternative Layout setting in administrator
2. Module position's `layout` parameter in `<jdoc:include>` tag
3. File permissions (must be readable)
4. Template is assigned to correct pages
### Layout Works But Looks Wrong
**Check:**
1. CSS is loaded: inspect element and check for `.{extension}-{view}__` classes
2. `template.css` is up to date
3. Browser cache is cleared
4. CSS variables are defined in template
---
## References
- [Joomla Docs: Layout Overrides](https://docs.joomla.org/Layout_Overrides_in_Joomla)
- [Joomla Docs: Alternative Layouts](https://docs.joomla.org/J3.x:How_to_use_the_alternative_layout_feature)
- [MODULE_OVERRIDES.md](MODULE_OVERRIDES.md) - Complete override inventory
- [CSS_VARIABLES.md](CSS_VARIABLES.md) - Template styling system
---
## Version History
- **03.08.04**: Created OVERRIDE_PHILOSOPHY.md document
- **03.08.03**: Removed mod_search override to align with philosophy
- **03.08.02**: Removed standard Joomla module overrides for proper language loading
- **Earlier**: Renamed all overrides from default.php to mobile.php (21 files)

333
docs/QUICK_START.md Normal file
View File

@@ -0,0 +1,333 @@
# Quick Start Guide - MokoOnyx Development
Get up and running with MokoOnyx development in minutes.
## Prerequisites
Before you begin, ensure you have:
- **Git** - For version control
- **PHP 8.0+** - Required runtime
- **Composer** - PHP dependency manager
- **Make** (optional) - For convenient commands
- **Code Editor** - VS Code recommended (tasks pre-configured)
## 5-Minute Setup
### 1. Clone the Repository
```bash
git clone https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
cd MokoOnyx
```
### 2. Install Development Dependencies
```bash
# Using Make (recommended)
make dev-setup
# Or manually
composer global require "squizlabs/php_codesniffer:^3.0"
composer global require phpstan/phpstan
composer global require "phpcompatibility/php-compatibility:^9.0"
composer global require codeception/codeception
```
### 3. Validate Everything Works
```bash
# Quick validation
make validate-required
# Or comprehensive validation
make validate
```
## Common Tasks
### Development Workflow
```bash
# 1. Make your changes
vim src/index.php
# 2. Validate locally
make validate-required
# 3. Check code quality
make quality
# 4. Commit
git add -A
git commit -m "feat: add new feature"
# (pre-commit hook runs automatically)
# 5. Push
git push origin your-branch
```
### Testing
```bash
# Run all tests
make test
# Run unit tests only
make test-unit
# Run acceptance tests only
make test-acceptance
```
### Code Quality
```bash
# Check everything
make quality
# PHP CodeSniffer only
make phpcs
# Auto-fix PHPCS issues
make phpcs-fix
# PHPStan only
make phpstan
# PHP compatibility check
make phpcompat
```
### Creating a Release Package
```bash
# Package with auto-detected version
make package
# Check package contents
ls -lh dist/
unzip -l dist/mokoonyx-*.zip
```
## VS Code Integration
If using VS Code, press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) and type "Run Task" to see available tasks:
- **Validate All** - Run all validation scripts (default test task)
- **Validate Required** - Run only required validations
- **PHP CodeSniffer** - Check code style
- **PHP CodeSniffer - Auto Fix** - Fix code style issues
- **PHPStan** - Static analysis
- **Run Tests** - Execute all tests
- **Create Package** - Build distribution ZIP
- **Install Git Hooks** - Set up pre-commit hooks
## Available Make Commands
Run `make help` to see all available commands:
```bash
make help # Show all commands
make dev-setup # Complete environment setup
make validate # Run all validations
make test # Run all tests
make quality # Check code quality
make package # Create distribution package
make clean # Remove generated files
make check # Quick check (validate + quality)
make all # Complete build pipeline
```
## Project Structure
```
moko-cassiopeia/
├── src/ # Joomla template source (template root)
│ ├── component.php # Component template file
│ ├── index.php # Main template file
│ ├── offline.php # Offline page template
│ ├── error.php # Error page template
│ ├── templateDetails.xml # Template manifest
│ ├── html/ # Module & component overrides
│ ├── media/ # Assets (CSS, JS, images, fonts)
│ ├── language/ # Frontend language files (en-GB, en-US)
│ └── administrator/ # Backend files
│ └── language/ # Backend language files
├── tests/ # Test suites
├── docs/ # Documentation
├── scripts/ # Build scripts
├── .github/workflows/ # CI/CD workflows
├── Makefile # Make commands
└── README.md # Project overview
```
## Next Steps
### Learning the Workflow
1. **Read the Workflow Guide**: [docs/WORKFLOW_GUIDE.md](./WORKFLOW_GUIDE.md)
2. **Review Joomla Development**: [docs/JOOMLA_DEVELOPMENT.md](./JOOMLA_DEVELOPMENT.md)
### Creating Your First Feature
1. **Create a version branch** via GitHub Actions:
- Go to Actions → Create version branch
- Enter version (e.g., 03.06.00)
- Select branch prefix: `dev/`
- Run workflow
2. **Checkout the branch**:
```bash
git fetch origin
git checkout dev/03.06.00
```
3. **Make changes and test**:
```bash
# Edit files
vim src/index.php
# Validate
make validate-required
# Check quality
make quality
```
4. **Commit and push**:
```bash
git add -A
git commit -m "feat: your feature description"
git push origin dev/03.06.00
```
5. **Watch CI**: Check GitHub Actions for automated testing
### Understanding the Release Process
```
Development → RC → Stable → Production
(dev/) (rc/) (version/) (main)
```
1. **dev/X.Y.Z** - Active development
2. **rc/X.Y.Z** - Release candidate testing
3. **version/X.Y.Z** - Stable release
4. **main** - Production (auto-merged from version/)
Use the Release Pipeline workflow to promote between stages.
## Troubleshooting
### Scripts Not Executable
```bash
make fix-permissions
### PHPStan/PHPCS Not Found
```bash
make install
# Or manually:
composer global require "squizlabs/php_codesniffer:^3.0" phpstan/phpstan
```
### CI Workflow Fails
1. Check the workflow logs in GitHub Actions
2. Run validation locally:
```bash
make validate-required
make quality
```
### Need Help?
- **Documentation**: Check [docs/](../docs/) directory
- **Issues**: Open an issue on GitHub
- **Contributing**: See [CONTRIBUTING.md](../CONTRIBUTING.md)
## Best Practices
### Before Committing
```bash
# Always validate first
make validate-required
# Check quality for PHP changes
make quality
# Run tests if you changed functionality
make test
```
### Code Style
- Follow PSR-12 standards
- Use `make phpcs-fix` to auto-fix issues
- Add SPDX license headers to new files
- Keep functions small and focused
### Documentation
- Update docs when changing workflows
- Add comments for complex logic
- Update CHANGELOG.md with changes
- Keep README.md current
### Version Management
- Use semantic versioning: Major.Minor.Patch (03.06.00)
- Update CHANGELOG.md with all changes
- Follow the version hierarchy: dev → rc → version → main
- Never skip stages in the release process
## Useful Resources
- [Joomla Documentation](https://docs.joomla.org/)
- [PSR-12 Coding Standard](https://www.php-fig.org/psr/psr-12/)
- [Semantic Versioning](https://semver.org/)
- [Conventional Commits](https://www.conventionalcommits.org/)
## Quick Reference Card
```bash
# Setup
make dev-setup # Initial setup
# Development
make validate-required # Quick validation
make quality # Code quality
make test # Run tests
# Building
make package # Create ZIP
# Maintenance
make clean # Clean generated files
make fix-permissions # Fix script permissions
# Help
make help # Show all commands
```
---
## Metadata
* Document: docs/QUICK_START.md
* Repository: [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* Path: /docs/QUICK_START.md
* Owner: Moko Consulting
* Version: 03.06.03
* Status: Active
* Effective Date: 2026-01-30
* Classification: Public Open Source Documentation
## Revision History
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------- | --------------- |
| 2026-01-30 | Updated metadata to MokoStandards format | GitHub Copilot |
| 2025-01-04 | Initial quick start guide created | GitHub Copilot |

188
docs/README.md Normal file
View File

@@ -0,0 +1,188 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: docs/README.md
VERSION: 03.09.03
BRIEF: Documentation index for MokoOnyx template
PATH: /docs/README.md
-->
# MokoOnyx Documentation
This directory contains comprehensive documentation for the MokoOnyx Joomla template.
## Documentation Overview
### Developer Documentation
* **[Quick Start Guide](QUICK_START.md)** - Get up and running in 5 minutes
* Development environment setup
* Essential commands and workflows
* First-time contributor guide
* **[Workflow Guide](WORKFLOW_GUIDE.md)** - Complete workflow reference
* Git branching strategy
* Development workflow
* Pull request guidelines
* **[Release Process](RELEASE_PROCESS.md)** ⭐ - Complete release documentation
* Automated release workflow with GitHub Actions
* Manual release procedures
* Update server configuration
* Testing and rollback procedures
* Build scripts and tools
* **[Joomla Development Guide](JOOMLA_DEVELOPMENT.md)** - Joomla-specific development
* Testing with Codeception
* PHP quality checks (PHPStan, PHPCS)
* Joomla extension packaging
* Multi-version testing
* **[Manual Deployment Guide](MANUAL_DEPLOYMENT.md)** - Deploy src directory without building
* Understanding src vs. installed structure
* Manual deployment methods (copy, symlink)
* Troubleshooting language files and media
* Best practices for development deployments
* **[CSS Variables Reference](CSS_VARIABLES.md)** - Complete CSS customization guide
* All available CSS variables
* Custom color palette creation
* Usage examples and tips
* Light and dark mode theming
* **[Module & Component Overrides](MODULE_OVERRIDES.md)** - Mobile-responsive overrides guide
* 16 module overrides + 12 component overrides
* VirtueMart, Community Builder, JEM, Kunena, industry extensions
* Mobile-first responsive design patterns
* Accessibility features and customization
* **[Override Philosophy](OVERRIDE_PHILOSOPHY.md)** ⭐ - Alternative layouts, not replacements
* Why overrides use `mobile.php` naming instead of `default.php`
* How to activate alternative layouts in Joomla
* Benefits of non-replacing overrides
* Developer guidelines and best practices
* **[Roadmap](ROADMAP.md)** - Version-specific roadmap
* Current features (v03.07.00)
* Feature evolution timeline
* Planned enhancements
* Development priorities
### User Documentation
For end-user documentation, installation instructions, and feature guides, see the main [README.md](../README.md) in the repository root.
## Project Structure
```
moko-cassiopeia/
├── docs/ # Documentation (you are here)
│ ├── README.md # This file - documentation index
│ ├── QUICK_START.md # Quick start guide for developers
│ ├── WORKFLOW_GUIDE.md # Development workflow guide
│ ├── JOOMLA_DEVELOPMENT.md # Joomla-specific development guide
│ ├── CSS_VARIABLES.md # CSS variables reference
│ ├── MODULE_OVERRIDES.md # Module & component overrides guide
│ └── ROADMAP.md # Version-specific roadmap
├── src/ # Template source code (Joomla template root)
│ ├── component.php # Component template
│ ├── index.php # Main template file
│ ├── offline.php # Offline template
│ ├── error.php # Error page template
│ ├── templateDetails.xml # Template manifest
│ ├── html/ # Module & component overrides (16 modules, 12 components)
│ ├── media/ # Assets (CSS, JS, images, fonts)
│ │ ├── css/ # Stylesheets
│ │ │ └── colors/ # Color schemes
│ │ │ ├── light/ # Light mode color files (colors_standard.css)
│ │ │ └── dark/ # Dark mode color files (colors_standard.css)
│ │ ├── js/ # JavaScript files
│ │ ├── images/ # Image assets
│ │ └── fonts/ # Font files
│ ├── language/ # Frontend language files
│ │ ├── en-GB/ # English (UK) translations
│ │ └── en-US/ # English (US) translations
│ └── administrator/ # Backend files
│ └── language/ # Backend language files
│ ├── en-GB/ # English (UK) system translations
│ └── en-US/ # English (US) system translations
├── templates/ # Reserved for future template files
│ └── README.md # Templates directory guide
├── scripts/ # Build and utility scripts
├── tests/ # Automated tests
└── .github/ # GitHub configuration and workflows
```
## Contributing
Before contributing, please read:
1. **[CONTRIBUTING.md](../CONTRIBUTING.md)** - Contribution guidelines and standards
2. **[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)** - Community standards and expectations
3. **[SECURITY.md](../SECURITY.md)** - Security policy and reporting procedures
## Standards Compliance
This project adheres to [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards) for:
* Coding standards and formatting
* Documentation requirements
* Git workflow and branching
* CI/CD pipeline configuration
* Security scanning and dependency management
## Additional Resources
* **Repository**: [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
* **Changelog**: [CHANGELOG.md](../CHANGELOG.md)
* **License**: [GPL-3.0-or-later](../LICENSE)
## Support
* **Email**: hello@mokoconsulting.tech
* **Website**: https://mokoconsulting.tech/support/joomla-cms/moko-cassiopeia-roadmap
---
## Metadata
* Document: docs/README.md
* Repository: [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* Path: /docs/README.md
* Owner: Moko Consulting
* Version: 03.07.00
* Status: Active
* Effective Date: 2026-01-30
* Classification: Public Open Source Documentation
## Revision History
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------- | --------------- |
| 2026-02-22 | Added MODULE_OVERRIDES.md reference, updated version to 03.07.00 | GitHub Copilot |
| 2026-01-30 | Added CSS Variables reference, updated version to 03.06.03 | GitHub Copilot |
| 2026-01-09 | Initial documentation index created for MokoStandards compliance. | GitHub Copilot |
| 2026-01-27 | Updated with roadmap link and version to 03.05.01. | GitHub Copilot |

638
docs/RELEASE_PROCESS.md Normal file
View File

@@ -0,0 +1,638 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: docs/RELEASE_PROCESS.md
VERSION: 03.09.03
BRIEF: Complete release process documentation for MokoOnyx
PATH: /docs/RELEASE_PROCESS.md
-->
# Release Process — MokoOnyx
This document describes the complete release process for MokoOnyx Joomla template, including automated workflows and manual procedures.
## Table of Contents
1. [Overview](#overview)
2. [Release Types](#release-types)
3. [Automated Release Process](#automated-release-process)
4. [Manual Release Process](#manual-release-process)
5. [Update Server Configuration](#update-server-configuration)
6. [Testing Releases](#testing-releases)
7. [Rollback Procedures](#rollback-procedures)
8. [Troubleshooting](#troubleshooting)
---
## Overview
MokoOnyx uses an automated release system powered by GitHub Actions. The system:
- **Builds** installation packages automatically
- **Generates** checksums for security verification
- **Creates** GitHub Releases with downloadable artifacts
- **Updates** the Joomla update server (`updates.xml`) automatically
- **Validates** package integrity with SHA-256 hashes
### Key Components
1. **Release Workflow** (`.github/workflows/release.yml`): Builds and publishes releases
2. **Auto-Update SHA** (`.github/workflows/auto-update-sha.yml`): Updates `updates.xml` after release
3. **Build Script** (`scripts/build-release.sh`): Local development builds
4. **Update Server** (`updates.xml`): Joomla update server manifest
---
## Release Types
### Patch Release (Third Digit)
**Format**: `XX.XX.XX``XX.XX.XX+1` (e.g., `03.08.03``03.08.04`)
**When to use**:
- Bug fixes
- Security patches
- Documentation updates
- Minor CSS/styling tweaks
- No breaking changes
**Example**: `03.08.03``03.08.04`
### Minor Release (Second Digit)
**Format**: `XX.XX.00``XX.XX+1.00` (e.g., `03.08.03``03.09.00`)
**When to use**:
- New features
- New module/component overrides
- Significant styling changes
- Backward-compatible changes
**Example**: `03.08.03``03.09.00`
### Major Release (First Digit)
**Format**: `XX.00.00``XX+1.00.00` (e.g., `03.08.03``04.00.00`)
**When to use**:
- Breaking changes
- Major architecture changes
- Joomla version upgrades
- Complete redesigns
**Example**: `03.08.03``04.00.00`
---
## Automated Release Process
**Recommended for most releases**
### Prerequisites
- [ ] All changes merged to `main` branch
- [ ] Tests passing
- [ ] Documentation updated
- [ ] CHANGELOG.md updated
- [ ] Local testing completed
### Step 1: Prepare Release Branch
```bash
# Create release branch
git checkout main
git pull
git checkout -b release/03.08.04
# Update version in templateDetails.xml
# Edit: src/templateDetails.xml
# Change: <version>03.08.03</version>
# To: <version>03.08.04</version>
# Update CHANGELOG.md
# Add new section:
## [03.08.04] - 2026-02-27
### Added
- Feature descriptions
### Fixed
- Bug fix descriptions
### Changed
- Change descriptions
# Commit changes
git add src/templateDetails.xml CHANGELOG.md
git commit -m "chore: Prepare release 03.08.04"
git push origin release/03.08.04
```
### Step 2: Create Pull Request
1. Go to GitHub repository
2. Click "Pull requests" → "New pull request"
3. Base: `main`, Compare: `release/03.08.04`
4. Title: `Release 03.08.04`
5. Description: Copy relevant CHANGELOG entries
6. Create pull request
7. Review and merge
### Step 3: Create and Push Tag
```bash
# Switch to main and pull changes
git checkout main
git pull
# Create tag
git tag 03.08.04
# Push tag (triggers release workflow)
git push origin 03.08.04
```
### Step 4: Monitor Automated Process
1. **Go to GitHub Actions tab**
2. **Watch "Create Release" workflow**:
- Builds package
- Generates checksums
- Creates GitHub Release
- Uploads artifacts
3. **Watch "Auto-Update SHA Hash" workflow**:
- Downloads release package
- Calculates SHA-256 hash
- Updates `updates.xml`
- Commits to main branch
### Step 5: Verify Release
1. **Check GitHub Release**:
- Go to Releases tab
- Verify release `03.08.04` exists
- Download ZIP package
- Verify checksums match
2. **Check updates.xml**:
```bash
git pull
cat updates.xml
```
- Verify version is `03.08.04`
- Verify download URL is correct
- Verify SHA-256 hash is present
3. **Test Joomla Update**:
- Install previous version in Joomla
- Go to Extensions → Update
- Verify update is detected
- Perform update
- Verify template works correctly
---
## Manual Release Process
**Use when automation fails or for local testing**
### Step 1: Prepare Repository
```bash
# Update version numbers
# Edit: src/templateDetails.xml
# Edit: CHANGELOG.md
# Commit changes
git add src/templateDetails.xml CHANGELOG.md
git commit -m "chore: Prepare release 03.08.04"
git push
```
### Step 2: Build Package Locally
```bash
# Run build script
./scripts/build-release.sh 03.08.04
# Output will be in build/ directory:
# - mokoonyx-src-03.08.04.zip
# - mokoonyx-src-03.08.04.zip.sha256
# - mokoonyx-src-03.08.04.zip.md5
```
### Step 3: Test Package
```bash
# Install in Joomla test environment
# Extensions → Manage → Install → Upload Package File
# Select: build/mokoonyx-src-03.08.04.zip
# Test all features:
# - Template displays correctly
# - Module overrides work
# - Alternative layouts selectable
# - Dark mode works
# - No JavaScript errors
```
### Step 4: Create GitHub Release
1. **Go to GitHub Releases**
2. **Click "Create a new release"**
3. **Tag**: `03.08.04` (create new tag)
4. **Release title**: `Release 03.08.04`
5. **Description**: Copy from CHANGELOG.md
6. **Upload files**:
- `mokoonyx-src-03.08.04.zip`
- `mokoonyx-src-03.08.04.zip.sha256`
- `mokoonyx-src-03.08.04.zip.md5`
7. **Publish release**
### Step 5: Update updates.xml Manually
```bash
# Extract SHA-256 hash
cat build/mokoonyx-src-03.08.04.zip.sha256
# Example output: a1b2c3d4e5f6...
# Edit updates.xml
# Update <version>03.08.04</version>
# Update <creationDate>2026-02-27</creationDate>
# Update <downloadurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/03.08.04/mokoonyx-src-03.08.04.zip</downloadurl>
# Update <sha256>sha256:a1b2c3d4e5f6...</sha256>
# Commit and push
git add updates.xml
git commit -m "chore: Update updates.xml for release 03.08.04"
git push
```
---
## Update Server Configuration
### updates.xml Structure
```xml
<updates>
<update>
<name>MokoOnyx</name>
<description>Moko Consulting's site template based on Cassiopeia.</description>
<element>mokoonyx</element>
<type>template</type>
<client>site</client>
<version>03.08.04</version>
<creationDate>2026-02-27</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<infourl title='MokoOnyx'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/03.08.04/mokoonyx-src-03.08.04.zip</downloadurl>
<sha256>sha256:a1b2c3d4e5f6...</sha256>
</downloads>
<tags>
<tag>stable</tag>
</tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://www.mokoconsulting.tech</maintainerurl>
<targetplatform name='joomla' version='5.*'/>
</update>
</updates>
```
### Hosting Update Server
The `updates.xml` file is hosted directly on GitHub:
**URL**: `https://raw.githubusercontent.com/mokoconsulting-tech/MokoOnyx/main/updates.xml`
This URL is configured in `src/templateDetails.xml`:
```xml
<updateservers>
<server type="extension" name="MokoOnyx Updates" priority="1">
https://raw.githubusercontent.com/mokoconsulting-tech/MokoOnyx/main/updates.xml
</server>
</updateservers>
```
---
## Testing Releases
### Pre-Release Testing
```bash
# 1. Build package locally
./scripts/build-release.sh
# 2. Set up Joomla test environment
# - Clean Joomla 5.x installation
# - Previous MokoOnyx version installed
# 3. Test current version features
# - All module overrides
# - Alternative layouts
# - Dark mode toggle
# - Responsive behavior
# 4. Install new package
# Extensions → Manage → Install → Upload Package
# 5. Verify upgrade process
# - No errors during installation
# - Settings preserved
# - Custom modifications retained
# 6. Test new features
# - New functionality works
# - Bug fixes applied
# - No regressions
```
### Update Server Testing
```bash
# 1. Install previous version in Joomla
# 2. Go to: Extensions → Update
# 3. Click "Find Updates"
# 4. Verify update shows: "MokoOnyx 03.08.04"
# 5. Click "Update"
# 6. Verify successful update
# 7. Test template functionality
```
### Checklist
- [ ] Package installs without errors
- [ ] Template activates correctly
- [ ] All module overrides work
- [ ] Alternative layouts selectable
- [ ] Dark mode functions
- [ ] Responsive on mobile/tablet/desktop
- [ ] No JavaScript console errors
- [ ] No PHP errors in Joomla logs
- [ ] Update server detects new version
- [ ] Update process completes successfully
---
## Rollback Procedures
### Rollback Release
If a release has critical issues:
1. **Delete GitHub Release**:
- Go to Releases
- Click release to delete
- Click "Delete"
- Confirm deletion
2. **Delete Git Tag**:
```bash
# Delete local tag
git tag -d 03.08.04
# Delete remote tag
git push --delete origin 03.08.04
```
3. **Revert updates.xml**:
```bash
# Revert to previous version
git revert <commit-hash-of-auto-update>
git push
```
4. **Notify Users**:
- Create GitHub issue explaining the problem
- Pin the issue
- Provide rollback instructions for users
### User Rollback Instructions
For users who installed the problematic version:
1. **Download previous version** from GitHub Releases
2. **Uninstall current version**:
- Extensions → Manage → Manage
- Find MokoOnyx
- Click "Uninstall"
3. **Install previous version**:
- Extensions → Manage → Install
- Upload previous version ZIP
4. **Verify functionality**
---
## Troubleshooting
### Release Workflow Fails
**Problem**: Build fails with "rsync: command not found"
**Solution**: The GitHub Actions runner always has rsync installed. If this occurs, check the workflow file syntax.
**Problem**: ZIP creation fails
**Solution**: Check that `src/` and `src/media/` directories exist and contain files.
**Problem**: Version update fails
**Solution**: Verify `sed` commands in workflow match actual XML structure.
### Auto-Update SHA Fails
**Problem**: Cannot download release package
**Solution**:
- Verify release was published (not draft)
- Check package naming: `mokoonyx-src-{version}.zip`
- Verify release tag format
**Problem**: SHA-256 hash mismatch
**Solution**:
- Package may have been modified after calculation
- Re-run the workflow manually
- Verify package integrity
**Problem**: Commit fails
**Solution**:
- Check workflow has write permissions
- Verify no branch protection rules blocking bot commits
### Manual Build Issues
**Problem**: `./scripts/build-release.sh: Permission denied`
**Solution**:
```bash
chmod +x scripts/build-release.sh
./scripts/build-release.sh
```
**Problem**: Build directory exists
**Solution**:
```bash
rm -rf build/
./scripts/build-release.sh
```
### Update Server Issues
**Problem**: Joomla doesn't detect update
**Solution**:
1. Check `updates.xml` is accessible:
```bash
curl https://raw.githubusercontent.com/mokoconsulting-tech/MokoOnyx/main/updates.xml
```
2. Verify version number is higher than installed version
3. Clear Joomla cache: System → Clear Cache
4. Check update URL in templateDetails.xml
**Problem**: Update fails with "Invalid package"
**Solution**:
- Verify SHA-256 hash matches
- Re-download package and check integrity
- Verify package structure is correct
---
## Best Practices
### Version Numbering
- **Always increment** version numbers sequentially
- **Never reuse** version numbers
- **Use consistent** format: `XX.XX.XX`
### Changelog
- **Update before** release
- **Include all changes** since last version
- **Categorize** changes: Added, Changed, Fixed, Removed
- **Write clear descriptions** for users
### Testing
- **Test locally** before pushing tag
- **Test update process** from previous version
- **Test on clean** Joomla installation
- **Test different** configurations
### Communication
- **Announce releases** on GitHub Discussions
- **Document breaking changes** clearly
- **Provide migration guides** for major changes
- **Respond promptly** to issue reports
---
## Quick Reference
### Automated Release Commands
```bash
# 1. Create release branch
git checkout -b release/03.08.04
# 2. Update version and CHANGELOG
# (edit files)
# 3. Commit and push
git add .
git commit -m "chore: Prepare release 03.08.04"
git push origin release/03.08.04
# 4. Create and merge PR (via GitHub UI)
# 5. Create and push tag
git checkout main
git pull
git tag 03.08.04
git push origin 03.08.04
# 6. Wait for automation to complete
```
### Manual Release Commands
```bash
# Build locally
./scripts/build-release.sh 03.08.04
# Test installation
# (manual Joomla testing)
# Create release on GitHub
# (via GitHub UI)
# Update updates.xml
# (edit file with SHA-256)
git add updates.xml
git commit -m "chore: Update updates.xml for 03.08.04"
git push
```
---
## Related Documentation
- **Build Scripts**: [scripts/README.md](../scripts/README.md)
- **Workflow Guide**: [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md)
- **Contributing**: [CONTRIBUTING.md](../CONTRIBUTING.md)
- **Changelog**: [CHANGELOG.md](../CHANGELOG.md)
---
## Support
- **Issues**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Discussions**: [GitHub Discussions](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/discussions)
- **Email**: hello@mokoconsulting.tech
---
## License
Copyright (C) 2026 Moko Consulting
This documentation is licensed under GPL-3.0-or-later.

917
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,917 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia
FILE: docs/ROADMAP.md
VERSION: 03.09.03
BRIEF: Version-specific roadmap for MokoOnyx template
PATH: /docs/ROADMAP.md
-->
# MokoOnyx Roadmap (VERSION: 03.09.03)
This document provides a comprehensive, version-specific roadmap for the MokoOnyx Joomla template, tracking feature evolution, current capabilities, and planned enhancements.
## Table of Contents
- [Version Timeline](#version-timeline)
- [Past Releases](#past-releases)
- [Future Roadmap (5-Year Plan)](#future-roadmap-5-year-plan)
- [Current Release (v03.06.03)](#current-release-v030603)
- [Implemented Features](#implemented-features)
- [Planned Features](#planned-features)
- [Development Priorities](#development-priorities)
- [Long-term Vision](#long-term-vision)
- [External Resources](#external-resources)
---
## Version Timeline
### Past Releases
### v03.05.01 (2026-01-09) - Standards & Security
**Status**: Released (CHANGELOG entry exists, code files pending version update)
**Added**:
- Dependency review workflow for vulnerability scanning
- Standards compliance workflow for MokoStandards validation
- Dependabot configuration for automated security updates
- Documentation index (`docs/README.md`)
**Changed**:
- Removed custom CodeQL workflow (using GitHub's default setup)
- Enforced repository compliance with MokoStandards
- Improved security posture with automated scanning
### v03.06.00 (2026-01-28) - Version Update
**Status**: Current Release (in code)
**Changed**:
- Updated version to 03.06.00 across all files
### v03.05.00 (2026-01-04) - Workflow & Governance
**Status**: Mentioned in CHANGELOG (v03.05.00)
**Added**:
- `.github/workflows` directory structure
- CODE_OF_CONDUCT.md from MokoStandards
- CONTRIBUTING.md from MokoStandards
**Changed**:
- TODO items to be split to separate file (tracked)
### v03.01.00 (2025-12-16) - CI/CD Foundation
**Added**:
- Initial GitHub Actions workflows
### v03.00.00 (2025-12-09) - Font Awesome 7 Upgrade
**Updated**:
- Copyright headers to MokoCodingDefaults standards
- Fixed color style injection in `index.php`
- Upgraded Font Awesome 6 to Font Awesome 7 Free
- Added Font Awesome 7 Free style fallback
**Removed**:
- Deprecated CODE_OF_CONDUCT.md
- Deprecated CONTRIBUTING.md
### v02.01.05 (2025-09-04) - CSS Refinement
**Fixed**:
- Removed vmbasic.css
- Repaired template.css and colors_standard.css
### v02.00.00 (2025-08-30) - Dark Mode & TOC
**Major Features**:
- **Dark Mode Toggle System**
- Frontend toggle switch with localStorage persistence
- Admin-configurable default mode
- CSS rules for light/dark themes
- JavaScript-powered mode switching
- **Enhanced Template Parameters**
- Logo parameter support
- GTM container ID configuration
- Dark mode defaults in settings
- Updated metadata and copyright headers
- **Expanded Table of Contents**
- Automatic TOC injection
- User-selectable placement (`toc-left` or `toc-right`)
- Article options integration
**Improvements**:
- Cleaned up `index.php` (removed duplicate skip-to-content calls)
- Consolidated JavaScript asset loading
- Streamlined CSS for toggle switch
- Accessibility refinements (typography, color contrast)
- Fixed missing logo parameter in header
- Corrected stylesheet inconsistencies
- Patched redundant script includes
### v01.00.00 - Initial Public Release
**Core Features**:
- Font Awesome 6 integration
- Bootstrap 5 helpers and utilities
- Automatic Table of Contents (TOC) utility
- Moko Expansions: Google Tag Manager / GA4 hooks
- Built on Joomla's Cassiopeia template
---
### Future Roadmap (5-Year Plan)
The following versions represent our planned annual major releases, each building upon the previous version's foundation.
#### v04.00.00 (Q4 2027) - Enhanced Accessibility & Performance
**Status**: Planned
**Target Release**: December 2027
**Major Template Features**:
- **WCAG 2.1 AA Compliance**
- Full accessibility audit and remediation
- High-contrast theme options
- Screen reader optimizations
- Keyboard navigation enhancements
- ARIA landmark improvements
- Skip navigation enhancements
- **Template Performance Optimizations**
- Critical CSS inlining for faster first paint
- Lazy loading for images and below-fold content
- WebP image support with automatic fallbacks
- Advanced asset bundling and minification
- Template asset caching (CSS/JS bundles)
- **Enhanced Layout System**
- Additional responsive grid layouts
- Flexible module position system
- Column layout presets (2-col, 3-col, 4-col variations)
- Grid/masonry article layouts
- Sticky sidebar options
- **Typography Enhancements**
- Advanced typography controls in template settings
- Additional font pairing presets
- Custom font upload support
- Line height and letter spacing controls
- Responsive typography scaling
- **Developer Experience**
- Development mode enablement (unminified assets, debug output)
- Live reload during development
- Enhanced error logging and diagnostics
- Template debugging tools
- Style guide generator
- **Content Display Features**
- Soft offline mode (category-based access during maintenance)
- Enhanced article layouts (grid, masonry, timeline)
- Image caption styling options
- Quote block styling variations
- Enhanced breadcrumb customization
**Template Infrastructure**:
- Expanded template parameter validation
- Enhanced template override detection
- Automated template compatibility testing
- Template performance profiling tools
---
#### v05.00.00 (Q4 2028) - Advanced Layouts & Template Customization
**Status**: Planned
**Target Release**: December 2028
**Major Template Features**:
- **Enhanced Layout Builder**
- Template-based page layout variations
- Configurable layout options via template parameters
- Layout presets library (blog, portfolio, business, magazine)
- Module position layout manager
- Visual layout preview in admin
- **Advanced Styling System**
- Extended color palette management (unlimited custom palettes)
- CSS variable editor in template settings
- Style presets for different site types
- Border radius and spacing controls
- Box shadow and effect controls
- **Template Component Enhancements**
- Enhanced menu styling options (mega menu support)
- Advanced header variations (transparent, sticky, minimal)
- Footer layout options (column variations, widgets)
- Sidebar styling and behavior options
- Hero section templates and variations
- **Content Display Options**
- Article intro/full text display controls
- Category layout variations (grid, list, masonry, cards)
- Featured content sections
- Related articles display options
- Author bio box styling
- **Responsive Design Improvements**
- Mobile-first navigation patterns
- Tablet-specific layout controls
- Responsive image sizing options
- Mobile header variations
- Touch-friendly interface elements
- **Template Integration Features**
- Enhanced VirtueMart template overrides
- Contact form styling variations
- Search result layout options
- Error page customization
- Archive page templates
**Template Infrastructure**:
- Joomla 6.x template compatibility (if released)
- PHP 8.2+ support
- Template child theme support
- Template preset import/export functionality
---
#### v06.00.00 (Q4 2029) - Template Extensions & Advanced Features
**Status**: Planned
**Target Release**: December 2029
**Major Template Features**:
- **Template Marketplace & Extensions**
- Template addon system for modular features
- Community-contributed template extensions
- Template preset marketplace
- Style pack distribution system
- Template component library
- **Advanced Module System**
- Custom module chrome options
- Module animation effects
- Module visibility controls (scroll, time-based)
- Module group management
- Module style inheritance
- **Enhanced Media Handling**
- Background image options per page/section
- Image overlay controls
- Parallax scrolling effects
- Video background support
- Gallery template variations
- **Template Branding Options**
- Multiple logo upload (standard, retina, mobile)
- Favicon and app icon management
- Custom loading screen/animations
- Watermark options
- Brand color scheme generator
- **Advanced Header/Footer**
- Multiple header layout presets
- Sticky header variations and behaviors
- Header transparency controls
- Footer widget areas expansion
- Floating action buttons
- **Content Enhancement Features**
- Reading progress indicator
- Social sharing buttons (template-integrated)
- Print-friendly styles
- Reading time estimation display
- Content table enhancements
- **Template SEO Features**
- Schema markup templates for common types
- Open Graph tag management
- Twitter Card support
- Breadcrumb schema integration
- Meta tag template controls
**Template Infrastructure**:
- Template versioning system
- Template backup/restore functionality
- Template A/B testing support
- Multi-language template variations
- Template documentation generator
---
#### v07.00.00 (Q4 2030) - Modern Template Standards & Enhancements
**Status**: Planned
**Target Release**: December 2030
**Major Template Features**:
- **Modern CSS Features**
- CSS Grid layout system integration
- CSS Container Queries support
- CSS Cascade Layers implementation (layered style priority system)
- Custom properties (CSS variables) UI
- Modern filter and backdrop effects
- **Progressive Template Features**
- Offline-capable template assets
- Service worker template integration
- App manifest generation
- Install to home screen support
- Template asset preloading strategies
- **Animation & Interaction**
- Scroll-triggered animations
- Hover effect library
- Page transition effects
- Micro-interactions for UI elements
- Loading animation options
- **Advanced Responsive Features**
- Container-based responsive design
- Element visibility by viewport
- Responsive navigation patterns library
- Mobile-optimized interactions
- Adaptive image loading
- **Template Accessibility Features**
- Focus indicators customization
- Reduced motion preferences support
- High contrast mode automation
- Keyboard navigation patterns
- ARIA live regions for dynamic content
- **Content Presentation**
- Advanced blockquote styles
- Code snippet highlighting themes
- Table styling variations
- List styling options
- Custom content block templates
- **Template Performance**
- Resource hints (preconnect, prefetch)
- Optimal asset delivery strategies
- Image format optimization (AVIF support)
- Font loading optimization
- Template metrics dashboard
**Template Infrastructure**:
- Template pattern library
- Design token system
- Template component documentation
- Automated template testing suite
- Template performance monitoring
---
#### v08.00.00 (Q4 2031) - Next-Generation Template Features
**Status**: Conceptual
**Target Release**: December 2031
**Major Template Features**:
- **Advanced Layout Systems**
- Subgrid support for complex layouts
- Multi-column layout variations
- Asymmetric grid systems
- Dynamic layout switching
- Layout constraint system
- **Enhanced Visual Customization**
- Real-time style editor
- Template style variations manager
- Custom CSS injection with validation
- Style inheritance and override system
- Visual design tokens editor
- **Template Component Library**
- Comprehensive UI component set
- Reusable template blocks
- Component variation system
- Template snippet library
- Pattern library integration
- **Advanced Typography System**
- Variable font support
- Advanced typographic scales
- Font pairing recommendations
- Fluid typography system
- Custom font fallback chains
- **Template Integration Features**
- Enhanced component overrides
- Template hooks system
- Event-based template modifications
- Custom field rendering templates
- Module position API enhancements
- **Responsive & Adaptive Design**
- Advanced breakpoint management
- Element-specific responsive controls
- Adaptive images with art direction
- Responsive typography system
- Context-aware component rendering
- **Template Ecosystem**
- Child template framework
- Template derivative system
- Community template marketplace
- Template rating and review system
- Professional template support network
- **Template Quality & Maintenance**
- Automated accessibility testing
- Template performance auditing
- Code quality monitoring
- Update notification system
- Template health dashboard
**Template Infrastructure**:
- Template API for extensibility
- Template package manager
- Template development CLI tools
- Template migration utilities
- Comprehensive template documentation system
---
## Current Release (v03.06.03)
### System Requirements
- **Joomla**: 4.4.x or 5.x
- **PHP**: 8.0+
- **Database**: MySQL/MariaDB compatible
### Architecture
- **Base Template**: Joomla Cassiopeia
- **Enhancement Layer**: Non-invasive overrides
- **Asset Management**: Joomla Web Asset Manager (WAM)
- **Frontend Framework**: Bootstrap 5
- **Icon Library**: Font Awesome 7 Free
---
## Implemented Features
### 🎨 Theming & Visual Design
#### Color Palette System
- **3 Built-in Palettes**: Standard, Alternative, Custom
- **Dual Mode Support**: Separate light and dark configurations
- **Custom Palettes**: User-definable via `colors_custom.css`
- **Location**: `src/media/css/colors/{light|dark}/`
#### Dark Mode System
- **Toggle Controls**: Switch (Light↔Dark) or Radios (Light/Dark/System)
- **Default Mode**: Admin-configurable (system, light, or dark)
- **Persistence**: localStorage for user preferences
- **Auto-Detection**: Optional system preference detection
- **Meta Tags**: `color-scheme` and `theme-color` support
- **ARIA Bridge**: Bootstrap ARIA compatibility
#### Typography
- **Font Schemes**:
- Local: Roboto
- Web (Google Fonts): Fira Sans, Roboto + Noto Sans
- **Admin-Configurable**: Template settings dropdown
#### Branding
- **Logo Support**: Custom logo upload
- **Site Title**: Text-based branding option
- **Site Description**: Tagline/subtitle field
- **Font Awesome Kit**: Optional custom kit integration
### 📐 Layout & Structure
#### Module Positions (23 Total)
**Header Area**:
- topbar, below-topbar, below-logo, menu, search, banner
**Content Area**:
- top-a, top-b, main-top, main-bottom, breadcrumbs
- sidebar-left, sidebar-right
**Footer Area**:
- bottom-a, bottom-b, footer-menu, footer
**Special**:
- debug, offline-header, offline, offline-footer
- drawer-left, drawer-right
#### Layout Options
- **Container Type**: Fluid or Static
- **Sticky Header**: Optional fixed navigation
- **Back-to-Top Button**: Scrollable page support
### 📝 Content Features
#### Table of Contents (TOC)
- **Automatic Generation**: From article headings
- **Placement Options**: `toc-left` or `toc-right` layouts
- **Article Integration**: Via Options → Layout dropdown
- **Responsive**: Mobile-friendly sidebar placement
#### Article Layouts
- **Default**: Standard Cassiopeia layout
- **TOC Variants**: Left-sidebar or right-sidebar TOC
- **Custom Overrides**: Located in `html/com_content/article/`
### 📊 Analytics & Tracking
#### Google Tag Manager (GTM)
- **Enable/Disable**: Admin toggle
- **Container ID**: Template parameter field
- **Implementation**: Head and body script injection
- **GDPR-Ready**: Configurable consent defaults
#### Google Analytics 4 (GA4)
- **Enable/Disable**: Admin toggle
- **Property ID**: Template parameter field
- **Universal Analytics Fallback**: Legacy UA support
- **Privacy-First**: Conditional loading based on settings
### 🎛️ Customization & Developer Tools
#### Custom Code Injection
- **Head Start**: Custom HTML/JS before `</head>`
- **Head End**: Custom HTML/JS at end of `<head>`
- **Raw HTML**: Unfiltered code injection for advanced users
#### Drawer System
- **Left/Right Drawers**: Offcanvas menu areas
- **Icon Customization**: Font Awesome icon selection
- **Default Icons**:
- Left: `fa-solid fa-chevron-right`
- Right: `fa-solid fa-chevron-left`
#### Asset Management
- **Joomla WAM**: Complete asset registry in `joomla.asset.json`
- **Development/Production Modes**: Minified and unminified assets
- **Dependency Management**: Automatic script/style loading
### 🏗️ Template Overrides
#### Component Overrides
**Content (com_content)**:
- Article layouts (default, toc-left, toc-right)
- Category layouts (blog, list)
- Featured articles
**Contact (com_contact)**:
- Contact form layouts
**Engage (com_engage)**:
- Comment system integration
#### Module Overrides
**Menu (mod_menu)**:
- Metis dropdown menu
- Offcanvas navigation
**VirtueMart**:
- Product display (`mod_virtuemart_product`)
- Shopping cart (`mod_virtuemart_cart`)
- Manufacturer display (`mod_virtuemart_manufacturer`)
- Category display (`mod_virtuemart_category`)
- Currency selector (`mod_virtuemart_currencies`)
**Other Modules**:
- Custom HTML (`mod_custom`)
- GABble social integration (`mod_gabble`)
**Membership System (OS Membership)**:
- Plan layouts (default, pricing tables)
- Member management interfaces
### 🔧 Configuration Parameters
#### Theme Tab
**General**:
- `theme_enabled` - Enable/disable theme system
- `theme_control_type` - Toggle UI type (switch/radios/none)
- `theme_default_choice` - Default mode (system/light/dark)
- `theme_auto_dark` - Auto-detect system preference
- `theme_meta_color_scheme` - Inject `color-scheme` meta tag
- `theme_meta_theme_color` - Inject `theme-color` meta tag
- `theme_bridge_bs_aria` - Bootstrap ARIA compatibility
**Variables & Palettes**:
- `colorLightName` - Light mode color scheme
- `colorDarkName` - Dark mode color scheme
**Typography**:
- `useFontScheme` - Font selection (local/web)
**Branding & Icons**:
- `brand` - Show/hide branding
- `logoFile` - Logo upload path
- `siteTitle` - Site title text
- `siteDescription` - Site tagline
- `fA6KitCode` - Font Awesome kit code
**Header & Navigation**:
- `stickyHeader` - Fixed navigation
- `backTop` - Back-to-top button
**Toggle UI**:
- `theme_fab_enabled` - Floating action button for theme toggle
- `theme_fab_pos` - FAB position (br/bl/tr/tl)
#### Google Tab
- `googletagmanager` - Enable GTM
- `googletagmanagerid` - GTM container ID
- `googleanalytics` - Enable GA4
- `googleanalyticsid` - GA4 property ID
#### Custom Code Tab
- `custom_head_start` - Custom code at head start
- `custom_head_end` - Custom code at head end
#### Drawers Tab
- `drawerLeftIcon` - Left drawer icon (Font Awesome class)
- `drawerRightIcon` - Right drawer icon (Font Awesome class)
#### Advanced Tab
- `fluidContainer` - Container layout (static/fluid)
### 🛠️ Development Tools
#### Quality Assurance
- **Codeception**: Automated testing framework
- **PHPStan**: Static analysis (level 8+)
- **PHPCS**: Code style validation (PSR-12)
- **PHPCompatibility**: PHP 8.0+ compatibility checks
#### CI/CD Workflows
- **Dependency Review**: Vulnerability scanning
- **Standards Compliance**: MokoStandards validation
- **CodeQL**: Security analysis (GitHub default)
- **Dependabot**: Automated dependency updates
#### Documentation
- **Quick Start**: 5-minute developer setup
- **Workflow Guide**: Git strategy, branching, releases
- **Joomla Development**: Testing, packaging, multi-version support
---
## Planned Features
### 🚧 In Development
#### Soft Offline Mode (v03.07.00 - Planned)
**Status**: Planned for v03.07.00
**Priority**: High
**Description**: Keep selected categories accessible during site maintenance mode with persistent links to essential pages
**Use Cases**:
- Legal documents remain viewable during downtime
- Policy pages accessible for compliance requirements
- Terms of service always available to users
- Privacy policy accessible at all times
- Essential public information during maintenance
**Technical Specifications**:
- **Configuration Method**: Template parameters in `templateDetails.xml`
- **Category Access**: Category IDs stored as comma-separated values
- **Persistent Links**: Direct article/menu item links always visible
- **Access Control**: Check in `offline.php` template file
- **Content Rendering**: Use Joomla's content component to fetch articles
- **Security**: Maintain proper access levels and permissions
**Implementation Plan**:
1. Add category selection field to template parameters
2. Add persistent link configuration (Terms of Service, Privacy Policy, etc.)
3. Modify `offline.php` to check for allowed categories
4. Add persistent link display in offline mode header/footer
5. Implement category content fetching during offline mode
6. Add styling for offline mode category display and persistent links
7. Test with various category and link configurations
8. Document admin configuration steps
**Configuration Interface**:
- **Category Field Type**: Category multiselect in template settings
- **Label**: "Categories Accessible During Offline Mode"
- **Default**: None (all content hidden by default)
- **Persistent Links**: Text fields for essential always-available links
- **Terms of Service URL**: Direct link to TOS article/page
- **Privacy Policy URL**: Direct link to privacy policy
- **Contact URL**: Optional contact page link
- **Custom Link 1-3**: Additional persistent links if needed
- **Admin Path**: System → Site Templates → MokoOnyx → Advanced → Offline Mode Settings
**Persistent Links Feature**:
- **Display Location**: Footer of offline page
- **Styling**: Clearly visible, accessible links
- **Format**: "Terms of Service | Privacy Policy | Contact"
- **Behavior**: Links bypass offline mode restrictions
- **Validation**: Check if URLs are valid Joomla routes
**Benefits**:
- ✅ Compliance: Keep legal pages accessible
- ✅ Transparency: Users can access essential information
- ✅ Flexibility: Admin control over which categories remain visible
- ✅ Security: Respects Joomla access levels
- ✅ Legal Protection: Terms of Service always accessible
- ✅ User Trust: Privacy policy always available
**Milestone**: Target release v03.07.00 (Q2 2026)
#### TODO Tracking System
**Status**: Mentioned in CHANGELOG (v03.05.00)
**Description**: Separate TODO tracking file
**Purpose**: Centralized issue and feature tracking outside changelog
### 🔮 Future Enhancements
#### Development Mode (Commented Out)
**Status**: Code exists but disabled
**Location**: `templateDetails.xml` line 91
**Description**: Comprehensive development mode toggle
**Potential Features**:
- Unminified asset loading
- Debug output
- Performance profiling
- Template cache bypass
#### Potential Features (Community Requested)
*Note: These are conceptual and not yet officially planned*
**Enhanced Accessibility**:
- WCAG 2.1 AAA compliance mode
- High-contrast themes
- Screen reader optimizations
- Keyboard navigation improvements
**Template Layout Features**:
- Advanced responsive grid layouts
- Multiple column variations
- Custom module position system
- Layout preset library
**Template Styling Features**:
- Extended color palette management
- Custom font upload support
- Typography scale controls
- Visual style editor
---
## Development Priorities
### Immediate Focus (v03.x - 2026)
1. **Bootstrap TOC Integration**: Complete and document v1.0.1 implementation ✅
2. **Soft Offline Mode**: Implement category-based offline access (Target: v03.07.00)
3. **TODO Tracking System**: Implement separate file for issue tracking
4. **Security Updates**: Maintain Dependabot and CodeQL scans
5. **Documentation**: Keep docs synchronized with features
6. **Bug Fixes**: Address reported issues and edge cases
### v04.00.00 Priorities (2027) - Template Foundation
1. **WCAG 2.1 AA Compliance**: Full template accessibility audit and implementation
2. **Template Performance**: Critical CSS, lazy loading, WebP support
3. **Layout System**: Enhanced responsive grid and module positions
4. **Development Mode**: Enable comprehensive template developer tools
### v05.00.00 Priorities (2028) - Template Customization
1. **Layout Builder**: Template-based page layout system
2. **Styling System**: Extended color palettes and CSS variable management
3. **Template Components**: Enhanced header, footer, and menu variations
4. **Responsive Design**: Mobile-first navigation and layout improvements
### v06.00.00 Priorities (2029) - Template Extensions
1. **Template Marketplace**: Addon system and community extensions
2. **Module System**: Advanced module chrome and animation options
3. **Media Handling**: Background images, parallax, video backgrounds
4. **Template SEO**: Schema markup templates and meta tag controls
### v07.00.00+ Priorities (2030+) - Modern Standards
1. **Modern CSS**: Grid, Container Queries, Cascade Layers
2. **Progressive Template**: Offline-capable assets and PWA features
3. **Animation System**: Scroll-triggered effects and micro-interactions
4. **Template Performance**: Advanced optimization and monitoring
---
## Long-term Vision
### Mission Statement
MokoOnyx aims to be the **most developer-friendly, user-customizable, and standards-compliant Joomla template** while maintaining minimal core overrides for maximum upgrade compatibility.
### Core Principles
1. **Non-Invasive**: Minimal Cassiopeia overrides
2. **Standards-First**: MokoStandards compliance
3. **Accessibility**: WCAG 2.1 compliance
4. **Performance**: Fast, optimized delivery
5. **Developer Experience**: Clear docs, easy setup, powerful tools
6. **Template-Focused**: Pure template features without complex external dependencies
### 5-Year Strategic Roadmap (Template Features)
#### 2027 (v04.00.00) - Accessibility & Performance
- Achieve WCAG 2.1 AA compliance for all template elements
- Implement critical template performance optimizations
- Enhance template layout system with flexible grids
- Enable comprehensive development mode for template developers
#### 2028 (v05.00.00) - Layouts & Customization
- Launch template-based layout builder system
- Deploy extended styling and customization options
- Enhance template component variations (headers, footers, menus)
- Improve responsive design patterns for all devices
#### 2029 (v06.00.00) - Extensions & Enhancements
- Introduce template addon and extension system
- Launch template preset marketplace
- Deploy advanced module styling and animation features
- Implement comprehensive template SEO controls
#### 2030 (v07.00.00) - Modern Standards
- Adopt modern CSS standards (Grid, Container Queries, Cascade Layers)
- Implement progressive template features (PWA support)
- Deploy advanced animation and interaction system
- Enhance template performance monitoring and optimization
#### 2031 (v08.00.00) - Next-Generation Template
- Advanced layout systems with subgrid support
- Comprehensive template component library
- Enhanced visual customization tools
- Template ecosystem with child themes and derivatives
---
## External Resources
### Official Links
- **Full Roadmap**: [https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap)
- **Repository**: [https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia)
- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia/issues)
- **Changelog**: [CHANGELOG.md](../CHANGELOG.md)
### Community
- **Email Support**: hello@mokoconsulting.tech
- **Contributing**: [CONTRIBUTING.md](../CONTRIBUTING.md)
- **Code of Conduct**: [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)
### Documentation
- **Quick Start**: [docs/QUICK_START.md](./QUICK_START.md)
- **Workflow Guide**: [docs/WORKFLOW_GUIDE.md](./WORKFLOW_GUIDE.md)
- **Joomla Development**: [docs/JOOMLA_DEVELOPMENT.md](./JOOMLA_DEVELOPMENT.md)
- **Main README**: [README.md](../README.md)
---
## Contributing to the Roadmap
Have ideas for future features? We welcome community input!
**How to Suggest Features**:
1. Check the [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia/issues) for existing requests
2. Open a new issue with the `enhancement` label
3. Provide clear use cases and benefits
4. Engage in community discussion
**Feature Evaluation Criteria**:
- Alignment with core principles
- User demand and use cases
- Technical feasibility
- Maintenance burden
- Performance impact
- Security implications
---
## Metadata
* Document: docs/ROADMAP.md
* Repository: [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* Path: /docs/ROADMAP.md
* Owner: Moko Consulting
* Version: 03.06.03
* Status: Active
* Effective Date: 2026-01-30
* Classification: Public Open Source Documentation
## Revision History
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------- | --------------- |
| 2026-01-27 | Initial version-specific roadmap generated from codebase scan. | GitHub Copilot |
| 2026-01-27 | Added 5-year future roadmap with annual major version releases (v04-v08). | GitHub Copilot |
| 2026-01-27 | Refocused roadmap to concentrate on template-oriented features only. | GitHub Copilot |

459
docs/WORKFLOW_GUIDE.md Normal file
View File

@@ -0,0 +1,459 @@
# Workflow Guide - Moko Cassiopeia
Quick reference guide for GitHub Actions workflows and common development tasks.
## Table of Contents
- [Overview](#overview)
- [Workflow Quick Reference](#workflow-quick-reference)
- [Common Development Tasks](#common-development-tasks)
- [Troubleshooting](#troubleshooting)
- [Best Practices](#best-practices)
## Overview
This repository uses GitHub Actions for continuous integration, testing, quality checks, and deployment. All workflows are located in `.github/workflows/`.
### Workflow Execution Model
```
┌─────────────────┐
│ Code Changes │
└────────┬────────┘
┌─────────────────┐
│ CI Pipeline │ ← Validation, Testing, Quality
└────────┬────────┘
┌─────────────────┐
│ Version Branch │ ← Create dev/X.Y.Z branch
└────────┬────────┘
┌─────────────────┐
│ Release Pipeline│ ← dev → rc → version → main
└────────┬────────┘
┌─────────────────┐
│ Distribution │ ← ZIP package + GitHub Release
└─────────────────┘
```
## Workflow Quick Reference
### Continuous Integration (ci.yml)
**Trigger:** Automatic on push/PR to main, dev/*, rc/*, version/* branches
**Purpose:** Validates code quality and repository structure
**What it does:**
- ✅ Validates Joomla manifest XML
- ✅ Checks XML well-formedness
- ✅ Validates GitHub Actions workflows
- ✅ Runs PHP syntax checks
- ✅ Checks for secrets in code
- ✅ Validates license headers
- ✅ Verifies version alignment
**When to check:** After every commit
**How to view results:**
```bash
# Via GitHub CLI
gh run list --workflow=ci.yml --limit 5
gh run view <run-id> --log
```
### PHP Quality Checks (php_quality.yml)
**Trigger:** Automatic on push/PR to main, dev/*, rc/*, version/* branches
**Purpose:** Ensures PHP code quality and compatibility
**What it does:**
- 🔍 PHPStan static analysis (level 5)
- 📏 PHP_CodeSniffer with PSR-12 standards
- ✔️ PHP 8.0+ compatibility checks
**Matrix:** PHP 8.0, 8.1, 8.2, 8.3
**When to check:** Before committing PHP changes
**How to run locally:**
```bash
# Install tools
composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
composer global require "phpstan/phpstan:^1.0" --with-all-dependencies
# Run checks
phpcs --standard=phpcs.xml src/
phpstan analyse --configuration=phpstan.neon
```
### Joomla Testing (joomla_testing.yml)
**Trigger:** Automatic on push/PR to main, dev/*, rc/* branches
**Purpose:** Tests template compatibility with Joomla versions
**What it does:**
- 📦 Downloads and installs Joomla (4.4, 5.0, 5.1)
- 🔧 Installs template into Joomla
- ✅ Validates template installation
- 🧪 Runs Codeception tests
**Matrix:** Joomla 4.4/5.0/5.1 × PHP 8.0/8.1/8.2/8.3
**When to check:** Before releasing new versions
**How to test locally:**
```bash
# See docs/JOOMLA_DEVELOPMENT.md for local testing setup
codecept run
```
### Version Branch Creation (version_branch.yml)
**Trigger:** Manual workflow dispatch
**Purpose:** Creates a new version branch and bumps version numbers
**What it does:**
- 🏷️ Creates dev/*, rc/*, or version/* branch
- 📝 Updates version in all files
- 📅 Updates manifest dates
- 📋 Moves CHANGELOG unreleased to version
- ✅ Validates version hierarchy
**When to use:** Starting work on a new version
**How to run:**
1. Go to Actions → Create version branch
2. Click "Run workflow"
3. Enter version (e.g., 03.06.00)
4. Select branch prefix (dev/, rc/, or version/)
5. Click "Run workflow"
**Example:**
```yaml
new_version: 03.06.00
branch_prefix: dev/
version_text: beta
```
### Release Pipeline (release_pipeline.yml)
**Trigger:** Manual workflow dispatch or release event
**Purpose:** Promotes branches through release stages and creates distributions
**What it does:**
- 🔄 Promotes branches: dev → rc → version → main
- 📅 Normalizes dates in manifest and CHANGELOG
- 📦 Builds distributable ZIP package
- 🚀 Uploads to SFTP server
- 🏷️ Creates Git tag
- 📋 Creates GitHub Release
- 🔒 Attests build provenance
**When to use:** Promoting a version through release stages
**How to run:**
1. Go to Actions → Release Pipeline
2. Click "Run workflow"
3. Select classification (auto/rc/stable)
4. Click "Run workflow"
**Release flow:**
```
dev/X.Y.Z → rc/X.Y.Z → version/X.Y.Z → main
(dev) (RC) (stable) (production)
```
### Deploy to Staging (deploy_staging.yml)
**Trigger:** Manual workflow dispatch
**Purpose:** Deploys template to staging/development environments
**What it does:**
- ✅ Validates deployment prerequisites
- 📦 Builds deployment package
- 🚀 Uploads via SFTP to environment
- 📝 Creates deployment summary
**When to use:** Testing in staging before production release
**How to run:**
1. Go to Actions → Deploy to Staging
2. Click "Run workflow"
3. Select environment (staging/development/preview)
4. Optionally specify version
5. Click "Run workflow"
**Required secrets:**
- `STAGING_HOST` - SFTP hostname
- `STAGING_USER` - SFTP username
- `STAGING_KEY` - SSH private key (or `STAGING_PASSWORD`)
- `STAGING_PATH` - Remote deployment path
### Repository Health (repo_health.yml)
**Trigger:** Manual workflow dispatch (admin only)
**Purpose:** Comprehensive repository health and configuration checks
**What it does:**
- 🔐 Validates release configuration
- 🌐 Tests SFTP connectivity
- 📂 Checks scripts governance
- 📄 Validates required artifacts
- 🔍 Extended checks (SPDX, ShellCheck, etc.)
**When to use:** Before major releases or when debugging deployment issues
**How to run:**
1. Go to Actions → Repo Health
2. Click "Run workflow"
3. Select profile (all/release/repo)
4. Click "Run workflow"
**Profiles:**
- `all` - Run all checks
- `release` - Release configuration and SFTP only
- `scripts` - Scripts governance only
- `repo` - Repository health only
## Common Development Tasks
### Starting a New Feature
```bash
# 1. Create a new version branch via GitHub Actions
# Actions → Create version branch → dev/X.Y.Z
# 2. Clone and checkout the new branch
git fetch origin
git checkout dev/X.Y.Z
# 3. Make your changes
vim src/index.php
# 4. Validate locally
make validate-required
# 5. Commit and push
git add -A
git commit -m "feat: add new feature"
git push origin dev/X.Y.Z
```
### Running All Validations Locally
```bash
# Run comprehensive validation suite
make validate-required
# Run quality checks
make quality
```
### Creating a Release Package
```bash
# Package with auto-detected version
```bash
# Package with auto-detected version
make package
# Verify package contents
unzip -l dist/moko-cassiopeia-*.zip
```
### Updating Version Numbers
```bash
# Via GitHub Actions (recommended)
# Actions → Release Management workflow
```
### Updating CHANGELOG
Update CHANGELOG.md manually or via pull request following the existing format.
## Troubleshooting
### CI Failures
#### PHP Syntax Errors
```bash
# Check specific file
php -l src/index.php
# Run validation
make validate-required
```
#### Manifest Validation Failed
```bash
# Validate manifest and XML files
make validate-required
```
#### Version Alignment Issues
```bash
# Check version consistency
make validate-required
```
### Workflow Failures
#### "Branch already exists"
```bash
# Check existing branches
git branch -r | grep dev/
# Delete remote branch if needed (carefully!)
git push origin --delete dev/03.06.00
```
#### "Missing required secrets"
Go to repository Settings → Secrets and variables → Actions, and add:
- `FTP_SERVER`
- `FTP_USER`
- `FTP_KEY` or `FTP_PASSWORD`
- `FTP_PATH`
#### SFTP Connection Failed
1. Verify credentials in repo_health workflow:
- Actions → Repo Health → profile: release
2. Check SSH key format (OpenSSH, not PuTTY PPK)
3. Verify server allows connections from GitHub IPs
### Quality Check Failures
#### PHPStan Errors
```bash
# Run locally to see full output
phpstan analyse --configuration=phpstan.neon
# Generate baseline to ignore existing issues
phpstan analyse --configuration=phpstan.neon --generate-baseline
```
#### PHPCS Violations
```bash
# Check violations
phpcs --standard=phpcs.xml src/
# Auto-fix where possible
phpcbf --standard=phpcs.xml src/
# Show specific error codes
phpcs --standard=phpcs.xml --report=source src/
```
#### Joomla Testing Failed
1. Check PHP/Joomla version matrix compatibility
2. Review MySQL connection errors
3. Verify template manifest structure
4. Check template file paths
## Best Practices
### Version Management
1. **Always use version branches:** dev/X.Y.Z, rc/X.Y.Z, version/X.Y.Z
2. **Follow hierarchy:** dev → rc → version → main
3. **Update CHANGELOG:** Document all changes in Unreleased section
4. **Semantic versioning:** Major.Minor.Patch (03.06.00)
### Code Quality
1. **Run validations locally** before pushing
2. **Fix PHPStan warnings** at level 5
3. **Follow PSR-12** coding standards
4. **Add SPDX license headers** to new files
5. **Keep functions small** and well-documented
### Workflow Usage
1. **Use CI for every commit** - automated validation
2. **Run repo_health before releases** - comprehensive checks
3. **Test on staging first** - never deploy directly to production
4. **Monitor workflow runs** - fix failures promptly
5. **Review workflow logs** - understand what changed
### Release Process
1. **Create dev branch** → Work on features
2. **Promote to rc** → Release candidate testing
3. **Promote to version** → Stable release
4. **Merge to main** → Production (auto-merged via PR)
5. **Create GitHub Release** → Public distribution
### Security
1. **Never commit secrets** - use GitHub Secrets
2. **Use SSH keys** for SFTP (not passwords)
3. **Scan for secrets** - runs automatically in CI
4. **Keep dependencies updated** - security patches
5. **Review security advisories** - GitHub Dependabot
### Documentation
1. **Update docs with code** - keep in sync
2. **Document workflow changes** - update this guide
3. **Add examples** - show, don't just tell
4. **Link to relevant docs** - cross-reference
5. **Keep README current** - first impression matters
## Quick Links
- [Main README](../README.md) - Project overview
- [Joomla Development Guide](./JOOMLA_DEVELOPMENT.md) - Testing and quality
- [CHANGELOG](../CHANGELOG.md) - Version history
- [CONTRIBUTING](../CONTRIBUTING.md) - Contribution guidelines
## Getting Help
1. **Check workflow logs** - Most issues have clear error messages
2. **Review this guide** - Common solutions documented
3. **Run validation scripts** - Identify specific issues
4. **Open an issue** - For bugs or questions
5. **Contact maintainers** - For access or configuration issues
---
## Metadata
* Document: docs/WORKFLOW_GUIDE.md
* Repository: [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
* Path: /docs/WORKFLOW_GUIDE.md
* Owner: Moko Consulting
* Version: 03.06.03
* Status: Active
* Effective Date: 2026-01-30
* Classification: Public Open Source Documentation
## Revision History
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------- | --------------- |
| 2026-01-30 | Updated metadata to MokoStandards format | GitHub Copilot |
| 2025-01-04 | Initial workflow guide created | GitHub Copilot |

12
package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "mokoonyx-build",
"private": true,
"description": "Build tooling for MokoOnyx Joomla template",
"scripts": {
"minify": "node scripts/minify.js"
},
"devDependencies": {
"clean-css": "^5.3.3",
"terser": "^5.39.0"
}
}

77
phpcs.xml Normal file
View File

@@ -0,0 +1,77 @@
<?xml version="1.0"?>
<ruleset name="Joomla Coding Standards">
<description>Joomla coding standards for MokoOnyx</description>
<!-- Show progress and sniff names -->
<arg value="ps"/>
<!-- Use colors in output -->
<arg name="colors"/>
<!-- Check PHP files only -->
<arg name="extensions" value="php"/>
<!-- Exclude patterns -->
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/tests/_output/*</exclude-pattern>
<exclude-pattern>*/cache/*</exclude-pattern>
<exclude-pattern>*/tmp/*</exclude-pattern>
<exclude-pattern>*/.git/*</exclude-pattern>
<!-- Include src directory -->
<file>src</file>
<!-- Use Joomla coding standard as base -->
<!-- When Joomla standard is installed, uncomment: -->
<!-- <rule ref="Joomla"/> -->
<!-- PSR-12 as fallback base standard -->
<rule ref="PSR12">
<!-- Allow long lines in some cases -->
<exclude name="Generic.Files.LineLength"/>
</rule>
<!-- Additional rules for PHP compatibility -->
<rule ref="PHPCompatibility"/>
<!-- Set minimum PHP version for compatibility checks -->
<config name="testVersion" value="8.0-"/>
<!-- Check for deprecated PHP functions -->
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<!-- Enforce proper file naming -->
<rule ref="Generic.Files.ByteOrderMark"/>
<rule ref="Generic.Files.LineEndings">
<properties>
<property name="eolChar" value="\n"/>
</properties>
</rule>
<!-- Code structure -->
<rule ref="Generic.Classes.DuplicateClassName"/>
<rule ref="Generic.CodeAnalysis.EmptyStatement"/>
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
<!-- Naming conventions -->
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
<!-- Security -->
<rule ref="Generic.PHP.BacktickOperator"/>
<!-- Formatting -->
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Formatting.SpaceAfterCast"/>
<!-- White space -->
<rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="indent" value="4"/>
<property name="tabIndent" value="true"/>
</properties>
</rule>
</ruleset>

32
phpstan.neon Normal file
View File

@@ -0,0 +1,32 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# PHPStan configuration for Joomla extension repositories.
# Extends the base MokoStandards config and adds Joomla framework class stubs
# so PHPStan can resolve Factory, CMSApplication, User, Table, etc.
# without requiring a full Joomla installation.
parameters:
level: 5
paths:
- src
excludePaths:
- vendor
- node_modules
# Joomla framework stubs — resolved via the enterprise package from vendor/
stubFiles:
- vendor/mokoconsulting-tech/enterprise/templates/stubs/joomla.php
# Suppress errors that are structural in Joomla's service-container architecture
ignoreErrors:
# Joomla's service-based dependency injection returns mixed from getApplication()
- '#Cannot call method .+ on Joomla\\CMS\\Application\\CMSApplication\|null#'
# Factory::getX() patterns are safe at runtime even when nullable in stubs
- '#Call to static method [a-zA-Z]+\(\) on an interface#'
reportUnmatchedIgnoredErrors: false
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false

View File

@@ -0,0 +1,73 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* # FILE INFORMATION
* DEFGROUP: Joomla.Template.Site
* INGROUP: MokoOnyx
* PATH: scripts/download-google-fonts.php
* VERSION: 03.09.03
* BRIEF: Download Google Fonts (woff2) for local self-hosting
*/
$fontsDir = __DIR__ . '/../src/media/fonts';
if (!is_dir($fontsDir)) {
fwrite(STDERR, "Error: Fonts directory not found: {$fontsDir}\n");
exit(1);
}
$fonts = [
'Roboto' => 'https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;700&display=swap',
'Noto Sans' => 'https://fonts.googleapis.com/css2?family=Noto+Sans:wght@100;300;400;700&display=swap',
'Fira Sans' => 'https://fonts.googleapis.com/css2?family=Fira+Sans:wght@100;300;400;700&display=swap',
];
$userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
echo "Google Fonts Downloader for MokoOnyx\n";
echo str_repeat('=', 48) . "\n";
echo "Target: {$fontsDir}\n\n";
foreach ($fonts as $name => $url) {
echo "Downloading {$name}...\n";
$ctx = stream_context_create(['http' => ['header' => "User-Agent: {$userAgent}\r\n"]]);
$css = @file_get_contents($url, false, $ctx);
if ($css === false) {
fwrite(STDERR, " FAIL: could not fetch CSS for {$name}\n");
continue;
}
preg_match_all('#https://fonts\.gstatic\.com[^)]*\.woff2#', $css, $matches);
if (empty($matches[0])) {
fwrite(STDERR, " FAIL: no woff2 URLs found for {$name}\n");
continue;
}
$count = 0;
foreach ($matches[0] as $fontUrl) {
$filename = basename($fontUrl);
$dest = $fontsDir . '/' . $filename;
$data = @file_get_contents($fontUrl, false, $ctx);
if ($data === false) {
fwrite(STDERR, " FAIL: {$filename}\n");
continue;
}
file_put_contents($dest, $data);
$size = round(strlen($data) / 1024, 1);
echo " OK: {$filename} ({$size} KB)\n";
$count++;
}
echo " {$count} file(s) downloaded\n\n";
}
echo "Done.\n";

170
src/component.php Normal file
View File

@@ -0,0 +1,170 @@
<?php
/* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Component\ComponentHelper;
/** @var Joomla\CMS\Document\HtmlDocument $this */
$app = Factory::getApplication();
$input = $app->getInput();
$document = $app->getDocument();
$wa = $document->getWebAssetManager();
// Template params - Component uses minimal configuration
$params_googletagmanager = $this->params->get('googletagmanager', false);
$params_googletagmanagerid = $this->params->get('googletagmanagerid', null);
$params_googleanalytics = $this->params->get('googleanalytics', false);
$params_googleanalyticsid = $this->params->get('googleanalyticsid', null);
$params_googlesitekey = $this->params->get('googlesitekey', null);
if (!empty($params_googlesitekey)) {
$this->setMetaData('google-site-verification', htmlspecialchars($params_googlesitekey, ENT_QUOTES, 'UTF-8'));
}
// Detecting Active Variables
$option = $input->getCmd('option', '');
$view = $input->getCmd('view', '');
$layout = $input->getCmd('layout', '');
$task = $input->getCmd('task', '');
$itemid = $input->getCmd('Itemid', '');
$sitenameR = $app->get('sitename'); // raw for title composition
$sitename = htmlspecialchars($sitenameR, ENT_QUOTES, 'UTF-8');
$menu = $app->getMenu()->getActive();
$pageclass = $menu !== null ? $menu->getParams()->get('pageclass_sfx', '') : '';
// Template/Media path
$templatePath = 'media/templates/site/mokoonyx';
// Core template CSS
$wa->useStyle('template.base'); // css/template.css
// Component always uses light theme only (no theme switching)
$wa->useStyle('template.light.standard'); // css/theme/light.standard.css
// Load Osaka font for site title
$wa->useStyle('template.font.osaka');
// Brand: logo from params OR siteTitle
// -------------------------------------
$brandHtml = '';
$logoFile = (string) $this->params->get('logoFile');
if ($logoFile !== '') {
$brandHtml = HTMLHelper::_(
'image',
Uri::root(false) . htmlspecialchars($logoFile, ENT_QUOTES, 'UTF-8'),
$sitename,
['class' => 'logo d-inline-block', 'loading' => 'eager', 'decoding' => 'async'],
false,
0
);
} else {
// If no logo file, show the title (defaults to "MokoOnyx" if not set)
$siteTitle = $this->params->get('siteTitle', 'MokoOnyx');
$brandHtml = '<span class="site-title" title="' . $sitename . '">'
. htmlspecialchars($siteTitle, ENT_COMPAT, 'UTF-8')
. '</span>';
}
?>
<!DOCTYPE html>
<html class="component" lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>" data-bs-theme="light">
<head>
<jdoc:include type="metas" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<jdoc:include type="styles" />
<jdoc:include type="scripts" />
</head>
<body class="<?php echo $this->direction === 'rtl' ? 'rtl' : ''; ?>">
<?php if (!empty($params_googletagmanager) && !empty($params_googletagmanagerid)) :
$gtmID = htmlspecialchars($params_googletagmanagerid, ENT_QUOTES, 'UTF-8'); ?>
<!-- Google Tag Manager -->
<script>
(function(w,d,s,l,i){
w[l]=w[l]||[];
w[l].push({'gtm.start': new Date().getTime(), event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),
dl=l!='dataLayer'?'&l='+l:'';
j.async=true;
j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','<?php echo $gtmID; ?>');
</script>
<!-- End Google Tag Manager -->
<!-- Google Tag Manager (noscript) -->
<noscript>
<iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo $gtmID; ?>"
height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<!-- End Google Tag Manager (noscript) -->
<?php endif; ?>
<?php if (!empty($params_googleanalytics) && !empty($params_googleanalyticsid)) :
$gaId = htmlspecialchars($params_googleanalyticsid, ENT_QUOTES, 'UTF-8'); ?>
<!-- Google Analytics (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo $gaId; ?>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'granted',
'ad_user_data': 'denied',
'ad_personalization': 'denied'
});
(function(id){
if (/^G-/.test(id)) {
gtag('config', id, { 'anonymize_ip': true });
} else if (/^UA-/.test(id)) {
gtag('config', id, { 'anonymize_ip': true });
console.warn('Using a UA- ID. Universal Analytics is sunset; consider migrating to GA4.');
} else {
console.warn('Unrecognized Google Analytics ID format:', id);
}
})('<?php echo $gaId; ?>');
</script>
<!-- End Google Analytics -->
<?php endif; ?>
<?php if ($this->params->get('brand', 1)) : ?>
<div class="navbar-brand">
<a class="brand-logo" href="<?php echo $this->baseurl; ?>/">
<?php echo $brandHtml; ?>
</a>
</div>
<?php endif; ?>
<jdoc:include type="message" />
<jdoc:include type="component" />
<footer class="container-footer footer full-width">
<?php if ($this->countModules('footer-menu', true)) : ?>
<div class="grid-child footer-menu">
<jdoc:include type="modules" name="footer-menu" />
</div>
<?php endif; ?>
<?php if ($this->countModules('footer', true)) : ?>
<div class="grid-child">
<jdoc:include type="modules" name="footer" style="none" />
</div>
<?php endif; ?>
</footer>
<jdoc:include type="modules" name="debug" style="none" />
</body>
</html>

20
src/custom.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
/* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
*/
function console_log($output, $with_script_tags = true) {
$js_code = 'console.log(' . json_encode($output, JSON_HEX_TAG) .
');';
if ($with_script_tags) {
$js_code = '<script>' . $js_code . '</script>';
}
echo $js_code;
}
?>
<!--
Custom code included here
-->

452
src/error.php Normal file
View File

@@ -0,0 +1,452 @@
<?php
/* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
/** @var Joomla\CMS\Document\ErrorDocument|Joomla\CMS\Document\HtmlDocument $this */
$app = Factory::getApplication();
$params = $this->params;
$wa = $this->getWebAssetManager();
// Template params
$params_LightColorName = (string) $params->get('colorLightName', 'standard'); // standard|custom
$params_DarkColorName = (string) $params->get('colorDarkName', 'standard'); // standard|custom
$params_googletagmanager = $params->get('googletagmanager', false);
$params_googletagmanagerid = $params->get('googletagmanagerid', '');
$params_googleanalytics = $params->get('googleanalytics', false);
$params_googleanalyticsid = $params->get('googleanalyticsid', '');
$params_custom_head_start = $params->get('custom_head_start', '');
$params_custom_head_end = $params->get('custom_head_end', '');
$params_developmentmode = $params->get('developmentmode', false);
// ------------------ Params ------------------
$fluidContainer = (bool) $params->get('fluidContainer', 0);
$wrapper = $fluidContainer ? 'wrapper-fluid' : 'wrapper-static';
$stickyHeader = (bool) $params->get('stickyHeader', 0);
// Drawer icon params (escaped)
$params_leftIcon = htmlspecialchars($params->get('drawerLeftIcon', 'fa-solid fa-chevron-left'), ENT_QUOTES, 'UTF-8');
$params_rightIcon = htmlspecialchars($params->get('drawerRightIcon', 'fa-solid fa-chevron-right'), ENT_QUOTES, 'UTF-8');
// Template/Media path
$templatePath = 'media/templates/site/mokoonyx';
// ===========================
// Web Asset Manager (WAM) — matches your joomla.asset.json
// ===========================
// Core template CSS
$wa->useStyle('template.base'); // css/template.css
// Load theme palette stylesheets based on configuration
$wa->useStyle('template.light.standard'); // css/theme/light.standard.css
$wa->useStyle('template.dark.standard'); // css/theme/dark.standard.css
// Load custom palettes only if selected in template configuration AND files exist
if ($params_LightColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokoonyx/css/theme/light.custom.css'))
{
$wa->useStyle('template.light.custom');
}
if ($params_DarkColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokoonyx/css/theme/dark.custom.css'))
{
$wa->useStyle('template.dark.custom');
}
// Scripts
$wa->useScript('template.js');
// Load Osaka font for site title
$wa->useStyle('template.font.osaka');
// Smart Bootstrap component loading - only load what's needed
if ($this->countModules('drawer-left', true) || $this->countModules('drawer-right', true)) {
// Load Bootstrap Offcanvas component for drawers
HTMLHelper::_('bootstrap.offcanvas');
}
// Meta
$this->setMetaData('viewport', 'width=device-width, initial-scale=1');
if ($this->params->get('faKitCode')) {
$faKit = "https://kit.fontawesome.com/" . $this->params->get('faKitCode') . ".js";
HTMLHelper::_('script', $faKit, ['crossorigin' => 'anonymous']);
} else {
try {
if ($params_developmentmode){
$wa->useStyle('vendor.fa7free.all');
$wa->useStyle('vendor.fa7free.brands');
$wa->useStyle('vendor.fa7free.fontawesome');
$wa->useStyle('vendor.fa7free.regular');
$wa->useStyle('vendor.fa7free.solid');
} else {
$wa->useStyle('vendor.fa7free.all.min');
$wa->useStyle('vendor.fa7free.brands.min');
$wa->useStyle('vendor.fa7free.fontawesome.min');
$wa->useStyle('vendor.fa7free.regular.min');
$wa->useStyle('vendor.fa7free.solid.min');
}
} catch (\Throwable $e) {
if ($params_developmentmode){
$wa->registerAndUseStyle('vendor.fa7free.all.dynamic', $templatePath . '/vendor/fa7free/css/all.css');
$wa->registerAndUseStyle('vendor.fa7free.brands.dynamic', $templatePath . '/vendor/fa7free/css/brands.css');
$wa->registerAndUseStyle('vendor.fa7free.fontawesome.dynamic', $templatePath . '/vendor/fa7free/css/fontawesome.css');
$wa->registerAndUseStyle('vendor.fa7free.regular.dynamic', $templatePath . '/vendor/fa7free/css/regular.css');
$wa->registerAndUseStyle('vendor.fa7free.solid.dynamic', $templatePath . '/vendor/fa7free/css/solid.css');
} else {
$wa->registerAndUseStyle('vendor.fa7free.all.min.dynamic', $templatePath . '/vendor/fa7free/css/all.min.css');
$wa->registerAndUseStyle('vendor.fa7free.brands.min.dynamic', $templatePath . '/vendor/fa7free/css/brands.min.css');
$wa->registerAndUseStyle('vendor.fa7free.fontawesome.min.dynamic', $templatePath . '/vendor/fa7free/css/fontawesome.min.css');
$wa->registerAndUseStyle('vendor.fa7free.regular.min.dynamic', $templatePath . '/vendor/fa7free/css/regular.min.css');
$wa->registerAndUseStyle('vendor.fa7free.solid.min.dynamic', $templatePath . '/vendor/fa7free/css/solid.min.css');
}
}
}
// ------------------ Context (logo, bootstrap needs) ------------------
$sitename = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
// -------------------------------------
// Brand: logo from params OR siteTitle
// -------------------------------------
$brandHtml = '';
$logoFile = (string) $this->params->get('logoFile');
if ($logoFile !== '') {
$brandHtml = HTMLHelper::_(
'image',
Uri::root(false) . htmlspecialchars($logoFile, ENT_QUOTES, 'UTF-8'),
$sitename,
['class' => 'logo d-inline-block', 'loading' => 'eager', 'decoding' => 'async'],
false,
0
);
} else {
// If no logo file, show the title (defaults to "MokoOnyx" if not set)
$siteTitle = $this->params->get('siteTitle', 'MokoOnyx');
$brandHtml = '<span class="site-title" title="' . $sitename . '">'
. htmlspecialchars($siteTitle, ENT_COMPAT, 'UTF-8')
. '</span>';
}
// ------------------ Error details ------------------
$errorObj = isset($this->error) && is_object($this->error) ? $this->error : null;
$errorCode = $errorObj ? (int) $errorObj->getCode() : 500;
$errorMsg = $errorObj ? $errorObj->getMessage() : Text::_('JERROR_AN_ERROR_HAS_OCCURRED');
$debugOn = defined('JDEBUG') && JDEBUG;
// Load user assets last (after all other styles and scripts)
$wa->useStyle('template.user'); // css/user.css
$wa->useScript('user.js'); // js/user.js
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<?php if ($params_custom_head_start !== '') : ?><?php echo $params_custom_head_start; ?><?php endif; ?>
<jdoc:include type="head" />
<script>
// Early theme application to avoid FOUC
(function () {
try {
var stored = localStorage.getItem('theme');
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = stored ? stored : (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-bs-theme', theme);
} catch (e) {}
})();
</script>
<script>
// Facebook in-app browser warning banner
window.addEventListener('DOMContentLoaded', function () {
var ua = navigator.userAgent || navigator.vendor || window.opera;
var isFacebookBrowser = ua.indexOf('FBAN') > -1 || ua.indexOf('FBAV') > -1;
if (isFacebookBrowser) {
var warning = document.createElement('div');
warning.textContent = '⚠️ KNOWN ISSUE: Images do not load in Facebook Web browser. Please open in external browser for full experience.';
warning.style.position = 'fixed';
warning.style.top = '0';
warning.style.left = '0';
warning.style.right = '0';
warning.style.zIndex = '10000';
warning.style.backgroundColor = '#007bff';
warning.style.color = '#fff';
warning.style.padding = '15px';
warning.style.textAlign = 'center';
warning.style.fontWeight = 'bold';
warning.style.fontSize = '16px';
warning.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
document.body.appendChild(warning);
}
});
</script>
<?php if ($params_custom_head_end !== '') : ?><?php echo $params_custom_head_end; ?><?php endif; ?>
</head>
<body data-bs-spy="scroll" data-bs-target="#toc" class="site error-page<?php
echo ($this->direction == 'rtl' ? ' rtl' : '');
?>">
<?php if (!empty($params_googletagmanager) && !empty($params_googletagmanagerid)) : ?>
<!-- Google Tag Manager -->
<script>
(function(w,d,s,l,i){
w[l]=w[l]||[];
w[l].push({'gtm.start': new Date().getTime(), event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),
dl=l!='dataLayer'?'&l='+l:'';
j.async=true;
j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer',<?php echo json_encode($params_googletagmanagerid, JSON_HEX_TAG | JSON_HEX_AMP); ?>);
</script>
<!-- End Google Tag Manager -->
<!-- Google Tag Manager (noscript) -->
<noscript>
<iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo htmlspecialchars($params_googletagmanagerid, ENT_QUOTES, 'UTF-8'); ?>"
height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<!-- End Google Tag Manager (noscript) -->
<?php endif; ?>
<?php if (!empty($params_googleanalytics) && !empty($params_googleanalyticsid)) : ?>
<!-- Google Analytics (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo htmlspecialchars($params_googleanalyticsid, ENT_QUOTES, 'UTF-8'); ?>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'granted',
'ad_user_data': 'denied',
'ad_personalization': 'denied'
});
(function(id){
if (/^G-/.test(id)) {
gtag('config', id, { 'anonymize_ip': true });
} else if (/^UA-/.test(id)) {
gtag('config', id, { 'anonymize_ip': true });
console.warn('Using a UA- ID. Universal Analytics is sunset; consider migrating to GA4.');
} else {
console.warn('Unrecognized Google Analytics ID format:', id);
}
})(<?php echo json_encode($params_googleanalyticsid, JSON_HEX_TAG | JSON_HEX_AMP); ?>);
</script>
<!-- End Google Analytics -->
<?php endif; ?>
<!-- ========== HEADER FROM INDEX ========== -->
<header class="header container-header full-width<?php echo $stickyHeader ? ' position-sticky sticky-top' : ''; ?>" role="banner">
<?php if ($this->countModules('topbar')) : ?>
<div class="container-topbar">
<jdoc:include type="modules" name="topbar" style="none" />
</div>
<?php endif; ?>
<div class="header-top">
<?php if ($this->countModules('below-topbar')) : ?>
<div class="grid-child container-below-topbar">
<jdoc:include type="modules" name="below-topbar" style="none" />
</div>
<?php endif; ?>
<?php if ($this->params->get('brand', 1)) : ?>
<div class="grid-child">
<div class="navbar-brand">
<a class="brand-logo" href="<?php echo $this->baseurl; ?>/">
<?php echo $brandHtml; ?>
</a>
<?php if ($this->params->get('siteDescription')) : ?>
<div class="site-description">
<?php echo htmlspecialchars($this->params->get('siteDescription'), ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php if ($this->countModules('below-logo')) : ?>
<div class="grid container-below-logo">
<jdoc:include type="modules" name="below-logo" style="none" />
</div>
<?php endif; ?>
</div>
<!-- Drawer Toggle Buttons -->
<?php if ($this->countModules('drawer-left')) : ?>
<button class="drawer-toggle-left btn btn-outline-secondary me-2"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#drawer-left"
aria-controls="drawer-left">
<span class="<?php echo $params_leftIcon; ?>"></span>
</button>
<?php endif; ?>
<?php if ($this->countModules('drawer-right')) : ?>
<button class="drawer-toggle-right btn btn-outline-secondary"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#drawer-right"
aria-controls="drawer-right">
<span class="<?php echo $params_rightIcon; ?>"></span>
</button>
<?php endif; ?>
<?php if ($this->countModules('menu', true) || $this->countModules('search', true)) : ?>
<div class="grid-child container-nav">
<?php if ($this->countModules('menu', true)) : ?>
<nav role="navigation" aria-label="Primary">
<jdoc:include type="modules" name="menu" style="none" />
</nav>
<?php endif; ?>
<?php if ($this->countModules('search', true)) : ?>
<div class="container-search">
<jdoc:include type="modules" name="search" style="none" />
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</header>
<!-- ========== END HEADER ========== -->
<main class="container my-4">
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h1 class="h3">
<span class="text-muted"><?php echo Text::_('JERROR_LAYOUT_ERROR_HAS_OCCURRED'); ?>:</span>
<strong><?php echo (int) $errorCode; ?></strong>
</h1>
<p class="lead mb-1">
<?php echo htmlspecialchars($errorMsg, ENT_QUOTES, 'UTF-8'); ?>
</p>
<p class="text-muted mb-0">
<?php echo Text::_('JERROR_LAYOUT_PLEASE_TRY_ONE_OF_THE_FOLLOWING_PAGES'); ?>
</p>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-primary" href="<?php echo htmlspecialchars(Uri::base(), ENT_QUOTES, 'UTF-8'); ?>">
<i class="fa-solid fa-home me-1" aria-hidden="true"></i>
<?php echo Text::_('JERROR_LAYOUT_HOME_PAGE'); ?>
</a>
<button class="btn btn-outline-secondary" type="button" onclick="history.back();">
<i class="fa-solid fa-arrow-left me-1" aria-hidden="true"></i>
<?php echo Text::_('JPREV'); ?>
</button>
</div>
<?php if ($debugOn && $errorObj) : ?>
<section class="mt-4" role="region" aria-label="Debug Details">
<div class="alert alert-warning"><strong>Debug mode is ON</strong> — detailed error information is shown below.</div>
<div class="card mb-3">
<div class="card-header fw-bold">Exception</div>
<div class="card-body small">
<dl class="row mb-0">
<dt class="col-sm-3">Class</dt>
<dd class="col-sm-9"><?php echo htmlspecialchars(get_class($errorObj), ENT_QUOTES, 'UTF-8'); ?></dd>
<dt class="col-sm-3">Code</dt>
<dd class="col-sm-9"><?php echo (int) $errorObj->getCode(); ?></dd>
<dt class="col-sm-3">Message</dt>
<dd class="col-sm-9 text-break"><?php echo htmlspecialchars($errorObj->getMessage(), ENT_QUOTES, 'UTF-8'); ?></dd>
<dt class="col-sm-3">File</dt>
<dd class="col-sm-9 text-break"><?php echo htmlspecialchars($errorObj->getFile(), ENT_QUOTES, 'UTF-8'); ?> : <?php echo (int) $errorObj->getLine(); ?></dd>
</dl>
</div>
</div>
<?php $trace = method_exists($errorObj, 'getTrace') ? $errorObj->getTrace() : []; ?>
<div class="card mb-3">
<div class="card-header fw-bold">Stack Trace (<?php echo count($trace); ?> frames)</div>
<div class="card-body small">
<?php if ($trace) : ?>
<ol class="mb-0 ps-3">
<?php foreach ($trace as $i => $frame) :
$file = $frame['file'] ?? '[internal]';
$line = isset($frame['line']) ? (int) $frame['line'] : 0;
$func = $frame['function'] ?? '';
$class= $frame['class'] ?? '';
$type = $frame['type'] ?? '';
?>
<li class="mb-2">
<div class="text-break"><code>#<?php echo $i; ?></code> <?php echo htmlspecialchars($class . $type . $func, ENT_QUOTES, 'UTF-8'); ?>()</div>
<div class="text-muted"><?php echo htmlspecialchars($file, ENT_QUOTES, 'UTF-8'); ?><?php echo $line ? ':' . $line : ''; ?></div>
</li>
<?php endforeach; ?>
</ol>
<?php else : ?>
<em>No stack trace available.</em>
<?php endif; ?>
</div>
</div>
</section>
<?php endif; ?>
</main>
<footer class="container-footer footer full-width">
<?php if ($this->countModules('footer-menu', true)) : ?>
<div class="grid-child footer-menu">
<jdoc:include type="modules" name="footer-menu" />
</div>
<?php endif; ?>
<?php if ($this->countModules('footer', true)) : ?>
<div class="grid-child">
<jdoc:include type="modules" name="footer" style="none" />
</div>
<?php endif; ?>
</footer>
<?php if ($this->params->get('backTop') == 1) : ?>
<a href="#top" id="back-top" class="back-to-top-link" aria-label="<?php echo Text::_('TPL_MOKOONYX_BACKTOTOP'); ?>">
<span class="fa-solid fa-arrow-up" aria-hidden="true"></span>
</a>
<?php endif; ?>
<?php if ($this->countModules('drawer-left', true)) : ?>
<!-- Left Offcanvas Drawer -->
<aside class="offcanvas offcanvas-start" tabindex="-1" id="drawer-left">
<div class="offcanvas-header">
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="<?php echo Text::_('JLIB_HTML_BEHAVIOR_CLOSE'); ?>"></button>
</div>
<div class="offcanvas-body">
<jdoc:include type="modules" name="drawer-left" style="none" />
</div>
</aside>
<?php endif; ?>
<?php if ($this->countModules('drawer-right', true)) : ?>
<!-- Right Offcanvas Drawer -->
<aside class="offcanvas offcanvas-end" tabindex="-1" id="drawer-right">
<div class="offcanvas-header">
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="<?php echo Text::_('JLIB_HTML_BEHAVIOR_CLOSE'); ?>"></button>
</div>
<div class="offcanvas-body">
<jdoc:include type="modules" name="drawer-right" style="none" />
</div>
</aside>
<?php endif; ?>
<jdoc:include type="modules" name="debug" style="none" />
</body>
</html>

518
src/helper/favicon.php Normal file
View File

@@ -0,0 +1,518 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Favicon generator — creates ICO, Apple Touch Icon, and Android icons
* from a single source image uploaded via the template config.
*
* Supports three backends in priority order:
* 1. GD (fastest, most common)
* 2. Imagick (common on shared hosting)
* 3. Pure PHP (zero-dependency fallback using raw PNG manipulation)
*/
defined('_JEXEC') or die;
use Joomla\CMS\Log\Log;
class MokoFaviconHelper
{
private const SIZES = [
'apple-touch-icon.png' => [180, 180],
'favicon-32x32.png' => [32, 32],
'favicon-16x16.png' => [16, 16],
'android-chrome-192x192.png' => [192, 192],
'android-chrome-512x512.png' => [512, 512],
];
/**
* Generate all favicon files from a source image.
*/
public static function generate(string $sourcePath, string $outputDir): bool
{
if (!is_file($sourcePath)) {
self::log('Favicon: source file not found: ' . $sourcePath, 'warning');
return false;
}
if (!is_dir($outputDir)) {
mkdir($outputDir, 0755, true);
}
$sourceTime = filemtime($sourcePath);
$stampFile = $outputDir . '/.favicon_generated';
if (is_file($stampFile) && filemtime($stampFile) >= $sourceTime) {
return true;
}
// Strip #joomlaImage fragment if present
$sourcePath = strtok($sourcePath, '#');
// Select backend
if (extension_loaded('gd')) {
$result = self::generateWithGd($sourcePath, $outputDir);
} elseif (extension_loaded('imagick')) {
$result = self::generateWithImagick($sourcePath, $outputDir);
} else {
$result = self::generatePurePHP($sourcePath, $outputDir);
}
if ($result) {
self::generateManifest($outputDir);
file_put_contents($stampFile, date('c'));
}
return $result;
}
// ── GD Backend ──────────────────────────────────────────────────
private static function generateWithGd(string $sourcePath, string $outputDir): bool
{
$imageInfo = @getimagesize($sourcePath);
if ($imageInfo === false) {
self::log('Favicon: cannot read image: ' . $sourcePath, 'warning');
return false;
}
$source = match ($imageInfo[2]) {
IMAGETYPE_PNG => @imagecreatefrompng($sourcePath),
IMAGETYPE_JPEG => @imagecreatefromjpeg($sourcePath),
IMAGETYPE_GIF => @imagecreatefromgif($sourcePath),
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($sourcePath) : false,
default => false,
};
if (!$source) {
self::log('Favicon: unsupported image type', 'warning');
return false;
}
imagealphablending($source, false);
imagesavealpha($source, true);
$srcW = imagesx($source);
$srcH = imagesy($source);
foreach (self::SIZES as $filename => [$w, $h]) {
$resized = imagecreatetruecolor($w, $h);
imagealphablending($resized, false);
imagesavealpha($resized, true);
imagefill($resized, 0, 0, imagecolorallocatealpha($resized, 0, 0, 0, 127));
imagecopyresampled($resized, $source, 0, 0, 0, 0, $w, $h, $srcW, $srcH);
imagepng($resized, $outputDir . '/' . $filename, 9);
imagedestroy($resized);
}
// ICO from GD
$icoEntries = [];
foreach ([16, 32] as $size) {
$resized = imagecreatetruecolor($size, $size);
imagealphablending($resized, false);
imagesavealpha($resized, true);
imagefill($resized, 0, 0, imagecolorallocatealpha($resized, 0, 0, 0, 127));
imagecopyresampled($resized, $source, 0, 0, 0, 0, $size, $size, $srcW, $srcH);
ob_start();
imagepng($resized, null, 9);
$icoEntries[] = ['size' => $size, 'data' => ob_get_clean()];
imagedestroy($resized);
}
self::writeIco($icoEntries, $outputDir . '/favicon.ico');
imagedestroy($source);
self::log('Favicon: generated with GD');
return true;
}
// ── Imagick Backend ─────────────────────────────────────────────
private static function generateWithImagick(string $sourcePath, string $outputDir): bool
{
try {
foreach (self::SIZES as $filename => [$w, $h]) {
$img = new \Imagick($sourcePath);
$img->setImageFormat('png');
$img->setImageCompressionQuality(95);
$img->thumbnailImage($w, $h, true);
// Center on transparent canvas if not square
$canvas = new \Imagick();
$canvas->newImage($w, $h, new \ImagickPixel('transparent'), 'png');
$offsetX = (int)(($w - $img->getImageWidth()) / 2);
$offsetY = (int)(($h - $img->getImageHeight()) / 2);
$canvas->compositeImage($img, \Imagick::COMPOSITE_OVER, $offsetX, $offsetY);
$canvas->writeImage($outputDir . '/' . $filename);
$img->destroy();
$canvas->destroy();
}
// ICO from Imagick
$icoEntries = [];
foreach ([16, 32] as $size) {
$img = new \Imagick($sourcePath);
$img->setImageFormat('png');
$img->thumbnailImage($size, $size, true);
$icoEntries[] = ['size' => $size, 'data' => (string) $img];
$img->destroy();
}
self::writeIco($icoEntries, $outputDir . '/favicon.ico');
self::log('Favicon: generated with Imagick');
return true;
} catch (\Exception $e) {
self::log('Favicon: Imagick failed: ' . $e->getMessage(), 'warning');
return false;
}
}
// ── Pure PHP Backend (zero dependencies) ────────────────────────
private static function generatePurePHP(string $sourcePath, string $outputDir): bool
{
$pngData = @file_get_contents($sourcePath);
if ($pngData === false) {
self::log('Favicon: cannot read source file', 'warning');
return false;
}
// Detect format — we can only resize PNG in pure PHP
// For JPEG/other formats, just copy the source as-is for each size
$isPng = (substr($pngData, 0, 8) === "\x89PNG\r\n\x1a\n");
if (!$isPng) {
// Non-PNG: copy source file for all sizes (no resize capability without extensions)
foreach (self::SIZES as $filename => [$w, $h]) {
copy($sourcePath, $outputDir . '/' . $filename);
}
// ICO: embed the raw source for 16 and 32 entries
self::writeIco([
['size' => 16, 'data' => $pngData],
['size' => 32, 'data' => $pngData],
], $outputDir . '/favicon.ico');
self::log('Favicon: non-PNG source copied without resize (no GD/Imagick)');
return true;
}
// Parse PNG dimensions from IHDR
$ihdr = self::parsePngIhdr($pngData);
if (!$ihdr) {
self::log('Favicon: cannot parse PNG header', 'warning');
return false;
}
$srcW = $ihdr['width'];
$srcH = $ihdr['height'];
// Decode PNG to raw RGBA pixel array
$pixels = self::decodePngToRgba($pngData, $srcW, $srcH, $ihdr);
if ($pixels === null) {
// Fallback: copy source for all sizes
foreach (self::SIZES as $filename => [$w, $h]) {
copy($sourcePath, $outputDir . '/' . $filename);
}
self::writeIco([
['size' => 16, 'data' => $pngData],
['size' => 32, 'data' => $pngData],
], $outputDir . '/favicon.ico');
self::log('Favicon: PNG decode failed, copied source without resize');
return true;
}
// Generate resized PNGs
foreach (self::SIZES as $filename => [$w, $h]) {
$resized = self::resizePixels($pixels, $srcW, $srcH, $w, $h);
$png = self::encodePng($resized, $w, $h);
file_put_contents($outputDir . '/' . $filename, $png);
}
// ICO
$icoEntries = [];
foreach ([16, 32] as $size) {
$resized = self::resizePixels($pixels, $srcW, $srcH, $size, $size);
$icoEntries[] = ['size' => $size, 'data' => self::encodePng($resized, $size, $size)];
}
self::writeIco($icoEntries, $outputDir . '/favicon.ico');
self::log('Favicon: generated with pure PHP');
return true;
}
/**
* Parse PNG IHDR chunk.
*/
private static function parsePngIhdr(string $data): ?array
{
if (strlen($data) < 33) return null;
// Skip 8-byte signature, 4-byte chunk length, 4-byte "IHDR"
$width = unpack('N', substr($data, 16, 4))[1];
$height = unpack('N', substr($data, 20, 4))[1];
$bitDepth = ord($data[24]);
$colorType = ord($data[25]);
return ['width' => $width, 'height' => $height, 'bitDepth' => $bitDepth, 'colorType' => $colorType];
}
/**
* Decode PNG to flat RGBA array using zlib decompression.
*
* @return array|null Flat array of [r,g,b,a, r,g,b,a, ...] or null on failure.
*/
private static function decodePngToRgba(string $data, int $w, int $h, array $ihdr): ?array
{
// Only support 8-bit RGBA (color type 6) and RGB (color type 2) for simplicity
$colorType = $ihdr['colorType'];
$bitDepth = $ihdr['bitDepth'];
if ($bitDepth !== 8 || ($colorType !== 6 && $colorType !== 2 && $colorType !== 3)) {
return null; // Unsupported format
}
// Collect all IDAT chunks
$idatData = '';
$pos = 8; // Skip PNG signature
$palette = null;
$trns = null;
while ($pos < strlen($data) - 4) {
$chunkLen = unpack('N', substr($data, $pos, 4))[1];
$chunkType = substr($data, $pos + 4, 4);
if ($chunkType === 'IDAT') {
$idatData .= substr($data, $pos + 8, $chunkLen);
} elseif ($chunkType === 'PLTE') {
$palette = substr($data, $pos + 8, $chunkLen);
} elseif ($chunkType === 'tRNS') {
$trns = substr($data, $pos + 8, $chunkLen);
} elseif ($chunkType === 'IEND') {
break;
}
$pos += 12 + $chunkLen; // 4 len + 4 type + data + 4 crc
}
$raw = @gzuncompress($idatData);
if ($raw === false) {
$raw = @gzinflate($idatData);
}
if ($raw === false) {
// Try with zlib header
$raw = @gzinflate(substr($idatData, 2));
}
if ($raw === false) {
return null;
}
$bpp = $colorType === 6 ? 4 : ($colorType === 2 ? 3 : 1); // bytes per pixel
$stride = 1 + $w * $bpp; // +1 for filter byte per row
$pixels = [];
$prevRow = array_fill(0, $w * $bpp, 0);
for ($y = 0; $y < $h; $y++) {
$rowStart = $y * $stride;
if ($rowStart >= strlen($raw)) break;
$filter = ord($raw[$rowStart]);
$row = [];
for ($x = 0; $x < $w * $bpp; $x++) {
$rawByte = ord($raw[$rowStart + 1 + $x]);
$a = ($x >= $bpp) ? $row[$x - $bpp] : 0;
$b = $prevRow[$x];
$c = ($x >= $bpp) ? $prevRow[$x - $bpp] : 0;
$val = match ($filter) {
0 => $rawByte,
1 => ($rawByte + $a) & 0xFF,
2 => ($rawByte + $b) & 0xFF,
3 => ($rawByte + (int)(($a + $b) / 2)) & 0xFF,
4 => ($rawByte + self::paethPredictor($a, $b, $c)) & 0xFF,
default => $rawByte,
};
$row[] = $val;
}
// Convert row to RGBA
for ($x = 0; $x < $w; $x++) {
if ($colorType === 6) { // RGBA
$pixels[] = $row[$x * 4];
$pixels[] = $row[$x * 4 + 1];
$pixels[] = $row[$x * 4 + 2];
$pixels[] = $row[$x * 4 + 3];
} elseif ($colorType === 2) { // RGB
$pixels[] = $row[$x * 3];
$pixels[] = $row[$x * 3 + 1];
$pixels[] = $row[$x * 3 + 2];
$pixels[] = 255;
} elseif ($colorType === 3 && $palette) { // Indexed
$idx = $row[$x];
$pixels[] = ord($palette[$idx * 3]);
$pixels[] = ord($palette[$idx * 3 + 1]);
$pixels[] = ord($palette[$idx * 3 + 2]);
$pixels[] = ($trns && $idx < strlen($trns)) ? ord($trns[$idx]) : 255;
}
}
$prevRow = $row;
}
return $pixels;
}
private static function paethPredictor(int $a, int $b, int $c): int
{
$p = $a + $b - $c;
$pa = abs($p - $a);
$pb = abs($p - $b);
$pc = abs($p - $c);
if ($pa <= $pb && $pa <= $pc) return $a;
if ($pb <= $pc) return $b;
return $c;
}
/**
* Bilinear resize of RGBA pixel array.
*/
private static function resizePixels(array $src, int $srcW, int $srcH, int $dstW, int $dstH): array
{
$dst = [];
$xRatio = $srcW / $dstW;
$yRatio = $srcH / $dstH;
for ($y = 0; $y < $dstH; $y++) {
$srcY = $y * $yRatio;
$y0 = (int) $srcY;
$y1 = min($y0 + 1, $srcH - 1);
$yFrac = $srcY - $y0;
for ($x = 0; $x < $dstW; $x++) {
$srcX = $x * $xRatio;
$x0 = (int) $srcX;
$x1 = min($x0 + 1, $srcW - 1);
$xFrac = $srcX - $x0;
for ($c = 0; $c < 4; $c++) {
$tl = $src[($y0 * $srcW + $x0) * 4 + $c];
$tr = $src[($y0 * $srcW + $x1) * 4 + $c];
$bl = $src[($y1 * $srcW + $x0) * 4 + $c];
$br = $src[($y1 * $srcW + $x1) * 4 + $c];
$top = $tl + ($tr - $tl) * $xFrac;
$bot = $bl + ($br - $bl) * $xFrac;
$dst[] = (int) round($top + ($bot - $top) * $yFrac);
}
}
}
return $dst;
}
/**
* Encode RGBA pixel array to PNG binary.
*/
private static function encodePng(array $pixels, int $w, int $h): string
{
// Build raw image data with filter byte 0 (None) per row
$raw = '';
for ($y = 0; $y < $h; $y++) {
$raw .= "\x00"; // filter: None
for ($x = 0; $x < $w; $x++) {
$i = ($y * $w + $x) * 4;
$raw .= chr($pixels[$i]) . chr($pixels[$i + 1]) . chr($pixels[$i + 2]) . chr($pixels[$i + 3]);
}
}
$compressed = gzcompress($raw);
// Build PNG
$png = "\x89PNG\r\n\x1a\n";
// IHDR
$ihdr = pack('NNCCCC', $w, $h, 8, 6, 0, 0, 0); // 8-bit RGBA
$png .= self::pngChunk('IHDR', $ihdr);
// IDAT
$png .= self::pngChunk('IDAT', $compressed);
// IEND
$png .= self::pngChunk('IEND', '');
return $png;
}
private static function pngChunk(string $type, string $data): string
{
$chunk = $type . $data;
return pack('N', strlen($data)) . $chunk . pack('N', crc32($chunk));
}
// ── Shared Utilities ────────────────────────────────────────────
/**
* Write ICO file from PNG data entries.
*/
private static function writeIco(array $entries, string $outPath): void
{
$count = count($entries);
$ico = pack('vvv', 0, 1, $count);
$offset = 6 + ($count * 16);
$imageData = '';
foreach ($entries as $entry) {
$size = $entry['size'] >= 256 ? 0 : $entry['size'];
$dataLen = strlen($entry['data']);
$ico .= pack('CCCCvvVV', $size, $size, 0, 0, 1, 32, $dataLen, $offset);
$imageData .= $entry['data'];
$offset += $dataLen;
}
file_put_contents($outPath, $ico . $imageData);
}
private static function generateManifest(string $outputDir): void
{
$manifest = [
'icons' => [
['src' => 'android-chrome-192x192.png', 'sizes' => '192x192', 'type' => 'image/png'],
['src' => 'android-chrome-512x512.png', 'sizes' => '512x512', 'type' => 'image/png'],
],
];
file_put_contents(
$outputDir . '/site.webmanifest',
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
public static function getHeadTags(string $basePath): string
{
$basePath = htmlspecialchars(rtrim($basePath, '/'), ENT_QUOTES, 'UTF-8');
return '<link rel="apple-touch-icon" sizes="180x180" href="' . $basePath . '/apple-touch-icon.png">' . "\n"
. '<link rel="icon" type="image/png" sizes="32x32" href="' . $basePath . '/favicon-32x32.png">' . "\n"
. '<link rel="icon" type="image/png" sizes="16x16" href="' . $basePath . '/favicon-16x16.png">' . "\n"
. '<link rel="manifest" href="' . $basePath . '/site.webmanifest">' . "\n"
. '<link rel="shortcut icon" href="' . $basePath . '/favicon.ico">' . "\n";
}
private static function log(string $message, string $priority = 'info'): void
{
$priorities = [
'info' => Log::INFO,
'warning' => Log::WARNING,
'error' => Log::ERROR,
];
Log::addLogger(
['text_file' => 'mokoonyx.log.php'],
Log::ALL,
['mokoonyx']
);
Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokoonyx');
}
}

1
src/helper/index.html Normal file
View File

@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

165
src/helper/minify.php Normal file
View File

@@ -0,0 +1,165 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* CSS/JS minifier — generates .min files from source when dev mode is off,
* deletes them when dev mode is on.
*/
defined('_JEXEC') or die;
class MokoMinifyHelper
{
/**
* Files to minify: source path relative to template media root.
* The .min variant is derived automatically (template.css → template.min.css).
*/
private const CSS_FILES = [
'css/template.css',
'css/offline.css',
'css/editor.css',
'css/a11y-high-contrast.css',
'css/theme/light.standard.css',
'css/theme/dark.standard.css',
'css/theme/light.custom.css',
'css/theme/dark.custom.css',
];
private const JS_FILES = [
'js/template.js',
];
/**
* When dev mode is ON: delete all .min files.
* When dev mode is OFF: regenerate .min files if source is newer.
*
* @param string $mediaRoot Absolute path to the template media directory.
* @param bool $devMode Whether development mode is enabled.
*/
public static function sync(string $mediaRoot, bool $devMode): void
{
$mediaRoot = rtrim($mediaRoot, '/\\');
foreach (self::CSS_FILES as $relPath) {
$source = $mediaRoot . '/' . $relPath;
$min = self::minPath($source);
if ($devMode) {
self::deleteIfExists($min);
} else {
self::buildIfStale($source, $min, 'css');
}
}
foreach (self::JS_FILES as $relPath) {
$source = $mediaRoot . '/' . $relPath;
$min = self::minPath($source);
if ($devMode) {
self::deleteIfExists($min);
} else {
self::buildIfStale($source, $min, 'js');
}
}
}
/**
* Derive the .min path from a source path.
* template.css → template.min.css
*/
private static function minPath(string $path): string
{
$info = pathinfo($path);
return $info['dirname'] . '/' . $info['filename'] . '.min.' . $info['extension'];
}
/**
* Delete a file if it exists.
*/
private static function deleteIfExists(string $path): void
{
if (is_file($path)) {
@unlink($path);
}
}
/**
* Build the minified file if the source is newer or the min file is missing.
*/
private static function buildIfStale(string $source, string $min, string $type): void
{
if (!is_file($source)) {
return;
}
// Skip if min file exists and is newer than source
if (is_file($min) && filemtime($min) >= filemtime($source)) {
return;
}
$content = file_get_contents($source);
if ($content === false) {
return;
}
$minified = ($type === 'css')
? self::minifyCss($content)
: self::minifyJs($content);
file_put_contents($min, $minified);
}
/**
* Minify CSS by stripping comments, excess whitespace, and unnecessary characters.
*/
private static function minifyCss(string $css): string
{
// Remove comments (but keep IE hacks like /*\*/)
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
// Remove whitespace around { } : ; , > + ~
$css = preg_replace('/\s*([{}:;,>+~])\s*/', '$1', $css);
// Remove remaining newlines and tabs
$css = preg_replace('/\s+/', ' ', $css);
// Remove spaces around selectors
$css = str_replace(['{ ', ' {', '; ', ' ;'], ['{', '{', ';', ';'], $css);
// Remove trailing semicolons before closing braces
$css = str_replace(';}', '}', $css);
// Remove leading/trailing whitespace
return trim($css);
}
/**
* Minify JS by stripping single-line comments, multi-line comments,
* and collapsing whitespace. Preserves string literals.
*/
private static function minifyJs(string $js): string
{
// Remove multi-line comments
$js = preg_replace('!/\*.*?\*/!s', '', $js);
// Remove single-line comments (but not URLs like http://)
$js = preg_replace('!(?<=^|[\s;{}()\[\]])//[^\n]*!m', '', $js);
// Collapse whitespace
$js = preg_replace('/\s+/', ' ', $js);
// Remove spaces around operators and punctuation
$js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js);
// Restore necessary spaces (after keywords)
$js = preg_replace('/(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)([^\s;})><=!&|?:,])/', '$1 $2', $js);
return trim($js);
}
}

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,135 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
// Load Bootstrap TOC assets
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->useStyle('vendor.bootstrap-toc');
$wa->useScript('vendor.bootstrap-toc.js');
// Get article params
$params = $this->item->params;
$images = json_decode($this->item->images);
$urls = json_decode($this->item->urls);
$canEdit = $params->get('access-edit');
$info = $params->get('info_block_position', 0);
// Check if associations are implemented
$assocParam = (Associations::isEnabled() && $params->get('show_associations'));
?>
<div class="com-content-article item-page<?php echo $this->pageclass_sfx; ?>">
<div class="row">
<!-- Table of Contents - Left Side -->
<div class="col-lg-3 col-md-4 order-md-1 mb-4">
<div class="sticky-top toc-wrapper" style="top: 20px;">
<nav id="toc" data-toggle="toc" class="toc-container">
<h5 class="toc-title"><?php echo Text::_('TPL_MOKOONYX_TOC_TITLE'); ?></h5>
</nav>
</div>
</div>
<!-- Article Content -->
<div class="col-lg-9 col-md-8 order-md-2">
<meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>" />
<?php if ($this->params->get('show_page_heading')) : ?>
<div class="page-header">
<h1><?php echo $this->escape($this->params->get('page_heading')); ?></h1>
</div>
<?php endif; ?>
<?php if (!$this->print) : ?>
<?php if ($canEdit || $params->get('show_print_icon') || $params->get('show_email_icon')) : ?>
<?php echo LayoutHelper::render('joomla.content.icons', ['params' => $params, 'item' => $this->item, 'print' => false]); ?>
<?php endif; ?>
<?php else : ?>
<?php if ($params->get('show_print_icon')) : ?>
<?php echo LayoutHelper::render('joomla.content.icons', ['params' => $params, 'item' => $this->item, 'print' => true]); ?>
<?php endif; ?>
<?php endif; ?>
<?php echo $this->item->event->afterDisplayTitle; ?>
<?php if ($params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<?php echo $this->item->event->beforeDisplayContent; ?>
<?php if (isset($urls) && ((!empty($urls->urls_position) && $urls->urls_position == '0') || ($params->get('urls_position') == '0' && empty($urls->urls_position))) || (empty($urls->urls_position) && (!$params->get('urls_position')))) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php if ($params->get('access-view')) : ?>
<?php echo LayoutHelper::render('joomla.content.full_image', $this->item); ?>
<?php if (isset($info) && $info == 0 && $params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'above']); ?>
<?php endif; ?>
<?php if ($info == 0 && $params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<div class="article-content" itemprop="articleBody" data-toc-scope>
<?php echo $this->item->text; ?>
</div>
<?php if (isset($urls) && ((!empty($urls->urls_position) && $urls->urls_position == '1') || ($params->get('urls_position') == '1'))) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php elseif ($params->get('show_noauth') == true && $this->user->get('guest')) : ?>
<?php echo LayoutHelper::render('joomla.content.intro_image', $this->item); ?>
<?php echo HTMLHelper::_('content.prepare', $this->item->introtext); ?>
<?php endif; ?>
<?php echo $this->item->event->afterDisplayContent; ?>
<?php if (isset($info) && ($info == 1 || $info == 2)) : ?>
<?php if ($params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'below']); ?>
<?php endif; ?>
</div>
</div>
</div>
<style>
.toc-container {
background: var(--cassiopeia-color-bg, #fff);
border: 1px solid var(--cassiopeia-color-border, #dee2e6);
border-radius: 0.375rem;
padding: 1rem;
}
.toc-title {
margin-bottom: 0.75rem;
font-size: 1rem;
font-weight: 600;
color: var(--cassiopeia-color-text, #212529);
border-bottom: 1px solid var(--cassiopeia-color-border, #dee2e6);
padding-bottom: 0.5rem;
}
@media (max-width: 767.98px) {
.toc-wrapper {
position: static !important;
margin-bottom: 1.5rem;
}
}
</style>

View File

@@ -0,0 +1,135 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
// Load Bootstrap TOC assets
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->useStyle('vendor.bootstrap-toc');
$wa->useScript('vendor.bootstrap-toc.js');
// Get article params
$params = $this->item->params;
$images = json_decode($this->item->images);
$urls = json_decode($this->item->urls);
$canEdit = $params->get('access-edit');
$info = $params->get('info_block_position', 0);
// Check if associations are implemented
$assocParam = (Associations::isEnabled() && $params->get('show_associations'));
?>
<div class="com-content-article item-page<?php echo $this->pageclass_sfx; ?>">
<div class="row">
<!-- Article Content -->
<div class="col-lg-9 col-md-8 order-md-1">
<meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>" />
<?php if ($this->params->get('show_page_heading')) : ?>
<div class="page-header">
<h1><?php echo $this->escape($this->params->get('page_heading')); ?></h1>
</div>
<?php endif; ?>
<?php if (!$this->print) : ?>
<?php if ($canEdit || $params->get('show_print_icon') || $params->get('show_email_icon')) : ?>
<?php echo LayoutHelper::render('joomla.content.icons', ['params' => $params, 'item' => $this->item, 'print' => false]); ?>
<?php endif; ?>
<?php else : ?>
<?php if ($params->get('show_print_icon')) : ?>
<?php echo LayoutHelper::render('joomla.content.icons', ['params' => $params, 'item' => $this->item, 'print' => true]); ?>
<?php endif; ?>
<?php endif; ?>
<?php echo $this->item->event->afterDisplayTitle; ?>
<?php if ($params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<?php echo $this->item->event->beforeDisplayContent; ?>
<?php if (isset($urls) && ((!empty($urls->urls_position) && $urls->urls_position == '0') || ($params->get('urls_position') == '0' && empty($urls->urls_position))) || (empty($urls->urls_position) && (!$params->get('urls_position')))) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php if ($params->get('access-view')) : ?>
<?php echo LayoutHelper::render('joomla.content.full_image', $this->item); ?>
<?php if (isset($info) && $info == 0 && $params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'above']); ?>
<?php endif; ?>
<?php if ($info == 0 && $params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<div class="article-content" itemprop="articleBody" data-toc-scope>
<?php echo $this->item->text; ?>
</div>
<?php if (isset($urls) && ((!empty($urls->urls_position) && $urls->urls_position == '1') || ($params->get('urls_position') == '1'))) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php elseif ($params->get('show_noauth') == true && $this->user->get('guest')) : ?>
<?php echo LayoutHelper::render('joomla.content.intro_image', $this->item); ?>
<?php echo HTMLHelper::_('content.prepare', $this->item->introtext); ?>
<?php endif; ?>
<?php echo $this->item->event->afterDisplayContent; ?>
<?php if (isset($info) && ($info == 1 || $info == 2)) : ?>
<?php if ($params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'below']); ?>
<?php endif; ?>
</div>
<!-- Table of Contents - Right Side -->
<div class="col-lg-3 col-md-4 order-md-2 mb-4">
<div class="sticky-top toc-wrapper" style="top: 20px;">
<nav id="toc" data-toggle="toc" class="toc-container">
<h5 class="toc-title"><?php echo Text::_('TPL_MOKOONYX_TOC_TITLE'); ?></h5>
</nav>
</div>
</div>
</div>
</div>
<style>
.toc-container {
background: var(--cassiopeia-color-bg, #fff);
border: 1px solid var(--cassiopeia-color-border, #dee2e6);
border-radius: 0.375rem;
padding: 1rem;
}
.toc-title {
margin-bottom: 0.75rem;
font-size: 1rem;
font-weight: 600;
color: var(--cassiopeia-color-text, #212529);
border-bottom: 1px solid var(--cassiopeia-color-border, #dee2e6);
padding-bottom: 0.5rem;
}
@media (max-width: 767.98px) {
.toc-wrapper {
position: static !important;
margin-bottom: 1.5rem;
}
}
</style>

View File

@@ -0,0 +1,36 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_archive.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-archive<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-archive__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-archive__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-archive__item">
<a href="<?php echo $item->link; ?>"><?php echo $item->text; ?></a>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,44 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_categories.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$showDescription = $params->get('show_description', 0);
$numitems = $params->get('numitems', 0);
?>
<div class="mod-articles-categories<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-categories__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-categories__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-categories__item">
<a href="<?php echo $item->link; ?>"><?php echo $item->title; ?></a>
<?php if ($numitems) : ?>
<span class="mod-articles-categories__count">(<?php echo $item->numitems; ?>)</span>
<?php endif; ?>
<?php if ($showDescription && !empty($item->description)) : ?>
<p class="mod-articles-categories__description"><?php echo $item->description; ?></p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,78 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_category.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
Factory::getApplication()->getLanguage()->load('mod_articles_category', JPATH_SITE);
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-category<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-category__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-category__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-category__item" itemscope itemtype="https://schema.org/Article">
<?php if ($params->get('link_titles') == 1) : ?>
<a class="mod-articles-category__link" href="<?php echo $item->link; ?>" itemprop="url">
<span itemprop="name"><?php echo $item->title; ?></span>
</a>
<?php else : ?>
<span itemprop="name"><?php echo $item->title; ?></span>
<?php endif; ?>
<?php if ($item->displayHits) : ?>
<span class="mod-articles-category__hits">
(<?php echo $item->displayHits; ?>)
</span>
<?php endif; ?>
<?php if ($params->get('show_author', 0)) : ?>
<span class="mod-articles-category__author">
<?php echo $item->displayAuthorName; ?>
</span>
<?php endif; ?>
<?php if ($item->displayDate) : ?>
<time class="mod-articles-category__date" datetime="<?php echo HTMLHelper::_('date', $item->displayDate, 'c'); ?>" itemprop="datePublished">
<?php echo $item->displayDate; ?>
</time>
<?php endif; ?>
<?php if ($params->get('show_introtext', 0)) : ?>
<div class="mod-articles-category__intro" itemprop="description">
<?php echo $item->displayIntrotext; ?>
</div>
<?php endif; ?>
<?php if ($params->get('show_readmore', 0)) : ?>
<a class="mod-articles-category__readmore" href="<?php echo $item->link; ?>" itemprop="url">
<?php echo Text::_('MOD_ARTICLES_CATEGORY_READ_MORE_TITLE'); ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_latest.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-latest<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-latest__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-latest__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-latest__item" itemscope itemtype="https://schema.org/Article">
<a href="<?php echo $item->link; ?>" itemprop="url">
<span itemprop="name"><?php echo $item->title; ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,58 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_news (newsflash).
* Adds showtitle support with card-based layout.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-news newsflash<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-news__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php foreach ($list as $item) : ?>
<div class="mod-articles-news__item" itemscope itemtype="https://schema.org/Article">
<?php if ($params->get('item_title')) : ?>
<h4 class="mod-articles-news__item-title" itemprop="name">
<?php if ($item->link !== '' && $params->get('link_titles')) : ?>
<a href="<?php echo $item->link; ?>" itemprop="url"><?php echo $item->title; ?></a>
<?php else : ?>
<?php echo $item->title; ?>
<?php endif; ?>
</h4>
<?php endif; ?>
<?php if (!empty($item->afterDisplayTitle)) : ?>
<?php echo $item->afterDisplayTitle; ?>
<?php endif; ?>
<?php if ($params->get('show_introtext', 1)) : ?>
<div class="mod-articles-news__intro" itemprop="description">
<?php echo $item->introtext; ?>
</div>
<?php endif; ?>
<?php if (isset($item->readmore) && $item->readmore) : ?>
<a class="mod-articles-news__readmore" href="<?php echo $item->link; ?>" itemprop="url">
<?php echo $item->linkText; ?>
</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_popular.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-popular<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-popular__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-popular__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-popular__item" itemscope itemtype="https://schema.org/Article">
<a href="<?php echo $item->link; ?>" itemprop="url">
<span itemprop="name"><?php echo $item->title; ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,52 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_banners.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-banners<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-banners__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php foreach ($list as $item) : ?>
<div class="mod-banners__item">
<?php $link = $item->params->get('url') ?: ''; ?>
<?php if ($item->type == 1) : ?>
<?php // Image banner ?>
<?php $imageUrl = $item->params->get('imageurl', ''); ?>
<?php $alt = htmlspecialchars($item->name, ENT_COMPAT, 'UTF-8'); ?>
<?php if ($link) : ?>
<a href="<?php echo htmlspecialchars($link, ENT_COMPAT, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
<img src="<?php echo htmlspecialchars($imageUrl, ENT_COMPAT, 'UTF-8'); ?>" alt="<?php echo $alt; ?>" class="mod-banners__image" loading="lazy" />
</a>
<?php else : ?>
<img src="<?php echo htmlspecialchars($imageUrl, ENT_COMPAT, 'UTF-8'); ?>" alt="<?php echo $alt; ?>" class="mod-banners__image" loading="lazy" />
<?php endif; ?>
<?php else : ?>
<?php // Custom HTML banner ?>
<?php echo $item->custombannercode; ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,53 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_breadcrumbs.
* Bootstrap 5 breadcrumb with schema.org BreadcrumbList markup.
* Module settings (showHome, showLast, homeText) are handled by Joomla core
* before $list reaches this template.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$showHere = $params->get('showHere', 1);
if (empty($list)) {
return;
}
?>
<nav class="mod-breadcrumbs<?php echo $suffix ? ' ' . $suffix : ''; ?>" aria-label="<?php echo Text::_('MOD_BREADCRUMBS_HERE'); ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-breadcrumbs__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php if ($showHere) : ?>
<span class="mod-breadcrumbs__here"><?php echo Text::_('MOD_BREADCRUMBS_HERE'); ?></span>
<?php endif; ?>
<ol class="breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">
<?php foreach ($list as $key => $item) : ?>
<?php $isLast = ($key === array_key_last($list)); ?>
<li class="breadcrumb-item<?php echo $isLast ? ' active' : ''; ?>" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"
<?php echo $isLast ? ' aria-current="page"' : ''; ?>>
<?php if (!$isLast && !empty($item->link)) : ?>
<a href="<?php echo $item->link; ?>" itemprop="item">
<span itemprop="name"><?php echo $item->name; ?></span>
</a>
<?php else : ?>
<span itemprop="name"><?php echo $item->name; ?></span>
<?php endif; ?>
<meta itemprop="position" content="<?php echo $key + 1; ?>" />
</li>
<?php endforeach; ?>
</ol>
</nav>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,39 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_custom.
* Adds showtitle support and respects all module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Uri\Uri;
$modId = 'mod-custom' . $module->id;
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
if ($params->get('backgroundimage')) {
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $app->getDocument()->getWebAssetManager();
$wa->addInlineStyle(
'#' . $modId . '{background-image: url("' . Uri::root(true) . '/' . HTMLHelper::_('cleanImageURL', $params->get('backgroundimage'))->url . '");}',
['name' => $modId]
);
}
?>
<div class="mod-custom custom<?php echo $suffix ? ' ' . $suffix : ''; ?>" id="<?php echo $modId; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-custom__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php echo $module->content; ?>
</div>

View File

@@ -0,0 +1,42 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Template override for mod_custom adding banner-overlay wrapper pattern.
* Based on Cassiopeia's banner layout approach.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Uri\Uri;
$modId = 'mod-custom' . $module->id;
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
if ($params->get('backgroundimage')) {
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $app->getDocument()->getWebAssetManager();
$wa->addInlineStyle(
'#' . $modId . '{background-image: url("' . Uri::root(true) . '/' . HTMLHelper::_('cleanImageURL', $params->get('backgroundimage'))->url . '");}',
['name' => $modId]
);
}
?>
<div class="mod-custom custom banner-overlay custom-hero<?php echo $suffix ? ' ' . $suffix : ''; ?>" id="<?php echo $modId; ?>">
<div class="overlay">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-custom__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php echo $module->content; ?>
</div>
</div>

View File

@@ -0,0 +1,88 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_feed.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
if (!$feed) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$rssurl = $params->get('rssurl', '');
$rsstitle = $params->get('rsstitle', 1);
$rssdesc = $params->get('rssrtl', 0) ? ' feed-rtl' : '';
$rssimage = $params->get('rssimage', 1);
$rssitems = $params->get('rssitems', 5);
$rssitemdesc = $params->get('rssitemdesc', 1);
$word_count = $params->get('word_count', 0);
?>
<div class="mod-feed<?php echo $suffix ? ' ' . $suffix : ''; ?><?php echo $rssdesc; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-feed__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php if ($feed->title && $rsstitle) : ?>
<h4 class="mod-feed__feed-title">
<?php if (!empty($rssurl)) : ?>
<a href="<?php echo htmlspecialchars($rssurl, ENT_COMPAT, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
<?php echo $feed->title; ?>
</a>
<?php else : ?>
<?php echo $feed->title; ?>
<?php endif; ?>
</h4>
<?php endif; ?>
<?php if ($feed->description && $rssdesc) : ?>
<p class="mod-feed__description"><?php echo $feed->description; ?></p>
<?php endif; ?>
<?php if ($rssimage && $feed->image) : ?>
<img src="<?php echo $feed->image->uri; ?>" alt="<?php echo $feed->image->title ?? ''; ?>" class="mod-feed__image" />
<?php endif; ?>
<?php if (!empty($feed->items)) : ?>
<ul class="mod-feed__list">
<?php for ($i = 0, $max = min(count($feed->items), $rssitems); $i < $max; $i++) :
$item = $feed->items[$i];
?>
<li class="mod-feed__item">
<?php if (!empty($item->uri)) : ?>
<a href="<?php echo htmlspecialchars($item->uri, ENT_COMPAT, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
<?php echo $item->title; ?>
</a>
<?php else : ?>
<?php echo $item->title; ?>
<?php endif; ?>
<?php if ($rssitemdesc && !empty($item->content)) :
$desc = $item->content;
if ($word_count) {
$words = explode(' ', strip_tags($desc));
if (count($words) > $word_count) {
$desc = implode(' ', array_slice($words, 0, $word_count)) . '&hellip;';
}
}
?>
<p class="mod-feed__item-description"><?php echo $desc; ?></p>
<?php endif; ?>
</li>
<?php endfor; ?>
</ul>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,85 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_finder (Smart Search).
* Bootstrap 5 search form with showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
// Load component language for search labels
$lang = $app->getLanguage();
$lang->load('com_finder', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$showLabel = $params->get('show_label', 1);
$labelClass = (!$showLabel ? 'visually-hidden ' : '') . 'finder';
Text::script('MOD_FINDER_SEARCH_VALUE');
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $app->getDocument()->getWebAssetManager();
$wa->getRegistry()->addExtensionRegistryFile('com_finder');
if ($params->get('show_autosuggest', 1)) {
$wa->usePreset('awesomplete');
$app->getDocument()->addScriptOptions('finder-search', ['url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false)]);
Text::script('COM_FINDER_SEARCH_FORM_LIST_LABEL');
Text::script('JLIB_JS_AJAX_ERROR_OTHER');
Text::script('JLIB_JS_AJAX_ERROR_PARSE');
}
$wa->useScript('com_finder.finder');
?>
<div class="mod-finder<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-finder__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<form class="mod-finder__form js-finder-searchform form-search" action="<?php echo Route::_($route); ?>" method="get" role="search">
<label for="mod-finder-searchword<?php echo $module->id; ?>" class="<?php echo $labelClass; ?>">
<?php echo $params->get('alt_label', Text::_('JSEARCH_FILTER_SUBMIT')); ?>
</label>
<div class="input-group">
<input type="text" name="q" id="mod-finder-searchword<?php echo $module->id; ?>"
class="js-finder-search-query form-control"
value="<?php echo htmlspecialchars($app->getInput()->get('q', '', 'string'), ENT_COMPAT, 'UTF-8'); ?>"
placeholder="<?php echo Text::_('MOD_FINDER_SEARCH_VALUE'); ?>">
<?php if ($params->get('show_button', 0)) : ?>
<button class="btn btn-primary" type="submit">
<span class="fa-solid fa-magnifying-glass" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('JSEARCH_FILTER_SUBMIT'); ?></span>
</button>
<?php endif; ?>
</div>
<?php $show_advanced = $params->get('show_advanced', 0); ?>
<?php if ($show_advanced == 2) : ?>
<a href="<?php echo Route::_($route); ?>" class="mod-finder__advanced-link mt-2 d-inline-block">
<?php echo Text::_('COM_FINDER_ADVANCED_SEARCH'); ?>
</a>
<?php elseif ($show_advanced == 1) : ?>
<div class="mod-finder__advanced js-finder-advanced mt-2">
<?php echo HTMLHelper::_('filter.select', $query, $params); ?>
</div>
<?php endif; ?>
<?php
$finderHelper = $app->bootModule('mod_finder', 'site')->getHelper('FinderHelper');
echo $finderHelper->getHiddenFields($route);
?>
</form>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,29 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_footer.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-footer<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-footer__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<div class="mod-footer__line1"><?php echo $lineone; ?></div>
<div class="mod-footer__line2"><?php echo Text::_('MOD_FOOTER_LINE2'); ?></div>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,63 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_languages.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-languages<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-languages__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-languages__list">
<?php foreach ($list as $language) : ?>
<?php $isActive = $language->active ? ' active' : ''; ?>
<li class="mod-languages__item<?php echo $isActive; ?>" dir="<?php echo $language->rtl ? 'rtl' : 'ltr'; ?>">
<?php if ($language->active) : ?>
<span class="mod-languages__link mod-languages__link--active" lang="<?php echo $language->sef; ?>">
<?php else : ?>
<a class="mod-languages__link" href="<?php echo htmlspecialchars($language->link, ENT_COMPAT, 'UTF-8'); ?>" lang="<?php echo $language->sef; ?>">
<?php endif; ?>
<?php if ($params->get('image', 1)) : ?>
<?php if ($language->image) : ?>
<?php echo HTMLHelper::_('image', 'mod_languages/' . $language->image . '.gif', '', null, true); ?>
<?php else : ?>
<span class="mod-languages__badge badge bg-secondary"><?php echo strtoupper($language->sef); ?></span>
<?php endif; ?>
<?php endif; ?>
<?php if ($params->get('show_name', 1)) : ?>
<?php echo $language->title_native; ?>
<?php endif; ?>
<?php if ($language->active) : ?>
</span>
<?php else : ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,126 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_login.
* Bootstrap 5 login form with showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
Factory::getApplication()->getLanguage()->load('mod_login', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-login<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-login__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php if ($type === 'logout') : ?>
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-login__form mod-login__form--logout">
<?php if ($params->get('greeting', 1)) : ?>
<div class="mod-login__greeting">
<?php if (!empty($user->name)) : ?>
<span class="mod-login__name"><?php echo Text::sprintf('MOD_LOGIN_HINAME', htmlspecialchars($user->name, ENT_COMPAT, 'UTF-8')); ?></span>
<?php else : ?>
<span class="mod-login__name"><?php echo Text::sprintf('MOD_LOGIN_HINAME', htmlspecialchars($user->username, ENT_COMPAT, 'UTF-8')); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="mod-login__submit">
<button type="submit" name="Submit" class="btn btn-primary w-100"><?php echo Text::_('JLOGOUT'); ?></button>
</div>
<input type="hidden" name="option" value="com_users">
<input type="hidden" name="task" value="user.logout">
<input type="hidden" name="return" value="<?php echo $return; ?>">
<?php echo HTMLHelper::_('form.token'); ?>
</form>
<?php else : ?>
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-login__form mod-login__form--login">
<?php if ($params->get('pretext')) : ?>
<div class="mod-login__pretext"><?php echo $params->get('pretext'); ?></div>
<?php endif; ?>
<div class="mod-login__field mb-3">
<label for="modlgn-username-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-user" aria-hidden="true"></i></span>
<input id="modlgn-username-<?php echo $module->id; ?>" type="text" name="username" class="form-control" autocomplete="username" placeholder="<?php echo Text::_('JGLOBAL_USERNAME'); ?>">
</div>
</div>
<div class="mod-login__field mb-3">
<label for="modlgn-passwd-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-lock" aria-hidden="true"></i></span>
<input id="modlgn-passwd-<?php echo $module->id; ?>" type="password" name="password" class="form-control" autocomplete="current-password" placeholder="<?php echo Text::_('JGLOBAL_PASSWORD'); ?>">
</div>
</div>
<?php if (!empty($twofactormethods) && count($twofactormethods) > 1) : ?>
<div class="mod-login__field mb-3">
<label for="modlgn-secretkey-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-shield-halved" aria-hidden="true"></i></span>
<input id="modlgn-secretkey-<?php echo $module->id; ?>" type="text" name="secretkey" class="form-control" autocomplete="one-time-code" placeholder="<?php echo Text::_('JGLOBAL_SECRETKEY'); ?>">
</div>
</div>
<?php endif; ?>
<?php if ($params->get('remember', 1)) : ?>
<div class="mod-login__remember form-check mb-3">
<input id="modlgn-remember-<?php echo $module->id; ?>" type="checkbox" name="remember" class="form-check-input" value="yes">
<label for="modlgn-remember-<?php echo $module->id; ?>" class="form-check-label"><?php echo Text::_('JGLOBAL_REMEMBER_ME'); ?></label>
</div>
<?php endif; ?>
<div class="mod-login__submit mb-3">
<button type="submit" name="Submit" class="btn btn-primary w-100"><?php echo Text::_('JLOGIN'); ?></button>
</div>
<?php $usersConfig = \Joomla\CMS\Component\ComponentHelper::getParams('com_users'); ?>
<ul class="mod-login__options list-unstyled small">
<?php if ($usersConfig->get('allowUserRegistration')) : ?>
<li>
<a href="<?php echo Route::_('index.php?option=com_users&view=registration'); ?>">
<?php echo Text::_('MOD_LOGIN_REGISTER'); ?>
</a>
</li>
<?php endif; ?>
<li>
<a href="<?php echo Route::_('index.php?option=com_users&view=remind'); ?>">
<?php echo Text::_('MOD_LOGIN_FORGOT_YOUR_USERNAME'); ?>
</a>
</li>
<li>
<a href="<?php echo Route::_('index.php?option=com_users&view=reset'); ?>">
<?php echo Text::_('MOD_LOGIN_FORGOT_YOUR_PASSWORD'); ?>
</a>
</li>
</ul>
<input type="hidden" name="option" value="com_users">
<input type="hidden" name="task" value="user.login">
<input type="hidden" name="return" value="<?php echo $return; ?>">
<?php echo HTMLHelper::_('form.token'); ?>
<?php if ($params->get('posttext')) : ?>
<div class="mod-login__posttext"><?php echo $params->get('posttext'); ?></div>
<?php endif; ?>
</form>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,95 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_menu.
* Simple list menu with showtitle support, suitable for sidebars and footers.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Helper\ModuleHelper;
$id = '';
if ($tagId = $params->get('tag_id', '')) {
$id = ' id="' . $tagId . '"';
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<nav class="mod-menu<?php echo $suffix ? ' ' . $suffix : ''; ?>"<?php echo $id; ?> aria-label="<?php echo htmlspecialchars($module->title, ENT_COMPAT, 'UTF-8'); ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-menu__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-menu__list nav flex-column">
<?php foreach ($list as $i => &$item) :
$itemParams = $item->getParams();
$class = 'nav-item mod-menu__item item-' . $item->id;
if ($item->id == $default_id) {
$class .= ' default';
}
if ($item->id == $active_id || ($item->type === 'alias' && $itemParams->get('aliasoptions') == $active_id)) {
$class .= ' current';
}
if (in_array($item->id, $path)) {
$class .= ' active';
} elseif ($item->type === 'alias') {
$aliasToId = $itemParams->get('aliasoptions');
if (count($path) > 0 && $aliasToId == $path[count($path) - 1]) {
$class .= ' active';
} elseif (in_array($aliasToId, $path)) {
$class .= ' alias-parent-active';
}
}
if ($item->type === 'separator') {
$class .= ' divider';
}
if ($item->deeper) {
$class .= ' deeper';
}
if ($item->parent) {
$class .= ' parent';
}
echo '<li class="' . $class . '">';
switch ($item->type) :
case 'separator':
case 'component':
case 'heading':
case 'url':
require ModuleHelper::getLayoutPath('mod_menu', 'default_' . $item->type);
break;
default:
require ModuleHelper::getLayoutPath('mod_menu', 'default_url');
break;
endswitch;
if ($item->deeper) {
echo '<ul class="mod-menu__sub nav flex-column ms-3">';
} elseif ($item->shallower) {
echo '</li>';
echo str_repeat('</ul></li>', $item->level_diff);
} else {
echo '</li>';
}
endforeach;
?></ul>
</nav>

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