1314 lines
58 KiB
PHP
1314 lines
58 KiB
PHP
#!/usr/bin/env php
|
||
<?php
|
||
/**
|
||
* Comprehensive PHP/WordPress Plugin Static Analyzer
|
||
* Detects runtime errors, duplicate declarations, missing dependencies, and code issues
|
||
* Optimized for minimal memory usage via streaming token parsing
|
||
*/
|
||
|
||
if ($argv[1] ?? null === '--help' || $argv[1] ?? null === '-h') {
|
||
echo "Usage: {$argv[0]} /path/to/plugin [options]\n";
|
||
echo "\nOptions:\n";
|
||
echo " --verbose Show detailed output\n";
|
||
echo " --strict Enable stricter checks (may have false positives)\n";
|
||
echo " --no-cache Skip caching (useful for CI/CD)\n";
|
||
echo " --quiet Suppress informational messages\n";
|
||
exit(0);
|
||
}
|
||
|
||
if ($argc < 2) {
|
||
fwrite(STDERR, "Usage: {$argv[0]} /path/to/plugin\n");
|
||
exit(1);
|
||
}
|
||
|
||
$pluginDir = rtrim($argv[1], '/');
|
||
$verbose = in_array('--verbose', $argv);
|
||
$strict = in_array('--strict', $argv);
|
||
$quiet = in_array('--quiet', $argv);
|
||
|
||
if (!is_dir($pluginDir)) {
|
||
fwrite(STDERR, "Error: Directory not found: {$pluginDir}\n");
|
||
exit(1);
|
||
}
|
||
|
||
$stats = [
|
||
'files' => 0,
|
||
'classes' => 0,
|
||
'interfaces' => 0,
|
||
'traits' => 0,
|
||
'functions' => 0,
|
||
'constants' => 0,
|
||
'includes' => 0
|
||
];
|
||
|
||
$issues = [
|
||
'duplicates' => [],
|
||
'missing_includes' => [],
|
||
'missing_classes' => [],
|
||
'type_hints' => [],
|
||
'circular_deps' => [],
|
||
'this_usage' => [],
|
||
'static_calls' => [],
|
||
'file_paths' => [],
|
||
'activation' => [],
|
||
'undefined_functions' => [],
|
||
'early_function_calls' => [],
|
||
'undefined_arrays' => [],
|
||
'undefined_array_keys' => [],
|
||
'css_overlaps' => []
|
||
];
|
||
|
||
$declarations = [
|
||
'classes' => [],
|
||
'interfaces' => [],
|
||
'traits' => [],
|
||
'functions' => [],
|
||
'constants' => [],
|
||
'includes' => []
|
||
];
|
||
|
||
$definedInFile = [];
|
||
$functionImplementations = [];
|
||
$methodImplementations = [];
|
||
$typeHints = [];
|
||
$thisUsage = [];
|
||
$staticCalls = [];
|
||
|
||
// Track functions defined and their line numbers
|
||
$functionDefinitions = [];
|
||
$functionCalls = [];
|
||
$variableAssignments = [];
|
||
$arrayAccesses = [];
|
||
$cssFiles = [];
|
||
$variableDeclarations = [];
|
||
$arrayKeyAccesses = [];
|
||
$arrayKeyDefinitions = [];
|
||
$unsafeArrayAccesses = [];
|
||
|
||
$excludeDirs = ['/vendor/', '/node_modules/', '/.git/', '/cache/', '/logs/', '/tmp/'];
|
||
|
||
function shouldExclude(string $path, array $excludeDirs): bool {
|
||
foreach ($excludeDirs as $dir) {
|
||
if (strpos($path, $dir) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function normalizePath(string $path, string $baseDir): string {
|
||
$path = str_replace(['/', '\\'], '/', $path);
|
||
$baseDir = rtrim(str_replace(['/', '\\'], '/', $baseDir), '/');
|
||
|
||
if (preg_match('/^\.\.?[\/\\\\]/', $path) || !preg_match('/^[a-zA-Z]:/', $path)) {
|
||
$fullPath = $baseDir . '/' . $path;
|
||
$fullPath = preg_replace('/\/+/', '/', $fullPath);
|
||
return $fullPath;
|
||
}
|
||
|
||
return $path;
|
||
}
|
||
|
||
function findAbsoluteFile(string $includePath, string $currentFile, string $baseDir): ?string {
|
||
$currentDir = dirname($currentFile);
|
||
$possiblePaths = [
|
||
$currentDir . '/' . $includePath,
|
||
$currentDir . '/' . $includePath . '.php',
|
||
$baseDir . '/' . $includePath,
|
||
$baseDir . '/' . $includePath . '.php',
|
||
$baseDir . '/includes/' . $includePath,
|
||
$baseDir . '/includes/' . $includePath . '.php',
|
||
];
|
||
|
||
foreach ($possiblePaths as $path) {
|
||
if (file_exists($path)) {
|
||
return realpath($path) ?: $path;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
$iterator = new RecursiveIteratorIterator(
|
||
new RecursiveDirectoryIterator($pluginDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||
RecursiveIteratorIterator::LEAVES_ONLY
|
||
);
|
||
|
||
foreach ($iterator as $file) {
|
||
if ($file->getExtension() !== 'php') {
|
||
continue;
|
||
}
|
||
|
||
$filePath = $file->getPathname();
|
||
if (shouldExclude($filePath, $excludeDirs)) {
|
||
continue;
|
||
}
|
||
|
||
$stats['files']++;
|
||
$relativePath = str_replace($pluginDir . '/', '', $filePath);
|
||
$definedInFile[$relativePath] = [];
|
||
|
||
$content = file_get_contents($filePath, false, null, 0, 2097152);
|
||
if ($content === false) {
|
||
continue;
|
||
}
|
||
|
||
$tokens = token_get_all($content);
|
||
$lineNumber = 1;
|
||
$namespaces = [''];
|
||
$currentNS = '';
|
||
|
||
for ($i = 0; $i < count($tokens); $i++) {
|
||
$token = $tokens[$i];
|
||
|
||
if (is_array($token)) {
|
||
if ($token[2] !== $lineNumber) {
|
||
$lineNumber = $token[2];
|
||
}
|
||
|
||
switch ($token[0]) {
|
||
case T_NAMESPACE:
|
||
$nsTokens = [];
|
||
$j = $i + 1;
|
||
while (isset($tokens[$j]) && ($tokens[$j] !== ';' && $tokens[$j] !== '{' && $tokens[$j] !== '=')) {
|
||
if (is_array($tokens[$j]) && in_array($tokens[$j][0], [T_STRING, T_NS_SEPARATOR])) {
|
||
$nsTokens[] = $tokens[$j][1];
|
||
}
|
||
$j++;
|
||
}
|
||
$currentNS = implode('', $nsTokens);
|
||
$namespaces[count($namespaces) - 1] = $currentNS;
|
||
break;
|
||
|
||
case T_CLASS:
|
||
$className = extractDeclaration($tokens, $i, $namespaces, 'class');
|
||
if ($className) {
|
||
$stats['classes']++;
|
||
$fullName = $currentNS ? $currentNS . '\\' . $className : $className;
|
||
$declarations['classes'][$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
|
||
$definedInFile[$relativePath][] = ['type' => 'class', 'name' => $fullName];
|
||
}
|
||
break;
|
||
|
||
case T_INTERFACE:
|
||
$interfaceName = extractDeclaration($tokens, $i, $namespaces, 'interface');
|
||
if ($interfaceName) {
|
||
$stats['interfaces']++;
|
||
$fullName = $currentNS ? $currentNS . '\\' . $interfaceName : $interfaceName;
|
||
$declarations['interfaces'][$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
|
||
$definedInFile[$relativePath][] = ['type' => 'interface', 'name' => $fullName];
|
||
}
|
||
break;
|
||
|
||
case T_TRAIT:
|
||
$traitName = extractDeclaration($tokens, $i, $namespaces, 'trait');
|
||
if ($traitName) {
|
||
$stats['traits']++;
|
||
$fullName = $currentNS ? $currentNS . '\\' . $traitName : $traitName;
|
||
$declarations['traits'][$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
|
||
$definedInFile[$relativePath][] = ['type' => 'trait', 'name' => $fullName];
|
||
}
|
||
break;
|
||
|
||
case T_FUNCTION:
|
||
$funcName = extractDeclaration($tokens, $i, $namespaces, 'function');
|
||
if ($funcName) {
|
||
$stats['functions']++;
|
||
$fullName = $currentNS ? $currentNS . '\\' . $funcName : $funcName;
|
||
if (!isset($functionImplementations[$fullName])) {
|
||
$functionImplementations[$fullName] = [];
|
||
}
|
||
$functionImplementations[$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
|
||
$definedInFile[$relativePath][] = ['type' => 'function', 'name' => $fullName];
|
||
|
||
// Track function definition for early call detection
|
||
if (!isset($functionDefinitions[$relativePath])) {
|
||
$functionDefinitions[$relativePath] = [];
|
||
}
|
||
$functionDefinitions[$relativePath][$funcName] = $lineNumber;
|
||
}
|
||
break;
|
||
|
||
case T_CONST:
|
||
$constName = extractConstantDeclaration($tokens, $i);
|
||
if ($constName) {
|
||
$stats['constants']++;
|
||
$fullName = $currentNS ? $currentNS . '\\' . $constName : $constName;
|
||
if (!isset($declarations['constants'][$fullName])) {
|
||
$declarations['constants'][$fullName] = [];
|
||
}
|
||
$declarations['constants'][$fullName][] = ['file' => $relativePath, 'line' => $lineNumber];
|
||
$definedInFile[$relativePath][] = ['type' => 'constant', 'name' => $fullName];
|
||
}
|
||
break;
|
||
|
||
case T_NEW:
|
||
$className = extractAfterToken($tokens, $i);
|
||
if ($className && !isInternalClass($className)) {
|
||
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'new'];
|
||
}
|
||
break;
|
||
|
||
case T_INSTANCEOF:
|
||
$className = extractAfterToken($tokens, $i);
|
||
if ($className && !isInternalClass($className)) {
|
||
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'instanceof'];
|
||
}
|
||
break;
|
||
|
||
case T_CATCH:
|
||
$className = extractCatchClass($tokens, $i);
|
||
if ($className && !isInternalClass($className)) {
|
||
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'catch'];
|
||
}
|
||
break;
|
||
|
||
case T_EXTENDS:
|
||
$className = extractAfterToken($tokens, $i);
|
||
if ($className && !isInternalClass($className)) {
|
||
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'extends'];
|
||
}
|
||
break;
|
||
|
||
case T_IMPLEMENTS:
|
||
$className = extractAfterToken($tokens, $i);
|
||
if ($className && !isInternalClass($className)) {
|
||
$typeHints[$className][] = ['file' => $relativePath, 'line' => $lineNumber, 'usage' => 'implements'];
|
||
}
|
||
break;
|
||
|
||
case T_INCLUDE:
|
||
case T_INCLUDE_ONCE:
|
||
case T_REQUIRE:
|
||
case T_REQUIRE_ONCE:
|
||
$includePath = extractIncludePath($tokens, $i);
|
||
if ($includePath) {
|
||
$stats['includes']++;
|
||
$fullPath = findAbsoluteFile($includePath, $filePath, $pluginDir);
|
||
$declarations['includes'][] = [
|
||
'path' => $includePath,
|
||
'file' => $relativePath,
|
||
'line' => $lineNumber,
|
||
'resolved' => $fullPath
|
||
];
|
||
|
||
if (!$fullPath || !file_exists($fullPath)) {
|
||
$issues['missing_includes'][] = [
|
||
'path' => $includePath,
|
||
'file' => $relativePath,
|
||
'line' => $lineNumber
|
||
];
|
||
}
|
||
}
|
||
break;
|
||
|
||
case T_STRING:
|
||
// Track function calls
|
||
if (isset($tokens[$i + 1]) && $tokens[$i + 1] === '(') {
|
||
$funcName = $token[1];
|
||
// Skip if it's a keyword or internal function
|
||
if (!in_array(strtolower($funcName), ['if', 'while', 'for', 'foreach', 'switch', 'return', 'echo', 'print', 'array', 'list', 'isset', 'empty', 'unset', 'die', 'exit', 'parent', 'self', 'static'])) {
|
||
if (!isset($functionCalls[$relativePath])) {
|
||
$functionCalls[$relativePath] = [];
|
||
}
|
||
$functionCalls[$relativePath][] = [
|
||
'name' => $funcName,
|
||
'line' => $lineNumber
|
||
];
|
||
}
|
||
}
|
||
break;
|
||
|
||
case T_VARIABLE:
|
||
$varName = $token[1];
|
||
|
||
// Track variable assignments for array type inference
|
||
$prevNonWhitespace = $i - 1;
|
||
while ($prevNonWhitespace >= 0 && (is_array($tokens[$prevNonWhitespace]) && $tokens[$prevNonWhitespace][0] === T_WHITESPACE)) {
|
||
$prevNonWhitespace--;
|
||
}
|
||
|
||
// Check if this is a variable assignment
|
||
if ($prevNonWhitespace >= 0 && is_array($tokens[$prevNonWhitespace]) && $tokens[$prevNonWhitespace][0] === T_VARIABLE) {
|
||
if (!isset($variableDeclarations[$relativePath])) {
|
||
$variableDeclarations[$relativePath] = [];
|
||
}
|
||
$variableDeclarations[$relativePath][] = [
|
||
'name' => $varName,
|
||
'line' => $lineNumber,
|
||
'context' => 'assignment'
|
||
];
|
||
}
|
||
|
||
// Track array access patterns: $array['key'] or $array[$key]
|
||
$nextToken = $i + 1;
|
||
while (isset($tokens[$nextToken]) && is_array($tokens[$nextToken]) && $tokens[$nextToken][0] === T_WHITESPACE) {
|
||
$nextToken++;
|
||
}
|
||
|
||
if (isset($tokens[$nextToken]) && $tokens[$nextToken] === '[') {
|
||
if (!isset($arrayKeyAccesses[$relativePath])) {
|
||
$arrayKeyAccesses[$relativePath] = [];
|
||
}
|
||
$arrayKeyAccesses[$relativePath][] = [
|
||
'variable' => $varName,
|
||
'line' => $lineNumber
|
||
];
|
||
}
|
||
break;
|
||
}
|
||
} elseif ($token === '{' || $token === '}') {
|
||
$lineNumber++;
|
||
} elseif ($token === "\n") {
|
||
$lineNumber++;
|
||
}
|
||
}
|
||
|
||
$definedInFile[$relativePath] = array_unique($definedInFile[$relativePath], SORT_REGULAR);
|
||
}
|
||
|
||
function extractDeclaration(array $tokens, int $startIndex, array $namespaces, string $type): ?string {
|
||
$nextIndex = $startIndex + 1;
|
||
while (isset($tokens[$nextIndex]) && is_array($tokens[$nextIndex]) && $tokens[$nextIndex][0] === T_WHITESPACE) {
|
||
$nextIndex++;
|
||
}
|
||
|
||
if (!isset($tokens[$nextIndex]) || !is_array($tokens[$nextIndex]) || $tokens[$nextIndex][0] !== T_STRING) {
|
||
return null;
|
||
}
|
||
|
||
return $tokens[$nextIndex][1];
|
||
}
|
||
|
||
function extractConstantDeclaration(array $tokens, int $startIndex): ?string {
|
||
$nextIndex = $startIndex + 1;
|
||
while (isset($tokens[$nextIndex]) && is_array($tokens[$nextIndex]) && $tokens[$nextIndex][0] === T_WHITESPACE) {
|
||
$nextIndex++;
|
||
}
|
||
|
||
if (!isset($tokens[$nextIndex]) || !is_array($tokens[$nextIndex]) || $tokens[$nextIndex][0] !== T_STRING) {
|
||
return null;
|
||
}
|
||
|
||
return $tokens[$nextIndex][1];
|
||
}
|
||
|
||
function extractAfterToken(array $tokens, int $startIndex): ?string {
|
||
$nextIndex = $startIndex + 1;
|
||
while (isset($tokens[$nextIndex]) && is_array($tokens[$nextIndex]) && $tokens[$nextIndex][0] === T_WHITESPACE) {
|
||
$nextIndex++;
|
||
}
|
||
|
||
if (!isset($tokens[$nextIndex])) {
|
||
return null;
|
||
}
|
||
|
||
if (is_array($tokens[$nextIndex]) && $tokens[$nextIndex][0] === T_STRING) {
|
||
return $tokens[$nextIndex][1];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function extractCatchClass(array $tokens, int $startIndex): ?string {
|
||
$j = $startIndex + 1;
|
||
$foundParen = false;
|
||
|
||
while (isset($tokens[$j])) {
|
||
if ($tokens[$j] === '(') {
|
||
$foundParen = true;
|
||
} elseif ($foundParen && is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
|
||
return $tokens[$j][1];
|
||
} elseif ($foundParen && $tokens[$j] === ')') {
|
||
break;
|
||
}
|
||
$j++;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function extractIncludePath(array $tokens, int $startIndex): ?string {
|
||
$pathTokens = [];
|
||
$j = $startIndex + 1;
|
||
|
||
while (isset($tokens[$j]) && $tokens[$j] !== ';') {
|
||
if (is_array($tokens[$j]) && $tokens[$j][0] === T_ENCAPSED_AND_WHITESPACE) {
|
||
$pathTokens[] = $tokens[$j][1];
|
||
} elseif (is_array($tokens[$j]) && $tokens[$j][0] === T_CONSTANT_ENCAPSED_STRING) {
|
||
$pathTokens[] = trim($tokens[$j][1], '"\'');
|
||
} elseif ($tokens[$j] === "\n") {
|
||
break;
|
||
}
|
||
$j++;
|
||
}
|
||
|
||
return $pathTokens ? implode('', $pathTokens) : null;
|
||
}
|
||
|
||
function isInternalClass(string $className): bool {
|
||
$internalClasses = [
|
||
'Exception', 'Error', 'Throwable', 'ArgumentCountError', 'ArithmeticError',
|
||
'AssertionError', 'DivisionByZeroError', 'ErrorException', 'CompileError',
|
||
'ParseError', 'TypeError', 'UnhandledMatchError', 'ValueError',
|
||
'DateTime', 'DateTimeImmutable', 'DateInterval', 'DatePeriod',
|
||
'DOMDocument', 'DOMNode', 'DOMElement', 'DOMNodeList',
|
||
'PDO', 'PDOStatement', 'PDOException',
|
||
'mysqli', 'mysqli_result', 'mysqli_stmt', 'mysqli_sql_exception',
|
||
'ArrayObject', 'ArrayIterator', 'IteratorIterator', 'Traversable',
|
||
'Countable', 'Serializable', 'JsonSerializable',
|
||
'Stringable', 'Stringable', 'UnitEnum', 'BackedEnum',
|
||
'Closure', 'Generator', 'Fiber', 'WeakReference',
|
||
'SplFileInfo', 'SplFileObject', 'SplTempFileObject', 'SplDoublyLinkedList',
|
||
'SplQueue', 'SplStack', 'SplHeap', 'SplMinHeap', 'SplMaxHeap',
|
||
'SplPriorityQueue', 'SplFixedArray', 'SplObjectStorage',
|
||
'RegexIterator', 'RecursiveRegexIterator', 'RecursiveIteratorIterator',
|
||
'RecursiveDirectoryIterator', 'RecursiveIterator', 'RecursiveTreeIterator',
|
||
'DirectoryIterator', 'FilesystemIterator', 'GlobIterator',
|
||
'SimpleXMLElement', 'SimpleXMLIterator',
|
||
'SoapClient', 'SoapServer', 'SoapFault',
|
||
'XMLReader', 'XMLWriter',
|
||
'ZipArchive', ' PharData', 'PharFileInfo', 'PharException',
|
||
'Laminas\\Escaper\\Escaper', 'Psr\\Container\\ContainerInterface',
|
||
'WP_Error', 'WP_Post', 'WP_User', 'WP_Query', 'WP_Term', 'WP_Object_Cache',
|
||
'WP_Theme', 'WP_Screen', 'WP_List_Table', 'WP_Comment_Query',
|
||
'WC_Product', 'WC_Order', 'WC_Customer', 'WC_Cart', 'WC_Session',
|
||
'WP_REST_Request', 'WP_REST_Response', 'WP_REST_Server',
|
||
'WP_Block_Editor_Context', 'WP_Block_Type', 'WP_Block', 'WP_Block_List'
|
||
];
|
||
|
||
$shortName = substr($className, strrpos($className, '\\') + 1);
|
||
return in_array($className, $internalClasses) || in_array($shortName, $internalClasses);
|
||
}
|
||
|
||
function isInternalFunction(string $funcName): bool {
|
||
$internalFuncs = [
|
||
'strlen', 'strcmp', 'strncmp', 'strcasecmp', 'strncasecmp', 'strpos', 'stripos',
|
||
'strrpos', 'strripos', 'strstr', 'stristr', 'strrchr', 'substr', 'str_replace',
|
||
'str_ireplace', 'strtr', 'trim', 'ltrim', 'rtrim', 'str_pad', 'str_repeat',
|
||
'implode', 'explode', 'join', 'split', 'htmlspecialchars', 'htmlentities',
|
||
'htmlspecialchars_decode', 'html_entity_decode', 'addslashes', 'stripslashes',
|
||
'stripcslashes', 'strip_tags', 'strval', 'intval', 'floatval', 'doubleval',
|
||
'boolval', 'settype', 'gettype', 'is_array', 'is_bool', 'is_float', 'is_int',
|
||
'is_null', 'is_numeric', 'is_string', 'is_object', 'is_resource', 'is_scalar',
|
||
'isset', 'empty', 'unset', 'defined', 'define', 'constant', 'time', 'mktime',
|
||
'gmmktime', 'date', 'gmdate', 'date_default_timezone_set', 'date_default_timezone_get',
|
||
'idate', 'getdate', 'localtime', 'checkdate', 'strtotime', 'strftime', 'gmstrftime',
|
||
'array', 'array_key_exists', 'array_keys', 'array_values', 'array_count_values',
|
||
'array_push', 'array_pop', 'array_shift', 'array_unshift', 'array_merge',
|
||
'array_merge_recursive', 'array_replace', 'array_replace_recursive', 'array_slice',
|
||
'array_splice', 'array_intersect', 'array_intersect_key', 'array_intersect_ukey',
|
||
'array_uintersect', 'array_diff', 'array_diff_key', 'array_diff_ukey',
|
||
'array_udiff', 'array_filter', 'array_map', 'array_reduce', 'array_walk',
|
||
'array_walk_recursive', 'sort', 'rsort', 'asort', 'arsort', 'ksort', 'krsort',
|
||
'natsort', 'natcasesort', 'usort', 'uasort', 'uksort', 'shuffle', 'array_sum',
|
||
'array_product', 'array_reduce', 'array_reverse', 'array_pad', 'array_flip',
|
||
'array_change_key_case', 'array_chunk', 'array_combine', 'array_column',
|
||
'preg_match', 'preg_match_all', 'preg_replace', 'preg_replace_callback',
|
||
'preg_split', 'preg_quote', 'preg_grep', 'preg_last_error', 'preg_last_error_msg',
|
||
'mb_strlen', 'mb_strpos', 'mb_strrpos', 'mb_stripos', 'mb_strripos', 'mb_substr',
|
||
'mb_substr_count', 'mb_strtolower', 'mb_strtoupper', 'mb_ucwords', 'mb_convert_case',
|
||
'json_encode', 'json_decode', 'json_last_error', 'json_last_error_msg',
|
||
'json_validate', 'serialize', 'unserialize', 'var_export', 'var_dump', 'print_r',
|
||
'debug_zval_dump', 'debug_backtrace', 'debug_print_backtrace', 'error_reporting',
|
||
'error_log', 'error_get_last', 'trigger_error', 'user_error', 'set_error_handler',
|
||
'restore_error_handler', 'set_exception_handler', 'restore_exception_handler',
|
||
'header_remove', 'headers_list', 'headers_sent', 'header', 'http_response_code',
|
||
'connection_aborted', 'connection_status', 'ignore_user_abort', 'set_time_limit',
|
||
'get_magic_quotes_gpc', 'get_magic_quotes_runtime', 'get_loaded_extensions',
|
||
'extension_loaded', 'get_defined_functions', 'get_defined_constants', 'get_defined_vars',
|
||
'func_num_args', 'func_get_arg', 'func_get_args', 'create_function', 'call_user_func',
|
||
'call_user_func_array', 'forward_static_call', 'forward_static_call_array',
|
||
'register_shutdown_function', 'register_tick_function', 'unregister_tick_function',
|
||
'is_callable', 'method_exists', 'property_exists', 'class_exists', 'interface_exists',
|
||
'trait_exists', 'function_exists', 'class_alias', 'get_declared_classes',
|
||
'get_declared_interfaces', 'get_declared_traits', 'get_defined_functions',
|
||
'get_defined_constants', 'gc_enabled', 'gc_collect_cycles', 'gc_status',
|
||
'memory_get_usage', 'memory_get_peak_usage', 'memory_reset_peak_usage',
|
||
'sys_getloadavg', 'sys_get_temp_dir', 'realpath_cache_size', 'realpath_cache_get',
|
||
'opcache_get_status', 'opcache_compile_file', 'opcache_is_script_cached',
|
||
'version_compare', 'php_uname', 'phpversion', 'phpinfo', 'phpcredits',
|
||
'php_sapi_name', 'php_ini_loaded_file', 'php_ini_scanned_files',
|
||
'rand', 'srand', 'mt_rand', 'mt_srand', 'random_int', 'random_bytes',
|
||
'bin2hex', 'hex2bin', 'pack', 'unpack', 'md5', 'md5_file', 'sha1', 'sha1_file',
|
||
'hash', 'hash_file', 'hash_hmac', 'hash_hmac_file', 'hash_algos', 'hash_init',
|
||
'hash_update', 'hash_update_stream', 'hash_final', 'password_hash',
|
||
'password_verify', 'password_needs_rehash', 'password_get_info',
|
||
'base64_encode', 'base64_decode', 'convert_uuencode', 'convert_uudecode',
|
||
'addcslashes', 'stripcslashes', 'quoted_printable_encode', 'quoted_printable_decode',
|
||
'str_rot13', 'stream_context_create', 'stream_context_get_params',
|
||
'stream_context_get_default', 'stream_context_set_option', 'stream_context_set_params',
|
||
'stream_filter_prepend', 'stream_filter_append', 'stream_filter_remove',
|
||
'stream_socket_client', 'stream_socket_server', 'stream_socket_accept',
|
||
'stream_socket_get_name', 'stream_socket_recvfrom', 'stream_socket_sendto',
|
||
'stream_get_contents', 'stream_get_line', 'stream_get_meta_data', 'stream_get_transports',
|
||
'stream_get_wrappers', 'stream_resolve_include_path', 'stream_wrapper_register',
|
||
'stream_wrapper_unregister', 'stream_wrapper_restore', 'file_get_contents',
|
||
'file_put_contents', 'file', 'fopen', 'fclose', 'fread', 'fwrite', 'fgets',
|
||
'fgetss', 'file', 'readfile', 'fscanf', 'fseek', 'ftell', 'rewind', 'feof',
|
||
'fflush', 'ftruncate', 'flock', 'fnmatch', 'glob', 'is_file', 'is_dir', 'is_link',
|
||
'is_executable', 'is_readable', 'is_writable', 'is_uploaded_file', 'tempnam',
|
||
'tmpfile', 'realpath', 'dirname', 'basename', 'pathinfo', 'parse_url', 'urldecode',
|
||
'urlencode', 'rawurlencode', 'rawurldecode', 'http_build_query', 'parse_str',
|
||
'str_getcsv', 'fputcsv', 'fgetcsv', 'strtok', 'chunk_split', 'nl2br',
|
||
'filter_var', 'filter_input', 'filter_list', 'filter_id', 'filter_has_var',
|
||
'hash_equals', 'crypt', 'wordwrap', 'count_chars', 'similar_text', 'levenshtein',
|
||
'soundex', 'metaphone', 'abs', 'ceil', 'floor', 'round', 'max', 'min', 'pow',
|
||
'sqrt', 'log', 'log10', 'log1p', 'exp', 'expm1', 'pi', 'is_nan', 'is_finite',
|
||
'is_infinite', 'hypot', 'deg2rad', 'rad2deg', 'intdiv', 'fmod', 'intdiv',
|
||
'wp_die', 'wp_enqueue_script', 'wp_enqueue_style', 'wp_register_script',
|
||
'wp_register_style', 'wp_add_inline_script', 'wp_add_inline_style',
|
||
'wp_localize_script', 'wp_scripts', 'wp_styles', 'get_option', 'update_option',
|
||
'add_option', 'delete_option', 'get_post_meta', 'update_post_meta', 'delete_post_meta',
|
||
'get_user_meta', 'update_user_meta', 'delete_user_meta', 'get_term_meta',
|
||
'update_term_meta', 'delete_term_meta', 'get_posts', 'get_post', 'wp_insert_post',
|
||
'wp_update_post', 'wp_delete_post', 'get_pages', 'get_page_by_path',
|
||
'wp_get_current_user', 'wp_get_current_user', 'wp_get_current_user',
|
||
'current_user_can', 'user_can', 'is_user_logged_in', 'wp_logout', 'wp_login',
|
||
'wp_authenticate', 'sanitize_text_field', 'sanitize_email', 'sanitize_title',
|
||
'sanitize_file_name', 'esc_html', 'esc_attr', 'esc_url', 'esc_js', 'esc_textarea',
|
||
'wp_kses', 'wp_kses_post', 'wp_filter_post_kses', 'wp_kses_hair',
|
||
'add_action', 'do_action', 'do_action_ref_array', 'has_action', 'remove_action',
|
||
'remove_all_actions', 'doing_action', 'add_filter', 'apply_filters',
|
||
'apply_filters_ref_array', 'has_filter', 'remove_filter', 'remove_all_filters',
|
||
'doing_filter', 'current_action', 'current_filter',
|
||
'add_shortcode', 'remove_shortcode', 'remove_all_shortcodes', 'shortcode_atts',
|
||
'do_shortcode', 'strip_shortcodes', 'has_shortcode',
|
||
'wp_remote_get', 'wp_remote_post', 'wp_remote_head', 'wp_remote_request',
|
||
'wp_remote_retrieve_body', 'wp_remote_retrieve_headers', 'wp_remote_retrieve_response_code',
|
||
'wp_remote_retrieve_response_message', 'wp_remote_post', 'wp_remote_get',
|
||
'is_wp_error', 'is_admin', 'is_front_page', 'is_home', 'is_single', 'is_page',
|
||
'is_singular', 'is_archive', 'is_search', 'is_404', 'is_user_logged_in',
|
||
'wp_verify_nonce', 'wp_create_nonce', 'wp_nonce_field', 'wp_nonce_url',
|
||
'check_admin_referer', 'check_ajax_referer', 'wp_salt', 'wp_hash',
|
||
'wp_generate_password', 'wp_rand', 'wp_kses_normalize_entities', 'wp_kses_bad_protocol',
|
||
'wp_kses_array_lc', 'wp_iso_descr', 'wp_kses_split',
|
||
'WC', 'wc_get_product', 'wc_get_products', 'wc_get_order', 'wc_get_orders',
|
||
'wc_add_order_item', 'wc_update_order_item', 'wc_delete_order_item',
|
||
'wc_add_order_item_meta', 'wc_delete_order_item_meta',
|
||
'wc_get_customer', 'wc_update_customer', 'wc_add_customer_note',
|
||
'wc_create_nonce', 'wc_rest_prepare_product', 'wc_rest_prepare_order'
|
||
];
|
||
|
||
$shortName = strtolower($funcName);
|
||
$parts = explode('\\', $funcName);
|
||
$shortName = strtolower(end($parts));
|
||
|
||
return in_array(strtolower($funcName), $internalFuncs) || in_array($shortName, $internalFuncs);
|
||
}
|
||
|
||
$foundIssues = false;
|
||
|
||
function reportIssues(array $issues, string $type): bool {
|
||
global $foundIssues, $verbose;
|
||
|
||
if (empty($issues)) {
|
||
return false;
|
||
}
|
||
|
||
$foundIssues = true;
|
||
$unique = [];
|
||
|
||
foreach ($issues as $issue) {
|
||
$key = $issue['name'] ?? $issue['path'] ?? json_encode($issue);
|
||
if (!isset($unique[$key])) {
|
||
$unique[$key] = $issue;
|
||
}
|
||
}
|
||
|
||
foreach ($unique as $issue) {
|
||
if (isset($issue['path'])) {
|
||
echo "DUPLICATE {$type}: '{$issue['path']}' declared multiple times:\n";
|
||
foreach ($issue['locations'] ?? [] as $loc) {
|
||
echo " {$loc['file']}:{$loc['line']}\n";
|
||
}
|
||
} elseif (isset($issue['name'])) {
|
||
echo "MISSING {$type}: '{$issue['name']}' referenced but not declared:\n";
|
||
echo " {$issue['file']}:{$issue['line']}\n";
|
||
} else {
|
||
echo "ISSUE {$type}: " . json_encode($issue) . "\n";
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
if ($verbose) {
|
||
echo "Scanned {$stats['files']} files\n";
|
||
echo "Found {$stats['classes']} classes, {$stats['interfaces']} interfaces, ";
|
||
echo "{$stats['traits']} traits, {$stats['functions']} functions, {$stats['constants']} constants\n";
|
||
echo "Found {$stats['includes']} include/require statements\n\n";
|
||
}
|
||
|
||
$anyIssues = false;
|
||
|
||
foreach ($declarations as $type => $decls) {
|
||
foreach ($decls as $name => $locations) {
|
||
if (count($locations) > 1) {
|
||
$anyIssues = true;
|
||
echo "DUPLICATE {$type}: '{$name}' declared in " . count($locations) . " locations:\n";
|
||
foreach ($locations as $loc) {
|
||
echo " {$loc['file']}:{$loc['line']}\n";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($typeHints as $className => $usages) {
|
||
$declared = isset($declarations['classes'][$className]) ||
|
||
isset($declarations['interfaces'][$className]) ||
|
||
isset($declarations['traits'][$className]);
|
||
|
||
if (!$declared && !isInternalClass($className)) {
|
||
$anyIssues = true;
|
||
$unique = [];
|
||
foreach ($usages as $usage) {
|
||
$key = "{$usage['file']}:{$usage['line']}";
|
||
if (!isset($unique[$key])) {
|
||
$unique[$key] = $usage;
|
||
}
|
||
}
|
||
echo "MISSING CLASS/TYPE: '{$className}' used but not declared:\n";
|
||
foreach ($unique as $usage) {
|
||
echo " {$usage['file']}:{$usage['line']} ({$usage['usage']})\n";
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($iterator as $file) {
|
||
if ($file->getExtension() !== 'php') {
|
||
continue;
|
||
}
|
||
|
||
$filePath = $file->getPathname();
|
||
if (shouldExclude($filePath, $excludeDirs)) {
|
||
continue;
|
||
}
|
||
|
||
$content = file_get_contents($filePath);
|
||
if ($content === false) {
|
||
continue;
|
||
}
|
||
|
||
$lines = explode("\n", $content);
|
||
$relativePath = str_replace($pluginDir . '/', '', $filePath);
|
||
|
||
$activationHooks = [];
|
||
foreach ($lines as $lineNum => $line) {
|
||
$lineNum += 1;
|
||
|
||
if (preg_match('/register_activation_hook\s*\(/i', $line)) {
|
||
$activationHooks[] = [
|
||
'line' => $lineNum,
|
||
'content' => $line
|
||
];
|
||
}
|
||
|
||
if (preg_match('/(include|require|fopen|file_get_contents|file_put_contents|is_file|is_dir|file_exists)\s*\(\s*[\'\"]([\/]|Windows|C:|D:|E:)/i', $line)) {
|
||
if (!preg_match('/ABSPATH|WPINC|WP_CONTENT_DIR|WP_PLUGIN_DIR|WPMU_PLUGIN_DIR|WP_CONTENT_URL|WP_PLUGIN_URL|WPMU_PLUGIN_URL/i', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'hardcoded_absolute_path',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Hardcoded absolute path detected. Use WordPress path functions.'
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/\$[a-zA-Z_]+\s*\.\s*[\'\"]([\/\\]?)[a-zA-Z_]/', $line)) {
|
||
if (!preg_match('/DIRECTORY_SEPARATOR|DS|plugin_dir_path|template_directory|get_template_directory|dirname\(realpath/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'missing_separator',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Path concatenation without proper directory separator.'
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/(plugin_dir_path|plugin_dir_url|template_directory|get_template_directory).*__FILE__/', $line) && !preg_match('/__FILE__.*dirname/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'incorrect_file_usage',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => '__FILE__ returns the file path. Use dirname(__FILE__) for directory paths.'
|
||
];
|
||
}
|
||
|
||
if (preg_match('/(include|require|fopen|file_get_contents|file_put_contents|unlink|mkdir|rmdir)\s*\(\s*(\$[_A-Z]+|\$_(GET|POST|REQUEST|COOKIE))/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'path_traversal_risk',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Direct user input used in file operation. Must validate and sanitize.'
|
||
];
|
||
}
|
||
|
||
if (preg_match('/(include|require)\s*\(\s*[\'\"][^\'\"]*\.(php|inc)/', $line)) {
|
||
if (!preg_match('/plugin_dir_path|plugin_dir_url|locate_template|get_template_directory|get_stylesheet_directory|dirname\(__FILE__\)|ABSPATH/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'missing_wp_path_function',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Use plugin_dir_path(), template_directory(), or similar WP functions.'
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/[A-Za-z]:\\\\\\\/', $line) && !preg_match('/DIRECTORY_SEPARATOR|DS|dirname|realpath|ABSPATH/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'windows_backslash',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Use forward slashes or DIRECTORY_SEPARATOR constant for cross-platform compatibility.'
|
||
];
|
||
}
|
||
|
||
if (preg_match('/\$[a-zA-Z_]+\s*=\s*\$[a-zA-Z_]+\s*\.\s*[\'\"]?\s*[\'\"]?\s*;\s*\$[a-zA-Z_]+/', $line) && preg_match('/(path|dir|file)/i', $line)) {
|
||
if (!preg_match('/DIRECTORY_SEPARATOR|DS|\/.*\//|\\\.*\\/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'incorrect_path_concat',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Incorrect path concatenation. Missing directory separator.'
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!empty($activationHooks)) {
|
||
foreach ($activationHooks as $hook) {
|
||
$hookLine = $hook['line'];
|
||
|
||
if (preg_match('/\[\s*(\$[a-zA-Z_]+|new\s+[A-Z][a-zA-Z0-9_]+\(\))\s*,\s*[\'\"](install|activate)[\'\"]/', $hook['content'])) {
|
||
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
|
||
$priorContent = implode("\n", $priorLines);
|
||
|
||
if (!preg_match('/class.*install|require|include|autoload/si', $priorContent)) {
|
||
$issues['activation'][] = [
|
||
'type' => 'install_class_not_loaded',
|
||
'file' => $relativePath,
|
||
'line' => $hookLine,
|
||
'message' => 'register_activation_hook references install class/object but class may not be loaded before hook registration.'
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/\[\s*[\'\"]([A-Z][a-zA-Z0-9_]*)[\'\"]\s*,\s*[\'\"](install|activate)[\'\"]/', $hook['content'], $matches)) {
|
||
$className = $matches[1];
|
||
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
|
||
$priorContent = implode("\n", $priorLines);
|
||
|
||
if (!preg_match('/class\s+' . preg_quote($className, '/') . '|require.*' . preg_quote($className, '/') . '|include.*' . preg_quote($className, '/') . '/si', $priorContent)) {
|
||
$issues['activation'][] = [
|
||
'type' => 'class_not_loaded',
|
||
'file' => $relativePath,
|
||
'line' => $hookLine,
|
||
'message' => "register_activation_hook references class '{$className}' but class file may not be loaded before hook registration."
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/([A-Z][a-zA-Z0-9_]*)::(install|activate)/', $hook['content'], $matches)) {
|
||
$className = $matches[1];
|
||
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
|
||
$priorContent = implode("\n", $priorLines);
|
||
|
||
if (!preg_match('/class\s+' . preg_quote($className, '/') . '|require.*' . preg_quote($className, '/') . '/si', $priorContent)) {
|
||
$issues['activation'][] = [
|
||
'type' => 'static_class_not_loaded',
|
||
'file' => $relativePath,
|
||
'line' => $hookLine,
|
||
'message' => "register_activation_hook references static class '{$className}' but class may not be loaded before hook registration."
|
||
];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!empty($issues['file_paths'])) {
|
||
echo "\nFILE PATH VALIDATION ISSUES:\n";
|
||
$uniquePathIssues = [];
|
||
|
||
foreach ($issues['file_paths'] as $issue) {
|
||
$key = "{$issue['type']}:{$issue['file']}:{$issue['line']}";
|
||
if (!isset($uniquePathIssues[$key])) {
|
||
$uniquePathIssues[$key] = $issue;
|
||
}
|
||
}
|
||
|
||
foreach ($uniquePathIssues as $issue) {
|
||
$anyIssues = true;
|
||
echo " FILE PATH ISSUE in {$issue['file']}:{$issue['line']}:\n";
|
||
echo " {$issue['message']}\n";
|
||
}
|
||
}
|
||
|
||
if (!empty($issues['activation'])) {
|
||
echo "\nACTIVATION HOOK VALIDATION ISSUES:\n";
|
||
$uniqueActivationIssues = [];
|
||
|
||
foreach ($issues['activation'] as $issue) {
|
||
$key = "{$issue['type']}:{$issue['file']}:{$issue['line']}";
|
||
if (!isset($uniqueActivationIssues[$key])) {
|
||
$uniqueActivationIssues[$key] = $issue;
|
||
}
|
||
}
|
||
|
||
foreach ($uniqueActivationIssues as $issue) {
|
||
$anyIssues = true;
|
||
echo " ACTIVATION HOOK ISSUE in {$issue['file']}:{$issue['line']}:\n";
|
||
echo " {$issue['message']}\n";
|
||
}
|
||
}
|
||
|
||
// Now run the additional enhanced runtime checks (undefined functions, early calls, arrays, css overlaps)
|
||
|
||
echo "Checking for undefined functions...\n";
|
||
$undefinedFunctionsFound = false;
|
||
|
||
foreach ($functionCalls as $fFile => $calls) {
|
||
foreach ($calls as $call) {
|
||
$funcName = $call['name'];
|
||
|
||
if (isInternalFunction($funcName)) {
|
||
continue;
|
||
}
|
||
|
||
// Check definition in same file
|
||
$definedInThisFile = isset($functionDefinitions[$fFile][$funcName]);
|
||
|
||
// Check global implementations
|
||
$isGloballyDefined = false;
|
||
foreach ($functionImplementations as $fullName => $impls) {
|
||
$parts = explode('\\', $fullName);
|
||
$shortName = end($parts);
|
||
if (strtolower($shortName) === strtolower($funcName)) {
|
||
$isGloballyDefined = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$definedInThisFile && !$isGloballyDefined) {
|
||
$undefinedFunctionsFound = true;
|
||
echo "UNDEFINED FUNCTION: '{$funcName}' called at {$fFile}:{$call['line']}\n";
|
||
$issues['undefined_functions'][] = [
|
||
'name' => $funcName,
|
||
'file' => $fFile,
|
||
'line' => $call['line']
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!$undefinedFunctionsFound) {
|
||
echo " ✓ No undefined functions found\n";
|
||
}
|
||
|
||
echo "\nChecking for early function calls...\n";
|
||
$earlyCallsFound = false;
|
||
|
||
foreach ($functionCalls as $fFile => $calls) {
|
||
if (!isset($functionDefinitions[$fFile])) continue;
|
||
foreach ($calls as $call) {
|
||
$funcName = $call['name'];
|
||
$callLine = $call['line'];
|
||
$defLine = null;
|
||
foreach ($functionDefinitions[$fFile] as $name => $line) {
|
||
if (strtolower($name) === strtolower($funcName)) { $defLine = $line; break; }
|
||
}
|
||
if ($defLine === null) continue;
|
||
if ($callLine > $defLine) continue;
|
||
if ($callLine < $defLine - 2) {
|
||
$earlyCallsFound = true;
|
||
echo "EARLY FUNCTION CALL: '{$funcName}' called at line {$callLine} but defined at line {$defLine} in {$fFile}\n";
|
||
$issues['early_function_calls'][] = [ 'name' => $funcName, 'file' => $fFile, 'call_line' => $callLine, 'def_line' => $defLine ];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!$earlyCallsFound) echo " ✓ No early function calls found\n";
|
||
|
||
echo "\nChecking for potential undefined arrays and array keys...\n";
|
||
$undefinedArraysFound = false;
|
||
|
||
$arrayInitializations = [];
|
||
foreach ($variableDeclarations as $fFile => $vars) {
|
||
foreach ($vars as $var) { $arrayInitializations[$fFile][] = $var['name']; }
|
||
}
|
||
|
||
foreach ($arrayKeyAccesses as $fFile => $accesses) {
|
||
foreach ($accesses as $access) {
|
||
$varName = $access['variable'];
|
||
$isInitialized = isset($arrayInitializations[$fFile]) && in_array($varName, $arrayInitializations[$fFile]);
|
||
if (!$isInitialized) {
|
||
$undefinedArraysFound = true;
|
||
echo "POTENTIAL UNDEFINED ARRAY: '\${$varName}' accessed as array at {$fFile}:{$access['line']}\n";
|
||
$issues['undefined_arrays'][] = [ 'variable' => $varName, 'file' => $fFile, 'line' => $access['line'] ];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!$undefinedArraysFound) echo " ✓ No potential undefined array issues found\n";
|
||
|
||
// Collect CSS and check for overlaps
|
||
$cssIterator = new RecursiveIteratorIterator(
|
||
new RecursiveDirectoryIterator($pluginDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||
RecursiveIteratorIterator::LEAVES_ONLY
|
||
);
|
||
|
||
foreach ($cssIterator as $cfile) {
|
||
if ($cfile->getExtension() === 'css') {
|
||
$cssFile = $cfile->getPathname();
|
||
if (!shouldExclude($cssFile, $excludeDirs)) {
|
||
$relativePath = str_replace($pluginDir . '/', '', $cssFile);
|
||
$cssFiles[$relativePath] = file_get_contents($cssFile);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty($cssFiles)) {
|
||
echo " ℹ No CSS files found to check for overlaps\n";
|
||
} else {
|
||
$overlapPatterns = [
|
||
'position:\s*absolute\s*;\s*z-index:\s*0|z-index:\s*0\s*;\s*position:\s*absolute' => 'Absolute positioned element at z-index 0 (may overlap)',
|
||
'position:\s*absolute\s*;[^}]*top:\s*0[^}]*bottom:\s*0' => 'Absolute element anchored to both top and bottom',
|
||
'position:\s*absolute\s*;[^}]*left:\s*0[^}]*right:\s*0' => 'Absolute element anchored to both left and right',
|
||
'position:\s*fixed\s*;[^}]*top:\s*0[^}]*bottom:\s*0' => 'Fixed element anchored to both top and bottom',
|
||
'position:\s*fixed\s*;[^}]*left:\s*0[^}]*right:\s*0' => 'Fixed element anchored to both left and right',
|
||
'margin-left:\s*-\d+px|margin-top:\s*-\d+px|margin-right:\s*-\d+px|margin-bottom:\s*-\d+px' => 'Negative margin (may cause overlap)',
|
||
];
|
||
|
||
foreach ($cssFiles as $cssPath => $cssContent) {
|
||
foreach ($overlapPatterns as $pattern => $description) {
|
||
if (preg_match_all('/' . $pattern . '/si', $cssContent, $matches, PREG_OFFSET_CAPTURE)) {
|
||
$lines = explode("\n", $cssContent);
|
||
foreach ($matches[0] as $match) {
|
||
$offset = $match[1];
|
||
$lineNumber = substr_count(substr($cssContent, 0, $offset), "\n") + 1;
|
||
echo "POTENTIAL CSS OVERLAP in {$cssPath}:{$lineNumber}\n";
|
||
echo " Reason: {$description}\n";
|
||
echo " Context: " . substr($match[0], 0, 100) . "...\n";
|
||
$issues['css_overlaps'][] = [ 'file' => $cssPath, 'line' => $lineNumber, 'pattern' => $pattern, 'description' => $description ];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty($issues['css_overlaps'])) {
|
||
echo " ✓ No potential CSS overlaps detected\n";
|
||
}
|
||
}
|
||
|
||
echo "\n";
|
||
|
||
// SUMMARY: report counts and decide exit status
|
||
$totalIssues = count($issues['undefined_functions']) + count($issues['early_function_calls']) + count($issues['undefined_arrays']) + count($issues['css_overlaps']);
|
||
|
||
if ($anyIssues || $totalIssues > 0) {
|
||
echo "\n" . str_repeat("=", 70) . "\n";
|
||
echo "VALIDATION SUMMARY\n";
|
||
echo str_repeat("=", 70) . "\n";
|
||
|
||
if ($anyIssues) {
|
||
echo "Runtime issues (duplicates, missing classes): FOUND\n";
|
||
}
|
||
|
||
if (count($issues['undefined_functions']) > 0) {
|
||
echo "Undefined functions: " . count($issues['undefined_functions']) . " found\n";
|
||
}
|
||
|
||
if (count($issues['early_function_calls']) > 0) {
|
||
echo "Early function calls: " . count($issues['early_function_calls']) . " found\n";
|
||
}
|
||
|
||
if (count($issues['undefined_arrays']) > 0) {
|
||
echo "Potential undefined arrays: " . count($issues['undefined_arrays']) . " found\n";
|
||
}
|
||
|
||
if (count($issues['css_overlaps']) > 0) {
|
||
echo "Potential CSS overlaps: " . count($issues['css_overlaps']) . " found\n";
|
||
}
|
||
|
||
echo "\nFAIL: Issues detected that should be reviewed\n";
|
||
exit(1);
|
||
}
|
||
|
||
// If we got here, no issues
|
||
if (!$quiet) {
|
||
if ($verbose) {
|
||
echo "No runtime error issues, file path problems, or activation hook issues found across {$stats['files']} files.\n";
|
||
echo "✓ No undefined functions\n";
|
||
echo "✓ No early function calls\n";
|
||
echo "✓ No potential CSS overlaps\n";
|
||
} else {
|
||
echo "PASS: No issues found.\n";
|
||
}
|
||
}
|
||
|
||
|
||
if (preg_match('/(include|require|fopen|file_get_contents|file_put_contents|is_file|is_dir|file_exists)\s*\(\s*[\'"]([\/]|Windows|C:|D:|E:)/i', $line)) {
|
||
if (!preg_match('/ABSPATH|WPINC|WP_CONTENT_DIR|WP_PLUGIN_DIR|WPMU_PLUGIN_DIR|WP_CONTENT_URL|WP_PLUGIN_URL|WPMU_PLUGIN_URL/i', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'hardcoded_absolute_path',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Hardcoded absolute path detected. Use WordPress path functions.'
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/\$[a-zA-Z_]+\s*\.\s*[\'"]([\/\\]?)[a-zA-Z_]/', $line)) {
|
||
if (!preg_match('/DIRECTORY_SEPARATOR|DS|plugin_dir_path|template_directory|get_template_directory|dirname\(realpath/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'missing_separator',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Path concatenation without proper directory separator.'
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/(plugin_dir_path|plugin_dir_url|template_directory|get_template_directory).*__FILE__/', $line) && !preg_match('/__FILE__.*dirname/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'incorrect_file_usage',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => '__FILE__ returns the file path. Use dirname(__FILE__) for directory paths.'
|
||
];
|
||
}
|
||
|
||
if (preg_match('/(include|require|fopen|file_get_contents|file_put_contents|unlink|mkdir|rmdir)\s*\(\s*(\$[_A-Z]+|\$_(GET|POST|REQUEST|COOKIE))/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'path_traversal_risk',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Direct user input used in file operation. Must validate and sanitize.'
|
||
];
|
||
}
|
||
|
||
if (preg_match('/(include|require)\s*\(\s*[\'"][^\'"]*\.(php|inc)/', $line)) {
|
||
if (!preg_match('/plugin_dir_path|plugin_dir_url|locate_template|get_template_directory|get_stylesheet_directory|dirname\(__FILE__\)|ABSPATH/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'missing_wp_path_function',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Use plugin_dir_path(), template_directory(), or similar WP functions.'
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/[A-Za-z]:\\\\\\\\/', $line) && !preg_match('/DIRECTORY_SEPARATOR|DS|dirname|realpath|ABSPATH/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'windows_backslash',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Use forward slashes or DIRECTORY_SEPARATOR constant for cross-platform compatibility.'
|
||
];
|
||
}
|
||
|
||
if (preg_match('/\$[a-zA-Z_]+\s*=\s*\$[a-zA-Z_]+\s*\.\s*[\'"]?\s*[\'"]?\s*;\s*\$[a-zA-Z_]+/', $line) && preg_match('/(path|dir|file)/i', $line)) {
|
||
if (!preg_match('/DIRECTORY_SEPARATOR|DS|\/.*\/|\\\\.*\\\\/', $line)) {
|
||
$issues['file_paths'][] = [
|
||
'type' => 'incorrect_path_concat',
|
||
'file' => $relativePath,
|
||
'line' => $lineNum,
|
||
'message' => 'Incorrect path concatenation. Missing directory separator.'
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!empty($activationHooks)) {
|
||
foreach ($activationHooks as $hook) {
|
||
$hookLine = $hook['line'];
|
||
|
||
if (preg_match('/\[\s*(\$[a-zA-Z_]+|new\s+[A-Z][a-zA-Z0-9_]+\(\))\s*,\s*[\'"](install|activate)[\'"]/', $hook['content'])) {
|
||
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
|
||
$priorContent = implode("\n", $priorLines);
|
||
|
||
if (!preg_match('/class.*install|require|include|autoload/si', $priorContent)) {
|
||
$issues['activation'][] = [
|
||
'type' => 'install_class_not_loaded',
|
||
'file' => $relativePath,
|
||
'line' => $hookLine,
|
||
'message' => 'register_activation_hook references install class/object but class may not be loaded before hook registration.'
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/\[\s*[\'"]([A-Z][a-zA-Z0-9_]*)[\'"]\s*,\s*[\'"](install|activate)[\'"]/', $hook['content'], $matches)) {
|
||
$className = $matches[1];
|
||
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
|
||
$priorContent = implode("\n", $priorLines);
|
||
|
||
if (!preg_match('/class\s+' . preg_quote($className, '/') . '|require.*' . preg_quote($className, '/') . '|include.*' . preg_quote($className, '/') . '/si', $priorContent)) {
|
||
$issues['activation'][] = [
|
||
'type' => 'class_not_loaded',
|
||
'file' => $relativePath,
|
||
'line' => $hookLine,
|
||
'message' => "register_activation_hook references class '{$className}' but class file may not be loaded before hook registration."
|
||
];
|
||
}
|
||
}
|
||
|
||
if (preg_match('/([A-Z][a-zA-Z0-9_]*)::(install|activate)/', $hook['content'], $matches)) {
|
||
$className = $matches[1];
|
||
$priorLines = array_slice($lines, max(0, $hookLine - 25), min(25, $hookLine));
|
||
$priorContent = implode("\n", $priorLines);
|
||
|
||
if (!preg_match('/class\s+' . preg_quote($className, '/') . '|require.*' . preg_quote($className, '/') . '/si', $priorContent)) {
|
||
$issues['activation'][] = [
|
||
'type' => 'static_class_not_loaded',
|
||
'file' => $relativePath,
|
||
'line' => $hookLine,
|
||
'message' => "register_activation_hook references static class '{$className}' but class may not be loaded before hook registration."
|
||
];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($cssIterator as $file) {
|
||
if ($file->getExtension() === 'css') {
|
||
$cssFile = $file->getPathname();
|
||
if (!shouldExclude($cssFile, $excludeDirs)) {
|
||
$relativePath = str_replace($pluginDir . '/', '', $cssFile);
|
||
$cssFiles[$relativePath] = file_get_contents($cssFile);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty($cssFiles)) {
|
||
echo " ℹ No CSS files found to check for overlaps\n";
|
||
} else {
|
||
// Check for CSS patterns that could cause overlapping elements
|
||
$overlapPatterns = [
|
||
'position:\s*absolute\s*;\s*z-index:\s*0|z-index:\s*0\s*;\s*position:\s*absolute' => 'Absolute positioned element at z-index 0 (may overlap)',
|
||
'position:\s*absolute\s*;[^}]*top:\s*0[^}]*bottom:\s*0' => 'Absolute element anchored to both top and bottom',
|
||
'position:\s*absolute\s*;[^}]*left:\s*0[^}]*right:\s*0' => 'Absolute element anchored to both left and right',
|
||
'position:\s*fixed\s*;[^}]*top:\s*0[^}]*bottom:\s*0' => 'Fixed element anchored to both top and bottom',
|
||
'position:\s*fixed\s*;[^}]*left:\s*0[^}]*right:\s*0' => 'Fixed element anchored to both left and right',
|
||
'margin-left:\s*-\d+px|margin-top:\s*-\d+px|margin-right:\s*-\d+px|margin-bottom:\s*-\d+px' => 'Negative margin (may cause overlap)',
|
||
];
|
||
|
||
foreach ($cssFiles as $cssPath => $cssContent) {
|
||
foreach ($overlapPatterns as $pattern => $description) {
|
||
if (preg_match_all('/' . $pattern . '/si', $cssContent, $matches, PREG_OFFSET_CAPTURE)) {
|
||
$lines = explode("\n", $cssContent);
|
||
foreach ($matches[0] as $match) {
|
||
$offset = $match[1];
|
||
$lineNumber = substr_count(substr($cssContent, 0, $offset), "\n") + 1;
|
||
$undefinedArraysFound = true;
|
||
echo "POTENTIAL CSS OVERLAP in {$cssPath}:{$lineNumber}\n";
|
||
echo " Reason: {$description}\n";
|
||
echo " Context: " . substr($match[0], 0, 100) . "...\n";
|
||
$issues['css_overlaps'][] = [
|
||
'file' => $cssPath,
|
||
'line' => $lineNumber,
|
||
'pattern' => $pattern,
|
||
'description' => $description
|
||
];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
if (!empty($issues['file_paths'])) {
|
||
echo "\nFILE PATH VALIDATION ISSUES:\n";
|
||
$uniquePathIssues = [];
|
||
|
||
foreach ($issues['file_paths'] as $issue) {
|
||
$key = "{$issue['type']}:{$issue['file']}:{$issue['line']}";
|
||
if (!isset($uniquePathIssues[$key])) {
|
||
$uniquePathIssues[$key] = $issue;
|
||
}
|
||
}
|
||
|
||
foreach ($uniquePathIssues as $issue) {
|
||
$anyIssues = true;
|
||
echo " FILE PATH ISSUE in {$issue['file']}:{$issue['line']}:\n";
|
||
echo " {$issue['message']}\n";
|
||
}
|
||
}
|
||
|
||
if (!empty($issues['activation'])) {
|
||
echo "\nACTIVATION HOOK VALIDATION ISSUES:\n";
|
||
$uniqueActivationIssues = [];
|
||
|
||
foreach ($issues['activation'] as $issue) {
|
||
$key = "{$issue['type']}:{$issue['file']}:{$issue['line']}";
|
||
if (!isset($uniqueActivationIssues[$key])) {
|
||
$uniqueActivationIssues[$key] = $issue;
|
||
}
|
||
}
|
||
|
||
foreach ($uniqueActivationIssues as $issue) {
|
||
$anyIssues = true;
|
||
echo " ACTIVATION HOOK ISSUE in {$issue['file']}:{$issue['line']}:\n";
|
||
echo " {$issue['message']}\n";
|
||
}
|
||
|
||
|
||
if (!$undefinedArraysFound) {
|
||
echo " ✓ No potential CSS overlaps detected\n";
|
||
}
|
||
}
|
||
echo "\n";
|
||
|
||
// ============================================
|
||
// SUMMARY
|
||
// ============================================
|
||
$totalIssues = count($issues['undefined_functions']) +
|
||
count($issues['early_function_calls']) +
|
||
count($issues['undefined_arrays']) +
|
||
count($issues['css_overlaps']);
|
||
|
||
if ($anyIssues || $totalIssues > 0) {
|
||
echo "\n" . str_repeat("=", 70) . "\n";
|
||
echo "VALIDATION SUMMARY\n";
|
||
echo str_repeat("=", 70) . "\n";
|
||
|
||
if ($anyIssues) {
|
||
echo "Runtime issues (duplicates, missing classes): FOUND\n";
|
||
}
|
||
|
||
if (count($issues['undefined_functions']) > 0) {
|
||
echo "Undefined functions: " . count($issues['undefined_functions']) . " found\n";
|
||
}
|
||
|
||
if (count($issues['early_function_calls']) > 0) {
|
||
echo "Early function calls: " . count($issues['early_function_calls']) . " found\n";
|
||
}
|
||
|
||
if (count($issues['undefined_arrays']) > 0) {
|
||
echo "Potential undefined arrays: " . count($issues['undefined_arrays']) . " found\n";
|
||
}
|
||
|
||
if (count($issues['css_overlaps']) > 0) {
|
||
echo "Potential CSS overlaps: " . count($issues['css_overlaps']) . " found\n";
|
||
}
|
||
|
||
echo "\nFAIL: Issues detected that should be reviewed\n";
|
||
exit(1);
|
||
}
|
||
|
||
if (!$anyIssues) {
|
||
if (!$quiet) {
|
||
if ($verbose) {
|
||
echo "No runtime error issues, file path problems, or activation hook issues found across {$stats['files']} files.\n";
|
||
echo "✓ No undefined functions\n";
|
||
echo "✓ No early function calls\n";
|
||
echo "✓ No potential CSS overlaps\n";
|
||
} else {
|
||
echo "PASS: No issues found.\n";
|
||
}
|
||
}
|
||
exit(0);
|
||
}
|
||
|
||
exit(1);
|