1d87be7d5e
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
- Update REPO: from MokoStandards-API to moko-platform in 125 files - Fix wrong org path (mokoconsulting-tech → MokoConsulting) in 10 files - Fix SPDX-LICENSE-IDENTIFIER case in 2 template files - Add missing REPO: field to 3 files Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
193 lines
7.1 KiB
PHP
193 lines
7.1 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.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());
|