Commit 72b582d6 authored by Thomas Löffler's avatar Thomas Löffler
Browse files

Merge branch 'develop' into 175-display-other-extension-of-same-user

parents 7f5e8f31 bf7a6857
Pipeline #11540 passed with stages
in 4 minutes and 16 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);
}
}
......
......@@ -16,6 +16,8 @@ use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use T3o\Ter\Api\Configuration;
use T3o\Ter\Exception\InternalServerErrorException;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
......@@ -117,6 +119,7 @@ class ExtensionIndexService implements LoggerAwareInterface
$dom = new \DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;
$extensionsObj = $dom->appendChild(new \DOMElement('extensions'));
$distributionBaseUrl = $this->getDistributionBaseUrl();
// Create the nested XML structure:
foreach ($extensionsAndVersionsArr as $extensionKey => $extensionVersionsArr) {
......@@ -139,15 +142,27 @@ class ExtensionIndexService implements LoggerAwareInterface
$reviewState = (string)((int)$extensionVersionArr['review_state'] !== -2 ? $extensionVersionArr['review_state'] : 0);
$versionObj->appendChild(new \DOMElement('reviewstate', $reviewState));
$versionObj->appendChild(new \DOMElement('category', $this->xmlentities((string)$extensionVersionArr['em_category'])));
if ($extensionVersionArr['category'] === 'distribution') {
if ($extensionVersionArr['em_category'] === 'distribution') {
$prefixDistributionFilePath = $extensionKey[0] . '/' . $extensionKey[1] . '/' . $extensionKey . '_' . $versionNumber . '_';
$distributionImage = $prefixDistributionFilePath . 'Distribution.png';
$distributionWelcomeImage = $prefixDistributionFilePath . 'DistributionWelcome.png';
if (is_file($this->basePath . $distributionImage)) {
$versionObj->appendChild(new \DOMElement('distributionImage', $this->xmlentities($distributionImage)));
$distributionImageUrl = $distributionBaseUrl . $distributionImage;
$versionObj->appendChild(
new \DOMElement(
'distributionImage',
$this->xmlentities($distributionImageUrl)
)
);
}
if (is_file($this->basePath . $distributionWelcomeImage)) {
$versionObj->appendChild(new \DOMElement('distributionImageWelcome', $this->xmlentities($distributionWelcomeImage)));
$distributionWelcomeImageUrl = $distributionBaseUrl . $distributionWelcomeImage;
$versionObj->appendChild(
new \DOMElement(
'distributionImageWelcome',
$this->xmlentities($distributionWelcomeImageUrl)
)
);
}
}
$versionObj->appendChild(
......@@ -239,4 +254,15 @@ class ExtensionIndexService implements LoggerAwareInterface
? (string)serialize(json_decode($dependencies, true))
: '';
}
protected function getDistributionBaseUrl(): string
{
return implode(
'',
[
(string)GeneralUtility::makeInstance(SiteFinder::class)->getSiteByIdentifier('extensions')->getBase() . '/',
GeneralUtility::makeInstance(ResourceFactory::class)->getDefaultStorage()->getFolder('ter')->getPublicUrl()
]
);
}
}
<?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>