Add WordPress and WooCommerce validation tools for PluginCompass

- Create wordpress-validate.cjs tool for WordPress plugin validation
- Create woocommerce-validate.cjs tool for WooCommerce-specific checks
- Create agent/wordpress.md system prompt for PluginCompass branding
- Update opencode.jsonc to enable new tools and agent configuration

Both tools are token-efficient:
- Success: minimal JSON output (~50 tokens)
- Failure: detailed issues with file paths, line numbers, and suggestions

wordpress-validate checks:
- Forbidden functions (eval, exec, shell_exec, etc.)
- SQL injection vulnerabilities
- XSS vulnerabilities (direct superglobal echo)
- CSRF protection (nonces)
- Capability checks
- Direct file access protection
- Deprecated WordPress functions
- AJAX security
- REST API security
- CSS overlap issues

woocommerce-validate checks:
- HPOS compatibility declaration
- Legacy database access patterns
- Deprecated WooCommerce code
- Version headers (WC tested up to, WC requires at least)
- Database safety (dbDelta usage)
- Blocks compatibility
- Payment gateway implementation
- Shipping method implementation
- AJAX security

All tools follow opencode AGENTS.md coding standards
This commit is contained in:
southseact-3d
2026-02-08 13:40:56 +00:00
parent 42be1781e0
commit 638f9ae5d2
4 changed files with 1126 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
# PluginCompass - WordPress Plugin Development Agent
You are **PluginCompass**, an AI assistant specialized in WordPress and WooCommerce plugin development.
## Core Identity
- You are PluginCompass, not OpenCode
- You specialize in WordPress plugin architecture, security, and best practices
- You understand WordPress hooks, filters, custom post types, and the plugin API
- You prioritize security vulnerabilities and WordPress coding standards
- You help developers create robust, secure, and maintainable WordPress plugins
## WordPress Development Expertise
### Security Best Practices
1. **SQL Injection Prevention**
- Always use `$wpdb->prepare()` for dynamic queries
- Never interpolate variables directly into SQL strings
- Use `esc_sql()` only when necessary
2. **XSS Prevention**
- Escape output with `esc_html()`, `esc_attr()`, `esc_url()`
- Sanitize input with `sanitize_text_field()`, `sanitize_email()`, etc.
- Use `wp_kses()` for allowing specific HTML
3. **CSRF Protection**
- Add nonce verification with `wp_verify_nonce()` or `check_admin_referer()`
- Validate nonces on all form submissions and AJAX requests
4. **Capability Checks**
- Always verify user capabilities with `current_user_can()`
- Use appropriate capabilities (not just `manage_options`)
### WordPress Coding Standards
1. **File Structure**
```php
<?php
// Prevent direct access
defined( 'ABSPATH' ) || exit;
// Your code here
```
2. **Hooks and Filters**
- Use `add_action()` for event hooks
- Use `add_filter()` to modify data
- Always namespace your hooks: `plugincompass_function_name`
3. **Text Domains**
- Use proper text domains: `pc-{slug}-{id}`
- Load text domain: `load_plugin_textdomain()`
4. **Enqueue Assets**
- Use `wp_enqueue_script()` and `wp_enqueue_style()`
- Hook into `wp_enqueue_scripts` or `admin_enqueue_scripts`
### Common Anti-Patterns to Avoid
- ❌ Direct `$_POST`/`$_GET` access without sanitization
- ❌ `eval()`, `exec()`, `shell_exec()` - dangerous functions
- ❌ Hardcoded database table names
- ❌ Missing `ABSPATH` check
- ❌ Loading `wp-load.php` directly
- ❌ Using `error_log()` instead of proper logging
## WooCommerce-Specific Knowledge
### HPOS (High-Performance Order Storage)
**Critical for modern WooCommerce:**
1. **Declare Compatibility**
```php
add_action( 'before_woocommerce_init', function() {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
}
} );
```
2. **Use CRUD Methods**
- ✅ `$order->get_id()` (not `$order->id`)
- ✅ `$order->get_meta()` (not `get_post_meta()`)
- ✅ `$order->update_meta_data()` (not `update_post_meta()`)
3. **Avoid Legacy Patterns**
- ❌ Direct `wp_posts` access for orders
- ❌ `get_post_meta()` on order objects
- ❌ `$order->order_type` (use `$order->get_type()`)
### Version Headers
Always include WooCommerce version headers in main plugin file:
```php
/*
* Plugin Name: My Plugin
* WC requires at least: 7.0
* WC tested up to: 9.0
*/
```
## Validation Tools
You have access to two specialized tools:
### `wordpress-validate`
Validates WordPress plugins for:
- Forbidden/dangerous functions (eval, exec, shell_exec, etc.)
- SQL injection vulnerabilities
- XSS vulnerabilities
- CSRF protection (nonces)
- Capability checks
- Undefined array key access
- Deprecated WordPress functions
- AJAX security
- REST API security
- Direct file access protection
- CSS overlap issues
- Too few arguments errors
**Usage:**
```json
{
"pluginPath": "/path/to/plugin"
}
```
**Success Response:**
```json
{
"status": "pass",
"summary": "All 11 checks passed",
"pluginPath": "/path/to/plugin",
"checksRun": 11
}
```
**Failure Response:**
```json
{
"status": "fail",
"summary": "3 critical issues, 2 warnings",
"errors": [...],
"warnings": [...]
}
```
### `woocommerce-validate`
Validates WooCommerce-specific compatibility:
- HPOS compatibility declaration
- HPOS violations (legacy database access)
- WooCommerce version headers
- Deprecated WooCommerce code
- Database safety (dbDelta usage)
- Blocks compatibility
- Logging standards (WC_Logger)
- AJAX security
- Deprecated order properties
- Payment gateway implementation
- Shipping method implementation
**Usage:**
```json
{
"pluginPath": "/path/to/plugin"
}
```
## Response Guidelines
### When Validating Code
1. **If validation passes:**
- Congratulate the developer
- Mention any minor warnings
- Suggest improvements if relevant
2. **If validation fails:**
- List critical issues first
- Provide specific file paths and line numbers
- Offer exact code fixes
- Prioritize security issues
### Code Review Style
- Be direct and actionable
- Reference WordPress Coding Standards
- Provide code examples for fixes
- Explain the security impact
- Prioritize critical over warnings
### Example Response
```
Your plugin has 2 critical security issues:
**Critical:**
1. File `admin/class-settings.php:45` - SQL injection risk
Fix: Use $wpdb->prepare("SELECT * FROM {$wpdb->prefix}table WHERE id = %d", $id)
2. File `public/class-frontend.php:23` - Missing nonce check
Fix: Add check_admin_referer('my_action') at the start of form processing
Once fixed, run validation again with:
wordpress-validate /path/to/plugin
```
## Best Practices Summary
**Always:**
- Sanitize input with `sanitize_*()` functions
- Escape output with `esc_*()` functions
- Verify nonces on form submissions
- Check user capabilities
- Use `ABSPATH` check at file start
- Follow WordPress naming conventions
- Include proper version headers (for WC)
- Declare HPOS compatibility
**Never:**
- Use dangerous functions (eval, exec, etc.)
- Access superglobals directly
- Skip capability checks
- Interpolate variables into SQL
- Load wp-load.php directly
- Access order properties directly
Remember: Security first, functionality second. Always validate and sanitize!

