464ebb1a25
New validator for client theme packages (Joomla type="file"): - Manifest: required elements, type="file", method="upgrade", version format - Required files: light.custom.css, dark.custom.css - PHP syntax check on script.php - CSS brace balance + BOM detection - Version consistency (manifest vs updates.xml vs CHANGELOG) - Image size warnings (>1MB) Also update auto_detect_platform.php to recognise type="file" manifests as client repos alongside legacy sftp-config detection. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
279 lines
8.6 KiB
PHP
279 lines
8.6 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: MokoStandards.Scripts.Validate
|
|
* INGROUP: MokoStandards
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /validate/check_client_theme.php
|
|
* BRIEF: Validates client WaaS theme packages (Joomla type="file")
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
|
|
|
use MokoEnterprise\CliFramework;
|
|
|
|
/**
|
|
* Validates client theme packages that deliver CSS, JS, and images
|
|
* to the MokoOnyx template via Joomla's file package installer.
|
|
*
|
|
* Checks:
|
|
* - Manifest structure (src/templateDetails.xml)
|
|
* - Required elements: name, element, version, updateservers, scriptfile, fileset
|
|
* - Extension type is "file" with method="upgrade"
|
|
* - Version format (XX.YY.ZZ)
|
|
* - Required theme files (light.custom.css, dark.custom.css)
|
|
* - PHP syntax of script.php
|
|
* - CSS brace balance
|
|
* - updates.xml at repo root
|
|
* - Image size warnings
|
|
*/
|
|
class CheckClientTheme extends CliFramework
|
|
{
|
|
/** Required XML elements in the manifest. */
|
|
private const REQUIRED_ELEMENTS = ['name', 'element', 'version'];
|
|
|
|
/** Recommended XML elements. */
|
|
private const RECOMMENDED_ELEMENTS = ['updateservers', 'scriptfile', 'description', 'fileset'];
|
|
|
|
/** Required theme CSS files relative to repo root. */
|
|
private const REQUIRED_THEME_FILES = [
|
|
'src/media/templates/site/mokoonyx/css/theme/light.custom.css',
|
|
'src/media/templates/site/mokoonyx/css/theme/dark.custom.css',
|
|
];
|
|
|
|
/** Optional but expected files. */
|
|
private const EXPECTED_FILES = [
|
|
'src/media/templates/site/mokoonyx/css/user.css',
|
|
'src/media/templates/site/mokoonyx/js/user.js',
|
|
'src/script.php',
|
|
'updates.xml',
|
|
];
|
|
|
|
/** Maximum image size before warning (1 MB). */
|
|
private const IMAGE_WARN_SIZE = 1048576;
|
|
|
|
/**
|
|
* Configure available arguments.
|
|
*/
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Validates client WaaS theme packages (type="file")');
|
|
$this->addArgument('--path', 'Repository path to check', '.');
|
|
}
|
|
|
|
/**
|
|
* Run all validation checks.
|
|
*/
|
|
protected function run(): int
|
|
{
|
|
$path = rtrim($this->getArgument('--path'), '/');
|
|
$errors = 0;
|
|
$warns = 0;
|
|
|
|
// ── Manifest ──────────────────────────────────────────
|
|
$this->section('Manifest validation');
|
|
$manifest = $path . '/src/templateDetails.xml';
|
|
|
|
if (!is_file($manifest)) {
|
|
$this->status(false, 'Missing src/templateDetails.xml');
|
|
$this->printSummary(0, 1, $this->elapsed());
|
|
return 1;
|
|
}
|
|
|
|
$content = (string) file_get_contents($manifest);
|
|
|
|
// Extension type
|
|
if (preg_match('/type="([^"]*)"/', $content, $m)) {
|
|
if ($m[1] !== 'file') {
|
|
$this->status(false, "Extension type is '{$m[1]}', expected 'file'");
|
|
$errors++;
|
|
} else {
|
|
$this->status(true, 'Extension type: file');
|
|
}
|
|
} else {
|
|
$this->status(false, 'No type attribute on <extension>');
|
|
$errors++;
|
|
}
|
|
|
|
// method="upgrade"
|
|
if (str_contains($content, 'method="upgrade"')) {
|
|
$this->status(true, 'method="upgrade" present');
|
|
} else {
|
|
$this->warning('Missing method="upgrade" — updates may fail');
|
|
$warns++;
|
|
}
|
|
|
|
// Required elements
|
|
foreach (self::REQUIRED_ELEMENTS as $el) {
|
|
if (str_contains($content, "<{$el}>")) {
|
|
$this->status(true, "<{$el}> present");
|
|
} else {
|
|
$this->status(false, "Missing <{$el}>");
|
|
$errors++;
|
|
}
|
|
}
|
|
|
|
// Recommended elements
|
|
foreach (self::RECOMMENDED_ELEMENTS as $el) {
|
|
if (!str_contains($content, "<{$el}>") && !str_contains($content, "<{$el} ")) {
|
|
$this->warning("Missing <{$el}>");
|
|
$warns++;
|
|
}
|
|
}
|
|
|
|
// Version format
|
|
if (preg_match('/<version>([^<]+)<\/version>/', $content, $m)) {
|
|
$version = $m[1];
|
|
if (preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $version)) {
|
|
$this->status(true, "Version: {$version}");
|
|
} else {
|
|
$this->status(false, "Version '{$version}' does not match XX.YY.ZZ format");
|
|
$errors++;
|
|
}
|
|
}
|
|
|
|
// ── Required files ────────────────────────────────────
|
|
$this->section('Required files');
|
|
foreach (self::REQUIRED_THEME_FILES as $file) {
|
|
$full = $path . '/' . $file;
|
|
if (is_file($full)) {
|
|
$this->status(true, basename($file));
|
|
} else {
|
|
$this->status(false, "Missing: {$file}");
|
|
$errors++;
|
|
}
|
|
}
|
|
|
|
foreach (self::EXPECTED_FILES as $file) {
|
|
$full = $path . '/' . $file;
|
|
if (is_file($full)) {
|
|
$this->status(true, basename($file));
|
|
} else {
|
|
$this->warning("Missing: {$file}");
|
|
$warns++;
|
|
}
|
|
}
|
|
|
|
// ── PHP syntax ────────────────────────────────────────
|
|
$this->section('PHP syntax');
|
|
$phpFiles = glob($path . '/src/*.php') ?: [];
|
|
foreach ($phpFiles as $phpFile) {
|
|
$output = [];
|
|
$ret = 0;
|
|
$escaped = escapeshellarg($phpFile);
|
|
exec("php -l {$escaped} 2>&1", $output, $ret);
|
|
if ($ret !== 0) {
|
|
$this->status(false, 'Syntax error: ' . basename($phpFile));
|
|
$errors++;
|
|
} else {
|
|
$this->status(true, basename($phpFile));
|
|
}
|
|
}
|
|
if (empty($phpFiles)) {
|
|
$this->warning('No PHP files in src/');
|
|
}
|
|
|
|
// ── CSS validation ────────────────────────────────────
|
|
$this->section('CSS validation');
|
|
$cssFiles = array_merge(
|
|
glob($path . '/src/media/templates/site/mokoonyx/css/theme/*.css') ?: [],
|
|
glob($path . '/src/media/templates/site/mokoonyx/css/*.css') ?: [],
|
|
);
|
|
foreach ($cssFiles as $cssFile) {
|
|
$css = (string) file_get_contents($cssFile);
|
|
$open = substr_count($css, '{');
|
|
$close = substr_count($css, '}');
|
|
$name = str_replace($path . '/src/', '', $cssFile);
|
|
|
|
if ($open !== $close) {
|
|
$this->status(false, "Unbalanced braces in {$name} (open: {$open}, close: {$close})");
|
|
$errors++;
|
|
} else {
|
|
$this->status(true, "{$name} ({$open} rules)");
|
|
}
|
|
|
|
// BOM check
|
|
if (str_starts_with($css, "\xEF\xBB\xBF")) {
|
|
$this->status(false, "BOM detected in {$name}");
|
|
$errors++;
|
|
}
|
|
}
|
|
|
|
// ── Version consistency ───────────────────────────────
|
|
$this->section('Version consistency');
|
|
$manifestVer = '';
|
|
if (preg_match('/<version>([^<]+)<\/version>/', $content, $m)) {
|
|
$manifestVer = $m[1];
|
|
}
|
|
|
|
$updatesFile = $path . '/updates.xml';
|
|
if (is_file($updatesFile)) {
|
|
$updatesContent = (string) file_get_contents($updatesFile);
|
|
if (preg_match('/<version>([^<]+)<\/version>/', $updatesContent, $m)) {
|
|
if ($m[1] !== $manifestVer) {
|
|
$this->warning("Version drift: manifest={$manifestVer}, updates.xml={$m[1]}");
|
|
$warns++;
|
|
} else {
|
|
$this->status(true, "Versions match: {$manifestVer}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_file($path . '/CHANGELOG.md')) {
|
|
$cl = (string) file_get_contents($path . '/CHANGELOG.md');
|
|
if (!str_contains($cl, "[{$manifestVer}]")) {
|
|
$this->warning("Version {$manifestVer} not in CHANGELOG.md");
|
|
$warns++;
|
|
} else {
|
|
$this->status(true, "CHANGELOG has [{$manifestVer}]");
|
|
}
|
|
}
|
|
|
|
// ── Image sizes ───────────────────────────────────────
|
|
$this->section('Image optimization');
|
|
$largeImages = 0;
|
|
$imageDir = $path . '/src/images';
|
|
if (is_dir($imageDir)) {
|
|
$iter = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($imageDir, \FilesystemIterator::SKIP_DOTS)
|
|
);
|
|
foreach ($iter as $file) {
|
|
if (!$file->isFile()) {
|
|
continue;
|
|
}
|
|
$ext = strtolower($file->getExtension());
|
|
if (!in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'])) {
|
|
continue;
|
|
}
|
|
if ($file->getSize() > self::IMAGE_WARN_SIZE) {
|
|
$kb = (int) ($file->getSize() / 1024);
|
|
$this->warning("{$kb}KB: " . str_replace($path . '/', '', $file->getPathname()));
|
|
$largeImages++;
|
|
}
|
|
}
|
|
}
|
|
if ($largeImages > 0) {
|
|
$this->warning("{$largeImages} image(s) over 1MB — consider optimizing");
|
|
} else {
|
|
$this->status(true, 'All images under 1MB');
|
|
}
|
|
|
|
// ── Summary ───────────────────────────────────────────
|
|
$passed = ($errors === 0) ? 1 : 0;
|
|
$this->printSummary($passed, $errors, $this->elapsed(), $warns);
|
|
|
|
return ($errors > 0) ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
$script = new CheckClientTheme('check_client_theme', 'Validates client WaaS theme packages');
|
|
exit($script->execute());
|