<?php

declare(strict_types=1);

/**
 * KiyoKart forbidden function scanner.
 *
 * Usage:
 *   php tools/security/forbidden-functions-scan.php
 */

$forbidden = [
    // Critical: command execution
    'exec',
    'shell_exec',
    'system',
    'passthru',
    'popen',
    'proc_open',
    'pcntl_exec',

    // High: process control in web apps
    'pcntl_alarm',
    'pcntl_fork',
    'pcntl_wait',
    'pcntl_waitpid',
    'pcntl_signal',
    'pcntl_signal_dispatch',

    // High: code execution
    'eval',
    'assert',
    'create_function',

    // High: filesystem link manipulation
    'link',
    'symlink',
    'readlink',

    // Info disclosure / env mutation
    'phpinfo',
    'getenv',
    'putenv',
];

$scanRoots = [
    __DIR__ . '/../../app',
    __DIR__ . '/../../routes',
    __DIR__ . '/../../config',
    __DIR__ . '/../../database',
    __DIR__ . '/../../bootstrap',
    __DIR__ . '/../../public',
    __DIR__ . '/../../tools',
];

$skipPaths = [
    DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR,
    DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR,
];

$files = getPhpFiles($scanRoots, $skipPaths);

if (empty($files)) {
    fwrite(STDOUT, "No PHP files found to scan.\n");
    exit(0);
}

$violations = [];

foreach ($files as $file) {
    $code = @file_get_contents($file);
    if ($code === false) {
        continue;
    }

    $tokens = token_get_all($code);
    $line = 1;
    $tokenCount = count($tokens);

    for ($i = 0; $i < $tokenCount; $i++) {
        $token = $tokens[$i];

        if (is_array($token)) {
            $line = $token[2];
        }

        // Match standard function calls parsed as T_STRING
        if (is_array($token) && $token[0] === T_STRING) {
            $name = strtolower($token[1]);
            if (!in_array($name, $forbidden, true)) {
                continue;
            }

            if (isFunctionCall($tokens, $i)) {
                $violations[] = [
                    'file' => normalizePath($file),
                    'line' => $line,
                    'function' => $name,
                ];
            }
        }
    }
}

if (empty($violations)) {
    fwrite(STDOUT, "OK: no forbidden functions detected.\n");
    exit(0);
}

fwrite(STDERR, "ERROR: forbidden PHP function usage detected:\n");
foreach ($violations as $v) {
    fwrite(
        STDERR,
        sprintf(" - %s:%d -> %s()\n", $v['file'], $v['line'], $v['function'])
    );
}

exit(1);

function isFunctionCall(array $tokens, int $index): bool
{
    // Ensure previous meaningful token is not object/static access
    for ($p = $index - 1; $p >= 0; $p--) {
        $prev = $tokens[$p];
        if (is_array($prev) && in_array($prev[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
            continue;
        }
        if (!is_array($prev) && ($prev === '->' || $prev === '::')) {
            return false;
        }
        break;
    }

    // Next meaningful token must be "("
    $count = count($tokens);
    for ($n = $index + 1; $n < $count; $n++) {
        $next = $tokens[$n];
        if (is_array($next) && in_array($next[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
            continue;
        }
        return $next === '(';
    }

    return false;
}

function getPhpFiles(array $roots, array $skipPaths): array
{
    $result = [];

    foreach ($roots as $root) {
        if (!is_dir($root)) {
            continue;
        }

        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)
        );

        /** @var SplFileInfo $fileInfo */
        foreach ($iterator as $fileInfo) {
            if (!$fileInfo->isFile()) {
                continue;
            }
            if (strtolower($fileInfo->getExtension()) !== 'php') {
                continue;
            }

            $path = $fileInfo->getPathname();
            $skip = false;
            foreach ($skipPaths as $segment) {
                if (str_contains($path, $segment)) {
                    $skip = true;
                    break;
                }
            }
            if ($skip) {
                continue;
            }

            $result[] = $path;
        }
    }

    sort($result);
    return $result;
}

function normalizePath(string $path): string
{
    $root = realpath(__DIR__ . '/../../');
    if ($root === false) {
        return str_replace('\\', '/', $path);
    }

    $root = str_replace('\\', '/', $root);
    $path = str_replace('\\', '/', $path);

    if (str_starts_with($path, $root . '/')) {
        return substr($path, strlen($root) + 1);
    }

    return $path;
}

