#!/usr/bin/env bash set -euo pipefail # ============================================================================== # COMPREHENSIVE WORDPRESS PLUGIN VALIDATOR (STRICT/PARANOID MODE) # ============================================================================== # Validates WP plugins against strict security and coding standards. # Flags ALL potential vulnerabilities for manual review. # ============================================================================== PLUGIN_DIR="${1:-}" # Color codes RED='\033[0;31m' YELLOW='\033[1;33m' GREEN='\033[0;32m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' NC='\033[0m' ERROR_COUNT=0 WARN_COUNT=0 if [[ -z "${PLUGIN_DIR}" ]]; then echo "Usage: $(basename "$0") /path/to/plugin" >&2 exit 1 fi echo -e "${BLUE}========================================================${NC}" echo -e "${BLUE} STRICT WORDPRESS PLUGIN SECURITY & CODE AUDIT ${NC}" echo -e "${BLUE}========================================================${NC}" echo "Scanning: ${PLUGIN_DIR}" echo "" mapfile -t php_files < <(find "${PLUGIN_DIR}" -type f -name "*.php" ! -path "*/vendor/*" ! -path "*/node_modules/*" 2>/dev/null | sort || true) # Fallback to glob if mapfile fails or array is empty if [[ ${#php_files[@]} -eq 0 ]] || [[ -z "${php_files[*]:-}" ]]; then IFS=$'\n' read -d '' -r -a php_files < <(find "${PLUGIN_DIR}" -type f -name "*.php" ! -path "*/vendor/*" ! -path "*/node_modules/*" 2>/dev/null | sort || true) || true fi if [[ "${#php_files[@]}" -eq 0 ]] || [[ -z "${php_files[*]:-}" ]]; then echo -e "${RED}✗ FATAL: No PHP files found.${NC}" exit 1 fi # ============================================ # 1. FORBIDDEN FUNCTIONS (SECURITY) # ============================================ echo -e "${MAGENTA}[1/11] Checking for Dangerous/Forbidden Functions...${NC}" # List of dangerous functions that should rarely/never be used in WP plugins FORBIDDEN_FUNCS=( "eval" "exec" "passthru" "shell_exec" "system" "proc_open" "popen" "curl_exec:Use wp_remote_get()" "file_get_contents:Use wp_remote_get()" "base64_decode:Suspicious obfuscation risk" "create_function:Deprecated and dangerous" "extract:Variable pollution risk" "assert:Deprecated in PHP 8" "preg_replace with /e:Use callback instead" ) for file in "${php_files[@]}"; do for rule in "${FORBIDDEN_FUNCS[@]}"; do func="${rule%%:*}" reason="${rule##*:}" # We use a regex that looks for the function name followed by ( if grep -nE "\b${func}\s*\(" "${file}" > /dev/null; then # Ignore benign usages if needed (e.g. file_get_contents on local file) # But for strict mode, we flag all. echo -e "${RED} ✗ SECURITY RISK in ${file}:${NC}" echo " Found forbidden function: '${func}'" if [[ "$reason" != "$func" ]]; then echo " Reason: $reason"; fi grep -nE "\b${func}\s*\(" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi done done echo "" # ============================================ # 2. SQL INJECTION (STRICT) # ============================================ echo -e "${MAGENTA}[2/11] Checking for SQL Injection Patterns...${NC}" for file in "${php_files[@]}"; do # 2.1 Direct variable interpolation in query # Matches: $wpdb->query( "SELECT ... $var" ) or similar # This is a high-confidence SQLi pattern if grep -nE "\$wpdb->(query|get_results|get_row|get_var|get_col)\s*\(\s*[\"'].*\\$[a-zA-Z].*[\"']" "${file}"; then echo -e "${RED} ✗ SQL INJECTION RISK in ${file}:${NC}" echo " Variable directly interpolated into SQL string." echo " MUST use \$wpdb->prepare()." ERROR_COUNT=$((ERROR_COUNT + 1)) fi # 2.2 Missing prepare # Matches queries that look dynamic but lack 'prepare' if grep -q "\$wpdb->" "${file}"; then # If file has wpdb usage but NO prepare() call, and isn't just checking errors if ! grep -q "prepare" "${file}" && grep -qE "SELECT|INSERT|UPDATE|DELETE" "${file}"; then echo -e "${YELLOW} ⚠ WARNING: SQL usage detected without \$wpdb->prepare() in ${file}.${NC}" echo " Verify manual escaping is robust." WARN_COUNT=$((WARN_COUNT + 1)) fi fi done echo "" # ============================================ # 3. XSS & SANITIZATION (STRICT) # ============================================ echo -e "${MAGENTA}[3/11] Checking for XSS & Input Sanitization...${NC}" for file in "${php_files[@]}"; do # 3.1 Echoing superglobals directly if grep -nE "echo\s+.*(\\\$_GET|\\\$_POST|\\\$_REQUEST|\\\$_SERVER)" "${file}"; then echo -e "${RED} ✗ XSS VULNERABILITY in ${file}:${NC}" echo " Direct echo of superglobal detected. Must use esc_html / esc_attr." ERROR_COUNT=$((ERROR_COUNT + 1)) fi # 3.2 Setting variables from input without sanitization (Paranoid Check) # Looks for $var = $_POST[...] without a sanitize_ function on the same line if grep -nE "\\\$[a-zA-Z0-9_]+\s*=\s*(\\\$_POST|\\\$_GET|\\\$_REQUEST)" "${file}" | grep -vE "sanitize_|absint|intval|esc_"; then echo -e "${YELLOW} ⚠ UNSANITIZED INPUT in ${file}:${NC}" echo " Raw input assigned to variable without immediate sanitization." grep -nE "\\\$[a-zA-Z0-9_]+\s*=\s*(\\\$_POST|\\\$_GET|\\\$_REQUEST)" "${file}" | head -1 WARN_COUNT=$((WARN_COUNT + 1)) fi # 3.3 Print/Echo without escaping functions (General Heuristic) # Flags lines like `echo $variable;` if $variable isn't obviously an object method # This produces noise, but ensures strictness. if grep -nE "(echo|print)\s+\\\$[a-zA-Z0-9_]+;" "${file}"; then # Simple variable echo - likely unsafe unless variable source is known trusted echo -e "${YELLOW} ⚠ UNESCAPED OUTPUT in ${file}:${NC}" echo " Printing variable without escaping function (esc_html, etc)." WARN_COUNT=$((WARN_COUNT + 1)) fi done echo "" # ============================================ # 4. NONCES & CAPABILITIES (CSRF/Auth) # ============================================ echo -e "${MAGENTA}[4/11] Checking Nonces and Permissions...${NC}" for file in "${php_files[@]}"; do # 4.1 Processing Form Data if grep -q "\$_POST" "${file}"; then # If file processes POST, it MUST have nonce verification if ! grep -qE "wp_verify_nonce|check_admin_referer|check_ajax_referer" "${file}"; then echo -e "${RED} ✗ CSRF VULNERABILITY in ${file}:${NC}" echo " File processes \$_POST but no nonce check found." ERROR_COUNT=$((ERROR_COUNT + 1)) fi # It SHOULD also have capability checks if ! grep -qE "current_user_can" "${file}"; then echo -e "${YELLOW} ⚠ MISSING CAPABILITY CHECK in ${file}:${NC}" echo " File processes \$_POST but 'current_user_can' not found." WARN_COUNT=$((WARN_COUNT + 1)) fi fi done echo "" # ============================================ # 5. PHP CODING STANDARDS (LINTING) # ============================================ echo -e "${MAGENTA}[5/11] PHP Syntax & Standards...${NC}" # 5.4 UNDEFINED ARRAY KEY DETECTION echo -e "${MAGENTA}[5.4/11] Checking for Undefined Array Keys...${NC}" for file in "${php_files[@]}"; do # 5.4.1 Direct array access without isset check on superglobals # Matches patterns like: $_POST['key'] or $_GET['key'] without preceding isset() UNSAFE_KEYS=$(grep -nE '\$_POST\[|\$_GET\[|\$_REQUEST\[|\$_COOKIE\[' "${file}" | grep -vB1 "isset\|array_key_exists" | grep -E '\$_POST\[|\$_GET\[|\$_REQUEST\[|\$_COOKIE\[' || true) if [ -n "$UNSAFE_KEYS" ]; then echo -e "${RED} ✗ UNDEFINED ARRAY KEY RISK in ${file}:${NC}" echo " Superglobal array access without isset()/array_key_exists() check." echo " Lines with potential undefined key access:" echo "$UNSAFE_KEYS" | head -3 ERROR_COUNT=$((ERROR_COUNT + 1)) fi # 5.4.2 Array key access in expressions without isset check # Matches: $array['key'] or $array[$var] where key might not exist if grep -nE '\$[a-zA-Z_][a-zA-Z0-9_]*\[' "${file}" | grep -vB1 "isset\|array_key_exists\|foreach.*as.*=>" | grep -E '\$[a-zA-Z_][a-zA-Z0-9_]*\[' > /dev/null 2>&1; then echo -e "${YELLOW} ⚠ POTENTIAL UNDEFINED ARRAY KEY in ${file}:${NC}" echo " Array key access without isset() or array_key_exists() check." echo " Consider using isset() or the null coalescing operator (??)" grep -nE '\$[a-zA-Z_][a-zA-Z0-9_]*\[' "${file}" | grep -vB1 "isset\|array_key_exists\|foreach.*as.*=>" | grep -E '\$[a-zA-Z_][a-zA-Z0-9_]*\[' | head -3 WARN_COUNT=$((WARN_COUNT + 1)) fi # 5.4.3 Multi-dimensional array access without validation # Matches: $array['key1']['key2'] which is risky without validation if grep -nE '\$[a-zA-Z_][a-zA-Z0-9_]*\[.*\]\[' "${file}" | grep -vB1 "isset\|array_key_exists" | head -1 > /dev/null 2>&1; then echo -e "${YELLOW} ⚠ MULTI-DIMENSIONAL ARRAY ACCESS in ${file}:${NC}" echo " Multi-dimensional array access without validation." grep -nE '\$[a-zA-Z_][a-zA-Z0-9_]*\[.*\]\[' "${file}" | grep -vB1 "isset\|array_key_exists" | head -1 WARN_COUNT=$((WARN_COUNT + 1)) fi # 5.4.4 Array access in assignment without check # Matches: $var = $_POST['key'] without isset if grep -nE '\$[a-zA-Z_]+\s*=\s*\$_(POST\[|GET\[|REQUEST\[|COOKIE\[)' "${file}" | grep -vB1 "isset\|array_key_exists" | head -1 > /dev/null 2>&1; then echo -e "${RED} ✗ UNSAFE ARRAY KEY ASSIGNMENT in ${file}:${NC}" echo " Assignment from array key without isset() check." grep -nE '\$[a-zA-Z_]+\s*=\s*\$_(POST\[|GET\[|REQUEST\[|COOKIE\[)' "${file}" | grep -vB1 "isset\|array_key_exists" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi done echo "" # 5.1 Syntax if command -v php >/dev/null 2>&1; then for file in "${php_files[@]}"; do if ! php -l "${file}" > /dev/null 2>&1; then echo -e "${RED} ✗ PHP SYNTAX ERROR in ${file}${NC}" ERROR_COUNT=$((ERROR_COUNT + 1)) fi done # 5.5 Duplicate Class Declaration Check (Runtime Error Detection) echo -e "${MAGENTA}[5.5/11] Checking for Duplicate Class/Function Declarations...${NC}" php "${BASH_SOURCE[0]%/*}/check-duplicate-classes.php" "${PLUGIN_DIR}" 2>&1 | while IFS= read -r line; do if [[ "$line" == *DUPLICATE* ]]; then echo -e "${RED} ✗ RUNTIME ERROR: ${line}${NC}" ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *MISSING* ]]; then echo -e "${RED} ✗ RUNTIME ERROR: ${line}${NC}" ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *WARNING* ]]; then echo -e "${YELLOW} ⚠ ${line}${NC}" WARN_COUNT=$((WARN_COUNT + 1)) elif [[ "$line" != *"No duplicate"* ]] && [[ "$line" != *"No runtime"* ]] && [[ "$line" != *"Scanned"* ]]; then echo " ${line}" fi done fi # 5.6 Direct file access check # Every PHP file should start with strict denial of direct access for file in "${php_files[@]}"; do # Skip templates usually, but plugin core files must have it first_lines=$(head -n 20 "${file}") if ! echo "$first_lines" | grep -qE "defined\s*\(\s*['\"]ABSPATH['\"]\s*\)|defined\s*\(\s*['\"]WPINC['\"]\s*\)"; then echo -e "${YELLOW} ⚠ DIRECT ACCESS PROTECTION MISSING in ${file}${NC}" echo " Add 'defined(ABSPATH) || exit;' at top of file." WARN_COUNT=$((WARN_COUNT + 1)) fi done echo "" # ============================================ # 6. ACTIVATION HOOK VALIDATION # ============================================ echo -e "${MAGENTA}[6/11] Checking Activation Hook Setup...${NC}" for file in "${php_files[@]}"; do # Check for register_activation_hook usage if grep -q "register_activation_hook" "${file}"; then # Extract the callback being used callbacks=$(grep -oE "register_activation_hook\s*\([^,]+,\s*[^)]+\)" "${file}" || true) while IFS= read -r callback; do # Check if callback references a class method like array($obj, 'method') or [new Class(), 'method'] if echo "$callback" | grep -qE "\[\s*(\$[a-zA-Z_]+|new\s+[A-Z][a-zA-Z0-9_]+\(\))\s*,\s*['\"]install['\"]"; then # Check if the class is instantiated after the register_activation_hook call if grep -A5 "register_activation_hook" "${file}" | grep -q "install"; then # Look for require/include statements that load the class if ! grep -B10 "register_activation_hook" "${file}" | grep -qE "(require|include|autoload)"; then echo -e "${RED} ✗ ACTIVATION HOOK - INSTALL CLASS NOT LOADED in ${file}:${NC}" echo " register_activation_hook references install class but class may not be loaded." grep -n "register_activation_hook" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi fi # Check for callback using class array syntax: ['ClassName', 'method'] if echo "$callback" | grep -qE "\[\s*['\"][A-Z][a-zA-Z0-9_]*['\"]\s*,\s*['\"]install['\"]"; then class_name=$(echo "$callback" | grep -oE "['\"][A-Z][a-zA-Z0-9_]*['\"]\s*," | sed "s/['\"]//g" | sed "s/[, ]//g" | head -1) # Check if this class file is loaded before the hook registration if ! grep -B20 "register_activation_hook" "${file}" | grep -qi "class ${class_name}"; then if ! grep -B20 "register_activation_hook" "${file}" | grep -qiE "require.*${class_name}|include.*${class_name}|autoload.*${class_name}"; then echo -e "${RED} ✗ ACTIVATION HOOK - CLASS NOT LOADED in ${file}:${NC}" echo " register_activation_hook references class '${class_name}' but class file may not be loaded." grep -n "register_activation_hook" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi fi # Check for callback like ClassName::install_method if echo "$callback" | grep -qE "[A-Z][a-zA-Z0-9_]*::install"; then class_name=$(echo "$callback" | grep -oE "[A-Z][a-zA-Z0-9_]*::install" | sed 's/::install//' | head -1) if ! grep -B20 "register_activation_hook" "${file}" | grep -qiE "class ${class_name}|require.*${class_name}|include.*${class_name}"; then echo -e "${RED} ✗ ACTIVATION HOOK - STATIC CLASS NOT LOADED in ${file}:${NC}" echo " register_activation_hook references static class '${class_name}' but class may not be loaded." grep -n "register_activation_hook" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi done <<< "$callbacks" fi # Check for activation hook in a separate file that references external classes if grep -q "register_activation_hook" "${file}" && grep -q "use.*;" "${file}"; then use_classes=$(grep "^use " "${file}" | grep -oE "use [A-Z][a-zA-Z0-9_\\]+;" | sed 's/use //;s/;//' || true) if [ -n "$use_classes" ]; then echo -e "${YELLOW} ⚠ ACTIVATION HOOK - EXTERNAL CLASS USAGE in ${file}:${NC}" echo " Activation hook uses external classes via 'use' statements. Verify they are autoloaded." grep -n "register_activation_hook" "${file}" | head -1 WARN_COUNT=$((WARN_COUNT + 1)) fi fi # Check if activation hook is inside a class (common anti-pattern) if grep -q "register_activation_hook" "${file}" && grep -qE "^\s*(public|protected|private)\s+static\s+function\s+.*install" "${file}"; then echo -e "${YELLOW} ⚠ ACTIVATION HOOK - INSIDE CLASS in ${file}:${NC}" echo " Activation hook inside class. Consider using separate activation file." grep -n "register_activation_hook" "${file}" | head -1 WARN_COUNT=$((WARN_COUNT + 1)) fi done echo "" # ============================================ # 7. CLASS LOADING VALIDATION (INSTALL/ADMIN/FRONTEND) # ============================================ echo -e "${MAGENTA}[7/11] Comprehensive Class Loading Validation...${NC}" # Define common WordPress hook patterns for different component types ADMIN_HOOKS_PATTERN="admin_menu|admin_init|admin_head|admin_footer|admin_enqueue_scripts|wp_ajax_|load-(tools|plugins|themes)-page" FRONTEND_HOOKS_PATTERN="wp_enqueue_scripts|wp_head|wp_footer|template_redirect|wp_ajax_nopriv_" INSTALL_HOOKS_PATTERN="register_activation_hook|register_deactivation_hook|register_uninstall_hook" # Find all PHP files and analyze class loading patterns for file in "${php_files[@]}"; do # Check for admin hooks without admin class loading if grep -qE "add_action\s*\(\s*['\"](${ADMIN_HOOKS_PATTERN})" "${file}"; then # Look for admin class references if grep -qE "(Admin|admin|Admin_[A-Z])" "${file}"; then # Check if admin class is loaded before hooks if ! grep -B10 "add_action.*(${ADMIN_HOOKS_PATTERN})" "${file}" | grep -qE "(require|include|autoload).*(Admin|admin|Admin_[A-Z])"; then # Also check for class instantiation before hooks if ! grep -B10 "add_action.*(${ADMIN_HOOKS_PATTERN})" "${file}" | grep -qE "(new\s+.*Admin|new\s+.*admin|\$admin\s*=)"; then echo -e "${RED} ✗ ADMIN CLASS NOT LOADED in ${file}:${NC}" echo " Admin hooks found but admin class may not be loaded before hook registration." grep -nE "add_action\s*\(\s*['\"](${ADMIN_HOOKS_PATTERN})" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi fi fi # Check for frontend hooks without frontend class loading if grep -qE "add_action\s*\(\s*['\"](${FRONTEND_HOOKS_PATTERN})" "${file}"; then # Look for frontend class references if grep -qE "(Frontend|frontend|Frontend_[A-Z])" "${file}"; then # Check if frontend class is loaded before hooks if ! grep -B10 "add_action.*(${FRONTEND_HOOKS_PATTERN})" "${file}" | grep -qE "(require|include|autoload).*(Frontend|frontend|Frontend_[A-Z])"; then # Also check for class instantiation before hooks if ! grep -B10 "add_action.*(${FRONTEND_HOOKS_PATTERN})" "${file}" | grep -qE "(new\s+.*Frontend|new\s+.*frontend|\$frontend\s*=)"; then echo -e "${RED} ✗ FRONTEND CLASS NOT LOADED in ${file}:${NC}" echo " Frontend hooks found but frontend class may not be loaded before hook registration." grep -nE "add_action\s*\(\s*['\"](${FRONTEND_HOOKS_PATTERN})" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi fi fi # Check for install/uninstall hooks without proper class loading if grep -qE "add_action\s*\(\s*['\"](activate_|deactivate_|uninstall_)" "${file}"; then # Look for install/uninstall class references if grep -qE "(Install|install|Install_[A-Z]|Uninstall|uninstall|Uninstall_[A-Z])" "${file}"; then # Check if install/uninstall class is loaded before hooks if ! grep -B10 "add_action.*(activate_|deactivate_|uninstall_)" "${file}" | grep -qE "(require|include|autoload).*(Install|install|Install_[A-Z]|Uninstall|uninstall|Uninstall_[A-Z])"; then echo -e "${RED} ✗ INSTALL/UNINSTALL CLASS NOT LOADED in ${file}:${NC}" echo " Activation/deactivation/uninstall hooks found but install/uninstall class may not be loaded." grep -nE "add_action\s*\(\s*['\"](activate_|deactivate_|uninstall_)" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi fi # Check for class instantiations that are used but may not be defined if grep -qE "(new\s+(Admin|Frontend|Install|Uninstall)[A-Za-z0-9_]*\s*\()" "${file}"; then class_name=$(grep -oE "(new\s+(Admin|Frontend|Install|Uninstall)[A-Za-z0-9_]*\s*\()" "${file}" | grep -oE "(Admin|Frontend|Install|Uninstall)[A-Za-z0-9_]*" | head -1) # Check if class file exists or is included class_file=$(find "${PLUGIN_DIR}" -name "class-${class_name,,}.php" -o -name "*${class_name}*.php" 2>/dev/null | head -1) if [[ -z "$class_file" ]]; then # Check if class is loaded via require/include if ! grep -qE "(require|include).*${class_name}" "${file}"; then echo -e "${RED} ✗ CLASS DEFINITION MISSING in ${file}:${NC}" echo " Class '${class_name}' instantiated but class file may not be included." grep -nE "(new\s+(Admin|Frontend|Install|Uninstall)[A-Za-z0-9_]*\s*\()" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi fi # Check for static method calls on classes that may not be loaded if grep -qE "(Admin|Frontend|Install|Uninstall)::[a-zA-Z_]+\s*\(" "${file}"; then class_name=$(echo "${file}" | grep -oE "(Admin|Frontend|Install|Uninstall)::" | sed 's/:://' | head -1) # Check if class is loaded or defined if ! grep -qE "(class\s+${class_name}|require.*${class_name}|include.*${class_name})" "${file}"; then echo -e "${YELLOW} ⚠ STATIC CLASS CALL WITHOUT LOAD in ${file}:${NC}" echo " Static method call on '${class_name}' but class may not be loaded." grep -nE "(Admin|Frontend|Install|Uninstall)::[a-zA-Z_]+\s*\(" "${file}" | head -1 WARN_COUNT=$((WARN_COUNT + 1)) fi fi # Check for proper dependency ordering in include statements # Look for common patterns where install/admin/frontend classes might be missing if grep -qE "(admin|frontend|install)" "${file}" | grep -qE "(add_action|new\s+|::)"; then # Extract potential class names from includes includes=$(grep -oE "(require|include).*(admin|frontend|install)" "${file}" | grep -oE "[a-zA-Z0-9_-]+\.(php|inc)" || true) # Check if required files exist for include_file in $includes; do if [[ -n "$include_file" ]]; then include_path=$(find "${PLUGIN_DIR}" -name "${include_file}" -type f 2>/dev/null | head -1) if [[ -z "$include_path" ]]; then echo -e "${RED} ✗ MISSING INCLUDE FILE in ${file}:${NC}" echo " Required file '${include_file}' not found." echo " Referenced in: ${file}" ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi done fi # Check for class method calls that might fail due to missing dependencies if grep -qE "->(install|activate|deactivate|uninstall|init|run|load)" "${file}"; then # Look for potential missing method definitions method_call=$(grep -oE "->(install|activate|deactivate|uninstall|init|run|load)\s*\(" "${file}" | head -1) if [[ -n "$method_call" ]]; then echo -e "${YELLOW} ⚠ POTENTIAL MISSING METHOD in ${file}:${NC}" echo " Method call ${method_call} found - ensure corresponding class method is defined." WARN_COUNT=$((WARN_COUNT + 1)) fi fi done # Additional comprehensive check: Scan for common class loading patterns echo -e "${MAGENTA}[7.1/11] Scanning for Class Loading Patterns...${NC}" # Define class name patterns to look for CLASS_PATTERNS=("Admin" "Frontend" "Install" "Uninstall" "Loader" "Controller" "Handler") for file in "${php_files[@]}"; do for class_pattern in "${CLASS_PATTERNS[@]}"; do # Check if class is referenced but not defined in file if grep -q "${class_pattern}" "${file}"; then # Check if class is actually defined in this file if ! grep -qE "class\s+${class_pattern}" "${file}"; then # Check if class is properly loaded via require/include if ! grep -qE "(require|include|require_once|include_once).*${class_pattern}" "${file}"; then # Count how many times it's referenced ref_count=$(grep -c "${class_pattern}" "${file}" || true) if [[ "$ref_count" -gt 2 ]]; then echo -e "${YELLOW} ⚠ CLASS REFERENCE WITHOUT LOAD in ${file}:${NC}" echo " Class '${class_pattern}' referenced ${ref_count} times but may not be loaded." WARN_COUNT=$((WARN_COUNT + 1)) fi fi fi fi done done echo "" # 5.7 RUNTIME ERROR DETECTION (Enhanced) # ============================================ echo -e "${MAGENTA}[5.7/11] Runtime Error Detection...${NC}" if command -v php >/dev/null 2>&1; then SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ANALYZER_OUTPUT=$(php "${SCRIPT_DIR}/check-duplicate-classes.php" "${PLUGIN_DIR}" 2>&1 || true) # Parse and report issues if echo "$ANALYZER_OUTPUT" | grep -q "UNDEFINED FUNCTION"; then echo -e "${RED} ✗ UNDEFINED FUNCTIONS DETECTED${NC}" echo "$ANALYZER_OUTPUT" | grep "UNDEFINED FUNCTION" | head -5 ERROR_COUNT=$((ERROR_COUNT + 1)) fi if echo "$ANALYZER_OUTPUT" | grep -q "EARLY FUNCTION CALL"; then echo -e "${RED} ✗ EARLY FUNCTION CALLS DETECTED${NC}" echo "$ANALYZER_OUTPUT" | grep "EARLY FUNCTION CALL" | head -5 ERROR_COUNT=$((ERROR_COUNT + 1)) fi if echo "$ANALYZER_OUTPUT" | grep -q "POTENTIAL UNDEFINED ARRAY"; then echo -e "${YELLOW} ⚠ POTENTIAL UNDEFINED ARRAYS${NC}" echo "$ANALYZER_OUTPUT" | grep "POTENTIAL UNDEFINED ARRAY" | head -3 WARN_COUNT=$((WARN_COUNT + 1)) fi if echo "$ANALYZER_OUTPUT" | grep -q "POTENTIAL CSS OVERLAP"; then echo -e "${YELLOW} ⚠ POTENTIAL CSS OVERLAPS${NC}" echo "$ANALYZER_OUTPUT" | grep "POTENTIAL CSS OVERLAP" | head -3 WARN_COUNT=$((WARN_COUNT + 1)) fi if ! echo "$ANALYZER_OUTPUT" | grep -qE "(UNDEFINED|EARLY|OVERLAP|FAIL)"; then echo -e "${GREEN} ✓ No runtime issues detected${NC}" fi fi echo "" # ============================================ # 8. FILE PERMISSIONS # ============================================ echo -e "${MAGENTA}[8/11] Checking File Permissions...${NC}" # Find any file that is world-writable or executable (excluding scripts) find "${PLUGIN_DIR}" -type f -perm -002 -print0 | while IFS= read -r -d '' file; do echo -e "${RED} ✗ INSECURE PERMISSION (World Writable): ${file}${NC}" ERROR_COUNT=$((ERROR_COUNT + 1)) done find "${PLUGIN_DIR}" -type f -name "*.php" -perm -100 -print0 | while IFS= read -r -d '' file; do echo -e "${YELLOW} ⚠ EXECUTABLE PHP FILE: ${file}${NC}" echo " PHP files should generally not have +x permission." WARN_COUNT=$((WARN_COUNT + 1)) done echo "" # ============================================ # 9. FILE PATH VALIDATION # ============================================ echo -e "${MAGENTA}[9/11] Checking File Path Construction...${NC}" for file in "${php_files[@]}"; do # 6.1 Hardcoded absolute paths (excluding WordPress constants) if grep -nE "(include|require|fopen|file_get_contents|file_put_contents|is_file|is_dir|file_exists)\s*\(\s*['\"](/|[A-Za-z]:)" "${file}" | grep -vE "(ABSPATH|WPINC|WP_CONTENT_DIR|WP_PLUGIN_DIR|WPMU_PLUGIN_DIR|WP_CONTENT_URL|WP_PLUGIN_URL|WPMU_PLUGIN_URL)"; then echo -e "${RED} ✗ HARDCODED ABSOLUTE PATH in ${file}:${NC}" echo " Hardcoded absolute path detected. Use WordPress path functions." grep -nE "(include|require|fopen|file_get_contents|file_put_contents|is_file|is_dir|file_exists)\s*\(\s*['\"](/|[A-Za-z]:)" "${file}" | grep -vE "(ABSPATH|WPINC|WP_CONTENT_DIR|WP_PLUGIN_DIR|WPMU_PLUGIN_DIR|WP_CONTENT_URL|WP_PLUGIN_URL|WPMU_PLUGIN_URL)" | head -2 ERROR_COUNT=$((ERROR_COUNT + 1)) fi # 6.2 Missing directory separators in path concatenation if grep -nE '\$[a-zA-Z_]+\s*\.\s*["'"'"']' "${file}" | grep -vE "DIRECTORY_SEPARATOR|DS|plugin_dir_path|template_directory|get_template_directory"; then echo -e "${YELLOW} ⚠ POTENTIAL PATH CONCATENATION ERROR in ${file}:${NC}" echo " Path concatenation without proper directory separator." grep -nE '\$[a-zA-Z_]+\s*\.\s*["'"'"']' "${file}" | grep -vE "DIRECTORY_SEPARATOR|DS|plugin_dir_path|template_directory|get_template_directory" | head -1 WARN_COUNT=$((WARN_COUNT + 1)) fi # 6.3 Direct usage of __FILE__ without dirname() for directory paths if grep -nE "(plugin_dir_path|plugin_dir_url|template_directory|get_template_directory)" "${file}" | grep -q "__FILE__" && ! grep -q "__FILE__.*dirname" "${file}"; then echo -e "${YELLOW} ⚠ INCORRECT __FILE__ USAGE in ${file}:${NC}" echo " __FILE__ returns the file path. Use dirname(__FILE__) for directory paths." grep -n "__FILE__" "${file}" | head -1 WARN_COUNT=$((WARN_COUNT + 1)) fi # 6.4 User input in file operations (path traversal risk) if grep -nE "(include|require|fopen|file_get_contents|file_put_contents|unlink|mkdir|rmdir)\s*\(\s*(\\\$_GET|\\\$_POST|\\\$_REQUEST|\\\$_COOKIE)" "${file}"; then echo -e "${RED} ✗ PATH TRAVERSAL RISK in ${file}:${NC}" echo " Direct user input used in file operation. Must validate and sanitize." grep -nE "(include|require|fopen|file_get_contents|file_put_contents|unlink|mkdir|rmdir)\s*\(\s*(\\\$_GET|\\\$_POST|\\\$_REQUEST|\\\$_COOKIE)" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi # 6.5 Not using WordPress path functions for plugin paths if grep -nE "(include|require)\s*\(\s*['\"][^'\"]*\.(php|inc)" "${file}" | grep -vE "plugin_dir_path|plugin_dir_url|locate_template|get_template_directory|get_stylesheet_directory"; then if ! grep -qE "defined\s*\(\s*['\"]ABSPATH['\"]" "${file}" | head -1; then echo -e "${YELLOW} ⚠ MISSING WORDPRESS PATH FUNCTION in ${file}:${NC}" echo " Use plugin_dir_path(), template_directory(), or similar WP functions." grep -nE "(include|require)\s*\(\s*['\"][^'\"]*\.(php|inc)" "${file}" | grep -vE "plugin_dir_path|plugin_dir_url|locate_template|get_template_directory|get_stylesheet_directory" | head -1 WARN_COUNT=$((WARN_COUNT + 1)) fi fi # 6.6 Windows backslashes in paths (should use forward slashes or DIRECTORY_SEPARATOR) if grep -nE "[A-Za-z]:\\\\" "${file}" | grep -vE "(DIRECTORY_SEPARATOR|DS|dirname|realpath)"; then echo -e "${YELLOW} ⚠ WINDOWS BACKSLASH DETECTED in ${file}:${NC}" echo " Use forward slashes or DIRECTORY_SEPARATOR constant for cross-platform compatibility." grep -nE "[A-Za-z]:\\\\" "${file}" | head -1 WARN_COUNT=$((WARN_COUNT + 1)) fi done echo "" # ============================================ # 10. DEPENDENCIES & ENQUEUES # ============================================ echo -e "${MAGENTA}[10/11] Checking Dependencies & Enqueues...${NC}" for file in "${php_files[@]}"; do # Check for direct wp-load.php inclusion (Major sin) if grep -q "wp-load.php" "${file}"; then echo -e "${RED} ✗ ARCHITECTURE FAIL: Direct load of wp-load.php in ${file}${NC}" echo " Plugins must hook into WP, not load it manually." ERROR_COUNT=$((ERROR_COUNT + 1)) fi # Check for enqueuing scripts outside hooks # We look for wp_enqueue_script NOT inside a function definition # (This is a rough heuristic) if grep -E "^wp_enqueue_script" "${file}"; then echo -e "${YELLOW} ⚠ DOING IT WRONG: wp_enqueue_script called at root level in ${file}${NC}" echo " Must be inside 'wp_enqueue_scripts' or 'admin_enqueue_scripts' hook." WARN_COUNT=$((WARN_COUNT + 1)) fi # Check for direct admin notice output if grep -q "add_action.*admin_notices" "${file}" && ! grep -q "is_admin()" "${file}"; then echo -e "${YELLOW} ⚠ Missing is_admin() check in ${file}${NC}" echo " admin_notices hook should verify is_admin()" WARN_COUNT=$((WARN_COUNT + 1)) fi done echo "" # ============================================ # 11. WORDPRESS COMPATIBILITY & DEPRECATED FUNCTIONS # ============================================ echo -e "${MAGENTA}[11/11] Checking WordPress Compatibility & Deprecated Code...${NC}" DEPRECATED_WP_PATTERNS=( "wp_get_http:Use wp_remote_get()" "make_url_footnote:Deprecated" "wp_specialchars:Use esc_html()" "translate_with_context:Deprecated" "get_the_author_email:Use get_the_author_meta('email')" "get_the_author_icq:Removed" "get_the_author_yim:Removed" "get_the_author_msn:Removed" "get_the_author_aim:Removed" "wp_dashboard_plugins:Removed" "the_editor:Use wp_editor()" ) for file in "${php_files[@]}"; do for pattern_set in "${DEPRECATED_WP_PATTERNS[@]}"; do pattern="${pattern_set%%:*}" advice="${pattern_set##*:}" if grep -qE "\b${pattern}\b" "${file}"; then echo -e "${RED} ✗ DEPRECATED WORDPRESS FUNCTION in ${file}:${NC}" echo " Found: '${pattern}'" echo " Fix: $advice" grep -nE "\b${pattern}\b" "${file}" | head -1 ERROR_COUNT=$((ERROR_COUNT + 1)) fi done # Check for proper text domain usage if grep -qE "__|_e|_x|_n|_ex|_nx|esc_html__|esc_html_e|esc_html_x|esc_attr__|esc_attr_e|esc_attr_x" "${file}"; then # Check if text domain matches plugin slug pattern text_domain=$(grep -oE "(__|_e|_x)\(['\"][^'\"]*['\"],\s*['\"][^'\"]*['\"]" "${file}" | head -1 | grep -oE "[^',\"]*$" | tr -d ')' | sed 's/^\s*//' | sed 's/\s*$//') if [[ -n "$text_domain" ]] && [[ ! "$text_domain" =~ ^pc-[a-z0-9-]+$ ]]; then echo -e "${YELLOW} ⚠ Text domain may not follow naming convention in ${file}${NC}" echo " Expected: pc-{{slug}}-{{id}}" WARN_COUNT=$((WARN_COUNT + 1)) fi fi # Check for hardcoded URLs if grep -qE "http://example\.com|http://your-site\.com|http://localhost" "${file}"; then echo -e "${YELLOW} ⚠ Hardcoded URLs found in ${file}${NC}" echo " Replace with site_url() or home_url()" WARN_COUNT=$((WARN_COUNT + 1)) fi # Check for AJAX implementation if grep -qE "add_action.*wp_ajax_" "${file}"; then if ! grep -qE "check_ajax_referer|wp_verify_nonce" "${file}"; then echo -e "${RED} ✗ AJAX SECURITY RISK in ${file}${NC}" echo " AJAX handler missing nonce verification" ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi # Check for proper REST API permissions if grep -qE "register_rest_route" "${file}"; then if ! grep -qE "permission_callback" "${file}"; then echo -e "${RED} ✗ REST API SECURITY RISK in ${file}${NC}" echo " REST route missing permission_callback" ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi done echo "" # ============================================ # 10. CSS & UI OVERLAP DETECTION # ============================================ echo -e "${MAGENTA}[11/11] Checking CSS & UI for Overlap Issues...${NC}" mapfile -t css_files < <(find "${PLUGIN_DIR}" -type f -name "*.css" ! -path "*/vendor/*" ! -path "*/node_modules/*" 2>/dev/null || true) if [[ "${#css_files[@]}" -gt 0 ]]; then CSS_ISSUE_COUNT=0 for file in "${css_files[@]}"; do # Check for negative margins (common overlap cause) if grep -nE "margin-(top|bottom|left|right)\s*:\s*-[0-9]" "${file}" >/dev/null 2>&1; then if [[ $CSS_ISSUE_COUNT -lt 5 ]]; then echo -e "${YELLOW} ⚠ CSS ISSUE in ${file}:${NC}" echo " Negative margins detected (may cause overlap)" grep -nE "margin-(top|bottom|left|right)\s*:\s*-[0-9]" "${file}" | head -1 fi CSS_ISSUE_COUNT=$((CSS_ISSUE_COUNT + 1)) WARN_COUNT=$((WARN_COUNT + 1)) fi # Check for absolute positioning without z-index if grep -E "position\s*:\s*absolute" "${file}" >/dev/null 2>&1; then if ! grep -E "z-index\s*:\s*[1-9]" "${file}" >/dev/null 2>&1; then if [[ $CSS_ISSUE_COUNT -lt 5 ]]; then echo -e "${YELLOW} ⚠ CSS ISSUE in ${file}:${NC}" echo " Absolute positioning without z-index (may cause overlap)" grep -nE "position\s*:\s*absolute" "${file}" | head -1 fi CSS_ISSUE_COUNT=$((CSS_ISSUE_COUNT + 1)) WARN_COUNT=$((WARN_COUNT + 1)) fi fi # Check for fixed positioning at z-index 0 if grep -E "position\s*:\s*fixed" "${file}" >/dev/null 2>&1; then if grep -E "position\s*:\s*fixed" "${file}" | grep -q "z-index\s*:\s*0"; then if [[ $CSS_ISSUE_COUNT -lt 5 ]]; then echo -e "${YELLOW} ⚠ CSS ISSUE in ${file}:${NC}" echo " Fixed positioning at z-index 0 (may overlap other elements)" grep -nE "position\s*:\s*fixed.*z-index\s*:\s*0" "${file}" | head -1 fi CSS_ISSUE_COUNT=$((CSS_ISSUE_COUNT + 1)) WARN_COUNT=$((WARN_COUNT + 1)) fi fi done if [[ $CSS_ISSUE_COUNT -eq 0 ]]; then echo -e "${GREEN} ✓ No CSS overlap issues detected${NC}" else echo -e "${YELLOW} (Additional CSS issues may exist - showing first 5)${NC}" fi else echo -e "${GREEN} ✓ No CSS files found${NC}" fi echo "" # ============================================ # 12. CLASS LOADING ERRORS & MISSING CLASSES # ============================================ echo -e "${MAGENTA}[12/12] Detecting Class Loading Errors and Missing Classes...${NC}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CLASS_ERRORS=0 CLASS_WARNINGS=0 if command -v php >/dev/null 2>&1; then php "${SCRIPT_DIR}/check-duplicate-classes.php" "${PLUGIN_DIR}" 2>&1 | while IFS= read -r line; do if [[ "$line" == *"MISSING CLASS"* ]]; then echo -e "${RED} ✗ CLASS LOADING ERROR: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"MISSING CLASS/TYPE"* ]]; then echo -e "${RED} ✗ CLASS LOADING ERROR: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"DUPLICATE CLASS"* ]]; then echo -e "${RED} ✗ CLASS LOADING ERROR: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"DUPLICATE INTERFACE"* ]]; then echo -e "${RED} ✗ CLASS LOADING ERROR: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"DUPLICATE TRAIT"* ]]; then echo -e "${RED} ✗ CLASS LOADING ERROR: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"install_class_not_loaded"* ]] || [[ "$line" == *"class_not_loaded"* ]]; then echo -e "${RED} ✗ CLASS LOADING ERROR: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"MISSING INCLUDE"* ]]; then echo -e "${RED} ✗ MISSING INCLUDE: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"ACTIVATION HOOK ISSUE"* ]]; then echo -e "${RED} ✗ ACTIVATION HOOK CLASS ERROR: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"UNDEFINED FUNCTION"* ]]; then echo -e "${RED} ✗ UNDEFINED FUNCTION: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"EARLY FUNCTION CALL"* ]]; then echo -e "${YELLOW} ⚠ CLASS LOADING WARNING: ${line}${NC}" CLASS_WARNINGS=$((CLASS_WARNINGS + 1)) WARN_COUNT=$((WARN_COUNT + 1)) elif [[ "$line" == *"static_class_not_loaded"* ]]; then echo -e "${RED} ✗ STATIC CLASS NOT LOADED: ${line}${NC}" CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) elif [[ "$line" == *"Checking for"* ]] || [[ "$line" == *"No"* ]] || [[ "$line" == *"✓"* ]] || [[ "$line" == *"PASS"* ]] || [[ "$line" == *"FAIL"* ]] || [[ "$line" == *"Scanned"* ]] || [[ "$line" == *"found"* ]]; then : else echo " ${line}" fi done echo "" echo -e "${MAGENTA}[12.1/12] Static Class Loading Pattern Analysis...${NC}" for file in "${php_files[@]}"; do if grep -qE "(class_exists|interface_exists|trait_exists)\s*\(" "${file}"; then if ! grep -qE "(class_exists|interface_exists|trait_exists)\s*\([^)]+\)\s*;" "${file}"; then echo -e "${YELLOW} ⚠ DYNAMIC CLASS CHECK in ${file}:${NC}" echo " Dynamic class loading detected - ensure autoload is configured." CLASS_WARNINGS=$((CLASS_WARNINGS + 1)) WARN_COUNT=$((WARN_COUNT + 1)) fi fi if grep -qE "spl_autoload_register|autoload\s*\(" "${file}"; then echo -e "${GREEN} ✓ Custom autoloader registered in ${file}${NC}" fi if grep -qE "new\s+[A-Z][a-zA-Z0-9_]*\s*\(" "${file}"; then class_refs=$(grep -oE "new\s+[A-Z][a-zA-Z0-9_]*\s*\(" "${file}" | grep -oE "[A-Z][a-zA-Z0-9_]*" | sort -u) for class_ref in $class_refs; do if ! grep -qE "(class|interface|trait)\s+${class_ref}" "${file}"; then include_check=$(grep -nE "(require|include).*${class_ref}" "${file}" | head -1 || true) if [[ -z "$include_check" ]]; then echo -e "${RED} ✗ POTENTIAL MISSING CLASS in ${file}:${NC}" echo " Class '${class_ref}' instantiated but may not be loaded." CLASS_ERRORS=$((CLASS_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi done fi if grep -qE "[A-Z][a-zA-Z0-9_]*::[a-z]" "${file}"; then static_refs=$(grep -oE "[A-Z][a-zA-Z0-9_]*::[a-z][a-zA-Z0-9_]*" "${file}" | cut -d':' -f1 | sort -u) for static_ref in $static_refs; do if ! grep -qE "(class|interface|trait)\s+${static_ref}" "${file}"; then include_check=$(grep -nE "(require|include|use).*${static_ref}" "${file}" | head -1 || true) if [[ -z "$include_check" ]]; then echo -e "${YELLOW} ⚠ POTENTIAL MISSING STATIC CLASS in ${file}:${NC}" echo " Static class '${static_ref}' referenced but may not be loaded." CLASS_WARNINGS=$((CLASS_WARNINGS + 1)) WARN_COUNT=$((WARN_COUNT + 1)) fi fi done fi done if [[ $CLASS_ERRORS -eq 0 ]] && [[ $CLASS_WARNINGS -eq 0 ]]; then echo -e "${GREEN} ✓ No class loading errors detected${NC}" elif [[ $CLASS_ERRORS -gt 0 ]]; then echo -e "${RED} ✗ ${CLASS_ERRORS} class loading error(s) found${NC}" else echo -e "${YELLOW} ⚠ ${CLASS_WARNINGS} class loading warning(s) found${NC}" fi else echo -e "${YELLOW} ⚠ PHP not available - skipping class loading validation${NC}" fi echo "" # ============================================ # 13. TOO FEW ARGUMENTS ERROR DETECTION # ============================================ echo -e "${MAGENTA}[13/13] Checking for Too Few Arguments Errors...${NC}" ARG_ERRORS=0 for file in "${php_files[@]}"; do # Check for array_key_exists with single argument (requires 2) if grep -nE "array_key_exists\s*\(\s*['\"][^'\"]+['\"]\s*\)" "${file}" >/dev/null 2>&1; then if ! grep -nE "array_key_exists\s*\(\s*['\"][^'\"]+['\"]\s*,\s*\\$" "${file}" >/dev/null 2>&1; then matches=$(grep -nE "array_key_exists\s*\(\s*['\"][^'\"]+['\"]\s*\)" "${file}" | head -1) if [[ -n "$matches" ]] && ! grep -E "array_key_exists\s*\(\s*['\"][^'\"]+['\"]\s*,\s*[a-zA-Z_]" "${file}" >/dev/null 2>&1; then echo -e "${RED} ✗ TOO FEW ARGUMENTS in ${file}:${NC}" echo " array_key_exists() called with only 1 argument (requires 2)" echo "$matches" ARG_ERRORS=$((ARG_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi fi # Check for function calls with missing required arguments # str_replace requires 3 arguments if grep -nE "str_replace\s*\(\s*[^)]+,\s*[^)]+\s*\)" "${file}" >/dev/null 2>&1; then if ! grep -E "str_replace\s*\(\s*[^)]+,\s*[^)]+,\s*[^)]+\s*\)" "${file}" >/dev/null 2>&1; then matches=$(grep -nE "str_replace\s*\(\s*[^)]+,\s*[^)]+\s*\)" "${file}" | head -1) if [[ -n "$matches" ]]; then echo -e "${RED} ✗ TOO FEW ARGUMENTS in ${file}:${NC}" echo " str_replace() called with only 2 arguments (requires 3: search, replace, subject)" echo "$matches" ARG_ERRORS=$((ARG_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi fi # Check for sprintf/printf with format but no arguments if grep -nE "(sprintf|printf)\s*\(\s*['\"][%].*['\"]\s*\)" "${file}" >/dev/null 2>&1; then matches=$(grep -nE "(sprintf|printf)\s*\(\s*['\"][%].*['\"]\s*\)" "${file}" | head -1) if [[ -n "$matches" ]]; then echo -e "${YELLOW} ⚠ POSSIBLE TOO FEW ARGUMENTS in ${file}:${NC}" echo " sprintf/printf has format specifiers but may be missing arguments" echo "$matches" ARG_ERRORS=$((ARG_ERRORS + 1)) WARN_COUNT=$((WARN_COUNT + 1)) fi fi # Check for array_slice with only array argument (requires at least 2) if grep -nE "array_slice\s*\(\s*\\\$[a-zA-Z_]+\s*\)" "${file}" >/dev/null 2>&1; then matches=$(grep -nE "array_slice\s*\(\s*\\\$[a-zA-Z_]+\s*\)" "${file}" | head -1) if [[ -n "$matches" ]]; then echo -e "${RED} ✗ TOO FEW ARGUMENTS in ${file}:${NC}" echo " array_slice() called with only 1 argument (requires at least 2: array, offset)" echo "$matches" ARG_ERRORS=$((ARG_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi # Check for in_array with only haystack argument (requires 2) if grep -nE "in_array\s*\(\s*\\\$[a-zA-Z_]+\s*\)" "${file}" >/dev/null 2>&1; then matches=$(grep -nE "in_array\s*\(\s*\\\$[a-zA-Z_]+\s*\)" "${file}" | head -1) if [[ -n "$matches" ]]; then echo -e "${RED} ✗ TOO FEW ARGUMENTS in ${file}:${NC}" echo " in_array() called with only 1 argument (requires 2: needle, haystack)" echo "$matches" ARG_ERRORS=$((ARG_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi # Check for explode with only delimiter argument (requires 2) if grep -nE "explode\s*\(\s*['\"][^'\"]+['\"]\s*\)" "${file}" >/dev/null 2>&1; then if ! grep -E "explode\s*\(\s*['\"][^'\"]+['\"]\s*,\s*\\$" "${file}" >/dev/null 2>&1; then matches=$(grep -nE "explode\s*\(\s*['\"][^'\"]+['\"]\s*\)" "${file}" | head -1) if [[ -n "$matches" ]] && ! grep -E "explode\s*\(\s*['\"][^'\"]+['\"]\s*,\s*[^)]+\)" "${file}" >/dev/null 2>&1; then echo -e "${RED} ✗ TOO FEW ARGUMENTS in ${file}:${NC}" echo " explode() called with only 1 argument (requires 2: delimiter, string)" echo "$matches" ARG_ERRORS=$((ARG_ERRORS + 1)) ERROR_COUNT=$((ERROR_COUNT + 1)) fi fi fi # Check for implode with only array argument (requires 1, but checking for wrong order) if grep -nE "implode\s*\(\s*\\\$[a-zA-Z_]+\s*,\s*['\"][^'\"]+['\"]\s*\)" "${file}" >/dev/null 2>&1; then matches=$(grep -nE "implode\s*\(\s*\\\$[a-zA-Z_]+\s*,\s*['\"][^'\"]+['\"]\s*\)" "${file}" | head -1) if [[ -n "$matches" ]]; then echo -e "${YELLOW} ⚠ POSSIBLE ARGUMENT ORDER ERROR in ${file}:${NC}" echo " implode() arguments may be in wrong order (expected glue, array)" echo "$matches" ARG_ERRORS=$((ARG_ERRORS + 1)) WARN_COUNT=$((WARN_COUNT + 1)) fi fi # Check for array_merge with single argument (unusual pattern) if grep -nE "array_merge\s*\(\s*\\\$[a-zA-Z_]+\s*\)" "${file}" >/dev/null 2>&1; then matches=$(grep -nE "array_merge\s*\(\s*\\\$[a-zA-Z_]+\s*\)" "${file}" | head -1) if [[ -n "$matches" ]]; then echo -e "${YELLOW} ⚠ POSSIBLE TOO FEW ARGUMENTS in ${file}:${NC}" echo " array_merge() with single argument may indicate missing array parameter" echo "$matches" ARG_ERRORS=$((ARG_ERRORS + 1)) WARN_COUNT=$((WARN_COUNT + 1)) fi fi done if [[ $ARG_ERRORS -eq 0 ]]; then echo -e "${GREEN} ✓ No too few arguments errors detected${NC}" fi echo "" # ============================================ # 14. FINAL REPORT # ============================================ echo -e "${BLUE}========================================================${NC}" echo -e "${BLUE} AUDIT RESULTS ${NC}" echo -e "${BLUE}========================================================${NC}" if [[ "${ERROR_COUNT}" -gt 0 ]]; then echo -e "${RED}FAIL: ${ERROR_COUNT} Critical Security/Stability Issues Found.${NC}" echo -e "${RED}This plugin is DANGEROUS or BROKEN. Do not deploy.${NC}" if [[ "${WARN_COUNT}" -gt 0 ]]; then echo -e "${YELLOW}(Plus ${WARN_COUNT} warnings requiring review)${NC}" fi exit 1 elif [[ "${WARN_COUNT}" -gt 0 ]]; then echo -e "${YELLOW}WARNING: ${WARN_COUNT} Potential issues found.${NC}" echo "Manual review required to verify false positives." echo "Strict mode does not allow clean pass with warnings." exit 1 else echo -e "${GREEN}SUCCESS: Plugin passed all strict checks.${NC}" exit 0 fi