From 4fd1acb68cb456069c6e2e3957641a637c8d7444 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 02:29:36 +0000 Subject: [PATCH 1/3] feat(cli): add theme_lint.php --- cli/theme_lint.php | 209 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 cli/theme_lint.php diff --git a/cli/theme_lint.php b/cli/theme_lint.php new file mode 100644 index 0000000..971ead8 --- /dev/null +++ b/cli/theme_lint.php @@ -0,0 +1,209 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: moko-platform.CLI + * INGROUP: moko-platform + * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform + * PATH: /cli/theme_lint.php + * BRIEF: Lint theme files — CSS syntax, image sizes, hardcoded URLs + * + * Usage: + * php theme_lint.php --path /repo + * php theme_lint.php --path /repo --max-image-kb 500 + * php theme_lint.php --path /repo --github-output + * + * Options: + * --path Repository root (default: .) + * --max-image-kb Maximum image file size in KB (default: 500) + * --github-output Export results to $GITHUB_OUTPUT + * --strict Exit 1 on any warning (default: only on errors) + */ + +declare(strict_types=1); + +$path = '.'; +$maxImageKb = 500; +$ghOutput = false; +$strict = false; + +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--max-image-kb' && isset($argv[$i + 1])) $maxImageKb = (int)$argv[$i + 1]; + if ($arg === '--github-output') $ghOutput = true; + if ($arg === '--strict') $strict = true; +} + +$root = realpath($path) ?: $path; +$errors = 0; +$warnings = 0; + +// ── Find source directory ─────────────────────────────────────────────── +$srcDir = null; +foreach (['src', 'htdocs'] as $d) { + if (is_dir("{$root}/{$d}")) { $srcDir = "{$root}/{$d}"; break; } +} +if ($srcDir === null) { + fwrite(STDERR, "No src/ or htdocs/ directory in {$root}\n"); + exit(1); +} + +echo "Theme Lint: {$srcDir}\n\n"; + +// ── Check 1: CSS syntax validation ────────────────────────────────────── +echo "--- CSS Syntax ---\n"; +$cssFiles = findFiles($srcDir, '*.css'); +$cssMinFiles = findFiles($srcDir, '*.min.css'); +$cssToCheck = array_diff($cssFiles, $cssMinFiles); + +if (empty($cssToCheck)) { + echo " No CSS files to check\n"; +} else { + foreach ($cssToCheck as $file) { + $content = file_get_contents($file); + $relPath = str_replace($root . '/', '', $file); + + // Check for unmatched braces + $openBraces = substr_count($content, '{'); + $closeBraces = substr_count($content, '}'); + if ($openBraces !== $closeBraces) { + echo " ERROR: {$relPath}: unmatched braces (open={$openBraces}, close={$closeBraces})\n"; + $errors++; + } + + // Check for empty rules + if (preg_match_all('/\{[\s]*\}/', $content, $m)) { + $count = count($m[0]); + echo " WARN: {$relPath}: {$count} empty rule(s)\n"; + $warnings++; + } + + // Check for !important abuse (more than 10 in one file) + $importantCount = substr_count($content, '!important'); + if ($importantCount > 10) { + echo " WARN: {$relPath}: {$importantCount} !important declarations (consider refactoring)\n"; + $warnings++; + } + } + + if ($errors === 0) { + echo " OK: " . count($cssToCheck) . " CSS file(s) checked\n"; + } +} + +// ── Check 2: Image file sizes ─────────────────────────────────────────── +echo "\n--- Image Sizes (max {$maxImageKb}KB) ---\n"; +$imageExts = ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.webp', '*.svg', '*.bmp']; +$images = []; +foreach ($imageExts as $ext) { + $images = array_merge($images, findFiles($srcDir, $ext)); +} +// Also check root images/ directory +if (is_dir("{$root}/images")) { + foreach ($imageExts as $ext) { + $images = array_merge($images, findFiles("{$root}/images", $ext)); + } +} + +$oversized = 0; +$totalSize = 0; +foreach ($images as $file) { + $size = filesize($file); + $totalSize += $size; + $relPath = str_replace($root . '/', '', $file); + $sizeKb = round($size / 1024); + + if ($sizeKb > $maxImageKb) { + echo " WARN: {$relPath}: {$sizeKb}KB (exceeds {$maxImageKb}KB limit)\n"; + $oversized++; + $warnings++; + } +} + +$totalMb = round($totalSize / 1024 / 1024, 1); +echo " " . count($images) . " image(s), {$totalMb}MB total"; +if ($oversized > 0) { + echo ", {$oversized} oversized"; +} +echo "\n"; + +// ── Check 3: Hardcoded URLs in CSS/JS ─────────────────────────────────── +echo "\n--- Hardcoded URLs ---\n"; +$codeFiles = array_merge( + findFiles($srcDir, '*.css'), + findFiles($srcDir, '*.js') +); +// Exclude minified files +$codeFiles = array_filter($codeFiles, function($f) { + return !preg_match('/\.min\.(css|js)$/', $f); +}); + +$urlPatterns = [ + '/https?:\/\/clarksvillefurs\.com/' => 'hardcoded production URL', + '/https?:\/\/[a-z]+\.dev\.mokoconsulting\.tech/' => 'hardcoded dev URL', + '/https?:\/\/localhost/' => 'localhost reference', +]; + +$urlIssues = 0; +foreach ($codeFiles as $file) { + $content = file_get_contents($file); + $relPath = str_replace($root . '/', '', $file); + + foreach ($urlPatterns as $pattern => $desc) { + if (preg_match_all($pattern, $content, $matches)) { + $count = count($matches[0]); + echo " WARN: {$relPath}: {$count} {$desc}\n"; + $urlIssues++; + $warnings++; + } + } +} + +if ($urlIssues === 0) { + echo " OK: No hardcoded URLs found\n"; +} + +// ── Summary ───────────────────────────────────────────────────────────── +echo "\n=== Summary ===\n"; +echo "Errors: {$errors}\n"; +echo "Warnings: {$warnings}\n"; + +if ($ghOutput) { + $ghFile = getenv('GITHUB_OUTPUT'); + if ($ghFile) { + file_put_contents($ghFile, "lint_errors={$errors}\n", FILE_APPEND); + file_put_contents($ghFile, "lint_warnings={$warnings}\n", FILE_APPEND); + file_put_contents($ghFile, "lint_images=" . count($images) . "\n", FILE_APPEND); + file_put_contents($ghFile, "lint_css=" . count($cssToCheck) . "\n", FILE_APPEND); + } +} + +if ($errors > 0) { + exit(1); +} +if ($strict && $warnings > 0) { + exit(1); +} +exit(0); + +// ── Helper: recursively find files matching a glob pattern ────────────── +function findFiles(string $dir, string $pattern): array +{ + $results = []; + if (!is_dir($dir)) return $results; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + if (fnmatch($pattern, $file->getFilename())) { + $results[] = $file->getPathname(); + } + } + + return $results; +} -- 2.52.0 From 78fcbdd4a949b2d30ccf7c5e5264c9eeed2cfd3d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 02:29:36 +0000 Subject: [PATCH 2/3] feat(cli): add joomla_compat_check.php --- cli/joomla_compat_check.php | 136 ++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 cli/joomla_compat_check.php diff --git a/cli/joomla_compat_check.php b/cli/joomla_compat_check.php new file mode 100644 index 0000000..87c6d34 --- /dev/null +++ b/cli/joomla_compat_check.php @@ -0,0 +1,136 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: moko-platform.CLI + * INGROUP: moko-platform + * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform + * PATH: /cli/joomla_compat_check.php + * BRIEF: Check if extension targetplatform regex matches the latest Joomla version + * + * Usage: + * php joomla_compat_check.php --path /repo + * php joomla_compat_check.php --path /repo --github-output + * + * Options: + * --path Repository root (default: .) + * --github-output Export results to $GITHUB_OUTPUT + */ + +declare(strict_types=1); + +$path = '.'; +$ghOutput = false; + +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--github-output') $ghOutput = true; +} + +$root = realpath($path) ?: $path; + +// ── Find manifest and extract targetplatform ──────────────────────────── +$manifest = null; +$searchDirs = ["{$root}/src", $root]; +foreach ($searchDirs as $dir) { + if (!is_dir($dir)) continue; + foreach (glob("{$dir}/*.xml") ?: [] as $f) { + $xml = file_get_contents($f); + if (strpos($xml, ']*version="([^"]+)"/', $xml, $m)) { + $targetRegex = $m[1]; +} + +if (empty($targetRegex)) { + echo "No targetplatform version found in {$relManifest}\n"; + exit(1); +} + +echo "Manifest: {$relManifest}\n"; +echo "Target regex: {$targetRegex}\n"; + +// ── Fetch latest Joomla version ───────────────────────────────────────── +$joomlaVersions = []; +$updateUrl = 'https://update.joomla.org/core/sts/list_sts.xml'; +$updateXml = @file_get_contents($updateUrl); + +if ($updateXml === false) { + // Fallback: try the LTS feed + $updateUrl = 'https://update.joomla.org/core/list.xml'; + $updateXml = @file_get_contents($updateUrl); +} + +if ($updateXml !== false) { + // Parse all version entries + preg_match_all('/([^<]+)<\/version>/', $updateXml, $matches); + $joomlaVersions = $matches[1] ?? []; +} + +if (empty($joomlaVersions)) { + echo "WARNING: Could not fetch Joomla versions from update server\n"; + echo "Tested URL: {$updateUrl}\n"; + exit(0); +} + +// Sort and get latest +usort($joomlaVersions, 'version_compare'); +$latestJoomla = end($joomlaVersions); + +echo "Latest Joomla: {$latestJoomla}\n"; + +// ── Test compatibility ────────────────────────────────────────────────── +// The targetplatform regex uses Joomla's regex format +// Common patterns: "5\.[0-9]+" or "((5.[0-9])|(6.[0-9]))" +$compatible = @preg_match("/{$targetRegex}/", $latestJoomla); + +if ($compatible === false) { + echo "ERROR: Invalid regex in targetplatform: {$targetRegex}\n"; + $result = 'error'; +} elseif ($compatible === 1) { + echo "PASS: Joomla {$latestJoomla} matches targetplatform regex\n"; + $result = 'pass'; +} else { + // Check which major versions are supported + $supported = []; + foreach (['5.0', '5.1', '5.2', '5.3', '5.4', '6.0', '6.1', '6.2', '7.0'] as $v) { + if (@preg_match("/{$targetRegex}/", $v)) { + $supported[] = $v; + } + } + + echo "WARN: Joomla {$latestJoomla} does NOT match targetplatform regex\n"; + echo "Supported versions: " . implode(', ', $supported) . "\n"; + echo "Consider updating targetplatform to include Joomla {$latestJoomla}\n"; + $result = 'warn'; +} + +// ── Export ─────────────────────────────────────────────────────────────── +if ($ghOutput) { + $ghFile = getenv('GITHUB_OUTPUT'); + if ($ghFile) { + file_put_contents($ghFile, "compat_result={$result}\n", FILE_APPEND); + file_put_contents($ghFile, "compat_joomla={$latestJoomla}\n", FILE_APPEND); + file_put_contents($ghFile, "compat_regex={$targetRegex}\n", FILE_APPEND); + } +} + +exit($result === 'error' ? 1 : 0); -- 2.52.0 From 44c6bcbc2d5f10eb2ee3d56ba1f66bf83453e8c0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 02:29:37 +0000 Subject: [PATCH 3/3] feat(cli): add client_health_check.php --- cli/client_health_check.php | 188 ++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 cli/client_health_check.php diff --git a/cli/client_health_check.php b/cli/client_health_check.php new file mode 100644 index 0000000..6f0268c --- /dev/null +++ b/cli/client_health_check.php @@ -0,0 +1,188 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: moko-platform.CLI + * INGROUP: moko-platform + * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform + * PATH: /cli/client_health_check.php + * BRIEF: Verify a client site's update server, installed version, and release availability + * + * Usage: + * php client_health_check.php --update-url URL + * php client_health_check.php --path /repo --github-output + * + * Options: + * --path Repository root (reads update server URL from manifest) + * --update-url Update server XML URL (overrides manifest) + * --site-url Live site URL for version checking via Joomla API (optional) + * --api-token Joomla API token for site-url (optional) + * --github-output Export results to $GITHUB_OUTPUT + */ + +declare(strict_types=1); + +$path = '.'; +$updateUrl = null; +$siteUrl = null; +$apiToken = null; +$ghOutput = false; + +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--update-url' && isset($argv[$i + 1])) $updateUrl = $argv[$i + 1]; + if ($arg === '--site-url' && isset($argv[$i + 1])) $siteUrl = $argv[$i + 1]; + if ($arg === '--api-token' && isset($argv[$i + 1])) $apiToken = $argv[$i + 1]; + if ($arg === '--github-output') $ghOutput = true; +} + +$root = realpath($path) ?: $path; +$checks = []; + +// ── Resolve update server URL from manifest ───────────────────────────── +if ($updateUrl === null) { + $searchDirs = ["{$root}/src", $root]; + foreach ($searchDirs as $dir) { + if (!is_dir($dir)) continue; + foreach (glob("{$dir}/*.xml") ?: [] as $f) { + $xml = file_get_contents($f); + if (preg_match('/]*>([^<]+)<\/server>/', $xml, $m)) { + $updateUrl = trim($m[1]); + break 2; + } + } + } +} + +if ($updateUrl === null) { + fwrite(STDERR, "No update server URL found. Use --update-url or provide a manifest with .\n"); + exit(1); +} + +echo "Update server: {$updateUrl}\n\n"; + +// ── Check 1: Update server accessible ─────────────────────────────────── +echo "--- Update Server ---\n"; +$ch = curl_init($updateUrl); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTPHEADER => ['User-Agent: MokoHealthCheck/1.0'], +]); +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($httpCode === 200 && !empty($response)) { + echo " PASS: HTTP {$httpCode}, " . strlen($response) . " bytes\n"; + $checks['update_server'] = 'pass'; +} else { + echo " FAIL: HTTP {$httpCode}\n"; + $checks['update_server'] = 'fail'; +} + +// ── Check 2: Parse updates.xml for stable version ─────────────────────── +$stableVersion = null; +$downloadUrl = null; + +if (!empty($response)) { + $sections = preg_split('//', $response); + foreach ($sections as $section) { + if (strpos($section, 'stable') !== false) { + if (preg_match('/([^<]+)<\/version>/', $section, $m)) { + $stableVersion = $m[1]; + } + if (preg_match('/]*>([^<]+)<\/downloadurl>/', $section, $m)) { + $downloadUrl = trim($m[1]); + } + break; + } + } + + if ($stableVersion === null && preg_match('/([^<]+)<\/version>/', $response, $m)) { + $stableVersion = $m[1]; + } +} + +echo "\n--- Stable Release ---\n"; +if ($stableVersion !== null) { + echo " Version: {$stableVersion}\n"; + $checks['stable_version'] = $stableVersion; +} else { + echo " FAIL: Could not parse stable version\n"; + $checks['stable_version'] = 'fail'; +} + +// ── Check 3: Download URL accessible ──────────────────────────────────── +if ($downloadUrl !== null) { + echo "\n--- Download URL ---\n"; + $ch = curl_init($downloadUrl); + curl_setopt_array($ch, [ + CURLOPT_NOBODY => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_FOLLOWLOCATION => true, + ]); + curl_exec($ch); + $dlCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $dlSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); + curl_close($ch); + + if ($dlCode === 200) { + $sizeKb = $dlSize > 0 ? round($dlSize / 1024) . 'KB' : 'unknown size'; + echo " PASS: HTTP {$dlCode}, {$sizeKb}\n"; + $checks['download'] = 'pass'; + } else { + echo " FAIL: HTTP {$dlCode}\n"; + $checks['download'] = 'fail'; + } +} + +// ── Check 4: Site version (optional) ──────────────────────────────────── +if ($siteUrl !== null && $apiToken !== null) { + echo "\n--- Site Version ---\n"; + $apiUrl = rtrim($siteUrl, '/') . '/api/index.php/v1/extensions?filter[type]=file'; + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_HTTPHEADER => [ + "X-Joomla-Token: {$apiToken}", + 'Accept: application/json', + ], + ]); + $siteResponse = curl_exec($ch); + $siteCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($siteCode === 200) { + echo " API accessible (HTTP {$siteCode})\n"; + $checks['site_api'] = 'pass'; + } else { + echo " WARN: Site API returned HTTP {$siteCode}\n"; + $checks['site_api'] = 'warn'; + } +} + +// ── Summary ───────────────────────────────────────────────────────────── +echo "\n=== Health Check Summary ===\n"; +$failed = 0; +foreach ($checks as $name => $result) { + $icon = ($result === 'fail') ? 'FAIL' : (($result === 'warn') ? 'WARN' : 'OK'); + if ($result === 'fail') $failed++; + echo " {$icon}: {$name} = {$result}\n"; +} + +if ($ghOutput) { + $ghFile = getenv('GITHUB_OUTPUT'); + if ($ghFile) { + file_put_contents($ghFile, "health_status=" . ($failed > 0 ? 'fail' : 'pass') . "\n", FILE_APPEND); + file_put_contents($ghFile, "health_version=" . ($stableVersion ?? 'unknown') . "\n", FILE_APPEND); + file_put_contents($ghFile, "health_failures={$failed}\n", FILE_APPEND); + } +} + +exit($failed > 0 ? 1 : 0); -- 2.52.0