TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/vendor/
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Authentication;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Exception\RfcComplianceException;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||||
|
use TYPO3\CMS\Core\Log\LogManager;
|
||||||
|
use TYPO3\CMS\Core\Mail\MailerInterface;
|
||||||
|
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\MailUtility;
|
||||||
|
use TYPO3\CMS\Install\Service\SessionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates a user (currently comparing it through the install tool password, but could be extended)
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
readonly class AuthenticationService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected MailerInterface $mailer,
|
||||||
|
protected TemplatedEmailFactory $emailFactory,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks against a given password
|
||||||
|
*
|
||||||
|
* @param string|null $password
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
* @return bool if authentication was successful, otherwise false
|
||||||
|
*/
|
||||||
|
public function loginWithPassword($password, ServerRequestInterface $request, SessionService $session): bool
|
||||||
|
{
|
||||||
|
$validPassword = false;
|
||||||
|
if ($password !== null && $password !== '') {
|
||||||
|
$installToolPassword = $GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'];
|
||||||
|
$hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
|
||||||
|
try {
|
||||||
|
$hashInstance = $hashFactory->get($installToolPassword, 'BE');
|
||||||
|
// @todo: This code should check required hash updates and update the hash if needed
|
||||||
|
$validPassword = $hashInstance->checkPassword($password, $installToolPassword);
|
||||||
|
} catch (InvalidPasswordHashException $e) {
|
||||||
|
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
|
||||||
|
$logger->error(
|
||||||
|
'Invalid install tool password hash specified in "BE/installToolPassword" configuration.',
|
||||||
|
['exceptionMessage' => $e->getMessage()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($validPassword) {
|
||||||
|
$session->setAuthorized();
|
||||||
|
$this->sendLoginSuccessfulMail($request);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$this->sendLoginFailedMail($request);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If install tool login mail is set, send a mail for a successful login.
|
||||||
|
*/
|
||||||
|
protected function sendLoginSuccessfulMail(ServerRequestInterface $request): void
|
||||||
|
{
|
||||||
|
$warningEmailAddress = $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'];
|
||||||
|
if (!$warningEmailAddress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$email = $this->emailFactory->createWithOverrides(
|
||||||
|
[20 => 'EXT:install/Resources/Private/Templates/Email/'],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
$request,
|
||||||
|
);
|
||||||
|
$email
|
||||||
|
->to($warningEmailAddress)
|
||||||
|
->subject('Install Tool Login at \'' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '\'')
|
||||||
|
->from(new Address($this->getSenderEmailAddress(), $this->getSenderEmailName()))
|
||||||
|
->setTemplate('Security/InstallToolLogin');
|
||||||
|
$this->sendEmail($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If install tool login mail is set, send a mail for a failed login.
|
||||||
|
*/
|
||||||
|
protected function sendLoginFailedMail(ServerRequestInterface $request): void
|
||||||
|
{
|
||||||
|
$warningEmailAddress = $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'];
|
||||||
|
if (!$warningEmailAddress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$formValues = $request->getParsedBody()['install'] ?? $request->getQueryParams()['install'] ?? null;
|
||||||
|
$email = $this->emailFactory->createWithOverrides(
|
||||||
|
[20 => 'EXT:install/Resources/Private/Templates/Email/'],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
$request,
|
||||||
|
);
|
||||||
|
$email
|
||||||
|
->to($warningEmailAddress)
|
||||||
|
->subject('Install Tool Login ATTEMPT at \'' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '\'')
|
||||||
|
->from(new Address($this->getSenderEmailAddress(), $this->getSenderEmailName()))
|
||||||
|
->setTemplate('Security/InstallToolLoginAttempt');
|
||||||
|
$this->sendEmail($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an email and gracefully logs if the mail could not be sent due to configuration errors.
|
||||||
|
*
|
||||||
|
* @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
|
||||||
|
*/
|
||||||
|
protected function sendEmail(RawMessage $email): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->mailer->send($email);
|
||||||
|
} catch (TransportException $e) {
|
||||||
|
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
|
||||||
|
$logger->warning('Could not send notification email to ' . $this->getSenderEmailAddress() . ' due to mailer settings error', [
|
||||||
|
'recipientList' => $this->getSenderEmailAddress(),
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
} catch (RfcComplianceException $e) {
|
||||||
|
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
|
||||||
|
$logger->warning('Could not send notification email to ' . $this->getSenderEmailAddress() . ' due to invalid email address', [
|
||||||
|
'recipientList' => $this->getSenderEmailAddress(),
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sender address from configuration
|
||||||
|
* ['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress']
|
||||||
|
* If this setting is empty fall back to 'no-reply@example.com'
|
||||||
|
*
|
||||||
|
* @return string Returns an email address
|
||||||
|
*/
|
||||||
|
protected function getSenderEmailAddress()
|
||||||
|
{
|
||||||
|
return MailUtility::getSystemFromAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets sender name from configuration
|
||||||
|
* ['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName']
|
||||||
|
* If this setting is empty, it falls back to a default string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getSenderEmailName()
|
||||||
|
{
|
||||||
|
return MailUtility::getSystemFromName() ?: 'TYPO3 CMS install tool';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumeration object for backend user groups
|
||||||
|
* @internal only and subject to change or be removed in TYPO3 v13. Usable only within `EXT:install`.
|
||||||
|
*/
|
||||||
|
enum BackendUserGroupType: string
|
||||||
|
{
|
||||||
|
case EDITOR = 'Editor';
|
||||||
|
case ADVANCED_EDITOR = 'Advanced Editor';
|
||||||
|
case ALL = 'Both';
|
||||||
|
case NONE = 'None';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getAllUserGroupTypes(): array
|
||||||
|
{
|
||||||
|
$groups = [];
|
||||||
|
foreach (self::cases() as $group) {
|
||||||
|
$groups[] = $group->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all but the "ALL|NONE" special type
|
||||||
|
*
|
||||||
|
* @return array<non-empty-string, non-empty-string>
|
||||||
|
*/
|
||||||
|
public function getActualUserGroupTypes(): array
|
||||||
|
{
|
||||||
|
$allGroups = self::cases();
|
||||||
|
$specificUserGroups = [];
|
||||||
|
foreach ($allGroups as $specificGroup) {
|
||||||
|
if (in_array($specificGroup->name, ['ALL', 'NONE'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$specificUserGroups[$specificGroup->name] = $specificGroup->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $specificUserGroups;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Question\Question;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||||
|
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||||
|
use TYPO3\CMS\Core\Exception\InvalidPasswordRulesException;
|
||||||
|
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||||
|
use TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGeneratorInterface;
|
||||||
|
use TYPO3\CMS\Core\PasswordPolicy\PasswordService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
final class PasswordSetCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
private readonly PasswordHashFactory $passwordHashFactory,
|
||||||
|
private readonly ConfigurationManager $configurationManager,
|
||||||
|
private readonly LanguageServiceFactory $languageServiceFactory,
|
||||||
|
private readonly PasswordService $passwordService,
|
||||||
|
) {
|
||||||
|
parent::__construct($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addOption(
|
||||||
|
'password-length',
|
||||||
|
'p',
|
||||||
|
InputOption::VALUE_OPTIONAL,
|
||||||
|
'Specify the length of auto-generated passwords.',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'dry-run',
|
||||||
|
'd',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'If this option is set, the password would only be shown but not saved in settings. This also reveals the resulting hash.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an install-tool password (either auto-generated or manual) and stores the hash (if not --dry-run)
|
||||||
|
*
|
||||||
|
* @throws InvalidPasswordHashException
|
||||||
|
* @throws InvalidPasswordRulesException
|
||||||
|
*/
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
if (!Bootstrap::checkIfEssentialConfigurationExists($this->configurationManager)) {
|
||||||
|
$io->error('Setting an Install Tool password requires a working installation (configuration files are missing).');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentSettingsWithoutAdditionalParsing = $this->configurationManager->getMergedLocalConfiguration();
|
||||||
|
if (($currentSettingsWithoutAdditionalParsing['BE']['installToolPassword'] ?? '') !== ($GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'] ?? '')) {
|
||||||
|
$io->error('Your Install Tool password is different in settings.php and additional.php. This command can only effectively change the password for "settings.php" and therefore any changes would not take effect.');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dryRun = $input->getOption('dry-run');
|
||||||
|
$noInteraction = $input->getOption('no-interaction');
|
||||||
|
|
||||||
|
$password = !$noInteraction
|
||||||
|
? $this
|
||||||
|
->getQuestionHelper()
|
||||||
|
->ask($input, $output, (new Question('Password (leave empty for auto generation): '))->setHidden(true))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if ($password === null) {
|
||||||
|
$generator = $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['installTool']['generator'] ?? null;
|
||||||
|
if (!class_exists($generator['className'] ?? '') || !is_array($generator['options'] ?? null)) {
|
||||||
|
throw new \LogicException(
|
||||||
|
'The TYPO3_CONF_VARS.SYS.passwordPolicies.installTool.generator configuration is misconfigured.'
|
||||||
|
. ' Please ensure that the sub key \'className\' is set, and the sub key \'options\' is an array of required option values.',
|
||||||
|
1770131006
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwordGeneratorClassName = $generator['className'];
|
||||||
|
$passwordGeneratorOptions = $generator['options'];
|
||||||
|
|
||||||
|
$passwordGenerator = GeneralUtility::makeInstance($passwordGeneratorClassName);
|
||||||
|
if (!$passwordGenerator instanceof PasswordGeneratorInterface) {
|
||||||
|
throw new \LogicException('Class ' . $passwordGeneratorClassName . ' does not implement PasswordGeneratorInterface', 1770131293);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input->getOption('password-length') !== null) {
|
||||||
|
$passwordGeneratorOptions['length'] = (int)$input->getOption('password-length');
|
||||||
|
}
|
||||||
|
$length = $passwordGeneratorOptions['length'];
|
||||||
|
$password = $passwordGenerator->generate($passwordGeneratorOptions);
|
||||||
|
$output->writeln(sprintf('Password length: %d characters', $length));
|
||||||
|
// A generated password may contain '<', '>' or '\', which Symfony's
|
||||||
|
// OutputFormatter would silently mangle on display. Emit the password via
|
||||||
|
// OUTPUT_RAW so it bypasses the formatter entirely, and apply the 'info'
|
||||||
|
// style manually when decoration is on to keep the green highlight.
|
||||||
|
$coloredPassword = $output->isDecorated()
|
||||||
|
? $output->getFormatter()->getStyle('info')->apply($password)
|
||||||
|
: $password;
|
||||||
|
$output->write('Generated password: ');
|
||||||
|
$output->writeln($coloredPassword, OutputInterface::OUTPUT_RAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation error messages require a valid LANG object to operate on (or a backend user context, which we don't need here)
|
||||||
|
$GLOBALS['LANG'] = $this->languageServiceFactory->create('en');
|
||||||
|
$validationResultErrors = $this->passwordService->getValidationErrorsForInstallToolUpdate($password);
|
||||||
|
if ($validationResultErrors !== []) {
|
||||||
|
$output->writeln('Your password could not be used. The following validation rules did not pass:');
|
||||||
|
foreach ($validationResultErrors as $validatorKey => $message) {
|
||||||
|
$output->writeln(sprintf(' - <error>%s</error> (%s)', $message, $validatorKey));
|
||||||
|
}
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwordHashed = $this->passwordHashFactory->getDefaultHashInstance('BE')->getHashedPassword($password);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$output->writeln(sprintf('Password hashed (dry run): <info>%s</info>', $passwordHashed));
|
||||||
|
} else {
|
||||||
|
$this
|
||||||
|
->configurationManager
|
||||||
|
->setLocalConfigurationValueByPath('BE/installToolPassword', $passwordHashed);
|
||||||
|
|
||||||
|
$output->writeln('<info>Install Tool password updated in "settings.php".</info>');
|
||||||
|
$output->writeln('<comment>Please note that a custom override of this password in "additional.php" will have higher priority.</comment>');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getQuestionHelper(): QuestionHelper
|
||||||
|
{
|
||||||
|
/** @var QuestionHelper $helper */
|
||||||
|
$helper = $this->getHelper('question');
|
||||||
|
return $helper;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,774 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Command;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Exception as DBALException;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||||
|
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||||
|
use Symfony\Component\Console\Question\Question;
|
||||||
|
use TYPO3\CMS\Core\Authentication\CommandLineUserCreation;
|
||||||
|
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||||
|
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Package\FailsafePackageManager;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\Service\Exception\ConfigurationFileAlreadyExistsException;
|
||||||
|
use TYPO3\CMS\Install\Service\LateBootService;
|
||||||
|
use TYPO3\CMS\Install\Service\SetupDatabaseService;
|
||||||
|
use TYPO3\CMS\Install\Service\SetupService;
|
||||||
|
use TYPO3\CMS\Install\WebserverType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI command for setting up TYPO3 via CLI
|
||||||
|
*/
|
||||||
|
class SetupCommand extends Command
|
||||||
|
{
|
||||||
|
protected array $connectionLabels = [
|
||||||
|
'mysqli' => '[MySQLi] Manually configured MySQL TCP/IP connection',
|
||||||
|
'mysqliSocket' => '[MySQLi] Manually configured MySQL socket connection',
|
||||||
|
'pdoMysql' => '[PDO] Manually configured MySQL TCP/IP connection',
|
||||||
|
'pdoMysqlSocket' => '[PDO] Manually configured MySQL socket connection',
|
||||||
|
'postgres' => 'Manually configured PostgreSQL connection',
|
||||||
|
'sqlite' => 'Manually configured SQLite connection',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
private readonly SetupService $setupService,
|
||||||
|
private readonly ConfigurationManager $configurationManager,
|
||||||
|
private readonly LateBootService $lateBootService,
|
||||||
|
private readonly FailsafePackageManager $packageManager,
|
||||||
|
) {
|
||||||
|
parent::__construct($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Setup TYPO3 via CLI using environment variables, CLI options or interactive')
|
||||||
|
// Connection Parameters
|
||||||
|
->addOption(
|
||||||
|
'driver',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_OPTIONAL,
|
||||||
|
'Select which database driver to use',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'host',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Set the database host to use',
|
||||||
|
'db'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'port',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_OPTIONAL,
|
||||||
|
'Set the database port to use',
|
||||||
|
'3306'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'dbname',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Set the database name to use',
|
||||||
|
'db'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'username',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Set the database username to use',
|
||||||
|
'db'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'password',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Set the database password to use'
|
||||||
|
)
|
||||||
|
// User to be created
|
||||||
|
->addOption(
|
||||||
|
'admin-username',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_OPTIONAL,
|
||||||
|
'Set a username',
|
||||||
|
'admin'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'admin-user-password',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Set users password'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'admin-email',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Set users email',
|
||||||
|
''
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'project-name',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Set the TYPO3 project name',
|
||||||
|
'New TYPO3 Project'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'create-site',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Create a basic site setup (root page and site configuration) with the given domain, ex. "https://my.domain.tld/"',
|
||||||
|
false
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'distribution',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_OPTIONAL,
|
||||||
|
$this->packageManager->isPackageActive('impexp')
|
||||||
|
? 'Import a distribution during site creation (package key, e.g. "theme_camino")'
|
||||||
|
: '[disabled] Requires typo3/cms-impexp to be installed'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'server-type',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_OPTIONAL,
|
||||||
|
'Define the web server the TYPO3 installation will be running on',
|
||||||
|
'other'
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'force',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Force settings overwrite - use this if TYPO3 has been installed already',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'no-interaction',
|
||||||
|
'n',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Do not ask any interactive question',
|
||||||
|
)->setHelp(
|
||||||
|
<<<EOT
|
||||||
|
The command offers 3 ways to setup TYPO3:
|
||||||
|
1. environment variables
|
||||||
|
2. commandline options
|
||||||
|
3. interactive guided walk-through
|
||||||
|
|
||||||
|
All values are validated no matter where it was set.
|
||||||
|
If a value is missing, the user will be asked for it.
|
||||||
|
|
||||||
|
<fg=green>Setup using environment variables</>
|
||||||
|
---------------------------------
|
||||||
|
TYPO3_DB_DRIVER=mysqli \
|
||||||
|
TYPO3_DB_USERNAME=db \
|
||||||
|
TYPO3_DB_PORT=3306 \
|
||||||
|
TYPO3_DB_HOST=db \
|
||||||
|
TYPO3_DB_DBNAME=db \
|
||||||
|
TYPO3_SETUP_ADMIN_EMAIL=admin@example.com \
|
||||||
|
TYPO3_SETUP_ADMIN_USERNAME=admin \
|
||||||
|
TYPO3_SETUP_CREATE_SITE="https://your-typo3-site.com/" \
|
||||||
|
TYPO3_SETUP_DISTRIBUTION="theme_camino" \
|
||||||
|
TYPO3_PROJECT_NAME="Automated Setup" \
|
||||||
|
TYPO3_SERVER_TYPE="apache" \
|
||||||
|
./bin/typo3 setup --force
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
<fg=yellow>
|
||||||
|
Variable `TYPO3_DB_PASSWORD` (option `--password`) can be used to provide a
|
||||||
|
password for the database and `TYPO3_SETUP_ADMIN_PASSWORD`
|
||||||
|
(option `--admin-user-password`) for the admin user password.
|
||||||
|
Using this can be a security risk since the password may end up in shell
|
||||||
|
history files. Prefer the interactive mode. Additionally, writing a command
|
||||||
|
to shell history can be suppressed by prefixing the command with a space
|
||||||
|
when using `bash` or `zsh`.
|
||||||
|
</>
|
||||||
|
EOT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the installation / setup process
|
||||||
|
*/
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$input->setInteractive(!$input->getOption('no-interaction'));
|
||||||
|
/** @var QuestionHelper $questionHelper */
|
||||||
|
$questionHelper = $this->getHelper('question');
|
||||||
|
|
||||||
|
// Ensure all required files and folders exist
|
||||||
|
$this->setupService->createDirectoryStructure($this->getServerType($questionHelper, $input, $output));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$force = $input->getOption('force');
|
||||||
|
$this->setupService->prepareSystemSettings($force);
|
||||||
|
} catch (ConfigurationFileAlreadyExistsException) {
|
||||||
|
$configOverwriteQuestion = new ConfirmationQuestion(
|
||||||
|
'Configuration already exists do you want to overwrite it [default: no] ? ',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
$configOverwrite = $questionHelper->ask($input, $output, $configOverwriteQuestion);
|
||||||
|
|
||||||
|
if (!$configOverwrite) {
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setupService->prepareSystemSettings(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$container = $this->lateBootService->getContainer();
|
||||||
|
$backup = $this->lateBootService->makeCurrent($container);
|
||||||
|
$setupDatabaseService = $container->get(SetupDatabaseService::class);
|
||||||
|
|
||||||
|
// Get database connection details
|
||||||
|
$databaseConnection = $this->getConnectionDetails($setupDatabaseService, $questionHelper, $input, $output);
|
||||||
|
|
||||||
|
// Select the database and prepare it
|
||||||
|
if ($exitCode = $this->selectAndImportDatabase($setupDatabaseService, $questionHelper, $input, $output, $databaseConnection)) {
|
||||||
|
return $exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$container->get(CommandLineUserCreation::class)->ensureCliUserExists();
|
||||||
|
$this->lateBootService->makeCurrent(null, $backup);
|
||||||
|
|
||||||
|
$username = $this->getAdminUserName($questionHelper, $input, $output);
|
||||||
|
$password = $this->getAdminUserPassword($setupDatabaseService, $questionHelper, $input, $output);
|
||||||
|
if ($password !== null) {
|
||||||
|
$email = $this->getAdminEmailAddress($questionHelper, $input, $output);
|
||||||
|
$this->setupService->createUser($username, $password, $email);
|
||||||
|
$this->setupService->setInstallToolPassword($password);
|
||||||
|
} elseif ($output->isVerbose()) {
|
||||||
|
$errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
|
||||||
|
$errOutput->writeln('<info>No admin password defined. Skipped user creation.</info>');
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteName = $this->getProjectName($questionHelper, $input, $output);
|
||||||
|
$this->setupService->setSiteName($siteName);
|
||||||
|
|
||||||
|
$distributions = $this->setupService->getAvailableDistributions();
|
||||||
|
$distributionFromCli = $this->getFallbackValueEnvOrOption($input, 'distribution', 'TYPO3_SETUP_DISTRIBUTION');
|
||||||
|
$createSiteFromCli = $this->getFallbackValueEnvOrOption($input, 'create-site', 'TYPO3_SETUP_CREATE_SITE');
|
||||||
|
if ($distributionFromCli !== false && $createSiteFromCli !== false) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'The --distribution and --create-site commandline options may not be used at the same time',
|
||||||
|
1775034289
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($distributions['active'] === []) {
|
||||||
|
$selectedDistribution = $this->getDistributionForActivation($distributions['inactive'], $questionHelper, $input, $output);
|
||||||
|
if ($selectedDistribution !== null) {
|
||||||
|
// Distribution handles all site creation (pages, content, site configuration)
|
||||||
|
$this->setupService->activateDistributionPackage($selectedDistribution);
|
||||||
|
} else {
|
||||||
|
$siteUrl = $this->getSiteSetup($questionHelper, $input, $output);
|
||||||
|
if ($siteUrl) {
|
||||||
|
$this->setupService->createSite('main', $siteUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($distributionFromCli !== false || $createSiteFromCli !== false) {
|
||||||
|
$this->writeWarning(
|
||||||
|
$output,
|
||||||
|
'The --distribution and --create-site commandline options have no effect, when distributions are already active'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// The new container is kept in GeneralUtility because the following code, especially
|
||||||
|
// the current state of the data import during extension setup still relies on GeneralUtility::makeInstance
|
||||||
|
// to fetch objects from the container
|
||||||
|
$container = $this->lateBootService->loadExtLocalconfDatabase(false);
|
||||||
|
$setupDatabaseService->markWizardsDone($container);
|
||||||
|
Bootstrap::initializeBackendAuthentication();
|
||||||
|
$this->setupService->setupExtensions($container);
|
||||||
|
$this->writeSuccess($output, 'Congratulations - TYPO3 Setup is done.');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function selectAndImportDatabase(
|
||||||
|
SetupDatabaseService $setupDatabaseService,
|
||||||
|
QuestionHelper $questionHelper,
|
||||||
|
InputInterface $input,
|
||||||
|
OutputInterface $output,
|
||||||
|
mixed $databaseConnection,
|
||||||
|
): int {
|
||||||
|
if ($databaseConnection['driver'] !== 'pdo_sqlite') {
|
||||||
|
// Set temporary database configuration, so we are able to
|
||||||
|
// get the available databases listed
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME] = $databaseConnection;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$databaseList = $setupDatabaseService->getDatabaseList();
|
||||||
|
} catch (DBALException $exception) {
|
||||||
|
$this->writeError($output, $exception->getMessage());
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($databaseList === []) {
|
||||||
|
$this->writeError($output, 'No databases are available to the specified user. At least one usable database needs to be accessible.');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbChoices = [];
|
||||||
|
foreach ($databaseList as $database) {
|
||||||
|
$usable = $database['tables'] > 0 ? '<fg=red>☓</>' : '<fg=green>✓</>';
|
||||||
|
$dbChoices[$database['name']] = $database['name'] . ' (Tables ' . $database['tables'] . ' ' . $usable . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbNameValidator = static function ($dbname) use ($dbChoices, $databaseList) {
|
||||||
|
if (!($dbChoices[$dbname] ?? false)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'The selected database "' . $dbname . '" is not available, pick one of these: ' . implode(
|
||||||
|
', ',
|
||||||
|
array_keys($dbChoices)
|
||||||
|
) . '.',
|
||||||
|
1669747192,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedDatabase = $databaseList[array_search($dbname, array_keys($dbChoices), true)];
|
||||||
|
if ($selectedDatabase['tables'] !== 0) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'The selected database contains already ' . $selectedDatabase['tables'] . ' tables. Please delete all tables or select another database.',
|
||||||
|
1669747200,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dbname;
|
||||||
|
};
|
||||||
|
|
||||||
|
$dbnameFromCli = $this->getFallbackValueEnvOrOption($input, 'dbname', 'TYPO3_DB_DBNAME');
|
||||||
|
if ($dbnameFromCli === false && $input->isInteractive()) {
|
||||||
|
$dbname = new ChoiceQuestion('Select which database to use: ', $dbChoices);
|
||||||
|
$dbname->setValidator($dbNameValidator);
|
||||||
|
$databaseConnection['database'] = $questionHelper->ask($input, $output, $dbname);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$dbNameValidator($dbnameFromCli);
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
$this->writeError($output, $e->getMessage());
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$databaseConnection['database'] = $dbnameFromCli;
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkDatabase = $setupDatabaseService->checkExistingDatabase($databaseConnection['database']);
|
||||||
|
if ($checkDatabase->getSeverity() !== ContextualFeedbackSeverity::OK) {
|
||||||
|
$this->writeError($output, $checkDatabase->getMessage());
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$databaseConnection['availableSet'] = 'sqliteManualConfiguration';
|
||||||
|
}
|
||||||
|
|
||||||
|
[$success, $messages] = $setupDatabaseService->setDefaultConnectionSettings($databaseConnection);
|
||||||
|
if (!$success) {
|
||||||
|
foreach ($messages as $message) {
|
||||||
|
$this->writeError($output, $message->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the actual config written to disk
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME] = $this->configurationManager->getLocalConfigurationValueByPath('DB/Connections/Default');
|
||||||
|
|
||||||
|
$setupDatabaseService->checkRequiredDatabasePermissions();
|
||||||
|
$importResults = $setupDatabaseService->importDatabaseData();
|
||||||
|
foreach ($importResults as $result) {
|
||||||
|
$this->writeError($output, (string)$result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($importResults) > 0) {
|
||||||
|
$this->writeError($output, 'Database import failed. Please see the errors shown above');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getConnectionDetails(
|
||||||
|
SetupDatabaseService $setupDatabaseService,
|
||||||
|
QuestionHelper $questionHelper,
|
||||||
|
InputInterface $input,
|
||||||
|
OutputInterface $output,
|
||||||
|
): array {
|
||||||
|
$input->hasParameterOption('--driver');
|
||||||
|
$driverTypeCli = $this->getFallbackValueEnvOrOption($input, 'driver', 'TYPO3_DB_DRIVER');
|
||||||
|
$driverOptions = $setupDatabaseService->getDriverOptions();
|
||||||
|
$availableConnectionTypes = implode(', ', array_keys($this->connectionLabels));
|
||||||
|
|
||||||
|
$connectionValidator = static function ($connectionType) use ($driverOptions, $availableConnectionTypes) {
|
||||||
|
if (!isset($driverOptions[$connectionType . 'ManualConfigurationOptions'])) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'The connection type "' . $connectionType . '" does not exist. Please use one of the following ' . $availableConnectionTypes,
|
||||||
|
1669905551,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $connectionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($driverTypeCli === false && $input->isInteractive()) {
|
||||||
|
$driver = new ChoiceQuestion('Database driver?', $this->connectionLabels);
|
||||||
|
$driver->setValidator($connectionValidator);
|
||||||
|
$driverType = $questionHelper->ask($input, $output, $driver);
|
||||||
|
} else {
|
||||||
|
$driverType = $connectionValidator($driverTypeCli);
|
||||||
|
}
|
||||||
|
|
||||||
|
$databaseConnectionOptions = $driverOptions[$driverType . 'ManualConfigurationOptions'];
|
||||||
|
|
||||||
|
// Ask for connection details
|
||||||
|
foreach ($databaseConnectionOptions as $key => $value) {
|
||||||
|
switch ($key) {
|
||||||
|
case 'database':
|
||||||
|
case 'socket':
|
||||||
|
case 'driver':
|
||||||
|
break;
|
||||||
|
case 'username':
|
||||||
|
$usernameFromCli = $this->getFallbackValueEnvOrOption($input, 'username', 'TYPO3_DB_USERNAME');
|
||||||
|
$emptyValidator = static function ($value) {
|
||||||
|
if (empty($value)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'The value must not be empty.',
|
||||||
|
1669747578,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($usernameFromCli === false && $input->isInteractive()) {
|
||||||
|
$default = $this->getDefinition()->getOption($key)->getDefault();
|
||||||
|
$defaultLabel = ' [default: ' . $default . ']';
|
||||||
|
$question = new Question('Enter the database "username"' . $defaultLabel . ' ? ', $default);
|
||||||
|
$question->setValidator($emptyValidator);
|
||||||
|
$username = $questionHelper->ask($input, $output, $question);
|
||||||
|
// @todo: Investigate the difference between username and user.... why?
|
||||||
|
$databaseConnectionOptions['username'] = $username;
|
||||||
|
$databaseConnectionOptions['user'] = $username;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validUsername = $emptyValidator($usernameFromCli);
|
||||||
|
$databaseConnectionOptions['username'] = $validUsername;
|
||||||
|
$databaseConnectionOptions['user'] = $validUsername;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$envValue = $this->getFallbackValueEnvOrOption($input, $key, 'TYPO3_DB_' . strtoupper($key));
|
||||||
|
$default = $this->getDefinition()->getOption($key)->getDefault();
|
||||||
|
$defaultLabel = empty($value) ? '' : ' [default: ' . $default . ']';
|
||||||
|
$question = new Question('Enter the database "' . $key . '"' . $defaultLabel . ' ? ', $default);
|
||||||
|
if ($key === 'password') {
|
||||||
|
$question = new Question('Enter the database "' . $key . '" ? ', $default);
|
||||||
|
$question->setHidden(true);
|
||||||
|
$question->setHiddenFallback(false);
|
||||||
|
} elseif ($key === 'host') {
|
||||||
|
$hostValidator = function ($host) use ($setupDatabaseService) {
|
||||||
|
if (!$setupDatabaseService->isValidDbHost($host)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Please enter a valid database host name.',
|
||||||
|
1669747572
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $host;
|
||||||
|
};
|
||||||
|
$question->setValidator($hostValidator);
|
||||||
|
} elseif ($key === 'port') {
|
||||||
|
$portValidator = function ($port) use ($setupDatabaseService) {
|
||||||
|
$port = (int)$port;
|
||||||
|
if (!$setupDatabaseService->isValidDbPort($port)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Please use a port in the range between 1 and 65535.',
|
||||||
|
1669747592,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $port;
|
||||||
|
};
|
||||||
|
$question->setValidator($portValidator);
|
||||||
|
} else {
|
||||||
|
$emptyValidator = function ($value) {
|
||||||
|
if (empty($value)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'The value must not be empty.',
|
||||||
|
1669747601,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
};
|
||||||
|
$question->setValidator($emptyValidator);
|
||||||
|
}
|
||||||
|
if ($envValue === false && $key === 'password') {
|
||||||
|
// Force this question if no `TYPO3_DB_PASSWORD` set via cli.
|
||||||
|
// Thus, the user will always be prompted for a password even --no-interaction is set.
|
||||||
|
$currentlyInteractive = $input->isInteractive();
|
||||||
|
$input->setInteractive(true);
|
||||||
|
$value = $questionHelper->ask($input, $output, $question);
|
||||||
|
$input->setInteractive($currentlyInteractive);
|
||||||
|
} elseif ($envValue === false && $input->isInteractive()) {
|
||||||
|
$value = $questionHelper->ask($input, $output, $question);
|
||||||
|
} else {
|
||||||
|
// All passed in values should go through the set validator,
|
||||||
|
// therefore, we can't break early
|
||||||
|
$validator = $question->getValidator();
|
||||||
|
$envValue = $envValue ?: $default;
|
||||||
|
$value = $validator ? $validator($envValue) : $envValue;
|
||||||
|
}
|
||||||
|
$databaseConnectionOptions[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $databaseConnectionOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getServerType(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): WebserverType
|
||||||
|
{
|
||||||
|
$serverTypeValidator = static function (?string $serverType): WebserverType {
|
||||||
|
if (!array_key_exists($serverType, WebserverType::getDescriptions())) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Webserver must be any of ' . implode(', ', array_keys(WebserverType::getDescriptions())),
|
||||||
|
1682329380,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebserverType::from($serverType);
|
||||||
|
};
|
||||||
|
$serverTypeFromCli = $this->getFallbackValueEnvOrOption($input, 'server-type', 'TYPO3_SERVER_TYPE');
|
||||||
|
if ($serverTypeFromCli === false && $input->isInteractive()) {
|
||||||
|
$questionServerType = new ChoiceQuestion('Which web server is used?', WebserverType::getDescriptions());
|
||||||
|
$questionServerType->setValidator($serverTypeValidator);
|
||||||
|
return $questionHelper->ask($input, $output, $questionServerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $serverTypeValidator($serverTypeFromCli);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getAdminUserName(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string
|
||||||
|
{
|
||||||
|
$usernameValidator = static function ($username) {
|
||||||
|
if (empty($username)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Admin username must not be empty.',
|
||||||
|
1669747607,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $username;
|
||||||
|
};
|
||||||
|
|
||||||
|
$usernameFromCli = $this->getFallbackValueEnvOrOption($input, 'admin-username', 'TYPO3_SETUP_ADMIN_USERNAME');
|
||||||
|
if ($usernameFromCli === false && $input->isInteractive()) {
|
||||||
|
$questionUsername = new Question('Admin username (user will be "system maintainer") ? ');
|
||||||
|
$questionUsername->setValidator($usernameValidator);
|
||||||
|
return $questionHelper->ask($input, $output, $questionUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default value for 'admin-username' if in non-interactive mode
|
||||||
|
if ($usernameFromCli === false && !$input->isInteractive()) {
|
||||||
|
$usernameFromCli = $this->getDefinition()->getOption('admin-username')->getDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $usernameValidator($usernameFromCli);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getAdminUserPassword(
|
||||||
|
SetupDatabaseService $setupDatabaseService,
|
||||||
|
QuestionHelper $questionHelper,
|
||||||
|
InputInterface $input,
|
||||||
|
OutputInterface $output,
|
||||||
|
): ?string {
|
||||||
|
$passwordValidator = function ($password) use ($setupDatabaseService) {
|
||||||
|
$passwordValidationErrors = $setupDatabaseService->getBackendUserPasswordValidationErrors((string)$password);
|
||||||
|
if (!empty($passwordValidationErrors)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Administrator password not secure enough!' . PHP_EOL
|
||||||
|
. '* ' . implode(PHP_EOL . '* ', $passwordValidationErrors),
|
||||||
|
1669747614,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
};
|
||||||
|
|
||||||
|
$passwordFromCli = $this->getFallbackValueEnvOrOption($input, 'admin-user-password', 'TYPO3_SETUP_ADMIN_PASSWORD');
|
||||||
|
if ($passwordFromCli === false && $input->isInteractive()) {
|
||||||
|
$currentlyInteractive = $input->isInteractive();
|
||||||
|
$input->setInteractive(true);
|
||||||
|
$questionPassword = new Question('Admin user and installer password ? ');
|
||||||
|
$questionPassword->setHidden(true);
|
||||||
|
$questionPassword->setHiddenFallback(false);
|
||||||
|
$questionPassword->setValidator($passwordValidator);
|
||||||
|
$password = $questionHelper->ask($input, $output, $questionPassword);
|
||||||
|
$input->setInteractive($currentlyInteractive);
|
||||||
|
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($passwordFromCli === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passwordValidator($passwordFromCli);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getAdminEmailAddress(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string
|
||||||
|
{
|
||||||
|
$emailValidator = static function ($email) {
|
||||||
|
if (!empty($email) && !GeneralUtility::validEmail($email)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'The given Email is not valid! Please try again.',
|
||||||
|
1669747620,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $email;
|
||||||
|
};
|
||||||
|
|
||||||
|
$emailFromCli = $this->getFallbackValueEnvOrOption($input, 'admin-email', 'TYPO3_SETUP_ADMIN_EMAIL');
|
||||||
|
if ($emailFromCli === false && $input->isInteractive()) {
|
||||||
|
$questionEmail = new Question('Admin user email ? ', '');
|
||||||
|
$questionEmail->setValidator($emailValidator);
|
||||||
|
|
||||||
|
return $questionHelper->ask($input, $output, $questionEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string)$emailValidator($emailFromCli);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getProjectName(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string
|
||||||
|
{
|
||||||
|
$nameFromCli = $this->getFallbackValueEnvOrOption($input, 'project-name', 'TYPO3_PROJECT_NAME');
|
||||||
|
$defaultProjectName = $this->getDefinition()->getOption('project-name')->getDefault();
|
||||||
|
|
||||||
|
if ($nameFromCli === false && $input->isInteractive()) {
|
||||||
|
$question = new Question(
|
||||||
|
'Give your project a name [default: ' . $defaultProjectName . '] ? ',
|
||||||
|
$defaultProjectName
|
||||||
|
);
|
||||||
|
|
||||||
|
return $questionHelper->ask($input, $output, $question);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $nameFromCli ?: $defaultProjectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSiteSetup(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string|bool
|
||||||
|
{
|
||||||
|
$urlValidator = static function ($url) {
|
||||||
|
if (!is_string($url) || in_array(strtolower($url), ['no', 'n'], true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (empty($url) || !GeneralUtility::isValidUrl($url)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Invalid URL provided for the site name. Please provide a valid URL.',
|
||||||
|
1669747625,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
};
|
||||||
|
|
||||||
|
$createSiteFromCli = $this->getFallbackValueEnvOrOption($input, 'create-site', 'TYPO3_SETUP_CREATE_SITE');
|
||||||
|
|
||||||
|
if ($createSiteFromCli === false && $input->isInteractive()) {
|
||||||
|
$questionCreateSite = new Question('Create a basic site? Please enter a URL [default: no] ', false);
|
||||||
|
$questionCreateSite->setValidator($urlValidator);
|
||||||
|
|
||||||
|
return $questionHelper->ask($input, $output, $questionCreateSite);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $urlValidator($createSiteFromCli);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDistributionForActivation(array $inactiveDistributions, QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): ?string
|
||||||
|
{
|
||||||
|
$distributionFromCli = $this->getFallbackValueEnvOrOption($input, 'distribution', 'TYPO3_SETUP_DISTRIBUTION');
|
||||||
|
|
||||||
|
if (!$this->packageManager->isPackageActive('impexp')) {
|
||||||
|
if ($distributionFromCli !== false) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
sprintf('Distribution "%s" is not installable, please require typo3/cms-impexp.', $distributionFromCli),
|
||||||
|
1775034287
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($distributionFromCli !== false) {
|
||||||
|
if (!isset($inactiveDistributions[$distributionFromCli])) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
sprintf('Distribution "%s" is not available.', $distributionFromCli),
|
||||||
|
1775034288
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $distributionFromCli;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input->isInteractive()) {
|
||||||
|
$choices = ['none' => 'Do not import a distribution'];
|
||||||
|
foreach ($inactiveDistributions as $packageKey => $info) {
|
||||||
|
$choices[$packageKey] = $info['title'];
|
||||||
|
}
|
||||||
|
$question = new ChoiceQuestion('Select a distribution to import [default: none]', $choices, 'none');
|
||||||
|
$answer = $questionHelper->ask($input, $output, $question);
|
||||||
|
return $answer === 'none' ? null : $answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function writeSuccess(OutputInterface $output, string $message): void
|
||||||
|
{
|
||||||
|
$output->writeln('<fg=green>✓</> ' . $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function writeWarning(OutputInterface $output, string $message): void
|
||||||
|
{
|
||||||
|
$output->writeln('<fg=yellow>!</> [Warning]: ' . $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function writeError(OutputInterface $output, string $message): void
|
||||||
|
{
|
||||||
|
$output->writeln('<fg=red>☓</> [Error]: ' . $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value from
|
||||||
|
*
|
||||||
|
* 1. cli option `$option`
|
||||||
|
* 2. environment variable `$envVar`
|
||||||
|
*
|
||||||
|
* Note that cli option has higher precedences and wins over environment variable.
|
||||||
|
*/
|
||||||
|
protected function getFallbackValueEnvOrOption(InputInterface $input, string $option, string $envVar): string|false
|
||||||
|
{
|
||||||
|
$value = ($input->hasParameterOption('--' . $option))
|
||||||
|
? $input->getOption($option)
|
||||||
|
: getenv($envVar);
|
||||||
|
return is_string($value) ? $value : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use TYPO3\CMS\Install\Service\SetupService;
|
||||||
|
|
||||||
|
final class SetupDefaultBackendUserGroupsCommand extends Command
|
||||||
|
{
|
||||||
|
private BackendUserGroupType $userGroupEnum = BackendUserGroupType::ALL;
|
||||||
|
private array $availableUserGroups = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
private readonly SetupService $setupService,
|
||||||
|
) {
|
||||||
|
parent::__construct($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->availableUserGroups = $this->userGroupEnum->getAllUserGroupTypes();
|
||||||
|
$actualGroups = $this->userGroupEnum->getActualUserGroupTypes();
|
||||||
|
$stringUserGroupList = $actualGroups !== []
|
||||||
|
? '- ' . implode("\n- ", $actualGroups)
|
||||||
|
: 'No groups available.';
|
||||||
|
$this->setDescription('Setup default backend user groups')
|
||||||
|
->addOption(
|
||||||
|
'no-interaction',
|
||||||
|
'n',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Do not ask any interactive question',
|
||||||
|
)->addOption(
|
||||||
|
'groups',
|
||||||
|
'g',
|
||||||
|
InputOption::VALUE_OPTIONAL,
|
||||||
|
'Which backend user groups do you want to create? [ ' . implode(', ', $this->availableUserGroups) . ']',
|
||||||
|
$this->userGroupEnum->value,
|
||||||
|
$this->availableUserGroups
|
||||||
|
)->addOption(
|
||||||
|
'force',
|
||||||
|
'f',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Force creating a new group with the same name, even if a group with that name already exists.'
|
||||||
|
)->setHelp(
|
||||||
|
<<<EOT
|
||||||
|
The command will allow you to create base backend user groups for your TYPO3 installation.
|
||||||
|
|
||||||
|
You can create either both or one of the following groups:
|
||||||
|
|
||||||
|
$stringUserGroupList
|
||||||
|
|
||||||
|
EOT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the backend groups setup command
|
||||||
|
*/
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$input->setInteractive(!$input->getOption('no-interaction'));
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$createGroups = $this->userGroupEnum->value;
|
||||||
|
if ($input->hasParameterOption('--groups') || $input->hasParameterOption('-g')) {
|
||||||
|
$createGroups = $input->getOption('groups');
|
||||||
|
} elseif (!$input->getOption('no-interaction')) {
|
||||||
|
$createGroups = $io->choice(
|
||||||
|
'Which backend groups do you want to create?',
|
||||||
|
$this->availableUserGroups,
|
||||||
|
$this->userGroupEnum->value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($createGroups === BackendUserGroupType::NONE->value) {
|
||||||
|
$io->info('No backend groups have been created.');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
if (!in_array($createGroups, $this->availableUserGroups, true)) {
|
||||||
|
$io->error('Invalid user group specified.');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$createEditor = false;
|
||||||
|
$createAdvancedEditor = false;
|
||||||
|
$creationNotices = [];
|
||||||
|
if ($createGroups === BackendUserGroupType::EDITOR->value || $createGroups === BackendUserGroupType::ALL->value) {
|
||||||
|
$createEditor = true;
|
||||||
|
$creationNotices[] = BackendUserGroupType::EDITOR->value;
|
||||||
|
}
|
||||||
|
if ($createGroups === BackendUserGroupType::ADVANCED_EDITOR->value || $createGroups === BackendUserGroupType::ALL->value) {
|
||||||
|
$createAdvancedEditor = true;
|
||||||
|
$creationNotices[] = BackendUserGroupType::ADVANCED_EDITOR->value;
|
||||||
|
}
|
||||||
|
$messages = $this->setupService->createBackendUserGroups($createEditor, $createAdvancedEditor, $input->hasParameterOption('-f'));
|
||||||
|
|
||||||
|
if ($messages !== []) {
|
||||||
|
foreach ($messages as $message) {
|
||||||
|
$io->warning($message);
|
||||||
|
}
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
$io->success(sprintf('Backend user group(s) created: %s', implode(', ', $creationNotices)));
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract custom preset class implements common preset code
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
abstract class AbstractCustomPreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset, always set to "Custom"
|
||||||
|
*/
|
||||||
|
protected $name = 'Custom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool TRUE if custom preset is active
|
||||||
|
*/
|
||||||
|
protected $isActive = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of custom prefix is usually the lowest
|
||||||
|
*/
|
||||||
|
protected $priority = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether custom preset is active is set by feature
|
||||||
|
*
|
||||||
|
* @return bool TRUE if custom preset is active
|
||||||
|
*/
|
||||||
|
public function isActive()
|
||||||
|
{
|
||||||
|
return $this->isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark preset as active.
|
||||||
|
* The custom features do not know by itself if they are
|
||||||
|
* active or not since the configuration options may overlay
|
||||||
|
* with other presets.
|
||||||
|
* Marking the custom preset as active is therefor taken care
|
||||||
|
* off by the feature itself if no other preset is active.
|
||||||
|
*/
|
||||||
|
public function setActive()
|
||||||
|
{
|
||||||
|
$this->isActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom configuration is always available
|
||||||
|
*
|
||||||
|
* @return bool TRUE
|
||||||
|
*/
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration values is used to persist data and is merged with given $postValues.
|
||||||
|
*
|
||||||
|
* @return array Configuration values needed to activate prefix
|
||||||
|
*/
|
||||||
|
public function getConfigurationValues()
|
||||||
|
{
|
||||||
|
return array_map(static fn($configuration) => $configuration['value'], $this->getConfigurationDescriptors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build configuration descriptors to be used in fluid to show configuration options.
|
||||||
|
* They are fetched from LocalConfiguration / DefaultConfiguration and
|
||||||
|
* merged with given $postValues.
|
||||||
|
*
|
||||||
|
* @return array Configuration values needed to activate prefix
|
||||||
|
*/
|
||||||
|
public function getConfigurationDescriptors()
|
||||||
|
{
|
||||||
|
$configurationValues = [];
|
||||||
|
foreach ($this->configurationValues as $configurationKey => $configurationValue) {
|
||||||
|
$readonly = isset($this->readonlyConfigurationValues[$configurationKey]);
|
||||||
|
if (!$readonly
|
||||||
|
&& isset($this->postValues['enable'])
|
||||||
|
&& $this->postValues['enable'] === $this->name
|
||||||
|
&& isset($this->postValues[$this->name][$configurationKey])
|
||||||
|
) {
|
||||||
|
$currentValue = $this->postValues[$this->name][$configurationKey];
|
||||||
|
} else {
|
||||||
|
$currentValue = $this->configurationManager->getConfigurationValueByPath($configurationKey);
|
||||||
|
}
|
||||||
|
$configurationValues[$configurationKey] = [
|
||||||
|
'value' => $currentValue,
|
||||||
|
'readonly' => $readonly,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $configurationValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract feature class implements common code
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
abstract class AbstractFeature
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of feature
|
||||||
|
*/
|
||||||
|
protected $name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array List of preset classes
|
||||||
|
*/
|
||||||
|
protected $presetRegistry = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Holds instances of presets
|
||||||
|
*/
|
||||||
|
protected $presetInstances = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array List of $POST values
|
||||||
|
*/
|
||||||
|
protected $postValues = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize presets of feature
|
||||||
|
*
|
||||||
|
* @param array $postValues List of $POST values of this feature
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function initializePresets(array $postValues)
|
||||||
|
{
|
||||||
|
// Give feature sub array of $POST values to preset and set to own property
|
||||||
|
$featurePostValues = [];
|
||||||
|
if (!empty($postValues[$this->name])) {
|
||||||
|
$featurePostValues = $postValues[$this->name];
|
||||||
|
}
|
||||||
|
$this->postValues = $featurePostValues;
|
||||||
|
|
||||||
|
$isNonCustomPresetActive = false;
|
||||||
|
$customPresetFound = false;
|
||||||
|
foreach ($this->presetRegistry as $presetClass) {
|
||||||
|
$presetInstance = GeneralUtility::makeInstance($presetClass);
|
||||||
|
if (!($presetInstance instanceof PresetInterface)) {
|
||||||
|
throw new Exception(
|
||||||
|
'Preset ' . $presetClass . ' does not implement PresetInterface',
|
||||||
|
1378644821
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$presetInstance->setPostValues($featurePostValues);
|
||||||
|
|
||||||
|
// Custom preset is set active if no preset before is active
|
||||||
|
if ($presetInstance->isActive()) {
|
||||||
|
$isNonCustomPresetActive = true;
|
||||||
|
}
|
||||||
|
if ($presetInstance instanceof CustomPresetInterface
|
||||||
|
&& !$isNonCustomPresetActive
|
||||||
|
) {
|
||||||
|
// Throw Exception if two custom presets are registered
|
||||||
|
if ($customPresetFound === true) {
|
||||||
|
throw new Exception(
|
||||||
|
'Preset ' . $presetClass . ' implements CustomPresetInterface, but another'
|
||||||
|
. ' custom preset is already registered',
|
||||||
|
1378645039
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var CustomPresetInterface $presetInstance */
|
||||||
|
$presetInstance->setActive();
|
||||||
|
$customPresetFound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->presetInstances[] = $presetInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return presets ordered by priority
|
||||||
|
*
|
||||||
|
* @return array|PresetInterface[]
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function getPresetsOrderedByPriority()
|
||||||
|
{
|
||||||
|
if (empty($this->presetInstances)) {
|
||||||
|
throw new Exception(
|
||||||
|
'Presets not initialized',
|
||||||
|
1378645155
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$orderedPresets = [];
|
||||||
|
foreach ($this->presetInstances as $presetInstance) {
|
||||||
|
/** @var PresetInterface $presetInstance */
|
||||||
|
$orderedPresets[$presetInstance->getPriority()] = $presetInstance;
|
||||||
|
}
|
||||||
|
krsort($orderedPresets, SORT_NUMERIC);
|
||||||
|
return $orderedPresets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return name of feature
|
||||||
|
*
|
||||||
|
* @return string Name of feature
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||||
|
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract preset class implements common preset code
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
abstract class AbstractPreset implements PresetInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \TYPO3\CMS\Core\Configuration\ConfigurationManager
|
||||||
|
*/
|
||||||
|
protected $configurationManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string Name of preset, must be set by extending classes
|
||||||
|
*/
|
||||||
|
protected $name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Default priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values that are visible but not editable via presets GUI
|
||||||
|
*/
|
||||||
|
protected $readonlyConfigurationValues = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array List of $POST values
|
||||||
|
*/
|
||||||
|
protected $postValues = [];
|
||||||
|
|
||||||
|
public function __construct(?ConfigurationManager $configurationManager = null)
|
||||||
|
{
|
||||||
|
$this->configurationManager = $configurationManager ?: GeneralUtility::makeInstance(ConfigurationManager::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set POST values
|
||||||
|
*
|
||||||
|
* @param array $postValues Post values of feature
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function setPostValues(array $postValues)
|
||||||
|
{
|
||||||
|
$this->postValues = $postValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for isAvailable, used in fluid
|
||||||
|
*
|
||||||
|
* @return bool TRUE if preset is available
|
||||||
|
*/
|
||||||
|
public function getIsAvailable()
|
||||||
|
{
|
||||||
|
return $this->isAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check is preset is currently active on the system
|
||||||
|
*
|
||||||
|
* @return bool TRUE if preset is active
|
||||||
|
*/
|
||||||
|
public function isActive()
|
||||||
|
{
|
||||||
|
$isActive = true;
|
||||||
|
foreach ($this->configurationValues as $configurationKey => $configurationValue) {
|
||||||
|
try {
|
||||||
|
$currentValue = $this->configurationManager->getConfigurationValueByPath($configurationKey);
|
||||||
|
} catch (MissingArrayPathException $e) {
|
||||||
|
$currentValue = null;
|
||||||
|
}
|
||||||
|
if ($currentValue === null && $configurationValue === '__UNSET') {
|
||||||
|
// Preset can define configuration values to be removed using `__UNSET` value handled by
|
||||||
|
// `ArrayUtility::mergeRecursiveWithOverrule()`, retrieving `$currentValue = null`. This
|
||||||
|
// case needs to be skipped to avoid taking the unset value of the other preset to flag
|
||||||
|
// the current preset as inactive. Continue with next value.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($currentValue !== $configurationValue) {
|
||||||
|
$isActive = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for isActive, used in fluid
|
||||||
|
*
|
||||||
|
* @return bool TRUE if preset is active
|
||||||
|
*/
|
||||||
|
public function getIsActive()
|
||||||
|
{
|
||||||
|
return $this->isActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get name of preset
|
||||||
|
*
|
||||||
|
* @return string Name
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get priority of preset
|
||||||
|
*
|
||||||
|
* @return int Priority, usually between 0 and 100
|
||||||
|
*/
|
||||||
|
public function getPriority()
|
||||||
|
{
|
||||||
|
return $this->priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration values to activate prefix
|
||||||
|
*
|
||||||
|
* @return array Configuration values needed to activate prefix
|
||||||
|
*/
|
||||||
|
public function getConfigurationValues()
|
||||||
|
{
|
||||||
|
return $this->configurationValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Cache;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractFeature;
|
||||||
|
use TYPO3\CMS\Install\Configuration\FeatureInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache feature sets best practices
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class CacheFeature extends AbstractFeature implements FeatureInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of feature
|
||||||
|
*/
|
||||||
|
protected $name = 'Cache';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array List of preset classes
|
||||||
|
*/
|
||||||
|
protected $presetRegistry = [
|
||||||
|
DatabaseCachePreset::class,
|
||||||
|
FileCachePreset::class,
|
||||||
|
CustomCachePreset::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Cache;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractCustomPreset;
|
||||||
|
use TYPO3\CMS\Install\Configuration\CustomPresetInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom preset is a fallback if no other preset fits
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class CustomCachePreset extends AbstractCustomPreset implements CustomPresetInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'SYS/caching/cacheConfigurations/hash/backend' => Typo3DatabaseBackend::class,
|
||||||
|
'SYS/caching/cacheConfigurations/pages/backend' => Typo3DatabaseBackend::class,
|
||||||
|
'SYS/caching/cacheConfigurations/rootline/backend' => Typo3DatabaseBackend::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Cache;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
class DatabaseCachePreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Database';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'SYS/caching/cacheConfigurations/hash/backend' => Typo3DatabaseBackend::class,
|
||||||
|
'SYS/caching/cacheConfigurations/pages/backend' => Typo3DatabaseBackend::class,
|
||||||
|
'SYS/caching/cacheConfigurations/pages/options/compression' => true,
|
||||||
|
'SYS/caching/cacheConfigurations/rootline/backend' => Typo3DatabaseBackend::class,
|
||||||
|
'SYS/caching/cacheConfigurations/rootline/options/compression' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database is always enabled
|
||||||
|
*
|
||||||
|
* @return bool TRUE if sendmail path if set
|
||||||
|
*/
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Cache;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Cache\Backend\FileBackend;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
class FileCachePreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'File';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'SYS/caching/cacheConfigurations/hash/backend' => FileBackend::class,
|
||||||
|
'SYS/caching/cacheConfigurations/pages/backend' => FileBackend::class,
|
||||||
|
'SYS/caching/cacheConfigurations/pages/options/compression' => '__UNSET',
|
||||||
|
'SYS/caching/cacheConfigurations/rootline/backend' => FileBackend::class,
|
||||||
|
'SYS/caching/cacheConfigurations/rootline/options/compression' => '__UNSET',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database is always enabled
|
||||||
|
*
|
||||||
|
* @return bool TRUE if sendmail path if set
|
||||||
|
*/
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Context;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractFeature;
|
||||||
|
use TYPO3\CMS\Install\Configuration\FeatureInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context feature sets development / production settings
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class ContextFeature extends AbstractFeature implements FeatureInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of feature
|
||||||
|
*/
|
||||||
|
protected $name = 'Context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array List of preset classes
|
||||||
|
*/
|
||||||
|
protected $presetRegistry = [
|
||||||
|
LivePreset::class,
|
||||||
|
DebugPreset::class,
|
||||||
|
CustomPreset::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Context;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractCustomPreset;
|
||||||
|
use TYPO3\CMS\Install\Configuration\CustomPresetInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom preset is a fallback if no other preset fits
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class CustomPreset extends AbstractCustomPreset implements CustomPresetInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'BE/debug' => '',
|
||||||
|
'FE/debug' => '',
|
||||||
|
'SYS/devIPmask' => '',
|
||||||
|
'SYS/displayErrors' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Context;
|
||||||
|
|
||||||
|
use Psr\Log\LogLevel;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Log\Writer\FileWriter;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug preset
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class DebugPreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Debug';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'BE/debug' => true,
|
||||||
|
'FE/debug' => true,
|
||||||
|
'SYS/devIPmask' => '*',
|
||||||
|
'SYS/displayErrors' => 1,
|
||||||
|
// Values below are not available in UI
|
||||||
|
'LOG/TYPO3/CMS/deprecations/writerConfiguration/' . LogLevel::NOTICE . '/' . FileWriter::class . '/disabled' => false,
|
||||||
|
// E_WARNING | E_RECOVERABLE_ERROR | E_DEPRECATED
|
||||||
|
'SYS/exceptionalErrors' => 12290,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Development preset is always available
|
||||||
|
*
|
||||||
|
* @return bool Always TRUE
|
||||||
|
*/
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If context is set to development, priority
|
||||||
|
* of this preset is raised.
|
||||||
|
*
|
||||||
|
* @return int Priority of preset
|
||||||
|
*/
|
||||||
|
public function getPriority()
|
||||||
|
{
|
||||||
|
$priority = $this->priority;
|
||||||
|
if (Environment::getContext()->isDevelopment()) {
|
||||||
|
$priority = $priority + 20;
|
||||||
|
}
|
||||||
|
return $priority;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Context;
|
||||||
|
|
||||||
|
use Psr\Log\LogLevel;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Log\Writer\FileWriter;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live preset
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class LivePreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Live';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'BE/debug' => false,
|
||||||
|
'FE/debug' => false,
|
||||||
|
'SYS/devIPmask' => '',
|
||||||
|
'SYS/displayErrors' => 0,
|
||||||
|
// Values below are not available in UI
|
||||||
|
'LOG/TYPO3/CMS/deprecations/writerConfiguration/' . LogLevel::NOTICE . '/' . FileWriter::class . '/disabled' => true,
|
||||||
|
// E_RECOVERABLE_ERROR
|
||||||
|
'SYS/exceptionalErrors' => 4096,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production preset is always available
|
||||||
|
*
|
||||||
|
* @return bool Always TRUE
|
||||||
|
*/
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If context is set to production, priority
|
||||||
|
* of this preset is raised.
|
||||||
|
*
|
||||||
|
* @return int Priority of preset
|
||||||
|
*/
|
||||||
|
public function getPriority()
|
||||||
|
{
|
||||||
|
$priority = $this->priority;
|
||||||
|
if (Environment::getContext()->isProduction()) {
|
||||||
|
$priority = $priority + 20;
|
||||||
|
}
|
||||||
|
return $priority;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom preset interface
|
||||||
|
*
|
||||||
|
* Interface for presets not caught by other presets.
|
||||||
|
* Represents "custom" configuration options of a feature.
|
||||||
|
*
|
||||||
|
* There must be only one custom preset per feature!
|
||||||
|
*/
|
||||||
|
interface CustomPresetInterface extends PresetInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Mark preset as active.
|
||||||
|
* The custom features do not know by itself if they are
|
||||||
|
* active or not since the configuration options may overlay
|
||||||
|
* with other presets.
|
||||||
|
* Marking the custom preset as active is therefor taken care
|
||||||
|
* off by the feature itself if no other preset is active.
|
||||||
|
*/
|
||||||
|
public function setActive();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A configuration exception
|
||||||
|
*/
|
||||||
|
class Exception extends \TYPO3\CMS\Install\Exception {}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A feature representation handles preset classes.
|
||||||
|
*/
|
||||||
|
interface FeatureInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Initialize presets
|
||||||
|
*
|
||||||
|
* @param array $postValues List of $POST values of this feature
|
||||||
|
*/
|
||||||
|
public function initializePresets(array $postValues);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of presets ordered by priority
|
||||||
|
*
|
||||||
|
* @return array<PresetInterface>
|
||||||
|
*/
|
||||||
|
public function getPresetsOrderedByPriority();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get name of feature
|
||||||
|
*
|
||||||
|
* @return string Name
|
||||||
|
*/
|
||||||
|
public function getName();
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\Configuration\Cache\CacheFeature;
|
||||||
|
use TYPO3\CMS\Install\Configuration\Context\ContextFeature;
|
||||||
|
use TYPO3\CMS\Install\Configuration\Image\ImageFeature;
|
||||||
|
use TYPO3\CMS\Install\Configuration\Mail\MailFeature;
|
||||||
|
use TYPO3\CMS\Install\Configuration\PasswordHashing\PasswordHashingFeature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate and configure all known features and presets
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class FeatureManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array List of feature class names
|
||||||
|
*/
|
||||||
|
protected $featureRegistry = [
|
||||||
|
CacheFeature::class,
|
||||||
|
ContextFeature::class,
|
||||||
|
ImageFeature::class,
|
||||||
|
MailFeature::class,
|
||||||
|
PasswordHashingFeature::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get initialized list of features with possible presets
|
||||||
|
*
|
||||||
|
* @param array $postValues List of $POST values
|
||||||
|
* @return FeatureInterface[]
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function getInitializedFeatures(array $postValues = [])
|
||||||
|
{
|
||||||
|
$features = [];
|
||||||
|
foreach ($this->featureRegistry as $featureClass) {
|
||||||
|
$featureInstance = GeneralUtility::makeInstance($featureClass);
|
||||||
|
if (!($featureInstance instanceof FeatureInterface)) {
|
||||||
|
throw new Exception(
|
||||||
|
'Feature ' . $featureClass . ' does not implement FeatureInterface',
|
||||||
|
1378644593
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$featureInstance->initializePresets($postValues);
|
||||||
|
$features[] = $featureInstance;
|
||||||
|
}
|
||||||
|
return $features;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration values to be set to LocalConfiguration from
|
||||||
|
* list of selected $POST feature presets
|
||||||
|
*
|
||||||
|
* @param array $postValues List of $POST values
|
||||||
|
* @return array List of configuration values
|
||||||
|
*/
|
||||||
|
public function getConfigurationForSelectedFeaturePresets(array $postValues)
|
||||||
|
{
|
||||||
|
$localConfigurationValuesToSet = [];
|
||||||
|
$features = $this->getInitializedFeatures($postValues);
|
||||||
|
foreach ($features as $feature) {
|
||||||
|
$featureName = $feature->getName();
|
||||||
|
$presets = $feature->getPresetsOrderedByPriority();
|
||||||
|
foreach ($presets as $preset) {
|
||||||
|
$presetName = $preset->getName();
|
||||||
|
if (!empty($postValues[$featureName]['enable'])
|
||||||
|
&& $postValues[$featureName]['enable'] === $presetName
|
||||||
|
&& (!$preset->isActive() || $preset instanceof CustomPresetInterface)
|
||||||
|
) {
|
||||||
|
$localConfigurationValuesToSet = array_merge(
|
||||||
|
$localConfigurationValuesToSet,
|
||||||
|
$preset->getConfigurationValues()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $localConfigurationValuesToSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cycle through features and get settings. First matching
|
||||||
|
* preset (highest priority) will be selected.
|
||||||
|
*
|
||||||
|
* @return array Configuration settings
|
||||||
|
*/
|
||||||
|
public function getBestMatchingConfigurationForAllFeatures()
|
||||||
|
{
|
||||||
|
$localConfigurationValuesToSet = [];
|
||||||
|
$features = $this->getInitializedFeatures([]);
|
||||||
|
foreach ($features as $feature) {
|
||||||
|
$presets = $feature->getPresetsOrderedByPriority();
|
||||||
|
foreach ($presets as $preset) {
|
||||||
|
// Only choose "normal" presets, no custom presets
|
||||||
|
if ($preset instanceof CustomPresetInterface) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($preset->isAvailable()) {
|
||||||
|
$localConfigurationValuesToSet = array_merge(
|
||||||
|
$localConfigurationValuesToSet,
|
||||||
|
$preset->getConfigurationValues()
|
||||||
|
);
|
||||||
|
// Setting for this feature done, go to next feature
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $localConfigurationValuesToSet;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Image;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class implements common image preset code
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
abstract class AbstractImagePreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array Default paths to search for executable, with trailing slash
|
||||||
|
*/
|
||||||
|
protected $defaultExecutableSearchPaths = [
|
||||||
|
'/usr/local/bin/',
|
||||||
|
'/opt/local/bin/',
|
||||||
|
'/usr/bin/',
|
||||||
|
'/usr/X11R6/bin/',
|
||||||
|
'/opt/bin/',
|
||||||
|
'C:/php/ImageMagick/',
|
||||||
|
'C:/php/GraphicsMagick/',
|
||||||
|
'C:/apache/ImageMagick/',
|
||||||
|
'C:/apache/GraphicsMagick/',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string Absolute path with found executable
|
||||||
|
*/
|
||||||
|
protected $foundPath = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path where executable was found
|
||||||
|
*
|
||||||
|
* @return string Found path
|
||||||
|
*/
|
||||||
|
public function getFoundPath()
|
||||||
|
{
|
||||||
|
return $this->foundPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check is preset is currently active on the system.
|
||||||
|
* Overwrites parent method to ignore processor_path setting
|
||||||
|
*
|
||||||
|
* @return bool TRUE if preset is active
|
||||||
|
*/
|
||||||
|
public function isActive()
|
||||||
|
{
|
||||||
|
$isActive = true;
|
||||||
|
foreach ($this->configurationValues as $configurationKey => $configurationValue) {
|
||||||
|
if ($configurationKey !== 'GFX/processor_path') {
|
||||||
|
$currentValue = $this->configurationManager->getConfigurationValueByPath($configurationKey);
|
||||||
|
if ($currentValue !== $configurationValue) {
|
||||||
|
$isActive = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find out if GraphicsMagick is available
|
||||||
|
*
|
||||||
|
* @return bool TRUE if GraphicsMagick executable is found in path
|
||||||
|
*/
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
$searchPaths = $this->getSearchPaths();
|
||||||
|
return $this->findExecutableInPath($searchPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration values to activate prefix
|
||||||
|
*
|
||||||
|
* @return array Configuration values needed to activate prefix
|
||||||
|
*/
|
||||||
|
public function getConfigurationValues()
|
||||||
|
{
|
||||||
|
$this->findExecutableInPath($this->getSearchPaths());
|
||||||
|
$configurationValues = $this->configurationValues;
|
||||||
|
$configurationValues['GFX/processor_path'] = $this->getFoundPath();
|
||||||
|
return $configurationValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find executable in path, wrapper for specific ImageMagick/GraphicsMagick find methods.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
abstract protected function findExecutableInPath(array $searchPaths);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of paths to search for image handling executables
|
||||||
|
*
|
||||||
|
* @return array List of paths to search for
|
||||||
|
*/
|
||||||
|
protected function getSearchPaths()
|
||||||
|
{
|
||||||
|
$searchPaths = $this->defaultExecutableSearchPaths;
|
||||||
|
|
||||||
|
// Add configured processor_path on top
|
||||||
|
$imPath = $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path'];
|
||||||
|
if ((string)$imPath !== '' && !in_array($imPath, $searchPaths)) {
|
||||||
|
$path = $this->cleanUpPath($imPath);
|
||||||
|
array_unshift($searchPaths, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add additional search path from form if given
|
||||||
|
if (isset($this->postValues['additionalSearchPath'])
|
||||||
|
&& (string)$this->postValues['additionalSearchPath'] !== ''
|
||||||
|
&& !in_array($this->postValues['additionalSearchPath'], $searchPaths)
|
||||||
|
) {
|
||||||
|
$path = $this->cleanUpPath($this->postValues['additionalSearchPath']);
|
||||||
|
array_unshift($searchPaths, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $searchPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consolidate between Windows and Unix and add trailing slash im missing
|
||||||
|
*
|
||||||
|
* @param string $path Given path
|
||||||
|
* @return string Cleaned up path
|
||||||
|
*/
|
||||||
|
protected function cleanUpPath($path)
|
||||||
|
{
|
||||||
|
$path = GeneralUtility::fixWindowsFilePath((string)$path);
|
||||||
|
// Add trailing slash if missing
|
||||||
|
if (!preg_match('/[\\/]$/', $path)) {
|
||||||
|
$path .= '/';
|
||||||
|
}
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Image;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractCustomPreset;
|
||||||
|
use TYPO3\CMS\Install\Configuration\CustomPresetInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom preset is a fallback if no other preset fits
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class CustomPreset extends AbstractCustomPreset implements CustomPresetInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'GFX/processor_enabled' => false,
|
||||||
|
'GFX/processor_path' => '',
|
||||||
|
'GFX/processor' => '',
|
||||||
|
'GFX/processor_effects' => false,
|
||||||
|
'GFX/processor_colorspace' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $readonlyConfigurationValues = [
|
||||||
|
'GFX/processor_path' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Image;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Utility\CommandUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset for GraphicsMagick
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class GraphicsMagickPreset extends AbstractImagePreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'GraphicsMagick';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 80;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'GFX/processor_enabled' => true,
|
||||||
|
// processor_path is determined and set by path lookup methods
|
||||||
|
'GFX/processor_path' => '',
|
||||||
|
'GFX/processor' => 'GraphicsMagick',
|
||||||
|
'GFX/processor_effects' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find executable in path, wrapper for specific ImageMagick/GraphicsMagick find methods.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function findExecutableInPath(array $searchPaths)
|
||||||
|
{
|
||||||
|
return $this->findGraphicsMagickInPaths($searchPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for GraphicsMagick executables in given paths.
|
||||||
|
*
|
||||||
|
* @param array $searchPaths List of paths to search for
|
||||||
|
* @return bool TRUE if graphics magick was found in path
|
||||||
|
*/
|
||||||
|
protected function findGraphicsMagickInPaths(array $searchPaths)
|
||||||
|
{
|
||||||
|
$result = false;
|
||||||
|
foreach ($searchPaths as $path) {
|
||||||
|
if (Environment::isWindows()) {
|
||||||
|
$executable = 'gm.exe';
|
||||||
|
} else {
|
||||||
|
$executable = 'gm';
|
||||||
|
}
|
||||||
|
|
||||||
|
$binaryPath = $path . $executable;
|
||||||
|
if (!file_exists($binaryPath) || !is_executable($binaryPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = escapeshellarg($binaryPath) . ' -version';
|
||||||
|
$executingResult = [];
|
||||||
|
@CommandUtility::exec($command, $executingResult);
|
||||||
|
// First line of exec command should contain string GraphicsMagick
|
||||||
|
$firstResultLine = array_shift($executingResult);
|
||||||
|
if (is_string($firstResultLine) && str_contains($firstResultLine, 'GraphicsMagick')) {
|
||||||
|
$this->foundPath = $path;
|
||||||
|
$result = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Image;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractFeature;
|
||||||
|
use TYPO3\CMS\Install\Configuration\FeatureInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image feature detects imagemagick / graphicsmagick versions
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class ImageFeature extends AbstractFeature implements FeatureInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of feature
|
||||||
|
*/
|
||||||
|
protected $name = 'Image';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array List of preset classes
|
||||||
|
*/
|
||||||
|
protected $presetRegistry = [
|
||||||
|
GraphicsMagickPreset::class,
|
||||||
|
ImageMagick6Preset::class,
|
||||||
|
CustomPreset::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image feature can be fed with an additional path to search for executables,
|
||||||
|
* this getter returns the given input string (for Fluid)
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getAdditionalSearchPath()
|
||||||
|
{
|
||||||
|
$additionalPath = '';
|
||||||
|
if (isset($this->postValues['additionalSearchPath']) && $this->postValues['additionalSearchPath'] !== '') {
|
||||||
|
$additionalPath = $this->postValues['additionalSearchPath'];
|
||||||
|
}
|
||||||
|
return $additionalPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Image;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Utility\CommandUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset for ImageMagick version 6 or higher
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class ImageMagick6Preset extends AbstractImagePreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'ImageMagick6';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 70;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'GFX/processor_enabled' => true,
|
||||||
|
// processor_path is determined and set by path lookup methods
|
||||||
|
'GFX/processor_path' => '',
|
||||||
|
'GFX/processor' => 'ImageMagick',
|
||||||
|
'GFX/processor_effects' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find executable in path, wrapper for specific ImageMagick/GraphicsMagick find methods.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function findExecutableInPath(array $searchPaths)
|
||||||
|
{
|
||||||
|
return $this->findImageMagick6InPaths($searchPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for GraphicsMagick executables in given paths.
|
||||||
|
*
|
||||||
|
* @param array $searchPaths List of paths to search for
|
||||||
|
* @return bool TRUE if graphics magick was found in path
|
||||||
|
*/
|
||||||
|
protected function findImageMagick6InPaths(array $searchPaths)
|
||||||
|
{
|
||||||
|
$result = false;
|
||||||
|
foreach ($searchPaths as $path) {
|
||||||
|
if (Environment::isWindows()) {
|
||||||
|
$executable = 'identify.exe';
|
||||||
|
|
||||||
|
if (!@is_file($path . $executable)) {
|
||||||
|
$executable = 'magick.exe';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$executable = 'identify';
|
||||||
|
}
|
||||||
|
|
||||||
|
$binaryPath = $path . $executable;
|
||||||
|
if (!file_exists($binaryPath) || !is_executable($binaryPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = escapeshellarg($binaryPath) . ' -version';
|
||||||
|
$executingResult = [];
|
||||||
|
CommandUtility::exec($command, $executingResult);
|
||||||
|
// First line of exec command should contain string GraphicsMagick
|
||||||
|
$firstResultLine = array_shift($executingResult);
|
||||||
|
// Example: "Version: ImageMagick 6.6.0-4 2012-05-02 Q16 http://www.imagemagick.org"
|
||||||
|
if (is_string($firstResultLine) && str_contains($firstResultLine, 'ImageMagick')) {
|
||||||
|
[, $version] = explode('ImageMagick', $firstResultLine);
|
||||||
|
// Example: "6.6.0-4"
|
||||||
|
[$version] = explode(' ', trim($version));
|
||||||
|
if (version_compare($version, '6.0.0') >= 0) {
|
||||||
|
$this->foundPath = $path;
|
||||||
|
$result = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Mail;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractCustomPreset;
|
||||||
|
use TYPO3\CMS\Install\Configuration\CustomPresetInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom preset is a fallback if no other preset fits
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class CustomPreset extends AbstractCustomPreset implements CustomPresetInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'MAIL/transport' => '',
|
||||||
|
'MAIL/transport_sendmail_command' => '',
|
||||||
|
'MAIL/transport_smtp_server' => '',
|
||||||
|
'MAIL/transport_smtp_encrypt' => '',
|
||||||
|
'MAIL/transport_smtp_username' => '',
|
||||||
|
'MAIL/transport_smtp_password' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $readonlyConfigurationValues = [
|
||||||
|
'MAIL/transport_sendmail_command' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Mail;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractFeature;
|
||||||
|
use TYPO3\CMS\Install\Configuration\FeatureInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mail feature detects sendmail settings
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class MailFeature extends AbstractFeature implements FeatureInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of feature
|
||||||
|
*/
|
||||||
|
protected $name = 'Mail';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array List of preset classes
|
||||||
|
*/
|
||||||
|
protected $presetRegistry = [
|
||||||
|
SendmailPreset::class,
|
||||||
|
SmtpPreset::class,
|
||||||
|
CustomPreset::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Mail;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendmail path handling preset
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class SendmailPreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Sendmail';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'MAIL/transport' => 'sendmail',
|
||||||
|
'MAIL/transport_sendmail_command' => '',
|
||||||
|
'MAIL/transport_smtp_server' => '',
|
||||||
|
'MAIL/transport_smtp_encrypt' => '',
|
||||||
|
'MAIL/transport_smtp_username' => '',
|
||||||
|
'MAIL/transport_smtp_password' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration values to activate prefix
|
||||||
|
*
|
||||||
|
* @return array Configuration values needed to activate prefix
|
||||||
|
*/
|
||||||
|
public function getConfigurationValues()
|
||||||
|
{
|
||||||
|
$configurationValues = $this->configurationValues;
|
||||||
|
$configurationValues['MAIL/transport_sendmail_command'] = $this->getSendmailPath();
|
||||||
|
if (($this->postValues['Mail']['enable'] ?? false) === 'Sendmail') {
|
||||||
|
$configurationValues['MAIL/transport'] = 'sendmail';
|
||||||
|
}
|
||||||
|
return $configurationValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if sendmail path if set
|
||||||
|
*
|
||||||
|
* @return bool TRUE if sendmail path if set
|
||||||
|
*/
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
return !empty($this->getSendmailPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path where executable was found
|
||||||
|
*
|
||||||
|
* @return string|bool Sendmail path or FALSE if not set
|
||||||
|
*/
|
||||||
|
public function getSendmailPath()
|
||||||
|
{
|
||||||
|
return ini_get('sendmail_path');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check is preset is currently active on the system
|
||||||
|
*
|
||||||
|
* @return bool TRUE if preset is active
|
||||||
|
*/
|
||||||
|
public function isActive()
|
||||||
|
{
|
||||||
|
$this->configurationValues['MAIL/transport_sendmail_command'] = $this->getSendmailPath();
|
||||||
|
return parent::isActive();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\Mail;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP settings handling preset
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class SmtpPreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Smtp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 40;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'MAIL/transport' => 'smtp',
|
||||||
|
'MAIL/transport_sendmail_command' => '',
|
||||||
|
'MAIL/transport_smtp_server' => 'localhost:25',
|
||||||
|
'MAIL/transport_smtp_encrypt' => '',
|
||||||
|
'MAIL/transport_smtp_username' => '',
|
||||||
|
'MAIL/transport_smtp_password' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration values to activate prefix
|
||||||
|
*
|
||||||
|
* @return array Configuration values needed to activate prefix
|
||||||
|
*/
|
||||||
|
public function getConfigurationValues()
|
||||||
|
{
|
||||||
|
$configurationValues = $this->configurationValues;
|
||||||
|
$keys = array_keys($configurationValues);
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (!empty($this->postValues['Smtp'][$key])) {
|
||||||
|
$configurationValues[$key] = $this->postValues['Smtp'][$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($this->postValues['Mail']['enable'] ?? '') === 'Smtp') {
|
||||||
|
$configurationValues['MAIL/transport'] = 'smtp';
|
||||||
|
}
|
||||||
|
return $configurationValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if sendmail path if set
|
||||||
|
*
|
||||||
|
* @return bool TRUE if sendmail path if set
|
||||||
|
*/
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset for password hashing method "argon2i"
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class Argon2iPreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Argon2i';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 70;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'BE/passwordHashing/className' => Argon2iPasswordHash::class,
|
||||||
|
'BE/passwordHashing/options' => [],
|
||||||
|
'FE/passwordHashing/className' => Argon2iPasswordHash::class,
|
||||||
|
'FE/passwordHashing/options' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find out if Argon2i is available on this system
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(Argon2iPasswordHash::class)->isAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset for password hashing method "argon2id"
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class Argon2idPreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Argon2id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 65;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'BE/passwordHashing/className' => Argon2idPasswordHash::class,
|
||||||
|
'BE/passwordHashing/options' => [],
|
||||||
|
'FE/passwordHashing/className' => Argon2idPasswordHash::class,
|
||||||
|
'FE/passwordHashing/options' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find out if Argon2id is available on this system
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(Argon2idPasswordHash::class)->isAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset for password hashing method "bcrypt"
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class BcryptPreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Bcrypt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'BE/passwordHashing/className' => BcryptPasswordHash::class,
|
||||||
|
'BE/passwordHashing/options' => [],
|
||||||
|
'FE/passwordHashing/className' => BcryptPasswordHash::class,
|
||||||
|
'FE/passwordHashing/options' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find out if bcrypt is available on this system
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(BcryptPasswordHash::class)->isAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractCustomPreset;
|
||||||
|
use TYPO3\CMS\Install\Configuration\CustomPresetInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset used if custom password hashing configuration has been applied.
|
||||||
|
* Note this custom preset does not allow manipulation via gui, this has to be done manually.
|
||||||
|
* This preset only find out if it is active and shows the current values.
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class CustomPreset extends AbstractCustomPreset implements CustomPresetInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get configuration values is used in fluid to show configuration options.
|
||||||
|
* They are fetched from LocalConfiguration / DefaultConfiguration.
|
||||||
|
*
|
||||||
|
* They are not merged with postValues for security reasons, as
|
||||||
|
* all options are readonly.
|
||||||
|
*
|
||||||
|
* @return array Current custom configuration values
|
||||||
|
*/
|
||||||
|
public function getConfigurationDescriptors(): array
|
||||||
|
{
|
||||||
|
$configurationValues = [];
|
||||||
|
$configurationValues['BE/passwordHashing/className'] = [
|
||||||
|
'value' => $this->configurationManager->getConfigurationValueByPath('BE/passwordHashing/className'),
|
||||||
|
'readonly' => true,
|
||||||
|
];
|
||||||
|
$options = (array)$this->configurationManager->getConfigurationValueByPath('BE/passwordHashing/options');
|
||||||
|
foreach ($options as $optionName => $optionValue) {
|
||||||
|
$configurationValues['BE/passwordHashing/options/' . $optionName] = [
|
||||||
|
'value' => $optionValue,
|
||||||
|
'readonly' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$configurationValues['FE/passwordHashing/className'] = [
|
||||||
|
'value' => $this->configurationManager->getConfigurationValueByPath('FE/passwordHashing/className'),
|
||||||
|
'readonly' => true,
|
||||||
|
];
|
||||||
|
$options = (array)$this->configurationManager->getConfigurationValueByPath('FE/passwordHashing/options');
|
||||||
|
foreach ($options as $optionName => $optionValue) {
|
||||||
|
$configurationValues['FE/passwordHashing/options/' . $optionName] = [
|
||||||
|
'value' => $optionValue,
|
||||||
|
'readonly' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $configurationValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractFeature;
|
||||||
|
use TYPO3\CMS\Install\Configuration\FeatureInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password hashing feature detects password hashing capabilities of the system
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class PasswordHashingFeature extends AbstractFeature implements FeatureInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of feature
|
||||||
|
*/
|
||||||
|
protected $name = 'PasswordHashing';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array List of preset classes
|
||||||
|
*/
|
||||||
|
protected $presetRegistry = [
|
||||||
|
Argon2iPreset::class,
|
||||||
|
Argon2idPreset::class,
|
||||||
|
BcryptPreset::class,
|
||||||
|
Pbkdf2Preset::class,
|
||||||
|
PhpassPreset::class,
|
||||||
|
CustomPreset::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset for password hashing method "PBKDF2"
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class Pbkdf2Preset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Pbkdf2';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'BE/passwordHashing/className' => Pbkdf2PasswordHash::class,
|
||||||
|
'BE/passwordHashing/options' => [],
|
||||||
|
'FE/passwordHashing/className' => Pbkdf2PasswordHash::class,
|
||||||
|
'FE/passwordHashing/options' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find out if PBKDF2 is available on this system
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(Pbkdf2PasswordHash::class)->isAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\Configuration\AbstractPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset for password hashing method "phpass"
|
||||||
|
* @internal only to be used within EXT:install
|
||||||
|
*/
|
||||||
|
class PhpassPreset extends AbstractPreset
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name of preset
|
||||||
|
*/
|
||||||
|
protected $name = 'Phpass';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Priority of preset
|
||||||
|
*/
|
||||||
|
protected $priority = 40;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Configuration values handled by this preset
|
||||||
|
*/
|
||||||
|
protected $configurationValues = [
|
||||||
|
'BE/passwordHashing/className' => PhpassPasswordHash::class,
|
||||||
|
'BE/passwordHashing/options' => [],
|
||||||
|
'FE/passwordHashing/className' => PhpassPasswordHash::class,
|
||||||
|
'FE/passwordHashing/options' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find out if Phpass is available on this system
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(PhpassPasswordHash::class)->isAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset interface
|
||||||
|
*
|
||||||
|
* A preset is a class for handling a specific configuration
|
||||||
|
* set of a feature.
|
||||||
|
*/
|
||||||
|
interface PresetInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Set POST values
|
||||||
|
*
|
||||||
|
* @param array $postValues Post values of feature
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function setPostValues(array $postValues);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if preset is available on the system
|
||||||
|
*
|
||||||
|
* @return bool TRUE if preset is available
|
||||||
|
*/
|
||||||
|
public function isAvailable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for isAvailable, used in fluid
|
||||||
|
*
|
||||||
|
* @return bool TRUE if preset is available
|
||||||
|
*/
|
||||||
|
public function getIsAvailable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check is preset is currently active on the system
|
||||||
|
*
|
||||||
|
* @return bool TRUE if preset is active
|
||||||
|
*/
|
||||||
|
public function isActive();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for isActive, used in fluid
|
||||||
|
*
|
||||||
|
* @return bool TRUE if preset is active
|
||||||
|
*/
|
||||||
|
public function getIsActive();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get name of preset
|
||||||
|
*
|
||||||
|
* @return string Name
|
||||||
|
*/
|
||||||
|
public function getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get priority of preset
|
||||||
|
*
|
||||||
|
* @return int Priority, usually between 0 and 100
|
||||||
|
*/
|
||||||
|
public function getPriority();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration values to activate prefix
|
||||||
|
*
|
||||||
|
* @return array Configuration values needed to activate prefix
|
||||||
|
*/
|
||||||
|
public function getConfigurationValues();
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\View\ViewInterface;
|
||||||
|
use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
|
||||||
|
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
|
||||||
|
use TYPO3Fluid\Fluid\View\TemplateView as FluidTemplateView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller abstract for shared parts of the install tool.
|
||||||
|
*
|
||||||
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
*/
|
||||||
|
class AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Helper method to initialize a view instance.
|
||||||
|
*/
|
||||||
|
protected function initializeView(ServerRequestInterface $request): ViewInterface
|
||||||
|
{
|
||||||
|
$templatePaths = [
|
||||||
|
'templateRootPaths' => ['EXT:install/Resources/Private/Templates'],
|
||||||
|
'partialRootPaths' => ['EXT:install/Resources/Private/Partials'],
|
||||||
|
'layoutRootPaths' => ['EXT:install/Resources/Private/Layouts'],
|
||||||
|
];
|
||||||
|
$renderingContext = GeneralUtility::makeInstance(RenderingContextFactory::class)->create($templatePaths, $request);
|
||||||
|
$fluidView = new FluidTemplateView($renderingContext);
|
||||||
|
$view = new FluidViewAdapter($fluidView);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'controller' => $request->getQueryParams()['install']['controller'] ?? 'maintenance',
|
||||||
|
'context' => $request->getQueryParams()['install']['context'] ?? 'install',
|
||||||
|
'composerMode' => Environment::isComposerMode(),
|
||||||
|
'currentTypo3Version' => (string)(new Typo3Version()),
|
||||||
|
'colorScheme' => $request->getQueryParams()['install']['colorScheme'] ?? '',
|
||||||
|
'theme' => $request->getQueryParams()['install']['theme'] ?? '',
|
||||||
|
'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '',
|
||||||
|
]);
|
||||||
|
return $view;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||||
|
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||||
|
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||||
|
use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
|
||||||
|
use TYPO3\CMS\Install\Service\SessionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend module controller to the Install Tool. Sets an Install Tool session
|
||||||
|
* marked as "initialized by a valid system administrator backend user" and
|
||||||
|
* redirects to the Install Tool entry point.
|
||||||
|
*
|
||||||
|
* This is a classic backend module that does not interfere with other code
|
||||||
|
* within the Install Tool, it can be seen as a facade around Install Tool just
|
||||||
|
* to embed the Install Tool in backend.
|
||||||
|
*
|
||||||
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
*/
|
||||||
|
readonly class BackendModuleController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected ModuleTemplateFactory $moduleTemplateFactory,
|
||||||
|
protected SessionService $sessionService,
|
||||||
|
#[Autowire(expression: 'service("session-manager").getSessionBackend("BE")')]
|
||||||
|
protected SessionBackendInterface $sessionBackend,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize session and redirect to "maintenance"
|
||||||
|
*/
|
||||||
|
public function maintenanceAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->setAuthorizedAndRedirect('maintenance', $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize session and redirect to "settings"
|
||||||
|
*/
|
||||||
|
public function settingsAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->setAuthorizedAndRedirect('settings', $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize session and redirect to "upgrade"
|
||||||
|
*/
|
||||||
|
public function upgradeAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->setAuthorizedAndRedirect('upgrade', $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize session and redirect to "environment"
|
||||||
|
*/
|
||||||
|
public function environmentAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->setAuthorizedAndRedirect('environment', $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts / updates the session and redirects to the Install Tool
|
||||||
|
* with given action.
|
||||||
|
*/
|
||||||
|
protected function setAuthorizedAndRedirect(string $controller, ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$redirectParameters = [
|
||||||
|
'install' => [
|
||||||
|
'controller' => $controller,
|
||||||
|
'context' => 'backend',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$backendUser = $this->getBackendUser();
|
||||||
|
$userTS = $backendUser->getTSConfig();
|
||||||
|
|
||||||
|
$themeDisabled = $userTS['setup.']['fields.']['theme.']['disabled'] ?? '0';
|
||||||
|
$theme = $GLOBALS['BE_USER']->uc['theme'] ?? $userTS['setup.']['fields.']['theme'] ?? 'auto';
|
||||||
|
if ($themeDisabled === '1') {
|
||||||
|
$theme = $userTS['setup.']['fields.']['theme'] ?? 'modern';
|
||||||
|
}
|
||||||
|
if ($theme !== 'modern') {
|
||||||
|
$redirectParameters['install']['theme'] = $theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
$colorSchemeDisabled = $userTS['setup.']['fields.']['colorScheme.']['disabled'] ?? '0';
|
||||||
|
$colorScheme = $GLOBALS['BE_USER']->uc['colorScheme'] ?? $userTS['setup.']['fields.']['colorScheme'] ?? 'auto';
|
||||||
|
if ($colorSchemeDisabled === '1') {
|
||||||
|
$colorScheme = $userTS['setup.']['fields.']['colorScheme'] ?? 'light';
|
||||||
|
}
|
||||||
|
if ($colorScheme !== 'auto') {
|
||||||
|
$redirectParameters['install']['colorScheme'] = $colorScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userSession = $this->getBackendUser()->getSession();
|
||||||
|
$this->sessionService->installSessionHandler($request);
|
||||||
|
$this->sessionService->startSession();
|
||||||
|
$this->sessionService->setAuthorizedBackendSession($userSession, $this->sessionBackend);
|
||||||
|
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||||
|
$redirectLocation = $normalizedParams->getSiteUrl() . '?__typo3_install&' . http_build_query($redirectParameters, '', '&', PHP_QUERY_RFC3986);
|
||||||
|
return new RedirectResponse($redirectLocation, 303);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getBackendUser(): BackendUserAuthentication
|
||||||
|
{
|
||||||
|
return $GLOBALS['BE_USER'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal This class is a specific implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
*/
|
||||||
|
trait ControllerTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Using fixed Content-Security-Policy for Admin Tool (extensions and database might not be available)
|
||||||
|
*/
|
||||||
|
protected function createContentSecurityPolicy(): Policy
|
||||||
|
{
|
||||||
|
return GeneralUtility::makeInstance(Policy::class)
|
||||||
|
->default(SourceKeyword::self)
|
||||||
|
// script-src 'nonce-...' required for importmaps
|
||||||
|
->extend(Directive::ScriptSrc, SourceKeyword::nonceProxy)
|
||||||
|
// `style-src 'unsafe-inline'` required for lit in safari and firefox to allow inline <style> tags
|
||||||
|
// (for browsers that do not support https://caniuse.com/mdn-api_shadowroot_adoptedstylesheets)
|
||||||
|
->extend(Directive::StyleSrc, SourceKeyword::unsafeInline)
|
||||||
|
->set(Directive::StyleSrcAttr, SourceKeyword::unsafeInline)
|
||||||
|
->extend(Directive::ImgSrc, SourceScheme::data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||||
|
use TYPO3\CMS\Core\Http\Uri;
|
||||||
|
|
||||||
|
class EntryPointRedirectController
|
||||||
|
{
|
||||||
|
public function redirectAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||||
|
return new RedirectResponse(
|
||||||
|
new Uri($normalizedParams->getSiteUrl() . '?__typo3_install')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||||
|
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||||
|
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||||
|
use TYPO3\CMS\Core\Imaging\IconState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for icon handling
|
||||||
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
*/
|
||||||
|
class IconController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected readonly IconFactory $iconFactory
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getIconAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$parsedBody = $request->getParsedBody();
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
$requestedIcon = json_decode($parsedBody['icon'] ?? $queryParams['icon'], true);
|
||||||
|
|
||||||
|
[$identifier, $size, $overlayIdentifier, $iconState, $alternativeMarkupIdentifier] = $requestedIcon;
|
||||||
|
|
||||||
|
if (empty($overlayIdentifier)) {
|
||||||
|
$overlayIdentifier = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iconState = IconState::tryFrom($iconState);
|
||||||
|
$size = IconSize::tryFrom($size);
|
||||||
|
$icon = $this->iconFactory->getIcon($identifier, $size, $overlayIdentifier, $iconState);
|
||||||
|
|
||||||
|
return new HtmlResponse($icon->render($alternativeMarkupIdentifier));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,640 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\DriverManager;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||||
|
use TYPO3\CMS\Backend\Routing\RouteRedirect;
|
||||||
|
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||||
|
use TYPO3\CMS\Core\Authentication\CommandLineUserCreation;
|
||||||
|
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||||
|
use TYPO3\CMS\Core\Core\BootService;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Crypto\HashService;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
|
||||||
|
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||||
|
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||||
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||||
|
use TYPO3\CMS\Core\Imaging\IconRegistry;
|
||||||
|
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||||
|
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
|
||||||
|
use TYPO3\CMS\Core\Package\PackageManager;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\Behavior;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\PolicyBag;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Core\Type\Map;
|
||||||
|
use TYPO3\CMS\Core\View\ViewInterface;
|
||||||
|
use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
|
||||||
|
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
|
||||||
|
use TYPO3\CMS\Install\Factory\ImportMapFactory;
|
||||||
|
use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
|
||||||
|
use TYPO3\CMS\Install\Service\EnableFileService;
|
||||||
|
use TYPO3\CMS\Install\Service\Exception\ConfigurationDirectoryDoesNotExistException;
|
||||||
|
use TYPO3\CMS\Install\Service\SetupDatabaseService;
|
||||||
|
use TYPO3\CMS\Install\Service\SetupService;
|
||||||
|
use TYPO3\CMS\Install\SystemEnvironment\Check;
|
||||||
|
use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
|
||||||
|
use TYPO3\CMS\Install\WebserverType;
|
||||||
|
use TYPO3Fluid\Fluid\View\TemplateView as FluidTemplateView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install step controller, dispatcher class of step actions.
|
||||||
|
*
|
||||||
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
* @phpstan-import-type Params from DriverManager
|
||||||
|
*/
|
||||||
|
#[Autoconfigure(public: true)]
|
||||||
|
final readonly class InstallerController
|
||||||
|
{
|
||||||
|
use ControllerTrait;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private BootService $bootService,
|
||||||
|
private ConfigurationManager $configurationManager,
|
||||||
|
private PackageManager $packageManager,
|
||||||
|
private VerifyHostHeader $verifyHostHeader,
|
||||||
|
private FormProtectionFactory $formProtectionFactory,
|
||||||
|
private SetupService $setupService,
|
||||||
|
private SetupDatabaseService $setupDatabaseService,
|
||||||
|
private ImportMapFactory $importMapFactory,
|
||||||
|
private HashService $hashService,
|
||||||
|
private IconRegistry $iconRegistry,
|
||||||
|
private DirectiveHashCollection $directiveHashCollection,
|
||||||
|
private CommandLineUserCreation $commandLineUserCreation,
|
||||||
|
private UriBuilder $uriBuilder,
|
||||||
|
private RenderingContextFactory $renderingContextFactory,
|
||||||
|
private ConnectionPool $connectionPool,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init action loads <head> with JS initiating further stuff
|
||||||
|
*/
|
||||||
|
public function initAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$bust = $GLOBALS['EXEC_TIME'];
|
||||||
|
if (!Environment::getContext()->isDevelopment()) {
|
||||||
|
$bust = $this->hashService->hmac((new Typo3Version()) . Environment::getProjectPath(), self::class);
|
||||||
|
}
|
||||||
|
$sitePath = $request->getAttribute('normalizedParams')->getSitePath();
|
||||||
|
$importMap = $this->importMapFactory->create($sitePath);
|
||||||
|
$initModule = $importMap->resolveImport('@typo3/install/init-installer.js', true, $sitePath);
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$view->assign('bust', $bust);
|
||||||
|
$view->assign('initModule', $initModule);
|
||||||
|
$view->assign('iconCacheIdentifier', sha1($this->iconRegistry->getBackendIconsCacheIdentifier()));
|
||||||
|
$nonce = new ConsumableNonce();
|
||||||
|
$view->assign('importmap', $importMap->render($sitePath, $nonce));
|
||||||
|
|
||||||
|
return new HtmlResponse(
|
||||||
|
$view->render('Installer/Init'),
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
'Content-Security-Policy' => $this->createContentSecurityPolicy()->compile(new PolicyBag(Scope::backend(), new Map(), new Behavior(), $nonce, $this->directiveHashCollection)),
|
||||||
|
'Cache-Control' => 'no-cache, no-store',
|
||||||
|
'Pragma' => 'no-cache',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main layout with progress bar, header
|
||||||
|
*/
|
||||||
|
public function mainLayoutAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Installer/MainLayout'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render "FIRST_INSTALL file need to exist" view
|
||||||
|
*/
|
||||||
|
public function showInstallerNotAvailableAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Installer/ShowInstallerNotAvailable'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if "environment and folders" should be shown
|
||||||
|
*/
|
||||||
|
public function checkEnvironmentAndFoldersAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => @is_file($this->configurationManager->getSystemConfigurationFileLocation()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render "environment and folders"
|
||||||
|
*/
|
||||||
|
public function showEnvironmentAndFoldersAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$systemCheckMessageQueue = new FlashMessageQueue('install');
|
||||||
|
$checkMessages = (new Check())->getStatus();
|
||||||
|
foreach ($checkMessages as $message) {
|
||||||
|
$systemCheckMessageQueue->enqueue($message);
|
||||||
|
}
|
||||||
|
$setupCheckMessages = (new SetupCheck())->getStatus();
|
||||||
|
foreach ($setupCheckMessages as $message) {
|
||||||
|
$systemCheckMessageQueue->enqueue($message);
|
||||||
|
}
|
||||||
|
$folderStructureFactory = new DefaultFactory();
|
||||||
|
$structureFacade = $folderStructureFactory->getStructure(WebserverType::fromRequest($request));
|
||||||
|
$structureMessageQueue = $structureFacade->getStatus();
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Installer/ShowEnvironmentAndFolders'),
|
||||||
|
'environmentStatusErrors' => $systemCheckMessageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR),
|
||||||
|
'environmentStatusWarnings' => $systemCheckMessageQueue->getAllMessages(ContextualFeedbackSeverity::WARNING),
|
||||||
|
'structureErrors' => $structureMessageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create main folder layout, LocalConfiguration, PackageStates
|
||||||
|
*/
|
||||||
|
public function executeEnvironmentAndFoldersAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$errorsFromStructure = $this->setupService->createDirectoryStructure(WebserverType::fromRequest($request));
|
||||||
|
try {
|
||||||
|
$this->setupService->prepareSystemSettings();
|
||||||
|
} catch (ConfigurationDirectoryDoesNotExistException) {
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'status' => $errorsFromStructure,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if trusted hosts pattern needs to be adjusted
|
||||||
|
*/
|
||||||
|
public function checkTrustedHostsPatternAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$serverParams = $request->getServerParams();
|
||||||
|
$host = $serverParams['HTTP_HOST'] ?? '';
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => $this->verifyHostHeader->isAllowedHostHeaderValue($host, $serverParams),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust trusted hosts pattern to '.*' if it does not match yet
|
||||||
|
*/
|
||||||
|
public function executeAdjustTrustedHostsPatternAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$serverParams = $request->getServerParams();
|
||||||
|
$host = $serverParams['HTTP_HOST'] ?? '';
|
||||||
|
|
||||||
|
if (!$this->verifyHostHeader->isAllowedHostHeaderValue($host, $serverParams)) {
|
||||||
|
$this->configurationManager->setLocalConfigurationValueByPath('SYS/trustedHostsPattern', '.*');
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if database connect step needs to be shown
|
||||||
|
*/
|
||||||
|
public function checkDatabaseConnectAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => $this->setupDatabaseService->isDatabaseConfigurationComplete() && $this->setupDatabaseService->isDatabaseConnectSuccessful(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show database connect step
|
||||||
|
*/
|
||||||
|
public function showDatabaseConnectAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
|
||||||
|
$driverOptions = $this->setupDatabaseService->getDriverOptions();
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$driverOptions['executeDatabaseConnectToken'] = $formProtection->generateToken('installTool', 'executeDatabaseConnect');
|
||||||
|
$view->assignMultiple($driverOptions);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Installer/ShowDatabaseConnect'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test database connect data
|
||||||
|
*/
|
||||||
|
public function executeDatabaseConnectAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$postValues = $request->getParsedBody()['install']['values'];
|
||||||
|
[$success, $messages] = $this->setupDatabaseService->setDefaultConnectionSettings($postValues);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => $success,
|
||||||
|
'status' => $messages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a database needs to be selected
|
||||||
|
*/
|
||||||
|
public function checkDatabaseSelectAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => $this->setupDatabaseService->checkDatabaseSelect(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render "select a database"
|
||||||
|
*/
|
||||||
|
public function showDatabaseSelectAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$errors = [];
|
||||||
|
try {
|
||||||
|
$view->assign('databaseList', $this->setupDatabaseService->getDatabaseList());
|
||||||
|
} catch (\Exception $exception) {
|
||||||
|
$errors[] = $exception->getMessage();
|
||||||
|
}
|
||||||
|
$view->assignMultiple([
|
||||||
|
'errors' => $errors,
|
||||||
|
'executeDatabaseSelectToken' => $formProtection->generateToken('installTool', 'executeDatabaseSelect'),
|
||||||
|
'executeCheckDatabaseRequirementsToken' => $formProtection->generateToken('installTool', 'checkDatabaseRequirements'),
|
||||||
|
]);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Installer/ShowDatabaseSelect'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-check whether all requirements for the installed database driver and platform are fulfilled
|
||||||
|
*/
|
||||||
|
public function checkDatabaseRequirementsAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$success = true;
|
||||||
|
$messages = [];
|
||||||
|
$databaseDriverName = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['driver'];
|
||||||
|
|
||||||
|
$databaseName = $this->retrieveDatabaseNameFromRequest($request);
|
||||||
|
if ($databaseName === '') {
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'status' => [
|
||||||
|
new FlashMessage(
|
||||||
|
'You must select a database.',
|
||||||
|
'No Database selected',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname'] = $databaseName;
|
||||||
|
|
||||||
|
foreach ($this->setupDatabaseService->checkDatabaseRequirementsForDriver($databaseDriverName) as $message) {
|
||||||
|
if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) {
|
||||||
|
$success = false;
|
||||||
|
$messages[] = $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check create and drop permissions
|
||||||
|
$statusMessages = [];
|
||||||
|
foreach ($this->setupDatabaseService->checkRequiredDatabasePermissions() as $checkRequiredPermission) {
|
||||||
|
$statusMessages[] = new FlashMessage(
|
||||||
|
$checkRequiredPermission,
|
||||||
|
'Missing required permissions',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($statusMessages !== []) {
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'status' => $statusMessages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if requirements are not fulfilled
|
||||||
|
if ($success === false) {
|
||||||
|
// remove the database again if we created it
|
||||||
|
if ($request->getParsedBody()['install']['values']['type'] === 'new') {
|
||||||
|
$connection = $this->connectionPool
|
||||||
|
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
|
||||||
|
$connection
|
||||||
|
->createSchemaManager()
|
||||||
|
->dropDatabase($connection->quoteIdentifier($databaseName));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->configurationManager->removeLocalConfigurationKeysByPath(['DB/Connections/Default/dbname']);
|
||||||
|
|
||||||
|
$message = new FlashMessage(
|
||||||
|
sprintf(
|
||||||
|
'Database with name "%s" has been removed due to the following errors. '
|
||||||
|
. 'Please solve them first and try again. If you tried to create a new database make also sure, that the DBMS charset is to use UTF-8',
|
||||||
|
$databaseName
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
ContextualFeedbackSeverity::INFO
|
||||||
|
);
|
||||||
|
array_unshift($messages, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname']);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => $success,
|
||||||
|
'status' => $messages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function retrieveDatabaseNameFromRequest(ServerRequestInterface $request): string
|
||||||
|
{
|
||||||
|
$postValues = $request->getParsedBody()['install']['values'];
|
||||||
|
if ($postValues['type'] === 'new') {
|
||||||
|
return $postValues['new'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($postValues['type'] === 'existing' && !empty($postValues['existing'])) {
|
||||||
|
return $postValues['existing'];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select / create and test a database
|
||||||
|
*/
|
||||||
|
public function executeDatabaseSelectAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$databaseName = $this->retrieveDatabaseNameFromRequest($request);
|
||||||
|
if ($databaseName === '') {
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'status' => [
|
||||||
|
new FlashMessage(
|
||||||
|
'You must select a database.',
|
||||||
|
'No Database selected',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$postValues = $request->getParsedBody()['install']['values'];
|
||||||
|
if ($postValues['type'] === 'new') {
|
||||||
|
$status = $this->setupDatabaseService->createNewDatabase($databaseName);
|
||||||
|
if ($status->getSeverity() === ContextualFeedbackSeverity::ERROR) {
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'status' => [$status],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} elseif ($postValues['type'] === 'existing') {
|
||||||
|
$status = $this->setupDatabaseService->checkExistingDatabase($databaseName);
|
||||||
|
if ($status->getSeverity() === ContextualFeedbackSeverity::ERROR) {
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'status' => [$status],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if initial data needs to be imported
|
||||||
|
*/
|
||||||
|
public function checkDatabaseDataAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
$existingTables = $this->connectionPool
|
||||||
|
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
|
||||||
|
->createSchemaManager()
|
||||||
|
->listTableNames();
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => !empty($existingTables),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render "import initial data"
|
||||||
|
*/
|
||||||
|
public function showDatabaseDataAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'executeDatabaseDataToken' => $formProtection->generateToken('installTool', 'executeDatabaseData'),
|
||||||
|
]);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Installer/ShowDatabaseData'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create main db layout
|
||||||
|
*/
|
||||||
|
public function executeDatabaseDataAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$messages = [];
|
||||||
|
$postValues = $request->getParsedBody()['install']['values'];
|
||||||
|
$username = (string)$postValues['username'] !== '' ? $postValues['username'] : 'admin';
|
||||||
|
// Check password and return early if not good enough
|
||||||
|
$password = (string)($postValues['password'] ?? '');
|
||||||
|
$email = $postValues['email'] ?? '';
|
||||||
|
$passwordValidationErrors = $this->setupDatabaseService->getBackendUserPasswordValidationErrors($password);
|
||||||
|
if (!empty($passwordValidationErrors)) {
|
||||||
|
$messages[] = new FlashMessage(
|
||||||
|
'Administrator password not secure enough!',
|
||||||
|
'',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add all password validation errors to the messages array
|
||||||
|
foreach ($passwordValidationErrors as $error) {
|
||||||
|
$messages[] = new FlashMessage(
|
||||||
|
$error,
|
||||||
|
'',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'status' => $messages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Set site name
|
||||||
|
if (!empty($postValues['sitename'])) {
|
||||||
|
$this->setupService->setSiteName($postValues['sitename']);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$messages = $this->setupDatabaseService->importDatabaseData();
|
||||||
|
if (!empty($messages)) {
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'status' => $messages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (StatementException $exception) {
|
||||||
|
$messages[] = new FlashMessage(
|
||||||
|
'Error detected in SQL statement:' . LF . $exception->getMessage(),
|
||||||
|
'Import of database data could not be performed',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'status' => $messages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->commandLineUserCreation->ensureCliUserExists();
|
||||||
|
$this->setupService->createUser($username, $password, $email);
|
||||||
|
$this->setupService->setInstallToolPassword($password);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show last "create site with theme / install distribution"
|
||||||
|
*/
|
||||||
|
public function showDefaultConfigurationAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$distributions = [];
|
||||||
|
if ($this->packageManager->isPackageActive('impexp')) {
|
||||||
|
$distributions = $this->setupService->getAvailableDistributions();
|
||||||
|
}
|
||||||
|
$view->assignMultiple([
|
||||||
|
'composerMode' => Environment::isComposerMode(),
|
||||||
|
'offerToCreateBasicSite' => $this->packageManager->isPackageActive('fluid_styled_content'),
|
||||||
|
'distributions' => $distributions,
|
||||||
|
'executeDefaultConfigurationToken' => $formProtection->generateToken('installTool', 'executeDefaultConfiguration'),
|
||||||
|
]);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Installer/ShowDefaultConfiguration'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last step execution: clean up, remove FIRST_INSTALL file, ...
|
||||||
|
*/
|
||||||
|
public function executeDefaultConfigurationAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
// Let the admin user redirect to the distributions page on first login
|
||||||
|
$siteSetup = $request->getParsedBody()['install']['values']['sitesetup'] ?? '';
|
||||||
|
$selectedDistribution = '';
|
||||||
|
if (str_starts_with($siteSetup, 'createsite:')) {
|
||||||
|
$selectedDistribution = substr($siteSetup, strlen('createsite:'));
|
||||||
|
$siteSetup = 'activateDistribution';
|
||||||
|
}
|
||||||
|
// It is crucial to activate the package *before* loading the container
|
||||||
|
if ($siteSetup === 'activateDistribution') {
|
||||||
|
// Distribution handles all site creation (pages, content, site configuration)
|
||||||
|
$this->setupService->activateDistributionPackage($selectedDistribution);
|
||||||
|
}
|
||||||
|
$nextStepUrl = $this->uriBuilder->buildUriFromRoute('login');
|
||||||
|
|
||||||
|
if ($siteSetup === 'createsite') {
|
||||||
|
$siteUrl = $request->getAttribute('normalizedParams')->getSiteUrl();
|
||||||
|
$this->setupService->createSite('main', $siteUrl);
|
||||||
|
} elseif ($siteSetup === 'loaddistribution'
|
||||||
|
&& !Environment::isComposerMode()
|
||||||
|
&& $this->packageManager->isPackageActive('extensionmanager')
|
||||||
|
) {
|
||||||
|
// Update the URL to redirect after login to the extension manager distributions list
|
||||||
|
$nextStepUrl = $this->uriBuilder->buildUriWithRedirect(
|
||||||
|
'login',
|
||||||
|
[],
|
||||||
|
RouteRedirect::create(
|
||||||
|
'extensionmanager',
|
||||||
|
[
|
||||||
|
'action' => 'distributions',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($request->getParsedBody()['install']['values']['backendgroups'] ?? '') === 'creategroups') {
|
||||||
|
$this->setupService->createBackendUserGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bootService->unsetInternalContainerInstance();
|
||||||
|
$container = $this->bootService->loadExtLocalconfDatabase(true);
|
||||||
|
|
||||||
|
// Mark upgrade wizards as done
|
||||||
|
$this->setupDatabaseService->markWizardsDone($container);
|
||||||
|
|
||||||
|
// Set up all installed extensions
|
||||||
|
// (includes e.g. publishing of assets, importing distribution data)
|
||||||
|
$this->setupService->setupExtensions($container);
|
||||||
|
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$formProtection->clean();
|
||||||
|
|
||||||
|
EnableFileService::removeFirstInstallFile();
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'redirect' => (string)$nextStepUrl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to initialize a standalone view instance.
|
||||||
|
*/
|
||||||
|
private function initializeView(ServerRequestInterface $request): ViewInterface
|
||||||
|
{
|
||||||
|
$templatePaths = [
|
||||||
|
'templateRootPaths' => ['EXT:install/Resources/Private/Templates'],
|
||||||
|
];
|
||||||
|
$renderingContext = $this->renderingContextFactory->create($templatePaths, $request);
|
||||||
|
$fluidView = new FluidTemplateView($renderingContext);
|
||||||
|
return new FluidViewAdapter($fluidView);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Configuration\Exception\SettingsWriteException;
|
||||||
|
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Crypto\HashService;
|
||||||
|
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||||
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||||
|
use TYPO3\CMS\Core\Imaging\IconRegistry;
|
||||||
|
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||||
|
use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\Behavior;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\PolicyBag;
|
||||||
|
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
|
||||||
|
use TYPO3\CMS\Core\Service\Exception\ConfigurationChangedException;
|
||||||
|
use TYPO3\CMS\Core\Service\Exception\SilentConfigurationUpgradeReadonlyException;
|
||||||
|
use TYPO3\CMS\Core\Service\SilentConfigurationUpgradeService;
|
||||||
|
use TYPO3\CMS\Core\Type\Map;
|
||||||
|
use TYPO3\CMS\Install\Factory\ImportMapFactory;
|
||||||
|
use TYPO3\CMS\Install\Service\Exception\TemplateFileChangedException;
|
||||||
|
use TYPO3\CMS\Install\Service\SilentTemplateFileUpgradeService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout controller
|
||||||
|
*
|
||||||
|
* Renders a first "load the Javascript in <head>" view, and the
|
||||||
|
* main layout of the install tool in second action.
|
||||||
|
*
|
||||||
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
*/
|
||||||
|
class LayoutController extends AbstractController
|
||||||
|
{
|
||||||
|
use ControllerTrait;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly SilentConfigurationUpgradeService $silentConfigurationUpgradeService,
|
||||||
|
private readonly SilentTemplateFileUpgradeService $silentTemplateFileUpgradeService,
|
||||||
|
private readonly BackendEntryPointResolver $backendEntryPointResolver,
|
||||||
|
private readonly ImportMapFactory $importMapFactory,
|
||||||
|
private readonly HashService $hashService,
|
||||||
|
private readonly IconRegistry $iconRegistry,
|
||||||
|
private readonly DirectiveHashCollection $directiveHashCollection,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The init action renders an HTML response with HTML view having <head> section
|
||||||
|
* containing resources to main .js routing.
|
||||||
|
*/
|
||||||
|
public function initAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$bust = $GLOBALS['EXEC_TIME'];
|
||||||
|
if (!Environment::getContext()->isDevelopment()) {
|
||||||
|
$bust = $this->hashService->hmac((new Typo3Version()) . Environment::getProjectPath(), self::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sitePath = $request->getAttribute('normalizedParams')->getSitePath();
|
||||||
|
$importMap = $this->importMapFactory->create($sitePath);
|
||||||
|
$initModule = $importMap->resolveImport('@typo3/install/init-install.js', true, $sitePath);
|
||||||
|
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$nonce = new ConsumableNonce();
|
||||||
|
$view->assignMultiple([
|
||||||
|
// time is used as cache bust for js and css resources
|
||||||
|
'bust' => $bust,
|
||||||
|
'iconCacheIdentifier' => sha1($this->iconRegistry->getBackendIconsCacheIdentifier()),
|
||||||
|
'initModule' => $initModule,
|
||||||
|
'importmap' => $importMap->render($sitePath, $nonce),
|
||||||
|
]);
|
||||||
|
return new HtmlResponse(
|
||||||
|
$view->render('Layout/Init'),
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
'Cache-Control' => 'no-cache, no-store',
|
||||||
|
'Content-Security-Policy' => $this->createContentSecurityPolicy()->compile(new PolicyBag(Scope::backend(), new Map(), new Behavior(), $nonce, $this->directiveHashCollection)),
|
||||||
|
'Pragma' => 'no-cache',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a json response with the main HTML layout body: Toolbar, main menu and
|
||||||
|
* doc header in standalone, doc header only in backend context. Silent updaters
|
||||||
|
* are executed before this main view is loaded.
|
||||||
|
*/
|
||||||
|
public function mainLayoutAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$view->assign('moduleName', 'system_' . ($request->getQueryParams()['install']['module'] ?? 'layout'));
|
||||||
|
$view->assign('backendUrl', (string)$this->backendEntryPointResolver->getUriFromRequest($request));
|
||||||
|
$view->assign('frontendUrl', $request->getAttribute('normalizedParams')->getSiteUrl());
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Layout/MainLayout'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute silent configuration update. May be called multiple times until success = true is returned.
|
||||||
|
*
|
||||||
|
* @return ResponseInterface success = true if no change has been done
|
||||||
|
*/
|
||||||
|
public function executeSilentConfigurationUpdateAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
$success = true;
|
||||||
|
try {
|
||||||
|
$this->silentConfigurationUpgradeService->execute();
|
||||||
|
} catch (ConfigurationChangedException) {
|
||||||
|
$success = false;
|
||||||
|
} catch (SettingsWriteException $e) {
|
||||||
|
throw new SilentConfigurationUpgradeReadonlyException(1688462974, $e);
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => $success,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute silent template files update. May be called multiple times until success = true is returned.
|
||||||
|
*
|
||||||
|
* @return ResponseInterface success = true if no change has been done
|
||||||
|
*/
|
||||||
|
public function executeSilentTemplateFileUpdateAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
$success = true;
|
||||||
|
try {
|
||||||
|
$this->silentTemplateFileUpgradeService->execute();
|
||||||
|
} catch (TemplateFileChangedException $e) {
|
||||||
|
$success = false;
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => $success,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize TYPO3_CONF_VARS['EXTENSIONS'] with possibly new defaults from extensions
|
||||||
|
* ext_conf_template.txt files. This make LocalConfiguration the only source of truth for
|
||||||
|
* extension configuration, and it is always up-to-date, also if an extension has been
|
||||||
|
* updated.
|
||||||
|
*/
|
||||||
|
public function executeSilentExtensionConfigurationSynchronizationAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
$extensionConfiguration = new ExtensionConfiguration();
|
||||||
|
$extensionConfiguration->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||||
|
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||||
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||||
|
use TYPO3\CMS\Install\Service\EnableFileService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login controller
|
||||||
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
*/
|
||||||
|
class LoginController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly FormProtectionFactory $formProtectionFactory,
|
||||||
|
private readonly ConfigurationManager $configurationManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the "Create an "enable install tool file" action
|
||||||
|
*/
|
||||||
|
public function showEnableInstallToolFileAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$view->assign('enableInstallToolPath', EnableFileService::getStaticLocationForInstallToolEnableFileDirectory());
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Login/ShowEnableInstallToolFile'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render login view
|
||||||
|
*/
|
||||||
|
public function showLoginAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'loginToken' => $formProtection->generateToken('installTool', 'login'),
|
||||||
|
'installToolEnableFilePermanent' => EnableFileService::isInstallToolEnableFilePermanent(),
|
||||||
|
'configFile' => $this->configurationManager->getSystemConfigurationFileLocation(true),
|
||||||
|
]);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Login/ShowLogin'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,891 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||||
|
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||||
|
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Database\ReferenceIndex;
|
||||||
|
use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
|
||||||
|
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
|
||||||
|
use TYPO3\CMS\Core\Database\Schema\SqlReader;
|
||||||
|
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||||
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||||
|
use TYPO3\CMS\Core\Localization\LanguagePackService;
|
||||||
|
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||||
|
use TYPO3\CMS\Core\Localization\Locales;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||||
|
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction;
|
||||||
|
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator;
|
||||||
|
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
|
||||||
|
use TYPO3\CMS\Core\Service\OpcodeCacheService;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\Service\ClearCacheService;
|
||||||
|
use TYPO3\CMS\Install\Service\ClearTableService;
|
||||||
|
use TYPO3\CMS\Install\Service\LateBootService;
|
||||||
|
use TYPO3\CMS\Install\Service\Typo3tempFileService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintenance controller
|
||||||
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
*/
|
||||||
|
class MaintenanceController extends AbstractController
|
||||||
|
{
|
||||||
|
protected PasswordPolicyValidator $passwordPolicyValidator;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly LateBootService $lateBootService,
|
||||||
|
private readonly ClearCacheService $clearCacheService,
|
||||||
|
private readonly ConfigurationManager $configurationManager,
|
||||||
|
private readonly PasswordHashFactory $passwordHashFactory,
|
||||||
|
private readonly Locales $locales,
|
||||||
|
private readonly LanguageServiceFactory $languageServiceFactory,
|
||||||
|
private readonly FormProtectionFactory $formProtectionFactory,
|
||||||
|
) {
|
||||||
|
$GLOBALS['LANG'] = $this->languageServiceFactory->create('en');
|
||||||
|
$passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default';
|
||||||
|
$this->passwordPolicyValidator = GeneralUtility::makeInstance(
|
||||||
|
PasswordPolicyValidator::class,
|
||||||
|
PasswordPolicyAction::NEW_USER_PASSWORD,
|
||||||
|
is_string($passwordPolicy) ? $passwordPolicy : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main "show the cards" view
|
||||||
|
*/
|
||||||
|
public function cardsAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Maintenance/Cards'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache framework and opcode caches
|
||||||
|
*/
|
||||||
|
public function cacheClearAllAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
$this->clearCacheService->clearAll();
|
||||||
|
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
$messageQueue->enqueue(
|
||||||
|
new FlashMessage('Successfully cleared all caches and all available opcode caches.', 'Caches cleared')
|
||||||
|
);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear typo3temp files statistics action
|
||||||
|
*/
|
||||||
|
public function clearTypo3tempFilesStatsAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$container = $this->lateBootService->loadExtLocalconfDatabase(false);
|
||||||
|
$typo3tempFileService = $container->get(Typo3tempFileService::class);
|
||||||
|
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'clearTypo3tempFilesToken' => $formProtection->generateToken('installTool', 'clearTypo3tempFiles'),
|
||||||
|
]);
|
||||||
|
return new JsonResponse(
|
||||||
|
[
|
||||||
|
'success' => true,
|
||||||
|
'stats' => $typo3tempFileService->getDirectoryStatistics(),
|
||||||
|
'html' => $view->render('Maintenance/ClearTypo3tempFiles'),
|
||||||
|
'buttons' => [
|
||||||
|
[
|
||||||
|
'btnClass' => 'btn-default t3js-clearTypo3temp-stats',
|
||||||
|
'text' => 'Scan again',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear typo3temp/assets or FAL processed Files
|
||||||
|
*/
|
||||||
|
public function clearTypo3tempFilesAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$container = $this->lateBootService->loadExtLocalconfDatabase(false);
|
||||||
|
$typo3tempFileService = $container->get(Typo3tempFileService::class);
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
$folder = $request->getParsedBody()['install']['folder'];
|
||||||
|
// storageUid is an optional post param if FAL storages should be cleaned
|
||||||
|
$storageUid = $request->getParsedBody()['install']['storageUid'] ?? null;
|
||||||
|
if ($storageUid === null) {
|
||||||
|
$typo3tempFileService->clearAssetsFolder($folder);
|
||||||
|
$messageQueue->enqueue(new FlashMessage('The directory "' . $folder . '" has been cleared successfully', 'Directory cleared'));
|
||||||
|
} else {
|
||||||
|
$storageUid = (int)$storageUid;
|
||||||
|
// We have to get the stats before deleting files, otherwise we're not able to retrieve the amount of files anymore
|
||||||
|
$stats = $typo3tempFileService->getStatsFromStorageByUid($storageUid);
|
||||||
|
$failedDeletions = $typo3tempFileService->clearProcessedFiles($storageUid);
|
||||||
|
if ($failedDeletions) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Failed to delete ' . $failedDeletions . ' processed files. See TYPO3 log (by default typo3temp/var/log/typo3_*.log)',
|
||||||
|
'Failed to delete files',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
sprintf('Removed %d files from directory "%s"', $stats['numberOfFiles'], $stats['directory']),
|
||||||
|
'Deleted processed files'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dump autoload information
|
||||||
|
*/
|
||||||
|
public function dumpAutoloadAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
if (Environment::isComposerMode()) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Skipped generating additional class loading information in Composer mode.',
|
||||||
|
'Autoloader not dumped',
|
||||||
|
ContextualFeedbackSeverity::NOTICE
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ClassLoadingInformation::dumpClassLoadingInformation();
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Successfully dumped class loading information for extensions.',
|
||||||
|
'Dumped autoloader'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get main database analyzer modal HTML
|
||||||
|
*/
|
||||||
|
public function databaseAnalyzerAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'databaseAnalyzerExecuteToken' => $formProtection->generateToken('installTool', 'databaseAnalyzerExecute'),
|
||||||
|
]);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Maintenance/DatabaseAnalyzer'),
|
||||||
|
'buttons' => [
|
||||||
|
[
|
||||||
|
'btnClass' => 'btn-default t3js-databaseAnalyzer-analyze',
|
||||||
|
'text' => 'Run database compare again',
|
||||||
|
], [
|
||||||
|
'btnClass' => 'btn-warning t3js-databaseAnalyzer-execute',
|
||||||
|
'text' => 'Apply selected changes',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze current database situation
|
||||||
|
*/
|
||||||
|
public function databaseAnalyzerAnalyzeAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$container = $this->lateBootService->loadExtLocalconfDatabase();
|
||||||
|
$schemaMigrator = $container->get(SchemaMigrator::class);
|
||||||
|
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
$suggestions = [];
|
||||||
|
try {
|
||||||
|
$sqlReader = $container->get(SqlReader::class);
|
||||||
|
$sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
|
||||||
|
$addCreateChange = $schemaMigrator->getUpdateSuggestions($sqlStatements);
|
||||||
|
|
||||||
|
// Aggregate the per-connection statements into one flat array
|
||||||
|
$addCreateChange = array_merge_recursive(...array_values($addCreateChange));
|
||||||
|
if (!empty($addCreateChange['create_table'])) {
|
||||||
|
$suggestion = [
|
||||||
|
'key' => 'addTable',
|
||||||
|
'label' => 'Add tables',
|
||||||
|
'enabled' => true,
|
||||||
|
'children' => [],
|
||||||
|
];
|
||||||
|
foreach ($addCreateChange['create_table'] as $hash => $statement) {
|
||||||
|
$suggestion['children'][] = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'statement' => $statement,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$suggestions[] = $suggestion;
|
||||||
|
}
|
||||||
|
if (!empty($addCreateChange['add'])) {
|
||||||
|
$suggestion = [
|
||||||
|
'key' => 'addField',
|
||||||
|
'label' => 'Add fields to tables',
|
||||||
|
'enabled' => true,
|
||||||
|
'children' => [],
|
||||||
|
];
|
||||||
|
foreach ($addCreateChange['add'] as $hash => $statement) {
|
||||||
|
$suggestion['children'][] = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'statement' => $statement,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$suggestions[] = $suggestion;
|
||||||
|
}
|
||||||
|
if (!empty($addCreateChange['change'])) {
|
||||||
|
$suggestion = [
|
||||||
|
'key' => 'change',
|
||||||
|
'label' => 'Change fields',
|
||||||
|
'enabled' => false,
|
||||||
|
'children' => [],
|
||||||
|
];
|
||||||
|
foreach ($addCreateChange['change'] as $hash => $statement) {
|
||||||
|
$child = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'statement' => $statement,
|
||||||
|
];
|
||||||
|
if (isset($addCreateChange['change_currentValue'][$hash])) {
|
||||||
|
$child['current'] = $addCreateChange['change_currentValue'][$hash];
|
||||||
|
}
|
||||||
|
$suggestion['children'][] = $child;
|
||||||
|
}
|
||||||
|
$suggestions[] = $suggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difference from current to expected
|
||||||
|
$dropRename = $schemaMigrator->getUpdateSuggestions($sqlStatements, true);
|
||||||
|
|
||||||
|
// Aggregate the per-connection statements into one flat array
|
||||||
|
$dropRename = array_merge_recursive(...array_values($dropRename));
|
||||||
|
if (!empty($dropRename['change_table'])) {
|
||||||
|
$suggestion = [
|
||||||
|
'key' => 'renameTableToUnused',
|
||||||
|
'label' => 'Remove tables (rename with prefix)',
|
||||||
|
'enabled' => false,
|
||||||
|
'children' => [],
|
||||||
|
];
|
||||||
|
foreach ($dropRename['change_table'] as $hash => $statement) {
|
||||||
|
$child = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'statement' => $statement,
|
||||||
|
];
|
||||||
|
if (!empty($dropRename['tables_count'][$hash])) {
|
||||||
|
$child['rowCount'] = $dropRename['tables_count'][$hash];
|
||||||
|
}
|
||||||
|
$suggestion['children'][] = $child;
|
||||||
|
}
|
||||||
|
$suggestions[] = $suggestion;
|
||||||
|
}
|
||||||
|
if (!empty($dropRename['change'])) {
|
||||||
|
$suggestion = [
|
||||||
|
'key' => 'renameTableFieldToUnused',
|
||||||
|
'label' => 'Remove unused fields (rename with prefix)',
|
||||||
|
'enabled' => false,
|
||||||
|
'children' => [],
|
||||||
|
];
|
||||||
|
foreach ($dropRename['change'] as $hash => $statement) {
|
||||||
|
$suggestion['children'][] = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'statement' => $statement,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$suggestions[] = $suggestion;
|
||||||
|
}
|
||||||
|
if (!empty($dropRename['drop'])) {
|
||||||
|
$suggestion = [
|
||||||
|
'key' => 'deleteField',
|
||||||
|
'label' => 'Drop fields (really!)',
|
||||||
|
'enabled' => false,
|
||||||
|
'children' => [],
|
||||||
|
];
|
||||||
|
foreach ($dropRename['drop'] as $hash => $statement) {
|
||||||
|
$suggestion['children'][] = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'statement' => $statement,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$suggestions[] = $suggestion;
|
||||||
|
}
|
||||||
|
if (!empty($dropRename['drop_table'])) {
|
||||||
|
$suggestion = [
|
||||||
|
'key' => 'deleteTable',
|
||||||
|
'label' => 'Drop tables (really!)',
|
||||||
|
'enabled' => false,
|
||||||
|
'children' => [],
|
||||||
|
];
|
||||||
|
foreach ($dropRename['drop_table'] as $hash => $statement) {
|
||||||
|
$child = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'statement' => $statement,
|
||||||
|
];
|
||||||
|
if (!empty($dropRename['tables_count'][$hash])) {
|
||||||
|
$child['rowCount'] = $dropRename['tables_count'][$hash];
|
||||||
|
}
|
||||||
|
$suggestion['children'][] = $child;
|
||||||
|
}
|
||||||
|
$suggestions[] = $suggestion;
|
||||||
|
}
|
||||||
|
} catch (StatementException $e) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
$e->getMessage(),
|
||||||
|
'Database analysis failed',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
'suggestions' => $suggestions,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply selected database changes
|
||||||
|
*/
|
||||||
|
public function databaseAnalyzerExecuteAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$container = $this->lateBootService->loadExtLocalconfDatabase();
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
$selectedHashes = $request->getParsedBody()['install']['hashes'] ?? [];
|
||||||
|
if (empty($selectedHashes)) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Please select any change by activating their respective checkboxes.',
|
||||||
|
'No database changes selected',
|
||||||
|
ContextualFeedbackSeverity::WARNING
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$sqlReader = $container->get(SqlReader::class);
|
||||||
|
$sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
|
||||||
|
$statementHashesToPerform = array_flip($selectedHashes);
|
||||||
|
$schemaMigrator = $container->get(SchemaMigrator::class);
|
||||||
|
$results = $schemaMigrator->migrate($sqlStatements, $statementHashesToPerform);
|
||||||
|
// Create error flash messages if any
|
||||||
|
foreach ($results as $errorMessage) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Error: ' . $errorMessage,
|
||||||
|
'Database update failed',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Executed database updates',
|
||||||
|
'Executed database updates'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear table overview statistics action
|
||||||
|
*/
|
||||||
|
public function clearTablesStatsAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'clearTablesClearToken' => $formProtection->generateToken('installTool', 'clearTablesClear'),
|
||||||
|
]);
|
||||||
|
$container = $this->lateBootService->getContainer(true);
|
||||||
|
$clearTableService = $container->get(ClearTableService::class);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'stats' => $clearTableService->getTableStatistics(),
|
||||||
|
'html' => $view->render('Maintenance/ClearTables'),
|
||||||
|
'buttons' => [
|
||||||
|
[
|
||||||
|
'btnClass' => 'btn-default t3js-clearTables-stats',
|
||||||
|
'text' => 'Scan again',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate a specific table
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function clearTablesClearAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$table = $request->getParsedBody()['install']['table'];
|
||||||
|
if (empty($table)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'No table name given',
|
||||||
|
1501944076
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$container = $this->lateBootService->getContainer(true);
|
||||||
|
$clearTableService = $container->get(ClearTableService::class);
|
||||||
|
$clearTableService->clearSelectedTable($table);
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
$messageQueue->enqueue(
|
||||||
|
new FlashMessage('The table ' . $table . ' has been cleared.', 'Table cleared')
|
||||||
|
);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create Admin Get Data action
|
||||||
|
*/
|
||||||
|
public function createAdminGetDataAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'createAdminToken' => $formProtection->generateToken('installTool', 'createAdmin'),
|
||||||
|
'passwordPolicyRequirements' => $this->passwordPolicyValidator->getRequirements(),
|
||||||
|
]);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Maintenance/CreateAdmin'),
|
||||||
|
'buttons' => [
|
||||||
|
[
|
||||||
|
'btnClass' => 'btn-default t3js-createAdmin-create',
|
||||||
|
'text' => 'Create administrator user',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a backend administrator from given username and password
|
||||||
|
*/
|
||||||
|
public function createAdminAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$userCreated = false;
|
||||||
|
$username = preg_replace('/\\s/i', '', $request->getParsedBody()['install']['userName']);
|
||||||
|
$password = $request->getParsedBody()['install']['userPassword'];
|
||||||
|
$passwordCheck = $request->getParsedBody()['install']['userPasswordCheck'];
|
||||||
|
$email = $request->getParsedBody()['install']['userEmail'] ?? '';
|
||||||
|
$realName = $request->getParsedBody()['install']['realName'] ?? '';
|
||||||
|
$isSystemMaintainer = ((bool)$request->getParsedBody()['install']['userSystemMaintainer'] == '1') ? true : false;
|
||||||
|
|
||||||
|
$messages = new FlashMessageQueue('install');
|
||||||
|
$contextData = new ContextData(newUsername: $username);
|
||||||
|
|
||||||
|
if ($username === '') {
|
||||||
|
$messages->enqueue(new FlashMessage(
|
||||||
|
'No username given.',
|
||||||
|
'Administrator user not created',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} elseif ($password !== $passwordCheck) {
|
||||||
|
$messages->enqueue(new FlashMessage(
|
||||||
|
'Passwords do not match.',
|
||||||
|
'Administrator user not created',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} elseif (!$this->passwordPolicyValidator->isValidPassword($password, $contextData)) {
|
||||||
|
$messages->enqueue(new FlashMessage(
|
||||||
|
'The password does not meet the password policy requirements.',
|
||||||
|
'Administrator user not created',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$container = $this->lateBootService->getContainer(true);
|
||||||
|
$connectionPool = $container->get(ConnectionPool::class);
|
||||||
|
$userExists = $connectionPool->getConnectionForTable('be_users')
|
||||||
|
->count(
|
||||||
|
'uid',
|
||||||
|
'be_users',
|
||||||
|
['username' => $username]
|
||||||
|
);
|
||||||
|
if ($userExists) {
|
||||||
|
$messages->enqueue(new FlashMessage(
|
||||||
|
'A user with username "' . $username . '" exists already.',
|
||||||
|
'Administrator user not created',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$hashInstance = $this->passwordHashFactory->getDefaultHashInstance('BE');
|
||||||
|
$hashedPassword = $hashInstance->getHashedPassword($password);
|
||||||
|
$adminUserFields = [
|
||||||
|
'username' => $username,
|
||||||
|
'password' => $hashedPassword,
|
||||||
|
'admin' => 1,
|
||||||
|
'realName' => $realName,
|
||||||
|
'tstamp' => $GLOBALS['EXEC_TIME'],
|
||||||
|
'crdate' => $GLOBALS['EXEC_TIME'],
|
||||||
|
];
|
||||||
|
if (GeneralUtility::validEmail($email)) {
|
||||||
|
$adminUserFields['email'] = $email;
|
||||||
|
}
|
||||||
|
$connectionPool->getConnectionForTable('be_users')->insert('be_users', $adminUserFields);
|
||||||
|
$userCreated = true;
|
||||||
|
|
||||||
|
if ($isSystemMaintainer) {
|
||||||
|
// Get the new admin user uid just created
|
||||||
|
$newAdminUserUid = (int)$connectionPool->getConnectionForTable('be_users')->lastInsertId();
|
||||||
|
|
||||||
|
// Get the list of the existing systemMaintainer
|
||||||
|
$existingSystemMaintainersList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
|
||||||
|
|
||||||
|
// Add the new admin user to the existing systemMaintainer list
|
||||||
|
$newSystemMaintainersList = $existingSystemMaintainersList;
|
||||||
|
$newSystemMaintainersList[] = $newAdminUserUid;
|
||||||
|
|
||||||
|
// Update the system/settings.php file with the new list
|
||||||
|
$this->configurationManager->setLocalConfigurationValuesByPathValuePairs(
|
||||||
|
['SYS/systemMaintainers' => $newSystemMaintainersList]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$messages->enqueue(new FlashMessage(
|
||||||
|
'An administrator with username "' . $username . '" has been created successfully.',
|
||||||
|
'Administrator created'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messages,
|
||||||
|
'userCreated' => $userCreated,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry action of language packs module gets
|
||||||
|
* * list of available languages with details like active or not and last update
|
||||||
|
* * list of loaded extensions
|
||||||
|
*/
|
||||||
|
public function languagePacksGetDataAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||||
|
$view->assignMultiple([
|
||||||
|
'isWritable' => $isWritable,
|
||||||
|
'languagePacksActivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksActivateLanguage'),
|
||||||
|
'languagePacksDeactivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksDeactivateLanguage'),
|
||||||
|
'languagePacksUpdatePackToken' => $formProtection->generateToken('installTool', 'languagePacksUpdatePack'),
|
||||||
|
'languagePacksUpdateIsoTimesToken' => $formProtection->generateToken('installTool', 'languagePacksUpdateIsoTimes'),
|
||||||
|
]);
|
||||||
|
// This action needs TYPO3_CONF_VARS for full GeneralUtility::getUrl() config
|
||||||
|
$container = $this->lateBootService->loadExtLocalconfDatabase(false, true);
|
||||||
|
$languagePackService = $container->get(LanguagePackService::class);
|
||||||
|
$extensions = $languagePackService->getExtensionLanguagePackDetails();
|
||||||
|
$extensionList = array_map(function (array $extension) {
|
||||||
|
$extension['packs'] = array_values($extension['packs']);
|
||||||
|
return $extension;
|
||||||
|
}, array_values($extensions));
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'languages' => $languagePackService->getLanguageDetails(),
|
||||||
|
'extensions' => $extensionList,
|
||||||
|
'activeLanguages' => $languagePackService->getActiveLanguages(),
|
||||||
|
'activeExtensions' => array_column($extensions, 'key'),
|
||||||
|
'html' => $view->render('Maintenance/LanguagePacks'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate a language and any possible dependency it may have
|
||||||
|
*/
|
||||||
|
public function languagePacksActivateLanguageAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
$container = $this->lateBootService->getContainer(true);
|
||||||
|
$languagePackService = $container->get(LanguagePackService::class);
|
||||||
|
$availableLanguages = $languagePackService->getAvailableLanguages();
|
||||||
|
$activeLanguages = $languagePackService->getActiveLanguages();
|
||||||
|
$iso = $request->getParsedBody()['install']['iso'];
|
||||||
|
|
||||||
|
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
sprintf('The language %s was not activated as the configuration file is not writable.', $availableLanguages[$iso]),
|
||||||
|
'Language not activated',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$activateArray = [];
|
||||||
|
foreach ($availableLanguages as $availableIso => $name) {
|
||||||
|
if ($availableIso === $iso && !in_array($availableIso, $activeLanguages, true)) {
|
||||||
|
$activateArray[] = $iso;
|
||||||
|
$dependencies = $this->locales->getLocaleDependencies($availableIso);
|
||||||
|
if (!empty($dependencies)) {
|
||||||
|
foreach ($dependencies as $dependency) {
|
||||||
|
if (!in_array($dependency, $activeLanguages, true)) {
|
||||||
|
$activateArray[] = $dependency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($activateArray)) {
|
||||||
|
$activeLanguages = array_merge($activeLanguages, $activateArray);
|
||||||
|
sort($activeLanguages);
|
||||||
|
$this->configurationManager->setLocalConfigurationValueByPath(
|
||||||
|
'LANG',
|
||||||
|
['availableLocales' => $activeLanguages]
|
||||||
|
);
|
||||||
|
$activationArray = [];
|
||||||
|
foreach ($activateArray as $activateIso) {
|
||||||
|
$activationArray[] = $availableLanguages[$activateIso] . ' (' . $activateIso . ')';
|
||||||
|
}
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'These languages have been activated: ' . implode(', ', $activationArray)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Language with ISO code "' . $iso . '" not found or already active.',
|
||||||
|
'',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate a language if no other active language depends on it
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function languagePacksDeactivateLanguageAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
$container = $this->lateBootService->getContainer(true);
|
||||||
|
$languagePackService = $container->get(LanguagePackService::class);
|
||||||
|
$availableLanguages = $languagePackService->getAvailableLanguages();
|
||||||
|
$activeLanguages = $languagePackService->getActiveLanguages();
|
||||||
|
$iso = $request->getParsedBody()['install']['iso'];
|
||||||
|
|
||||||
|
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
sprintf('The language %s was not deactivated as the configuration file is not writable.', $availableLanguages[$iso]),
|
||||||
|
'Language not deactivated',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
if (empty($iso)) {
|
||||||
|
throw new \RuntimeException('No iso code given', 1520109807);
|
||||||
|
}
|
||||||
|
$otherActiveLanguageDependencies = [];
|
||||||
|
foreach ($activeLanguages as $activeLanguage) {
|
||||||
|
if ($activeLanguage === $iso) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$dependencies = $this->locales->getLocaleDependencies($activeLanguage);
|
||||||
|
if (in_array($iso, $dependencies, true)) {
|
||||||
|
$otherActiveLanguageDependencies[] = $activeLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($otherActiveLanguageDependencies)) {
|
||||||
|
// Error: Must disable dependencies first
|
||||||
|
$dependentArray = [];
|
||||||
|
foreach ($otherActiveLanguageDependencies as $dependency) {
|
||||||
|
$dependentArray[] = $availableLanguages[$dependency] . ' (' . $dependency . ')';
|
||||||
|
}
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" can not be deactivated. These'
|
||||||
|
. ' other languages depend on it and need to be deactivated before:'
|
||||||
|
. implode(', ', $dependentArray),
|
||||||
|
'',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
if (in_array($iso, $activeLanguages, true)) {
|
||||||
|
// Deactivate this language
|
||||||
|
$newActiveLanguages = [];
|
||||||
|
foreach ($activeLanguages as $activeLanguage) {
|
||||||
|
if ($activeLanguage === $iso) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$newActiveLanguages[] = $activeLanguage;
|
||||||
|
}
|
||||||
|
$this->configurationManager->setLocalConfigurationValueByPath(
|
||||||
|
'LANG',
|
||||||
|
['availableLocales' => $newActiveLanguages]
|
||||||
|
);
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has been deactivated'
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has not been deactivated',
|
||||||
|
'',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a pack of one extension and one language
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function languagePacksUpdatePackAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$container = $this->lateBootService->loadExtLocalconfDatabase(false, true);
|
||||||
|
$iso = $request->getParsedBody()['install']['iso'];
|
||||||
|
$key = $request->getParsedBody()['install']['extension'];
|
||||||
|
|
||||||
|
$languagePackService = $container->get(LanguagePackService::class);
|
||||||
|
|
||||||
|
// Gate untrusted user input against the set of extensions and languages exposed for download.
|
||||||
|
$extensions = $languagePackService->getExtensionLanguagePackDetails();
|
||||||
|
if (!isset($extensions[$key]['packs'][$iso])) {
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'packResult' => 'skipped',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'packResult' => $languagePackService->languagePackDownload($key, $iso),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set "last updated" time in registry for fully updated language packs.
|
||||||
|
*/
|
||||||
|
public function languagePacksUpdateIsoTimesAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$isos = $request->getParsedBody()['install']['isos'];
|
||||||
|
$container = $this->lateBootService->getContainer(true);
|
||||||
|
$languagePackService = $container->get(LanguagePackService::class);
|
||||||
|
$languagePackService->setLastUpdatedIsoCode($isos);
|
||||||
|
|
||||||
|
// The cache manager is already instantiated in the install tool
|
||||||
|
// with some hacked settings to disable caching of extbase and fluid.
|
||||||
|
// We want a "fresh" object here to operate on a different cache setup.
|
||||||
|
// cacheManager implements SingletonInterface, so the only way to get a "fresh"
|
||||||
|
// instance is by circumventing makeInstance and using new directly!
|
||||||
|
$cacheManager = new CacheManager();
|
||||||
|
$cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
|
||||||
|
$cacheManager->getCache('l10n')->flush();
|
||||||
|
|
||||||
|
return new JsonResponse(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set 'uc' field of all backend users to empty string
|
||||||
|
*/
|
||||||
|
public function resetBackendUserUcAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
$container = $this->lateBootService->getContainer(true);
|
||||||
|
$connectionPool = $container->get(ConnectionPool::class);
|
||||||
|
$connectionPool
|
||||||
|
->getQueryBuilderForTable('be_users')
|
||||||
|
->update('be_users')
|
||||||
|
->set('uc', '')
|
||||||
|
->executeStatement();
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Preferences of all backend users have been reset',
|
||||||
|
'Reset preferences of all backend users'
|
||||||
|
));
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show reference index card
|
||||||
|
*/
|
||||||
|
public function referenceIndexAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'referenceIndexToken' => $this->formProtectionFactory->createFromRequest($request)->generateToken('installTool', 'referenceIndexUpdate'),
|
||||||
|
'binaryPath' => ExtensionManagementUtility::extPath('core', 'bin/typo3'),
|
||||||
|
]);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Maintenance/ReferenceIndex'),
|
||||||
|
'buttons' => [
|
||||||
|
[
|
||||||
|
'btnClass' => 'btn-default t3js-referenceIndex-check',
|
||||||
|
'text' => 'Check Reference Index',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'btnClass' => 'btn-default t3js-referenceIndex-update',
|
||||||
|
'text' => 'Update Reference Index',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check or update reference index
|
||||||
|
*/
|
||||||
|
public function referenceIndexUpdateAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$isCheckOnly = (bool)($request->getParsedBody()['install']['checkOnly'] ?? false);
|
||||||
|
|
||||||
|
$container = $this->lateBootService->loadExtLocalconfDatabase(false, true);
|
||||||
|
$result = $container->get(ReferenceIndex::class)->updateIndex($isCheckOnly);
|
||||||
|
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
if (!empty($result['errors'])) {
|
||||||
|
foreach ($result['errors'] as $error) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
$error,
|
||||||
|
'Reference Index Issue',
|
||||||
|
ContextualFeedbackSeverity::WARNING
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
$isCheckOnly ? 'Reference index check completed successfully' : 'Reference index has been updated successfully',
|
||||||
|
$isCheckOnly ? 'Check Complete' : 'Update Complete'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
'result' => $result,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Crypto\HashService;
|
||||||
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used from backend `/typo3` context to check webserver response in general (independent of install tool).
|
||||||
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
*/
|
||||||
|
readonly class ServerResponseCheckController
|
||||||
|
{
|
||||||
|
public function __construct(private HashService $hashService) {}
|
||||||
|
|
||||||
|
public function checkHostAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$time = $request->getQueryParams()['src-time'] ?? null;
|
||||||
|
$hash = $request->getQueryParams()['src-hash'] ?? null;
|
||||||
|
|
||||||
|
if (empty($time) || !is_string($time) || empty($hash) || !is_string($hash)) {
|
||||||
|
return new JsonResponse(['error' => 'Query params src-time` and src-hash` are required.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedHash = $this->hashService->hmac($time, 'server-response-check');
|
||||||
|
if (!hash_equals($expectedHash, $hash)) {
|
||||||
|
return new JsonResponse(['error' => 'Invalid time or hash provided.'], 400);
|
||||||
|
}
|
||||||
|
if ((int)$time + 60 < time()) {
|
||||||
|
return new JsonResponse(['error' => 'Request expired.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'server.HTTP_HOST' => $_SERVER['HTTP_HOST'] ?? null,
|
||||||
|
'server.SERVER_NAME' => $_SERVER['SERVER_NAME'] ?? null,
|
||||||
|
'server.SERVER_PORT' => $_SERVER['SERVER_PORT'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,654 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||||
|
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
|
||||||
|
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||||
|
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||||
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||||
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||||
|
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||||
|
use TYPO3\CMS\Core\Localization\Locales;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||||
|
use TYPO3\CMS\Core\Package\PackageManager;
|
||||||
|
use TYPO3\CMS\Core\PasswordPolicy\PasswordService;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Core\TypoScript\AST\CommentAwareAstBuilder;
|
||||||
|
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||||
|
use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser;
|
||||||
|
use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstConstantCommentVisitor;
|
||||||
|
use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||||
|
use TYPO3\CMS\Install\Configuration\FeatureManager;
|
||||||
|
use TYPO3\CMS\Install\Service\LateBootService;
|
||||||
|
use TYPO3\CMS\Install\Service\LocalConfigurationValueService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings controller
|
||||||
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||||
|
*/
|
||||||
|
class SettingsController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly LateBootService $lateBootService,
|
||||||
|
private readonly PackageManager $packageManager,
|
||||||
|
private readonly LanguageServiceFactory $languageServiceFactory,
|
||||||
|
private readonly CommentAwareAstBuilder $astBuilder,
|
||||||
|
private readonly LosslessTokenizer $losslessTokenizer,
|
||||||
|
private readonly AstTraverser $astTraverser,
|
||||||
|
private readonly FormProtectionFactory $formProtectionFactory,
|
||||||
|
private readonly ConfigurationManager $configurationManager,
|
||||||
|
private readonly PasswordService $passwordService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main "show the cards" view
|
||||||
|
*/
|
||||||
|
public function cardsAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$view->assign('isWritable', $this->configurationManager->canWriteConfiguration());
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Settings/Cards'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change install tool password
|
||||||
|
*/
|
||||||
|
public function changeInstallToolPasswordGetDataAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||||
|
$view->assignMultiple([
|
||||||
|
'isWritable' => $isWritable,
|
||||||
|
'changeInstallToolPasswordToken' => $formProtection->generateToken('installTool', 'changeInstallToolPassword'),
|
||||||
|
]);
|
||||||
|
$buttons = [];
|
||||||
|
if ($isWritable) {
|
||||||
|
$buttons[] = [
|
||||||
|
'btnClass' => 'btn-default t3js-changeInstallToolPassword-change',
|
||||||
|
'text' => 'Set new password',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Settings/ChangeInstallToolPassword'),
|
||||||
|
'buttons' => $buttons,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change install tool password
|
||||||
|
*/
|
||||||
|
public function changeInstallToolPasswordAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'The configuration file is not writable.',
|
||||||
|
'Configuration not writable',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$password = $request->getParsedBody()['install']['password'] ?? '';
|
||||||
|
$passwordCheck = $request->getParsedBody()['install']['passwordCheck'];
|
||||||
|
$validationResultErrors = $this->passwordService->getValidationErrorsForInstallToolUpdate($password);
|
||||||
|
if ($password !== $passwordCheck) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'Given passwords do not match.',
|
||||||
|
'Install tool password not changed',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} elseif ($validationResultErrors !== []) {
|
||||||
|
$errors = array_values($validationResultErrors);
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
implode('. ', $errors) . '.',
|
||||||
|
'Install tool password not changed',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
|
||||||
|
$this->configurationManager->setLocalConfigurationValueByPath(
|
||||||
|
'BE/installToolPassword',
|
||||||
|
$hashInstance->getHashedPassword($password)
|
||||||
|
);
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'The Install tool password has been changed successfully.',
|
||||||
|
'Install tool password changed'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of possible and active system maintainers
|
||||||
|
*/
|
||||||
|
public function systemMaintainerGetListAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$container = $this->lateBootService->getContainer(true);
|
||||||
|
$connectionPool = $container->get(ConnectionPool::class);
|
||||||
|
|
||||||
|
// We have to respect the enable fields here by our own because no TCA is loaded
|
||||||
|
$queryBuilder = $connectionPool->getQueryBuilderForTable('be_users');
|
||||||
|
$queryBuilder->getRestrictions()->removeAll();
|
||||||
|
$users = $queryBuilder
|
||||||
|
->select('uid', 'username', 'disable', 'starttime', 'endtime')
|
||||||
|
->from('be_users')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->and(
|
||||||
|
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||||
|
$queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)),
|
||||||
|
$queryBuilder->expr()->neq('username', $queryBuilder->createNamedParameter('_cli_'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->orderBy('uid')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$systemMaintainerList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
|
||||||
|
$systemMaintainerList = array_map('intval', $systemMaintainerList);
|
||||||
|
$currentTime = time();
|
||||||
|
foreach ($users as &$user) {
|
||||||
|
$user['disable'] = $user['disable']
|
||||||
|
|| ((int)$user['starttime'] !== 0 && $user['starttime'] > $currentTime)
|
||||||
|
|| ((int)$user['endtime'] !== 0 && $user['endtime'] < $currentTime);
|
||||||
|
$user['isSystemMaintainer'] = in_array((int)$user['uid'], $systemMaintainerList, true);
|
||||||
|
}
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||||
|
$view->assignMultiple([
|
||||||
|
'isWritable' => $isWritable,
|
||||||
|
'users' => $users,
|
||||||
|
'systemMaintainerWriteToken' => $formProtection->generateToken('installTool', 'systemMaintainerWrite'),
|
||||||
|
'systemMaintainerIsDevelopmentContext' => Environment::getContext()->isDevelopment(),
|
||||||
|
]);
|
||||||
|
$buttons = [];
|
||||||
|
if ($isWritable) {
|
||||||
|
$buttons[] = [
|
||||||
|
'btnClass' => 'btn-default t3js-systemMaintainer-write',
|
||||||
|
'text' => 'Save system maintainer list',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'users' => $users,
|
||||||
|
'html' => $view->render('Settings/SystemMaintainer'),
|
||||||
|
'buttons' => $buttons,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write new system maintainer list
|
||||||
|
*/
|
||||||
|
public function systemMaintainerWriteAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$messages = [];
|
||||||
|
|
||||||
|
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||||
|
$messages[] = new FlashMessage(
|
||||||
|
'The configuration file is not writable.',
|
||||||
|
'Configuration not writable',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Sanitize given user list and write out
|
||||||
|
$newUserList = [];
|
||||||
|
$users = $request->getParsedBody()['install']['users'] ?? [];
|
||||||
|
if (is_array($users)) {
|
||||||
|
foreach ($users as $uid) {
|
||||||
|
if (MathUtility::canBeInterpretedAsInteger($uid)) {
|
||||||
|
$newUserList[] = (int)$uid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$container = $this->lateBootService->getContainer(true);
|
||||||
|
$connectionPool = $container->get(ConnectionPool::class);
|
||||||
|
$queryBuilder = $connectionPool->getQueryBuilderForTable('be_users');
|
||||||
|
$queryBuilder->getRestrictions()->removeAll();
|
||||||
|
|
||||||
|
$validatedUserList = $queryBuilder
|
||||||
|
->select('uid')
|
||||||
|
->from('be_users')
|
||||||
|
->where(
|
||||||
|
$queryBuilder->expr()->and(
|
||||||
|
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||||
|
$queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)),
|
||||||
|
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($newUserList, Connection::PARAM_INT_ARRAY))
|
||||||
|
)
|
||||||
|
)->executeQuery()->fetchAllAssociative();
|
||||||
|
|
||||||
|
$validatedUserList = array_column($validatedUserList, 'uid');
|
||||||
|
$validatedUserList = array_map('intval', $validatedUserList);
|
||||||
|
|
||||||
|
$this->configurationManager->setLocalConfigurationValuesByPathValuePairs(
|
||||||
|
['SYS/systemMaintainers' => $validatedUserList]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (empty($validatedUserList)) {
|
||||||
|
$messages[] = new FlashMessage(
|
||||||
|
'The system has no maintainers enabled anymore. Please use the standalone Install Tools from now on.',
|
||||||
|
'Cleared system maintainer list',
|
||||||
|
ContextualFeedbackSeverity::INFO
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$messages[] = new FlashMessage(
|
||||||
|
'New system maintainer uid list: ' . implode(', ', $validatedUserList),
|
||||||
|
'Updated system maintainers',
|
||||||
|
ContextualFeedbackSeverity::INFO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main LocalConfiguration card content
|
||||||
|
*/
|
||||||
|
public function localConfigurationGetContentAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$localConfigurationValueService = new LocalConfigurationValueService();
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'isWritable' => $isWritable,
|
||||||
|
'localConfigurationWriteToken' => $formProtection->generateToken('installTool', 'localConfigurationWrite'),
|
||||||
|
'localConfigurationData' => $this->enrichConfigurationData($localConfigurationValueService->getCurrentConfigurationData()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$buttons = [
|
||||||
|
[
|
||||||
|
'btnClass' => 'btn-default t3js-localConfiguration-toggleAll',
|
||||||
|
'text' => 'Toggle All',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isWritable) {
|
||||||
|
$buttons[] = [
|
||||||
|
'btnClass' => 'btn-default t3js-localConfiguration-write',
|
||||||
|
'text' => 'Write configuration',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Settings/LocalConfigurationGetContent'),
|
||||||
|
'buttons' => $buttons,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write given LocalConfiguration settings
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function localConfigurationWriteAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'The configuration file is not writable.',
|
||||||
|
'Configuration not writable',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$settings = $request->getParsedBody()['install']['configurationValues'];
|
||||||
|
if (!is_array($settings) || empty($settings)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Expected value array not found',
|
||||||
|
1502282283
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$localConfigurationValueService = new LocalConfigurationValueService();
|
||||||
|
$messageQueue = $localConfigurationValueService->updateLocalConfigurationValues($settings);
|
||||||
|
if ($messageQueue->count() === 0) {
|
||||||
|
$messageQueue->enqueue(new FlashMessage(
|
||||||
|
'No configuration changes have been detected in the submitted form.',
|
||||||
|
'Configuration not updated',
|
||||||
|
ContextualFeedbackSeverity::WARNING
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messageQueue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main preset card content
|
||||||
|
*/
|
||||||
|
public function presetsGetContentAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||||
|
$presetFeatures = GeneralUtility::makeInstance(FeatureManager::class);
|
||||||
|
$presetFeatures = $presetFeatures->getInitializedFeatures($request->getParsedBody()['install']['values'] ?? []);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'isWritable' => $isWritable,
|
||||||
|
'presetsActivateToken' => $formProtection->generateToken('installTool', 'presetsActivate'),
|
||||||
|
// This action is called again from within the card itself if a custom image path is supplied
|
||||||
|
'presetsGetContentToken' => $formProtection->generateToken('installTool', 'presetsGetContent'),
|
||||||
|
'presetFeatures' => $presetFeatures,
|
||||||
|
]);
|
||||||
|
$buttons = [];
|
||||||
|
if ($isWritable) {
|
||||||
|
$buttons[] = [
|
||||||
|
'btnClass' => 'btn-default t3js-presets-activate',
|
||||||
|
'text' => 'Activate preset',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Settings/PresetsGetContent'),
|
||||||
|
'buttons' => $buttons,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write selected presets
|
||||||
|
*/
|
||||||
|
public function presetsActivateAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$messages = new FlashMessageQueue('install');
|
||||||
|
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||||
|
$messages->enqueue(new FlashMessage(
|
||||||
|
'The configuration file is not writable.',
|
||||||
|
'Configuration not writable',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$featureManager = new FeatureManager();
|
||||||
|
$configurationValues = $featureManager->getConfigurationForSelectedFeaturePresets($request->getParsedBody()['install']['values'] ?? []);
|
||||||
|
if (!empty($configurationValues)) {
|
||||||
|
$this->configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationValues);
|
||||||
|
$messageBody = [];
|
||||||
|
foreach ($configurationValues as $configurationKey => $configurationValue) {
|
||||||
|
if (is_array($configurationValue)) {
|
||||||
|
$configurationValue = json_encode($configurationValue);
|
||||||
|
}
|
||||||
|
$messageBody[] = '\'' . $configurationKey . '\' => \'' . $configurationValue . '\'';
|
||||||
|
}
|
||||||
|
$messages->enqueue(new FlashMessage(
|
||||||
|
implode(', ', $messageBody),
|
||||||
|
'Configuration written'
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$messages->enqueue(new FlashMessage(
|
||||||
|
'',
|
||||||
|
'No configuration change selected',
|
||||||
|
ContextualFeedbackSeverity::INFO
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a list of extensions with their configuration form.
|
||||||
|
*/
|
||||||
|
public function extensionConfigurationGetContentAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
// Extension configuration needs initialized $GLOBALS['LANG']
|
||||||
|
$GLOBALS['LANG'] = $this->languageServiceFactory->create('en');
|
||||||
|
$extensionsWithConfigurations = [];
|
||||||
|
$activePackages = $this->packageManager->getActivePackages();
|
||||||
|
$extensionConfiguration = new ExtensionConfiguration();
|
||||||
|
foreach ($activePackages as $extensionKey => $activePackage) {
|
||||||
|
if (@file_exists($activePackage->getPackagePath() . 'ext_conf_template.txt')) {
|
||||||
|
$ast = $this->astBuilder->build(
|
||||||
|
$this->losslessTokenizer->tokenize(file_get_contents($activePackage->getPackagePath() . 'ext_conf_template.txt')),
|
||||||
|
new RootNode()
|
||||||
|
);
|
||||||
|
$astConstantCommentVisitor = new (AstConstantCommentVisitor::class);
|
||||||
|
$this->astTraverser->traverse($ast, [$astConstantCommentVisitor]);
|
||||||
|
$constants = $astConstantCommentVisitor->getConstants();
|
||||||
|
// @todo: It would be better to fetch all LocalConfiguration settings of an extension at once
|
||||||
|
// and feed it as pseudo-TS to the AST builder. This way the full AstConstantCommentVisitor
|
||||||
|
// preparation magic would kick in and the JS-side processing in extension-configuration.ts
|
||||||
|
// could be removed (especially the 'wrap' and 'offset' stuff) by handling it in fluid directly.
|
||||||
|
foreach ($constants as $constantName => &$constantDetails) {
|
||||||
|
try {
|
||||||
|
$valueFromLocalConfiguration = $extensionConfiguration->get($extensionKey, str_replace('.', '/', $constantName));
|
||||||
|
$constantDetails['value'] = $valueFromLocalConfiguration;
|
||||||
|
} catch (ExtensionConfigurationPathDoesNotExistException $e) {
|
||||||
|
// Deliberately empty - it can happen at runtime that a written config does not return
|
||||||
|
// back all values (eg. saltedpassword with its userFuncs), which then miss in the written
|
||||||
|
// configuration and are only synced after next install tool run. This edge case is
|
||||||
|
// taken care of here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$displayConstants = [];
|
||||||
|
foreach ($astConstantCommentVisitor->getCategories() as $category => $details) {
|
||||||
|
if ($details['usageCount'] > 0) {
|
||||||
|
$displayConstants[$category]['label'] = $details['label'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($constants as $constant) {
|
||||||
|
$displayConstants[$constant['cat']]['items'][$constant['subcat_sorting_first']]['label'] = $constant['subcat_label'];
|
||||||
|
$displayConstants[$constant['cat']]['items'][$constant['subcat_sorting_first']]['items'][$constant['subcat_sorting_second']] = $constant;
|
||||||
|
}
|
||||||
|
foreach ($displayConstants as &$constantCategory) {
|
||||||
|
ksort($constantCategory['items'], SORT_NATURAL);
|
||||||
|
foreach ($constantCategory['items'] as &$constantDetailItems) {
|
||||||
|
ksort($constantDetailItems['items'], SORT_NATURAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$extensionsWithConfigurations[$extensionKey] = $displayConstants;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ksort($extensionsWithConfigurations);
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'isWritable' => $isWritable,
|
||||||
|
'extensionsWithConfigurations' => $extensionsWithConfigurations,
|
||||||
|
'extensionConfigurationWriteToken' => $formProtection->generateToken('installTool', 'extensionConfigurationWrite'),
|
||||||
|
]);
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Settings/ExtensionConfigurationGetContent'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write extension configuration
|
||||||
|
*/
|
||||||
|
public function extensionConfigurationWriteAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$messages = [];
|
||||||
|
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||||
|
$messages[] = new FlashMessage(
|
||||||
|
'The configuration file is not writable.',
|
||||||
|
'Configuration not writable',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$extensionKey = $request->getParsedBody()['install']['extensionKey'];
|
||||||
|
$configuration = $request->getParsedBody()['install']['extensionConfiguration'] ?? [];
|
||||||
|
$nestedConfiguration = [];
|
||||||
|
foreach ($configuration as $configKey => $value) {
|
||||||
|
$nestedConfiguration = ArrayUtility::setValueByPath($nestedConfiguration, $configKey, $value, '.');
|
||||||
|
}
|
||||||
|
(new ExtensionConfiguration())->set($extensionKey, $nestedConfiguration);
|
||||||
|
$messages[] = new FlashMessage(
|
||||||
|
'Successfully saved configuration for extension "' . $extensionKey . '".',
|
||||||
|
'Configuration saved',
|
||||||
|
ContextualFeedbackSeverity::OK
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $messages,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render feature toggles
|
||||||
|
*/
|
||||||
|
public function featuresGetContentAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||||
|
$configurationDescription = GeneralUtility::makeInstance(YamlFileLoader::class)
|
||||||
|
->load($this->configurationManager->getDefaultConfigurationDescriptionFileLocation());
|
||||||
|
$allFeatures = $GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] ?? [];
|
||||||
|
$features = [];
|
||||||
|
foreach ($allFeatures as $featureName => $featureValue) {
|
||||||
|
// Only features that have a .yml description will be listed. There is currently no
|
||||||
|
// way for extensions to extend this, so feature toggles of non-core extensions are
|
||||||
|
// not listed here.
|
||||||
|
if (isset($configurationDescription['SYS']['items']['features']['items'][$featureName]['description'])) {
|
||||||
|
$default = $this->configurationManager->getDefaultConfigurationValueByPath('SYS/features/' . $featureName);
|
||||||
|
$features[] = [
|
||||||
|
'label' => ucfirst(str_replace(['_', '.'], ' ', strtolower(GeneralUtility::camelCaseToLowerCaseUnderscored(preg_replace('/\./', ': ', $featureName, 1))))),
|
||||||
|
'name' => $featureName,
|
||||||
|
'description' => $configurationDescription['SYS']['items']['features']['items'][$featureName]['description'],
|
||||||
|
'default' => $default,
|
||||||
|
'value' => $featureValue,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||||
|
$view = $this->initializeView($request);
|
||||||
|
$view->assignMultiple([
|
||||||
|
'isWritable' => $isWritable,
|
||||||
|
'features' => $features,
|
||||||
|
'featuresSaveToken' => $formProtection->generateToken('installTool', 'featuresSave'),
|
||||||
|
]);
|
||||||
|
$buttons = [];
|
||||||
|
if ($isWritable) {
|
||||||
|
$buttons[] = [
|
||||||
|
'btnClass' => 'btn-default t3js-features-save',
|
||||||
|
'text' => 'Save',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'html' => $view->render('Settings/FeaturesGetContent'),
|
||||||
|
'buttons' => $buttons,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update feature toggles state
|
||||||
|
*/
|
||||||
|
public function featuresSaveAction(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||||
|
$message = new FlashMessage(
|
||||||
|
'The configuration file is not writable.',
|
||||||
|
'Configuration not writable',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$enabledFeaturesFromPost = $request->getParsedBody()['install']['values'] ?? [];
|
||||||
|
$allFeatures = array_keys($GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] ?? []);
|
||||||
|
$configurationDescription = GeneralUtility::makeInstance(YamlFileLoader::class)
|
||||||
|
->load($this->configurationManager->getDefaultConfigurationDescriptionFileLocation());
|
||||||
|
$updatedFeatures = [];
|
||||||
|
$configurationPathValuePairs = [];
|
||||||
|
foreach ($allFeatures as $featureName) {
|
||||||
|
// Only features that have a .yml description will be listed. There is currently no
|
||||||
|
// way for extensions to extend this, so feature toggles of non-core extensions are
|
||||||
|
// not considered.
|
||||||
|
if (isset($configurationDescription['SYS']['items']['features']['items'][$featureName]['description'])) {
|
||||||
|
$path = 'SYS/features/' . $featureName;
|
||||||
|
$newValue = isset($enabledFeaturesFromPost[$featureName]);
|
||||||
|
if ($newValue !== $this->configurationManager->getConfigurationValueByPath($path)) {
|
||||||
|
$configurationPathValuePairs[$path] = $newValue;
|
||||||
|
$updatedFeatures[] = $featureName . ' [' . ($newValue ? 'On' : 'Off') . ']';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($configurationPathValuePairs !== []) {
|
||||||
|
$success = $this->configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationPathValuePairs);
|
||||||
|
if ($success) {
|
||||||
|
$this->configurationManager->exportConfiguration();
|
||||||
|
$message = new FlashMessage(
|
||||||
|
"Successfully updated the following feature toggles:\n" . implode(",\n", $updatedFeatures),
|
||||||
|
'Features updated',
|
||||||
|
ContextualFeedbackSeverity::OK
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$message = new FlashMessage(
|
||||||
|
'An error occurred while saving. Some settings may not have been updated.',
|
||||||
|
'Features not updated',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message = new FlashMessage(
|
||||||
|
'Nothing to update.',
|
||||||
|
'Features not updated',
|
||||||
|
ContextualFeedbackSeverity::INFO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'status' => [$message],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function enrichConfigurationData(array $data): array
|
||||||
|
{
|
||||||
|
foreach ($data['SYS']['items'] as &$item) {
|
||||||
|
if ($item['key'] === 'systemLocale') {
|
||||||
|
$locales = Locales::getAllSystemLocales();
|
||||||
|
if ($locales === []) {
|
||||||
|
// Install tool operates in English context only, no xlf language label here.
|
||||||
|
$item['description'] .= 'N/A (locale listing could not be fetched)';
|
||||||
|
} else {
|
||||||
|
$locales = array_map(static function ($locale) {
|
||||||
|
return '<code>' . $locale . '</code>';
|
||||||
|
}, $locales);
|
||||||
|
$item['description'] .= implode(', ', $locales);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\CoreVersion;
|
||||||
|
|
||||||
|
class CoreRelease
|
||||||
|
{
|
||||||
|
protected const RELEASE_TYPE_REGULAR = 'regular';
|
||||||
|
protected const RELEASE_TYPE_SECURITY = 'security';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected readonly string $version,
|
||||||
|
protected readonly \DateTimeInterface $date,
|
||||||
|
protected readonly string $type,
|
||||||
|
protected readonly string $checksum,
|
||||||
|
protected readonly bool $isElts = false
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromApiResponse(array $response): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
(string)($response['version'] ?? ''),
|
||||||
|
new \DateTimeImmutable((string)($response['date'] ?? '')),
|
||||||
|
(string)($response['type'] ?? ''),
|
||||||
|
(string)($response['tar_package']['sha1sum'] ?? ''),
|
||||||
|
$response['elts'] ?? false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): string
|
||||||
|
{
|
||||||
|
return $this->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDate(): \DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSecurityUpdate(): bool
|
||||||
|
{
|
||||||
|
return $this->type === self::RELEASE_TYPE_SECURITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChecksum(): string
|
||||||
|
{
|
||||||
|
return $this->checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isElts(): bool
|
||||||
|
{
|
||||||
|
return $this->isElts;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\CoreVersion;
|
||||||
|
|
||||||
|
class MaintenanceWindow
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected readonly ?\DateTimeInterface $communitySupport,
|
||||||
|
protected readonly ?\DateTimeInterface $eltsSupport
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromApiResponse(array $response): self
|
||||||
|
{
|
||||||
|
$maintainedUntil = isset($response['maintained_until']) ? new \DateTimeImmutable($response['maintained_until']) : null;
|
||||||
|
$eltsUntil = isset($response['elts_until']) ? new \DateTimeImmutable($response['elts_until']) : null;
|
||||||
|
|
||||||
|
return new self($maintainedUntil, $eltsUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSupportedByCommunity(): bool
|
||||||
|
{
|
||||||
|
return $this->isSupported($this->communitySupport);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSupportedByElts(): bool
|
||||||
|
{
|
||||||
|
return $this->isSupported($this->eltsSupport);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isSupported(?\DateTimeInterface $supportedUntil): bool
|
||||||
|
{
|
||||||
|
return $supportedUntil !== null
|
||||||
|
&& (
|
||||||
|
$supportedUntil
|
||||||
|
>= new \DateTimeImmutable('now', new \DateTimeZone('UTC'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\CoreVersion;
|
||||||
|
|
||||||
|
class MajorRelease
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected readonly string $version,
|
||||||
|
protected readonly ?string $lts,
|
||||||
|
protected readonly string $title,
|
||||||
|
protected readonly MaintenanceWindow $maintenanceWindow
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromApiResponse(array $response): self
|
||||||
|
{
|
||||||
|
$maintenanceWindow = MaintenanceWindow::fromApiResponse($response);
|
||||||
|
$ltsVersion = isset($response['lts']) ? (string)$response['lts'] : null;
|
||||||
|
|
||||||
|
return new self((string)($response['version'] ?? ''), $ltsVersion, (string)($response['title'] ?? ''), $maintenanceWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): string
|
||||||
|
{
|
||||||
|
return $this->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLts(): ?string
|
||||||
|
{
|
||||||
|
return $this->lts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMaintenanceWindow(): MaintenanceWindow
|
||||||
|
{
|
||||||
|
return $this->maintenanceWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Database;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Doctrine\DBAL\DriverManager;
|
||||||
|
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Install\Configuration\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check all required permissions within the install process.
|
||||||
|
* @internal This is NOT an API class, it is for internal use in the install tool only.
|
||||||
|
*/
|
||||||
|
class PermissionsCheck
|
||||||
|
{
|
||||||
|
private $testTableName = 't3install_test_table';
|
||||||
|
private $messages = [];
|
||||||
|
|
||||||
|
public function checkCreateAndDrop(): self
|
||||||
|
{
|
||||||
|
$tableCreated = $this->checkCreateTable($this->testTableName);
|
||||||
|
if (!$tableCreated) {
|
||||||
|
$this->messages[] = 'The database user needs CREATE permissions.';
|
||||||
|
}
|
||||||
|
$tableDropped = $this->checkDropTable($this->testTableName);
|
||||||
|
if (!$tableDropped) {
|
||||||
|
$this->messages[] = 'The database user needs DROP permissions.';
|
||||||
|
}
|
||||||
|
if ($tableCreated && !$tableDropped) {
|
||||||
|
$this->messages[] = sprintf('Attention: A test table with name "%s" was created but could not be deleted, please remove the table manually!', $this->testTableName);
|
||||||
|
}
|
||||||
|
if (!$tableCreated || !$tableDropped) {
|
||||||
|
throw new Exception('A test table could not be created or dropped, skipping all further checks now', 1590850369);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkAlter(): self
|
||||||
|
{
|
||||||
|
$this->checkCreateTable($this->testTableName);
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
$schemaManager = $this->createSchemaManager();
|
||||||
|
$schemaCurrent = $schemaManager->introspectSchema();
|
||||||
|
$schemaNew = $schemaManager->introspectSchema();
|
||||||
|
$schemaDiff = $schemaManager->createComparator()->compareSchemas($schemaCurrent, $schemaNew);
|
||||||
|
$schemaNew
|
||||||
|
->getTable($this->testTableName)
|
||||||
|
->addColumn('index_test', 'integer', ['unsigned' => true]);
|
||||||
|
$platform = $connection->getDatabasePlatform();
|
||||||
|
try {
|
||||||
|
foreach ($platform->getAlterSchemaSQL($schemaDiff) as $query) {
|
||||||
|
$connection->executeQuery($query);
|
||||||
|
}
|
||||||
|
} catch (\Exception) {
|
||||||
|
$this->messages[] = 'The database user needs ALTER permission';
|
||||||
|
}
|
||||||
|
$this->checkDropTable($this->testTableName);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkIndex(): self
|
||||||
|
{
|
||||||
|
if ($this->checkCreateTable($this->testTableName)) {
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
$schemaManager = $this->createSchemaManager();
|
||||||
|
$schemaCurrent = $schemaManager->introspectSchema();
|
||||||
|
$schemaNew = $schemaManager->introspectSchema();
|
||||||
|
$testTable = $schemaNew->getTable($this->testTableName);
|
||||||
|
$testTable->addColumn('index_test', 'integer', ['unsigned' => true]);
|
||||||
|
$testTable->addIndex(['index_test'], 'test_index');
|
||||||
|
$schemaDiff = $schemaManager->createComparator()->compareSchemas($schemaCurrent, $schemaNew);
|
||||||
|
$platform = $connection->getDatabasePlatform();
|
||||||
|
try {
|
||||||
|
$statements = $platform->getAlterSchemaSQL($schemaDiff);
|
||||||
|
foreach ($statements as $query) {
|
||||||
|
$connection->executeQuery($query);
|
||||||
|
}
|
||||||
|
} catch (\Exception) {
|
||||||
|
$this->messages[] = 'The database user needs INDEX permission';
|
||||||
|
}
|
||||||
|
$this->checkDropTable($this->testTableName);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkCreateTemporaryTable(): self
|
||||||
|
{
|
||||||
|
$this->checkCreateTable($this->testTableName);
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
try {
|
||||||
|
$sql = 'CREATE TEMPORARY TABLE %s AS (SELECT id FROM %s )';
|
||||||
|
$connection->executeStatement(sprintf($sql, $this->testTableName . '_tmp', $this->testTableName));
|
||||||
|
} catch (\Exception) {
|
||||||
|
$this->messages[] = 'The database user needs CREATE TEMPORARY TABLE permission';
|
||||||
|
}
|
||||||
|
$this->checkDropTable($this->testTableName);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkSelect(): self
|
||||||
|
{
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
try {
|
||||||
|
$connection->executeQuery($connection->getDatabasePlatform()->getDummySelectSQL());
|
||||||
|
} catch (\Exception) {
|
||||||
|
$this->messages[] = 'The database user needs SELECT permission';
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkInsert(): self
|
||||||
|
{
|
||||||
|
$this->checkCreateTable($this->testTableName);
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
try {
|
||||||
|
$connection->insert($this->testTableName, ['id' => 1]);
|
||||||
|
} catch (\Exception) {
|
||||||
|
$this->messages[] = 'The database user needs INSERT permission';
|
||||||
|
}
|
||||||
|
$this->checkDropTable($this->testTableName);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkUpdate(): self
|
||||||
|
{
|
||||||
|
$this->checkCreateTable($this->testTableName);
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
try {
|
||||||
|
$connection->insert($this->testTableName, ['id' => 1]);
|
||||||
|
$connection->update($this->testTableName, ['id' => 2], ['id' => 1]);
|
||||||
|
} catch (\Exception) {
|
||||||
|
$this->messages[] = 'The database user needs UPDATE permission';
|
||||||
|
}
|
||||||
|
$this->checkDropTable($this->testTableName);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkDelete(): self
|
||||||
|
{
|
||||||
|
$this->checkCreateTable($this->testTableName);
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
try {
|
||||||
|
$connection->insert($this->testTableName, ['id' => 1]);
|
||||||
|
$connection->delete($this->testTableName, ['id' => 1]);
|
||||||
|
} catch (\Exception) {
|
||||||
|
$this->messages[] = 'The database user needs DELETE permission';
|
||||||
|
}
|
||||||
|
$this->checkDropTable($this->testTableName);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessages(): array
|
||||||
|
{
|
||||||
|
return $this->messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkCreateTable(string $tablename): bool
|
||||||
|
{
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
$schema = $connection->createSchemaManager()->introspectSchema();
|
||||||
|
$testTable = $schema->createTable($tablename);
|
||||||
|
$testTable->addColumn('id', 'integer', ['unsigned' => true]);
|
||||||
|
$testTable->setPrimaryKey(['id']);
|
||||||
|
$platform = $connection->getDatabasePlatform();
|
||||||
|
try {
|
||||||
|
foreach ($schema->toSql($platform) as $query) {
|
||||||
|
$connection->executeQuery($query);
|
||||||
|
}
|
||||||
|
} catch (\Exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkDropTable(string $tablename): bool
|
||||||
|
{
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
try {
|
||||||
|
$schemaManager = $connection->createSchemaManager();
|
||||||
|
$schemaCurrent = $schemaManager->introspectSchema();
|
||||||
|
$schemaNew = $schemaManager->introspectSchema();
|
||||||
|
|
||||||
|
$schemaNew->dropTable($tablename);
|
||||||
|
$schemaDiff = $schemaManager->createComparator()->compareSchemas($schemaCurrent, $schemaNew);
|
||||||
|
$platform = $connection->getDatabasePlatform();
|
||||||
|
foreach ($platform->getAlterSchemaSQL($schemaDiff) as $query) {
|
||||||
|
$connection->executeQuery($query);
|
||||||
|
}
|
||||||
|
} catch (\Exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getConnection(): Connection
|
||||||
|
{
|
||||||
|
// Use plain Doctrine connection to avoid early TYPO3 context dependencies
|
||||||
|
return DriverManager::getConnection($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createSchemaManager(): AbstractSchemaManager
|
||||||
|
{
|
||||||
|
return $this->getConnection()->createSchemaManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Exception as CoreException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic Install exception
|
||||||
|
*/
|
||||||
|
class Exception extends CoreException {}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to be implemented by all classes which can search and find code places.
|
||||||
|
*/
|
||||||
|
interface CodeScannerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Each match is an array with detail information
|
||||||
|
*/
|
||||||
|
public function getMatches(): array;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt\Class_;
|
||||||
|
use PhpParser\NodeVisitorAbstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A visitor doing some counting.
|
||||||
|
* It sums the number of ignored lines and lines of effective code.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class CodeStatistics extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var bool True if a class statement has @extensionScannerIgnoreFile
|
||||||
|
*/
|
||||||
|
protected $isCurrentFileIgnored = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Counts @extensionScannerIgnoreLine statements
|
||||||
|
*/
|
||||||
|
protected $numberOfIgnoreLines = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Number of effective code lines - class and method statements, function calls ...
|
||||||
|
*/
|
||||||
|
protected $numberOfEffectiveCodeLines = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Current line number given not is in, runtime helper var
|
||||||
|
*/
|
||||||
|
protected $currentLineNumber = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser during traversal.
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
$startLineOfNode = $node->getAttribute('startLine');
|
||||||
|
if ($startLineOfNode !== $this->currentLineNumber) {
|
||||||
|
$this->currentLineNumber = $startLineOfNode;
|
||||||
|
$this->numberOfEffectiveCodeLines++;
|
||||||
|
|
||||||
|
// Class statements may contain the @extensionScannerIgnoreFile statements
|
||||||
|
if ($node instanceof Class_) {
|
||||||
|
$comments = $node->getAttribute('comments');
|
||||||
|
if (!empty($comments)) {
|
||||||
|
foreach ($comments as $comment) {
|
||||||
|
if (str_contains($comment->getText(), '@extensionScannerIgnoreFile')) {
|
||||||
|
$this->isCurrentFileIgnored = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First node of line may contain the @extensionScannerIgnoreLine comment
|
||||||
|
$comments = $node->getAttribute('comments');
|
||||||
|
if (!empty($comments)) {
|
||||||
|
foreach ($comments as $comment) {
|
||||||
|
if (str_contains($comment->getText(), '@extensionScannerIgnoreLine')) {
|
||||||
|
$this->numberOfIgnoreLines++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if a @extensionScannerIgnoreFile has been found.
|
||||||
|
* Called externally *after* traversing
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isFileIgnored()
|
||||||
|
{
|
||||||
|
return $this->isCurrentFileIgnored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of "effective" code lines: No comments, no empty lines,
|
||||||
|
* but "class" statements, "function" statements, "use xy", etc.
|
||||||
|
* Called externally *after* traversing
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getNumberOfEffectiveCodeLines()
|
||||||
|
{
|
||||||
|
return $this->numberOfEffectiveCodeLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns number of found @extensionScannerIgnoreLine comments
|
||||||
|
* Called externally *after* traversing
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getNumberOfIgnoredLines()
|
||||||
|
{
|
||||||
|
return $this->numberOfIgnoreLines;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php;
|
||||||
|
|
||||||
|
use PhpParser\BuilderFactory;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr;
|
||||||
|
use PhpParser\Node\Expr\ClassConstFetch;
|
||||||
|
use PhpParser\Node\Expr\New_;
|
||||||
|
use PhpParser\Node\Expr\StaticCall;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
use PhpParser\Node\Scalar\String_;
|
||||||
|
use PhpParser\NodeVisitorAbstract;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\AbstractCoreMatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fully qualified class name object from first argument of
|
||||||
|
* GeneralUtility::makeInstance('My\\Package\\Class\\Name') if given as string
|
||||||
|
* and not as My\Package\Class\Name::class language construct.
|
||||||
|
*
|
||||||
|
* This resolver is to be called after generic NameResolver::class, but before
|
||||||
|
* other search and find visitors that implement CodeScannerInterface::class
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class GeneratorClassesResolver extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var BuilderFactory
|
||||||
|
*/
|
||||||
|
protected $builderFactory;
|
||||||
|
|
||||||
|
public function __construct(?BuilderFactory $builderFactory = null)
|
||||||
|
{
|
||||||
|
$this->builderFactory = $builderFactory ?? new BuilderFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Create an fqdn object from first makeInstance argument if it is a String
|
||||||
|
*
|
||||||
|
* @param Node $node Incoming node
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node)
|
||||||
|
{
|
||||||
|
if ($node instanceof StaticCall
|
||||||
|
&& $node->class instanceof FullyQualified
|
||||||
|
&& $node->class->toString() === GeneralUtility::class
|
||||||
|
&& $node->name->name === 'makeInstance'
|
||||||
|
&& isset($node->args[0]->value)
|
||||||
|
&& $node->args[0]->value instanceof Expr
|
||||||
|
) {
|
||||||
|
$argValue = $node->args[0]->value;
|
||||||
|
$argAlternative = $this->substituteClassString($argValue);
|
||||||
|
if ($argAlternative !== null) {
|
||||||
|
$node->args[0]->value = $argAlternative;
|
||||||
|
$argValue = $argAlternative;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nodeAlternative = $this->substituteMakeInstance($node, $argValue);
|
||||||
|
if ($nodeAlternative !== null) {
|
||||||
|
$node->setAttribute(AbstractCoreMatcher::NODE_RESOLVED_AS, $nodeAlternative);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Substitutes class-string values with their corresponding class constant
|
||||||
|
* representation (`'Vendor\\ClassName'` -> `\Vendor\ClassName::class`).
|
||||||
|
*/
|
||||||
|
protected function substituteClassString(Expr $argValue): ?ClassConstFetch
|
||||||
|
{
|
||||||
|
// skip non-strings, and those starting with (invalid) namespace separator
|
||||||
|
if (!$argValue instanceof String_ || $argValue->value[0] === '\\') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$classString = ltrim($argValue->value, '\\');
|
||||||
|
$className = new FullyQualified($classString);
|
||||||
|
$classArg = $this->builderFactory->classConstFetch($className, 'class');
|
||||||
|
$this->duplicateNodeAttributes($argValue, $className, $classArg);
|
||||||
|
return $classArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Substitutes `makeInstance` invocations with proper `new` invocations.
|
||||||
|
* `GeneralUtility(\Vendor\ClassName::class, 'a', 'b')` -> `new \Vendor\ClassName('a', 'b')`
|
||||||
|
*/
|
||||||
|
protected function substituteMakeInstance(StaticCall $node, Expr $argValue): ?New_
|
||||||
|
{
|
||||||
|
if (!$argValue instanceof ClassConstFetch
|
||||||
|
|| !$argValue->class instanceof FullyQualified
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newExpr = $this->builderFactory->new(
|
||||||
|
$argValue->class,
|
||||||
|
array_slice($node->args, 1),
|
||||||
|
);
|
||||||
|
$this->duplicateNodeAttributes($node, $newExpr);
|
||||||
|
return $newExpr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicates node positions in source file, based on the assumption
|
||||||
|
* that only lines are relevant. In case this shall be used for
|
||||||
|
* code-migration, real offset positions would be required.
|
||||||
|
*/
|
||||||
|
protected function duplicateNodeAttributes(Node $source, Node ...$targets): void
|
||||||
|
{
|
||||||
|
foreach ($targets as $target) {
|
||||||
|
$target->setAttributes([
|
||||||
|
'startLine' => $source->getStartLine(),
|
||||||
|
'endLine' => $source->getEndLine(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt\Class_;
|
||||||
|
use PhpParser\NodeVisitorAbstract;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\ExtensionScanner\CodeScannerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single "core matcher" classes extend from this.
|
||||||
|
* It brings a set of protected methods to help single matcher classes doing common stuff.
|
||||||
|
* This abstract extends the nikic/php-parser NodeVisitorAbstract which implements the main
|
||||||
|
* parser interface, and it implements the TYPO3 specific CodeScannerInterface to retrieve matches.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
abstract class AbstractCoreMatcher extends NodeVisitorAbstract implements CodeScannerInterface
|
||||||
|
{
|
||||||
|
public const NODE_RESOLVED_AS = 'nodeResolvedAs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incoming main configuration array.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $matcherDefinitions = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array List of accumulated matches
|
||||||
|
*/
|
||||||
|
protected $matches = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper property containing an array derived from $this->matcherDefinitions
|
||||||
|
* created in __construct() if needed.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $flatMatcherDefinitions = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Helper variable for ignored line detection
|
||||||
|
*/
|
||||||
|
protected $currentCodeLine = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool True if line with $lastIgnoredLineNumber is ignored
|
||||||
|
*/
|
||||||
|
protected $isCurrentLineIgnored = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool True if the entire file is ignored due to a @extensionScannerIgnoreFile class comment
|
||||||
|
*/
|
||||||
|
protected $isFullFileIgnored = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of matches after processing
|
||||||
|
*/
|
||||||
|
public function getMatches(): array
|
||||||
|
{
|
||||||
|
return $this->matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some matcher need specific keys in the array definition to work properly.
|
||||||
|
* This method is called typically in __construct() of a matcher to
|
||||||
|
* verify these are given.
|
||||||
|
* This method is a measure against broken core configuration. It should be
|
||||||
|
* pretty quick and is only called in __construct() once, no kitten should be harmed.
|
||||||
|
*
|
||||||
|
* This method works on $this->matcherDefinitions.
|
||||||
|
*
|
||||||
|
* @param array $requiredArrayKeys List of required keys for single matchers
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
protected function validateMatcherDefinitions(array $requiredArrayKeys = [])
|
||||||
|
{
|
||||||
|
foreach ($this->matcherDefinitions as $key => $matcherDefinition) {
|
||||||
|
$this->validateMatcherDefinitionKeys($key, $matcherDefinition, $requiredArrayKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateMatcherDefinitionKeys(string $key, array $matcherDefinition, array $requiredArrayKeys = []): void
|
||||||
|
{
|
||||||
|
// Each config must point to at least one .rst file
|
||||||
|
if (empty($matcherDefinition['restFiles'])) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'Each configuration must have at least one referenced "restFiles" entry. Offending key: ' . $key,
|
||||||
|
1500496068
|
||||||
|
);
|
||||||
|
}
|
||||||
|
foreach ($matcherDefinition['restFiles'] as $file) {
|
||||||
|
if (empty($file)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'Empty restFiles definition',
|
||||||
|
1500735983
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Config broken if not all required array keys are specified in config
|
||||||
|
$sharedArrays = array_intersect(array_keys($matcherDefinition), $requiredArrayKeys);
|
||||||
|
if (count($sharedArrays) !== count($requiredArrayKeys)) {
|
||||||
|
$missingKeys = array_diff($requiredArrayKeys, array_keys($matcherDefinition));
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'Required matcher definitions missing: ' . implode(', ', $missingKeys) . ' offending key: ' . $key,
|
||||||
|
1500492001
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize helper lookup array $this->flatMatcherDefinitions.
|
||||||
|
* For class\name->foo matcherDefinitions, it creates a helper array
|
||||||
|
* containing only the method name as array keys for "weak" matches.
|
||||||
|
*
|
||||||
|
* If methods with the same name from different classes are defined,
|
||||||
|
* a "candidate" array is created containing details of single possible
|
||||||
|
* matches for further analysis.
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
protected function initializeFlatMatcherDefinitions()
|
||||||
|
{
|
||||||
|
$methodNameArray = [];
|
||||||
|
foreach ($this->matcherDefinitions as $classAndMethod => $details) {
|
||||||
|
$method = GeneralUtility::trimExplode('::', $classAndMethod);
|
||||||
|
if (count($method) !== 2) {
|
||||||
|
$method = GeneralUtility::trimExplode('->', $classAndMethod);
|
||||||
|
}
|
||||||
|
if (count($method) !== 2) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Keys in $this->matcherDefinitions must have a Class\Name->method or Class\Name::method structure',
|
||||||
|
1500557309
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$method = $method[1];
|
||||||
|
if (!array_key_exists($method, $methodNameArray)) {
|
||||||
|
$methodNameArray[$method]['candidates'] = [];
|
||||||
|
}
|
||||||
|
$methodNameArray[$method]['candidates'][] = $details;
|
||||||
|
}
|
||||||
|
$this->flatMatcherDefinitions = $methodNameArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if one argument is given as "...$someArray".
|
||||||
|
* If so, it kinda defeats any "argument count" approach.
|
||||||
|
*
|
||||||
|
* @param array $arguments List of arguments
|
||||||
|
*/
|
||||||
|
protected function isArgumentUnpackingUsed(array $arguments = []): bool
|
||||||
|
{
|
||||||
|
foreach ($arguments as $arg) {
|
||||||
|
if ($arg->unpack === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a comment before a statement is
|
||||||
|
* marked as "@extensionScannerIgnoreLine"
|
||||||
|
*/
|
||||||
|
protected function isLineIgnored(Node $node): bool
|
||||||
|
{
|
||||||
|
// Early return if this line is marked as ignored
|
||||||
|
$startLineOfNode = $node->getAttribute('startLine');
|
||||||
|
if ($startLineOfNode === $this->currentCodeLine) {
|
||||||
|
return $this->isCurrentLineIgnored;
|
||||||
|
}
|
||||||
|
if ($this->isCurrentLineIgnored) {
|
||||||
|
// "ignoreMode" is still active, but we're past the line
|
||||||
|
// where it was enabled. Reset this beauty.
|
||||||
|
$this->isCurrentLineIgnored = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentLineIsIgnored = false;
|
||||||
|
if ($startLineOfNode !== $this->currentCodeLine) {
|
||||||
|
$this->currentCodeLine = $startLineOfNode;
|
||||||
|
// First node of a new line may contain the annotation
|
||||||
|
$comments = $node->getAttribute('comments');
|
||||||
|
if (!empty($comments)) {
|
||||||
|
foreach ($comments as $comment) {
|
||||||
|
if (str_contains($comment->getText(), '@extensionScannerIgnoreLine')) {
|
||||||
|
$this->isCurrentLineIgnored = true;
|
||||||
|
$currentLineIsIgnored = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $currentLineIsIgnored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the node is ignored since the entire file is ignored.
|
||||||
|
* Sets ignore status if a class node is given having the annotation.
|
||||||
|
*/
|
||||||
|
protected function isFileIgnored(Node $node): bool
|
||||||
|
{
|
||||||
|
if ($this->isFullFileIgnored) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$currentFileIsIgnored = false;
|
||||||
|
if ($node instanceof Class_) {
|
||||||
|
$comments = $node->getAttribute('comments');
|
||||||
|
if (!empty($comments)) {
|
||||||
|
foreach ($comments as $comment) {
|
||||||
|
if (str_contains($comment->getText(), '@extensionScannerIgnoreFile')) {
|
||||||
|
$this->isFullFileIgnored = true;
|
||||||
|
$currentFileIsIgnored = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $currentFileIsIgnored;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of defined methods within a class that are deprecated/removed.
|
||||||
|
* Requires to extend a TYPO3 API class/abstract.
|
||||||
|
* This is a strong match.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class AbstractMethodImplementationMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
protected const DEFINITION_STATIC = 'static';
|
||||||
|
protected const DEFINITION_LOCAL = 'local';
|
||||||
|
protected array $matcherDefinitionLookup = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
|
||||||
|
// initializeFlatMatcherDefinitions() unfortunately does not deliver the actual
|
||||||
|
// method, so we need to do something 99% similar here for a custom
|
||||||
|
// property, to not require larger changes to the underlying abstract method.
|
||||||
|
foreach ($this->matcherDefinitions as $classAndMethod => $details) {
|
||||||
|
$parts = GeneralUtility::trimExplode('::', $classAndMethod);
|
||||||
|
$definition = self::DEFINITION_STATIC;
|
||||||
|
if (count($parts) !== 2) {
|
||||||
|
$parts = GeneralUtility::trimExplode('->', $classAndMethod);
|
||||||
|
$definition = self::DEFINITION_LOCAL;
|
||||||
|
}
|
||||||
|
// Exception-Handling removed, covered by initializeFlatMatcherDefinitions();
|
||||||
|
|
||||||
|
$method = $parts[1];
|
||||||
|
$class = $parts[0];
|
||||||
|
if (!array_key_exists($class, $this->matcherDefinitionLookup)) {
|
||||||
|
$this->matcherDefinitionLookup[$class][$definition][$method]['candidates'] = [];
|
||||||
|
}
|
||||||
|
$this->matcherDefinitionLookup[$class][$definition][$method]['candidates'][] = $details;
|
||||||
|
|
||||||
|
// Builds something like:
|
||||||
|
// [
|
||||||
|
// 'TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper' => [
|
||||||
|
// 'static' => [
|
||||||
|
// 'renderStatic' => [
|
||||||
|
// 'candidates' => [
|
||||||
|
// [
|
||||||
|
// 'restFiles' => [
|
||||||
|
// 'Deprecation-104789-RenderStaticForFluidViewHelpers.rst',
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ]
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// 'TYPO3\CMS\AbstractSomething' => [
|
||||||
|
// 'local' => [
|
||||||
|
// 'someMethodName' => [
|
||||||
|
// 'candidates' => [
|
||||||
|
// [
|
||||||
|
// 'restFiles' => [
|
||||||
|
// 'Breaking-12345-something.rst',
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// 'restFiles' => [
|
||||||
|
// 'Breaking-67890-something.rst',
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ]
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for a defined method that shall longer be utilized (strong match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof Node\Stmt\Class_
|
||||||
|
&& $node->extends) {
|
||||||
|
|
||||||
|
// We found a class definition.
|
||||||
|
// Check what classes this definition is extending (Abstract).
|
||||||
|
// Without a class extending something, this is not API usage and thus not scanned.
|
||||||
|
// Now check if the extended class is part of our matcherDefinition to inspect
|
||||||
|
if (array_key_exists($node->extends->name, $this->matcherDefinitionLookup)) {
|
||||||
|
|
||||||
|
// Iterate all declared methods (of the inspected custom class, NOT the abstract!)
|
||||||
|
$lookupMethods = $this->matcherDefinitionLookup[$node->extends->name];
|
||||||
|
foreach ($node->getMethods() as $method) {
|
||||||
|
|
||||||
|
// The matcherDefinition can utilize 'Abstract::staticMethod' or 'Abstract->localMethod',
|
||||||
|
// which is handled distinctly, so that the matches are stronger.
|
||||||
|
$lookupKey = $method->isStatic() ? self::DEFINITION_STATIC : self::DEFINITION_LOCAL;
|
||||||
|
|
||||||
|
if (isset($lookupMethods[$lookupKey][$method->name->toString()]['candidates'])) {
|
||||||
|
// The checked method of an object extending a deprecated/BC class was a match.
|
||||||
|
// Gather final match info (multiple ReST files can apply to a single class+method)
|
||||||
|
foreach ($lookupMethods[$lookupKey][$method->name->toString()]['candidates'] as $candidate) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $candidate['restFiles'],
|
||||||
|
'line' => $method->getAttribute('startLine'),
|
||||||
|
'message' => sprintf(
|
||||||
|
'Definition of %s method "%s" extends from "%s"',
|
||||||
|
$lookupKey,
|
||||||
|
$method->name->toString(),
|
||||||
|
$node->extends->name
|
||||||
|
),
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of dropped configuration values and hook registrations.
|
||||||
|
* Matches on "last" key only.
|
||||||
|
* Definition of $GLOBALS['foo']['bar'] and usage as $foo['bar'] matches.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class ArrayDimensionMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Initialize "flat" matcher array from matcher definitions.
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
$this->initializeLastArrayKeyNameArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof ArrayDimFetch
|
||||||
|
&& isset($node->dim->value)
|
||||||
|
&& array_key_exists($node->dim->value, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Access to array key "' . $node->dim->value . '"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->dim->value]['candidates'] as $candidate) {
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare 'lastKey' => [$details] array in flatMatcherDefinitions
|
||||||
|
*/
|
||||||
|
protected function initializeLastArrayKeyNameArray()
|
||||||
|
{
|
||||||
|
$methodNameArray = [];
|
||||||
|
foreach ($this->matcherDefinitions as $fullArrayString => $details) {
|
||||||
|
// Goal: find last part "foobar" of an array path "$foo['bar']['foobar']"
|
||||||
|
// Reverse string $foo['bar']['foobar']
|
||||||
|
$lastKey = strrev($fullArrayString);
|
||||||
|
// Cut off "['"
|
||||||
|
$lastKey = substr($lastKey, 2);
|
||||||
|
$lastKey = GeneralUtility::trimExplode('\'[', $lastKey);
|
||||||
|
// Last key name
|
||||||
|
$lastKey = $lastKey[0];
|
||||||
|
// And reverse key name again
|
||||||
|
$lastKey = strrev($lastKey);
|
||||||
|
|
||||||
|
if (!array_key_exists($lastKey, $methodNameArray)) {
|
||||||
|
$methodNameArray[$lastKey]['candidates'] = [];
|
||||||
|
}
|
||||||
|
$methodNameArray[$lastKey]['candidates'][] = $details;
|
||||||
|
}
|
||||||
|
$this->flatMatcherDefinitions = $methodNameArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||||
|
use PhpParser\Node\Expr\Variable;
|
||||||
|
use PhpParser\Node\Scalar\String_;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match access to a one dimensional $GLOBAL array
|
||||||
|
* Example "$GLOBALS['TYPO3_DB']"
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class ArrayGlobalMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Initialize "flat" matcher array from matcher definitions.
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof ArrayDimFetch
|
||||||
|
&& $node->var instanceof Variable
|
||||||
|
&& $node->var->name === 'GLOBALS'
|
||||||
|
&& $node->dim instanceof String_
|
||||||
|
&& array_key_exists('$GLOBALS[\'' . $node->dim->value . '\']', $this->matcherDefinitions)
|
||||||
|
) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions['$GLOBALS[\'' . $node->dim->value . '\']']['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Access to array global array "' . $node->dim->value . '"',
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\ClassConstFetch;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of class constants.
|
||||||
|
*
|
||||||
|
* Test for "Class\Name::THE_CONSTANT", matches are considered "strong"
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class ClassConstantMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof ClassConstFetch
|
||||||
|
&& $node->class instanceof FullyQualified
|
||||||
|
&& array_key_exists($node->class->toString() . '::' . $node->name, $this->matcherDefinitions)
|
||||||
|
) {
|
||||||
|
// No weak test implemented - combination class::const name tested
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$node->class->toString() . '::' . $node->name]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Call to class constant "' . $node->class->toString() . '::' . $node->name . '"',
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of class / interface names which are entirely deprecated or removed
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class ClassNameMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Default constructor validates matcher definition.
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*
|
||||||
|
* @param Node $node Given node to test
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof FullyQualified
|
||||||
|
) {
|
||||||
|
$fullyQualifiedClassName = $node->toString();
|
||||||
|
if (array_key_exists($fullyQualifiedClassName, $this->matcherDefinitions)) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$fullyQualifiedClassName]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Usage of class "' . $fullyQualifiedClassName . '"',
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\ConstFetch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of class constants.
|
||||||
|
*
|
||||||
|
* Test for "THE_CONSTANT", matches are considered "strong"
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class ConstantMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof ConstFetch
|
||||||
|
&& array_key_exists($node->name->toString(), $this->matcherDefinitions)
|
||||||
|
) {
|
||||||
|
// Access to constants is detected as strong match
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$node->name->toString()]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Call to global constant "' . $node->name->toString() . '"',
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\ConstFetch;
|
||||||
|
use PhpParser\Node\Expr\New_;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds invocations to class constructors and the amount of passed arguments.
|
||||||
|
* This matcher supports direct `new MyClass(123)` invocations as well as delegated
|
||||||
|
* calls to `GeneralUtility::makeInstance(MyClass::class, 123)` using `GeneratorClassResolver`.
|
||||||
|
*
|
||||||
|
* These configuration property names are handled independently:
|
||||||
|
* + numberOfMandatoryArguments
|
||||||
|
* + maximumNumberOfArguments
|
||||||
|
* + unusedArgumentNumbers
|
||||||
|
*
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class ConstructorArgumentMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
protected const TOPIC_TYPE_REQUIRED = 'required';
|
||||||
|
protected const TOPIC_TYPE_DROPPED = 'dropped';
|
||||||
|
protected const TOPIC_TYPE_CALLED = 'called';
|
||||||
|
protected const TOPIC_TYPE_UNUSED = 'unused';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once and validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitionsTopicRequirements([
|
||||||
|
self::TOPIC_TYPE_REQUIRED => ['numberOfMandatoryArguments'],
|
||||||
|
self::TOPIC_TYPE_DROPPED => ['maximumNumberOfArguments'],
|
||||||
|
self::TOPIC_TYPE_CALLED => ['numberOfMandatoryArguments', 'maximumNumberOfArguments'],
|
||||||
|
self::TOPIC_TYPE_UNUSED => ['unusedArgumentNumbers'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "->deprecated()" (weak match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if ($this->isFileIgnored($node) || $this->isLineIgnored($node)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$resolvedNode = $node->getAttribute(self::NODE_RESOLVED_AS, null) ?? $node;
|
||||||
|
if (!$resolvedNode instanceof New_
|
||||||
|
|| !isset($resolvedNode->class)
|
||||||
|
|| (isset($node->class) && is_object($node->class) && !method_exists($node->class, '__toString'))
|
||||||
|
|| !array_key_exists((string)$resolvedNode->class, $this->matcherDefinitions)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A method call is considered a match if it is not called with argument unpacking
|
||||||
|
// and number of used arguments is lower than numberOfMandatoryArguments
|
||||||
|
if ($this->isArgumentUnpackingUsed($resolvedNode->args)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
|
||||||
|
// $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
|
||||||
|
$this->handleRequiredArguments($node, $resolvedNode);
|
||||||
|
$this->handleDroppedArguments($node, $resolvedNode);
|
||||||
|
$this->handleCalledArguments($node, $resolvedNode);
|
||||||
|
$this->handleUnusedArguments($node, $resolvedNode);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
|
||||||
|
* @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
|
||||||
|
*/
|
||||||
|
protected function handleRequiredArguments(Node $node, Node $resolvedNode): bool
|
||||||
|
{
|
||||||
|
$className = (string)($resolvedNode->class ?? '');
|
||||||
|
$candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_REQUIRED] ?? null;
|
||||||
|
$mandatoryArguments = $candidate['numberOfMandatoryArguments'] ?? null;
|
||||||
|
$numberOfArguments = count($resolvedNode->args ?? []);
|
||||||
|
|
||||||
|
if ($candidate === null || $numberOfArguments >= $mandatoryArguments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $candidate['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => sprintf(
|
||||||
|
'%s::__construct requires at least %d arguments (%d given).',
|
||||||
|
$className,
|
||||||
|
$mandatoryArguments,
|
||||||
|
$numberOfArguments
|
||||||
|
),
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
|
||||||
|
* @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
|
||||||
|
*/
|
||||||
|
protected function handleDroppedArguments(Node $node, Node $resolvedNode): bool
|
||||||
|
{
|
||||||
|
$className = (string)($resolvedNode->class ?? '');
|
||||||
|
$candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_DROPPED] ?? null;
|
||||||
|
$maximumArguments = $candidate['maximumNumberOfArguments'] ?? null;
|
||||||
|
$numberOfArguments = count($resolvedNode->args ?? []);
|
||||||
|
|
||||||
|
if ($candidate === null || $numberOfArguments <= $maximumArguments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $candidate['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => sprintf(
|
||||||
|
'%s::__construct supports only %d arguments (%d given).',
|
||||||
|
$className,
|
||||||
|
$maximumArguments,
|
||||||
|
$numberOfArguments
|
||||||
|
),
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
|
||||||
|
* @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
|
||||||
|
*/
|
||||||
|
protected function handleCalledArguments(Node $node, Node $resolvedNode): bool
|
||||||
|
{
|
||||||
|
$className = (string)($resolvedNode->class ?? '');
|
||||||
|
$candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_CALLED] ?? null;
|
||||||
|
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($resolvedNode->args ?? []);
|
||||||
|
$mandatoryArguments = $candidate['numberOfMandatoryArguments'] ?? null;
|
||||||
|
$maximumArguments = $candidate['maximumNumberOfArguments'] ?? null;
|
||||||
|
$numberOfArguments = count($resolvedNode->args ?? []);
|
||||||
|
|
||||||
|
if ($candidate === null
|
||||||
|
|| !$isArgumentUnpackingUsed
|
||||||
|
&& ($numberOfArguments < $mandatoryArguments || $numberOfArguments > $maximumArguments)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $candidate['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => sprintf(
|
||||||
|
'%s::__construct being called (%d arguments given).',
|
||||||
|
$className,
|
||||||
|
$numberOfArguments
|
||||||
|
),
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
|
||||||
|
* @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
|
||||||
|
*/
|
||||||
|
protected function handleUnusedArguments(Node $node, Node $resolvedNode): bool
|
||||||
|
{
|
||||||
|
$className = (string)($resolvedNode->class ?? '');
|
||||||
|
$candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_UNUSED] ?? null;
|
||||||
|
// values in array (if any) are actual position counts
|
||||||
|
// e.g. `[2, 4]` refers to internal argument indexes `[1, 3]`
|
||||||
|
$unusedArgumentPositions = $candidate['unusedArgumentNumbers'] ?? null;
|
||||||
|
|
||||||
|
if ($candidate === null || empty($unusedArgumentPositions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments = $resolvedNode->args ?? [];
|
||||||
|
// keeping positions having argument values that are not null
|
||||||
|
$unusedArgumentPositions = array_filter(
|
||||||
|
$unusedArgumentPositions,
|
||||||
|
static function (int $position) use ($arguments) {
|
||||||
|
$index = $position - 1;
|
||||||
|
return isset($arguments[$index]->value)
|
||||||
|
&& !$arguments[$index]->value instanceof ConstFetch
|
||||||
|
&& (
|
||||||
|
!isset($arguments[$index]->value->name->name->parts[0])
|
||||||
|
|| $arguments[$index]->value->name->name->parts[0] !== null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (empty($unusedArgumentPositions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $candidate['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => sprintf(
|
||||||
|
'%s::__construct was called with argument positions %s not being null.',
|
||||||
|
$className,
|
||||||
|
implode(', ', $unusedArgumentPositions)
|
||||||
|
),
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateMatcherDefinitionsTopicRequirements(array $topicRequirements): void
|
||||||
|
{
|
||||||
|
foreach ($this->matcherDefinitions as $key => $matcherDefinition) {
|
||||||
|
foreach ($topicRequirements as $topic => $requiredArrayKeys) {
|
||||||
|
if (empty($matcherDefinition[$topic])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->validateMatcherDefinitionKeys($key, $matcherDefinition[$topic], $requiredArrayKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\FuncCall;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of global function calls which were removed / deprecated.
|
||||||
|
* This is a strong match.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class FunctionCallMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions(['numberOfMandatoryArguments', 'maximumNumberOfArguments']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "removedFunction()" (strong match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Match method call (not static)
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof FuncCall
|
||||||
|
&& $node->name instanceof FullyQualified
|
||||||
|
&& array_key_exists($node->name->toString(), $this->matcherDefinitions)
|
||||||
|
) {
|
||||||
|
$functionName = $node->name->toString();
|
||||||
|
$matchDefinition = $this->matcherDefinitions[$functionName];
|
||||||
|
|
||||||
|
$numberOfArguments = count($node->args);
|
||||||
|
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
|
||||||
|
|
||||||
|
if ($isArgumentUnpackingUsed
|
||||||
|
|| ($numberOfArguments >= $matchDefinition['numberOfMandatoryArguments']
|
||||||
|
&& $numberOfArguments <= $matchDefinition['maximumNumberOfArguments'])
|
||||||
|
) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $matchDefinition['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Call to function "' . $functionName . '"',
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Modifiers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\MethodCall;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Stmt\ClassMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches interface method arguments which have been dropped.
|
||||||
|
*
|
||||||
|
* This does *not* test if a class implements an interface.
|
||||||
|
* The scanner only looks for:
|
||||||
|
* - Class method names not having specified number of arguments
|
||||||
|
* - Method calls with given method name not having this number of arguments
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class InterfaceMethodChangedMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Default constructor validates config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
// newNumberOfArguments must exist in all matcherDefinitions
|
||||||
|
$this->validateMatcherDefinitions(['newNumberOfArguments']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "public function like($arg1, $arg2, $arg3) {}" (weak match)
|
||||||
|
* Test for "->like($arg1, $arg2, $arg3); (weak match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if ($this->isFileIgnored($node) || $this->isLineIgnored($node)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match method name of a class, must be public, wouldn't make sense as interface if protected/private
|
||||||
|
if ($node instanceof ClassMethod
|
||||||
|
&& array_key_exists($node->name->name, $this->matcherDefinitions)
|
||||||
|
&& $node->flags & Modifiers::PUBLIC // public
|
||||||
|
&& ($node->flags & Modifiers::STATIC) !== Modifiers::STATIC // not static
|
||||||
|
) {
|
||||||
|
$methodName = $node->name->name;
|
||||||
|
$numberOfUsedArguments = 0;
|
||||||
|
if (is_array($node->params ?? null)) {
|
||||||
|
$numberOfUsedArguments = count($node->params);
|
||||||
|
}
|
||||||
|
$numberOfAllowedArguments = $this->matcherDefinitions[$methodName]['newNumberOfArguments'];
|
||||||
|
if ($numberOfUsedArguments > $numberOfAllowedArguments) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$methodName]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Implementation of dropped interface argument for method "' . $methodName . '()"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match method call (not static) with number of arguments
|
||||||
|
if ($node instanceof MethodCall
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->matcherDefinitions)
|
||||||
|
) {
|
||||||
|
$methodName = $node->name->name;
|
||||||
|
$numberOfUsedArguments = 0;
|
||||||
|
if (is_array($node->args ?? null)) {
|
||||||
|
$numberOfUsedArguments = count($node->args);
|
||||||
|
}
|
||||||
|
// @todo: Test for argument unpacking
|
||||||
|
$numberOfAllowedArguments = $this->matcherDefinitions[$methodName]['newNumberOfArguments'];
|
||||||
|
if ($numberOfUsedArguments > $numberOfAllowedArguments) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$methodName]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Call to interface method "' . $methodName . '()"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Comment\Doc;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt\ClassMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of method annotations
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MethodAnnotationMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once and validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for method annotations (strong match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if ($node instanceof ClassMethod
|
||||||
|
&& ($docComment = $node->getDocComment()) instanceof Doc
|
||||||
|
&& !$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
) {
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
|
||||||
|
$matches = [];
|
||||||
|
preg_match_all(
|
||||||
|
'/\s*\s@(?<annotations>[^\s.]*).*\n/',
|
||||||
|
$docComment->getText(),
|
||||||
|
$matches
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($matches['annotations'] as $annotation) {
|
||||||
|
$annotation = '@' . $annotation;
|
||||||
|
|
||||||
|
if (!isset($this->matcherDefinitions[$annotation])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['message'] = 'Method "' . $node->name . '" uses an ' . $annotation . ' annotation.';
|
||||||
|
$match['restFiles'] = array_unique(array_merge(
|
||||||
|
$match['restFiles'],
|
||||||
|
$this->matcherDefinitions[$annotation]['restFiles']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\MethodCall;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of method calls which changed signature and dropped arguments,
|
||||||
|
* but are called with more arguments.
|
||||||
|
* This is a "weak" match since we're just testing for method name
|
||||||
|
* but not connected class.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MethodArgumentDroppedMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once and validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions(['maximumNumberOfArguments']);
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "->deprecated()" (weak match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Match method call (not static)
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof MethodCall
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
|
||||||
|
|
||||||
|
$numberOfArguments = count($node->args);
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
|
||||||
|
// A method call is considered a match if it is not called with argument unpacking
|
||||||
|
// and number of used arguments is higher than maximumNumberOfArguments
|
||||||
|
if (!$isArgumentUnpackingUsed
|
||||||
|
&& $numberOfArguments > $candidate['maximumNumberOfArguments']
|
||||||
|
) {
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['message'] = 'Method "' . $node->name->name . '()" supports only ' . $candidate['maximumNumberOfArguments'] . ' arguments.';
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\StaticCall;
|
||||||
|
use PhpParser\Node\Expr\Variable;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of static method calls which were removed / deprecated.
|
||||||
|
* This is a "strong" match if class name is given and "weak" if not.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MethodArgumentDroppedStaticMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once and validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions(['maximumNumberOfArguments']);
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "->deprecated()" (weak match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Match static method call
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof StaticCall
|
||||||
|
) {
|
||||||
|
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
|
||||||
|
|
||||||
|
if ($node->class instanceof FullyQualified && $node->name instanceof Identifier) {
|
||||||
|
// 'Foo\Bar::aMethod()' -> strong match
|
||||||
|
$fqdnClassWithMethod = $node->class->toString() . '::' . $node->name->name;
|
||||||
|
if (!$isArgumentUnpackingUsed
|
||||||
|
&& array_key_exists($fqdnClassWithMethod, $this->matcherDefinitions)
|
||||||
|
&& count($node->args) > $this->matcherDefinitions[$fqdnClassWithMethod]['maximumNumberOfArguments']
|
||||||
|
) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$fqdnClassWithMethod]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Method "' . $node->name->name . '()" supports only '
|
||||||
|
. $this->matcherDefinitions[$fqdnClassWithMethod]['maximumNumberOfArguments']
|
||||||
|
. ' arguments.',
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} elseif ($node->class instanceof Variable
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
$numberOfArguments = count($node->args);
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
|
||||||
|
// A method call is considered a match if it is not called with argument unpacking
|
||||||
|
// and number of used arguments is higher than maximumNumberOfArguments
|
||||||
|
if (!$isArgumentUnpackingUsed
|
||||||
|
&& $numberOfArguments > $candidate['maximumNumberOfArguments']
|
||||||
|
) {
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['message'] = 'Method "' . $node->name->name . '()" supports only '
|
||||||
|
. $candidate['maximumNumberOfArguments'] . ' arguments.';
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\MethodCall;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of method calls which changed signature and added required arguments.
|
||||||
|
* This is a "weak" match since we're just testing for method name
|
||||||
|
* but not connected class.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MethodArgumentRequiredMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once and validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions(['numberOfMandatoryArguments']);
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "->deprecated()" (weak match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Match method call (not static)
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof MethodCall
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
|
||||||
|
|
||||||
|
$numberOfArguments = count($node->args);
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
|
||||||
|
// A method call is considered a match if it is not called with argument unpacking
|
||||||
|
// and number of used arguments is lower than numberOfMandatoryArguments
|
||||||
|
if (!$isArgumentUnpackingUsed
|
||||||
|
&& $numberOfArguments < $candidate['numberOfMandatoryArguments']
|
||||||
|
&& $numberOfArguments <= $candidate['maximumNumberOfArguments']
|
||||||
|
) {
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['message'] = 'Method ' . $node->name->name . '() needs at least ' . $candidate['numberOfMandatoryArguments'] . ' arguments.';
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\StaticCall;
|
||||||
|
use PhpParser\Node\Expr\Variable;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of static method calls which gained new mandatory arguments.
|
||||||
|
* This is a "strong" match if class name is given and "weak" if not.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MethodArgumentRequiredStaticMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once and validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions(['numberOfMandatoryArguments', 'maximumNumberOfArguments']);
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "::function($1, $2, $3)" (strong match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Match static method call
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof StaticCall
|
||||||
|
) {
|
||||||
|
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
|
||||||
|
|
||||||
|
if ($node->class instanceof FullyQualified && $node->name instanceof Identifier) {
|
||||||
|
// 'Foo\Bar::aMethod()' -> strong match
|
||||||
|
$fqdnClassWithMethod = $node->class->toString() . '::' . $node->name->name;
|
||||||
|
$numberOfArguments = count($node->args);
|
||||||
|
if (!$isArgumentUnpackingUsed
|
||||||
|
&& array_key_exists($fqdnClassWithMethod, $this->matcherDefinitions)
|
||||||
|
&& $numberOfArguments < $this->matcherDefinitions[$fqdnClassWithMethod]['numberOfMandatoryArguments']
|
||||||
|
// maximum number of arguments is just a measure against false positives
|
||||||
|
&& $numberOfArguments <= $this->matcherDefinitions[$fqdnClassWithMethod]['maximumNumberOfArguments']
|
||||||
|
) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$fqdnClassWithMethod]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Method "' . $node->name->name . '()" needs at least '
|
||||||
|
. $this->matcherDefinitions[$fqdnClassWithMethod]['numberOfMandatoryArguments']
|
||||||
|
. ' arguments.',
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} elseif ($node->class instanceof Variable
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
$numberOfArguments = count($node->args);
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
|
||||||
|
// A method call is considered a match if it is not called with argument unpacking
|
||||||
|
// and number of used arguments is lesser than numberOfMandatoryArguments
|
||||||
|
if (!$isArgumentUnpackingUsed
|
||||||
|
&& $numberOfArguments < $candidate['numberOfMandatoryArguments']
|
||||||
|
// maximum number of arguments is just a measure against false positives
|
||||||
|
&& $numberOfArguments <= $candidate['maximumNumberOfArguments']
|
||||||
|
) {
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['message'] = 'Method "' . $node->name->name . '()" needs at least '
|
||||||
|
. $candidate['numberOfMandatoryArguments'] . ' arguments.';
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\ConstFetch;
|
||||||
|
use PhpParser\Node\Expr\MethodCall;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match method usages where arguments "in between" are unused but not given as "null":
|
||||||
|
*
|
||||||
|
* public function foo($arg1, $unused1 = null, $unused2 = null, $arg4)
|
||||||
|
* but called with:
|
||||||
|
* ->foo('arg1', 'notNull', null, 'arg4');
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MethodArgumentUnusedMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once and validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions(['unusedArgumentNumbers']);
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Match method call (not static)
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof MethodCall
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
|
||||||
|
|
||||||
|
$numberOfArguments = count($node->args);
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
|
||||||
|
foreach ($candidate['unusedArgumentNumbers'] as $droppedArgumentNumber) {
|
||||||
|
// A method call is considered a match if name matches, unpacking is not used
|
||||||
|
// and the registered argument is not given as null.
|
||||||
|
if (!$isArgumentUnpackingUsed
|
||||||
|
&& $numberOfArguments >= $droppedArgumentNumber
|
||||||
|
&& !($node->args[$droppedArgumentNumber - 1]->value instanceof ConstFetch)
|
||||||
|
&& (!isset($node->args[$droppedArgumentNumber - 1]->value->name->name->parts[0])
|
||||||
|
|| $node->args[$droppedArgumentNumber - 1]->value->name->name->parts[0] !== null)
|
||||||
|
) {
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['message'] = 'Call to method "' . $node->name->name . '()" with'
|
||||||
|
. ' argument ' . $droppedArgumentNumber . ' not given as null.';
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\MethodCall;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
use PhpParser\Node\Scalar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of arguments in method calls which were removed / deprecated.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MethodCallArgumentValueMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions(['argumentMatches']);
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "->method($someArgument)" (weak match)
|
||||||
|
* and for "fqcn::method($someArgument)" (strong match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Match method call (not static)
|
||||||
|
if ($this->isFileIgnored($node)
|
||||||
|
|| $this->isLineIgnored($node)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node instanceof Node\Expr\StaticCall
|
||||||
|
&& $node->class instanceof FullyQualified
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->class->toString() . '::' . $node->name->name, $this->matcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Call to specific argument (#%s) of static method "' . $node->class->toString() . '::' . $node->name->name . '()"',
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
|
||||||
|
$matchCandidate = [$this->matcherDefinitions[$node->class->toString() . '::' . $node->name->name]];
|
||||||
|
} elseif ($node instanceof MethodCall
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Call to specific argument (#%s) of method "' . $node->name->name . '()"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
$matchCandidate = $this->flatMatcherDefinitions[$node->name->name]['candidates'];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
|
||||||
|
// So far, the candidates just have their argument numbering and method name matching applied
|
||||||
|
// Now let's inspect whether the argument actually holds the value our droids are looking for
|
||||||
|
foreach ($matchCandidate as $candidate) {
|
||||||
|
$argumentNumbers = $this->isArgumentMatched($node, $candidate);
|
||||||
|
if ($argumentNumbers !== []) {
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
$match['message'] = sprintf($match['message'], implode(', ', $argumentNumbers));
|
||||||
|
// One match will shortcut checking for others.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the array of matched arguments on a match candidate.
|
||||||
|
* Returns empty array if either none found or not ALL matches are matched (AND combined)
|
||||||
|
*/
|
||||||
|
private function isArgumentMatched(Node $node, array $candidate): array
|
||||||
|
{
|
||||||
|
$matchedArgumentNumbers = [];
|
||||||
|
|
||||||
|
foreach (($candidate['argumentMatches'] ?? []) as $argumentMatchArray) {
|
||||||
|
if (isset($node->args[$argumentMatchArray['argumentIndex']]->value->value)
|
||||||
|
&& $node->args[$argumentMatchArray['argumentIndex']]->value instanceof Scalar
|
||||||
|
&& $node->args[$argumentMatchArray['argumentIndex']]->value->value === $argumentMatchArray['argumentValue']) {
|
||||||
|
|
||||||
|
$matchedArgumentNumbers[] = $argumentMatchArray['argumentIndex'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($matchedArgumentNumbers) !== count($candidate['argumentMatches'] ?? [])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $matchedArgumentNumbers;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\MethodCall;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of method calls which were removed / deprecated.
|
||||||
|
* This is a "weak" match since we're just testing for method name
|
||||||
|
* but not connected class.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MethodCallMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions(['numberOfMandatoryArguments', 'maximumNumberOfArguments']);
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "->deprecated()" (weak match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Match method call (not static)
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof MethodCall
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Call to method "' . $node->name->name . '()"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
$numberOfArguments = count($node->args);
|
||||||
|
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
|
||||||
|
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
|
||||||
|
// A method call is considered a match if it is called with argument unpacking, or
|
||||||
|
// if the number of given arguments is within range of mandatory / max number of arguments
|
||||||
|
if ($isArgumentUnpackingUsed
|
||||||
|
|| ($numberOfArguments >= $candidate['numberOfMandatoryArguments']
|
||||||
|
&& $numberOfArguments <= $candidate['maximumNumberOfArguments'])
|
||||||
|
) {
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\StaticCall;
|
||||||
|
use PhpParser\Node\Expr\Variable;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of static method calls which were removed / deprecated.
|
||||||
|
*
|
||||||
|
* This match is performed either is case of a direct "foo\bar::aMethod()" call
|
||||||
|
* as "strong" match, or as only "::aMethod()" as "weak" match.
|
||||||
|
*
|
||||||
|
* As additional indicator, the number of required, mandatory arguments is
|
||||||
|
* recognized: If calling a static method as "$foo::aMethod($arg1), but the
|
||||||
|
* method needs two arguments, this is *not* considered a match. This would
|
||||||
|
* have raised a fatal PHP error anyway and this is nothing we test here.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MethodCallStaticMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate config and prepare weak matcher array
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions(['numberOfMandatoryArguments', 'maximumNumberOfArguments']);
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for "foo\bar::deprecated()" (strong match)
|
||||||
|
* Test for "::deprecated()" (weak match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Static call, not method call
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof StaticCall
|
||||||
|
) {
|
||||||
|
if ($node->class instanceof FullyQualified && $node->name instanceof Identifier) {
|
||||||
|
// 'Foo\Bar::deprecated()' -> strong match
|
||||||
|
$fqdnClassWithMethod = $node->class->toString() . '::' . $node->name->name;
|
||||||
|
if (array_key_exists($fqdnClassWithMethod, $this->matcherDefinitions)) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$fqdnClassWithMethod]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Use of static class method call "' . $fqdnClassWithMethod . '()"',
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} elseif ($node->class instanceof Variable
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Use of static class method call "' . $node->name->name . '()"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
$numberOfArguments = count($node->args);
|
||||||
|
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
|
||||||
|
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
|
||||||
|
// A method call is considered a match if it is called with argument unpacking, or
|
||||||
|
// if the number of given arguments is within range of mandatory / max number of arguments
|
||||||
|
if ($isArgumentUnpackingUsed
|
||||||
|
|| ($numberOfArguments >= $candidate['numberOfMandatoryArguments']
|
||||||
|
&& $numberOfArguments <= $candidate['maximumNumberOfArguments'])
|
||||||
|
) {
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Comment\Doc;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\PropertyItem;
|
||||||
|
use PhpParser\Node\Stmt\Property;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of property annotations
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class PropertyAnnotationMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once and validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
* Test for property annotations (strong match)
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if ($node instanceof Property
|
||||||
|
&& ($property = reset($node->props)) instanceof PropertyItem
|
||||||
|
&& ($docComment = $node->getDocComment()) instanceof Doc
|
||||||
|
&& !$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
) {
|
||||||
|
/** @var PropertyItem $property */
|
||||||
|
$isPossibleMatch = false;
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $property->getAttribute('startLine'),
|
||||||
|
'indicator' => 'strong',
|
||||||
|
];
|
||||||
|
|
||||||
|
$matches = [];
|
||||||
|
preg_match_all(
|
||||||
|
'/\s*\s@(?<annotations>[^\s.]*).*\n/',
|
||||||
|
$docComment->getText(),
|
||||||
|
$matches
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($matches['annotations'] as $annotation) {
|
||||||
|
$annotation = '@' . $annotation;
|
||||||
|
|
||||||
|
if (!isset($this->matcherDefinitions[$annotation])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isPossibleMatch = true;
|
||||||
|
$match['message'] = 'Property "' . $property->name . '" uses an ' . $annotation . ' annotation.';
|
||||||
|
$match['restFiles'] = array_unique(array_merge(
|
||||||
|
$match['restFiles'],
|
||||||
|
$this->matcherDefinitions[$annotation]['restFiles']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isPossibleMatch) {
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt\Property;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of properties which have been deprecated or removed.
|
||||||
|
* Useful if abstract classes remove properties.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class PropertyExistsStaticMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate config and prepare flat mach array
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof Property
|
||||||
|
&& $node->isStatic()
|
||||||
|
&& !$node->isPrivate()
|
||||||
|
&& array_key_exists($node->props[0]->name->name, $this->matcherDefinitions)
|
||||||
|
) {
|
||||||
|
$propertyName = $node->props[0]->name->name;
|
||||||
|
$match = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$propertyName]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Use of property "' . $node->props[0]->name->name . '"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\PropertyFetch;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of properties which have been made protected and are
|
||||||
|
* not called in $this context.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class PropertyProtectedMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate config and prepare flat mach array
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof PropertyFetch
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& ($node->var->name ?? '') !== 'this'
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Fetch of property "' . $node->name->name . '"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\PropertyFetch;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of properties which were removed / deprecated.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class PropertyPublicMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare $this->flatMatcherDefinitions once and validate config
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
$this->initializeFlatMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Match property access (not static)
|
||||||
|
if (!$this->isFileIgnored($node)
|
||||||
|
&& !$this->isLineIgnored($node)
|
||||||
|
&& $node instanceof PropertyFetch
|
||||||
|
&& $node->name instanceof Identifier
|
||||||
|
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
|
||||||
|
) {
|
||||||
|
$match = [
|
||||||
|
'restFiles' => [],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Fetch of property "' . $node->name->name . '"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
|
||||||
|
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
|
||||||
|
}
|
||||||
|
$this->matches[] = $match;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usage of special "magic" strings like TYPO3_MODE, so that
|
||||||
|
* usage scenarios like `defined('TYPO3_MODE') || die()` will be scanned,
|
||||||
|
* where the actual constant is NOT used.
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class ScalarStringMatcher extends AbstractCoreMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Default constructor validates matcher definition.
|
||||||
|
*
|
||||||
|
* @param array $matcherDefinitions Incoming main configuration
|
||||||
|
*/
|
||||||
|
public function __construct(array $matcherDefinitions)
|
||||||
|
{
|
||||||
|
$this->matcherDefinitions = $matcherDefinitions;
|
||||||
|
$this->validateMatcherDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by PhpParser.
|
||||||
|
*
|
||||||
|
* @param Node $node Given node to test
|
||||||
|
*/
|
||||||
|
public function enterNode(Node $node): null
|
||||||
|
{
|
||||||
|
// Early return
|
||||||
|
if ($this->isFileIgnored($node)
|
||||||
|
|| $this->isLineIgnored($node)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the node contains the specific string
|
||||||
|
if (!$node instanceof Node\Scalar\String_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: This is intentionally meant to be an exact match for now, no trimming or substring.
|
||||||
|
// Could be enhanced in the future with options to the configuration how to match.
|
||||||
|
// Using weak match to indicate that the magic string usage may not necessarily
|
||||||
|
// refer to the functionality we're matching. Other than TYPO3_MODE, future definitions
|
||||||
|
// will probably be weaker than this strong constant comparison.
|
||||||
|
$stringToMatch = (string)($node->name ?? $node->value);
|
||||||
|
if (array_key_exists($stringToMatch, $this->matcherDefinitions)) {
|
||||||
|
$this->matches[] = [
|
||||||
|
'restFiles' => $this->matcherDefinitions[$stringToMatch]['restFiles'],
|
||||||
|
'line' => $node->getAttribute('startLine'),
|
||||||
|
'message' => 'Usage of string "' . $stringToMatch . '"',
|
||||||
|
'indicator' => 'weak',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\ExtensionScanner\Php;
|
||||||
|
|
||||||
|
use PhpParser\NodeVisitor;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Install\ExtensionScanner\CodeScannerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory preparing matcher instances
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class MatcherFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create matcher instances and hand over configuration.
|
||||||
|
*
|
||||||
|
* @param array $matcherConfigurations Incoming configuration array
|
||||||
|
* @return NodeVisitor[]|CodeScannerInterface[]
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
public function createAll(array $matcherConfigurations)
|
||||||
|
{
|
||||||
|
$instances = [];
|
||||||
|
foreach ($matcherConfigurations as $matcherConfiguration) {
|
||||||
|
if (empty($matcherConfiguration['class'])) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Each matcher must have a class name',
|
||||||
|
1501415721
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($matcherConfiguration['configurationFile']) && !isset($matcherConfiguration['configurationArray'])) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Each matcher must have either a configurationFile or configurationArray defined',
|
||||||
|
1501416365
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($matcherConfiguration['configurationFile']) && isset($matcherConfiguration['configurationArray'])) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Having both a configurationFile and configurationArray is invalid',
|
||||||
|
1501419367
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$configuration = [];
|
||||||
|
if (isset($matcherConfiguration['configurationFile'])) {
|
||||||
|
$configuration = GeneralUtility::getFileAbsFileName($matcherConfiguration['configurationFile']);
|
||||||
|
if (empty($configuration) || !is_file($configuration)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Configuration file ' . $matcherConfiguration['configurationFile'] . ' not found',
|
||||||
|
1501509605
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$configuration = require $configuration;
|
||||||
|
if (!is_array($configuration)) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Configuration file ' . $matcherConfiguration['configurationFile'] . ' must return an array',
|
||||||
|
1501509548
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($matcherConfiguration['configurationArray'])) {
|
||||||
|
if (!is_array($matcherConfiguration['configurationArray'])) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Configuration array ' . $matcherConfiguration['configurationArray'] . ' must not be empty',
|
||||||
|
1501509738
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$configuration = $matcherConfiguration['configurationArray'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$matcherInstance = new $matcherConfiguration['class']($configuration);
|
||||||
|
if (!$matcherInstance instanceof CodeScannerInterface
|
||||||
|
|| !$matcherInstance instanceof NodeVisitor) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Matcher ' . $matcherConfiguration['class'] . ' must implement CodeScannerInterface'
|
||||||
|
. ' and NodeVisitor',
|
||||||
|
1501510168
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$instances[] = $matcherInstance;
|
||||||
|
}
|
||||||
|
return $instances;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\Factory;
|
||||||
|
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Psr\EventDispatcher\ListenerProviderInterface;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Crypto\HashService;
|
||||||
|
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
|
||||||
|
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||||
|
use TYPO3\CMS\Core\Package\FailsafePackageManager;
|
||||||
|
use TYPO3\CMS\Core\Page\Event\ResolveVirtualJavaScriptImportEvent;
|
||||||
|
use TYPO3\CMS\Core\Page\ImportMap;
|
||||||
|
|
||||||
|
final class ImportMapFactory
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly FailsafePackageManager $packageManager,
|
||||||
|
private readonly HashService $hashService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function create(string $sitePath): ImportMap
|
||||||
|
{
|
||||||
|
$packages = [
|
||||||
|
$this->packageManager->getPackage('core'),
|
||||||
|
$this->packageManager->getPackage('backend'),
|
||||||
|
$this->packageManager->getPackage('install'),
|
||||||
|
];
|
||||||
|
$bust = (string)$GLOBALS['EXEC_TIME'];
|
||||||
|
if (!Environment::getContext()->isDevelopment()) {
|
||||||
|
$bust = $this->hashService->hmac((new Typo3Version()) . Environment::getProjectPath(), self::class);
|
||||||
|
}
|
||||||
|
return new ImportMap(
|
||||||
|
hashService: $this->hashService,
|
||||||
|
packages: $packages,
|
||||||
|
eventDispatcher: $this->createEventDispatcher($sitePath, $bust),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createEventDispatcher(string $sitePath, string $bust): EventDispatcherInterface
|
||||||
|
{
|
||||||
|
return new EventDispatcher(
|
||||||
|
new class ($sitePath, $bust) implements ListenerProviderInterface {
|
||||||
|
public function __construct(private string $sitePath, private string $bust) {}
|
||||||
|
public function getListenersForEvent(object $event): iterable
|
||||||
|
{
|
||||||
|
if ($event instanceof ResolveVirtualJavaScriptImportEvent) {
|
||||||
|
return [
|
||||||
|
function (ResolveVirtualJavaScriptImportEvent $event): void {
|
||||||
|
if ($event->resolution === null && str_starts_with($event->virtualName, 'install-labels/')) {
|
||||||
|
$parameters = [
|
||||||
|
'install' => [
|
||||||
|
'action' => 'labels',
|
||||||
|
'domain' => str_replace('install-labels/', '', $event->virtualName),
|
||||||
|
'bust' => $this->bust,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$event->resolution = '/' . ltrim($this->sitePath, '/') . '?__typo3_install&'
|
||||||
|
. http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Install\FolderStructure\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract node implements common methods
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
abstract class AbstractNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name
|
||||||
|
*/
|
||||||
|
protected $name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string|null Target permissions for unix, eg. '2775' or '0664' (4 characters string)
|
||||||
|
*/
|
||||||
|
protected $targetPermission;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NodeInterface|null Parent object of this structure node
|
||||||
|
*/
|
||||||
|
protected $parent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Directories and root may have children, files and link always empty array
|
||||||
|
*/
|
||||||
|
protected $children = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get name
|
||||||
|
*
|
||||||
|
* @return string Name
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get target permission
|
||||||
|
*
|
||||||
|
* Make sure to call octdec on the value when passing this to chmod
|
||||||
|
*
|
||||||
|
* @return string Permissions as a 4 character octal string, i.e. 2775 or 0644
|
||||||
|
*/
|
||||||
|
protected function getTargetPermission()
|
||||||
|
{
|
||||||
|
return $this->targetPermission ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set target permission
|
||||||
|
*
|
||||||
|
* @param string $permission Permissions as a 4 character octal string, i.e. 2775 or 0644
|
||||||
|
*/
|
||||||
|
protected function setTargetPermission($permission)
|
||||||
|
{
|
||||||
|
// Normalize the permission string to "4 characters", padding with leading "0" if necessary:
|
||||||
|
$permission = substr($permission, 0, 4);
|
||||||
|
$permission = str_pad($permission, 4, '0', STR_PAD_LEFT);
|
||||||
|
$this->targetPermission = $permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get children
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getChildren()
|
||||||
|
{
|
||||||
|
return $this->children;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get parent
|
||||||
|
*
|
||||||
|
* @return NodeInterface|null
|
||||||
|
*/
|
||||||
|
protected function getParent()
|
||||||
|
{
|
||||||
|
return $this->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get absolute path of node
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getAbsolutePath()
|
||||||
|
{
|
||||||
|
return $this->getParent()->getAbsolutePath() . '/' . $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current node is writable if parent is writable
|
||||||
|
*
|
||||||
|
* @return bool TRUE if parent is writable
|
||||||
|
*/
|
||||||
|
public function isWritable()
|
||||||
|
{
|
||||||
|
return $this->getParent()->isWritable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if node exists.
|
||||||
|
* Returns TRUE if it is there, even if it is only a link.
|
||||||
|
* Does not check the type!
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function exists()
|
||||||
|
{
|
||||||
|
if (@is_link($this->getAbsolutePath())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return @file_exists($this->getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix permission if they are not equal to target permission
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function fixPermission(): FlashMessage
|
||||||
|
{
|
||||||
|
if ($this->isPermissionCorrect()) {
|
||||||
|
return new FlashMessage(
|
||||||
|
'',
|
||||||
|
'Permission on ' . $this->getAbsolutePath() . ' is already ok.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result = @chmod($this->getAbsolutePath(), (int)octdec($this->getTargetPermission()));
|
||||||
|
if ($result === true) {
|
||||||
|
return new FlashMessage(
|
||||||
|
'',
|
||||||
|
'Fixed permission on ' . $this->getRelativePathBelowSiteRoot() . '.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new FlashMessage(
|
||||||
|
'Permissions could not be changed to ' . $this->getTargetPermission()
|
||||||
|
. '. This only is a problem if files and folders within this node cannot be written.',
|
||||||
|
'Permission change on ' . $this->getRelativePathBelowSiteRoot() . ' not successful',
|
||||||
|
ContextualFeedbackSeverity::NOTICE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if current permission are identical to target permission
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isPermissionCorrect()
|
||||||
|
{
|
||||||
|
if ($this->isWindowsOs()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($this->getCurrentPermission() === $this->getTargetPermission()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current permission of node
|
||||||
|
*
|
||||||
|
* @return string eg. 2775 for dirs, 0664 for files
|
||||||
|
*/
|
||||||
|
protected function getCurrentPermission()
|
||||||
|
{
|
||||||
|
$permissions = decoct((int)fileperms($this->getAbsolutePath()));
|
||||||
|
return substr($permissions, -4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns TRUE if OS is windows
|
||||||
|
*
|
||||||
|
* @return bool TRUE on windows
|
||||||
|
*/
|
||||||
|
protected function isWindowsOs()
|
||||||
|
{
|
||||||
|
return Environment::isWindows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cut off project path from given path
|
||||||
|
*
|
||||||
|
* @param string $path Given path
|
||||||
|
* @return string Relative path, but beginning with /
|
||||||
|
* @throws Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
protected function getRelativePathBelowSiteRoot($path = null)
|
||||||
|
{
|
||||||
|
if ($path === null) {
|
||||||
|
$path = $this->getAbsolutePath();
|
||||||
|
}
|
||||||
|
$projectPath = Environment::getProjectPath();
|
||||||
|
if (strpos($path, $projectPath, 0) !== 0) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Public path is not first part of given path',
|
||||||
|
1366398198
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$relativePath = substr($path, strlen($projectPath), strlen($path));
|
||||||
|
// Add a forward slash again, so we don't end up with an empty string
|
||||||
|
if ($relativePath === '') {
|
||||||
|
$relativePath = '/';
|
||||||
|
}
|
||||||
|
return $relativePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Install\WebserverType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory returns default folder structure object hierarchy
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
final readonly class DefaultFactory
|
||||||
|
{
|
||||||
|
private const string TEMPLATE_PATH = __DIR__ . '/../../Resources/Private/FolderStructureTemplateFiles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default structure object hierarchy
|
||||||
|
*/
|
||||||
|
public function getStructure(WebserverType $webserverType = WebserverType::Other): StructureFacadeInterface
|
||||||
|
{
|
||||||
|
$rootNode = new RootNode($this->getDefaultStructureDefinition($webserverType), null);
|
||||||
|
return new StructureFacade($rootNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default definition of folder and file structure with dynamic
|
||||||
|
* permission settings
|
||||||
|
*/
|
||||||
|
private function getDefaultStructureDefinition(WebserverType $webserverType): array
|
||||||
|
{
|
||||||
|
$filePermission = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'];
|
||||||
|
$directoryPermission = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'];
|
||||||
|
if (Environment::getPublicPath() === Environment::getProjectPath()) {
|
||||||
|
$structure = [
|
||||||
|
// Note that root node has no trailing slash like all others
|
||||||
|
'name' => Environment::getPublicPath(),
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => 'typo3temp',
|
||||||
|
'type' => LinkOrDirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => 'index.html',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContent' => '',
|
||||||
|
],
|
||||||
|
$this->getTemporaryAssetsFolderStructure(),
|
||||||
|
[
|
||||||
|
'name' => 'var',
|
||||||
|
'type' => LinkOrDirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => '.htaccess',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/typo3temp-var-htaccess',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'cache',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'build',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'lock',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'transient',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
$this->getFileadminStructure(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Have a default .htaccess if running apache web server or a default web.config if running IIS
|
||||||
|
if ($webserverType->isApacheServer()) {
|
||||||
|
$structure['children'][] = [
|
||||||
|
'name' => '.htaccess',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/root-htaccess',
|
||||||
|
];
|
||||||
|
} elseif ($webserverType->isMicrosoftInternetInformationServer()) {
|
||||||
|
$structure['children'][] = [
|
||||||
|
'name' => 'web.config',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/root-web-config',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Environment::isComposerMode()) {
|
||||||
|
$structure['children'][] = [
|
||||||
|
'name' => 'typo3conf',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => 'ext',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'l10n',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'sites',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'system',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is when the public path is a subfolder (e.g. public/ or web/)
|
||||||
|
$publicPath = rtrim(Environment::getRelativePublicPath(), '/');
|
||||||
|
|
||||||
|
$publicPathSubStructure = [
|
||||||
|
[
|
||||||
|
'name' => 'typo3temp',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => 'index.html',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContent' => '',
|
||||||
|
],
|
||||||
|
$this->getTemporaryAssetsFolderStructure(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
$this->getFileadminStructure(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Have a default .htaccess if running apache web server or a default web.config if running IIS
|
||||||
|
if ($webserverType->isApacheServer()) {
|
||||||
|
$publicPathSubStructure[] = [
|
||||||
|
'name' => '.htaccess',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/root-htaccess',
|
||||||
|
];
|
||||||
|
} elseif ($webserverType->isMicrosoftInternetInformationServer()) {
|
||||||
|
$publicPathSubStructure[] = [
|
||||||
|
'name' => 'web.config',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/root-web-config',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Environment::isComposerMode()) {
|
||||||
|
$publicPathSubStructure[] = [
|
||||||
|
'name' => 'typo3conf',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$structure = [
|
||||||
|
// Note that root node has no trailing slash like all others
|
||||||
|
'name' => Environment::getProjectPath(),
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => 'config',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => 'sites',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'system',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
$this->getPublicStructure($publicPath, $publicPathSubStructure),
|
||||||
|
[
|
||||||
|
'name' => 'var',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => '.htaccess',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/typo3temp-var-htaccess',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'charset',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'cache',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'labels',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'lock',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'transient',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get public path structure while resolving nested paths
|
||||||
|
*/
|
||||||
|
private function getPublicStructure(string $publicPath, array $subStructure): array
|
||||||
|
{
|
||||||
|
$directoryPermission = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'];
|
||||||
|
$publicPathParts = array_reverse(explode('/', $publicPath));
|
||||||
|
|
||||||
|
$lastNode = null;
|
||||||
|
foreach ($publicPathParts as $publicPathPart) {
|
||||||
|
$node = [
|
||||||
|
'name' => $publicPathPart,
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
];
|
||||||
|
if ($lastNode !== null) {
|
||||||
|
$node['children'][] = $lastNode;
|
||||||
|
} else {
|
||||||
|
$node['children'] = $subStructure;
|
||||||
|
}
|
||||||
|
$lastNode = $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lastNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFileadminStructure(): array
|
||||||
|
{
|
||||||
|
$filePermission = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'];
|
||||||
|
$directoryPermission = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'];
|
||||||
|
return [
|
||||||
|
'name' => !empty($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir']) ? rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/') : 'fileadmin',
|
||||||
|
'type' => LinkOrDirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => '.htaccess',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/resources-root-htaccess',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '_temp_',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => '.htaccess',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/fileadmin-temp-htaccess',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'index.html',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/fileadmin-temp-index.html',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'user_upload',
|
||||||
|
'type' => LinkOrDirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => '_temp_',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => 'index.html',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContent' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'importexport',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => '.htaccess',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/fileadmin-user_upload-temp-importexport-htaccess',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'index.html',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContentFile' => self::TEMPLATE_PATH . '/fileadmin-temp-index.html',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'index.html',
|
||||||
|
'type' => FileNode::class,
|
||||||
|
'targetPermission' => $filePermission,
|
||||||
|
'targetContent' => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This defines the structure for typo3temp/assets
|
||||||
|
*/
|
||||||
|
private function getTemporaryAssetsFolderStructure(): array
|
||||||
|
{
|
||||||
|
$directoryPermission = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'];
|
||||||
|
return [
|
||||||
|
'name' => 'assets',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'name' => 'css',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'js',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'images',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '_processed_',
|
||||||
|
'type' => DirectoryNode::class,
|
||||||
|
'targetPermission' => $directoryPermission,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service class to check the default folder permissions
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class DefaultPermissionsCheck
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array Recommended values for a secure production site
|
||||||
|
*
|
||||||
|
* These are not the default settings (which are 0664/2775), because they might not work on every installation.
|
||||||
|
* For security reasons these are the recommended values nevertheless (no world-readable files).
|
||||||
|
* It's up to the admins to decide if these recommended secure values can be applied to their installation.
|
||||||
|
*/
|
||||||
|
protected $recommended = [
|
||||||
|
'fileCreateMask' => '0660',
|
||||||
|
'folderCreateMask' => '2770',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Verbose names of the settings
|
||||||
|
*/
|
||||||
|
protected $names = [
|
||||||
|
'fileCreateMask' => 'Default File permissions',
|
||||||
|
'folderCreateMask' => 'Default Directory permissions',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a BE/*mask setting for it's security
|
||||||
|
*
|
||||||
|
* If it permits world writing: Error
|
||||||
|
* If it permits world reading: Warning
|
||||||
|
* If it permits group writing: Notice
|
||||||
|
* If it permits group reading: Notice
|
||||||
|
* If it permits only user read/write: Ok
|
||||||
|
*
|
||||||
|
* @param string $which fileCreateMask or folderCreateMask
|
||||||
|
*/
|
||||||
|
public function getMaskStatus($which): FlashMessage
|
||||||
|
{
|
||||||
|
$octal = '0' . $GLOBALS['TYPO3_CONF_VARS']['SYS'][$which];
|
||||||
|
$dec = octdec($octal);
|
||||||
|
$perms = [
|
||||||
|
'ox' => ($dec & 001) == 001,
|
||||||
|
'ow' => ($dec & 002) == 002,
|
||||||
|
'or' => ($dec & 004) == 004,
|
||||||
|
'gx' => ($dec & 010) == 010,
|
||||||
|
'gw' => ($dec & 020) == 020,
|
||||||
|
'gr' => ($dec & 040) == 040,
|
||||||
|
'ux' => ($dec & 0100) == 0100,
|
||||||
|
'uw' => ($dec & 0200) == 0200,
|
||||||
|
'ur' => ($dec & 0400) == 0400,
|
||||||
|
'setgid' => ($dec & 02000) == 02000,
|
||||||
|
];
|
||||||
|
$extraMessage = '';
|
||||||
|
$groupPermissions = false;
|
||||||
|
if (!$perms['uw'] || !$perms['ur']) {
|
||||||
|
$permissionStatus = ContextualFeedbackSeverity::ERROR;
|
||||||
|
$extraMessage = ' (not read or writable by the user)';
|
||||||
|
} elseif ($perms['ow']) {
|
||||||
|
if (Environment::isWindows()) {
|
||||||
|
$permissionStatus = ContextualFeedbackSeverity::INFO;
|
||||||
|
$extraMessage = ' (writable by anyone on the server). This is the default behavior on a Windows system';
|
||||||
|
} else {
|
||||||
|
$permissionStatus = ContextualFeedbackSeverity::ERROR;
|
||||||
|
$extraMessage = ' (writable by anyone on the server)';
|
||||||
|
}
|
||||||
|
} elseif ($perms['or']) {
|
||||||
|
$permissionStatus = ContextualFeedbackSeverity::NOTICE;
|
||||||
|
$extraMessage = ' (readable by anyone on the server). This is the default set by TYPO3 CMS to be as much compatible as possible but if your system allows, please consider to change rights';
|
||||||
|
} elseif ($perms['gw']) {
|
||||||
|
$permissionStatus = ContextualFeedbackSeverity::OK;
|
||||||
|
$extraMessage = ' (group writable)';
|
||||||
|
$groupPermissions = true;
|
||||||
|
} elseif ($perms['gr']) {
|
||||||
|
$permissionStatus = ContextualFeedbackSeverity::OK;
|
||||||
|
$extraMessage = ' (group readable)';
|
||||||
|
$groupPermissions = true;
|
||||||
|
} else {
|
||||||
|
$permissionStatus = ContextualFeedbackSeverity::OK;
|
||||||
|
}
|
||||||
|
$message = 'Recommended: ' . $this->recommended[$which] . '.';
|
||||||
|
$message .= ' Currently configured as ';
|
||||||
|
if ($GLOBALS['TYPO3_CONF_VARS']['SYS'][$which] === $this->recommended[$which]) {
|
||||||
|
$message .= 'recommended';
|
||||||
|
} else {
|
||||||
|
$message .= $GLOBALS['TYPO3_CONF_VARS']['SYS'][$which];
|
||||||
|
}
|
||||||
|
$message .= $extraMessage . '.';
|
||||||
|
if ($groupPermissions) {
|
||||||
|
$message .= ' This is fine as long as the web server\'s group only comprises trusted users.';
|
||||||
|
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'])) {
|
||||||
|
$message .= ' Your site is configured (SYS/createGroup) to write as group \'' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'] . '\'.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new FlashMessage(
|
||||||
|
$message,
|
||||||
|
$this->names[$which] . ' (SYS/' . $which . ')',
|
||||||
|
$permissionStatus
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||||
|
use TYPO3\CMS\Install\FolderStructure\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directory
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class DirectoryNode extends AbstractNode implements NodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Default for directories is octal 02775 == decimal 1533
|
||||||
|
*/
|
||||||
|
protected $targetPermission = '2775';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement constructor
|
||||||
|
*
|
||||||
|
* @param array $structure Structure array
|
||||||
|
* @param NodeInterface $parent Parent object
|
||||||
|
* @throws Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function __construct(array $structure, ?NodeInterface $parent = null)
|
||||||
|
{
|
||||||
|
if ($parent === null) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Node must have parent',
|
||||||
|
1366222203
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->parent = $parent;
|
||||||
|
|
||||||
|
// Ensure name is a single segment, but not a path like foo/bar or an absolute path /foo
|
||||||
|
if (str_contains($structure['name'], '/')) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Directory name must not contain forward slash',
|
||||||
|
1366226639
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->name = $structure['name'];
|
||||||
|
|
||||||
|
if (isset($structure['targetPermission'])) {
|
||||||
|
$this->setTargetPermission($structure['targetPermission']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('children', $structure)) {
|
||||||
|
$this->createChildren($structure['children']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get own status and status of child objects
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
public function getStatus(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
if (!$this->exists()) {
|
||||||
|
$status = new FlashMessage(
|
||||||
|
'The Install Tool can try to create it',
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot() . ' does not exist',
|
||||||
|
ContextualFeedbackSeverity::WARNING
|
||||||
|
);
|
||||||
|
$result[] = $status;
|
||||||
|
} else {
|
||||||
|
$result = $this->getSelfStatus();
|
||||||
|
}
|
||||||
|
return array_merge($result, $this->getChildrenStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a test file and delete again if directory exists
|
||||||
|
*
|
||||||
|
* @return bool TRUE if test file creation was successful
|
||||||
|
*/
|
||||||
|
public function isWritable()
|
||||||
|
{
|
||||||
|
$result = true;
|
||||||
|
if (!$this->exists()) {
|
||||||
|
$result = false;
|
||||||
|
} elseif (!$this->canFileBeCreated()) {
|
||||||
|
$result = false;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix structure
|
||||||
|
*
|
||||||
|
* If there is nothing to fix, returns an empty array
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
public function fix(): array
|
||||||
|
{
|
||||||
|
$result = $this->fixSelf();
|
||||||
|
foreach ($this->children as $child) {
|
||||||
|
/** @var NodeInterface $child */
|
||||||
|
$result = array_merge($result, $child->fix());
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix this directory:
|
||||||
|
*
|
||||||
|
* - create with correct permissions if it was not existing
|
||||||
|
* - if there is no "write" permissions, try to fix it
|
||||||
|
* - leave it alone otherwise
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
protected function fixSelf(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
if (!$this->exists()) {
|
||||||
|
$resultCreateDirectory = $this->createDirectory();
|
||||||
|
$result[] = $resultCreateDirectory;
|
||||||
|
if ($resultCreateDirectory->getSeverity() === ContextualFeedbackSeverity::OK
|
||||||
|
&& !$this->isPermissionCorrect()
|
||||||
|
) {
|
||||||
|
$result[] = $this->fixPermission();
|
||||||
|
}
|
||||||
|
} elseif (!$this->isWritable()) {
|
||||||
|
// If directory is not writable, we might have permissions to fix that
|
||||||
|
// Try it:
|
||||||
|
$result[] = $this->fixPermission();
|
||||||
|
} elseif (!$this->isDirectory()) {
|
||||||
|
$fileType = @filetype($this->getAbsolutePath());
|
||||||
|
if ($fileType) {
|
||||||
|
$messageBody
|
||||||
|
= 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a directory,'
|
||||||
|
. ' but is of type ' . $fileType . '. This cannot be fixed automatically. Please investigate.'
|
||||||
|
;
|
||||||
|
} else {
|
||||||
|
$messageBody
|
||||||
|
= 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a directory,'
|
||||||
|
. ' but is of unknown type, probably because an upper level directory does not exist. Please investigate.'
|
||||||
|
;
|
||||||
|
}
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
$messageBody,
|
||||||
|
'Path ' . $this->getRelativePathBelowSiteRoot() . ' is not a directory',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create directory if not exists
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function createDirectory(): FlashMessage
|
||||||
|
{
|
||||||
|
if ($this->exists()) {
|
||||||
|
throw new Exception(
|
||||||
|
'Directory ' . $this->getAbsolutePath() . ' already exists',
|
||||||
|
1366740091
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result = @mkdir($this->getAbsolutePath());
|
||||||
|
if ($result === true) {
|
||||||
|
return new FlashMessage(
|
||||||
|
'',
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot() . ' successfully created.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new FlashMessage(
|
||||||
|
'The target directory could not be created. There is probably a'
|
||||||
|
. ' group or owner permission problem on the parent directory.',
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot() . ' not created!',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status of directory - used in root and directory node
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
protected function getSelfStatus(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
if (!$this->isDirectory()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot() . ' should be a directory,'
|
||||||
|
. ' but is of type ' . filetype($this->getAbsolutePath()),
|
||||||
|
$this->getRelativePathBelowSiteRoot() . ' is not a directory',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
} elseif (!$this->isWritable()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Path ' . $this->getAbsolutePath() . ' exists, but no file underneath it'
|
||||||
|
. ' can be created.',
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot() . ' is not writable',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
} elseif (!$this->isPermissionCorrect()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Default configured permissions are ' . $this->getTargetPermission()
|
||||||
|
. ' but current permissions are ' . $this->getCurrentPermission(),
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot() . ' permissions mismatch',
|
||||||
|
ContextualFeedbackSeverity::NOTICE
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Is a directory with the configured permissions of ' . $this->getTargetPermission(),
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status of children
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
protected function getChildrenStatus(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach ($this->children as $child) {
|
||||||
|
/** @var NodeInterface $child */
|
||||||
|
$result = array_merge($result, $child->getStatus());
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a test file and delete again - helper for isWritable
|
||||||
|
*
|
||||||
|
* @return bool TRUE if test file creation was successful
|
||||||
|
*/
|
||||||
|
protected function canFileBeCreated()
|
||||||
|
{
|
||||||
|
$testFileName = StringUtility::getUniqueId('installToolTest_');
|
||||||
|
$result = @touch($this->getAbsolutePath() . '/' . $testFileName);
|
||||||
|
if ($result === true) {
|
||||||
|
unlink($this->getAbsolutePath() . '/' . $testFileName);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if not is a directory
|
||||||
|
*
|
||||||
|
* @return bool True if node is a directory
|
||||||
|
*/
|
||||||
|
protected function isDirectory()
|
||||||
|
{
|
||||||
|
$path = $this->getAbsolutePath();
|
||||||
|
return !@is_link($path) && @is_dir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create children nodes - done in directory and root node
|
||||||
|
*
|
||||||
|
* @param array $structure Array of children
|
||||||
|
* @throws Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
protected function createChildren(array $structure)
|
||||||
|
{
|
||||||
|
foreach ($structure as $child) {
|
||||||
|
if (!array_key_exists('type', $child)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Child must have type',
|
||||||
|
1366222204
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!array_key_exists('name', $child)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Child must have name',
|
||||||
|
1366222205
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$name = $child['name'];
|
||||||
|
foreach ($this->children as $existingChild) {
|
||||||
|
/** @var NodeInterface $existingChild */
|
||||||
|
if ($existingChild->getName() === $name) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Child name must be unique',
|
||||||
|
1366222206
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->children[] = new $child['type']($child, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A folder structure exception
|
||||||
|
*/
|
||||||
|
class Exception extends \TYPO3\CMS\Install\Exception {}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure\Exception;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\FolderStructure\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An invalid argument exception
|
||||||
|
*/
|
||||||
|
class InvalidArgumentException extends Exception {}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure\Exception;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Install\FolderStructure\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A root node exception
|
||||||
|
*/
|
||||||
|
class RootNodeException extends Exception {}
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Install\FolderStructure\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class FileNode extends AbstractNode implements NodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Default for files is octal 0664 == decimal 436
|
||||||
|
*/
|
||||||
|
protected $targetPermission = '0664';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string|null Target content of file. If NULL, target content is ignored
|
||||||
|
*/
|
||||||
|
protected $targetContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement constructor
|
||||||
|
*
|
||||||
|
* @param array $structure Structure array
|
||||||
|
* @param NodeInterface $parent Parent object
|
||||||
|
* @throws Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function __construct(array $structure, ?NodeInterface $parent = null)
|
||||||
|
{
|
||||||
|
if ($parent === null) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'File node must have parent',
|
||||||
|
1366927513
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->parent = $parent;
|
||||||
|
|
||||||
|
// Ensure name is a single segment, but not a path like foo/bar or an absolute path /foo
|
||||||
|
if (str_contains($structure['name'], '/')) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'File name must not contain forward slash',
|
||||||
|
1366222207
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->name = $structure['name'];
|
||||||
|
|
||||||
|
if (isset($structure['targetPermission'])) {
|
||||||
|
$this->setTargetPermission($structure['targetPermission']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($structure['targetContent']) && isset($structure['targetContentFile'])) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Either targetContent or targetContentFile can be set, but not both',
|
||||||
|
1380364361
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($structure['targetContent'])) {
|
||||||
|
$this->targetContent = $structure['targetContent'];
|
||||||
|
}
|
||||||
|
if (isset($structure['targetContentFile'])) {
|
||||||
|
if (!is_readable($structure['targetContentFile'])) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'targetContentFile ' . $structure['targetContentFile'] . ' does not exist or is not readable',
|
||||||
|
1380364362
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$fileContent = file_get_contents($structure['targetContentFile']);
|
||||||
|
if ($fileContent === false) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Error while reading targetContentFile ' . $structure['targetContentFile'],
|
||||||
|
1380364363
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->targetContent = $fileContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get own status
|
||||||
|
* Returns warning if file not exists
|
||||||
|
* Returns error if file exists but content is not as expected (can / shouldn't be fixed)
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
public function getStatus(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
if (!$this->exists()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'By using "Try to fix errors" we can try to create it',
|
||||||
|
'File ' . $this->getRelativePathBelowSiteRoot() . ' does not exist',
|
||||||
|
ContextualFeedbackSeverity::WARNING
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$result = $this->getSelfStatus();
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix structure
|
||||||
|
*
|
||||||
|
* If there is nothing to fix, returns an empty array
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
public function fix(): array
|
||||||
|
{
|
||||||
|
$result = $this->fixSelf();
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix this node: create if not there, fix permissions
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
protected function fixSelf(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
if (!$this->exists()) {
|
||||||
|
$resultCreateFile = $this->createFile();
|
||||||
|
$result[] = $resultCreateFile;
|
||||||
|
if ($resultCreateFile->getSeverity() === ContextualFeedbackSeverity::OK
|
||||||
|
&& $this->targetContent !== null
|
||||||
|
) {
|
||||||
|
$result[] = $this->setContent();
|
||||||
|
if (!$this->isPermissionCorrect()) {
|
||||||
|
$result[] = $this->fixPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (!$this->isFile()) {
|
||||||
|
$fileType = @filetype($this->getAbsolutePath());
|
||||||
|
if ($fileType) {
|
||||||
|
$messageBody
|
||||||
|
= 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,'
|
||||||
|
. ' but is of type ' . $fileType . '. This cannot be fixed automatically. Please investigate.'
|
||||||
|
;
|
||||||
|
} else {
|
||||||
|
$messageBody
|
||||||
|
= 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,'
|
||||||
|
. ' but is of unknown type, probably because an upper level directory does not exist. Please investigate.'
|
||||||
|
;
|
||||||
|
}
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
$messageBody,
|
||||||
|
'Path ' . $this->getRelativePathBelowSiteRoot() . ' is not a file',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
} elseif (!$this->isPermissionCorrect()) {
|
||||||
|
$result[] = $this->fixPermission();
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create file if not exists
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function createFile(): FlashMessage
|
||||||
|
{
|
||||||
|
if ($this->exists()) {
|
||||||
|
throw new Exception(
|
||||||
|
'File ' . $this->getRelativePathBelowSiteRoot() . ' already exists',
|
||||||
|
1367048077
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result = @touch($this->getAbsolutePath());
|
||||||
|
if ($result === true) {
|
||||||
|
return new FlashMessage(
|
||||||
|
'',
|
||||||
|
'File ' . $this->getRelativePathBelowSiteRoot() . ' successfully created.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new FlashMessage(
|
||||||
|
'The target file could not be created. There is probably a'
|
||||||
|
. ' group or owner permission problem on the parent directory.',
|
||||||
|
'File ' . $this->getRelativePathBelowSiteRoot() . ' not created!',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status of file
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
protected function getSelfStatus(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
if (!$this->isFile()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Path ' . $this->getAbsolutePath() . ' should be a file,'
|
||||||
|
. ' but is of type ' . filetype($this->getAbsolutePath()),
|
||||||
|
$this->getRelativePathBelowSiteRoot() . ' is not a file',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
} elseif (!$this->isWritable()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'File ' . $this->getRelativePathBelowSiteRoot() . ' exists, but is not writable.',
|
||||||
|
'File ' . $this->getRelativePathBelowSiteRoot() . ' is not writable',
|
||||||
|
ContextualFeedbackSeverity::NOTICE
|
||||||
|
);
|
||||||
|
} elseif (!$this->isPermissionCorrect()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Default configured permissions are ' . $this->getTargetPermission()
|
||||||
|
. ' but file permissions are ' . $this->getCurrentPermission(),
|
||||||
|
'File ' . $this->getRelativePathBelowSiteRoot() . ' permissions mismatch',
|
||||||
|
ContextualFeedbackSeverity::NOTICE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($this->isFile() && !$this->isContentCorrect()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'File content is not identical to default content. This file may have been changed manually.'
|
||||||
|
. ' The Install Tool will not overwrite the current version!',
|
||||||
|
'File ' . $this->getRelativePathBelowSiteRoot() . ' content differs',
|
||||||
|
ContextualFeedbackSeverity::NOTICE
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Is a file with the default content and configured permissions of ' . $this->getTargetPermission(),
|
||||||
|
'File ' . $this->getRelativePathBelowSiteRoot()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare current file content with target file content
|
||||||
|
*
|
||||||
|
* @throws Exception If file does not exist
|
||||||
|
* @return bool TRUE if current and target file content are identical
|
||||||
|
*/
|
||||||
|
protected function isContentCorrect()
|
||||||
|
{
|
||||||
|
$absolutePath = $this->getAbsolutePath();
|
||||||
|
if (is_link($absolutePath) || !is_file($absolutePath)) {
|
||||||
|
throw new Exception(
|
||||||
|
'File ' . $absolutePath . ' must exist',
|
||||||
|
1367056363
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result = false;
|
||||||
|
if ($this->targetContent === null) {
|
||||||
|
$result = true;
|
||||||
|
} else {
|
||||||
|
$targetContentHash = md5($this->targetContent);
|
||||||
|
$currentContentHash = md5((string)file_get_contents($absolutePath));
|
||||||
|
if ($targetContentHash === $currentContentHash) {
|
||||||
|
$result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets content of file to target content
|
||||||
|
*
|
||||||
|
* @throws Exception If file does not exist
|
||||||
|
*/
|
||||||
|
protected function setContent(): FlashMessage
|
||||||
|
{
|
||||||
|
$absolutePath = $this->getAbsolutePath();
|
||||||
|
if (is_link($absolutePath) || !is_file($absolutePath)) {
|
||||||
|
throw new Exception(
|
||||||
|
'File ' . $absolutePath . ' must exist',
|
||||||
|
1367060201
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($this->targetContent === null) {
|
||||||
|
throw new Exception(
|
||||||
|
'Target content not defined for ' . $absolutePath,
|
||||||
|
1367060202
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result = @file_put_contents($absolutePath, $this->targetContent);
|
||||||
|
if ($result !== false) {
|
||||||
|
return new FlashMessage(
|
||||||
|
'',
|
||||||
|
'Set content to ' . $this->getRelativePathBelowSiteRoot()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new FlashMessage(
|
||||||
|
'Setting content of the file failed for unknown reasons.',
|
||||||
|
'Setting content to ' . $this->getRelativePathBelowSiteRoot() . ' failed',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if not is a file
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isFile()
|
||||||
|
{
|
||||||
|
$path = $this->getAbsolutePath();
|
||||||
|
return !is_link($path) && is_file($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Install\FolderStructure\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A link
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class LinkNode extends AbstractNode implements NodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Optional link target
|
||||||
|
*/
|
||||||
|
protected $target = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement constructor
|
||||||
|
*
|
||||||
|
* @param array $structure Structure array
|
||||||
|
* @param NodeInterface $parent Parent object
|
||||||
|
* @throws Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function __construct(array $structure, ?NodeInterface $parent = null)
|
||||||
|
{
|
||||||
|
if ($parent === null) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Link node must have parent',
|
||||||
|
1380485700
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->parent = $parent;
|
||||||
|
|
||||||
|
// Ensure name is a single segment, but not a path like foo/bar or an absolute path /foo
|
||||||
|
if (str_contains($structure['name'], '/')) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'File name must not contain forward slash',
|
||||||
|
1380546061
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->name = $structure['name'];
|
||||||
|
|
||||||
|
if (isset($structure['target']) && $structure['target'] !== '') {
|
||||||
|
$this->target = $structure['target'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get own status
|
||||||
|
* Returns information status if running on Windows
|
||||||
|
* Returns OK status if is link and possible target is correct
|
||||||
|
* Else returns error (not fixable)
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
public function getStatus(): array
|
||||||
|
{
|
||||||
|
if ($this->isWindowsOs()) {
|
||||||
|
return [
|
||||||
|
new FlashMessage(
|
||||||
|
'This node is not handled for Windows OS and should be checked manually.',
|
||||||
|
$this->getRelativePathBelowSiteRoot() . ' should be a link, but this support is incomplete for Windows.',
|
||||||
|
ContextualFeedbackSeverity::INFO
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->exists()) {
|
||||||
|
return [
|
||||||
|
new FlashMessage(
|
||||||
|
'Links cannot be fixed by this system',
|
||||||
|
$this->getRelativePathBelowSiteRoot() . ' should be a link, but it does not exist',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isLink()) {
|
||||||
|
$type = @filetype($this->getAbsolutePath());
|
||||||
|
if ($type) {
|
||||||
|
$messageBody
|
||||||
|
= 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a link,'
|
||||||
|
. ' but is of type ' . $type . '. This cannot be fixed automatically. Please investigate.'
|
||||||
|
;
|
||||||
|
} else {
|
||||||
|
$messageBody
|
||||||
|
= 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,'
|
||||||
|
. ' but is of unknown type, probably because an upper level directory does not exist. Please investigate.'
|
||||||
|
;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
new FlashMessage(
|
||||||
|
$messageBody,
|
||||||
|
'Path ' . $this->getRelativePathBelowSiteRoot() . ' is not a link',
|
||||||
|
ContextualFeedbackSeverity::WARNING
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isTargetCorrect()) {
|
||||||
|
return [
|
||||||
|
new FlashMessage(
|
||||||
|
'Link target should be ' . $this->getTarget() . ' but is ' . $this->getCurrentTarget(),
|
||||||
|
$this->getRelativePathBelowSiteRoot() . ' is a link, but link target is not as specified',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$message = 'Is a link';
|
||||||
|
if ($this->getTarget() !== '') {
|
||||||
|
$message .= ' and correctly points to target ' . $this->getTarget();
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
new FlashMessage(
|
||||||
|
$message,
|
||||||
|
$this->getRelativePathBelowSiteRoot()
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix structure
|
||||||
|
*
|
||||||
|
* If there is nothing to fix, returns an empty array
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
public function fix(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get link target
|
||||||
|
*
|
||||||
|
* @return string Link target
|
||||||
|
*/
|
||||||
|
protected function getTarget()
|
||||||
|
{
|
||||||
|
return $this->target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find out if node is a link
|
||||||
|
*
|
||||||
|
* @throws Exception\InvalidArgumentException
|
||||||
|
* @return bool TRUE if node is a link
|
||||||
|
*/
|
||||||
|
protected function isLink()
|
||||||
|
{
|
||||||
|
if (!$this->exists()) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Link does not exist',
|
||||||
|
1380556246
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return @is_link($this->getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the real link target is identical to given target
|
||||||
|
*
|
||||||
|
* @throws Exception\InvalidArgumentException
|
||||||
|
* @return bool TRUE if target is correct
|
||||||
|
*/
|
||||||
|
protected function isTargetCorrect()
|
||||||
|
{
|
||||||
|
if (!$this->exists()) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Link does not exist',
|
||||||
|
1380556245
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!$this->isLink()) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Node is not a link',
|
||||||
|
1380556247
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = false;
|
||||||
|
$expectedTarget = $this->getTarget();
|
||||||
|
if (empty($expectedTarget)) {
|
||||||
|
$result = true;
|
||||||
|
} else {
|
||||||
|
$actualTarget = $this->getCurrentTarget();
|
||||||
|
if ($expectedTarget === rtrim((string)$actualTarget, '/')) {
|
||||||
|
$result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current target of link
|
||||||
|
*
|
||||||
|
* @return false|string target
|
||||||
|
*/
|
||||||
|
protected function getCurrentTarget()
|
||||||
|
{
|
||||||
|
return readlink($this->getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directory but a link is ok as well
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class LinkOrDirectoryNode extends DirectoryNode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get status of directory - used in root and directory node
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
protected function getSelfStatus(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
if (!$this->isDirectory()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot() . ' should be a directory or link,'
|
||||||
|
. ' but is of type ' . filetype($this->getAbsolutePath()),
|
||||||
|
$this->getRelativePathBelowSiteRoot() . ' is not a directory',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
} elseif (!$this->isWritable()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Path ' . $this->getAbsolutePath() . ' exists, but no file underneath it'
|
||||||
|
. ' can be created.',
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot() . ' is not writable',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
} elseif (!$this->isPermissionCorrect()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Default configured permissions are ' . $this->getTargetPermission()
|
||||||
|
. ' but current permissions are ' . $this->getCurrentPermission(),
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot() . ' permissions mismatch',
|
||||||
|
ContextualFeedbackSeverity::NOTICE
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if ($this->isLink()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Is a link to a directory with the configured permissions of ' . $this->getTargetPermission(),
|
||||||
|
'Link ' . $this->getRelativePathBelowSiteRoot()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'Is a directory with the configured permissions of ' . $this->getTargetPermission(),
|
||||||
|
'Directory ' . $this->getRelativePathBelowSiteRoot()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if node is a directory or link
|
||||||
|
*
|
||||||
|
* @return bool True if node is a directory
|
||||||
|
*/
|
||||||
|
protected function isDirectory()
|
||||||
|
{
|
||||||
|
$path = $this->getAbsolutePath();
|
||||||
|
return $this->isLink() || @is_dir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isLink(): bool
|
||||||
|
{
|
||||||
|
$path = $this->getAbsolutePath();
|
||||||
|
return @is_link($path) && @is_dir(realpath($path));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for structure nodes root, link, file, ...
|
||||||
|
*/
|
||||||
|
interface NodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructor gets structure and parent object defaulting to NULL
|
||||||
|
*
|
||||||
|
* @param array $structure Structure
|
||||||
|
* @param NodeInterface $parent Parent
|
||||||
|
*/
|
||||||
|
public function __construct(array $structure, ?NodeInterface $parent = null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get node name
|
||||||
|
*
|
||||||
|
* @return string Node name
|
||||||
|
*/
|
||||||
|
public function getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get absolute path of node
|
||||||
|
*
|
||||||
|
* @return string Absolute path
|
||||||
|
*/
|
||||||
|
public function getAbsolutePath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the status of the object tree, recursive for directory and root node
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
public function getStatus(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if node is writable - can be created and permission can be fixed
|
||||||
|
*
|
||||||
|
* @return bool TRUE if node is writable
|
||||||
|
*/
|
||||||
|
public function isWritable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix structure
|
||||||
|
*
|
||||||
|
* If there is nothing to fix, returns an empty array
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
public function fix(): array;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||||
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||||
|
use TYPO3\CMS\Install\FolderStructure\Exception\InvalidArgumentException;
|
||||||
|
use TYPO3\CMS\Install\FolderStructure\Exception\RootNodeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root node of structure
|
||||||
|
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||||
|
*/
|
||||||
|
class RootNode extends DirectoryNode implements RootNodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Implement constructor
|
||||||
|
*
|
||||||
|
* @param array $structure Given structure
|
||||||
|
* @param NodeInterface $parent Must be NULL for RootNode
|
||||||
|
* @throws Exception\RootNodeException
|
||||||
|
* @throws Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function __construct(array $structure, ?NodeInterface $parent = null)
|
||||||
|
{
|
||||||
|
if ($parent !== null) {
|
||||||
|
throw new RootNodeException(
|
||||||
|
'Root node must not have parent',
|
||||||
|
1366140117
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($structure['name'])
|
||||||
|
|| ($this->isWindowsOs() && substr($structure['name'], 1, 2) !== ':/')
|
||||||
|
|| (!$this->isWindowsOs() && $structure['name'][0] !== '/')
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Root node expects absolute path as name',
|
||||||
|
1366141329
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->name = $structure['name'];
|
||||||
|
|
||||||
|
if (isset($structure['targetPermission'])) {
|
||||||
|
$this->setTargetPermission($structure['targetPermission']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('children', $structure)) {
|
||||||
|
$this->createChildren($structure['children']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get own status and status of child objects - Root node gives error status if not exists
|
||||||
|
*
|
||||||
|
* @return FlashMessage[]
|
||||||
|
*/
|
||||||
|
public function getStatus(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
if (!$this->exists()) {
|
||||||
|
$result[] = new FlashMessage(
|
||||||
|
'',
|
||||||
|
$this->getAbsolutePath() . ' does not exist',
|
||||||
|
ContextualFeedbackSeverity::ERROR
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$result = $this->getSelfStatus();
|
||||||
|
}
|
||||||
|
$result = array_merge($result, $this->getChildrenStatus());
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root node does not call parent, but returns own name only
|
||||||
|
*
|
||||||
|
* @return string Absolute path
|
||||||
|
*/
|
||||||
|
public function getAbsolutePath()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface implemented by root node
|
||||||
|
*/
|
||||||
|
interface RootNodeInterface extends NodeInterface {}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure facade, a facade class in front of root node.
|
||||||
|
* This is the main API interface to the node structure and should
|
||||||
|
* be the only class used from outside.
|
||||||
|
*/
|
||||||
|
class StructureFacade implements StructureFacadeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var RootNodeInterface The structure to work on
|
||||||
|
*/
|
||||||
|
protected $structure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor sets structure to work on
|
||||||
|
*/
|
||||||
|
public function __construct(RootNodeInterface $structure)
|
||||||
|
{
|
||||||
|
$this->structure = $structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status of node tree
|
||||||
|
*/
|
||||||
|
public function getStatus(): FlashMessageQueue
|
||||||
|
{
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
foreach ($this->structure->getStatus() as $message) {
|
||||||
|
$messageQueue->enqueue($message);
|
||||||
|
}
|
||||||
|
return $messageQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix structure
|
||||||
|
*/
|
||||||
|
public function fix(): FlashMessageQueue
|
||||||
|
{
|
||||||
|
$messageQueue = new FlashMessageQueue('install');
|
||||||
|
foreach ($this->structure->fix() as $message) {
|
||||||
|
$messageQueue->enqueue($message);
|
||||||
|
}
|
||||||
|
return $messageQueue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TYPO3\CMS\Install\FolderStructure;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface of structure facade, a facade class in front of root node
|
||||||
|
*/
|
||||||
|
interface StructureFacadeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructor gets structure to work on
|
||||||
|
*/
|
||||||
|
public function __construct(RootNodeInterface $structure);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status of node tree
|
||||||
|
*/
|
||||||
|
public function getStatus(): FlashMessageQueue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix structure
|
||||||
|
*/
|
||||||
|
public function fix(): FlashMessageQueue;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user