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