Commit 45300d46 authored by Thomas Löffler's avatar Thomas Löffler

Merge branch 'develop' into 490-add-composer-script-to-run-php-cs-fixer-locally

parents 253ee303 a10756b4
Pipeline #10062 passed with stages
in 6 minutes and 18 seconds
#!/bin/bash
## Description: Generate ecdsa keys for generating tokens in the REST API
## Usage: create-keys
## Example: "ddev create-keys"
readonly KEYS_PATH=/var/www/html
mkdir -p $KEYS_PATH
openssl ecparam -name secp521r1 -genkey -noout -out $KEYS_PATH/private.test.ec.key
openssl pkcs8 -topk8 -in $KEYS_PATH/private.test.ec.key -out $KEYS_PATH/private.test.pem -passin env:ECDSA_KEY_PASSPHRASE -passout env:ECDSA_KEY_PASSPHRASE
openssl ec -in $KEYS_PATH/private.test.pem -pubout -out $KEYS_PATH/public.test.pem -passin env:ECDSA_KEY_PASSPHRASE
rm $KEYS_PATH/private.test.ec.key
......@@ -8,3 +8,11 @@ services:
- DB_HOST=db
- DB_NAME=db
- DB_PASSWORD=db
- ECDSA_PUBLIC_KEY_FILE=/var/www/html/public.test.pem
- ECDSA_PRIVATE_KEY_FILE=/var/www/html/private.test.pem
- ECDSA_KEY_PASSPHRASE=passphrase
- TER_REST_RANDOM_LENGTH=30
- TER_REST_DEFAULT_LIFETIME=604800
- TER_REST_JWT_SUBJECT=t3o-ter-rest
- TER_REST_JWT_LATENCY=60
- TER_REST_SIGNATURE_IDENTIFIER=ecdsa
......@@ -9,6 +9,7 @@
!/public/.well-known/security.txt
/auth.json
/assets/
/*.pem
sequelpro.spf
.php_cs.cache
.ddev/db_snapshots/
......
......@@ -32,13 +32,14 @@ test:unit:
- apt-get update -yqq
- apt-get install git unzip zlib1g-dev libzip-dev -yqq
- docker-php-ext-install zip
- pecl install xdebug
- pecl install xdebug-2.9.2
- docker-php-ext-enable xdebug
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- export TYPO3_PATH_WEB="$PWD/private"
- composer config cache-dir /cache/composer
- mkdir $TYPO3_PATH_WEB/fileadmin/ && touch $TYPO3_PATH_WEB/fileadmin/currentcoredata.json
script:
- composer selfupdate --1
- composer install --ignore-platform-reqs
- composer test:unit
artifacts:
......@@ -57,13 +58,14 @@ test:mutation:
- apt-get update -yqq
- apt-get install git unzip zlib1g-dev libzip-dev -yqq
- docker-php-ext-install zip
- pecl install xdebug
- pecl install xdebug-2.9.2
- docker-php-ext-enable xdebug
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- export TYPO3_PATH_WEB="$PWD/private"
- composer config cache-dir /cache/composer
- mkdir $TYPO3_PATH_WEB/fileadmin/ && touch $TYPO3_PATH_WEB/fileadmin/currentcoredata.json
script:
- composer selfupdate --1
- composer install --ignore-platform-reqs
- ./vendor/bin/infection --min-msi=10 --min-covered-msi=75 --threads=4
artifacts:
......@@ -97,4 +99,4 @@ mutation:badge:
paths:
- badges/
when: always
expire_in: 4 weeks
\ No newline at end of file
expire_in: 4 weeks
......@@ -17,10 +17,14 @@
<testsuite name="ter_fe2 tests">
<directory>../../extensions/ter_fe2/Tests/Unit</directory>
</testsuite>
<testsuite name="ter_rest tests">
<directory>../../extensions/ter_rest/Tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../../extensions/ter_fe2/Classes/</directory>
<directory suffix=".php">../../extensions/ter_rest/Classes/</directory>
</whitelist>
</filter>
<logging>
......
This diff is collapsed.
<?php
declare(strict_types = 1);
namespace T3o\TerFe2\Controller;
/*
* This file is part of TYPO3 CMS-extension "ter_fe2", 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.
*/
use GuzzleHttp\Exception\RequestException;
use T3o\TerFe2\Domain\DTO\TokenFormData;
use T3o\TerFe2\Domain\Repository\ExtensionRepository;
use T3o\TerFe2\Service\TokenRequestService;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter;
/**
* Controller for managing access tokens
*/
class TokenController extends ActionController
{
private const API_ACTIONS = ['create', 'refresh', 'revoke'];
protected ExtensionRepository $extensionRepository;
protected TokenRequestService $tokenRequestService;
protected Context $context;
public function __construct(
ExtensionRepository $extensionRepository,
TokenRequestService $tokenRequestService,
Context $context
) {
$this->extensionRepository = $extensionRepository;
$this->tokenRequestService = $tokenRequestService;
$this->context = $context;
}
public function indexAction(): void
{
$this->view->assignMultiple([
'actions' => self::API_ACTIONS,
'activeTab' => $this->getActiveTab(),
'extensions' => $this->extensionRepository->findByFrontendUser(
(string)$this->context->getPropertyFromAspect('frontend.user', 'username')
)
]);
}
/**
* @param TokenFormData $tokenFormData
* @TYPO3\CMS\Extbase\Annotation\Validate("T3o\TerFe2\Validation\Validator\TokenCreationValidator", param="tokenFormData")
*/
public function createAction(TokenFormData $tokenFormData): void
{
try {
$responseContent = (string)$this->tokenRequestService->request(
'/token',
$tokenFormData->getPassword(),
['query' => $this->tokenRequestService->createQueryArguments($tokenFormData)]
)->getBody()->getContents();
} catch (RequestException $e) {
$this->addFlashMessageForCode($e->getCode());
$this->redirectToIndexAction();
}
$this->view->assign('tokenData', json_decode($responseContent, true, 512, JSON_THROW_ON_ERROR));
}
/**
* @param TokenFormData $tokenFormData
* @TYPO3\CMS\Extbase\Annotation\Validate("T3o\TerFe2\Validation\Validator\TokenUpdateValidator", param="tokenFormData")
*/
public function refreshAction(TokenFormData $tokenFormData): void
{
try {
$responseContent = (string)$this->tokenRequestService->request(
'/token/refresh',
$tokenFormData->getPassword(),
['form_params' => $this->tokenRequestService->createFormParams($tokenFormData)]
)->getBody()->getContents();
} catch (RequestException $e) {
$this->addFlashMessageForCode($e->getCode());
$this->redirectToIndexAction();
}
$this->view->assign('tokenData', json_decode($responseContent, true, 512, JSON_THROW_ON_ERROR));
}
/**
* @param TokenFormData $tokenFormData
* @TYPO3\CMS\Extbase\Annotation\Validate("T3o\TerFe2\Validation\Validator\TokenUpdateValidator", param="tokenFormData")
*/
public function revokeAction(TokenFormData $tokenFormData): void
{
try {
$this->tokenRequestService->request(
'/token/revoke',
$tokenFormData->getPassword(),
['form_params' => $this->tokenRequestService->createFormParams($tokenFormData)]
);
$this->addFlashMessage('', 'Token sucessfully revoked');
} catch (RequestException $e) {
$this->addFlashMessageForCode($e->getCode());
}
$this->redirectToIndexAction();
}
protected function initializeCreateAction(): void
{
if ($this->arguments->hasArgument('tokenFormData')) {
$this->arguments
->getArgument('tokenFormData')
->getPropertyMappingConfiguration()
->forProperty('expires')
->setTypeConverterOption(DateTimeConverter::class, DateTimeConverter::CONFIGURATION_DATE_FORMAT, 'Y-m-d');
}
}
protected function getActiveTab(): string
{
$activeTab = (string)($this->request->getArguments()['activeTab'] ?? '');
$apiActions = self::API_ACTIONS;
if ($activeTab === '' || !in_array($activeTab, $apiActions, true)) {
$this->redirectToIndexAction(array_shift($apiActions));
}
return $activeTab;
}
protected function redirectToIndexAction(string $activeTab = ''): void
{
if ($activeTab === '') {
$activeTab = str_replace('Action', '', $this->actionMethodName);
}
$this->redirect('index', 'Token', null, ['activeTab' => $activeTab]);
}
protected function getErrorFlashMessage(): bool
{
return false;
}
protected function addFlashMessageForCode(int $code): void
{
$action = str_replace('Action', '', $this->actionMethodName);
switch ($code) {
case 400:
$this->addFlashMessage(
'Could not ' . $action . ' token because the token is invalid or not yet ready.',
'Error on validation',
AbstractMessage::ERROR
);
break;
case 401:
case 403:
$this->addFlashMessage(
'Could not ' . $action . ' token due to invalid authorization.',
'Error on authentication',
AbstractMessage::ERROR
);
break;
default:
$this->addFlashMessage(
'Could not ' . $action . ' token due to a server error. Please try again.',
'General server error',
AbstractMessage::ERROR
);
break;
}
}
}
<?php
declare(strict_types = 1);
namespace T3o\TerFe2\Domain\DTO;
/*
* This file is part of TYPO3 CMS-extension "ter_fe2", 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.
*/
/**
* DTO for token request data
*/
class TokenFormData
{
protected string $password;
protected string $name;
protected ?\DateTime $expires;
protected array $scope;
protected array $extensions;
protected string $jwt;
public function __construct(
string $password,
string $name = '',
\DateTime $expires = null,
array $scope = [],
array $extensions = [],
string $jwt = ''
) {
$this->password = $password;
$this->name = $name;
$this->expires = $expires;
$this->scope = $scope;
$this->extensions = $extensions;
$this->jwt = $jwt;
}
public function getPassword(): string
{
return $this->password;
}
public function getName(): string
{
return $this->name;
}
public function getExpires(): ?\DateTime
{
return $this->expires;
}
public function getScope(): array
{
return $this->scope;
}
public function getExtensions(): array
{
return $this->extensions;
}
public function getJwt(): string
{
return $this->jwt;
}
}
......@@ -15,6 +15,7 @@ namespace T3o\TerFe2\Domain\Model;
*/
use T3o\TerFe2\Utility\VersionUtility;
use T3o\TerRest\DTO\Extension as ExtensionDto;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
......@@ -22,7 +23,7 @@ use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
/**
* Extension container
*/
class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity implements \JsonSerializable
{
/**
* Extension key
......@@ -253,6 +254,27 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
return $this->versions;
}
/**
* Getter for a single version
*
* @param mixed $identifier Either the version number (e.g. 1000001) or the version string (e.g. 1.0.1)
* @return Version|null
*/
public function getVersion($identifier): ?Version
{
foreach ($this->versions as $version) {
if ($version instanceof Version
&& ((is_int($identifier) && $version->getVersionNumber() === $identifier)
|| (is_string($identifier) && $version->getVersionString() === $identifier))
) {
return $version;
}
}
return null;
}
/**
* Get versions reverse sorted by version number
*
......@@ -749,4 +771,15 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
return $supportVersions;
}
/**
* Actually an array is returned since the DTO object
* does also implement JsonSerialize.
*
* @return array|ExtensionDto
*/
public function jsonSerialize()
{
return new ExtensionDto($this);
}
}
......@@ -190,6 +190,11 @@ class Version extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
*/
protected $composerInfo = '';
/**
* @var string
*/
protected $compatibleTypo3Versions = '';
/**
* Constructor. Initializes all ObjectStorage instances.
*/
......@@ -786,6 +791,14 @@ class Version extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
return $composerName;
}
/**
* @return array
*/
public function getCompatibleTypo3Versions(): array
{
return GeneralUtility::intExplode(',', $this->compatibleTypo3Versions, true);
}
/**
* @return \T3o\TerFe2\Domain\Model\Relation|null
*/
......
......@@ -15,7 +15,12 @@ namespace T3o\TerFe2\Domain\Repository;
*/
use ApacheSolrForTypo3\Solr\IndexQueue\Queue;
use T3o\TerFe2\Domain\Model\Extension;
use T3o\TerRest\DTO\ExtensionDemand;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
/**
* Repository for \T3o\TerFe2\Domain\Model\Extension
......@@ -110,6 +115,24 @@ class ExtensionRepository extends \T3o\TerFe2\Domain\Repository\AbstractReposito
return $query->execute()->getFirst();
}
/**
* Returns a extension with a unique name across the database.
* This means, underscores are taken into account, so the key
* "ter_ter" is equal to "te_rt_er".
*
* @param string $key
* @return Extension|null
*/
public function findOneByUniqueKey(string $key): ?Extension
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_terfe2_domain_model_extension');
$statement = $queryBuilder
->select('*')
->from('tx_terfe2_domain_model_extension')
->add('where', 'REPLACE(ext_key, "_", "") = ' . $queryBuilder->createNamedParameter(str_replace('_', '', $key)));
return $this->createQuery(0, 1)->statement($statement)->execute()->getFirst();
}
/**
* Returns extension by extension key
*
......@@ -218,4 +241,54 @@ class ExtensionRepository extends \T3o\TerFe2\Domain\Repository\AbstractReposito
time()
);
}
public function findDemanded(ExtensionDemand $demand): QueryResultInterface
{
return $this->matchDemand($this->createQuery($demand->getOffset(), $demand->getPerPage()), $demand)->execute();
}
public function countDemanded(ExtensionDemand $demand, bool $respectPagination = true): int
{
$query = $this->createQuery();
if ($respectPagination) {
$query
->setOffset($demand->getOffset())
->setLimit($demand->getPerPage());
}
return $this->matchDemand($query, $demand)->execute()->count();
}
protected function matchDemand(QueryInterface $query, ExtensionDemand $extensionDemand): QueryInterface
{
$constraints = [];