Files
mcphub/wordpress-plugin/airano-mcp-bridge-wporg/tests/check.sh
airano-ir 43fd2201a0 feat(v3.13.0): settings live DB reads, remove Directus/Appwrite, admin stats
Settings fixes:
- MAX_SITES_PER_USER, USER_RATE_LIMIT_PER_MIN/HR now read from DB settings
  table (DB > ENV > default), so dashboard/settings changes apply without
  restart. Sync cache refreshed on every save or delete.
- /api/me reports the live DB value for max_sites_per_user.

Admin improvements:
- Admin users bypass per-user rate limiting entirely (role=admin or ADMIN_EMAILS).
- Admin Overview now shows platform stats: registered users, new users (7d),
  total user sites, available tools.

Plugin cleanup:
- Appwrite and Directus plugins removed from the active registry (8 plugins
  now: WordPress, WooCommerce, WordPress Specialist, Gitea, n8n, Supabase,
  OpenPanel, Coolify). Plugin code is retained for future re-enabling.
- Settings page plugin visibility list updated to match.

Mobile onboarding:
- Stepper steps on narrow viewports stack vertically with correct full border
  and rounded corners on each step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:33:20 +02:00

171 lines
7.8 KiB
Bash

#!/usr/bin/env bash
# Pre-submission checks for airano-mcp-bridge (wp.org-bound version).
#
# Run from the repo root or from this directory:
# bash wordpress-plugin/airano-mcp-bridge-wporg/tests/check.sh
#
# Exit code is non-zero on any FAIL. Prints a one-line summary at the end.
set -u
PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/airano-mcp-bridge"
PHP_FILE="${PLUGIN_DIR}/airano-mcp-bridge.php"
README_TXT="${PLUGIN_DIR}/readme.txt"
fail=0
pass() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail+1)); }
section() { printf '\n\033[1m%s\033[0m\n' "$1"; }
[ -f "$PHP_FILE" ] || { echo "main PHP file not found: $PHP_FILE"; exit 2; }
[ -f "$README_TXT" ] || { echo "readme.txt not found: $README_TXT"; exit 2; }
section "1. PHP syntax (php -l)"
if php -l "$PHP_FILE" > /tmp/phplint.out 2>&1; then
pass "syntax clean"
else
fail "php -l reported errors:"
cat /tmp/phplint.out
fi
section "2. Plugin headers"
ver_php=$(grep -oE '^\s*\*\s*Version:\s*\S+' "$PHP_FILE" | head -1 | awk '{print $NF}')
ver_const=$(grep -oE "const VERSION\s*=\s*'[^']+'" "$PHP_FILE" | head -1 | grep -oE "'[^']+'" | tr -d "'")
stable=$(grep -oE '^Stable tag:\s*\S+' "$README_TXT" | awk '{print $NF}')
tested=$(grep -oE '^Tested up to:\s*\S+' "$README_TXT" | awk '{print $NF}')
plugin_uri=$(grep -oE '^\s*\*\s*Plugin URI:\s*\S+' "$PHP_FILE" | head -1 | awk '{print $NF}')
author_uri=$(grep -oE '^\s*\*\s*Author URI:\s*\S+' "$PHP_FILE" | head -1 | awk '{print $NF}')
[ "$ver_php" = "$ver_const" ] && pass "Version header == const VERSION ($ver_php)" || fail "Version mismatch: header=$ver_php const=$ver_const"
[ "$ver_php" = "$stable" ] && pass "Version header == readme.txt Stable tag ($ver_php)" || fail "Stable tag mismatch: $ver_php vs $stable"
case "$tested" in
*.*.*) fail "Tested up to has minor version ($tested) — wp.org rejects this. Use major.minor only." ;;
*.*) pass "Tested up to is major.minor ($tested)" ;;
*) fail "Tested up to has unexpected format: $tested" ;;
esac
[ -n "$plugin_uri" ] && [ -n "$author_uri" ] && [ "$plugin_uri" != "$author_uri" ] \
&& pass "Plugin URI ($plugin_uri) and Author URI ($author_uri) are distinct" \
|| fail "Plugin URI and Author URI are the same or missing"
section "3. wp.org review issues from review email"
# Class name must use the Airano_MCP_ prefix (no generic SEO_API_Bridge)
if grep -q "^class SEO_API_Bridge" "$PHP_FILE"; then
fail "main class still named SEO_API_Bridge — wp.org rejects generic prefixes"
else
pass "main class is not the generic SEO_API_Bridge"
fi
if grep -q "^class Airano_MCP_Bridge" "$PHP_FILE"; then
pass "main class uses the Airano_MCP_ prefix"
else
fail "main class is not Airano_MCP_Bridge"
fi
# (a) media.php must NOT be loaded directly outside of the comment/changelog
includes=$(grep -nE "^\s*require(_once)?\s*\(?\s*ABSPATH\s*\.\s*['\"]wp-admin/includes/media\.php" "$PHP_FILE" | wc -l)
if [ "$includes" -eq 0 ]; then
pass "no direct require of wp-admin/includes/media.php"
else
fail "found $includes direct require(_once) of wp-admin/includes/media.php"
grep -nE "^\s*require(_once)?\s*\(?\s*ABSPATH\s*\.\s*['\"]wp-admin/includes/media\.php" "$PHP_FILE"
fi
# (b) /upload-and-attach must use the new dedicated permission_callback
if grep -q "'permission_callback' => \[\$this, 'require_upload_and_attach_capability'\]" "$PHP_FILE"; then
pass "/upload-and-attach is gated by require_upload_and_attach_capability"
else
fail "/upload-and-attach does not use require_upload_and_attach_capability"
fi
if grep -q "function require_upload_and_attach_capability" "$PHP_FILE"; then
pass "require_upload_and_attach_capability method is defined"
else
fail "require_upload_and_attach_capability method is missing"
fi
# It must call current_user_can('edit_post', $attach_to_post)
if grep -q "current_user_can('edit_post', \$attach_to_post)" "$PHP_FILE"; then
pass "edit_post per-target check is enforced at the route gate"
else
fail "edit_post per-target check is missing"
fi
section "4. REST routes — every register_rest_route has a permission_callback"
# Walk the file and check each register_rest_route(...) block individually.
# A block runs from its register_rest_route line up to the matching closing
# `]);` at the same brace depth. We check each block contains
# `permission_callback`.
audit=$(php -r '
$src = file_get_contents($argv[1]);
$lines = preg_split("/\r?\n/", $src);
$total = 0; $missing = 0; $missing_lines = [];
$inBlock = false; $depth = 0; $hasPerm = false; $startLine = 0;
foreach ($lines as $i => $line) {
$lineNo = $i + 1;
if (!$inBlock && preg_match("/register_rest_route\s*\(/", $line)) {
$inBlock = true; $depth = 0; $hasPerm = false; $startLine = $lineNo; $total++;
}
if ($inBlock) {
if (strpos($line, "permission_callback") !== false) $hasPerm = true;
$depth += substr_count($line, "[") + substr_count($line, "(");
$depth -= substr_count($line, "]") + substr_count($line, ")");
if ($depth <= 0 && $lineNo > $startLine) {
if (!$hasPerm) { $missing++; $missing_lines[] = $startLine; }
$inBlock = false;
}
}
}
echo "$total $missing " . implode(",", $missing_lines);
' "$PHP_FILE")
total=$(echo "$audit" | awk '{print $1}')
missing=$(echo "$audit" | awk '{print $2}')
missing_lines=$(echo "$audit" | awk '{print $3}')
if [ "$missing" -eq 0 ]; then
pass "all $total REST routes have a permission_callback"
else
fail "$missing of $total REST routes are missing permission_callback (lines: $missing_lines)"
fi
# No __return_true on routes that change state (POST/PUT/DELETE)
public_writes=$(awk '/register_rest_route/,/\]\s*\)\s*;/' "$PHP_FILE" \
| grep -B2 "__return_true" \
| grep -E "'methods'\s*=>\s*'(POST|PUT|DELETE|PATCH)'" \
| wc -l)
if [ "$public_writes" -eq 0 ]; then
pass "no public (__return_true) callbacks on POST/PUT/DELETE routes"
else
fail "$public_writes write routes use __return_true (public) — review needed"
fi
section "5. Output escaping & sanitisation (heuristic — manual review still required)"
# Find raw echo/print of REST request params
raw_echo=$(grep -nE "echo\s+\\\$request->|print\s+\\\$request->" "$PHP_FILE" | wc -l)
[ "$raw_echo" -eq 0 ] && pass "no raw echo of \$request-> values" || fail "$raw_echo raw echoes of \$request->"
# Reject sql concatenation patterns
sql_concat=$(grep -nE '\$wpdb->(query|get_results|get_var|get_row).*\.\s*\$' "$PHP_FILE" | wc -l)
[ "$sql_concat" -eq 0 ] && pass "no obvious SQL concatenation (no \$wpdb-> ... . \$var)" || fail "$sql_concat possible SQL concatenations — manual review"
# eval / system / exec must NOT appear
dangerous=$(grep -nE "\b(eval|exec|system|passthru|popen|shell_exec|proc_open)\s*\(" "$PHP_FILE" | grep -v "^\s*\*" | wc -l)
[ "$dangerous" -eq 0 ] && pass "no eval/exec/system/passthru/popen calls" || fail "$dangerous dangerous function calls"
section "6. Text domain"
td_count=$(grep -cE "(__|_e|_x|_n|esc_html__|esc_attr__|esc_html_e|esc_attr_e)\(\s*['\"]" "$PHP_FILE")
non_plugin_td=$(grep -oE "(__|_e|_x|_n|esc_html__|esc_attr__|esc_html_e|esc_attr_e)\(\s*['\"][^'\"]*['\"]\s*,\s*['\"][^'\"]+['\"]" "$PHP_FILE" \
| grep -oE "['\"][^'\"]+['\"]\s*\)\s*$" | grep -vc "airano-mcp-bridge" || true)
if [ "$non_plugin_td" -eq 0 ]; then
pass "$td_count translation calls all use 'airano-mcp-bridge' domain"
else
fail "$non_plugin_td translation calls use a non-plugin text domain"
fi
section "7. Direct file access guard"
if grep -q "if (!defined('ABSPATH'))" "$PHP_FILE" && grep -q "exit;" "$PHP_FILE"; then
pass "ABSPATH guard present"
else
fail "missing 'if (!defined(ABSPATH)) exit;' guard"
fi
section "Summary"
if [ "$fail" -eq 0 ]; then
printf '\033[32m✓ all checks passed\033[0m — version %s ready for wp.org review reply\n' "$ver_php"
exit 0
else
printf '\033[31m✗ %d check(s) failed\033[0m\n' "$fail"
exit 1
fi