diff --git a/opencode/.opencode/agent/wordpress.md b/opencode/.opencode/agent/wordpress.md new file mode 100644 index 0000000..b74f874 --- /dev/null +++ b/opencode/.opencode/agent/wordpress.md @@ -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 + 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! diff --git a/opencode/.opencode/opencode.jsonc b/opencode/.opencode/opencode.jsonc index e2350c9..0008c82 100644 --- a/opencode/.opencode/opencode.jsonc +++ b/opencode/.opencode/opencode.jsonc @@ -12,5 +12,13 @@ "tools": { "github-triage": false, "github-pr-search": false, + "wordpress-validate": true, + "woocommerce-validate": true, }, + "agent": { + "wordpress": { + "prompt": "agent/wordpress.md", + "model": "opencode/kimi-k2.5" + } + } } diff --git a/opencode/.opencode/tool/woocommerce-validate.cjs b/opencode/.opencode/tool/woocommerce-validate.cjs new file mode 100644 index 0000000..e955c0c --- /dev/null +++ b/opencode/.opencode/tool/woocommerce-validate.cjs @@ -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 "); + process.exit(1); + } + + const result = validateWooCommerce(pluginPath); + console.log(JSON.stringify(result, null, 2)); + process.exit(result.status === "fail" ? 1 : 0); +} + +module.exports = { validateWooCommerce }; diff --git a/opencode/.opencode/tool/wordpress-validate.cjs b/opencode/.opencode/tool/wordpress-validate.cjs new file mode 100644 index 0000000..28cfae5 --- /dev/null +++ b/opencode/.opencode/tool/wordpress-validate.cjs @@ -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 "); + 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); + }); +}