diff --git a/Classes/Updates/MigrateOldLocations.php b/Classes/Updates/MigrateOldLocations.php index bcd4cde..2640f65 100644 --- a/Classes/Updates/MigrateOldLocations.php +++ b/Classes/Updates/MigrateOldLocations.php @@ -28,8 +28,10 @@ use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\DataHandling\DataHandler; use TYPO3\CMS\Core\Log\Logger; use TYPO3\CMS\Core\Log\LogManager; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite; use TYPO3\CMS\Install\Updates\UpgradeWizardInterface; +use Wrm\Events\Updates\UserAuthentication\User; class MigrateOldLocations implements UpgradeWizardInterface { @@ -159,6 +161,11 @@ class MigrateOldLocations implements UpgradeWizardInterface } $recordUid = 'NEW12121'; $l10nParentUid = $this->uidsForTranslation[$event['l10n_parent'] . '-0'] ?? 0; + + if (($GLOBALS['BE_USER'] ?? null) === null) { + $GLOBALS['BE_USER'] = GeneralUtility::makeInstance(User::class); + $GLOBALS['BE_USER']->authenticate(); + } $dataHandler = clone $this->dataHandler; if ($event['sys_language_uid'] > 0 && $l10nParentUid > 0) { diff --git a/Classes/Updates/UserAuthentication/User.php b/Classes/Updates/UserAuthentication/User.php new file mode 100644 index 0000000..6994062 --- /dev/null +++ b/Classes/Updates/UserAuthentication/User.php @@ -0,0 +1,142 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace Wrm\Events\Updates\UserAuthentication; + +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; +use TYPO3\CMS\Core\Crypto\Random; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * An backend user used by Update Wizards. + * + * That way they can use Data Handler no matter how they are executed, e.g. cli, or install tool. + * That way edits also always have this user assigned. + * + * This was mostly copied from TYPO3 core CommandLineUserAuthentication. + */ +class User extends BackendUserAuthentication +{ + /** + * @var string + */ + protected $username = '_events_'; + + public function __construct() + { + if (!$this->isUserAllowedToLogin()) { + throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1483971855); + } + $this->dontSetCookie = true; + parent::__construct(); + } + + public function start(ServerRequestInterface $request = null) + { + // do nothing + } + + public function checkAuthentication(ServerRequestInterface $request = null) + { + // do nothing + } + + public function authenticate() + { + // check if a _events_ user exists, if not, create one + $this->setBeUserByName($this->username); + if (!$this->user['uid']) { + // create a new BE user in the database + if (!$this->checkIfEventUserExists()) { + $this->createEventUser(); + } else { + throw new \RuntimeException('No backend user named "_events_" could be authenticated, maybe this user is "hidden"?', 1484050401); + } + $this->setBeUserByName($this->username); + } + if (!$this->user['uid']) { + throw new \RuntimeException('No backend user named "_events_" could be created.', 1476107195); + } + // The groups are fetched and ready for permission checking in this initialization. + $this->fetchGroupData(); + $this->backendSetUC(); + // activate this functionality for DataHandler + $this->uc['recursiveDelete'] = true; + } + + public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false) + { + $this->authenticate(); + } + + /** + * Determines whether a CLI backend user is allowed to access TYPO3. + * Only when adminOnly is off (=0), and only allowed for admins and CLI users (=2) + */ + public function isUserAllowedToLogin() + { + return in_array((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'], [0, 2], true); + } + + protected function checkIfEventUserExists(): bool + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $count = $queryBuilder + ->count('*') + ->from('be_users') + ->where($queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter('_events_'))) + ->execute() + ->fetchColumn(0); + return (bool)$count; + } + + protected function createEventUser(): void + { + $userFields = [ + 'username' => $this->username, + 'password' => $this->generateHashedPassword(), + 'admin' => 1, + 'tstamp' => $GLOBALS['EXEC_TIME'], + 'crdate' => $GLOBALS['EXEC_TIME'], + ]; + + $databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable('be_users'); + $databaseConnection->insert('be_users', $userFields); + } + + protected function generateHashedPassword(): string + { + $cryptoService = GeneralUtility::makeInstance(Random::class); + $password = $cryptoService->generateRandomBytes(20); + $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE'); + return $hashInstance->getHashedPassword($password); + } +} diff --git a/Classes/Updates/UserAuthentication/UserV10.php b/Classes/Updates/UserAuthentication/UserV10.php new file mode 100644 index 0000000..49eaa6e --- /dev/null +++ b/Classes/Updates/UserAuthentication/UserV10.php @@ -0,0 +1,142 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace Wrm\Events\Updates\UserAuthentication; + +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; +use TYPO3\CMS\Core\Crypto\Random; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\VersionNumberUtility; + +/** + * An backend user used by Update Wizards. + * + * That way they can use Data Handler no matter how they are executed, e.g. cli, or install tool. + * That way edits also always have this user assigned. + * + * This was mostly copied from TYPO3 core CommandLineUserAuthentication. + */ +class UserV10 extends BackendUserAuthentication +{ + /** + * @var string + */ + protected $username = '_events_'; + + public function __construct() + { + if (!$this->isUserAllowedToLogin()) { + throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1483971855); + } + $this->dontSetCookie = true; + parent::__construct(); + } + + public function start() + { + // do nothing + } + + public function checkAuthentication() + { + // do nothing + } + + public function authenticate() + { + // check if a _events_ user exists, if not, create one + $this->setBeUserByName($this->username); + if (!$this->user['uid']) { + // create a new BE user in the database + if (!$this->checkIfCliUserExists()) { + $this->createCliUser(); + } else { + throw new \RuntimeException('No backend user named "_events_" could be authenticated, maybe this user is "hidden"?', 1484050401); + } + $this->setBeUserByName($this->username); + } + if (!$this->user['uid']) { + throw new \RuntimeException('No backend user named "_events_" could be created.', 1476107195); + } + // The groups are fetched and ready for permission checking in this initialization. + $this->fetchGroupData(); + $this->backendSetUC(); + // activate this functionality for DataHandler + $this->uc['recursiveDelete'] = true; + } + + public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false) + { + $this->authenticate(); + } + + /** + * Determines whether a CLI backend user is allowed to access TYPO3. + * Only when adminOnly is off (=0), and only allowed for admins and CLI users (=2) + */ + protected function isUserAllowedToLogin(): bool + { + return in_array((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'], [0, 2], true); + } + + protected function checkIfCliUserExists(): bool + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $count = $queryBuilder + ->count('*') + ->from('be_users') + ->where($queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter('_events_'))) + ->execute() + ->fetchColumn(0); + return (bool)$count; + } + + protected function createCliUser(): void + { + $userFields = [ + 'username' => $this->username, + 'password' => $this->generateHashedPassword(), + 'admin' => 1, + 'tstamp' => $GLOBALS['EXEC_TIME'], + 'crdate' => $GLOBALS['EXEC_TIME'], + ]; + + $databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable('be_users'); + $databaseConnection->insert('be_users', $userFields); + } + + protected function generateHashedPassword(): string + { + $cryptoService = GeneralUtility::makeInstance(Random::class); + $password = $cryptoService->generateRandomBytes(20); + $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE'); + return $hashInstance->getHashedPassword($password); + } +} diff --git a/Configuration/Services.php b/Configuration/Services.php index 6e76670..189507c 100644 --- a/Configuration/Services.php +++ b/Configuration/Services.php @@ -7,8 +7,11 @@ namespace Wrm\Events; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use TYPO3\CMS\Core\Utility\VersionNumberUtility; use Wrm\Events\Service\DestinationDataImportService\Slugger\Registry; use Wrm\Events\Service\DestinationDataImportService\Slugger\SluggerType; +use Wrm\Events\Updates\UserAuthentication\User; +use Wrm\Events\Updates\UserAuthentication\UserV10; return static function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) { $containerBuilder->registerForAutoconfiguration(SluggerType::class)->addTag('tx_events.slugger_type'); @@ -21,4 +24,8 @@ return static function (ContainerConfigurator $container, ContainerBuilder $cont } } }); + + if (version_compare(VersionNumberUtility::getNumericTypo3Version(), '11.0', '<')) { + $container->services()->set(User::class, UserV10::class); + } }; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 060160b..55ece5a 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -6,6 +6,7 @@ services: Wrm\Events\: resource: '../Classes/*' + exclude: '../Classes/Updates/UserAuthentication*' Wrm\Events\Command\ImportDestinationDataViaConfigruationCommand: tags: diff --git a/Documentation/Changelog/3.4.1.rst b/Documentation/Changelog/3.4.1.rst new file mode 100644 index 0000000..5b198b3 --- /dev/null +++ b/Documentation/Changelog/3.4.1.rst @@ -0,0 +1,32 @@ +3.4.1 +===== + +Breaking +-------- + +Nothing + +Features +-------- + +Nothing + +Fixes +----- + +* Do not break Upgrade Wizards in install tool. + + TYPO3 does not provide a BE_USER within install tool. + That will break the Upgrade Wizards as they use DataHandler that relies on an existing backend user. + This was not an issue on command line where _cli_ user will be created and used by TYPO3. + The extension now adds an _events_ user as admin as well when necessary. + +Tasks +----- + +Nothing + +Deprecation +----------- + +Nothing diff --git a/Documentation/Maintenance/TYPO3/V10.rst b/Documentation/Maintenance/TYPO3/V10.rst index e4f8619..45780c7 100644 --- a/Documentation/Maintenance/TYPO3/V10.rst +++ b/Documentation/Maintenance/TYPO3/V10.rst @@ -3,9 +3,14 @@ TYPO3 V10 Changes that should happen once we drop TYPO3 v10. - Remove fetching cached page stage from body from tests ------------------------------------------------------ We have different assertions based on TYPO3 version, due to how TYPO3 exposes the info. We can remove the condition with its content once we drop v10. + +Remove UserAuthenticationV10 +---------------------------- + +TYPO3 has different signatures for AbstractUserAuthentication class. +There is an implementation for v10 that can be removed once we drop v10. diff --git a/ext_emconf.php b/ext_emconf.php index e213833..ad6cf46 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -9,7 +9,7 @@ $EM_CONF['events'] = [ 'state' => 'alpha', 'createDirs' => '', 'clearCacheOnLoad' => 0, - 'version' => '3.4.0', + 'version' => '3.4.1', 'constraints' => [ 'depends' => [ 'typo3' => '10.4.00-11.5.99', diff --git a/phpstan.neon b/phpstan.neon index 186b490..29a8cc1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,25 +1,28 @@ includes: - phpstan-baseline.neon parameters: - level: max - paths: - - Classes - - Configuration - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false - reportUnmatchedIgnoredErrors: false - ignoreErrors: - - "#^Call to an undefined method Doctrine\\\\DBAL\\\\Result\\:\\:fetch\\(\\)\\.$#" - - "#^Cannot call method fetchAll\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" - - "#^Cannot call method fetchAll\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - - "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" - - "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - - "#^Cannot call method fetchColumn\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" - - "#^Cannot call method fetchColumn\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - - "#^Cannot call method fetchFirstColumn\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" - - "#^Cannot call method fetchFirstColumn\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - - "#^Cannot call method fetch\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" - - "#^Cannot call method fetch\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - - "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" - - "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - - "#^Parameter \\#1 \\.\\.\\.\\$predicates of method TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\:\\:where\\(\\) expects array\\\\|Doctrine\\\\DBAL\\\\Query\\\\Expression\\\\CompositeExpression, string given\\.$#" + level: max + paths: + - Classes + - Configuration + excludePaths: + analyseAndScan: + - Classes/Updates/UserAuthentication/ + checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false + reportUnmatchedIgnoredErrors: false + ignoreErrors: + - "#^Call to an undefined method Doctrine\\\\DBAL\\\\Result\\:\\:fetch\\(\\)\\.$#" + - "#^Cannot call method fetchAll\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" + - "#^Cannot call method fetchAll\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + - "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" + - "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + - "#^Cannot call method fetchColumn\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" + - "#^Cannot call method fetchColumn\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + - "#^Cannot call method fetchFirstColumn\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" + - "#^Cannot call method fetchFirstColumn\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + - "#^Cannot call method fetch\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" + - "#^Cannot call method fetch\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + - "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" + - "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + - "#^Parameter \\#1 \\.\\.\\.\\$predicates of method TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\:\\:where\\(\\) expects array\\\\|Doctrine\\\\DBAL\\\\Query\\\\Expression\\\\CompositeExpression, string given\\.$#"