<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Mvc\Cli\Symfony\Descriptor;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;

/**
 * Text descriptor.
 *
 * @author Jean-François Simon <contact@jfsimon.fr>
 *
 * @internal
 */
class TextDescriptor extends \Symfony\Component\Console\Descriptor\TextDescriptor
{
    /**
     * {@inheritdoc}
     */
    protected function describeInputArgument(InputArgument $argument, array $options = [])
    {
        $totalWidth = isset($options['total_width']) ? $options['total_width'] : Helper::strlen($argument->getName());
        $spacingWidth = $totalWidth - strlen($argument->getName());

        // + 4 = 2 spaces before <info>, 2 spaces after </info>
        $indent = $totalWidth + 4;
        if (null !== $argument->getDefault() && (!is_array($argument->getDefault()) || count($argument->getDefault()))) {
            $default = "\n" . str_repeat(' ', $indent) . sprintf('<comment> • [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
        } else {
            $default = '';
        }

        $maxWidth = isset($options['screen_width']) ? ($options['screen_width'] - $indent) : null;
        $this->writeText(sprintf(
            '  <info>%s</info>  %s%s%s%s',
            $argument->getName(),
            str_repeat(' ', $spacingWidth),
            $this->wordWrap($argument->getDescription(), $indent, $maxWidth),
            $default,
            $argument->isArray() ? "\n" . str_repeat(' ', $indent) . '<comment> • (can be specified multiple times)</comment>' : ''
        ), $options);
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputOption(InputOption $option, array $options = [])
    {
        $totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
        // + 4 = 2 spaces before <info>, 2 spaces after </info>
        $indent = $totalWidth + 4;

        if ($option->acceptValue() && null !== $option->getDefault() && (!is_array($option->getDefault()) || count($option->getDefault()))) {
            $default = "\n" . str_repeat(' ', $indent) . sprintf('<comment> • [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
        } else {
            $default = '';
        }

        $value = '';
        if ($option->acceptValue()) {
            $value = '=' . strtoupper($option->getName());

            if ($option->isValueOptional()) {
                $value = '[' . $value . ']';
            }
        }

        $synopsis = sprintf(
            '%s%s',
            $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : '    ',
            sprintf('--%s%s', $option->getName(), $value)
        );

        $spacingWidth = $totalWidth - Helper::strlen($synopsis);
        $maxWidth = isset($options['screen_width']) ? ($options['screen_width'] - $indent) : null;
        $this->writeText(sprintf(
            '  <info>%s</info>  %s%s%s%s',
            $synopsis,
            str_repeat(' ', $spacingWidth),
            $this->wordWrap($option->getDescription(), $indent, $maxWidth),
            $default,
            $option->isArray() ? "\n" . str_repeat(' ', $indent) . '<comment> • (can be specified multiple times)</comment>' : ''
        ), $options);
    }

    /**
     * {@inheritdoc}
     * @throws \Symfony\Component\Console\Exception\InvalidArgumentException
     */
    protected function describeCommand(Command $command, array $options = [])
    {
        $command->getSynopsis(true);
        $command->getSynopsis(false);
        $command->mergeApplicationDefinition(false);

        $this->writeText('<comment>Usage:</comment>', $options);
        foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
            $this->writeText("\n");
            $this->writeText('  ' . $usage, $options);
        }
        $this->writeText("\n");

        $definition = $command->getNativeDefinition();

        if ($definition->getOptions() || $definition->getArguments()) {
            $this->writeText("\n");
            $this->describeInputDefinition($definition, $options);
            $this->writeText("\n");
        }

        if ($help = $command->getProcessedHelp()) {
            $this->writeText("\n");
            $this->writeText('<comment>Help:</comment>', $options);
            $this->writeText("\n");
            $this->writeText('  ' . str_replace("\n", "\n  ", $help), $options);
            $this->writeText("\n");
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function describeApplication(Application $application, array $options = [])
    {
        $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
        $description = new ApplicationDescription($application, $describedNamespace);

        if (isset($options['raw_text']) && $options['raw_text']) {
            $width = $this->getColumnWidth($description->getCommands());

            foreach ($description->getCommands() as $command) {
                $this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
                $this->writeText("\n");
            }
        } else {
            if ('' !== $help = $application->getHelp()) {
                $this->writeText("$help\n\n", $options);
            }

            $this->writeText("<comment>Usage:</comment>\n", $options);
            $this->writeText("  command [options] [arguments]\n\n", $options);

            $this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options);

            $this->writeText("\n");
            $this->writeText("\n");

            $commands = $description->getCommands();

            if ($application instanceof \Helhum\Typo3Console\Mvc\Cli\Symfony\Application) {
                $showUnavailable = $options['show_unavailable'] ?? false;
                $commands = array_filter(
                    $commands,
                    function ($command) use ($application, $showUnavailable) {
                        return $showUnavailable || $application->isCommandAvailable($command);
                    }
                );
            }
            $namespaces = $description->getNamespaces();
            if ($describedNamespace && $namespaces) {
                // make sure all alias commands are included when describing a specific namespace
                $describedNamespaceInfo = reset($namespaces);
                foreach ($describedNamespaceInfo['commands'] as $name) {
                    $commands[$name] = $description->getCommand($name);
                }
            }

            // calculate max. width based on available commands per namespace
            $width = $this->getColumnWidth(call_user_func_array('array_merge', array_map(function ($namespace) use ($commands) {
                return array_intersect($namespace['commands'], array_keys($commands));
            }, $namespaces)));

            if ($describedNamespace) {
                $this->writeText(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
            } else {
                $this->writeText('<comment>Available commands:</comment>', $options);
            }

            foreach ($namespaces as $namespace) {
                $namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) {
                    return isset($commands[$name]);
                });

                if (!$namespace['commands']) {
                    continue;
                }

                if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
                    $this->writeText("\n");
                    $this->writeText(' <comment>' . $namespace['id'] . '</comment>', $options);
                }

                // Two spaces before <info>
                $indent = $width + 2;
                $maxWidth = isset($options['screen_width']) ? ($options['screen_width'] - $indent) : null;
                foreach ($namespace['commands'] as $name) {
                    $this->writeText("\n");
                    $spacingWidth = $width - Helper::strlen($name);
                    $command = $commands[$name];
                    $this->writeText(sprintf('  <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $this->wordWrap((string)$command->getDescription(), $indent, $maxWidth)), $options);
                }
            }

            $this->writeText("\n");
        }
    }

    /**
     * Wraps a text and adds indentation to new lines
     *
     * @param string $stringToWrap
     * @param int $indent
     * @param int|null $maxWidth
     * @return string
     */
    private function wordWrap(string $stringToWrap, int $indent, $maxWidth): string
    {
        $wrapped = $maxWidth === null ? $stringToWrap : wordwrap($stringToWrap, $maxWidth, "\n", true);

        return preg_replace('/\s*[\r\n]\s*/', "\n" . str_repeat(' ', $indent), $wrapped);
    }

    // Copied private methods

    /**
     * Formats input option/argument default value.
     *
     * @param mixed $default
     *
     * @return string
     */
    private function formatDefaultValue($default)
    {
        if (is_string($default)) {
            $default = OutputFormatter::escape($default);
        } elseif (is_array($default)) {
            foreach ($default as $key => $value) {
                if (is_string($value)) {
                    $default[$key] = OutputFormatter::escape($value);
                }
            }
        }

        return str_replace('\\\\', '\\', json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
    }

    /**
     * {@inheritdoc}
     */
    private function writeText($content, array $options = [])
    {
        $this->write(
            isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
            !isset($options['raw_output']) || !$options['raw_output']
        );
    }

    /**
     * @param (Command|string)[] $commands
     *
     * @return int
     */
    private function getColumnWidth(array $commands)
    {
        $widths = [];

        foreach ($commands as $command) {
            if ($command instanceof Command) {
                $widths[] = Helper::strlen($command->getName());
                foreach ($command->getAliases() as $alias) {
                    $widths[] = Helper::strlen($alias);
                }
            } else {
                $widths[] = Helper::strlen($command);
            }
        }

        return $widths ? max($widths) + 2 : 0;
    }

    /**
     * @param InputOption[] $options
     *
     * @return int
     */
    private function calculateTotalWidthForOptions($options)
    {
        $totalWidth = 0;
        foreach ($options as $option) {
            // "-" + shortcut + ", --" + name
            $nameLength = 1 + max(Helper::strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName());

            if ($option->acceptValue()) {
                $valueLength = 1 + Helper::strlen($option->getName()); // = + value
                $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]

                $nameLength += $valueLength;
            }
            $totalWidth = max($totalWidth, $nameLength);
        }

        return $totalWidth;
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Command;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Mvc\Controller\CommandController;
use Symfony\Component\Process\PhpProcess;

class FrontendCommandController extends CommandController
{
    /**
     * Submit frontend request
     *
     * Submits a frontend request to TYPO3 on the specified URL.
     *
     * @param string $requestUrl URL to make a frontend request.
     */
    public function requestCommand($requestUrl)
    {
        // TODO: this needs heavy cleanup!
        $template = file_get_contents(__DIR__ . '/../../../Resources/Private/Templates/request.tpl');
        $arguments = [
            'documentRoot' => getenv('TYPO3_PATH_WEB') ?: PATH_site,
            'requestUrl' => $this->makeAbsolute($requestUrl),
        ];
        // No other solution atm than to fake a CLI request type
        $code = str_replace('{arguments}', var_export($arguments, true), $template);
        $process = new PhpProcess($code, null, null, 0);
        $process->mustRun();
        $rawResponse = json_decode($process->getOutput());
        if ($rawResponse === null || $rawResponse->status === 'failure') {
            $this->outputLine('<error>An error occurred while trying to request the specified URL.</error>');
            $this->outputLine(sprintf('<error>Error: %s</error>', !empty($rawResponse->error) ? $rawResponse->error : 'Could not decode response. Please check your error log!'));
            $this->outputLine(sprintf('<error>Content: %s</error>', $process->getOutput()));
            $this->quit(1);
        }

        $this->output($rawResponse->content);
    }

    /**
     * Make URL absolute, so that the core fake frontend request bootstrap
     * correctly configures the environment for trusted host pattern.
     *
     * @param string $url
     * @return string
     */
    protected function makeAbsolute($url)
    {
        $parsedUrl = parse_url($url);

        $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] : 'http';
        $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : 'localhost';
        $path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/';
        $query = isset($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '';

        return $scheme . '://' . $host . '/' . ltrim($path, '/') . $query;
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Install\Upgrade;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Mvc\Cli\ConsoleOutput;

/**
 * Renders results of executed upgrade wizards
 */
class UpgradeWizardResultRenderer
{
    /**
     * Renders a table for upgrade wizards
     *
     * @param UpgradeWizardResult[] $upgradeWizardResults
     * @param ConsoleOutput $output
     */
    public function render(array $upgradeWizardResults, ConsoleOutput $output)
    {
        if (empty($upgradeWizardResults)) {
            return;
        }
        foreach ($upgradeWizardResults as $identifier => $result) {
            $output->outputLine();
            if (!$result->hasPerformed()) {
                $output->outputLine('<warning>Skipped upgrade wizard "%s" because it was not scheduled for execution or marked as done.</warning>', [$identifier]);
            } else {
                $output->outputLine('Successfully executed upgrade wizard "%s".', [$identifier]);
            }
            if (!empty($messages = array_filter($result->getMessages()))) {
                $this->printMessages($messages, 'Messages', $output);
            }
            if (!empty($queries = array_filter($result->getSqlQueries()))) {
                $this->printMessages($queries, 'SQL Queries executed', $output);
            }
        }
    }

    private function printMessages(array $messages, string $title, ConsoleOutput $output)
    {
        $output->outputLine('<info>%s:</info>', [$title]);
        foreach ($messages as $message) {
            $output->outputLine(html_entity_decode(strip_tags($message)));
        }
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Command;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Annotation\Command\Definition;
use Helhum\Typo3Console\Install\Upgrade\UpgradeHandling;
use Helhum\Typo3Console\Install\Upgrade\UpgradeWizardListRenderer;
use Helhum\Typo3Console\Install\Upgrade\UpgradeWizardResultRenderer;
use Helhum\Typo3Console\Mvc\Controller\CommandController;
use TYPO3\CMS\Core\Package\Exception\UnknownPackageException;

class UpgradeCommandController extends CommandController
{
    /**
     * @var UpgradeHandling
     */
    private $upgradeHandling;

    /**
     * @param UpgradeHandling|null $upgradeHandling
     */
    public function __construct(
        UpgradeHandling $upgradeHandling = null
    ) {
        $this->upgradeHandling = $upgradeHandling ?: new UpgradeHandling();
    }

    /**
     * Check TYPO3 version constraints of extensions
     *
     * This command is especially useful **before** switching sources to a new TYPO3 version.
     * It checks the version constraints of all third party extensions against a given TYPO3 version.
     * It therefore relies on the constraints to be correct.
     *
     * @param array $extensionKeys Extension keys to check. Separate multiple extension keys with comma.
     * @param string $typo3Version TYPO3 version to check against. Defaults to current TYPO3 version.
     * @Definition\Argument(name="extensionKeys")
     */
    public function checkExtensionConstraintsCommand(array $extensionKeys = [], $typo3Version = TYPO3_version)
    {
        if (empty($extensionKeys)) {
            $failedPackageMessages = $this->upgradeHandling->matchAllExtensionConstraints($typo3Version);
        } else {
            $failedPackageMessages = [];
            foreach ($extensionKeys as $extensionKey) {
                try {
                    if (!empty($result = $this->upgradeHandling->matchExtensionConstraints($extensionKey, $typo3Version))) {
                        $failedPackageMessages[$extensionKey] = $result;
                    }
                } catch (UnknownPackageException $e) {
                    $this->outputLine('<warning>Extension "%s" is not found in the system</warning>', [$extensionKey]);
                }
            }
        }
        foreach ($failedPackageMessages as $constraintMessage) {
            $this->outputLine('<error>%s</error>', [$constraintMessage]);
        }
        if (empty($failedPackageMessages)) {
            $this->outputLine('<info>All third party extensions claim to be compatible with TYPO3 version %s</info>', [$typo3Version]);
        } else {
            $this->quit(1);
        }
    }

    /**
     * List upgrade wizards
     *
     * @param bool $all If set, all wizards will be listed, even the once marked as ready or done
     */
    public function listCommand($all = false)
    {
        $verbose = $this->output->getSymfonyConsoleOutput()->isVerbose();
        $messages = [];
        $wizards = $this->upgradeHandling->executeInSubProcess('listWizards', [], $messages);

        $listRenderer = new UpgradeWizardListRenderer();
        $this->outputLine('<comment>Wizards scheduled for execution:</comment>');
        $listRenderer->render($wizards['scheduled'], $this->output, $verbose);

        if ($all) {
            $this->outputLine(PHP_EOL . '<comment>Wizards marked as done:</comment>');
            $listRenderer->render($wizards['done'], $this->output, $verbose);
        }
        $this->outputLine();
        foreach ($messages as $message) {
            $this->outputLine($message);
        }
    }

    /**
     * Execute a single upgrade wizard
     *
     * @param string $identifier Identifier of the wizard that should be executed
     * @param array $arguments Arguments for the wizard prefixed with the identifier, e.g. compatibility7Extension[install]=0
     * @param bool $force Force execution, even if the wizard has been marked as done
     */
    public function wizardCommand($identifier, array $arguments = [], $force = false)
    {
        $messages = [];
        $result = $this->upgradeHandling->executeInSubProcess('executeWizard', [$identifier, $arguments, $force], $messages);
        (new UpgradeWizardResultRenderer())->render([$identifier => $result], $this->output);
        $this->outputLine();
        foreach ($messages as $message) {
            $this->outputLine($message);
        }
    }

    /**
     * Execute all upgrade wizards that are scheduled for execution
     *
     * @param array $arguments Arguments for the wizard prefixed with the identifier, e.g. compatibility7Extension[install]=0; multiple arguments separated with comma
     */
    public function allCommand(array $arguments = [])
    {
        $verbose = $this->output->getSymfonyConsoleOutput()->isVerbose();
        $this->outputLine(PHP_EOL . 'Initiating TYPO3 upgrade' . PHP_EOL);

        $messages = [];
        $results = $this->upgradeHandling->executeAll($arguments, $this->output, $messages);

        $this->outputLine(PHP_EOL . PHP_EOL . 'Successfully upgraded TYPO3 to version %s', [TYPO3_version]);

        if ($verbose) {
            $this->outputLine();
            $this->outputLine('<comment>Upgrade report:</comment>');
            (new UpgradeWizardResultRenderer())->render($results, $this->output);
        }
        $this->outputLine();
        foreach ($messages as $message) {
            $this->outputLine($message);
        }
    }

    /**
     * This is where the hard work happens in a fully bootstrapped TYPO3
     * It will be called as sub process
     *
     * @param string $upgradeCommand
     * @param string $arguments Serialized arguments
     * @internal
     */
    public function subProcessCommand($upgradeCommand, $arguments)
    {
        $arguments = unserialize($arguments, ['allowed_classes' => false]);
        $result = $this->upgradeHandling->$upgradeCommand(...$arguments);
        $this->output(serialize($result));
    }

    /**
     * Checks for broken extensions
     *
     * This command in meant to be executed as sub process as it is is subject to cause fatal errors
     * when extensions have broken (incompatible) code
     *
     * @param string $extensionKey Extension key for extension to check
     * @param bool $configOnly
     * @internal
     */
    public function checkExtensionCompatibilityCommand($extensionKey, $configOnly = false)
    {
        $this->output(\json_encode($this->upgradeHandling->isCompatible($extensionKey, $configOnly)));
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Install\Upgrade;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Symfony\Component\Console\Exception\RuntimeException;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;

/**
 * Used to migrate deprecated LocalConfiguration.php values to new values
 * It is a wrapper around the TYPO3 class to properly handle redirect exceptions
 */
class SilentConfigurationUpgrade
{
    /**
     * @var ConfigurationManager
     */
    private $configurationManager;

    public function __construct(ConfigurationManager $configurationManager = null)
    {
        $this->configurationManager = $configurationManager ?: GeneralUtility::makeInstance(ConfigurationManager::class);
    }

    /**
     * Call silent upgrade class, redirect to self if configuration was changed.
     *
     * @throws \UnexpectedValueException
     * @throws RuntimeException
     */
    public function executeSilentConfigurationUpgradesIfNeeded()
    {
        if (!file_exists($this->configurationManager->getLocalConfigurationFileLocation())) {
            return;
        }
        // We need to write the extension configuration for all active extensions ourselves
        // as the core does not take care doing so (yet)
        $extensionConfigurationService = new ExtensionConfiguration();
        $extensionConfigurationService->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();

        $upgradeService = new SilentConfigurationUpgradeService($this->configurationManager);
        $count = 0;
        do {
            try {
                $count++;
                $upgradeService->execute();
                $redirect = false;
            } catch (ConfigurationChangedException $e) {
                $redirect = true;
                $this->configurationManager->exportConfiguration();
                if ($count > 20) {
                    throw new RuntimeException('Too many loops when silently upgrading configuration', 1493897404, $e);
                }
            }
        } while ($redirect === true);
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Extension;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Database\Schema\SchemaUpdateType;
use Helhum\Typo3Console\Install\FolderStructure\ExtensionFactory;
use Helhum\Typo3Console\Service\Database\SchemaService;
use TYPO3\CMS\Core\Package\PackageInterface;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extensionmanager\Utility\InstallUtility;

class ExtensionSetup
{
    /**
     * @var ExtensionFactory
     */
    private $extensionFactory;

    /**
     * @var InstallUtility
     */
    private $extensionInstaller;

    /**
     * @var SchemaService
     */
    private $schemaService;

    /**
     * @var ExtensionConfiguration
     */
    private $extensionConfiguration;

    public function __construct(
        ExtensionFactory $extensionFactory = null,
        InstallUtility $extensionInstaller = null,
        SchemaService $schemaService = null,
        ExtensionConfiguration $extensionConfiguration = null
    ) {
        $this->extensionFactory = $extensionFactory ?: new ExtensionFactory(GeneralUtility::makeInstance(PackageManager::class));
        $this->extensionInstaller = $extensionInstaller ?: GeneralUtility::makeInstance(ObjectManager::class)->get(InstallUtility::class);
        $this->schemaService = $schemaService ?: GeneralUtility::makeInstance(ObjectManager::class)->get(SchemaService::class);
        $this->extensionConfiguration = $extensionConfiguration ?: new ExtensionConfiguration();
    }

    /**
     * Performs all necessary operations to integrate extensions into the system.
     * Instead of using buggy TYPO3 API, we created our own instead.
     * This might be removed, once the bug is fixed in TYPO3.
     *
     * @param PackageInterface[] $packages
     */
    public function setupExtensions(array $packages)
    {
        foreach ($packages as $package) {
            $this->extensionFactory->getExtensionStructure($package)->fix();
            $this->callInstaller('importInitialFiles', [PathUtility::stripPathSitePrefix($package->getPackagePath()), $package->getPackageKey()]);
            $this->extensionConfiguration->saveDefaultConfiguration($package->getPackageKey());
        }

        $this->schemaService->updateSchema(SchemaUpdateType::expandSchemaUpdateTypes(['safe']));

        foreach ($packages as $package) {
            $relativeExtensionPath = PathUtility::stripPathSitePrefix($package->getPackagePath());
            $extensionKey = $package->getPackageKey();
            $this->callInstaller('importStaticSqlFile', [$relativeExtensionPath]);
            $this->callInstaller('importT3DFile', [$relativeExtensionPath]);
            $this->callInstaller('emitAfterExtensionInstallSignal', [$extensionKey]);
        }
    }

    /**
     * @param string $method
     * @param array $arguments
     */
    private function callInstaller($method, array $arguments)
    {
        $installer = $this->extensionInstaller;
        \Closure::bind(function () use ($installer, $method, $arguments) {
            return $installer->$method(...$arguments);
        }, null, $installer)();
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Install\FolderStructure;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Package\UncachedPackageManager;
use TYPO3\CMS\Core\Package\PackageInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
use TYPO3\CMS\Install\FolderStructure\DirectoryNode;
use TYPO3\CMS\Install\FolderStructure\RootNode;
use TYPO3\CMS\Install\FolderStructure\StructureFacade;

/**
 * Factory returns folder structure object hierarchy
 * for TYPO3 core and Extensions
 */
class ExtensionFactory extends DefaultFactory
{
    /**
     * @var UncachedPackageManager
     */
    private $packageManager;

    /**
     * ExtensionFactory constructor.
     *
     * @param UncachedPackageManager $packageManager
     */
    public function __construct(UncachedPackageManager $packageManager)
    {
        $this->packageManager = $packageManager;
    }

    /**
     * Get default structure object hierarchy
     *
     * @throws \TYPO3\CMS\Install\FolderStructure\Exception\RootNodeException
     * @throws \TYPO3\CMS\Install\FolderStructure\Exception\InvalidArgumentException
     * @return StructureFacade
     */
    public function getStructure()
    {
        $structure = $this->getDefaultStructureDefinition();
        $structure['children'] = $this->appendStructureDefinition($structure['children'], $this->createExtensionStructureDefinition($this->packageManager->getActivePackages()));

        return new StructureFacade(new RootNode($structure));
    }

    /**
     * Creates the folder structure for one extension
     *
     * @param PackageInterface $package
     * @return StructureFacade
     */
    public function getExtensionStructure(PackageInterface $package)
    {
        $structure = [
            'name' => substr(PATH_site, 0, -1),
            'targetPermission' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'],
            'children' => $this->appendStructureDefinition([], $this->createExtensionStructureDefinition([$package])),
        ];

        return new StructureFacade(new RootNode($structure));
    }

    /**
     * Default definition of folder and file structure with dynamic
     * permission settings
     *
     * @param PackageInterface[] $packages
     * @return array
     */
    private function createExtensionStructureDefinition(array $packages)
    {
        $structureBase = [];
        foreach ($packages as $package) {
            $extensionConfiguration = $this->packageManager->getExtensionConfiguration($package);

            if (isset($extensionConfiguration['uploadfolder']) && (bool)$extensionConfiguration['uploadfolder']) {
                $structureBase[] = $this->getExtensionUploadDirectory($package->getPackageKey());
            }

            if (!empty($extensionConfiguration['createDirs'])) {
                foreach (explode(',', $extensionConfiguration['createDirs']) as $directoryToCreate) {
                    $absolutePath = GeneralUtility::getFileAbsFileName(trim($directoryToCreate));
                    // Only create valid paths.
                    if (!empty($absolutePath)) {
                        $structureBase[] = $this->getDirectoryNodeByPath(PathUtility::stripPathSitePrefix($absolutePath));
                    }
                }
            }
        }

        return $structureBase;
    }

    /**
     * Extension uploads directory.
     *
     * @param string $extension Extension key
     * @return array
     */
    private function getExtensionUploadDirectory($extension)
    {
        return $this->getDirectoryNodeByPath('uploads/tx_' . str_replace('_', '', $extension));
    }

    /**
     * Build directory nodes by given $path
     *
     * @param string $path Path to directory
     * @return array
     */
    private function getDirectoryNodeByPath($path)
    {
        $baseNode = [];
        $parts = explode('/', $path);
        $node = &$baseNode;
        foreach ($parts as $part) {
            $node[0] = [
                'name' => $part,
                'type' => DirectoryNode::class,
                'targetPermission' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'],
                'children' => [],
            ];
            // Add next directory as children of current node
            $node = &$node[0]['children'];
        }

        return isset($baseNode[0]) ? $baseNode[0] : [];
    }

    /**
     * Append $original structure definition with $additional structure definition.
     * Only add missing nodes.
     *
     * @param array $original
     * @param array $additional
     * @return array
     */
    private function appendStructureDefinition(array $original, array $additional)
    {
        foreach ($additional as $additionalStructure) {
            $structureKey = false;
            foreach ($original as $key => $originalStructure) {
                if ($originalStructure['name'] === $additionalStructure['name']) {
                    $structureKey = $key;
                }
            }

            if ($structureKey === false) {
                // Append key to original
                $original[] = $additionalStructure;
            } else {
                // Append children, if necessary
                if (isset($additionalStructure['children'])) {
                    if (isset($original[$structureKey]['children'])) {
                        $original[$structureKey]['children'] = $this->appendStructureDefinition(
                            $original[$structureKey]['children'],
                            $additionalStructure['children']
                        );
                    } else {
                        $original[$structureKey]['children'] = $additionalStructure['children'];
                    }
                }
            }
        }

        return $original;
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Service;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Parser\ParsingException;
use Helhum\Typo3Console\Parser\PhpParser;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use TYPO3\CMS\Extbase\Reflection\DocCommentParser;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
use TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3\CMS\Fluid\Core\ViewHelper\ArgumentDefinition;

/**
 * XML Schema (XSD) Generator. Will generate an XML schema which can be used for auto-completion
 * in schema-aware editors like Eclipse XML editor.
 */
class XsdGenerator
{
    /**
     * @var PackageManager
     */
    protected $packageManager;

    /**
     * @var ObjectManagerInterface
     */
    protected $objectManager;

    /**
     * The doc comment parser.
     *
     * @var DocCommentParser
     */
    protected $docCommentParser;

    /**
     * @var ReflectionService
     */
    protected $reflectionService;

    public function __construct(
        PackageManager $packageManager,
        ObjectManagerInterface $objectManager,
        DocCommentParser $docCommentParser,
        ReflectionService $reflectionService
    ) {
        $this->packageManager = $packageManager;
        $this->objectManager = $objectManager;
        $this->docCommentParser = $docCommentParser;
        $this->reflectionService = $reflectionService;
    }

    /**
     * Generate the XML Schema definition for a given namespace.
     * It will generate an XSD file for all view helpers in this namespace.
     *
     * @param string $viewHelperNamespace Namespace identifier to generate the XSD for, without leading Backslash.
     * @param string $xsdNamespace $xsdNamespace unique target namespace used in the XSD schema (for example "http://yourdomain.org/ns/viewhelpers")
     * @throws Exception
     * @return string XML Schema definition
     */
    public function generateXsd($viewHelperNamespace, $xsdNamespace)
    {
        $viewHelperNamespace = rtrim($viewHelperNamespace, '_\\') . $this->getDelimiterFromNamespace($viewHelperNamespace);
        $classNames = $this->getClassNamesInNamespace($viewHelperNamespace);
        if (count($classNames) === 0) {
            throw new Exception(sprintf('No ViewHelpers found in namespace "%s"', $viewHelperNamespace), 1330029328);
        }

        return $this->generateXsdFromClassNames($classNames, $xsdNamespace);
    }

    /**
     * Generate the XML Schema definition for a given namespace.
     * It will generate an XSD file for all view helpers in this namespace.
     *
     * @param array $viewHelperPaths One or more paths to a view helper class files
     * @param string $xsdNamespace $xsdNamespace unique target namespace used in the XSD schema (for example "http://yourdomain.org/ns/viewhelpers")
     * @throws Exception
     * @return string XML Schema definition
     */
    public function generateXsdFromClassFiles(array $viewHelperPaths, $xsdNamespace)
    {
        $classNames = $this->getClassNamesInPaths($viewHelperPaths);
        if (count($classNames) === 0) {
            throw new Exception(sprintf('No ViewHelpers found in paths "%s"', implode(',', $viewHelperPaths)), 1464982249);
        }

        return $this->generateXsdFromClassNames($classNames, $xsdNamespace);
    }

    /**
     * Generate the XML Schema definition for a given namespace.
     * It will generate an XSD file for all view helpers in this namespace.
     *
     * @param array $classNames One or more view helper class names
     * @param string $xsdNamespace $xsdNamespace unique target namespace used in the XSD schema (for example "http://yourdomain.org/ns/viewhelpers")
     * @throws Exception
     * @return string XML Schema definition
     */
    protected function generateXsdFromClassNames(array $classNames, $xsdNamespace)
    {
        if (count($classNames) === 0) {
            throw new Exception(sprintf('No ViewHelper classes given'), 1464984856);
        }
        $xmlRootNode = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?>
            <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="' . $xsdNamespace . '"></xsd:schema>');

        foreach ($classNames as $className) {
            $this->generateXmlForClassName($className, $xmlRootNode);
        }

        return $xmlRootNode->asXML();
    }

    /**
     * Generate the XML Schema for a given class name.
     *
     * @param string $className Class name to generate the schema for.
     * @param \SimpleXMLElement $xmlRootNode XML root node where the xsd:element is appended.
     * @return void
     */
    protected function generateXmlForClassName($className, \SimpleXMLElement $xmlRootNode)
    {
        $reflectionClass = new \ReflectionClass($className);
        $tagName = $this->getTagNameForClass($className);
        $xsdElement = $xmlRootNode->addChild('xsd:element');
        $xsdElement['name'] = $tagName;
        $this->docCommentParser->parseDocComment($reflectionClass->getDocComment());
        $this->addDocumentation($this->docCommentParser->getDescription(), $xsdElement);

        $xsdComplexType = $xsdElement->addChild('xsd:complexType');
        $xsdComplexType['mixed'] = 'true';
        $xsdSequence = $xsdComplexType->addChild('xsd:sequence');
        $xsdAny = $xsdSequence->addChild('xsd:any');
        $xsdAny['minOccurs'] = '0';
        $xsdAny['maxOccurs'] = 'unbounded';

        $this->addAttributes($className, $xsdComplexType);
    }

    /**
     * Add attribute descriptions to a given tag.
     * Initializes the view helper and its arguments, and then reads out the list of arguments.
     *
     * @param string $className Class name where to add the attribute descriptions
     * @param \SimpleXMLElement $xsdElement XML element to add the attributes to.
     * @return void
     */
    protected function addAttributes($className, \SimpleXMLElement $xsdElement)
    {
        /** @var AbstractViewHelper $viewHelper */
        $viewHelper = $this->objectManager->get($className);
        $argumentDefinitions = $viewHelper->prepareArguments();

        /** @var $argumentDefinition ArgumentDefinition */
        foreach ($argumentDefinitions as $argumentDefinition) {
            $xsdAttribute = $xsdElement->addChild('xsd:attribute');
            $xsdAttribute['type'] = 'xsd:string';
            $xsdAttribute['name'] = $argumentDefinition->getName();
            $this->addDocumentation($argumentDefinition->getDescription(), $xsdAttribute);
            if ($argumentDefinition->isRequired()) {
                $xsdAttribute['use'] = 'required';
            }
        }
    }

    /**
     * Add documentation XSD to a given XML node
     *
     * @param string $documentation Documentation string to add.
     * @param \SimpleXMLElement $xsdParentNode Node to add the documentation to
     * @return void
     */
    protected function addDocumentation($documentation, \SimpleXMLElement $xsdParentNode)
    {
        $xsdAnnotation = $xsdParentNode->addChild('xsd:annotation');
        $this->addChildWithCData($xsdAnnotation, 'xsd:documentation', $documentation);
    }

    /**
     * Get all class names inside this namespace and return them as array.
     * This has to be done by iterating over class files for TYPO3
     * as the Extbase Reflection Service cannot return all implementations
     * of Fluid AbstractViewHelpers
     *
     * @param array $paths
     * @return array Array of all class names inside a given namespace.
     */
    protected function getClassNamesInPaths(array $paths)
    {
        $viewHelperClassFiles = [];
        foreach ($paths as $path) {
            $viewHelperClassFiles = array_merge(
                $viewHelperClassFiles,
                GeneralUtility::getAllFilesAndFoldersInPath(
                    [],
                    $path,
                    'php'
                )
            );
        }
        $affectedViewHelperClassNames = [];
        foreach ($viewHelperClassFiles as $filePathAndFilename) {
            try {
                $potentialViewHelperClassName = $this->getClassNameFromFile($filePathAndFilename);
            } catch (ParsingException $e) {
                continue;
            }
            if (strpos($potentialViewHelperClassName, 'ViewHelpers') === false) {
                continue;
            }
            if (
                is_subclass_of($potentialViewHelperClassName, \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper::class)
                || is_subclass_of($potentialViewHelperClassName, \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper::class)
            ) {
                if (!class_exists($potentialViewHelperClassName)) {
                    require $filePathAndFilename;
                }
                $classReflection = new \ReflectionClass($potentialViewHelperClassName);
                if ($classReflection->isAbstract() === true) {
                    continue;
                }
                $affectedViewHelperClassNames[] = $potentialViewHelperClassName;
            }
        }
        sort($affectedViewHelperClassNames);

        return $affectedViewHelperClassNames;
    }

    /**
     * Get all class names inside this namespace and return them as array.
     * This has to be done by iterating over class files for TYPO3
     * as the Extbase Reflection Service cannot return all implementations
     * of Fluid AbstractViewHelpers
     *
     * @param string $namespace
     * @return array Array of all class names inside a given namespace.
     */
    protected function getClassNamesInNamespace($namespace)
    {
        $packageKey = $this->getPackageKeyFromNamespace($namespace);
        $viewHelperClassFilePaths[] = $this->packageManager->getPackage($packageKey)->getPackagePath() . 'Classes/ViewHelpers/';
        if ($packageKey === 'fluid') {
            $viewHelperClassFilePaths[] = realpath(PATH_site . 'typo3/') . '/../vendor/typo3fluid/fluid/src/ViewHelpers/';
        }

        return $this->getClassNamesInPaths($viewHelperClassFilePaths);
    }

    /**
     * @param string $namespace
     * @return string
     */
    protected function getPackageKeyFromNamespace($namespace)
    {
        $delimiter = $this->getDelimiterFromNamespace($namespace);
        $namespaceParts = explode($delimiter, $namespace);
        if ($namespaceParts[0] === 'TYPO3' && $namespaceParts[1] === 'CMS') {
            $packageKey = GeneralUtility::camelCaseToLowerCaseUnderscored($namespaceParts[2]);
        } else {
            $packageKey = GeneralUtility::camelCaseToLowerCaseUnderscored($namespaceParts[1]);
        }

        return $packageKey;
    }

    /**
     * @param string $filePath
     * @return string
     */
    protected function getClassNameFromFile($filePath)
    {
        $phpParser = new PhpParser();
        $parsedClass = $phpParser->parseClassFile($filePath);

        return $parsedClass->getFullyQualifiedClassName();
    }

    /**
     * @param string $namespace
     * @return string
     */
    protected function getDelimiterFromNamespace($namespace)
    {
        return strpos($namespace, '\\') === false ? '_' : '\\';
    }

    /**
     * Get a tag name for a given ViewHelper class.
     * Example: For the View Helper TYPO3\CMS\Fluid\ViewHelpers\Form\SelectViewHelper, and the
     * namespace prefix TYPO3\CMS\Fluid\ViewHelpers\, this method returns "form.select".
     *
     * @param string $className Class name
     * @return string Tag name
     */
    protected function getTagNameForClass($className)
    {
        /// Strip namespace from the beginning and "ViewHelper" from the end of the class name
        $strippedClassName = substr($className, strpos($className, 'ViewHelpers') + 12, -10);
        $classNameParts = explode('\\', $strippedClassName);

        return implode(
            '.',
            array_map(
                function ($element) {
                    return lcfirst($element);
                },
                $classNameParts
            )
        );
    }

    /**
     * Add a child node to $parentXmlNode, and wrap the contents inside a CDATA section.
     *
     * @param \SimpleXMLElement $parentXmlNode Parent XML Node to add the child to
     * @param string $childNodeName Name of the child node
     * @param string $childNodeValue Value of the child node. Will be placed inside CDATA.
     * @return \SimpleXMLElement the new element
     */
    protected function addChildWithCData(\SimpleXMLElement $parentXmlNode, $childNodeName, $childNodeValue)
    {
        $parentDomNode = dom_import_simplexml($parentXmlNode);
        $domDocument = new \DOMDocument();

        $childNode = $domDocument->appendChild($domDocument->createElement($childNodeName));
        $childNode->appendChild($domDocument->createCDATASection($childNodeValue));
        $childNodeTarget = $parentDomNode->ownerDocument->importNode($childNode, true);
        $parentDomNode->appendChild($childNodeTarget);

        return simplexml_import_dom($childNodeTarget);
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Mvc\Controller;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

use Helhum\Typo3Console\Log\Writer\ConsoleWriter;
use Helhum\Typo3Console\Mvc\Cli\CommandReflection;
use Helhum\Typo3Console\Mvc\Cli\ConsoleOutput;
use Helhum\Typo3Console\Mvc\Cli\Response;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Extbase\Mvc\Cli\CommandArgumentDefinition;
use TYPO3\CMS\Extbase\Mvc\Cli\Request;
use TYPO3\CMS\Extbase\Mvc\Controller\Argument;
use TYPO3\CMS\Extbase\Mvc\Controller\Arguments;
use TYPO3\CMS\Extbase\Mvc\Controller\CommandControllerInterface;
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchCommandException;
use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
use TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\ResponseInterface;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;

/**
 * A controller which processes requests from the command line
 *
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 or later
 */
abstract class CommandController implements CommandControllerInterface
{
    /**
     * @var Request
     */
    protected $request;

    /**
     * @var Response
     */
    protected $response;

    /**
     * @var Arguments
     */
    protected $arguments;

    /**
     * Name of the command method
     *
     * @var string
     */
    protected $commandMethodName = '';

    /**
     * Whether the command needs admin access to perform its job
     *
     * @var bool
     * @api
     */
    protected $requestAdminPermissions = false;

    /**
     * @var ReflectionService
     */
    protected $reflectionService;

    /**
     * @var ObjectManagerInterface
     */
    protected $objectManager;

    /**
     * @var ConsoleOutput
     */
    protected $output;

    /**
     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
     */
    public function injectObjectManager(ObjectManagerInterface $objectManager)
    {
        $this->objectManager = $objectManager;
        $this->arguments = $this->objectManager->get(\TYPO3\CMS\Extbase\Mvc\Controller\Arguments::class);
    }

    /**
     * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
     */
    public function injectReflectionService(ReflectionService $reflectionService)
    {
        $this->reflectionService = $reflectionService;
    }

    /**
     * Checks if the current request type is supported by the controller.
     *
     * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request The current request
     * @return bool true if this request type is supported, otherwise false
     */
    public function canProcessRequest(\TYPO3\CMS\Extbase\Mvc\RequestInterface $request)
    {
        return $request instanceof Request;
    }

    /**
     * Processes a command line request.
     *
     * @param RequestInterface $request The request object
     * @param ResponseInterface $response The response, modified by this handler
     * @throws UnsupportedRequestTypeException if the controller doesn't support the current request type
     * @api
     */
    public function processRequest(RequestInterface $request, ResponseInterface $response)
    {
        if (!$request instanceof Request) {
            throw new UnsupportedRequestTypeException(sprintf('%s only supports command line requests – requests of type "%s" given.', static::class, get_class($request)), 1300787096);
        }
        if ($response instanceof Response) {
            $output = $this->objectManager->get(ConsoleOutput::class, $response->getOutput(), $response->getInput());
        }
        $this->output = $output ?? $this->objectManager->get(ConsoleOutput::class);

        $this->request = $request;
        $this->request->setDispatched(true);
        $this->response = $response;

        $this->commandMethodName = $this->resolveCommandMethodName();
        $this->initializeCommandMethodArguments();
        $this->mapRequestArgumentsToControllerArguments();
        $this->callCommandMethod();
    }

    /**
     * Resolves and checks the current command method name
     *
     * Note: The resulting command method name might not have the correct case, which isn't a problem because PHP is
     * case insensitive regarding method names.
     *
     * @throws NoSuchCommandException
     * @return string Method name of the current command
     */
    protected function resolveCommandMethodName()
    {
        $commandMethodName = $this->request->getControllerCommandName() . 'Command';
        if (!is_callable([$this, $commandMethodName])) {
            throw new NoSuchCommandException(sprintf('A command method "%s()" does not exist in controller "%s".', $commandMethodName, get_class($this)), 1300902143);
        }

        return $commandMethodName;
    }

    /**
     * Initializes the arguments array of this controller by creating an empty argument object for each of the
     * method arguments found in the designated command method.
     *
     * @throws InvalidArgumentTypeException
     */
    protected function initializeCommandMethodArguments()
    {
        $this->arguments->removeAll();
        $methodParameters = (new CommandReflection(get_class($this), $this->commandMethodName))->getParameters();

        foreach ($methodParameters as $parameterName => $parameterInfo) {
            $dataType = null;
            if (isset($parameterInfo['type'])) {
                $dataType = $parameterInfo['type'];
            } elseif ($parameterInfo['array']) {
                $dataType = 'array';
            }
            if ($dataType === null) {
                throw new InvalidArgumentTypeException(sprintf('The argument type for parameter $%s of method %s->%s() could not be detected.', $parameterName, get_class($this), $this->commandMethodName), 1306755296);
            }
            $defaultValue = (isset($parameterInfo['defaultValue']) ? $parameterInfo['defaultValue'] : null);
            $this->arguments->addNewArgument($parameterName, $dataType, ($parameterInfo['optional'] === false), $defaultValue);
        }
    }

    /**
     * Maps arguments delivered by the request object to the local controller arguments.
     */
    protected function mapRequestArgumentsToControllerArguments()
    {
        /** @var Argument $argument */
        foreach ($this->arguments as $argument) {
            $argumentName = $argument->getName();
            if ($this->request->hasArgument($argumentName)) {
                $argument->setValue($this->request->getArgument($argumentName));
                continue;
            }
            if (!$argument->isRequired()) {
                continue;
            }
            $argumentValue = null;
            $commandArgumentDefinition = $this->objectManager->get(CommandArgumentDefinition::class, $argumentName, true, null);
            while ($argumentValue === null) {
                $argumentValue = $this->output->ask(sprintf('<comment>Please specify the required argument "%s":</comment> ', $commandArgumentDefinition->getDashedName()));
            }
            $argument->setValue($argumentValue);
        }
    }

    /**
     * Forwards the request to another command and / or CommandController.
     *
     * Request is directly transferred to the other command / controller
     * without the need for a new request.
     *
     * @param string $commandName
     * @param string $controllerObjectName
     * @param array $arguments
     * @throws StopActionException
     */
    protected function forward($commandName, $controllerObjectName = null, array $arguments = [])
    {
        $this->request->setDispatched(false);
        $this->request->setControllerCommandName($commandName);
        if ($controllerObjectName !== null) {
            $this->request->setControllerObjectName($controllerObjectName);
        }
        $this->request->setArguments($arguments);

        $this->arguments->removeAll();
        throw new StopActionException('forward', 1476107661);
    }

    /**
     * Calls the specified command method and passes the arguments.
     *
     * If the command returns a string, it is appended to the content in the
     * response object. If the command doesn't return anything and a valid
     * view exists, the view is rendered automatically.
     */
    protected function callCommandMethod()
    {
        $preparedArguments = [];
        foreach ($this->arguments as $argument) {
            $preparedArguments[] = $argument->getValue();
        }
        $commandResult = $this->{$this->commandMethodName}(...$preparedArguments);
        if ($commandResult !== null) {
            $this->outputLine((string)$commandResult);
            $this->output->getSymfonyConsoleOutput()->getErrorOutput()->writeln('<warning>Returning a string from a command method is deprecated.</warning>');
            $this->output->getSymfonyConsoleOutput()->getErrorOutput()->writeln('<warning>Please use $this->outputLine() instead.</warning>');
        }
    }

    /**
     * Outputs specified text to the console window
     * You can specify arguments that will be passed to the text via sprintf
     *
     * @see http://www.php.net/sprintf
     * @param string $text Text to output
     * @param array $arguments Optional arguments to use for sprintf
     */
    protected function output($text, array $arguments = [])
    {
        $this->output->output($text, $arguments);
    }

    /**
     * Outputs specified text to the console window and appends a line break
     *
     * @param string $text Text to output
     * @param array $arguments Optional arguments to use for sprintf
     * @see output()
     */
    protected function outputLine($text = '', array $arguments = [])
    {
        $this->output->outputLine($text, $arguments);
    }

    /**
     * Formats the given text to fit into $this->output->getMaximumLineLength() and outputs it to the
     * console window
     *
     * @param string $text Text to output
     * @param array $arguments Optional arguments to use for sprintf
     * @param int $leftPadding The number of spaces to use for indentation
     * @see outputLine()
     */
    protected function outputFormatted($text = '', array $arguments = [], $leftPadding = 0)
    {
        $this->output->outputFormatted($text, $arguments, $leftPadding);
    }

    /**
     * Exits the CLI through the dispatcher
     * An exit status code can be specified @see http://www.php.net/exit
     *
     * @param int $exitCode Exit code to return on exit
     * @throws StopActionException
     */
    protected function quit($exitCode = 0)
    {
        $this->response->setExitCode($exitCode);
        throw new StopActionException('quit', 1476107681);
    }

    /**
     * Sends the response and exits the CLI without any further code execution
     * Should be used for commands that flush code caches.
     *
     * @param int $exitCode Exit code to return on exit
     * @deprecated Will be removed with 6.0.0
     */
    protected function sendAndExit($exitCode = 0)
    {
        $this->outputLine('<warning>Using sendAndExit() is deprecated. Please use quit() instead.</warning>');
        $this->quit($exitCode);
    }

    /**
     * Creates a logger which outputs to the console
     *
     * @param int $minimumLevel Minimum log level that should trigger output
     * @param array $options Additional options for the console writer
     * @return LoggerInterface
     */
    protected function createDefaultLogger($minimumLevel = LogLevel::DEBUG, $options = [])
    {
        $options['output'] = $this->output->getSymfonyConsoleOutput();
        $logger = new Logger(get_class($this));
        $logger->addWriter($minimumLevel, new ConsoleWriter($options));

        return $logger;
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Mvc\Cli;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Doctrine\Common\Annotations\AnnotationReader;
use Helhum\Typo3Console\Annotation\Command\Definition\Argument;
use Helhum\Typo3Console\Annotation\Command\Definition\Option;
use Helhum\Typo3Console\Annotation\Command\Definition\Validate;
use Helhum\Typo3Console\Mvc\Cli\Symfony\Application;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Represents a Command
 */
class Command
{
    /**
     * @var string
     */
    protected $controllerClassName;

    /**
     * @var string
     */
    protected $controllerCommandName;

    /**
     * @var string
     */
    private $controllerCommandMethod;

    /**
     * @var string
     */
    protected $commandIdentifier;

    /**
     * Name of the extension to which this command belongs
     *
     * @var string
     */
    protected $extensionName;

    /**
     * @var CommandArgumentDefinition[]
     */
    private $argumentDefinitions;

    /**
     * @var CommandArgumentDefinition[]
     */
    private $arguments = [];

    /**
     * @var CommandArgumentDefinition[]
     */
    private $options = [];

    /**
     * @var string[]
     */
    private $synopsis;

    /**
     * @var array
     */
    private $commandMethodDefinitions = null;

    /**
     * @var array
     */
    private $inputDefinitions;

    /**
     * @var CommandReflection
     */
    private $commandReflection;

    /**
     * @param string $controllerClassName Class name of the controller providing the command
     * @param string $controllerCommandName Command name, i.e. the method name of the command, without the "Command" suffix
     * @param CommandReflection $commandReflection
     * @throws InvalidArgumentException
     */
    public function __construct(string $controllerClassName, string $controllerCommandName, CommandReflection $commandReflection = null)
    {
        $this->controllerClassName = $controllerClassName;
        $this->controllerCommandName = $controllerCommandName;
        $this->controllerCommandMethod = $this->controllerCommandName . 'Command';
        $this->commandReflection = $commandReflection ?: new CommandReflection($this->controllerClassName, $this->controllerCommandMethod);
        $delimiter = strpos($controllerClassName, '\\') !== false ? '\\' : '_';
        $classNameParts = explode($delimiter, $controllerClassName);
        if (isset($classNameParts[0], $classNameParts[1]) && $classNameParts[0] === 'TYPO3' && $classNameParts[1] === 'CMS') {
            $classNameParts[0] .= '\\' . $classNameParts[1];
            unset($classNameParts[1]);
            $classNameParts = array_values($classNameParts);
        }
        $numberOfClassNameParts = count($classNameParts);
        if ($numberOfClassNameParts < 3) {
            throw new InvalidArgumentException(
                'Controller class names must at least consist of three parts: vendor, extension name and command controller name.',
                1438782187
            );
        }
        if (strpos($classNameParts[$numberOfClassNameParts - 1], 'CommandController') === false) {
            throw new InvalidArgumentException(
                'Invalid controller class name "' . $controllerClassName . '". Class name must end with "CommandController" (e.g. Vendor\ExtensionName\Command\MySimpleCommandController).',
                1305100019
            );
        }
        $this->extensionName = $classNameParts[1];
        $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($this->extensionName);
        $this->commandIdentifier = strtolower($extensionKey . ':' . substr($classNameParts[$numberOfClassNameParts - 1], 0, -17) . ':' . $controllerCommandName);
    }

    /**
     * @return string
     */
    public function getControllerClassName(): string
    {
        return $this->controllerClassName;
    }

    /**
     * @return string
     */
    public function getControllerCommandName(): string
    {
        return $this->controllerCommandName;
    }

    /**
     * Returns the command identifier for this command
     *
     * @return string The command identifier for this command, following the pattern extensionname:controllername:commandname
     */
    public function getCommandIdentifier(): string
    {
        return $this->commandIdentifier;
    }

    /**
     * Returns the name of the extension to which this command belongs
     *
     * @return string
     */
    public function getExtensionName(): string
    {
        return $this->extensionName;
    }

    /**
     * Returns a short description of this command
     *
     * @return string A short description
     */
    public function getShortDescription(): string
    {
        $lines = explode(LF, $this->commandReflection->getDescription());

        return !empty($lines) ? trim($lines[0]) : '<no description available>';
    }

    /**
     * Returns a longer description of this command
     * This is the complete method description except for the first line which can be retrieved via getShortDescription()
     * If The command description only consists of one line, an empty string is returned
     *
     * @return string A longer description of this command
     */
    public function getDescription(): string
    {
        $lines = explode(LF, $this->commandReflection->getDescription());
        array_shift($lines);
        $descriptionLines = [];
        foreach ($lines as $line) {
            $trimmedLine = trim($line);
            if ($descriptionLines !== [] || $trimmedLine !== '') {
                $descriptionLines[] = $trimmedLine;
            }
        }

        return implode(LF, $descriptionLines);
    }

    /**
     * Returns true if this command expects required and/or optional arguments, otherwise false
     *
     * @return bool
     */
    public function hasArguments(): bool
    {
        return !empty($this->commandReflection->getParameters());
    }

    /**
     * @return bool
     */
    public function hasOptions(): bool
    {
        return count($this->getOptions()) > 0;
    }

    /**
     * @return bool
     */
    public function hasRequiredArguments(): bool
    {
        return count($this->getArguments()) > 0;
    }

    /**
     * @return CommandArgumentDefinition[]
     */
    public function getArguments(): array
    {
        if ($this->argumentDefinitions === null) {
            $this->getArgumentDefinitions();
        }

        return $this->arguments;
    }

    /**
     * @return CommandArgumentDefinition[]
     */
    public function getOptions(): array
    {
        if ($this->argumentDefinitions === null) {
            $this->getArgumentDefinitions();
        }

        return $this->options;
    }

    /**
     * @return bool
     * @deprecated in 5.0 will be removed in 6.0
     */
    public function shouldValidateInputStrict(): bool
    {
        return !empty($this->getCommandMethodDefinitions()['Validate']->strict);
    }

    /**
     * Returns an array of \TYPO3\CMS\Extbase\Mvc\Cli\CommandArgumentDefinition that contains
     * information about required/optional arguments of this command.
     * If the command does not expect any arguments, an empty array is returned
     *
     * @return CommandArgumentDefinition[]
     */
    public function getArgumentDefinitions(): array
    {
        if ($this->argumentDefinitions !== null) {
            return $this->argumentDefinitions;
        }
        if (!$this->hasArguments()) {
            return $this->argumentDefinitions = [];
        }
        $this->argumentDefinitions = [];
        $commandParameters = $this->commandReflection->getParameters();
        $commandParameterTags = $this->commandReflection->getTagsValues()['param'];
        $i = 0;
        $definedArguments = $this->getDefinedArguments();
        $definedOptions = $this->getDefinedOptions();
        foreach ($commandParameters as $commandParameterName => $commandParameterDefinition) {
            $description = '';
            if (isset($commandParameterTags[$i])) {
                $explodedAnnotation = preg_split('/\s+/', $commandParameterTags[$i], 3);
                $description = !empty($explodedAnnotation[2]) ? $explodedAnnotation[2] : '';
            }
            $dataType = $commandParameterDefinition['type'] ?? 'null';
            if ($commandParameterDefinition['array']) {
                $dataType = 'array';
            }
            $default = $commandParameterDefinition['defaultValue'] ?? null;
            $required = $commandParameterDefinition['optional'] !== true;
            $isArgument = isset($definedArguments[$commandParameterName]) || ($required && !isset($definedOptions[$commandParameterName]));
            $argumentDefinition = new CommandArgumentDefinition($commandParameterName, $required, $description, $dataType, $default, $isArgument);
            if ($isArgument) {
                $this->arguments[] = $argumentDefinition;
            } else {
                $this->options[] = $argumentDefinition;
            }
            $this->argumentDefinitions[] = $argumentDefinition;
            $i++;
        }

        return $this->argumentDefinitions;
    }

    public function getInputDefinitions(bool $strict = null): array
    {
        if ($strict === null) {
            $strict = $this->shouldValidateInputStrict();
        }
        $key = $strict ? 'strict' : 'relaxed';

        if (!empty($this->inputDefinitions[$key])) {
            return $this->inputDefinitions[$key];
        }

        $applicationDefinition = (new Application())->getDefinition();
        $definitions = [];
        foreach ($this->getArguments() as $argument) {
            $argumentName = $argument->getName();
            if (!$strict && $applicationDefinition->hasArgument($argumentName)) {
                // That is most likely the "command" argument
                // We append the full command identifier so we don't get an exception from Symfony
                $argumentName .= '_' . $this->getCommandIdentifier();
            }
            $definitions[] = new InputArgument(
                $argumentName,
                $strict && $argument->isRequired() ? InputArgument::REQUIRED : InputArgument::OPTIONAL,
                $argument->getDescription(),
                $strict && $argument->isRequired() ? null : $argument->getDefaultValue()
            );

            if ($strict) {
                continue;
            }
            // Fallback option for compatibility but only in case no other option with same name is defined already
            // @deprecated in 5.0 will be removed in 6.0
            if ($applicationDefinition->hasOption($argument->getOptionName())) {
                // Option is globally defined already, so we can skip here
                continue;
            }
            $definitions[] = new InputOption(
                $argument->getOptionName(),
                null,
                InputOption::VALUE_REQUIRED,
                $argument->getDescription()
            );
        }
        foreach ($this->getOptions() as $option) {
            if ($applicationDefinition->hasOption($option->getOptionName())) {
                // Option is globally defined already, so we can skip here
                continue;
            }
            if (!$option->acceptsValue()) {
                $definitions[] = new InputOption(
                    $option->getOptionName(),
                    null,
                    $strict ? InputOption::VALUE_NONE : InputOption::VALUE_OPTIONAL,
                    $option->getDescription()
                );
            } else {
                $definitions[] = new InputOption(
                    $option->getOptionName(),
                    null,
                    InputOption::VALUE_REQUIRED,
                    $option->getDescription(),
                    $option->getDefaultValue()
                );
            }

            if ($strict) {
                continue;
            }
            // Fallback to argument for compatibility
            // @deprecated in 5.0 will be removed in 6.0
            $definitions[] = new InputArgument(
                $option->getName(),
                InputArgument::OPTIONAL,
                $option->getDescription(),
                $option->getDefaultValue()
            );
        }

        return $this->inputDefinitions[$key] = $definitions;
    }

    private function getDefinedArguments(): array
    {
        $argumentDefinitions = $this->getCommandMethodDefinitions()['Argument'] ?? [];
        if (empty($argumentDefinitions)) {
            return [];
        }
        $definedArguments = [];
        foreach ($argumentDefinitions as $definedArgument) {
            $definedArguments[$definedArgument->name] = $definedArgument;
        }

        return $definedArguments;
    }

    private function getDefinedOptions(): array
    {
        $optionDefinitions = $this->getCommandMethodDefinitions()['Option'] ?? [];
        if (empty($optionDefinitions)) {
            return [];
        }
        $definedOptions = [];
        foreach ($optionDefinitions as $definedOption) {
            $definedOptions[$definedOption->name] = $definedOption;
        }

        return $definedOptions;
    }

    private function getCommandMethodDefinitions(): array
    {
        if ($this->commandMethodDefinitions !== null) {
            return $this->commandMethodDefinitions;
        }
        $this->commandMethodDefinitions = $this->parseDefinitions();

        return $this->commandMethodDefinitions;
    }

    /**
     * Get parsed annotations if command has any
     *
     * @throws InvalidArgumentException
     * @return array
     */
    private function parseDefinitions(): array
    {
        $definitions = [];
        $reader = new AnnotationReader();
        $method = new \ReflectionMethod($this->controllerClassName, $this->controllerCommandMethod);
        foreach ($reader->getMethodAnnotations($method) as $annotation) {
            if ($annotation instanceof Option) {
                $definitions['Option'][] = $annotation;
            }
            if ($annotation instanceof Argument) {
                $definitions['Argument'][] = $annotation;
            }
            if ($annotation instanceof Validate) {
                $definitions['Validate'] = $annotation;
            }
        }

        return $definitions;
    }

    /**
     * Get synopsis for this command, either short or long
     *
     * @param bool $short
     * @return string
     */
    public function getSynopsis($short = false): string
    {
        $key = $short ? 'short' : 'long';
        if (isset($this->synopsis[$key])) {
            return $this->synopsis[$key];
        }

        $elements = [];
        if ($short && $this->hasOptions()) {
            $elements[] = '[options]';
        } elseif (!$short) {
            foreach ($this->getOptions() as $argumentDefinition) {
                $value = '';
                if ($argumentDefinition->acceptsValue()) {
                    $value = ' ' . strtoupper($argumentDefinition->getOptionName());
                }
                $elements[] = sprintf('[%s%s]', $argumentDefinition->getDashedName(), $value);
            }
        }
        if (count($elements) && $this->hasRequiredArguments()) {
            $elements[] = '[--]';
        }
        foreach ($this->getArguments() as $argumentDefinition) {
            $elements[] = '<' . $argumentDefinition->getName() . '>';
        }

        return $this->synopsis[$key] = implode(' ', $elements);
    }

    /**
     * Tells if this command is internal and thus should not be exposed through help texts, user documentation etc.
     * Internal commands are still accessible through the regular command line interface, but should not be used
     * by users.
     *
     * @return bool
     */
    public function isInternal(): bool
    {
        return isset($this->commandReflection->getTagsValues()['internal']);
    }

    /**
     * Tells if this command is meant to be used on CLI only.
     *
     * @return bool
     */
    public function isCliOnly(): bool
    {
        return isset($this->commandReflection->getTagsValues()['cli']);
    }

    /**
     * Tells if this command flushes all caches and thus needs special attention in the interactive shell.
     *
     * Note that neither this method nor the @flushesCaches annotation is currently part of the official API.
     *
     * @return bool
     */
    public function isFlushingCaches(): bool
    {
        return isset($this->commandReflection->getTagsValues()['flushesCaches']);
    }

    /**
     * Returns an array of command identifiers which were specified in the "@see"
     * annotation of a command method.
     *
     * @throws \ReflectionException
     * @return array
     */
    public function getRelatedCommandIdentifiers(): array
    {
        if (!isset($this->commandReflection->getTagsValues()['see'])) {
            return [];
        }
        $relatedCommandIdentifiers = [];
        foreach ($this->commandReflection->getTagsValues()['see'] as $tagValue) {
            if (preg_match('/^[\\w\\._]+:[\\w]+:[\\w]+$/', $tagValue) === 1) {
                $relatedCommandIdentifiers[] = $tagValue;
            }
        }

        return $relatedCommandIdentifiers;
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Core\Booting;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Exception;
use TYPO3\CMS\Core\Core\Bootstrap;

/**
 * A boot sequence, consisting of individual steps, each of them initializing a
 * specific part of the application.
 *
 * @api
 */
class Sequence
{
    /**
     * @var string
     */
    protected $identifier;

    /**
     * @var array
     */
    protected $steps = [];

    /**
     * @param string $identifier
     */
    public function __construct($identifier)
    {
        $this->identifier = $identifier;
    }

    /**
     * Adds the given step to this sequence, to be executed after then step specified
     * by $previousStepIdentifier. If no previous step is specified, the new step
     * is added to the list of steps executed right at the start of the sequence.
     *
     * @param \Helhum\Typo3Console\Core\Booting\Step $step The new step to add
     * @param string $previousStepIdentifier The preceding step
     * @return void
     */
    public function addStep(Step $step, $previousStepIdentifier = 'start')
    {
        $this->steps[$previousStepIdentifier][] = $step;
    }

    /**
     * Removes all occurrences of the specified step from this sequence
     *
     * @param string $stepIdentifier
     * @throws Exception
     * @return void
     */
    public function removeStep($stepIdentifier)
    {
        $removedOccurrences = 0;
        foreach ($this->steps as $previousStepIdentifier => $steps) {
            foreach ($steps as $index => $step) {
                if ($step->getIdentifier() === $stepIdentifier) {
                    unset($this->steps[$previousStepIdentifier][$index]);
                    $removedOccurrences ++;
                }
            }
        }
        if ($removedOccurrences === 0) {
            throw new Exception(sprintf('Cannot remove sequence step with identifier "%s" because no such step exists in the given sequence.', $stepIdentifier), 1322591669);
        }
    }

    /**
     * Executes all steps of this sequence
     *
     * @param Bootstrap $bootstrap
     * @throws StepFailedException
     * @return void
     */
    public function invoke(Bootstrap $bootstrap)
    {
        if (isset($this->steps['start'])) {
            foreach ($this->steps['start'] as $step) {
                $this->invokeStep($step, $bootstrap);
            }
        }
    }

    /**
     * Invokes a single step of this sequence and also invokes all steps registered
     * to be executed after the given step.
     *
     * @param Step $step The step to invoke
     * @param Bootstrap $bootstrap
     * @throws StepFailedException
     * @return void
     */
    protected function invokeStep(Step $step, Bootstrap $bootstrap)
    {
        $identifier = $step->getIdentifier();
        try {
            $step($bootstrap);
        } catch (\Throwable $e) {
            throw new StepFailedException($step, $e);
        }
        if (isset($this->steps[$identifier])) {
            foreach ($this->steps[$identifier] as $followingStep) {
                $this->invokeStep($followingStep, $bootstrap);
            }
        }
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\Command;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use Helhum\Typo3Console\Core\Booting\CompatibilityScripts;
use Helhum\Typo3Console\Extension\ExtensionSetup;
use Helhum\Typo3Console\Extension\ExtensionSetupResultRenderer;
use Helhum\Typo3Console\Install\FolderStructure\ExtensionFactory;
use Helhum\Typo3Console\Mvc\Controller\CommandController;
use Helhum\Typo3Console\Service\CacheService;
use TYPO3\CMS\Core\Package\PackageInterface;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
use TYPO3\CMS\Extensionmanager\Utility\InstallUtility;

/**
 * CommandController for working with extension management through CLI
 */
class ExtensionCommandController extends CommandController
{
    /**
     * @var bool
     */
    protected $requestAdminPermissions = true;

    /**
     * @var Dispatcher
     */
    protected $signalSlotDispatcher;

    /**
     * @var InstallUtility
     */
    protected $extensionInstaller;

    /**
     * @var PackageManager
     */
    protected $packageManager;

    /**
     * @var CacheService
     */
    protected $cacheService;

    public function __construct(
        Dispatcher $signalSlotDispatcher,
        PackageManager $packageManager,
        CacheService $cacheService
    ) {
        $this->signalSlotDispatcher = $signalSlotDispatcher;
        $this->packageManager = $packageManager;
        $this->cacheService = $cacheService;
    }

    /**
     * Activate extension(s)
     *
     * Activates one or more extensions by key.
     * Marks extensions as active, sets them up and clears caches for every activated extension.
     *
     * This command is deprecated (and hidden) in Composer mode.
     *
     * @param array $extensionKeys Extension keys to activate. Separate multiple extension keys with comma.
     * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
     */
    public function activateCommand(array $extensionKeys)
    {
        // @deprecated for composer usage in 5.0 will be removed with 6.0
        $verbose = $this->output->getSymfonyConsoleOutput()->isVerbose();
        $this->showDeprecationMessageIfApplicable();
        $this->emitPackagesMayHaveChangedSignal();
        $activatedExtensions = [];
        $extensionsToSetUp = [];
        foreach ($extensionKeys as $extensionKey) {
            $extensionsToSetUp[] = $this->packageManager->getPackage($extensionKey);
            if (!$this->packageManager->isPackageActive($extensionKey)) {
                $this->packageManager->activatePackage($extensionKey);
                $activatedExtensions[] = $extensionKey;
            }
        }

        if (!empty($activatedExtensions)) {
            $this->cacheService->flush();
            $this->getExtensionInstaller()->reloadCaches();

            $extensionKeysAsString = implode('", "', $activatedExtensions);
            if (count($activatedExtensions) === 1) {
                $this->outputLine('<info>Extension "%s" is now active.</info>', [$extensionKeysAsString]);
            } else {
                $this->outputLine('<info>Extensions "%s" are now active.</info>', [$extensionKeysAsString]);
            }
        }

        if (!empty($extensionsToSetUp)) {
            $this->setupExtensions($extensionsToSetUp, $verbose);
        }
    }

    /**
     * Deactivate extension(s)
     *
     * Deactivates one or more extensions by key.
     * Marks extensions as inactive in the system and clears caches for every deactivated extension.
     *
     * This command is deprecated (and hidden) in Composer mode.
     *
     * @param array $extensionKeys Extension keys to deactivate. Separate multiple extension keys with comma.
     * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
     */
    public function deactivateCommand(array $extensionKeys)
    {
        // @deprecated for composer usage in 5.0 will be removed with 6.0
        $this->showDeprecationMessageIfApplicable();
        foreach ($extensionKeys as $extensionKey) {
            $this->getExtensionInstaller()->uninstall($extensionKey);
        }
        $extensionKeysAsString = implode('", "', $extensionKeys);
        if (count($extensionKeys) === 1) {
            $this->outputLine('<info>Extension "%s" is now inactive.</info>', [$extensionKeysAsString]);
        } else {
            $this->outputLine('<info>Extensions "%s" are now inactive.</info>', [$extensionKeysAsString]);
        }
    }

    private function showDeprecationMessageIfApplicable()
    {
        if (CompatibilityScripts::isComposerMode()) {
            $this->output->outputLine('<warning>This command is deprecated when TYPO3 is composer managed.</warning>');
            $this->output->outputLine('<warning>It might lead to unexpected results.</warning>');
            $this->output->outputLine('<warning>The PackageStates.php file that tracks which extension should be active,</warning>');
            $this->output->outputLine('<warning>should be generated automatically using install:generatepackagestates.</warning>');
            $this->output->outputLine('<warning>To set up all active extensions, extension:setupactive should be used.</warning>');
            $this->output->outputLine('<warning>This command will be disabled, when TYPO3 is composer managed, in TYPO3 Console 6</warning>');
        }
    }

    /**
     * Set up extension(s)
     *
     * Sets up one or more extensions by key.
     * Set up means:
     *
     * - Database migrations and additions
     * - Importing files and data
     * - Writing default extension configuration
     *
     * @param array $extensionKeys Extension keys to set up. Separate multiple extension keys with comma.
     */
    public function setupCommand(array $extensionKeys)
    {
        $verbose = $this->output->getSymfonyConsoleOutput()->isVerbose();
        $packages = [];
        foreach ($extensionKeys as $extensionKey) {
            $packages[] = $this->packageManager->getPackage($extensionKey);
        }
        $this->setupExtensions($packages, $verbose);
    }

    /**
     * Performs all necessary operations to integrate an extension into the system.
     * To do so, we avoid buggy TYPO3 API and use our own instead.
     *
     * @param PackageInterface[] $packages
     * @param bool $verbose Whether or not to output results
     */
    private function setupExtensions(array $packages, $verbose = false)
    {
        $extensionSetupResultRenderer = new ExtensionSetupResultRenderer($this->signalSlotDispatcher);

        $extensionSetup = new ExtensionSetup(
            new ExtensionFactory($this->packageManager),
            $this->getExtensionInstaller()
        );

        $extensionSetup->setupExtensions($packages);
        $extensionKeysAsString = implode('", "', array_map(function (PackageInterface $package) {
            return $package->getPackageKey();
        }, $packages));
        if (count($packages) === 1) {
            $this->outputLine('<info>Extension "%s" is now set up.</info>', [$extensionKeysAsString]);
        } else {
            $this->outputLine('<info>Extensions "%s" are now set up.</info>', [$extensionKeysAsString]);
        }

        if ($verbose) {
            $this->outputLine();
            $extensionSetupResultRenderer->renderSchemaResult($this->output);
            $extensionSetupResultRenderer->renderExtensionDataImportResult($this->output);
            $extensionSetupResultRenderer->renderExtensionFileImportResult($this->output);
            $extensionSetupResultRenderer->renderImportedStaticDataResult($this->output);
        }
    }

    /**
     * Set up all active extensions
     *
     * Sets up all extensions that are marked as active in the system.
     *
     * This command is especially useful for deployment, where extensions
     * are already marked as active, but have not been set up yet or might have changed. It ensures every necessary
     * setup step for the (changed) extensions is performed.
     * As an additional benefit no caches are flushed, which significantly improves performance of this command
     * and avoids unnecessary cache clearing.
     *
     * @see typo3_console:extension:setup
     * @see typo3_console:install:generatepackagestates
     * @see typo3_console:cache:flush
     */
    public function setupActiveCommand()
    {
        $verbose = $this->output->getSymfonyConsoleOutput()->isVerbose();
        $this->setupExtensions($this->packageManager->getActivePackages(), $verbose);
    }

    /**
     * Removes all extensions that are not marked as active
     *
     * Directories of inactive extension are <comment>removed</comment> from typo3/sysext and typo3conf/ext.
     * This is a one way command with no way back. Don't blame anybody if this command destroys your data.
     * <comment>Handle with care!</comment>
     *
     * This command is deprecated.
     * Instead of adding extensions and then removing them, just don't add them in the first place.
     *
     * @param bool $force The option has to be specified, otherwise nothing happens
     */
    public function removeInactiveCommand($force = false)
    {
        $this->outputLine('<warning>This command is deprecated and will be removed with TYPO3 Console 6.0</warning>');
        if ($force) {
            $activePackages = $this->packageManager->getActivePackages();
            $this->packageManager->scanAvailablePackages();
            foreach ($this->packageManager->getAvailablePackages() as $package) {
                if (empty($activePackages[$package->getPackageKey()])) {
                    $this->packageManager->unregisterPackage($package);
                    if (is_dir($package->getPackagePath())) {
                        GeneralUtility::flushDirectory($package->getPackagePath());
                        $removedPaths[] = PathUtility::stripPathSitePrefix($package->getPackagePath());
                    }
                }
            }
            $this->packageManager->forceSortAndSavePackageStates();
            if (!empty($removedPaths)) {
                $this->outputLine('<info>The following directories have been removed:</info>' . chr(10) . implode(chr(10), $removedPaths));
            } else {
                $this->outputLine('<info>Nothing was removed</info>');
            }
        } else {
            $this->outputLine('<warning>Operation not confirmed and has been skipped</warning>');
            $this->quit(1);
        }
    }

    /**
     * List extensions that are available in the system
     *
     * @param bool $active Only show active extensions
     * @param bool $inactive Only show inactive extensions
     * @param bool $raw Enable machine readable output (just extension keys separated by line feed)
     */
    public function listCommand($active = false, $inactive = false, $raw = false)
    {
        $extensionInformation = [];
        if (!$active || $inactive) {
            $this->emitPackagesMayHaveChangedSignal();
            $packages = $this->packageManager->getAvailablePackages();
        } else {
            $packages = $this->packageManager->getActivePackages();
        }
        foreach ($packages as $package) {
            if ($inactive && $this->packageManager->isPackageActive($package->getPackageKey())) {
                continue;
            }
            $metaData = $package->getPackageMetaData();
            $extensionInformation[] = [
                'package_key' => $package->getPackageKey(),
                'version' => $metaData->getVersion(),
                'description' => $metaData->getDescription(),
            ];
        }
        if ($raw) {
            $this->outputLine('%s', [implode(PHP_EOL, array_column($extensionInformation, 'package_key'))]);
        } else {
            $this->output->outputTable(
                $extensionInformation,
                ['Extension key', 'Version', 'Description']
            );
        }
    }

    /**
     * @return InstallUtility
     */
    private function getExtensionInstaller(): InstallUtility
    {
        if ($this->extensionInstaller === null) {
            $this->extensionInstaller = $this->objectManager->get(InstallUtility::class);
        }

        return $this->extensionInstaller;
    }

    /**
     * Emits packages may have changed signal
     */
    protected function emitPackagesMayHaveChangedSignal()
    {
        $this->signalSlotDispatcher->dispatch('PackageManagement', 'packagesMayHaveChanged');
    }
}


<?php
declare(strict_types=1);
namespace Helhum\Typo3Console\TYPO3v87\Service;

/*
 * This file is part of the TYPO3 Console project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read
 * LICENSE file that was distributed with this source code.
 *
 */

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Flushes caches low level
 */
class CacheLowLevelCleaner
{
    /**
     * Recursively delete cache directory
     */
    public function forceFlushCachesFiles()
    {
        $cacheDirectory = PATH_site . 'typo3temp/var/Cache';
        GeneralUtility::flushDirectory($cacheDirectory, true);
    }

    /**
     * Truncate all DB tables prefixed with 'cf_'
     */
    public function forceFlushDatabaseCacheTables()
    {
        // Get all table names from Default connection starting with 'cf_' and truncate them
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
        $connection = $connectionPool->getConnectionByName('Default');
        $tableNames = $connection->getSchemaManager()->listTableNames();
        foreach ($tableNames as $tableName) {
            if ($tableName === 'cache_treelist' || strpos($tableName, 'cf_') === 0) {
                $connection->truncate($tableName);
            }
        }
        // Check tables on other connections
        $remappedTables = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
            ? array_keys((array)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
            : [];
        foreach ($remappedTables as $tableName) {
            if ($tableName === 'cache_treelist' || strpos($tableName, 'cf_') === 0) {
                $connectionPool->getConnectionForTable($tableName)->truncate($tableName);
            }
        }
    }
}