Template
refactor: restructure template into samples/ + source/ layout
Replace per-type types/<type>/ scaffolds with a samples/ reference directory (manifest XML + install script templates for all six extension types) and a single source/ build directory that CI scans. - Add samples/manifest/*.xml for component, template, module, plugin, package, and library - Add samples/script/*.php install/update script templates - Add source/ as the canonical build root (CI scans source/src/htdocs) - Remove types/ per-type scaffolds - Rewrite root README for the new structure and expanded CI suite - Anchor Python MANIFEST ignore rule to root (/MANIFEST) so it no longer swallows samples/manifest/ on case-insensitive filesystems - Expand ci-joomla.yml: SQL, language-key, PHPCS, security, updates.xml, asset, MVC naming, router, ACL, webservices, ZIP dry-run, JS/CSS checks Authored-by: Moko Consulting
This commit is contained in:
+1
-1
@@ -180,7 +180,7 @@ __pycache__/
|
||||
*.egg
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
MANIFEST
|
||||
/MANIFEST
|
||||
develop-eggs/
|
||||
downloads/
|
||||
eggs/
|
||||
|
||||
@@ -392,38 +392,135 @@ jobs:
|
||||
echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for FILE in $SQL_FILES; do
|
||||
# Basic syntax check: balanced parentheses, no empty files
|
||||
# 1. Empty / whitespace-only file check
|
||||
SIZE=$(wc -c < "$FILE" | tr -d ' ')
|
||||
if [ "$SIZE" -eq 0 ]; then
|
||||
echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "::error file=${FILE}::Empty SQL file"
|
||||
echo "- **Empty** SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check for common SQL errors
|
||||
if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then
|
||||
echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
CONTENT_SIZE=$(grep -cP '\S' "$FILE" 2>/dev/null || echo "0")
|
||||
if [ "$CONTENT_SIZE" -eq 0 ]; then
|
||||
echo "::error file=${FILE}::Whitespace-only SQL file"
|
||||
echo "- **Whitespace-only** SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 2. Joomla table prefix — must use #__ not hardcoded prefixes
|
||||
# Match table names in CREATE/ALTER/DROP/INSERT/UPDATE/DELETE that don't use #__
|
||||
HARDCODED=$(grep -nPi '(CREATE\s+TABLE|ALTER\s+TABLE|DROP\s+TABLE|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+(`?)(?!#__)[a-z][a-z0-9_]+\2' "$FILE" 2>/dev/null || true)
|
||||
if [ -n "$HARDCODED" ]; then
|
||||
COUNT=$(echo "$HARDCODED" | wc -l)
|
||||
echo "::error file=${FILE}::${COUNT} table reference(s) missing #__ prefix"
|
||||
echo " - **${COUNT} hardcoded table name(s)** — must use \`#__\` prefix" >> $GITHUB_STEP_SUMMARY
|
||||
while IFS= read -r LINE; do
|
||||
LINE_NUM=$(echo "$LINE" | cut -d: -f1)
|
||||
LINE_TEXT=$(echo "$LINE" | cut -d: -f2- | sed 's/^ *//')
|
||||
echo " - Line ${LINE_NUM}: \`${LINE_TEXT}\`" >> $GITHUB_STEP_SUMMARY
|
||||
done <<< "$HARDCODED"
|
||||
ERRORS=$((ERRORS + COUNT))
|
||||
fi
|
||||
|
||||
# 3. CREATE TABLE must use IF NOT EXISTS
|
||||
BAD_CREATE=$(grep -nPi 'CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)' "$FILE" 2>/dev/null || true)
|
||||
if [ -n "$BAD_CREATE" ]; then
|
||||
COUNT=$(echo "$BAD_CREATE" | wc -l)
|
||||
echo "::error file=${FILE}::${COUNT} CREATE TABLE without IF NOT EXISTS"
|
||||
echo " - **${COUNT} CREATE TABLE** missing \`IF NOT EXISTS\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + COUNT))
|
||||
fi
|
||||
|
||||
# 4. DROP TABLE must use IF EXISTS
|
||||
BAD_DROP=$(grep -nPi 'DROP\s+TABLE\s+(?!IF\s+EXISTS)' "$FILE" 2>/dev/null || true)
|
||||
if [ -n "$BAD_DROP" ]; then
|
||||
COUNT=$(echo "$BAD_DROP" | wc -l)
|
||||
echo "::error file=${FILE}::${COUNT} DROP TABLE without IF EXISTS"
|
||||
echo " - **${COUNT} DROP TABLE** missing \`IF EXISTS\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + COUNT))
|
||||
fi
|
||||
|
||||
# 5. Balanced parentheses in CREATE TABLE statements
|
||||
OPEN=$(grep -oP '\(' "$FILE" 2>/dev/null | wc -l)
|
||||
CLOSE=$(grep -oP '\)' "$FILE" 2>/dev/null | wc -l)
|
||||
if [ "$OPEN" -ne "$CLOSE" ]; then
|
||||
echo "::error file=${FILE}::Unbalanced parentheses (${OPEN} open vs ${CLOSE} close)"
|
||||
echo " - **Unbalanced parentheses**: ${OPEN} \`(\` vs ${CLOSE} \`)\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
# 6. ENGINE=InnoDB must be specified on CREATE TABLE
|
||||
if grep -qPi 'CREATE\s+TABLE' "$FILE" 2>/dev/null; then
|
||||
if ! grep -qPi 'ENGINE\s*=\s*InnoDB' "$FILE" 2>/dev/null; then
|
||||
echo "::error file=${FILE}::CREATE TABLE missing ENGINE=InnoDB"
|
||||
echo " - **Missing \`ENGINE=InnoDB\`** — required for Joomla 4+/5" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# 7. DEFAULT CHARSET=utf8mb4 must be specified on CREATE TABLE
|
||||
if grep -qPi 'CREATE\s+TABLE' "$FILE" 2>/dev/null; then
|
||||
if ! grep -qPi 'CHARSET\s*=\s*utf8mb4|CHARACTER\s+SET\s+utf8mb4' "$FILE" 2>/dev/null; then
|
||||
echo "::error file=${FILE}::CREATE TABLE missing DEFAULT CHARSET=utf8mb4"
|
||||
echo " - **Missing \`DEFAULT CHARSET=utf8mb4\`** — required for Joomla 4+/5" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Check update SQL files follow version numbering pattern
|
||||
UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||
if [ -n "$UPDATE_DIR" ]; then
|
||||
BAD_NAMES=0
|
||||
for UFILE in "$UPDATE_DIR"/*.sql; do
|
||||
[ ! -f "$UFILE" ] && continue
|
||||
BASENAME=$(basename "$UFILE" .sql)
|
||||
if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then
|
||||
echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY
|
||||
BAD_NAMES=$((BAD_NAMES + 1))
|
||||
# 8. Update SQL files must follow version numbering pattern
|
||||
UPDATE_DIRS=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null)
|
||||
if [ -n "$UPDATE_DIRS" ]; then
|
||||
while IFS= read -r UPDATE_DIR; do
|
||||
for UFILE in "$UPDATE_DIR"/*.sql; do
|
||||
[ ! -f "$UFILE" ] && continue
|
||||
BASENAME=$(basename "$UFILE" .sql)
|
||||
if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then
|
||||
echo "::error file=${UFILE}::Update file does not follow version naming (expected X.Y.Z.sql)"
|
||||
echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done
|
||||
done <<< "$UPDATE_DIRS"
|
||||
fi
|
||||
|
||||
# 9. Install/uninstall pairing — if install exists, uninstall must too
|
||||
INSTALL_FILES=$(find . -name "install.mysql.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
if [ -n "$INSTALL_FILES" ]; then
|
||||
while IFS= read -r INSTALL_FILE; do
|
||||
SQL_DIR=$(dirname "$INSTALL_FILE")
|
||||
if [ ! -f "${SQL_DIR}/uninstall.mysql.sql" ]; then
|
||||
echo "::error file=${INSTALL_FILE}::install.mysql.sql found but uninstall.mysql.sql is missing"
|
||||
echo "- **Missing \`uninstall.mysql.sql\`** in \`${SQL_DIR}/\` — every install must have a matching uninstall" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done <<< "$INSTALL_FILES"
|
||||
fi
|
||||
|
||||
# 10. Current manifest version should have an update SQL file
|
||||
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
|
||||
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||
if [ -n "$MANIFEST_VERSION" ]; then
|
||||
UPDATE_DIRS=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null)
|
||||
if [ -n "$UPDATE_DIRS" ]; then
|
||||
while IFS= read -r UPDATE_DIR; do
|
||||
if [ ! -f "${UPDATE_DIR}/${MANIFEST_VERSION}.sql" ]; then
|
||||
echo "::error file=${MANIFEST}::No update SQL file for current version ${MANIFEST_VERSION}"
|
||||
echo "- **Missing \`${UPDATE_DIR}/${MANIFEST_VERSION}.sql\`** — update file must exist for manifest version" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done <<< "$UPDATE_DIRS"
|
||||
fi
|
||||
done
|
||||
if [ "$BAD_NAMES" -gt 0 ]; then
|
||||
ERRORS=$((ERRORS + BAD_NAMES))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -979,6 +1076,842 @@ jobs:
|
||||
echo "**Empty language keys check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: Language key usage consistency
|
||||
run: |
|
||||
echo "### Language Key Usage Consistency" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
# Collect all defined language keys from .ini files
|
||||
LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
if [ -z "$LANG_FILES" ]; then
|
||||
echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Build list of defined keys
|
||||
DEFINED_KEYS=$(cat $LANG_FILES 2>/dev/null | grep -oP '^[A-Z][A-Z0-9_]+(?==)' | sort -u)
|
||||
if [ -z "$DEFINED_KEYS" ]; then
|
||||
echo "No language keys defined — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
DEFINED_COUNT=$(echo "$DEFINED_KEYS" | wc -l)
|
||||
echo "Found ${DEFINED_COUNT} defined key(s) across .ini files" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Find keys used in PHP files (Text::_('KEY'), Text::sprintf('KEY'), etc.)
|
||||
SRC_DIR=""
|
||||
for DIR in source/ src/ htdocs/; do
|
||||
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
||||
done
|
||||
|
||||
if [ -n "$SRC_DIR" ]; then
|
||||
USED_IN_PHP=$(grep -rohP "Text::(?:_|sprintf|plural|alt)\(\s*['\"]([A-Z][A-Z0-9_]+)['\"]" "$SRC_DIR" --include="*.php" 2>/dev/null | grep -oP "[A-Z][A-Z0-9_]+" | sort -u || true)
|
||||
fi
|
||||
|
||||
# Find keys used in XML files (label=, description=, etc.)
|
||||
USED_IN_XML=$(find . -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -ohP '(?:label|description|title|message|note)="([A-Z][A-Z0-9_]+)"' {} \; 2>/dev/null | grep -oP '[A-Z][A-Z0-9_]+' | sort -u || true)
|
||||
|
||||
ALL_USED=$(printf '%s\n%s' "$USED_IN_PHP" "$USED_IN_XML" | sort -u | grep -v '^$' || true)
|
||||
|
||||
if [ -n "$ALL_USED" ]; then
|
||||
# Keys used but not defined
|
||||
MISSING=$(comm -23 <(echo "$ALL_USED") <(echo "$DEFINED_KEYS") 2>/dev/null || true)
|
||||
if [ -n "$MISSING" ]; then
|
||||
COUNT=$(echo "$MISSING" | wc -l)
|
||||
echo "::error::${COUNT} language key(s) used in code but not defined in .ini files"
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Used but not defined:**" >> $GITHUB_STEP_SUMMARY
|
||||
while IFS= read -r KEY; do
|
||||
echo "- \`${KEY}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
done <<< "$MISSING"
|
||||
fi
|
||||
else
|
||||
echo "No language key usage found in source files." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} undefined language key(s) — these will appear as raw key strings in the UI.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**Language key usage consistency passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: Orphan language keys
|
||||
run: |
|
||||
echo "### Orphan Language Keys" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
if [ -z "$LANG_FILES" ]; then
|
||||
echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
DEFINED_KEYS=$(cat $LANG_FILES 2>/dev/null | grep -oP '^[A-Z][A-Z0-9_]+(?==)' | sort -u)
|
||||
if [ -z "$DEFINED_KEYS" ]; then
|
||||
echo "No language keys defined — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Search all PHP, XML, and INI files for key references
|
||||
ALL_SOURCE=$(find . \( -name "*.php" -o -name "*.xml" -o -name "*.ini" -o -name "*.js" \) -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null)
|
||||
ORPHANS=""
|
||||
while IFS= read -r KEY; do
|
||||
# Skip sys.ini _DESC/_TITLE keys — these are Joomla convention keys
|
||||
# Search for the key in all source files
|
||||
if ! grep -rql "$KEY" --include="*.php" --include="*.xml" --include="*.js" . 2>/dev/null | grep -qv '\.ini' 2>/dev/null; then
|
||||
ORPHANS="${ORPHANS}${KEY}\n"
|
||||
fi
|
||||
done <<< "$DEFINED_KEYS"
|
||||
|
||||
ORPHANS=$(printf '%b' "$ORPHANS" | grep -v '^$' || true)
|
||||
if [ -n "$ORPHANS" ]; then
|
||||
COUNT=$(echo "$ORPHANS" | wc -l)
|
||||
echo "::error::${COUNT} language key(s) defined in .ini but never referenced in code"
|
||||
echo "**Orphan keys (defined but unreferenced):**" >> $GITHUB_STEP_SUMMARY
|
||||
while IFS= read -r KEY; do
|
||||
echo "- \`${KEY}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
done <<< "$ORPHANS"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} orphan language key(s) — remove unused keys to reduce translation burden.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**Orphan language keys check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: PHP CodeSniffer (Joomla standard)
|
||||
run: |
|
||||
echo "### PHP CodeSniffer" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
SRC_DIR=""
|
||||
for DIR in source/ src/ htdocs/; do
|
||||
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
||||
done
|
||||
|
||||
if [ -z "$SRC_DIR" ]; then
|
||||
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Install PHPCS + Joomla coding standard if not already available
|
||||
PHPCS=""
|
||||
if [ -f "vendor/bin/phpcs" ]; then
|
||||
PHPCS="vendor/bin/phpcs"
|
||||
else
|
||||
composer global require --no-interaction --quiet \
|
||||
squizlabs/php_codesniffer \
|
||||
joomla/coding-standards 2>/dev/null || true
|
||||
GLOBAL_BIN=$(composer global config bin-dir --absolute 2>/dev/null)
|
||||
if [ -f "${GLOBAL_BIN}/phpcs" ]; then
|
||||
PHPCS="${GLOBAL_BIN}/phpcs"
|
||||
# Register Joomla standard
|
||||
GLOBAL_VENDOR=$(composer global config vendor-dir --absolute 2>/dev/null)
|
||||
if [ -d "${GLOBAL_VENDOR}/joomla/coding-standards" ]; then
|
||||
$PHPCS --config-set installed_paths "${GLOBAL_VENDOR}/joomla/coding-standards" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$PHPCS" ]; then
|
||||
echo "Could not install PHP CodeSniffer — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Use repo phpcs.xml if present, otherwise use Joomla standard
|
||||
ARGS="--report=summary --no-colors"
|
||||
if [ -f "phpcs.xml" ] || [ -f "phpcs.xml.dist" ] || [ -f ".phpcs.xml" ]; then
|
||||
echo "Using project PHPCS config." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Check if Joomla standard is available
|
||||
if $PHPCS -i 2>/dev/null | grep -qi "Joomla"; then
|
||||
ARGS="$ARGS --standard=Joomla"
|
||||
echo "Using Joomla coding standard." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
ARGS="$ARGS --standard=PSR12"
|
||||
echo "Joomla standard not available — falling back to PSR-12." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
ARGS="$ARGS ${SRC_DIR}"
|
||||
fi
|
||||
|
||||
$PHPCS $ARGS 2>&1 | tee /tmp/phpcs-output.txt
|
||||
EXIT=${PIPESTATUS[0]}
|
||||
|
||||
if [ $EXIT -eq 0 ]; then
|
||||
echo "**No coding standard violations found.**" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat /tmp/phpcs-output.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Coding standard violations found.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Security patterns check
|
||||
run: |
|
||||
echo "### Security Patterns" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
SRC_DIR=""
|
||||
for DIR in source/ src/ htdocs/; do
|
||||
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
||||
done
|
||||
|
||||
if [ -z "$SRC_DIR" ]; then
|
||||
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# 1. Direct superglobal access ($_GET, $_POST, $_REQUEST, $_COOKIE)
|
||||
SUPERGLOBAL_HITS=$(grep -rnP '\$_(GET|POST|REQUEST|COOKIE|FILES)\b' "$SRC_DIR" --include="*.php" 2>/dev/null || true)
|
||||
if [ -n "$SUPERGLOBAL_HITS" ]; then
|
||||
COUNT=$(echo "$SUPERGLOBAL_HITS" | wc -l)
|
||||
echo "::error::${COUNT} direct superglobal access(es) — use Joomla Input API instead"
|
||||
echo "**Direct superglobal access (use \`\$app->getInput()\` or \`InputFilter\`):**" >> $GITHUB_STEP_SUMMARY
|
||||
while IFS= read -r HIT; do
|
||||
FILE=$(echo "$HIT" | cut -d: -f1)
|
||||
LINE=$(echo "$HIT" | cut -d: -f2)
|
||||
echo "- \`${FILE}:${LINE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
done <<< "$SUPERGLOBAL_HITS"
|
||||
ERRORS=$((ERRORS + COUNT))
|
||||
fi
|
||||
|
||||
# 2. Raw SQL query construction (string concatenation in query)
|
||||
RAW_SQL=$(grep -rnP '\$db->setQuery\(\s*["\x27]' "$SRC_DIR" --include="*.php" 2>/dev/null || true)
|
||||
if [ -n "$RAW_SQL" ]; then
|
||||
COUNT=$(echo "$RAW_SQL" | wc -l)
|
||||
echo "::error::${COUNT} raw SQL string(s) in setQuery() — use query builder or prepared statements"
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Raw SQL strings in \`setQuery()\` (use query builder):**" >> $GITHUB_STEP_SUMMARY
|
||||
while IFS= read -r HIT; do
|
||||
FILE=$(echo "$HIT" | cut -d: -f1)
|
||||
LINE=$(echo "$HIT" | cut -d: -f2)
|
||||
echo "- \`${FILE}:${LINE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
done <<< "$RAW_SQL"
|
||||
ERRORS=$((ERRORS + COUNT))
|
||||
fi
|
||||
|
||||
# 3. eval() usage
|
||||
EVAL_HITS=$(grep -rnP '\beval\s*\(' "$SRC_DIR" --include="*.php" 2>/dev/null || true)
|
||||
if [ -n "$EVAL_HITS" ]; then
|
||||
COUNT=$(echo "$EVAL_HITS" | wc -l)
|
||||
echo "::error::${COUNT} eval() usage(s) — never use eval in extensions"
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**\`eval()\` usage (critical security risk):**" >> $GITHUB_STEP_SUMMARY
|
||||
while IFS= read -r HIT; do
|
||||
FILE=$(echo "$HIT" | cut -d: -f1)
|
||||
LINE=$(echo "$HIT" | cut -d: -f2)
|
||||
echo "- \`${FILE}:${LINE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
done <<< "$EVAL_HITS"
|
||||
ERRORS=$((ERRORS + COUNT))
|
||||
fi
|
||||
|
||||
# 4. Missing defined('_JEXEC') or JDEFINED guard
|
||||
while IFS= read -r -d '' PHP_FILE; do
|
||||
FIRST_PHP=$(grep -nP '^\s*<\?php' "$PHP_FILE" 2>/dev/null | head -1)
|
||||
if [ -n "$FIRST_PHP" ]; then
|
||||
# Check first 5 non-empty lines after <?php for JEXEC guard
|
||||
if ! head -15 "$PHP_FILE" | grep -qP "defined\s*\(\s*['\"]_JEXEC|defined\s*\(\s*['\"]JPATH_BASE" 2>/dev/null; then
|
||||
# Skip service provider files (they use a different entry point)
|
||||
if ! echo "$PHP_FILE" | grep -q "provider.php"; then
|
||||
echo "::error file=${PHP_FILE}::Missing _JEXEC direct access guard"
|
||||
echo "- \`${PHP_FILE}\`: missing \`defined('_JEXEC') or die;\` guard" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0 2>/dev/null)
|
||||
|
||||
if [ "$ERRORS" -eq 0 ]; then
|
||||
echo "No security issues found." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} security issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**Security patterns check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: updates.xml structure validation
|
||||
run: |
|
||||
echo "### updates.xml Validation" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
UPDATE_XML=$(find . -maxdepth 2 -name "updates.xml" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null | head -1)
|
||||
if [ -z "$UPDATE_XML" ]; then
|
||||
echo "No updates.xml found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "Found: \`${UPDATE_XML}\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 1. Well-formed XML
|
||||
if command -v php &> /dev/null; then
|
||||
if ! php -r "@simplexml_load_file('$UPDATE_XML') ?: exit(1);" 2>/dev/null; then
|
||||
echo "::error file=${UPDATE_XML}::updates.xml is not well-formed XML"
|
||||
echo "- **Malformed XML** — cannot be parsed" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "- Well-formed XML ✓" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 2. Must have <updates> root element
|
||||
if ! grep -q '<updates>' "$UPDATE_XML" 2>/dev/null; then
|
||||
echo "::error file=${UPDATE_XML}::Missing <updates> root element"
|
||||
echo "- **Missing** \`<updates>\` root element" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "- \`<updates>\` root element ✓" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# 3. Each <update> must have required children
|
||||
UPDATE_COUNT=$(grep -c '<update>' "$UPDATE_XML" 2>/dev/null || echo "0")
|
||||
if [ "$UPDATE_COUNT" -eq 0 ]; then
|
||||
echo "::error file=${UPDATE_XML}::No <update> entries found"
|
||||
echo "- **No \`<update>\` entries** — file is empty" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "- ${UPDATE_COUNT} \`<update>\` entr(ies) found" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for TAG in name version type downloadurl; do
|
||||
if ! grep -q "<${TAG}>" "$UPDATE_XML" 2>/dev/null; then
|
||||
echo "::error file=${UPDATE_XML}::Missing required <${TAG}> in update entry"
|
||||
echo "- **Missing** \`<${TAG}>\` in update entry" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "- \`<${TAG}>\` present ✓" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. Must have <targetplatform>
|
||||
if ! grep -q '<targetplatform' "$UPDATE_XML" 2>/dev/null; then
|
||||
echo "::error file=${UPDATE_XML}::Missing <targetplatform> — Joomla cannot filter by compatible version"
|
||||
echo "- **Missing** \`<targetplatform>\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "- \`<targetplatform>\` present ✓" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} updates.xml issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**updates.xml validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: CSS/JS asset existence check
|
||||
run: |
|
||||
echo "### CSS/JS Asset Existence" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
SRC_DIR=""
|
||||
for DIR in source/ src/ htdocs/; do
|
||||
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
||||
done
|
||||
|
||||
if [ -z "$SRC_DIR" ]; then
|
||||
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Find WebAssetManager registrations in PHP
|
||||
# registerAndUseScript/Style, useScript/Style with asset name
|
||||
ASSET_REFS=$(grep -rohP "(?:registerAndUse|register|use)(?:Script|Style)\(\s*['\"]([^'\"]+)['\"]" "$SRC_DIR" --include="*.php" 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'\"" | sort -u || true)
|
||||
|
||||
# Find joomla.asset.json files
|
||||
ASSET_JSONS=$(find . -name "joomla.asset.json" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
|
||||
# Find media directories
|
||||
MEDIA_DIRS=$(find . -type d -name "media" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null)
|
||||
|
||||
if [ -z "$ASSET_REFS" ] && [ -z "$ASSET_JSONS" ]; then
|
||||
echo "No asset registrations found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Validate joomla.asset.json is well-formed
|
||||
if [ -n "$ASSET_JSONS" ]; then
|
||||
for ASSET_JSON in $ASSET_JSONS; do
|
||||
if command -v php &> /dev/null; then
|
||||
VALID=$(php -r "echo json_decode(file_get_contents('$ASSET_JSON')) ? 'VALID' : 'INVALID';" 2>/dev/null)
|
||||
if [ "$VALID" != "VALID" ]; then
|
||||
echo "::error file=${ASSET_JSON}::joomla.asset.json is not valid JSON"
|
||||
echo "- **Invalid JSON**: \`${ASSET_JSON}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "- \`${ASSET_JSON}\`: valid JSON ✓" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Check that referenced URIs in asset JSON point to existing files
|
||||
URIS=$(php -r "
|
||||
\$j = json_decode(file_get_contents('$ASSET_JSON'), true);
|
||||
\$assets = array_merge(\$j['assets'] ?? [], []);
|
||||
foreach (\$assets as \$a) {
|
||||
if (isset(\$a['uri'])) echo \$a['uri'] . PHP_EOL;
|
||||
if (isset(\$a['css'])) foreach ((array)\$a['css'] as \$c) echo (is_array(\$c) ? \$c['uri'] ?? '' : \$c) . PHP_EOL;
|
||||
if (isset(\$a['js'])) foreach ((array)\$a['js'] as \$c) echo (is_array(\$c) ? \$c['uri'] ?? '' : \$c) . PHP_EOL;
|
||||
}
|
||||
" 2>/dev/null | grep -v '^$' | grep -v '^http' || true)
|
||||
|
||||
ASSET_DIR=$(dirname "$ASSET_JSON")
|
||||
for URI in $URIS; do
|
||||
# Resolve relative path from asset JSON location
|
||||
if [ ! -f "${ASSET_DIR}/${URI}" ]; then
|
||||
echo "::error file=${ASSET_JSON}::Asset URI '${URI}' references missing file"
|
||||
echo " - **Missing**: \`${URI}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} asset issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**CSS/JS asset existence check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: MVC naming conventions check
|
||||
run: |
|
||||
echo "### MVC Naming Conventions" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
SRC_DIR=""
|
||||
for DIR in source/ src/ htdocs/; do
|
||||
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
||||
done
|
||||
|
||||
if [ -z "$SRC_DIR" ]; then
|
||||
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Check View classes — file must match class name pattern
|
||||
VIEW_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/View/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
if [ -n "$VIEW_FILES" ]; then
|
||||
for FILE in $VIEW_FILES; do
|
||||
BASENAME=$(basename "$FILE" .php)
|
||||
# View class should contain class declaration matching filename
|
||||
CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1)
|
||||
if [ -n "$CLASS_NAME" ]; then
|
||||
# Class name should end with View (HtmlView, JsonView, etc.)
|
||||
if ! echo "$CLASS_NAME" | grep -qP 'View$'; then
|
||||
echo "::error file=${FILE}::View class '${CLASS_NAME}' does not end with 'View'"
|
||||
echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`View\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check Controller classes
|
||||
CTRL_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/Controller/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
if [ -n "$CTRL_FILES" ]; then
|
||||
for FILE in $CTRL_FILES; do
|
||||
CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1)
|
||||
if [ -n "$CLASS_NAME" ]; then
|
||||
if ! echo "$CLASS_NAME" | grep -qP 'Controller$'; then
|
||||
echo "::error file=${FILE}::Controller class '${CLASS_NAME}' does not end with 'Controller'"
|
||||
echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`Controller\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check Model classes
|
||||
MODEL_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/Model/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
if [ -n "$MODEL_FILES" ]; then
|
||||
for FILE in $MODEL_FILES; do
|
||||
CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1)
|
||||
if [ -n "$CLASS_NAME" ]; then
|
||||
if ! echo "$CLASS_NAME" | grep -qP 'Model$'; then
|
||||
echo "::error file=${FILE}::Model class '${CLASS_NAME}' does not end with 'Model'"
|
||||
echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`Model\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check Table classes
|
||||
TABLE_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/Table/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
if [ -n "$TABLE_FILES" ]; then
|
||||
for FILE in $TABLE_FILES; do
|
||||
CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1)
|
||||
if [ -n "$CLASS_NAME" ]; then
|
||||
if ! echo "$CLASS_NAME" | grep -qP 'Table$'; then
|
||||
echo "::error file=${FILE}::Table class '${CLASS_NAME}' does not end with 'Table'"
|
||||
echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`Table\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$VIEW_FILES" ] && [ -z "$CTRL_FILES" ] && [ -z "$MODEL_FILES" ] && [ -z "$TABLE_FILES" ]; then
|
||||
echo "No MVC classes found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} MVC naming issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**MVC naming conventions check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: Router validation
|
||||
run: |
|
||||
echo "### Router Validation" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
# Only applies to components
|
||||
COMP_MANIFESTS=$(find . -maxdepth 10 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./.claude/*" -exec grep -l '<extension[^>]*type="component"' {} \; 2>/dev/null || true)
|
||||
|
||||
if [ -z "$COMP_MANIFESTS" ]; then
|
||||
echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
for MANIFEST in $COMP_MANIFESTS; do
|
||||
COMP_DIR=$(dirname "$MANIFEST")
|
||||
COMP_NAME=$(basename "$COMP_DIR")
|
||||
echo "Component: \`${COMP_NAME}\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Check for router class in src/Service/
|
||||
ROUTER_FILE=$(find "$COMP_DIR" -name "Router.php" -path "*/Service/*" -not -path "./vendor/*" 2>/dev/null | head -1)
|
||||
if [ -z "$ROUTER_FILE" ]; then
|
||||
# Also check for legacy router.php
|
||||
ROUTER_FILE=$(find "$COMP_DIR" -name "router.php" -not -path "./vendor/*" 2>/dev/null | head -1)
|
||||
fi
|
||||
|
||||
if [ -z "$ROUTER_FILE" ]; then
|
||||
echo "::error file=${MANIFEST}::Component '${COMP_NAME}' has no Router class"
|
||||
echo "- **Missing** Router — SEF URLs will not work" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "- Router found: \`${ROUTER_FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Check router implements RouterServiceInterface or RouterInterface
|
||||
if ! grep -qP 'RouterServiceInterface|RouterInterface|RouterBase' "$ROUTER_FILE" 2>/dev/null; then
|
||||
echo "::error file=${ROUTER_FILE}::Router does not implement RouterServiceInterface or RouterInterface"
|
||||
echo " - **Does not implement** required router interface" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo " - Implements router interface ✓" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} router issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**Router validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: ACL language keys check
|
||||
run: |
|
||||
echo "### ACL Language Keys" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
ACCESS_FILES=$(find . -name "access.xml" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
if [ -z "$ACCESS_FILES" ]; then
|
||||
echo "No access.xml files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Collect all defined language keys
|
||||
LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||
DEFINED_KEYS=""
|
||||
if [ -n "$LANG_FILES" ]; then
|
||||
DEFINED_KEYS=$(cat $LANG_FILES 2>/dev/null | grep -oP '^[A-Z][A-Z0-9_]+(?==)' | sort -u)
|
||||
fi
|
||||
|
||||
for ACCESS_FILE in $ACCESS_FILES; do
|
||||
echo "Checking: \`${ACCESS_FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Extract action names and titles/descriptions from access.xml
|
||||
ACTIONS=$(grep -oP '<action\s[^>]*name="\K[^"]+' "$ACCESS_FILE" 2>/dev/null || true)
|
||||
TITLES=$(grep -oP '<action\s[^>]*title="\K[^"]+' "$ACCESS_FILE" 2>/dev/null || true)
|
||||
DESCS=$(grep -oP '<action\s[^>]*description="\K[^"]+' "$ACCESS_FILE" 2>/dev/null || true)
|
||||
|
||||
# Check title keys are defined
|
||||
for KEY in $TITLES $DESCS; do
|
||||
# Only check keys that look like language constants (ALL_CAPS_WITH_UNDERSCORES)
|
||||
if echo "$KEY" | grep -qP '^[A-Z][A-Z0-9_]+$'; then
|
||||
if [ -n "$DEFINED_KEYS" ]; then
|
||||
if ! echo "$DEFINED_KEYS" | grep -qx "$KEY"; then
|
||||
echo "::error file=${ACCESS_FILE}::ACL key '${KEY}' not defined in language files"
|
||||
echo "- **Undefined** ACL key: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} ACL language key issue(s) — these will show raw key strings in permissions UI.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**ACL language keys check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: Webservices plugin API manifest check
|
||||
run: |
|
||||
echo "### Webservices API Manifest" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
# Find webservices plugin manifests
|
||||
WS_MANIFESTS=$(find . -maxdepth 10 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./.claude/*" -exec grep -l '<extension[^>]*type="plugin"' {} \; 2>/dev/null | while read -r F; do
|
||||
if grep -q 'group="webservices"' "$F" 2>/dev/null; then
|
||||
echo "$F"
|
||||
fi
|
||||
done)
|
||||
|
||||
if [ -z "$WS_MANIFESTS" ]; then
|
||||
echo "No webservices plugins found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
for MANIFEST in $WS_MANIFESTS; do
|
||||
WS_DIR=$(dirname "$MANIFEST")
|
||||
WS_NAME=$(basename "$WS_DIR")
|
||||
echo "Webservices plugin: \`${WS_NAME}\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Find the plugin PHP file
|
||||
WS_PHP=$(find "$WS_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" 2>/dev/null | head -1)
|
||||
if [ -z "$WS_PHP" ]; then
|
||||
WS_PHP=$(find "$WS_DIR" -name "*.php" -not -name "provider.php" -not -path "*/services/*" -not -path "./vendor/*" 2>/dev/null | head -1)
|
||||
fi
|
||||
|
||||
if [ -n "$WS_PHP" ]; then
|
||||
# Check it implements WebserviceInterface or registers routes
|
||||
if ! grep -qP 'WebserviceInterface|onBeforeApiRoute|ApiRouter' "$WS_PHP" 2>/dev/null; then
|
||||
echo "::error file=${WS_PHP}::Webservices plugin does not implement WebserviceInterface or register API routes"
|
||||
echo "- \`${WS_PHP}\`: missing \`WebserviceInterface\` or route registration" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "- \`${WS_PHP}\`: API routing ✓" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
else
|
||||
echo "- No plugin PHP class found in \`${WS_DIR}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} webservices API issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**Webservices API manifest check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: ZIP dry-run build
|
||||
run: |
|
||||
echo "### ZIP Dry-Run Build" >> $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 [ -z "$MANIFEST" ]; then
|
||||
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||
|
||||
# Collect all files that should be in the ZIP
|
||||
MISSING_FILES=""
|
||||
TOTAL_FILES=0
|
||||
|
||||
# Files from <filename> tags
|
||||
for F in $(grep -oP '<filename[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true); do
|
||||
TOTAL_FILES=$((TOTAL_FILES + 1))
|
||||
if [ ! -f "${MANIFEST_DIR}/${F}" ]; then
|
||||
MISSING_FILES="${MISSING_FILES}${F}\n"
|
||||
fi
|
||||
done
|
||||
|
||||
# Folders from <folder> tags
|
||||
for F in $(grep -oP '<folder[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true); do
|
||||
TOTAL_FILES=$((TOTAL_FILES + 1))
|
||||
if [ ! -d "${MANIFEST_DIR}/${F}" ]; then
|
||||
MISSING_FILES="${MISSING_FILES}${F}/\n"
|
||||
fi
|
||||
done
|
||||
|
||||
# Script file
|
||||
SCRIPT=$(grep -oP '<scriptfile>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||
if [ -n "$SCRIPT" ]; then
|
||||
TOTAL_FILES=$((TOTAL_FILES + 1))
|
||||
if [ ! -f "${MANIFEST_DIR}/${SCRIPT}" ]; then
|
||||
MISSING_FILES="${MISSING_FILES}${SCRIPT}\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Media folder
|
||||
MEDIA_FOLDER=$(grep -oP '<media[^>]*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||
if [ -n "$MEDIA_FOLDER" ]; then
|
||||
TOTAL_FILES=$((TOTAL_FILES + 1))
|
||||
if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then
|
||||
MISSING_FILES="${MISSING_FILES}${MEDIA_FOLDER}/\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
# SQL folder
|
||||
SQL_FOLDER=$(grep -oP '<install>\s*<sql>\s*<file[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||
if [ -n "$SQL_FOLDER" ]; then
|
||||
SQL_DIR=$(dirname "$SQL_FOLDER")
|
||||
TOTAL_FILES=$((TOTAL_FILES + 1))
|
||||
if [ ! -f "${MANIFEST_DIR}/${SQL_FOLDER}" ]; then
|
||||
MISSING_FILES="${MISSING_FILES}${SQL_FOLDER}\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
MISSING_FILES=$(printf '%b' "$MISSING_FILES" | grep -v '^$' || true)
|
||||
if [ -n "$MISSING_FILES" ]; then
|
||||
COUNT=$(echo "$MISSING_FILES" | wc -l)
|
||||
echo "::error file=${MANIFEST}::ZIP build would fail — ${COUNT} referenced file(s) missing"
|
||||
echo "**Missing files that would cause ZIP build to fail:**" >> $GITHUB_STEP_SUMMARY
|
||||
while IFS= read -r F; do
|
||||
echo "- \`${F}\`" >> $GITHUB_STEP_SUMMARY
|
||||
done <<< "$MISSING_FILES"
|
||||
ERRORS=$((ERRORS + COUNT))
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Checked ${TOTAL_FILES} manifest reference(s) for \`${EXT_TYPE}\` extension." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} ZIP build issue(s) — package would be incomplete.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**ZIP dry-run build passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: JavaScript linting
|
||||
run: |
|
||||
echo "### JavaScript Linting" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
JS_FILES=$(find . -name "*.js" -not -name "*.min.js" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null)
|
||||
if [ -z "$JS_FILES" ]; then
|
||||
echo "No JavaScript files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
JS_COUNT=$(echo "$JS_FILES" | wc -l)
|
||||
echo "Found ${JS_COUNT} JavaScript file(s)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Check if Node.js is available
|
||||
if command -v node &> /dev/null && command -v npx &> /dev/null; then
|
||||
# Use project ESLint config if present, otherwise install and use defaults
|
||||
if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f ".eslintrc.yml" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then
|
||||
if [ -f "package.json" ]; then
|
||||
npm install --silent 2>/dev/null || true
|
||||
fi
|
||||
npx eslint $JS_FILES 2>&1 | tee /tmp/eslint-output.txt
|
||||
EXIT=${PIPESTATUS[0]}
|
||||
else
|
||||
# Basic syntax check without ESLint config
|
||||
for FILE in $JS_FILES; do
|
||||
if ! node --check "$FILE" 2>/tmp/js-syntax-err.txt; then
|
||||
echo "::error file=${FILE}::JavaScript syntax error"
|
||||
echo "- \`${FILE}\`: syntax error" >> $GITHUB_STEP_SUMMARY
|
||||
cat /tmp/js-syntax-err.txt >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done
|
||||
EXIT=$ERRORS
|
||||
fi
|
||||
|
||||
if [ $EXIT -ne 0 ] && [ -f /tmp/eslint-output.txt ]; then
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat /tmp/eslint-output.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
echo "Node.js not available — skipping JavaScript lint." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} JavaScript issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**JavaScript linting passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: CSS linting
|
||||
run: |
|
||||
echo "### CSS Linting" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
CSS_FILES=$(find . -name "*.css" -not -name "*.min.css" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null)
|
||||
if [ -z "$CSS_FILES" ]; then
|
||||
echo "No CSS files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
CSS_COUNT=$(echo "$CSS_FILES" | wc -l)
|
||||
echo "Found ${CSS_COUNT} CSS file(s)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for FILE in $CSS_FILES; do
|
||||
# Basic CSS validation: check for unbalanced braces
|
||||
OPEN=$(grep -oP '\{' "$FILE" 2>/dev/null | wc -l)
|
||||
CLOSE=$(grep -oP '\}' "$FILE" 2>/dev/null | wc -l)
|
||||
if [ "$OPEN" -ne "$CLOSE" ]; then
|
||||
echo "::error file=${FILE}::Unbalanced braces (${OPEN} open vs ${CLOSE} close)"
|
||||
echo "- \`${FILE}\`: **unbalanced braces** (${OPEN} \`{\` vs ${CLOSE} \`}\`)" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
# Check for empty file
|
||||
SIZE=$(wc -c < "$FILE" | tr -d ' ')
|
||||
if [ "$SIZE" -eq 0 ]; then
|
||||
echo "::error file=${FILE}::Empty CSS file"
|
||||
echo "- \`${FILE}\`: **empty file**" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Use Stylelint if available and configured
|
||||
if command -v npx &> /dev/null; then
|
||||
if [ -f ".stylelintrc" ] || [ -f ".stylelintrc.json" ] || [ -f "stylelint.config.js" ] || [ -f "stylelint.config.mjs" ]; then
|
||||
if [ -f "package.json" ]; then
|
||||
npm install --silent 2>/dev/null || true
|
||||
fi
|
||||
npx stylelint "**/*.css" --ignore-pattern "*.min.css" --ignore-pattern "node_modules" --ignore-pattern "vendor" 2>&1 | tee /tmp/stylelint-output.txt
|
||||
EXIT=${PIPESTATUS[0]}
|
||||
if [ $EXIT -ne 0 ]; then
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat /tmp/stylelint-output.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} CSS issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**CSS linting passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
release-readiness:
|
||||
name: Release Readiness Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,70 +1,73 @@
|
||||
# Template-Joomla
|
||||
|
||||
Unified scaffolding templates for all Joomla extension types.
|
||||
Unified scaffolding templates for all Joomla extension types — component, template, module, plugin, package, and library.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
.gitea/workflows/ ← Standard 10-workflow CI/CD suite
|
||||
types/
|
||||
plugin/ ← Joomla plugin scaffold
|
||||
template/ ← Joomla site template scaffold (full src/)
|
||||
module/ ← Joomla module scaffold
|
||||
component/ ← Joomla component scaffold
|
||||
package/ ← Joomla package scaffold
|
||||
library/ ← Joomla library scaffold
|
||||
.mokogitea/workflows/ ← CI/CD workflow suite (lint, validate, release)
|
||||
samples/
|
||||
manifest/ ← Reference manifest XML for each extension type
|
||||
component_sample.xml
|
||||
template_sample.xml
|
||||
module_sample.xml
|
||||
plugin_sample.xml
|
||||
package_sample.xml
|
||||
library_sample.xml
|
||||
script/ ← Reference install/update script (script.php) templates
|
||||
script_component.php
|
||||
script_module.php
|
||||
script_plugin.php
|
||||
script_package.php
|
||||
source/ ← Your extension source lives here (CI scans this dir)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Create a new repo from this template (or clone manually)
|
||||
2. Copy the relevant `types/<type>/` content to your project root
|
||||
3. Customize the manifest XML, namespace, and source files
|
||||
4. Workflows are already configured — push to trigger CI
|
||||
1. Create a new repo from this template (or clone manually).
|
||||
2. Build your extension under `source/`.
|
||||
3. Copy the matching manifest from `samples/manifest/` to `source/`, rename it, and
|
||||
replace the placeholder name, namespace, and version.
|
||||
4. If your extension needs install/update logic, copy the matching
|
||||
`samples/script/` file and replace the `{REPONAME}`/`{PACKAGENAME}` placeholders.
|
||||
5. Push — the workflows validate the manifest, source, SQL, language keys, and build.
|
||||
|
||||
## Shared Files
|
||||
## Samples
|
||||
|
||||
The root contains files shared by all extension types:
|
||||
- `.gitea/workflows/` — CI/CD (auto-release, ci-joomla, pr-check, etc.)
|
||||
- `.editorconfig`, `.gitignore`, `.gitattributes` — editor/git config
|
||||
- `composer.json`, `phpstan.neon`, `Makefile` — build tooling
|
||||
- `updates.xml` — Joomla update server manifest template
|
||||
- `LICENSE`, `SECURITY.md`, `CONTRIBUTING.md` — standard docs
|
||||
`samples/` holds read-only reference material — do not build against it directly.
|
||||
|
||||
## Workflow Suite (10 workflows)
|
||||
- **`manifest/`** — a well-formed `<extension>` manifest for each of the six
|
||||
Joomla extension types, showing the expected structure, namespace declaration,
|
||||
media/SQL wiring, and update-server hookup.
|
||||
- **`script/`** — the standard `script.php` install/uninstall/update handler
|
||||
pattern shared across Moko extension repos. Copy, then replace `{REPONAME}`
|
||||
(UPPERCASE) and `{PACKAGENAME}` (e.g. `com_myextension`).
|
||||
|
||||
| Workflow | Purpose |
|
||||
|----------|---------|
|
||||
| `auto-release.yml` | Stable release on PR merge to main |
|
||||
| `pre-release.yml` | Manual dev/alpha/beta/rc builds |
|
||||
| `ci-joomla.yml` | PHP lint, PHPStan, coding standards |
|
||||
| `pr-check.yml` | PR gate: manifest + build validation |
|
||||
| `deploy-manual.yml` | Manual SFTP deploy |
|
||||
| `repo-health.yml` | Structure compliance checks |
|
||||
| `update-server.yml` | updates.xml validation |
|
||||
| `security-audit.yml` | Dependency vulnerability scanning |
|
||||
| `notify.yml` | ntfy notifications |
|
||||
| `cleanup.yml` | Merged branch + old run cleanup |
|
||||
## CI/CD
|
||||
|
||||
## Version Policy
|
||||
`.mokogitea/workflows/ci-joomla.yml` validates every push and PR against `source/`:
|
||||
|
||||
- **Stable** (PR merge to main): Minor bump (`03.00.07` -> `03.01.00`)
|
||||
- **Pre-release** (manual): Patch bump (`03.00.07` -> `03.00.08`)
|
||||
| Check | What it verifies |
|
||||
|-------|------------------|
|
||||
| Manifest | Well-formed XML, required elements, version |
|
||||
| SQL | `#__` prefixes, `IF [NOT] EXISTS`, `ENGINE=InnoDB`, `utf8mb4`, install/uninstall pairing, version-named update files |
|
||||
| Language keys | Keys used in code are defined; orphan (unused) keys flagged |
|
||||
| PHP CodeSniffer | Joomla coding standard (falls back to PSR-12) |
|
||||
| Security | No direct superglobals, raw SQL in `setQuery()`, `eval()`, or missing `_JEXEC` guard |
|
||||
| updates.xml | Well-formed, required `<update>` children, `<targetplatform>` |
|
||||
| Assets | `joomla.asset.json` is valid and its URIs exist |
|
||||
| MVC naming | View/Controller/Model/Table classes follow suffix conventions |
|
||||
| Router | Components ship a Router implementing the router interface |
|
||||
| ACL | `access.xml` action keys are defined in language files |
|
||||
| Webservices | Webservices plugins register API routes |
|
||||
| ZIP dry-run | Every file/folder the manifest references actually exists |
|
||||
| JS/CSS | Syntax + balanced braces (ESLint/Stylelint if configured) |
|
||||
|
||||
## Release Cascade
|
||||
## Standards
|
||||
|
||||
Higher releases auto-delete lower pre-release channels:
|
||||
- stable -> all | rc -> beta,alpha,dev | beta -> alpha,dev | alpha -> dev
|
||||
|
||||
## Platform Type
|
||||
|
||||
`.mokostandards` platform: `joomla-template` or `waas-component`
|
||||
|
||||
## Canonical Source
|
||||
|
||||
This repo is the **single source of truth** for Joomla workflows and scaffolding.
|
||||
Sync to production repos by copying `.gitea/workflows/` from here.
|
||||
This repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home).
|
||||
Documentation lives in the [Template-Joomla Wiki](https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla/wiki).
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-or-later - Moko Consulting
|
||||
GPL-3.0-or-later — Moko Consulting
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="component" method="upgrade">
|
||||
<name>Moko Component</name>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>July 2026</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>1.0.0</version>
|
||||
<description>A premium core business engine component engineered by Moko Consulting for Joomla 6 platform solutions.</description>
|
||||
|
||||
<namespace path="src">MokoConsulting\Component\MyExtension</namespace>
|
||||
|
||||
<files folder="site">
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
</files>
|
||||
|
||||
<media destination="com_myextension" folder="media">
|
||||
<filename>joomla.asset.json</filename>
|
||||
<folder>css</folder>
|
||||
<folder>js</folder>
|
||||
</media>
|
||||
|
||||
<administration>
|
||||
<menu link="option=com_myextension">Moko Component</menu>
|
||||
<files folder="admin">
|
||||
<folder>services</folder>
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
<filename>myextension.xml</filename>
|
||||
</files>
|
||||
|
||||
<languages folder="admin">
|
||||
<language tag="en-GB">language/en-GB/com_myextension.ini</language>
|
||||
<language tag="en-GB">language/en-GB/com_myextension.sys.ini</language>
|
||||
<language tag="en-US">language/en-US/com_myextension.ini</language>
|
||||
<language tag="en-US">language/en-US/com_myextension.sys.ini</language>
|
||||
</languages>
|
||||
</administration>
|
||||
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="Moko Consulting Updates" dlid="id">
|
||||
https://update.mokoconsulting.tech/com_myextension.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
</extension>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="library" method="upgrade">
|
||||
<name>Moko Shared Core Library</name>
|
||||
<libraryname>mokolibrary</libraryname>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>July 2026</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>1.0.0</version>
|
||||
<description>Centralized helper framework and base API abstraction wrappers used across all active Moko Consulting extension matrices.</description>
|
||||
|
||||
<namespace path="src">MokoConsulting\Library\MokoLibrary</namespace>
|
||||
|
||||
<files>
|
||||
<folder>src</folder>
|
||||
</files>
|
||||
|
||||
<languages>
|
||||
<language tag="en-GB">language/en-GB/lib_mokolibrary.sys.ini</language>
|
||||
<language tag="en-US">language/en-US/lib_mokolibrary.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="Moko Consulting Updates" dlid="id">
|
||||
https://update.mokoconsulting.tech/lib_mokolibrary.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
</extension>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="module" client="site" method="upgrade">
|
||||
<name>Moko Dynamic Content Showcase</name>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>July 2026</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>1.0.0</version>
|
||||
<description>A high-performance asynchronous frontend display module designed to cleanly inject dynamic layout matrices into module zones.</description>
|
||||
|
||||
<namespace path="src">MokoConsulting\Module\MokoModule</namespace>
|
||||
|
||||
<files>
|
||||
<folder module="mod_mokomodule">services</folder>
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
<filename>mod_mokomodule.xml</filename>
|
||||
</files>
|
||||
|
||||
<languages>
|
||||
<language tag="en-GB">language/en-GB/mod_mokomodule.ini</language>
|
||||
<language tag="en-GB">language/en-GB/mod_mokomodule.sys.ini</language>
|
||||
<language tag="en-US">language/en-US/mod_mokomodule.ini</language>
|
||||
<language tag="en-US">language/en-US/mod_mokomodule.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic">
|
||||
<field name="layout" type="modulelayout" label="Select Layout File" />
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="Moko Consulting Updates" dlid="id">
|
||||
https://update.mokoconsulting.tech/mod_mokomodule.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
</extension>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Moko Full Suite Package</name>
|
||||
<packagename>mokopackage</packagename>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>July 2026</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>1.0.0</version>
|
||||
<description>All-in-one distribution bundle wrapping the Moko Component, Dynamic Showcase Module, and System Interceptor Plugin into a single installation step.</description>
|
||||
|
||||
<languages>
|
||||
<language tag="en-GB">en-GB.pkg_mokopackage.sys.ini</language>
|
||||
<language tag="en-US">en-US.pkg_mokopackage.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
<files folder="constituents">
|
||||
<file type="component" id="com_myextension">com_myextension.zip</file>
|
||||
<file type="module" id="mod_mokomodule" client="site">mod_mokomodule.zip</file>
|
||||
<file type="plugin" id="mokoplugin" group="system">plg_system_mokoplugin.zip</file>
|
||||
</files>
|
||||
|
||||
<updateservers>
|
||||
<server type="collection" priority="1" name="Moko Consulting Updates" dlid="id">
|
||||
https://update.mokoconsulting.tech/pkg_mokopackage.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
<blockChildUninstall>1</blockChildUninstall>
|
||||
</extension>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - Moko Automation Plugin</name>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>July 2026</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>1.0.0</version>
|
||||
<description>Advanced system-level interceptor framework developed by Moko Consulting for request optimizations in Joomla 6.</description>
|
||||
|
||||
<namespace path="src">MokoConsulting\Plugin\System\MokoPlugin</namespace>
|
||||
|
||||
<files>
|
||||
<folder plugin="mokoplugin">services</folder>
|
||||
<folder>src</folder>
|
||||
</files>
|
||||
|
||||
<languages>
|
||||
<language tag="en-GB">language/en-GB/plg_system_mokoplugin.ini</language>
|
||||
<language tag="en-GB">language/en-GB/plg_system_mokoplugin.sys.ini</language>
|
||||
<language tag="en-US">language/en-US/plg_system_mokoplugin.ini</language>
|
||||
<language tag="en-US">language/en-US/plg_system_mokoplugin.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic">
|
||||
<field name="example_param" type="text" label="API Endpoint Parameter" />
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="Moko Consulting Updates" dlid="id">
|
||||
https://update.mokoconsulting.tech/plg_system_mokoplugin.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
</extension>
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="template" client="site" method="upgrade">
|
||||
<name>tpl_mokotemplate</name>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>July 2026</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>1.0.0</version>
|
||||
<description>Moko Corporate Template - A modern, high-performance responsive frontend design system for Joomla 6.</description>
|
||||
|
||||
<files>
|
||||
<filename>templateDetails.xml</filename>
|
||||
<filename>index.php</filename>
|
||||
<filename>template_thumbnail.png</filename>
|
||||
<filename>template_preview.png</filename>
|
||||
<folder>css</folder>
|
||||
<folder>js</folder>
|
||||
<folder>images</folder>
|
||||
<folder>html</folder>
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<positions>
|
||||
<position>topbar</position>
|
||||
<position>header</position>
|
||||
<position>menu</position>
|
||||
<position>breadcrumbs</position>
|
||||
<position>sidebar-left</position>
|
||||
<position>sidebar-right</position>
|
||||
<position>main-top</position>
|
||||
<position>main-bottom</position>
|
||||
<position>footer</position>
|
||||
</positions>
|
||||
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/tpl_mokotemplate.ini</language>
|
||||
<language tag="en-GB">en-GB/tpl_mokotemplate.sys.ini</language>
|
||||
<language tag="en-US">en-US/tpl_mokotemplate.ini</language>
|
||||
<language tag="en-US">en-US/tpl_mokotemplate.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic" label="Basic Parameters">
|
||||
<field name="logo" type="media" label="Select Corporate Logo" />
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="Moko Consulting Updates" dlid="id">
|
||||
https://update.mokoconsulting.tech/tpl_mokotemplate.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
</extension>
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* ============================================================================
|
||||
* INSTRUCTIONS FOR USE:
|
||||
* 1. This file template serves as the standard structure for all script files
|
||||
* across the extension repositories. Copy and implement this pattern
|
||||
* universally in every script file to maintain consistency.
|
||||
* 2. Replace '{REPONAME}' with the actual name of the repo (UPPERCASE).
|
||||
* Example: "MOKOSUITEBACKUP"
|
||||
* 3. Replace '{PACKAGENAME}' with the Joomla extension element name (lowercase).
|
||||
* Example: "pkg_mokosuitebackup", "com_mokosuitebackup", "mod_mokosuitebackup"
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Joomla.Installer
|
||||
* @subpackage {PACKAGENAME}
|
||||
*/
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
defined('_JEXEC') || die;
|
||||
|
||||
class {REPONAME}InstallerScript
|
||||
{
|
||||
/**
|
||||
* @var string The saved download key cached during preflight to survive updates
|
||||
*/
|
||||
private $savedDownloadKey = '';
|
||||
|
||||
/**
|
||||
* Preflight hook runs before any installation/update/uninstallation action.
|
||||
*/
|
||||
public function preflight($type, $parent): bool
|
||||
{
|
||||
if ($type === 'update') {
|
||||
$this->backupDownloadKey();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postflight hook runs after any installation/update/uninstallation action.
|
||||
*/
|
||||
public function postflight($type, $parent): void
|
||||
{
|
||||
if ($type === 'install') {
|
||||
$this->installSuccessful();
|
||||
$this->warnMissingLicenseKey();
|
||||
}
|
||||
|
||||
if ($type === 'update') {
|
||||
$this->restoreDownloadKey();
|
||||
$this->installSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
public function install($parent): void {}
|
||||
public function update($parent): void {}
|
||||
public function uninstall($parent): void {}
|
||||
|
||||
/**
|
||||
* Cache the existing download key from the update sites table before update runs.
|
||||
*/
|
||||
private function backupDownloadKey(): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('us.extra_query'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__update_sites_extensions', 'use')
|
||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||
)
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__extensions', 'e')
|
||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||
)
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('{PACKAGENAME}'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('component'))
|
||||
->setLimit(1);
|
||||
|
||||
$db->setQuery($query);
|
||||
$extraQuery = (string) $db->loadResult();
|
||||
|
||||
if (!empty($extraQuery)) {
|
||||
parse_str($extraQuery, $output);
|
||||
$this->savedDownloadKey = $output['dlid'] ?? $extraQuery;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::add('{REPONAME}: Could not backup download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the download key to the (possibly new) update site record.
|
||||
*/
|
||||
private function restoreDownloadKey(): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('us.update_site_id'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__update_sites_extensions', 'use')
|
||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||
)
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__extensions', 'e')
|
||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||
)
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('{PACKAGENAME}'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('component'))
|
||||
->setLimit(1);
|
||||
|
||||
$db->setQuery($query);
|
||||
$updateSiteId = (int) $db->loadResult();
|
||||
|
||||
if ($updateSiteId > 0 && !empty($this->savedDownloadKey)) {
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__update_sites'))
|
||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $this->savedDownloadKey))
|
||||
->where($db->quoteName('update_site_id') . ' = ' . $updateSiteId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::add('{REPONAME}: Could not restore download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME}</h4>'
|
||||
. '<p>Your download/license key could not be preserved during the update.</p>'
|
||||
. '<p>Please re-enter it in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]={PACKAGENAME}">Update Sites</a> manager to continue receiving updates.</p>',
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show post-install license key prompt
|
||||
*/
|
||||
private function warnMissingLicenseKey(): void
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME} License Key Required</h4>'
|
||||
. '<p>A download/license key (DLID) is required to receive updates.</p>'
|
||||
. '<p>Enter your key in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]={PACKAGENAME}">Update Sites</a> manager '
|
||||
. 'or contact <a class="btn btn-sm btn-warning ms-2" href="https://mokoconsulting.tech/support" target="_blank" rel="noopener">Moko Consulting Support</a> to obtain one.</p>',
|
||||
'warning'
|
||||
);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show install successful prompt
|
||||
*/
|
||||
private function installSuccessful(): void
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME} installed successfully!</h4>',
|
||||
'info'
|
||||
);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* ============================================================================
|
||||
* INSTRUCTIONS FOR USE:
|
||||
* 1. This file template serves as the standard structure for all script files
|
||||
* across the extension repositories. Copy and implement this pattern
|
||||
* universally in every script file to maintain consistency.
|
||||
* 2. Replace '{REPONAME}' with the actual name of the repo (UPPERCASE).
|
||||
* Example: "MOKOSUITEBACKUP"
|
||||
* 3. Replace '{PACKAGENAME}' with the Joomla extension element name (lowercase).
|
||||
* Example: "pkg_mokosuitebackup", "com_mokosuitebackup", "mod_mokosuitebackup"
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Joomla.Installer
|
||||
* @subpackage {PACKAGENAME}
|
||||
*/
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
defined('_JEXEC') || die;
|
||||
|
||||
class {REPONAME}InstallerScript
|
||||
{
|
||||
/**
|
||||
* @var string The saved download key cached during preflight to survive updates
|
||||
*/
|
||||
private $savedDownloadKey = '';
|
||||
|
||||
/**
|
||||
* Preflight hook runs before any installation/update/uninstallation action.
|
||||
*/
|
||||
public function preflight($type, $parent): bool
|
||||
{
|
||||
if ($type === 'update') {
|
||||
$this->backupDownloadKey();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postflight hook runs after any installation/update/uninstallation action.
|
||||
*/
|
||||
public function postflight($type, $parent): void
|
||||
{
|
||||
if ($type === 'install') {
|
||||
$this->installSuccessful();
|
||||
$this->warnMissingLicenseKey();
|
||||
}
|
||||
|
||||
if ($type === 'update') {
|
||||
$this->restoreDownloadKey();
|
||||
$this->installSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
public function install($parent): void {}
|
||||
public function update($parent): void {}
|
||||
public function uninstall($parent): void {}
|
||||
|
||||
/**
|
||||
* Cache the existing download key from the update sites table before update runs.
|
||||
*/
|
||||
private function backupDownloadKey(): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('us.extra_query'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__update_sites_extensions', 'use')
|
||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||
)
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__extensions', 'e')
|
||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||
)
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('{PACKAGENAME}'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('module'))
|
||||
->setLimit(1);
|
||||
|
||||
$db->setQuery($query);
|
||||
$extraQuery = (string) $db->loadResult();
|
||||
|
||||
if (!empty($extraQuery)) {
|
||||
parse_str($extraQuery, $output);
|
||||
$this->savedDownloadKey = $output['dlid'] ?? $extraQuery;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::add('{REPONAME}: Could not backup download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the download key to the (possibly new) update site record.
|
||||
*/
|
||||
private function restoreDownloadKey(): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('us.update_site_id'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__update_sites_extensions', 'use')
|
||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||
)
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__extensions', 'e')
|
||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||
)
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('{PACKAGENAME}'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('module'))
|
||||
->setLimit(1);
|
||||
|
||||
$db->setQuery($query);
|
||||
$updateSiteId = (int) $db->loadResult();
|
||||
|
||||
if ($updateSiteId > 0 && !empty($this->savedDownloadKey)) {
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__update_sites'))
|
||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $this->savedDownloadKey))
|
||||
->where($db->quoteName('update_site_id') . ' = ' . $updateSiteId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::add('{REPONAME}: Could not restore download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME}</h4>'
|
||||
. '<p>Your download/license key could not be preserved during the update.</p>'
|
||||
. '<p>Please re-enter it in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]={PACKAGENAME}">Update Sites</a> manager to continue receiving updates.</p>',
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show post-install license key prompt
|
||||
*/
|
||||
private function warnMissingLicenseKey(): void
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME} License Key Required</h4>'
|
||||
. '<p>A download/license key (DLID) is required to receive updates.</p>'
|
||||
. '<p>Enter your key in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]={PACKAGENAME}">Update Sites</a> manager '
|
||||
. 'or contact <a class="btn btn-sm btn-warning ms-2" href="https://mokoconsulting.tech/support" target="_blank" rel="noopener">Moko Consulting Support</a> to obtain one.</p>',
|
||||
'warning'
|
||||
);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show install successful prompt
|
||||
*/
|
||||
private function installSuccessful(): void
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME} installed successfully!</h4>',
|
||||
'info'
|
||||
);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* ============================================================================
|
||||
* INSTRUCTIONS FOR USE:
|
||||
* 1. This file template serves as the standard structure for all script files
|
||||
* across the extension repositories. Copy and implement this pattern
|
||||
* universally in every script file to maintain consistency.
|
||||
* 2. Replace '{REPONAME}' with the actual name of the repo (UPPERCASE).
|
||||
* Example: "MOKOSUITEBACKUP"
|
||||
* 3. Replace '{PACKAGENAME}' with the Joomla extension element name (lowercase).
|
||||
* Example: "pkg_mokosuitebackup", "com_mokosuitebackup", "mod_mokosuitebackup"
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Joomla.Installer
|
||||
* @subpackage {PACKAGENAME}
|
||||
*/
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
defined('_JEXEC') || die;
|
||||
|
||||
class {REPONAME}InstallerScript
|
||||
{
|
||||
/**
|
||||
* @var string The saved download key cached during preflight to survive updates
|
||||
*/
|
||||
private $savedDownloadKey = '';
|
||||
|
||||
/**
|
||||
* Preflight hook runs before any installation/update/uninstallation action.
|
||||
*/
|
||||
public function preflight($type, $parent): bool
|
||||
{
|
||||
if ($type === 'update') {
|
||||
$this->backupDownloadKey();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postflight hook runs after any installation/update/uninstallation action.
|
||||
*/
|
||||
public function postflight($type, $parent): void
|
||||
{
|
||||
if ($type === 'install') {
|
||||
$this->installSuccessful();
|
||||
$this->warnMissingLicenseKey();
|
||||
}
|
||||
|
||||
if ($type === 'update') {
|
||||
$this->restoreDownloadKey();
|
||||
$this->installSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
public function install($parent): void {}
|
||||
public function update($parent): void {}
|
||||
public function uninstall($parent): void {}
|
||||
|
||||
/**
|
||||
* Cache the existing download key from the update sites table before update runs.
|
||||
*/
|
||||
private function backupDownloadKey(): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('us.extra_query'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__update_sites_extensions', 'use')
|
||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||
)
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__extensions', 'e')
|
||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||
)
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('{PACKAGENAME}'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
||||
->setLimit(1);
|
||||
|
||||
$db->setQuery($query);
|
||||
$extraQuery = (string) $db->loadResult();
|
||||
|
||||
if (!empty($extraQuery)) {
|
||||
parse_str($extraQuery, $output);
|
||||
$this->savedDownloadKey = $output['dlid'] ?? $extraQuery;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::add('{REPONAME}: Could not backup download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the download key to the (possibly new) update site record.
|
||||
*/
|
||||
private function restoreDownloadKey(): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('us.update_site_id'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__update_sites_extensions', 'use')
|
||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||
)
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__extensions', 'e')
|
||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||
)
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('{PACKAGENAME}'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
||||
->setLimit(1);
|
||||
|
||||
$db->setQuery($query);
|
||||
$updateSiteId = (int) $db->loadResult();
|
||||
|
||||
if ($updateSiteId > 0 && !empty($this->savedDownloadKey)) {
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__update_sites'))
|
||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $this->savedDownloadKey))
|
||||
->where($db->quoteName('update_site_id') . ' = ' . $updateSiteId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::add('{REPONAME}: Could not restore download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME}</h4>'
|
||||
. '<p>Your download/license key could not be preserved during the update.</p>'
|
||||
. '<p>Please re-enter it in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]={PACKAGENAME}">Update Sites</a> manager to continue receiving updates.</p>',
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show post-install license key prompt
|
||||
*/
|
||||
private function warnMissingLicenseKey(): void
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME} License Key Required</h4>'
|
||||
. '<p>A download/license key (DLID) is required to receive updates.</p>'
|
||||
. '<p>Enter your key in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]={PACKAGENAME}">Update Sites</a> manager '
|
||||
. 'or contact <a class="btn btn-sm btn-warning ms-2" href="https://mokoconsulting.tech/support" target="_blank" rel="noopener">Moko Consulting Support</a> to obtain one.</p>',
|
||||
'warning'
|
||||
);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show install successful prompt
|
||||
*/
|
||||
private function installSuccessful(): void
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME} installed successfully!</h4>',
|
||||
'info'
|
||||
);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* ============================================================================
|
||||
* INSTRUCTIONS FOR USE:
|
||||
* 1. This file template serves as the standard structure for all script files
|
||||
* across the extension repositories. Copy and implement this pattern
|
||||
* universally in every script file to maintain consistency.
|
||||
* 2. Replace '{REPONAME}' with the actual name of the repo (UPPERCASE).
|
||||
* Example: "MOKOSUITEBACKUP"
|
||||
* 3. Replace '{PACKAGENAME}' with the Joomla extension element name (lowercase).
|
||||
* Example: "pkg_mokosuitebackup", "com_mokosuitebackup", "mod_mokosuitebackup"
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Joomla.Installer
|
||||
* @subpackage {PACKAGENAME}
|
||||
*/
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
defined('_JEXEC') || die;
|
||||
|
||||
class {REPONAME}InstallerScript
|
||||
{
|
||||
/**
|
||||
* @var string The saved download key cached during preflight to survive updates
|
||||
*/
|
||||
private $savedDownloadKey = '';
|
||||
|
||||
/**
|
||||
* Preflight hook runs before any installation/update/uninstallation action.
|
||||
*/
|
||||
public function preflight($type, $parent): bool
|
||||
{
|
||||
if ($type === 'update') {
|
||||
$this->backupDownloadKey();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postflight hook runs after any installation/update/uninstallation action.
|
||||
*/
|
||||
public function postflight($type, $parent): void
|
||||
{
|
||||
if ($type === 'install') {
|
||||
$this->installSuccessful();
|
||||
$this->warnMissingLicenseKey();
|
||||
}
|
||||
|
||||
if ($type === 'update') {
|
||||
$this->restoreDownloadKey();
|
||||
$this->installSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
public function install($parent): void {}
|
||||
public function update($parent): void {}
|
||||
public function uninstall($parent): void {}
|
||||
|
||||
/**
|
||||
* Cache the existing download key from the update sites table before update runs.
|
||||
*/
|
||||
private function backupDownloadKey(): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('us.extra_query'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__update_sites_extensions', 'use')
|
||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||
)
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__extensions', 'e')
|
||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||
)
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('{PACKAGENAME}'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('plugin'))
|
||||
->setLimit(1);
|
||||
|
||||
$db->setQuery($query);
|
||||
$extraQuery = (string) $db->loadResult();
|
||||
|
||||
if (!empty($extraQuery)) {
|
||||
parse_str($extraQuery, $output);
|
||||
$this->savedDownloadKey = $output['dlid'] ?? $extraQuery;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::add('{REPONAME}: Could not backup download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the download key to the (possibly new) update site record.
|
||||
*/
|
||||
private function restoreDownloadKey(): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('us.update_site_id'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__update_sites_extensions', 'use')
|
||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||
)
|
||||
->join(
|
||||
'INNER',
|
||||
$db->quoteName('#__extensions', 'e')
|
||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||
)
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('{PACKAGENAME}'))
|
||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('plugin'))
|
||||
->setLimit(1);
|
||||
|
||||
$db->setQuery($query);
|
||||
$updateSiteId = (int) $db->loadResult();
|
||||
|
||||
if ($updateSiteId > 0 && !empty($this->savedDownloadKey)) {
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__update_sites'))
|
||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $this->savedDownloadKey))
|
||||
->where($db->quoteName('update_site_id') . ' = ' . $updateSiteId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::add('{REPONAME}: Could not restore download key: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME}</h4>'
|
||||
. '<p>Your download/license key could not be preserved during the update.</p>'
|
||||
. '<p>Please re-enter it in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]={PACKAGENAME}">Update Sites</a> manager to continue receiving updates.</p>',
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show post-install license key prompt
|
||||
*/
|
||||
private function warnMissingLicenseKey(): void
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME} License Key Required</h4>'
|
||||
. '<p>A download/license key (DLID) is required to receive updates.</p>'
|
||||
. '<p>Enter your key in the <a class="btn btn-sm btn-warning ms-2" href="index.php?option=com_installer&view=updatesites&filter[search]={PACKAGENAME}">Update Sites</a> manager '
|
||||
. 'or contact <a class="btn btn-sm btn-warning ms-2" href="https://mokoconsulting.tech/support" target="_blank" rel="noopener">Moko Consulting Support</a> to obtain one.</p>',
|
||||
'warning'
|
||||
);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show install successful prompt
|
||||
*/
|
||||
private function installSuccessful(): void
|
||||
{
|
||||
try {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<h4>{REPONAME} installed successfully!</h4>',
|
||||
'info'
|
||||
);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# Component Scaffold
|
||||
|
||||
Copy this directory as the starting point for a new Joomla component.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Library Scaffold
|
||||
|
||||
Copy this directory as the starting point for a new Joomla library.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Module Scaffold
|
||||
|
||||
Copy this directory as the starting point for a new Joomla module.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Package Scaffold
|
||||
|
||||
Copy this directory as the starting point for a new Joomla package.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Plugin Scaffold
|
||||
|
||||
Copy this directory as the starting point for a new Joomla plugin.
|
||||
@@ -1,4 +0,0 @@
|
||||
# Template Scaffold
|
||||
|
||||
Copy this directory as the starting point for a new Joomla site template.
|
||||
Includes: index.php, error.php, offline.php, templateDetails.xml, css/, js/, fonts/, images/, html/
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Custom CSS for yourtemplate
|
||||
*
|
||||
* This file is for user customizations.
|
||||
* It will not be overwritten during template updates.
|
||||
*
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.yourtemplate
|
||||
* @copyright Copyright (C) 2026. All rights reserved.
|
||||
* @license GNU General Public License version 2 or later
|
||||
*/
|
||||
|
||||
/* Add your custom styles below this line */
|
||||
@@ -1,591 +0,0 @@
|
||||
/**
|
||||
* Template CSS for yourtemplate
|
||||
*
|
||||
* This file contains all template styles in one place for simplicity.
|
||||
* For larger projects, consider splitting into modular files:
|
||||
* - base.css (resets, typography, base elements)
|
||||
* - layout.css (grid, containers, responsive)
|
||||
* - components.css (buttons, forms, alerts)
|
||||
* - utilities.css (helper classes)
|
||||
*
|
||||
* Alternatively, use CSS preprocessing (Sass, Less, PostCSS) to organize
|
||||
* code into partial files that compile into a single output file.
|
||||
*
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.yourtemplate
|
||||
* @copyright Copyright (C) 2026. All rights reserved.
|
||||
* @license GNU General Public License version 2 or later
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
CSS Custom Properties (Variables)
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-primary: #0066cc;
|
||||
--color-primary-dark: #0052a3;
|
||||
--color-text: #333;
|
||||
--color-text-light: #666;
|
||||
--color-background: #fff;
|
||||
--color-background-light: #f9f9f9;
|
||||
--color-background-gray: #f5f5f5;
|
||||
--color-border: #e0e0e0;
|
||||
--color-error: #d9534f;
|
||||
--color-success: #5cb85c;
|
||||
--color-warning: #f0ad4e;
|
||||
--color-info: #5bc0de;
|
||||
|
||||
/* Typography */
|
||||
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-size-base: 1rem;
|
||||
--line-height-base: 1.6;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 3rem;
|
||||
|
||||
/* Layout */
|
||||
--container-max-width: 1200px;
|
||||
--border-radius: 4px;
|
||||
--box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Base Styles
|
||||
========================================================================== */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-family-base);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Layout Containers
|
||||
========================================================================== */
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--container-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -15px;
|
||||
}
|
||||
|
||||
[class*="col-"] {
|
||||
padding: 0 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Grid System
|
||||
========================================================================== */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.col-md-3 {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.col-md-6 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.col-md-9 {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.col-md-12 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Header Styles
|
||||
========================================================================== */
|
||||
|
||||
.site-header {
|
||||
background: var(--color-background);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.site-header.sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.site-branding {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.site-logo {
|
||||
max-height: 60px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.site-title a {
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-title a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.site-description {
|
||||
margin-top: 5px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.header-modules {
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Top Bar
|
||||
========================================================================== */
|
||||
|
||||
.top-bar {
|
||||
background: var(--color-background-gray);
|
||||
padding: 10px 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Navigation Styles
|
||||
========================================================================== */
|
||||
|
||||
.site-navigation {
|
||||
background: var(--color-primary);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.site-navigation ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.site-navigation li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.site-navigation a {
|
||||
display: block;
|
||||
padding: 15px 20px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.site-navigation a:hover,
|
||||
.site-navigation a:focus {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.site-navigation .active > a {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Banner Area
|
||||
========================================================================== */
|
||||
|
||||
.banner-area {
|
||||
padding: 40px 0;
|
||||
background: var(--color-background-light);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Main Content Area
|
||||
========================================================================== */
|
||||
|
||||
.site-main {
|
||||
padding: 40px 0;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.main-top,
|
||||
.main-bottom {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Sidebar Styles
|
||||
========================================================================== */
|
||||
|
||||
.sidebar {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.sidebar .module {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: var(--color-background-light);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.sidebar .module h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.25rem;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Footer Styles
|
||||
========================================================================== */
|
||||
|
||||
.site-footer {
|
||||
background: var(--color-text);
|
||||
color: var(--color-background);
|
||||
padding: 40px 0 20px;
|
||||
}
|
||||
|
||||
.site-footer a {
|
||||
color: var(--color-background);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-footer a:hover {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Error Page Styles
|
||||
========================================================================== */
|
||||
|
||||
.site-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: var(--color-background-gray);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
max-width: 600px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
background: var(--color-background);
|
||||
padding: 40px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-logo {
|
||||
max-height: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-message h2 {
|
||||
color: var(--color-error);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.error-technical {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: var(--color-background-light);
|
||||
border-radius: var(--border-radius);
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Offline Page Styles
|
||||
========================================================================== */
|
||||
|
||||
.site-offline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: var(--color-background-gray);
|
||||
}
|
||||
|
||||
.offline-container {
|
||||
max-width: 500px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.offline-content {
|
||||
background: var(--color-background);
|
||||
padding: 40px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.offline-logo {
|
||||
max-height: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Buttons
|
||||
========================================================================== */
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--color-primary-dark);
|
||||
border-color: var(--color-primary-dark);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Messages
|
||||
========================================================================== */
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert-error,
|
||||
.alert-danger {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Typography
|
||||
========================================================================== */
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 { font-size: 2.5rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.75rem; }
|
||||
h4 { font-size: 1.5rem; }
|
||||
h5 { font-size: 1.25rem; }
|
||||
h6 { font-size: 1rem; }
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-primary-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Responsive Styles
|
||||
========================================================================== */
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.header-inner {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-modules {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mobile navigation styling
|
||||
* Note: This provides basic stacking for mobile. For a toggle menu,
|
||||
* add a .mobile-menu-toggle button and implement show/hide functionality.
|
||||
* See template.js for the toggle handler.
|
||||
*/
|
||||
.site-navigation ul {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.site-navigation a {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
/* Mobile menu toggle button (add to your template HTML) */
|
||||
.mobile-menu-toggle {
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Mobile menu active state */
|
||||
.site-navigation.mobile-menu-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[class*="col-"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Accessibility
|
||||
========================================================================== */
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
button:focus {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Utilities
|
||||
========================================================================== */
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-1 { margin-top: 0.25rem; }
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.mt-3 { margin-top: 1rem; }
|
||||
.mt-4 { margin-top: 1.5rem; }
|
||||
.mt-5 { margin-top: 3rem; }
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-1 { margin-bottom: 0.25rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-3 { margin-bottom: 1rem; }
|
||||
.mb-4 { margin-bottom: 1.5rem; }
|
||||
.mb-5 { margin-bottom: 3rem; }
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.yourtemplate
|
||||
*
|
||||
* @copyright Copyright (C) 2026. All rights reserved.
|
||||
* @license GNU General Public License version 2 or later
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
|
||||
/** @var Joomla\CMS\Document\ErrorDocument $this */
|
||||
|
||||
$app = Factory::getApplication();
|
||||
$wa = $this->getWebAssetManager();
|
||||
|
||||
// Template params
|
||||
$logo = $this->params->get('logo');
|
||||
$sitename = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// Add template CSS
|
||||
$wa->registerAndUseStyle(
|
||||
'template.yourtemplate',
|
||||
'templates/' . $this->template . '/css/template.css',
|
||||
[],
|
||||
['version' => 'auto']
|
||||
);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo $this->title; ?> - <?php echo $sitename; ?></title>
|
||||
<jdoc:include type="styles" />
|
||||
<jdoc:include type="scripts" />
|
||||
</head>
|
||||
<body class="site-error">
|
||||
<div class="error-container">
|
||||
<div class="error-content">
|
||||
<?php if ($logo) : ?>
|
||||
<a href="<?php echo Uri::root(); ?>">
|
||||
<img src="<?php echo Uri::root() . htmlspecialchars($logo, ENT_QUOTES, 'UTF-8'); ?>" alt="<?php echo $sitename; ?>" class="error-logo">
|
||||
</a>
|
||||
<?php else : ?>
|
||||
<h1 class="site-title">
|
||||
<a href="<?php echo Uri::root(); ?>"><?php echo $sitename; ?></a>
|
||||
</h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="error-message">
|
||||
<h2><?php echo Text::_('JERROR_LAYOUT_PAGE_NOT_FOUND'); ?></h2>
|
||||
<div class="error-details">
|
||||
<p><strong><?php echo Text::_('JERROR_LAYOUT_ERROR_HAS_OCCURRED_WHILE_PROCESSING_YOUR_REQUEST'); ?></strong></p>
|
||||
<?php if ($this->debug) : ?>
|
||||
<div class="error-debug">
|
||||
<p><?php echo Text::_('JERROR_LAYOUT_NOT_ABLE_TO_VISIT'); ?></p>
|
||||
<ul>
|
||||
<li><?php echo Text::_('JERROR_LAYOUT_AN_OUT_OF_DATE_BOOKMARK_FAVOURITE'); ?></li>
|
||||
<li><?php echo Text::_('JERROR_LAYOUT_MIS_TYPED_ADDRESS'); ?></li>
|
||||
<li><?php echo Text::_('JERROR_LAYOUT_SEARCH_ENGINE_OUT_OF_DATE_LISTING'); ?></li>
|
||||
<li><?php echo Text::_('JERROR_LAYOUT_YOU_HAVE_NO_ACCESS_TO_THIS_PAGE'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-actions">
|
||||
<a href="<?php echo Uri::root(); ?>" class="btn btn-primary">
|
||||
<?php echo Text::_('JERROR_LAYOUT_GO_TO_THE_HOME_PAGE'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ($this->debug) : ?>
|
||||
<div class="error-technical">
|
||||
<h3><?php echo Text::_('JERROR_LAYOUT_TECHNICAL_INFORMATION'); ?></h3>
|
||||
<p><?php echo $this->error->getMessage(); ?></p>
|
||||
<p>
|
||||
<?php if ($this->error->getCode()) : ?>
|
||||
<?php echo Text::_('JERROR_LAYOUT_ERROR_CODE'); ?>: <?php echo $this->error->getCode(); ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,183 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.yourtemplate
|
||||
*
|
||||
* @copyright Copyright (C) 2026. All rights reserved.
|
||||
* @license GNU General Public License version 2 or later
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
|
||||
/** @var Joomla\CMS\Document\HtmlDocument $this */
|
||||
|
||||
$app = Factory::getApplication();
|
||||
$wa = $this->getWebAssetManager();
|
||||
|
||||
// Detecting Active Variables
|
||||
$option = $app->input->getCmd('option', '');
|
||||
$view = $app->input->getCmd('view', '');
|
||||
$layout = $app->input->getCmd('layout', '');
|
||||
$task = $app->input->getCmd('task', '');
|
||||
$itemid = $app->input->getCmd('Itemid', '');
|
||||
$sitename = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
|
||||
$menu = $app->getMenu()->getActive();
|
||||
$pageclass = $menu !== null ? $menu->getParams()->get('pageclass_sfx', '') : '';
|
||||
|
||||
// Template params
|
||||
$logo = $this->params->get('logo');
|
||||
$siteTitle = $this->params->get('siteTitle', $sitename);
|
||||
$siteDescription = $this->params->get('siteDescription', '');
|
||||
$containerFluid = (bool) $this->params->get('containerFluid', false);
|
||||
$stickyHeader = (bool) $this->params->get('stickyHeader', false);
|
||||
|
||||
// Add template CSS
|
||||
$wa->registerAndUseStyle(
|
||||
'template.yourtemplate',
|
||||
'templates/' . $this->template . '/css/template.css',
|
||||
[],
|
||||
['version' => 'auto']
|
||||
);
|
||||
|
||||
// Add template JavaScript
|
||||
$wa->registerAndUseScript(
|
||||
'template.yourtemplate',
|
||||
'templates/' . $this->template . '/js/template.js',
|
||||
[],
|
||||
['version' => 'auto', 'defer' => true]
|
||||
);
|
||||
|
||||
// Add viewport meta tag for responsive design
|
||||
$this->setMetaData('viewport', 'width=device-width, initial-scale=1.0');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
|
||||
<head>
|
||||
<jdoc:include type="metas" />
|
||||
<jdoc:include type="styles" />
|
||||
<jdoc:include type="scripts" />
|
||||
</head>
|
||||
<body class="site <?php echo $option
|
||||
. ' view-' . $view
|
||||
. ($layout ? ' layout-' . $layout : ' no-layout')
|
||||
. ($task ? ' task-' . $task : ' no-task')
|
||||
. ($itemid ? ' itemid-' . $itemid : '')
|
||||
. ($pageclass ? ' ' . $pageclass : '')
|
||||
. ($this->direction === 'rtl' ? ' rtl' : '');
|
||||
?>">
|
||||
|
||||
<?php if ($this->countModules('top')) : ?>
|
||||
<div class="top-bar">
|
||||
<div class="<?php echo $containerFluid ? 'container-fluid' : 'container'; ?>">
|
||||
<jdoc:include type="modules" name="top" style="html5" />
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<header class="site-header<?php echo $stickyHeader ? ' sticky-header' : ''; ?>">
|
||||
<div class="<?php echo $containerFluid ? 'container-fluid' : 'container'; ?>">
|
||||
<div class="header-inner">
|
||||
<div class="site-branding">
|
||||
<?php if ($logo) : ?>
|
||||
<a href="<?php echo Uri::root(); ?>" title="<?php echo $siteTitle; ?>">
|
||||
<img src="<?php echo Uri::root() . htmlspecialchars($logo, ENT_QUOTES, 'UTF-8'); ?>" alt="<?php echo $siteTitle; ?>" class="site-logo">
|
||||
</a>
|
||||
<?php else : ?>
|
||||
<div class="site-title">
|
||||
<a href="<?php echo Uri::root(); ?>" title="<?php echo $siteTitle; ?>">
|
||||
<?php echo $siteTitle; ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($siteDescription) : ?>
|
||||
<div class="site-description">
|
||||
<?php echo htmlspecialchars($siteDescription, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($this->countModules('header')) : ?>
|
||||
<div class="header-modules">
|
||||
<jdoc:include type="modules" name="header" style="html5" />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($this->countModules('navigation')) : ?>
|
||||
<nav class="site-navigation" role="navigation">
|
||||
<div class="<?php echo $containerFluid ? 'container-fluid' : 'container'; ?>">
|
||||
<jdoc:include type="modules" name="navigation" style="html5" />
|
||||
</div>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($this->countModules('banner')) : ?>
|
||||
<div class="banner-area">
|
||||
<div class="<?php echo $containerFluid ? 'container-fluid' : 'container'; ?>">
|
||||
<jdoc:include type="modules" name="banner" style="html5" />
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<main class="site-main">
|
||||
<div class="<?php echo $containerFluid ? 'container-fluid' : 'container'; ?>">
|
||||
<div class="row">
|
||||
<?php if ($this->countModules('sidebar-left')) : ?>
|
||||
<aside class="sidebar sidebar-left col-md-3">
|
||||
<jdoc:include type="modules" name="sidebar-left" style="html5" />
|
||||
</aside>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="content-area <?php
|
||||
echo $this->countModules('sidebar-left') && $this->countModules('sidebar-right') ? 'col-md-6'
|
||||
: ($this->countModules('sidebar-left') || $this->countModules('sidebar-right') ? 'col-md-9'
|
||||
: 'col-md-12');
|
||||
?>">
|
||||
<?php if ($this->countModules('main-top')) : ?>
|
||||
<div class="main-top">
|
||||
<jdoc:include type="modules" name="main-top" style="html5" />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="main-content">
|
||||
<jdoc:include type="message" />
|
||||
<jdoc:include type="component" />
|
||||
</div>
|
||||
|
||||
<?php if ($this->countModules('main-bottom')) : ?>
|
||||
<div class="main-bottom">
|
||||
<jdoc:include type="modules" name="main-bottom" style="html5" />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($this->countModules('sidebar-right')) : ?>
|
||||
<aside class="sidebar sidebar-right col-md-3">
|
||||
<jdoc:include type="modules" name="sidebar-right" style="html5" />
|
||||
</aside>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php if ($this->countModules('footer')) : ?>
|
||||
<footer class="site-footer">
|
||||
<div class="<?php echo $containerFluid ? 'container-fluid' : 'container'; ?>">
|
||||
<jdoc:include type="modules" name="footer" style="html5" />
|
||||
</div>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($this->countModules('debug')) : ?>
|
||||
<div class="debug-area">
|
||||
<jdoc:include type="modules" name="debug" style="none" />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Custom JavaScript for yourtemplate
|
||||
*
|
||||
* This file is for user customizations.
|
||||
* It will not be overwritten during template updates.
|
||||
*
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.yourtemplate
|
||||
* @copyright Copyright (C) 2026. All rights reserved.
|
||||
* @license GNU General Public License version 2 or later
|
||||
*/
|
||||
|
||||
// Add your custom JavaScript code below this line
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* Template JavaScript for yourtemplate
|
||||
*
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.yourtemplate
|
||||
* @copyright Copyright (C) 2026. All rights reserved.
|
||||
* @license GNU General Public License version 2 or later
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize template functionality when DOM is ready
|
||||
*/
|
||||
function initTemplate() {
|
||||
// Mobile menu toggle
|
||||
initMobileMenu();
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
initSmoothScroll();
|
||||
|
||||
// Sticky header enhancement
|
||||
initStickyHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile menu functionality
|
||||
*
|
||||
* Note: This requires a mobile menu toggle button to be added to your template.
|
||||
* Add the following button in your header area:
|
||||
*
|
||||
* <button class="mobile-menu-toggle" aria-expanded="false" aria-label="Toggle navigation">
|
||||
* <span class="menu-icon"></span>
|
||||
* </button>
|
||||
*
|
||||
* And add CSS for the toggle button and mobile menu states.
|
||||
*/
|
||||
function initMobileMenu() {
|
||||
const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
|
||||
const navigation = document.querySelector('.site-navigation');
|
||||
|
||||
if (mobileMenuToggle && navigation) {
|
||||
mobileMenuToggle.addEventListener('click', function () {
|
||||
navigation.classList.toggle('mobile-menu-active');
|
||||
const isExpanded = navigation.classList.contains('mobile-menu-active');
|
||||
this.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth scroll for anchor links
|
||||
*/
|
||||
function initSmoothScroll() {
|
||||
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
||||
|
||||
anchorLinks.forEach(function (link) {
|
||||
link.addEventListener('click', function (e) {
|
||||
const targetId = this.getAttribute('href');
|
||||
|
||||
// Skip if it's just "#" or "#top"
|
||||
if (targetId === '#' || targetId === '#top') {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetElement = document.querySelector(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
e.preventDefault();
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sticky header enhancement
|
||||
*/
|
||||
function initStickyHeader() {
|
||||
const header = document.querySelector('.site-header.sticky-header');
|
||||
|
||||
if (header) {
|
||||
let lastScrollTop = 0;
|
||||
|
||||
window.addEventListener('scroll', function () {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
// Add/remove class based on scroll direction
|
||||
if (scrollTop > lastScrollTop && scrollTop > 100) {
|
||||
// Scrolling down
|
||||
header.classList.add('header-hidden');
|
||||
} else {
|
||||
// Scrolling up
|
||||
header.classList.remove('header-hidden');
|
||||
}
|
||||
|
||||
lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize when DOM is ready
|
||||
*/
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTemplate);
|
||||
} else {
|
||||
initTemplate();
|
||||
}
|
||||
})();
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.yourtemplate
|
||||
*
|
||||
* @copyright Copyright (C) 2026. All rights reserved.
|
||||
* @license GNU General Public License version 2 or later
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
|
||||
/** @var Joomla\CMS\Document\HtmlDocument $this */
|
||||
|
||||
$app = Factory::getApplication();
|
||||
$wa = $this->getWebAssetManager();
|
||||
|
||||
// Template params
|
||||
$logo = $this->params->get('logo');
|
||||
$sitename = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// Add template CSS
|
||||
$wa->registerAndUseStyle(
|
||||
'template.yourtemplate',
|
||||
'templates/' . $this->template . '/css/template.css',
|
||||
[],
|
||||
['version' => 'auto']
|
||||
);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
|
||||
<head>
|
||||
<jdoc:include type="metas" />
|
||||
<jdoc:include type="styles" />
|
||||
<jdoc:include type="scripts" />
|
||||
</head>
|
||||
<body class="site-offline">
|
||||
<div class="offline-container">
|
||||
<div class="offline-content">
|
||||
<?php if ($logo) : ?>
|
||||
<img src="<?php echo Uri::root() . htmlspecialchars($logo, ENT_QUOTES, 'UTF-8'); ?>" alt="<?php echo $sitename; ?>" class="offline-logo">
|
||||
<?php else : ?>
|
||||
<h1><?php echo $sitename; ?></h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<jdoc:include type="message" />
|
||||
<jdoc:include type="component" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,104 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="template" client="site" method="upgrade">
|
||||
<name>yourtemplate</name>
|
||||
<creationDate>2026-04-27</creationDate>
|
||||
<author>Your Name</author>
|
||||
<authorEmail>your.email@example.com</authorEmail>
|
||||
<authorUrl>https://www.example.com</authorUrl>
|
||||
<copyright>Copyright (C) 2026. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 2 or later</license>
|
||||
<version>1.0.0</version>
|
||||
<description>TPL_YOURTEMPLATE_XML_DESCRIPTION</description>
|
||||
|
||||
<files>
|
||||
<filename>index.php</filename>
|
||||
<filename>templateDetails.xml</filename>
|
||||
<!--
|
||||
IMPORTANT: Add actual PNG images before distribution
|
||||
|
||||
1. Replace template_thumbnail.png.txt and template_preview.png.txt with real PNG images
|
||||
2. Uncomment the two lines below after adding the actual image files
|
||||
3. See src/IMAGES.md for detailed instructions
|
||||
|
||||
Once images are added, uncomment these lines:
|
||||
-->
|
||||
<!-- <filename>template_thumbnail.png</filename> -->
|
||||
<!-- <filename>template_preview.png</filename> -->
|
||||
<filename>offline.php</filename>
|
||||
<filename>error.php</filename>
|
||||
<folder>css</folder>
|
||||
<folder>js</folder>
|
||||
<folder>images</folder>
|
||||
<folder>fonts</folder>
|
||||
<folder>language</folder>
|
||||
<folder>html</folder>
|
||||
</files>
|
||||
|
||||
<positions>
|
||||
<position>top</position>
|
||||
<position>header</position>
|
||||
<position>navigation</position>
|
||||
<position>banner</position>
|
||||
<position>sidebar-left</position>
|
||||
<position>sidebar-right</position>
|
||||
<position>main-top</position>
|
||||
<position>main</position>
|
||||
<position>main-bottom</position>
|
||||
<position>footer</position>
|
||||
<position>debug</position>
|
||||
</positions>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="advanced">
|
||||
<field
|
||||
name="logo"
|
||||
type="media"
|
||||
label="TPL_YOURTEMPLATE_LOGO_LABEL"
|
||||
description="TPL_YOURTEMPLATE_LOGO_DESC"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="siteTitle"
|
||||
type="text"
|
||||
label="TPL_YOURTEMPLATE_SITE_TITLE_LABEL"
|
||||
description="TPL_YOURTEMPLATE_SITE_TITLE_DESC"
|
||||
default=""
|
||||
/>
|
||||
|
||||
<field
|
||||
name="siteDescription"
|
||||
type="textarea"
|
||||
label="TPL_YOURTEMPLATE_SITE_DESCRIPTION_LABEL"
|
||||
description="TPL_YOURTEMPLATE_SITE_DESCRIPTION_DESC"
|
||||
rows="3"
|
||||
cols="30"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="containerFluid"
|
||||
type="radio"
|
||||
label="TPL_YOURTEMPLATE_CONTAINER_FLUID_LABEL"
|
||||
description="TPL_YOURTEMPLATE_CONTAINER_FLUID_DESC"
|
||||
default="0"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
>
|
||||
<option value="0">JNO</option>
|
||||
<option value="1">JYES</option>
|
||||
</field>
|
||||
|
||||
<field
|
||||
name="stickyHeader"
|
||||
type="radio"
|
||||
label="TPL_YOURTEMPLATE_STICKY_HEADER_LABEL"
|
||||
description="TPL_YOURTEMPLATE_STICKY_HEADER_DESC"
|
||||
default="0"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
>
|
||||
<option value="0">JNO</option>
|
||||
<option value="1">JYES</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
Reference in New Issue
Block a user