Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Cli/Runner.php
<?php

namespace XF\Cli;

use
Symfony\Component\Console\Application as ConsoleApplication;
use
Symfony\Component\Console\Command\Command;
use
Symfony\Component\Console\Formatter\OutputFormatterStyle;
use
Symfony\Component\Console\Input\ArgvInput;
use
Symfony\Component\Console\Output\ConsoleOutput;
use
Symfony\Component\Console\Output\OutputInterface;
use
XF\Cli\Command\AllowInactiveAddOnCommandInterface;
use
XF\Cli\Command\CustomAppCommandInterface;

use function
get_class, strlen, strval;

class
Runner
{
    public function
run()
    {
        if (
PHP_SAPI != 'cli')
        {
            die(
'This script can only be run via the command line interface.');
        }

       
// CLI requests can run for longer than a web request, so we don't want to get tripped up by memory issues
       
\XF::setMemoryLimit(-1);
        @
set_time_limit(0);

       
$console = new ConsoleApplication('XenForo', \XF::$version);

       
$this->registerCommands($console);

       
$input = new ArgvInput();
       
$output = new ConsoleOutput();
       
$output->getFormatter()->setStyle('warning', new OutputFormatterStyle('black', 'yellow'));

       
$console->setCatchExceptions(false);
       
$console->setAutoExit(false);

        try
        {
           
$name = $input->getFirstArgument() ?: 'list';
           
$command = $console->find($name);

            if (
$command instanceof CustomAppCommandInterface)
            {
               
$appClass = $command::getCustomAppClass();
            }
            else
            {
               
$appClass = 'XF\Cli\App';
            }

           
$app = \XF::setupApp($appClass);
           
$app->start();

           
$forceCliUser = $app->config('forceCliUser');
            if (
$forceCliUser)
            {
                if (!
is_callable('posix_getuid'))
                {
                   
$output->writeln('<error>POSIX functions are not available. Cannot use forceCliUser.</error>');
                    exit(
1);
                }

               
$user = posix_getpwnam($forceCliUser);
                if (!
$user)
                {
                   
$output->writeln('<error>The forceCliUser set in your config.php file does not exist.</error>');
                    exit(
1);
                }

               
$runUserId = posix_getuid();

                if (
$user['uid'] != $runUserId)
                {
                    if (
$runUserId != 0)
                    {
                       
// Running as a different user than the expected account and we're not root, so switching
                        // isn't possible. Trigger an error instead.
                       
$output->writeln('<error>Cannot switch to forceCliUser value. Run commands as ' . $forceCliUser . ' or root.</error>');
                        exit(
1);
                    }

                   
posix_setgid($user['gid']);
                   
posix_setuid($user['uid']);
                }
            }

            if (!
$this->canRunCommand($command, $error))
            {
                if (!
$error)
                {
                   
$error = 'This command cannot be run.';
                }

               
$output->writeln(sprintf('<error>%s</error>', $error));
                exit(
1);
            }
        }
        catch (\
Symfony\Component\Console\Exception\CommandNotFoundException $e)
        {
           
$output->writeln($e->getMessage());
            exit;
        }

       
$exitCode = 0;

        try
        {
           
$exitCode = $console->run($input, $output);
           
$this->postExecutionCleanUp($output);
        }
        catch (\
Symfony\Component\Console\Exception\RuntimeException $e)
        {
           
// this will usually indicate that they passed unexpected arguments in or had a local problem of some sort
           
$output->writeln($e->getMessage());
           
$exitCode = 1;
        }
        catch (\
XF\PrintableException $e)
        {
           
$output->writeln($e->getMessage());
           
$exitCode = 1;
        }
        catch (\
Exception $e)
        {
            \
XF::logException($e, true); // exiting so rollback
           
$console->renderException($e, $output);
           
$this->outputAdditionalInfo($e, $output);
           
$exitCode = 1;
        }
        catch (\
Throwable $e)
        {
            \
XF::logException($e, true); // exiting so rollback
           
$console->renderException(
                new \
ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine(), $e),
               
$output)
            ;
           
$this->outputAdditionalInfo($e, $output);
           
$exitCode = 1;
        }

        if (
$exitCode > 255)
        {
           
$exitCode = 255;
        }

        exit(
$exitCode);
    }

   
/**
     * @param string|null $error
     */
   
protected function canRunCommand(Command $command, &$error = null): bool
   
{
       
$commandAddOn = $this->getAddOnForCommand($command);
        if (
           
$commandAddOn && $commandAddOn !== 'XF' &&
            !\
XF::isAddOnActive($commandAddOn) &&
            !(
$command instanceof AllowInactiveAddOnCommandInterface)
        )
        {
           
$error = "This command belongs to an inactive add-on ($commandAddOn). You must activate this add-on before running this command.";
            return
false;
        }

        return
true;
    }

   
/**
     * @return string|null
     */
   
protected function getAddOnForCommand(Command $command)
    {
        if (!
preg_match(
           
'/^(.+)\\\\Cli\\\\Command\\\\/',
           
get_class($command),
           
$matches
       
))
        {
            return
null;
        }

        return
str_replace('\\', '/', $matches[1]);
    }

    protected function
