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>
171 lines
7.8 KiB
Bash
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
|