Commit e797ab41 authored by Thomas Löffler's avatar Thomas Löffler

Merge branch 'develop' into 'main'

Release 15-04-21

See merge request !656
parents 970e1fe7 ee4a40c9
Pipeline #10572 passed with stages
in 7 minutes and 45 seconds
......@@ -30,6 +30,7 @@
"ext-zip": "*",
"ext-zlib": "*",
"apache-solr-for-typo3/solr": "^11.0",
"b13/typo3-composerize": "dev-main",
"cweagans/composer-patches": "^1.7",
"gordalina/cachetool": "^4.0",
"lcobucci/jwt": "^3.3",
......@@ -72,10 +73,10 @@
},
"scripts": {
"test:unit": [
"./vendor/bin/phpunit -c .gitlab-ci/Tests/phpunit.xml --log-junit build/junit-report.xml --coverage-text --colors=never"
"XDEBUG_MODE='coverage' ./vendor/bin/phpunit -c .gitlab-ci/Tests/phpunit.xml --log-junit build/junit-report.xml --coverage-text --colors=never"
],
"test:mutation": [
"./vendor/bin/infection"
"XDEBUG_MODE='coverage' ./vendor/bin/infection"
],
"test:api": [
"./vendor/bin/codecept run api --steps"
......
This diff is collapsed.
<?php
declare(strict_types = 1);
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* 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.
*/
namespace T3o\Ter\Controller;
use B13\Typo3Composerize\Utilities\ComposerManifestCreator;
use B13\Typo3Composerize\Utilities\ExtensionKeyMap;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Request handler for `/composerize` endpoint
*/
class ComposerizeController
{
public function createExtensionComposerMapAction(ServerRequestInterface $request): ResponseInterface
{
return new JsonResponse($this->getExtensionComposerMap(), 200);
}
public function createComposerManifestAction(ServerRequestInterface $request): ResponseInterface
{
$extensionKey = $request->getAttribute('routeResult')['key'] ?? '';
$emConf = $request->getBody()->getContents();
if ($extensionKey === '' || $emConf === '') {
return new JsonResponse([], 400);
}
try {
$emConf = json_decode($emConf, true, 512, JSON_THROW_ON_ERROR);
return new JsonResponse((new ComposerManifestCreator(
new ExtensionKeyMap($this->getExtensionComposerMap())
))->createComposerManifest($extensionKey, $emConf), 200);
} catch (\JsonException $e) {
return new JsonResponse([], 400);
}
}
protected function getExtensionComposerMap(): array
{
$extensionComposerMap = [];
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_terfe2_domain_model_extension');
$result = $queryBuilder
->select('ext_key', 'composer_name')
->from('tx_terfe2_domain_model_extension')
->where($queryBuilder->expr()->neq('composer_name', $queryBuilder->createNamedParameter('')))
->execute();
while ($row = $result->fetch()) {
$extensionComposerMap[$row['ext_key']] = [
'composer_name' => $row['composer_name']
];
}
return $extensionComposerMap;
}
}
<?php
declare(strict_types = 1);
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* 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.
*/
namespace T3o\Ter\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use T3o\Ter\Controller\ComposerizeController;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This middleware handles requests to the /composerize/* endpoints
*/
class ComposerizeEndpoint implements MiddlewareInterface
{
protected string $base = '/composerize';
protected array $routes = [
'createExtensionComposerMap' => ['endpoint' => '', 'method' => 'GET'],
'createComposerManifest' => ['endpoint' => '/{key}', 'method' => 'POST']
];
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$this->canHandle($request)) {
return $handler->handle($request);
}
try {
$routeResult = (new UrlMatcher(
$this->getRouteCollection(),
$this->getRequestContext($request)
))->match($request->getUri()->getPath());
} catch (\Exception $e) {
return $handler->handle($request);
}
if (!isset($routeResult['_controller'], $routeResult['_route'])) {
return $handler->handle($request);
}
$controller = GeneralUtility::makeInstance($routeResult['_controller']);
$action = $routeResult['_route'] . 'Action';
if (is_callable([$controller, $action])) {
return $controller->{$action}(
$request->withAttribute('routeResult', array_filter($routeResult, static function ($_, $k) {
return strpos($k, '_') !== 0;
}, ARRAY_FILTER_USE_BOTH)),
);
}
return $handler->handle($request);
}
protected function canHandle(ServerRequestInterface $request): bool
{
return strpos($request->getUri()->getPath(), $this->base) === 0
&& isset(array_flip(array_unique(array_column($this->routes, 'method')))[$request->getMethod()]);
}
protected function getRouteCollection(): RouteCollection
{
$routeCollection = new RouteCollection();
foreach ($this->routes as $name => $options) {
$routeCollection->add(
$name,
(new Route($this->base . $options['endpoint']))
->setMethods([$options['method']])
->setDefault('_controller', ComposerizeController::class)
);
}
return $routeCollection;
}
protected function getRequestContext(ServerRequestInterface $request): RequestContext
{
$uri = $request->getUri();
return new RequestContext(
'',
$request->getMethod(),
(string)idn_to_ascii($uri->getHost()),
$uri->getScheme(),
80,
443,
$uri->getPath(),
$uri->getQuery()
);
}
}
<?php
return [
'frontend' => [
't3o/ter/composerize' => [
'target' => \T3o\Ter\Middleware\ComposerizeEndpoint::class,
'before' => [
'typo3/cms-frontend/base-redirect-resolver'
],
'after' => [
'typo3/cms-frontend/maintenance-mode'
]
],
't3o/ter/soap' => [
'target' => \T3o\Ter\Middleware\SoapEndpoint::class,
'after' => [
......
<?php
namespace T3o\TerFe2\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use T3o\TerFe2\Service\ValidComposerNameFileService;
class UpdateValidComposerNameFileCommand extends Command
{
protected function configure()
{
$this->setDescription('Checks if JSON file needs to be updated');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
if (ValidComposerNameFileService::issetRegistryFlag()) {
ValidComposerNameFileService::updateFile();
}
}
}
......@@ -14,19 +14,15 @@ namespace T3o\TerFe2\Controller\Eid;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use T3o\TerFe2\Service\ValidComposerNameFileService;
/**
* Class ExtensionController
*
* @deprecated Use ValidComposerNameFileService::output()
*/
class ExtensionController
{
/**
* @var array
*/
protected $jsonArray = [
'meta' => null,
'data' => null
];
/**
* @param $action
*/
......@@ -41,49 +37,9 @@ class ExtensionController
}
}
/**
*/
protected function findAllWithValidComposerName()
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_terfe2_domain_model_extension');
$expr = $queryBuilder->expr();
$result = $queryBuilder->select(
'ext_key',
'composer_name'
)
->from('tx_terfe2_domain_model_extension')
->where(
$expr->neq(
'composer_name',
$queryBuilder->createNamedParameter('')
)
)
->execute();
while ($extension = $result->fetch()) {
$this->jsonArray['data'][$extension['ext_key']] = [
'composer_name' => $extension['composer_name'],
];
}
$json = json_encode($this->jsonArray, true);
if (JSON_ERROR_NONE !== ($jsonErrorCode = json_last_error())) {
$this->jsonArray['meta'] = [
'error' => [
'type' => 'json encoding error',
'code' => $jsonErrorCode
]
];
$this->jsonArray['data'] = null;
$json = json_encode($this->jsonArray);
}
header('Content-Type: application/json');
echo $json;
exit();
ValidComposerNameFileService::output();
}
}
......
......@@ -19,11 +19,13 @@ use T3o\Ter\Api\ExtensionKey;
use T3o\Ter\Api\ExtensionVersion;
use T3o\TerFe2\Domain\Model\Extension;
use T3o\TerFe2\Provider\FileProvider;
use T3o\TerFe2\Service\ValidComposerNameFileService;
use T3o\TerFe2\Utility\ExtensionUtility;
use T3o\TerFe2\Utility\VersionUtility;
use T3o\TerFe2\Validation\Validator\ComposerNameValidator;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;
/**
* Controller for the extension object
......@@ -130,7 +132,7 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController
*
* @param \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager $persistenceManager
*/
public function injectPersistenceManager(\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager $persistenceManager)
public function injectPersistenceManager(PersistenceManager $persistenceManager)
{
$this->persistenceManager = $persistenceManager;
}
......@@ -312,7 +314,13 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController
$this->addFlashMessage('Tag "' . htmlspecialchars($tag) . '" added to extension');
}
}
$this->extensionRepository->update($extension);
if ($extension->_isDirty('composerName')) {
ValidComposerNameFileService::setRegistryFlag(1);
}
if (!empty($save)) {
$this->redirectWithMessage(
$this->translate('msg.extension_updated'),
......@@ -523,6 +531,12 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController
if ($extensionVersion->doesExtensionVersionExist()) {
$this->forwardWithError($this->translate('msg.createVersionVersionExists'), 'uploadVersion');
}
$composerStatus = '';
if (($composerManifest = $this->getComposerManifest($files)) === []) {
$composerStatus = 'missing';
} elseif (($composerManifest['extra']['typo3/cms']['extension-key'] ?? '') !== $extensionKey) {
$composerStatus = 'missingExtensionKey';
}
$extensionInfo->extensionKey = $extensionKey;
$extensionInfo->infoData->uploadComment = $form['comment'];
try {
......@@ -531,7 +545,7 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController
$uploader = new ApiUser($this->frontendUser['username']);
$uploader->authenticate();
$extensionVersion->upload($uploader, $extensionInfo, $files);
$this->redirect('index', 'Registerkey', null, ['uploaded' => true], $this->settings['pages']['manageKeysPID']);
$this->redirect('index', 'Registerkey', null, ['uploaded' => true, 'composerStatus' => $composerStatus], $this->settings['pages']['manageKeysPID']);
} catch (\Exception $exception) {
$this->forwardWithError('Error ' . $exception->getCode() . ': ' . $exception->getMessage(), 'uploadVersion');
}
......@@ -662,4 +676,21 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController
{
return $this->getBaseUrl() . '/' . ExtensionUtility::getExtensionIcon($extension->getExtKey(), $extension->getLastVersion()->getVersionString());
}
private function getComposerManifest(array $files): array
{
$composerManifest = [];
foreach ($files as $file) {
$fileName = trim($file->name, '/\\');
if ($fileName === 'composer.json') {
try {
$composerManifest = json_decode(base64_decode($file->content), true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
// Ignore invalid json since this will just return an empty manifest
}
break;
}
}
return $composerManifest;
}
}
......@@ -75,8 +75,9 @@ class RegisterkeyController extends \T3o\TerFe2\Controller\AbstractTerBasedContr
* Initialize all actions
*
* @param bool $uploaded TRUE if an extension version was successfully uploaded
* @param string $composerStatus Contains information about the composer status, e.g. missing
*/
public function indexAction($uploaded = false)
public function indexAction(bool $uploaded = false, string $composerStatus = '')
{
// get extensions by user if a user is logged in
if ($GLOBALS['TSFE']->fe_user->user['uid']) {
......@@ -89,6 +90,7 @@ class RegisterkeyController extends \T3o\TerFe2\Controller\AbstractTerBasedContr
$this->view->assign('expiringExtensions', $expiringExtensions);
$this->view->assign('likedExtensions', $likedExtensions);
$this->view->assign('uploaded', $uploaded);
$this->view->assign('composerStatus', $composerStatus);
}
}
......
<?php
namespace T3o\TerFe2\Service;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class ValidComposerNameFileService
{
protected static string $fileName = 'currentvalidcomposernames.json';
public static function output(): void
{
$fileName = self::getFullPathOfFile();
if (!file_exists($fileName) || self::issetRegistryFlag()) {
self::updateFile();
}
header('Content-Type: application/json');
echo file_get_contents($fileName);
exit();
}
public static function updateFile(): void
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_terfe2_domain_model_extension');
$expr = $queryBuilder->expr();
$result = $queryBuilder
->select(
'ext_key',
'composer_name'
)
->from('tx_terfe2_domain_model_extension')
->where(
$expr->neq(
'composer_name',
$queryBuilder->createNamedParameter('')
)
)
->execute();
$extensionArray = [
'meta' => null,
'data' => null,
];
while ($extension = $result->fetch()) {
$extensionArray['data'][$extension['ext_key']] = [
'composer_name' => $extension['composer_name'],
];
}
if (JSON_ERROR_NONE !== ($jsonErrorCode = json_last_error())) {
$extensionArray['meta'] = [
'error' => [
'type' => 'json encoding error',
'code' => $jsonErrorCode
]
];
$extensionArray['data'] = null;
}
file_put_contents(
self::getFullPathOfFile(),
json_encode($extensionArray)
);
self::setRegistryFlag(0);
}
public static function setRegistryFlag(int $flag): void
{
$registry = GeneralUtility::makeInstance(Registry::class);
$registry->set('ter_fe2', 'validComposerNameUpdate', $flag);
}
public static function issetRegistryFlag(): bool
{
$registry = GeneralUtility::makeInstance(Registry::class);
return (int)$registry->get('ter_fe2', 'validComposerNameUpdate') === 1;
}
protected static function getFullPathOfFile(): string
{
return Environment::getPublicPath() . '/' . $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] . '/' . self::$fileName;
}
}
......@@ -41,4 +41,7 @@ return [
'packagist:fetchdownloaddata' => [
'class' => \T3o\TerFe2\Command\PackagistCommand::class
],
'ter:updatevalidcomposernamefile' => [
'class' => \T3o\TerFe2\Command\UpdateValidComposerNameFileCommand::class
],
];
......@@ -246,6 +246,12 @@
<trans-unit id="msg.createVersionUploadSuccess" xml:space="preserve">
<source>Thank you for the upload of your new extension version. It may take up to 15 minutes until it appears in TER.</source>
</trans-unit>
<trans-unit id="msg.composerStatus.missing" xml:space="preserve">
<source>Your uploaded extension version does not contain a composer.json file. This will be mandatory in the future.</source>
</trans-unit>
<trans-unit id="msg.composerStatus.missingExtensionKey" xml:space="preserve">
<source>The composer.json of your uploaded extension version does not contain the extension key. This will be mandatory in the future.</source>
</trans-unit>
<trans-unit id="msg.createVersionUploadFailed" xml:space="preserve">
<source>Extension upload failed.</source>
</trans-unit>
......
......@@ -10,6 +10,20 @@
</p>
</div>
</f:if>
<f:if condition="{composerStatus}">
<div class="alert alert-warning">
<p>
<f:translate key="LLL:EXT:ter_fe2/Resources/Private/Language/locallang.xlf:msg.composerStatus.{composerStatus}" />
More information can be found in the official
<f:link.typolink parameter="https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ExtensionArchitecture/ComposerJson/Index.html">
TYPO3 documentation
</f:link.typolink>.
</p>
</div>
</f:if>
<header class="mb-4 mt-4">
<h1>Managing my extensions</h1>
</header>
......
......@@ -16,6 +16,7 @@ use ApacheSolrForTypo3\Solr\GarbageCollector;
use ApacheSolrForTypo3\Solr\IndexQueue\Queue;
use T3o\TerFe2\Domain\Repository\ExtensionRepository as ExtbaseExtensionRepository;
use T3o\TerFe2\Service\ExtensionIndexService;
use T3o\TerFe2\Service\ValidComposerNameFileService;
use T3o\TerRest\Repository\ExtensionRepository;
use T3o\TerRest\Repository\FrontendUserRepository;
use T3o\TerRest\Service\ConfigurationService;
......@@ -56,6 +57,12 @@ abstract class AbstractService
return $this;
}
protected function requestUpdateOfValidComposerNamesFile(): self
{
ValidComposerNameFileService::setRegistryFlag(1);
return $this;
}
protected function flushCaches(): self
{
GeneralUtility::makeInstance(CacheManager::class)->flushCaches();
......
......@@ -107,7 +107,15 @@ class KeyService extends AbstractService
$fieldsToUpdate[$property->getName()] = $property->getValue();
}
return (bool)$this->extensionRepository->update($this->key, $fieldsToUpdate);
if (!$this->extensionRepository->update($this->key, $fieldsToUpdate)) {
return false;
}