#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: MokoStandards.Templates.Scripts.Validate * INGROUP: MokoStandards.Templates * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /templates/scripts/validate/validate_manifest.php * BRIEF: Validate a Joomla component XML manifest against MokoStandards requirements * NOTE: Deployed to bin/validate_manifest.php in governed WaaS component repos. * Run: php bin/validate_manifest.php [--path DIR] [--verbose] */ declare(strict_types=1); // Deployed to bin/validate_manifest.php in org repos — vendor/ is one level up (repo root) require_once __DIR__ . '/../vendor/autoload.php'; use MokoEnterprise\CliFramework; /** * Validates a Joomla component XML manifest against MokoStandards requirements. * * Checks performed: * - XML manifest exists and is well-formed * - Required manifest attributes present (type, version, method) * - Required manifest elements present (name, author, authorEmail, license, version) * - License is GPL-3.0-or-later * - Required directories present (src/, languages/ or administrator/) * - Required files present (README.md, CHANGELOG.md, LICENSE, composer.json) * - SPDX-License-Identifier header present in all PHP source files * - No tab characters in YAML/JSON config files */ class ValidateJoomlaManifest extends CliFramework { protected function configure(): void { $this->setDescription('Validate a Joomla component XML manifest against MokoStandards requirements'); $this->addArgument('--path', 'Repository root to validate', '.'); } protected function run(): int { $path = rtrim((string) $this->getArgument('--path'), '/\\'); $passed = 0; $failed = 0; if (!is_dir($path)) { $this->error("Path does not exist: {$path}"); } // ── Find XML manifest ───────────────────────────────────────────── $this->section('XML manifest'); $manifest = $this->findManifest($path); if ($manifest === null) { $this->status(false, 'No Joomla component XML manifest found'); $failed++; } else { $this->status(true, 'Manifest found: ' . substr($manifest, strlen($path) + 1)); $passed++; // Parse and validate libxml_use_internal_errors(true); $xml = simplexml_load_file($manifest); if ($xml === false) { $this->status(false, 'Manifest is not valid XML'); $failed++; } else { $this->status(true, 'Manifest is well-formed XML'); $passed++; // Required attributes foreach (['type', 'version', 'method'] as $attr) { $ok = isset($xml[$attr]) && (string) $xml[$attr] !== ''; $this->status($ok, "Attribute \"{$attr}\" present"); $ok ? $passed++ : $failed++; } // Required elements foreach (['name', 'author', 'authorEmail', 'license', 'version'] as $elem) { $ok = isset($xml->{$elem}) && trim((string) $xml->{$elem}) !== ''; $this->status($ok, "Element <{$elem}> present and non-empty"); $ok ? $passed++ : $failed++; } // License check $license = trim((string) ($xml->license ?? '')); $okLicense = str_contains(strtolower($license), 'gpl') || str_contains($license, '3.0'); $this->status($okLicense, "License is GPL-3.0-or-later (found: \"{$license}\")"); $okLicense ? $passed++ : $failed++; } } // ── Required files ──────────────────────────────────────────────── $this->section('Required files'); foreach (['README.md', 'CHANGELOG.md', 'LICENSE', 'composer.json'] as $file) { $ok = file_exists("{$path}/{$file}"); $this->status($ok, $file); $ok ? $passed++ : $failed++; } // ── Required directories ────────────────────────────────────────── $this->section('Required directories'); foreach (['src'] as $dir) { $ok = is_dir("{$path}/{$dir}"); $this->status($ok, $dir . '/'); $ok ? $passed++ : $failed++; } // ── SPDX headers ────────────────────────────────────────────────── $this->section('SPDX-License-Identifier headers'); $phpFiles = $this->findFiles($path . '/src', '*.php'); $missingHeader = 0; foreach ($phpFiles as $file) { $head = file_get_contents($file, false, null, 0, 512) ?: ''; if (!str_contains($head, 'SPDX-License-Identifier')) { $missingHeader++; $this->log('WARN', 'Missing SPDX header: ' . substr($file, strlen($path) + 1)); } } $ok = $missingHeader === 0; $this->status($ok, "All PHP files have SPDX header ({$missingHeader} missing)"); $ok ? $passed++ : $failed++; // ── No tabs in config files ─────────────────────────────────────── $this->section('No tab characters in config files'); $configFiles = array_merge( $this->findFiles($path, '*.yml'), $this->findFiles($path, '*.yaml'), $this->findFiles($path, '*.json') ); $tabFiles = 0; foreach ($configFiles as $file) { if (str_contains((string) file_get_contents($file), "\t")) { $tabFiles++; $this->log('WARN', 'Tab found: ' . substr($file, strlen($path) + 1)); } } $ok = $tabFiles === 0; $this->status($ok, "No tabs in YAML/JSON ({$tabFiles} files affected)"); $ok ? $passed++ : $failed++; // ── Summary ─────────────────────────────────────────────────────── $this->printSummary($passed, $failed, $this->elapsed()); return $failed > 0 ? 1 : 0; } // ── Helpers ─────────────────────────────────────────────────────────── private function findManifest(string $path): ?string { // Check root first, then common Joomla locations $candidates = array_merge( glob($path . '/*.xml') ?: [], glob($path . '/administrator/components/com_*/*.xml') ?: [] ); foreach ($candidates as $xml) { $content = (string) file_get_contents($xml); if (str_contains($content, 'type="component"') || str_contains($content, "type='component'")) { return $xml; } } return null; } private function findFiles(string $dir, string $pattern): array { if (!is_dir($dir)) { return []; } $results = []; $iter = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS) ); foreach ($iter as $file) { if ($file->isFile() && fnmatch($pattern, $file->getFilename())) { $results[] = $file->getPathname(); } } return $results; } } $script = new ValidateJoomlaManifest('validate_manifest', 'Validate a Joomla component XML manifest against MokoStandards requirements'); exit($script->execute());