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:
462
opencode/.opencode/tool/woocommerce-validate.cjs
Normal file
462
opencode/.opencode/tool/woocommerce-validate.cjs
Normal 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 };
|
||||
Reference in New Issue
Block a user