outputAdditionalInfo($e, OutputInterface $output)
    {
       
$messages = [];

        if (
$e instanceof \XF\Db\Exception && $e->query)
        {
           
$messages[] = '<comment>Exception query:</comment>';

           
$emptyLine = sprintf('<error>  %s  </error>', str_repeat(' ', strlen($e->query)));

           
$messages[] = $emptyLine;
           
$messages[] = sprintf('<error>  %s  </error>', $e->query);
           
$messages[] = $emptyLine;
           
$messages[] = '';
        }

       
$output->writeln($messages);
    }

    protected function
postExecutionCleanUp(OutputInterface $output = null)
    {
        \
XF::triggerRunOnce();

       
$app = \XF::app();

        if (
$app->container()->isCached('job.manager'))
        {
           
$jobManager = $app->jobManager();

            if (
$jobManager->hasManualEnqueued())
            {
               
$output->writeln(['', 'Running clean up tasks...']);

               
$manualJobs = array_keys($jobManager->getManualEnqueued());
                foreach (
$manualJobs AS $manualJobId)
                {
                    while (
$runner = $jobManager->runById($manualJobId, \XF::config('jobMaxRunTime')))
                    {
                        if (
$output)
                        {
                           
$output->writeln(strval($runner->statusMessage));
                        }

                       
// keep the memory limit down on long running jobs
                       
$app->em()->clearEntityCache();
                        \
XF::updateTime();

                        if (
$runner->continueDate)
                        {
                           
// job is finished but set to resume in the future
                           
break;
                        }
                    }
                }
            }
        }
    }

    protected function
registerCommands(ConsoleApplication $app)
    {
       
$directoryMap = $this->getCommandDirectoryMap();
       
$classes = $this->getValidCommandClasses($directoryMap);
        foreach (
$classes AS $class)
        {
           
$app->add(new $class());
        }
    }

    protected function
getCommandDirectoryMap()
    {
       
$dirs = [
            \
XF::getSourceDirectory() . '/XF/Cli/Command' => 'XF\Cli\Command'
       
];

       
$addOnIds = [];
       
$addOnBaseDir = \XF::getAddOnDirectory();

        foreach (new \
DirectoryIterator($addOnBaseDir) AS $entry)
        {
            if (!
$this->isPotentialAddOnDirectory($entry))
            {
                continue;
            }

            if (
$this->isAddOnRootDirectory($entry))
            {
               
$addOnIds[] = $entry->getBasename();
            }
            else
            {
               
$vendorPrefix = $entry->getBasename();
                foreach (new \
DirectoryIterator($entry->getPathname()) AS $addOnDir)
                {
                    if (!
$this->isPotentialAddOnDirectory($addOnDir))
                    {
                        continue;
                    }

                    if (
$this->isAddOnRootDirectory($addOnDir))
                    {
                       
$addOnIds[] = "$vendorPrefix/{$addOnDir->getBasename()}";
                    }
                }
            }
        }

        foreach (
$addOnIds AS $addOnId)
        {
           
$searchPath = $addOnBaseDir . '/' . $addOnId . '/Cli/Command';
           
$classBase = '\\' . str_replace('/', '\\', $addOnId) . '\Cli\Command';

           
$dirs[$searchPath] = $classBase;
        }

        return
$dirs;
    }

    protected function
isPotentialAddOnDirectory(\DirectoryIterator $entry)
    {
        if (!
$entry->isDir()
            ||
$entry->isDot()
            || !
preg_match('/^[a-z0-9_]+$/i', $entry->getBasename())
        )
        {
            return
false;
        }
        else
        {
            return
true;
        }
    }

    protected function
isAddOnRootDirectory(\DirectoryIterator $entry)
    {
       
$ds = \XF::$DS;

       
$pathname = $entry->getPathname();
       
$addOnJson = "{$pathname}{$ds}addon.json";
       
$outputDir = "{$pathname}{$ds}_output";
       
$dataDir = "{$pathname}{$ds}_data";

        if (
file_exists($addOnJson) || file_exists($outputDir) || file_exists($dataDir))
        {
            return
true;
        }
        else
        {
            return
false;
        }
    }

    protected function
getValidCommandClasses(array $directoryMap)
    {
       
$classes = [];

        foreach (
$directoryMap AS $dir => $baseClass)
        {
           
$fullPath = \XF\Util\File::canonicalizePath($dir);
            if (!
file_exists($fullPath) || !is_dir($fullPath))
            {
                continue;
            }

           
$iterator = new \RecursiveCallbackFilterIterator(
                new \
RecursiveDirectoryIterator($fullPath),
                function(\
SplFileInfo $entry, $key, \RecursiveIterator $iterator)
                {
                    if (
$iterator->hasChildren())
                    {
                        return
true;
                    }

                    return (
$entry->isFile() && $entry->getExtension() == 'php');
                }
            );
            foreach (new \
RecursiveIteratorIterator($iterator) AS $file)
            {
               
/** @var \DirectoryIterator $file */
               
$localPath = str_replace($fullPath, '', $file->getPathname());
               
$localPath = trim(str_replace('\\', '/', $localPath), '/');

               
$className = $baseClass . '\\' . str_replace('/', '\\', $localPath);
               
$className = preg_replace('/\.php$/', '', $className);

                if (
$this->isValidCommandClass($className))
                {
                   
$classes[] = $className;
                }
            }
        }

        return
$classes;
    }

    protected function
isValidCommandClass($class)
    {
        if (!
class_exists($class))
        {
            return
false;
        }

       
$reflection = new \ReflectionClass($class);
        return (
           
$reflection->isInstantiable()
            &&
$reflection->isSubclassOf('Symfony\Component\Console\Command\Command')
        );
    }
}