#!/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 };