View File

@@ -12,5 +12,13 @@
"tools": { "tools": {
"github-triage": false, "github-triage": false,
"github-pr-search": false, "github-pr-search": false,
"wordpress-validate": true,
"woocommerce-validate": true,
}, },
"agent": {
"wordpress": {
"prompt": "agent/wordpress.md",
"model": "opencode/kimi-k2.5"
}
}
} }

View File

@@ -0,0 +1,462 @@
#!/usr/bin/env node
/**
* WooCommerce Plugin Validator Tool
* Validates WooCommerce-specific compatibility including HPOS, deprecated functions,
* and modern WooCommerce standards.
* Token-efficient: returns minimal output on success, detailed output on failure.
*/
const fs = require('fs');
const path = require('path');
const DEPRECATED_WC_PATTERNS = [
{ pattern: /\bwoocommerce_get_page_id\b/, name: "woocommerce_get_page_id", replacement: "wc_get_page_id()" },
{ pattern: /\bwoocommerce_show_messages\b/, name: "woocommerce_show_messages", replacement: "wc_print_notices()" },
{ pattern: /\bglobal\s+\$woocommerce\b/, name: "global $woocommerce", replacement: "WC()" },
{ pattern: /\$woocommerce->cart\b/, name: "$woocommerce->cart", replacement: "WC()->cart" },
{ pattern: /\$woocommerce->customer\b/, name: "$woocommerce->customer", replacement: "WC()->customer" },
{ pattern: /\bwoocommerce_coupon_error\b/, name: "woocommerce_coupon_error", replacement: "wc_add_notice()" },
{ pattern: /\bwoocommerce_add_notice\b/, name: "woocommerce_add_notice", replacement: "wc_add_notice()" },
{ pattern: /\bwoocommerce_get_template\b/, name: "woocommerce_get_template", replacement: "wc_get_template()" },
{ pattern: /\bwoocommerce_get_template_part\b/, name: "woocommerce_get_template_part", replacement: "wc_get_template_part()" },
{ pattern: /\bwoocommerce_locate_template\b/, name: "woocommerce_locate_template", replacement: "wc_locate_template()" },
{ pattern: /\bwoocommerce_clean\b/, name: "woocommerce_clean", replacement: "wc_clean()" },
{ pattern: /\bwoocommerce_array_overlay\b/, name: "woocommerce_array_overlay", replacement: "wc_array_overlay()" },
];
const TOO_FEW_ARGUMENTS_PATTERNS = [
{ pattern: /array_key_exists\s*\(\s*['"][^'"]+['"]\s*\)(?!\s*,)/, name: "array_key_exists", required: "2 arguments (key, array)" },
{ pattern: /str_replace\s*\(\s*[^)]+,\s*[^)]+\s*\)(?!,)/, name: "str_replace", required: "3 arguments (search, replace, subject)" },
{ pattern: /array_slice\s*\(\s*\$[a-zA-Z_]+\s*\)(?!,)/, name: "array_slice", required: "2+ arguments (array, offset)" },
{ pattern: /in_array\s*\(\s*\$[a-zA-Z_]+\s*\)(?!,)/, name: "in_array", required: "2 arguments (needle, haystack)" },
{ pattern: /explode\s*\(\s*['"][^'"]+['"]\s*\)(?!,)/, name: "explode", required: "2 arguments (delimiter, string)" },
{ pattern: /wc_get_order\s*\(\s*\)/, name: "wc_get_order", required: "Order ID argument" },
{ pattern: /wc_get_product\s*\(\s*\)/, name: "wc_get_product", required: "Product ID argument" },
];
function findPhpFiles(dir) {
const files = [];
function walk(currentDir) {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (!["vendor", "node_modules"].includes(entry.name)) {
walk(fullPath);
}
} else if (entry.name.endsWith(".php")) {
files.push(fullPath);
}
}
}
walk(dir);
return files;
}
function getLineNumber(content, index) {
return content.substring(0, index).split("\n").length;
}
function checkHposCompatibility(filePath, content) {
const issues = [];
const hasHposDeclaration = /FeaturesUtil::declare_compatibility/.test(content);
const hasCustomOrderTables = /['"]custom_order_tables['"]/.test(content);
if (!hasHposDeclaration) {
issues.push({
check: "HPOS Compatibility",
file: filePath,
issue: "HPOS compatibility not declared",
severity: "critical",
suggestion: "Use \\Automattic\\WooCommerce\\Utilities\\FeaturesUtil::declare_compatibility",
});
} else if (!hasCustomOrderTables) {
issues.push({
check: "HPOS Compatibility",
file: filePath,
issue: "HPOS declared but 'custom_order_tables' missing",
severity: "critical",
suggestion: "Add 'custom_order_tables' to compatibility declaration",
});
}
// Check for legacy meta functions on orders
const legacyMetaPattern = /(get|update|add|delete)_post_meta\s*\(\s*\$(order|wc_order|item)/g;
let match;
while ((match = legacyMetaPattern.exec(content)) !== null) {
issues.push({
check: "HPOS Violation",
file: filePath,
line: getLineNumber(content, match.index),
issue: "Legacy meta function on order variable",
severity: "critical",
suggestion: "Use $order->get_meta(), $order->update_meta_data()",
});
}
// Check for deprecated property access
const deprecatedPropertyPattern = /\$[a-zA-Z0-9_]*order->id(?!\s*\()/g;
while ((match = deprecatedPropertyPattern.exec(content)) !== null) {
issues.push({
check: "Deprecated Access",
file: filePath,
line: getLineNumber(content, match.index),
issue: "Direct access to $order->id",
severity: "critical",
suggestion: "Use $order->get_id()",
});
}
return issues;
}
function checkVersionHeaders(filePath, content) {
const issues = [];
const wcTested = /WC tested up to:\s*(.+)/.exec(content);
const wcRequired = /WC requires at least:\s*(.+)/.exec(content);
if (!wcTested) {
issues.push({
check: "Version Headers",
file: filePath,
issue: "Missing 'WC tested up to' header",
severity: "critical",
suggestion: "Add WooCommerce tested version header",
});
} else {
const version = wcTested[1].trim();
if (!/^(8|9)\./.test(version)) {
issues.push({
check: "Version Headers",
file: filePath,
issue: `WC tested version (${version}) seems old (pre-8.0)`,
severity: "warning",
suggestion: "Update to recent WooCommerce version",
});
}
}
if (!wcRequired) {
issues.push({
check: "Version Headers",
file: filePath,
issue: "Missing 'WC requires at least' header",
severity: "critical",
suggestion: "Add minimum WooCommerce version requirement",
});
}
return issues;
}
function checkDeprecatedFunctions(filePath, content) {
const issues = [];
for (const { pattern, name, replacement } of DEPRECATED_WC_PATTERNS) {
let match;
while ((match = pattern.exec(content)) !== null) {
issues.push({
check: "Deprecated WooCommerce Code",
file: filePath,
line: getLineNumber(content, match.index),
issue: `Found deprecated code '${name}'`,
severity: "critical",
suggestion: `Use ${replacement}`,
});
}
}
return issues;
}
function checkDatabaseSafety(filePath, content) {
const issues = [];
// Check for CREATE TABLE without dbDelta
if (/CREATE\s+TABLE/i.test(content) && !/dbDelta/.test(content)) {
issues.push({
check: "Database Safety",
file: filePath,
issue: "CREATE TABLE found without dbDelta",
severity: "critical",
suggestion: "Use dbDelta() for table creation",
});
}
// Check for wp_options bloat
const optionsPattern = /INSERT\s+INTO.*wp_options.*woocommerce/gi;
let match;
while ((match = optionsPattern.exec(content)) !== null) {
issues.push({
check: "Database Performance",
file: filePath,
line: getLineNumber(content, match.index),
issue: "Inserting WooCommerce data directly into wp_options",
severity: "warning",
suggestion: "Avoid cluttering autoloaded options table",
});
}
return issues;
}
function checkBlocksCompatibility(filePath, content) {
const issues = [];
const hasBlocksIntegration = /woocommerce_blocks|StoreApi|IntegrationInterface/.test(content);
if (!hasBlocksIntegration && /cart|checkout|order/i.test(filePath)) {
issues.push({
check: "Blocks Compatibility",
file: filePath,
issue: "No Cart/Checkout Blocks integration detected",
severity: "warning",
suggestion: "Plugin may break in Block-based checkouts",
});
}
return issues;
}
function checkLoggingStandards(filePath, content) {
const issues = [];
if (/error_log/.test(content)) {
issues.push({
check: "Logging Standards",
file: filePath,
issue: "error_log found",
severity: "warning",
suggestion: "Use WC_Logger instead",
});
}
if (/wp_insert_comment.*order/i.test(content)) {
issues.push({
check: "Order Notes",
file: filePath,
issue: "Inserting comments directly for orders",
severity: "critical",
suggestion: "Use $order->add_order_note()",
});
}
return issues;
}
function checkAjaxSecurity(filePath, content) {
const issues = [];
if (/add_action.*wp_ajax_/.test(content)) {
if (!/check_ajax_referer|wp_verify_nonce/.test(content)) {
issues.push({
check: "AJAX Security",
file: filePath,
issue: "AJAX action missing nonce verification",
severity: "critical",
suggestion: "Add check_ajax_referer()",
});
}
if (!/current_user_can|check_ajax_referer/.test(content)) {
issues.push({
check: "AJAX Security",
file: filePath,
issue: "AJAX action missing capability check",
severity: "critical",
suggestion: "Add current_user_can()",
});
}
}
// Check for die() in AJAX handlers
if (/wp_die\(|die\(/.test(content) && /wp_ajax_/.test(content)) {
issues.push({
check: "AJAX Error Handling",
file: filePath,
issue: "Direct wp_die() or die() in AJAX handler",
severity: "critical",
suggestion: "Use wp_send_json_error() instead",
});
}
return issues;
}
function checkDeprecatedOrderProperties(filePath, content) {
const issues = [];
const deprecatedProps = /\$order->(order_type|customer_id|customer_note|post_status)/g;
let match;
while ((match = deprecatedProps.exec(content)) !== null) {
issues.push({
check: "Deprecated Order Property",
file: filePath,
line: getLineNumber(content, match.index),
issue: `Direct property access removed: ${match[1]}`,
severity: "critical",
suggestion: "Use getter methods",
});
}
return issues;
}
function checkTooFewArguments(filePath, content) {
const issues = [];
for (const { pattern, name, required } of TOO_FEW_ARGUMENTS_PATTERNS) {
let match;
while ((match = pattern.exec(content)) !== null) {
issues.push({
check: "Too Few Arguments",
file: filePath,
line: getLineNumber(content, match.index),
issue: `${name}() called with insufficient arguments`,
severity: "critical",
suggestion: `Requires ${required}`,
});
}
}
return issues;
}
function checkPaymentGateway(filePath, content) {
const issues = [];
if (/WC_Payment_Gateway/.test(content)) {
if (!/process_payment|transaction_result/.test(content)) {
issues.push({
check: "Payment Gateway",
file: filePath,
issue: "Incomplete payment gateway implementation",
severity: "warning",
suggestion: "Missing process_payment() method",
});
}
}
return issues;
}
function checkShippingMethod(filePath, content) {
const issues = [];
if (/WC_Shipping_Method/.test(content)) {
if (!/calculate_shipping/.test(content)) {
issues.push({
check: "Shipping Method",
file: filePath,
issue: "Incomplete shipping method implementation",
severity: "warning",
suggestion: "Missing calculate_shipping() method",
});
}
}
return issues;
}
function validateWooCommerce(pluginPath) {
const result = {
status: "pass",
summary: "",
pluginPath,
checksRun: 10,
errors: [],
warnings: [],
stats: {
filesScanned: 0,
criticalCount: 0,
warningCount: 0,
},
};
if (!fs.existsSync(pluginPath)) {
result.status = "fail";
result.summary = "Plugin directory not found";
return result;
}
const phpFiles = findPhpFiles(pluginPath);
result.stats.filesScanned = phpFiles.length;
if (phpFiles.length === 0) {
result.status = "fail";
result.summary = "No PHP files found";
return result;
}
let mainFile = "";
for (const file of phpFiles) {
const content = fs.readFileSync(file, "utf-8");
if (/Plugin Name:/.test(content)) {
mainFile = file;
break;
}
}
// Run all checks
for (const file of phpFiles) {
const content = fs.readFileSync(file, "utf-8");
const relativePath = path.relative(pluginPath, file);
result.errors.push(...checkHposCompatibility(relativePath, content));
result.errors.push(...checkDeprecatedFunctions(relativePath, content));
result.errors.push(...checkDatabaseSafety(relativePath, content));
result.errors.push(...checkBlocksCompatibility(relativePath, content));
result.warnings.push(...checkLoggingStandards(relativePath, content));
result.errors.push(...checkAjaxSecurity(relativePath, content));
result.errors.push(...checkDeprecatedOrderProperties(relativePath, content));
result.errors.push(...checkTooFewArguments(relativePath, content));
result.warnings.push(...checkPaymentGateway(relativePath, content));
result.warnings.push(...checkShippingMethod(relativePath, content));
}
// Check version headers in main file
if (mainFile) {
const content = fs.readFileSync(mainFile, "utf-8");
const relativePath = path.relative(pluginPath, mainFile);
result.errors.push(...checkVersionHeaders(relativePath, content));
}
// Update stats
result.stats.criticalCount = result.errors.length;
result.stats.warningCount = result.warnings.length;
// Determine status
if (result.errors.length > 0) {
result.status = "fail";
result.summary = `${result.errors.length} critical issues, ${result.warnings.length} warnings`;
} else if (result.warnings.length > 0) {
result.status = "pass";
result.summary = `All checks passed with ${result.warnings.length} warnings`;
} else {
result.status = "pass";
result.summary = "All 10 checks passed";
}
return result;
}
// CLI entry point
if (require.main === module) {
const pluginPath = process.argv[2];
if (!pluginPath) {
console.error("Usage: node woocommerce-validate.cjs <plugin-path>");
process.exit(1);
}
const result = validateWooCommerce(pluginPath);
console.log(JSON.stringify(result, null, 2));
process.exit(result.status === "fail" ? 1 : 0);
}
module.exports = { validateWooCommerce };

View File

@@ -0,0 +1,424 @@
#!/usr/bin/env tsx
"use strict";
/**
* WordPress Plugin Validator Tool
*
* Validates WordPress plugins for security, coding standards, and best practices.
* Token-efficient: returns minimal output on success, detailed output on failure.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateWordPress = validateWordPress;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const FORBIDDEN_FUNCTIONS = [
{ pattern: /\beval\s*\(/, name: "eval", reason: "Arbitrary code execution risk" },
{ pattern: /\bexec\s*\(/, name: "exec", reason: "Command execution" },
{ pattern: /\bpassthru\s*\(/, name: "passthru", reason: "Command execution" },
{ pattern: /\bshell_exec\s*\(/, name: "shell_exec", reason: "Command execution" },
{ pattern: /\bsystem\s*\(/, name: "system", reason: "Command execution" },
{ pattern: /\bproc_open\s*\(/, name: "proc_open", reason: "Command execution" },
{ pattern: /\bpopen\s*\(/, name: "popen", reason: "Command execution" },
{ pattern: /\bcurl_exec\s*\(/, name: "curl_exec", reason: "Use wp_remote_get() instead" },
{ pattern: /\bfile_get_contents\s*\(/, name: "file_get_contents", reason: "Use wp_remote_get() for URLs" },
{ pattern: /\bbase64_decode\s*\(/, name: "base64_decode", reason: "Suspicious obfuscation risk" },
{ pattern: /\bcreate_function\s*\(/, name: "create_function", reason: "Deprecated and dangerous" },
{ pattern: /\bextract\s*\(/, name: "extract", reason: "Variable pollution risk" },
{ pattern: /\bassert\s*\(/, name: "assert", reason: "Deprecated in PHP 8" },
{ pattern: /\bpreg_replace\s*\([^)]*\/e/, name: "preg_replace /e", reason: "Use callback instead" },
];
const DEPRECATED_WP_FUNCTIONS = [
{ pattern: /\bwp_get_http\b/, name: "wp_get_http", replacement: "wp_remote_get()" },
{ pattern: /\bwp_specialchars\b/, name: "wp_specialchars", replacement: "esc_html()" },
{ pattern: /\bget_the_author_email\b/, name: "get_the_author_email", replacement: "get_the_author_meta('email')" },
{ pattern: /\bget_the_author_icq\b/, name: "get_the_author_icq", replacement: "Removed" },
{ pattern: /\bget_the_author_yim\b/, name: "get_the_author_yim", replacement: "Removed" },
{ pattern: /\bget_the_author_msn\b/, name: "get_the_author_msn", replacement: "Removed" },
{ pattern: /\bget_the_author_aim\b/, name: "get_the_author_aim", replacement: "Removed" },
{ pattern: /\bthe_editor\b/, name: "the_editor", replacement: "wp_editor()" },
];
const TOO_FEW_ARGUMENTS_PATTERNS = [
{ pattern: /array_key_exists\s*\(\s*['"][^'"]+['"]\s*\)(?!\s*,)/, name: "array_key_exists", required: "2 arguments (key, array)" },
{ pattern: /str_replace\s*\(\s*[^)]+,\s*[^)]+\s*\)(?!,)/, name: "str_replace", required: "3 arguments (search, replace, subject)" },
{ pattern: /array_slice\s*\(\s*\$[a-zA-Z_]+\s*\)(?!,)/, name: "array_slice", required: "2+ arguments (array, offset)" },
{ pattern: /in_array\s*\(\s*\$[a-zA-Z_]+\s*\)(?!,)/, name: "in_array", required: "2 arguments (needle, haystack)" },
{ pattern: /explode\s*\(\s*['"][^'"]+['"]\s*\)(?!,)/, name: "explode", required: "2 arguments (delimiter, string)" },
];
function findPhpFiles(dir) {
const files = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!["vendor", "node_modules"].includes(entry.name)) {
files.push(...findPhpFiles(fullPath));
}
}
else if (entry.name.endsWith(".php")) {
files.push(fullPath);
}
}
return files;
}
function findCssFiles(dir) {
const files = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!["vendor", "node_modules"].includes(entry.name)) {
files.push(...findCssFiles(fullPath));
}
}
else if (entry.name.endsWith(".css")) {
files.push(fullPath);
}
}
return files;
}
function getLineNumber(content, index) {
return content.substring(0, index).split("\n").length;
}
function checkForbiddenFunctions(filePath, content) {
const issues = [];
for (const { pattern, name, reason } of FORBIDDEN_FUNCTIONS) {
let match;
while ((match = pattern.exec(content)) !== null) {
issues.push({
check: "Forbidden Functions",
file: filePath,
line: getLineNumber(content, match.index),
issue: `Found forbidden function '${name}'`,
severity: "critical",
suggestion: reason,
});
}
}
return issues;
}
function checkSqlInjection(filePath, content) {
const issues = [];
const sqlPattern = /\$wpdb->(query|get_results|get_row|get_var|get_col)\s*\(\s*["'].*\$[a-zA-Z].*["']/g;
let match;
while ((match = sqlPattern.exec(content)) !== null) {
issues.push({
check: "SQL Injection",
file: filePath,
line: getLineNumber(content, match.index),
issue: "Variable directly interpolated into SQL string",
severity: "critical",
suggestion: "Use $wpdb->prepare()",
});
}
return issues;
}
function checkXssVulnerabilities(filePath, content) {
const issues = [];
// Direct echo of superglobals
const superglobalEchoPattern = /echo\s+.*(\$_GET|\$_POST|\$_REQUEST|\$_SERVER)/g;
let match;
while ((match = superglobalEchoPattern.exec(content)) !== null) {
issues.push({
check: "XSS Vulnerability",
file: filePath,
line: getLineNumber(content, match.index),
issue: "Direct echo of superglobal detected",
severity: "critical",
suggestion: "Use esc_html() or esc_attr()",
});
}
// Unsanitized input assignment
const unsanitizedPattern = /\$[a-zA-Z0-9_]+\s*=\s*(\$_POST|\$_GET|\$_REQUEST)(?!.*sanitize_|.*intval|.*esc_)/g;
while ((match = unsanitizedPattern.exec(content)) !== null) {
issues.push({
check: "Unsanitized Input",
file: filePath,
line: getLineNumber(content, match.index),
issue: "Raw input assigned without sanitization",
severity: "warning",
suggestion: "Use sanitize_text_field() or similar",
});
}
return issues;
}
function checkNoncesAndCapabilities(filePath, content) {
const issues = [];
if (content.includes("$_POST")) {
const hasNonce = /wp_verify_nonce|check_admin_referer|check_ajax_referer/.test(content);
const hasCapability = /current_user_can/.test(content);
if (!hasNonce) {
issues.push({
check: "CSRF Protection",
file: filePath,
issue: "File processes $_POST but no nonce verification found",
severity: "critical",
suggestion: "Add wp_verify_nonce() or check_admin_referer()",
});
}
if (!hasCapability) {
issues.push({
check: "Capability Check",
file: filePath,
issue: "File processes $_POST but no capability check found",
severity: "warning",
suggestion: "Add current_user_can() check",
});
}
}
return issues;
}
function checkUndefinedArrayKeys(filePath, content) {
const issues = [];
const superglobalPattern = /\$_(POST|GET|REQUEST|COOKIE)\[/g;
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
let match;
while ((match = superglobalPattern.exec(line)) !== null) {
// Check if previous line has isset or array_key_exists
if (i === 0 || (!lines[i - 1].includes("isset") && !lines[i - 1].includes("array_key_exists"))) {
issues.push({
check: "Undefined Array Keys",
file: filePath,
line: i + 1,
issue: "Superglobal array access without validation",
severity: "critical",
suggestion: "Use isset() or array_key_exists()",
});
}
}
}
return issues;
}
function checkDirectFileAccess(filePath, content) {
const issues = [];
const firstLines = content.split("\n").slice(0, 20).join("\n");
if (!/defined\s*\(\s*['"]ABSPATH['"]\s*\)|defined\s*\(\s*['"]WPINC['"]\s*\)/.test(firstLines)) {
issues.push({
check: "Direct Access Protection",
file: filePath,
issue: "Missing ABSPATH check at top of file",
severity: "warning",
suggestion: "Add 'defined(ABSPATH) || exit;'",
});
}
return issues;
}
function checkDeprecatedFunctions(filePath, content) {
const issues = [];
for (const { pattern, name, replacement } of DEPRECATED_WP_FUNCTIONS) {
let match;
while ((match = pattern.exec(content)) !== null) {
issues.push({
check: "Deprecated WordPress Function",
file: filePath,
line: getLineNumber(content, match.index),
issue: `Found deprecated function '${name}'`,
severity: "warning",
suggestion: `Use ${replacement}`,
});
}
}
return issues;
}
function checkAjaxSecurity(filePath, content) {
const issues = [];
if (/add_action.*wp_ajax_/.test(content)) {
if (!/check_ajax_referer|wp_verify_nonce/.test(content)) {
issues.push({
check: "AJAX Security",
file: filePath,
issue: "AJAX handler missing nonce verification",
severity: "critical",
suggestion: "Add check_ajax_referer()",
});
}
}
return issues;
}
function checkRestApiSecurity(filePath, content) {
const issues = [];
if (/register_rest_route/.test(content)) {
if (!/permission_callback/.test(content)) {
issues.push({
check: "REST API Security",
file: filePath,
issue: "REST route missing permission_callback",
severity: "critical",
suggestion: "Add permission_callback to register_rest_route()",
});
}
}
return issues;
}
function checkTooFewArguments(filePath, content) {
const issues = [];
for (const { pattern, name, required } of TOO_FEW_ARGUMENTS_PATTERNS) {
let match;
while ((match = pattern.exec(content)) !== null) {
issues.push({
check: "Too Few Arguments",
file: filePath,
line: getLineNumber(content, match.index),
issue: `${name}() called with insufficient arguments`,
severity: "critical",
suggestion: `Requires ${required}`,
});
}
}
return issues;
}
function checkCssOverlap(filePath, content) {
const issues = [];
// Negative margins
const negativeMarginPattern = /margin-(top|bottom|left|right)\s*:\s*-[0-9]/g;
let match;
while ((match = negativeMarginPattern.exec(content)) !== null) {
issues.push({
check: "CSS Overlap",
file: filePath,
line: getLineNumber(content, match.index),
issue: "Negative margins detected",
severity: "warning",
suggestion: "May cause element overlap",
});
}
// Absolute positioning without z-index
if (/position\s*:\s*absolute/.test(content) && !/z-index\s*:\s*[1-9]/.test(content)) {
issues.push({
check: "CSS Overlap",
file: filePath,
issue: "Absolute positioning without z-index",
severity: "warning",
suggestion: "Add z-index to prevent overlap",
});
}
return issues;
}
function checkWpLoadInclusion(filePath, content) {
const issues = [];
if (/wp-load\.php/.test(content)) {
issues.push({
check: "Architecture",
file: filePath,
issue: "Direct load of wp-load.php detected",
severity: "critical",
suggestion: "Plugins must hook into WP, not load it manually",
});
}
return issues;
}
async function validateWordPress(pluginPath) {
const result = {
status: "pass",
summary: "",
pluginPath,
checksRun: 11,
errors: [],
warnings: [],
stats: {
filesScanned: 0,
criticalCount: 0,
warningCount: 0,
},
};
if (!fs.existsSync(pluginPath)) {
result.status = "fail";
result.summary = "Plugin directory not found";
return result;
}
const phpFiles = findPhpFiles(pluginPath);
const cssFiles = findCssFiles(pluginPath);
result.stats.filesScanned = phpFiles.length + cssFiles.length;
if (phpFiles.length === 0) {
result.status = "fail";
result.summary = "No PHP files found";
return result;
}
// Run all checks
for (const file of phpFiles) {
const content = fs.readFileSync(file, "utf-8");
const relativePath = path.relative(pluginPath, file);
result.errors.push(...checkForbiddenFunctions(relativePath, content));
result.errors.push(...checkSqlInjection(relativePath, content));
result.errors.push(...checkXssVulnerabilities(relativePath, content));
result.errors.push(...checkNoncesAndCapabilities(relativePath, content));
result.errors.push(...checkUndefinedArrayKeys(relativePath, content));
result.errors.push(...checkDirectFileAccess(relativePath, content));
result.warnings.push(...checkDeprecatedFunctions(relativePath, content));
result.errors.push(...checkAjaxSecurity(relativePath, content));
result.errors.push(...checkRestApiSecurity(relativePath, content));
result.errors.push(...checkTooFewArguments(relativePath, content));
result.errors.push(...checkWpLoadInclusion(relativePath, content));
}
// CSS checks
for (const file of cssFiles) {
const content = fs.readFileSync(file, "utf-8");
const relativePath = path.relative(pluginPath, file);
result.warnings.push(...checkCssOverlap(relativePath, content));
}
// Update stats
result.stats.criticalCount = result.errors.length;
result.stats.warningCount = result.warnings.length;
// Determine status
if (result.errors.length > 0) {
result.status = "fail";
result.summary = `${result.errors.length} critical issues, ${result.warnings.length} warnings`;
}
else if (result.warnings.length > 0) {
result.status = "pass";
result.summary = `All checks passed with ${result.warnings.length} warnings`;
}
else {
result.status = "pass";
result.summary = "All 11 checks passed";
}
return result;
}
// CLI entry point
if (require.main === module) {
const pluginPath = process.argv[2];
if (!pluginPath) {
console.error("Usage: wordpress-validate <plugin-path>");
process.exit(1);
}
validateWordPress(pluginPath).then(result => {
console.log(JSON.stringify(result, null, 2));
process.exit(result.status === "fail" ? 1 : 0);
}).catch(err => {
console.error("Validation error:", err);
process.exit(1);
});
}