Commit 55569c3e authored by Thomas Löffler's avatar Thomas Löffler

Merge branch 'composer-name' into 'develop'

Make it possible to confirm a composer name for an extension

See merge request !344
parents cc772d0d ec240cc3
Pipeline #4541 passed with stages
in 6 minutes and 52 seconds
...@@ -19,7 +19,6 @@ namespace T3o\TerFe2\Controller; ...@@ -19,7 +19,6 @@ namespace T3o\TerFe2\Controller;
*/ */
abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{ {
/** /**
* @var \T3o\TerFe2\Security\Role; * @var \T3o\TerFe2\Security\Role;
*/ */
...@@ -47,6 +46,11 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti ...@@ -47,6 +46,11 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
} }
} }
protected function addErrorFlashMessage()
{
// Do NOT add an error flash message, output validation errors only
}
/** /**
* Translate a label * Translate a label
* *
...@@ -60,7 +64,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti ...@@ -60,7 +64,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
return \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate($label, $extensionKey, $arguments); return \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate($label, $extensionKey, $arguments);
} }
/** /**
* Send flash message and redirect to given action * Send flash message and redirect to given action
* *
...@@ -87,7 +90,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti ...@@ -87,7 +90,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
$this->redirect($action, $controller, $extension, $arguments); $this->redirect($action, $controller, $extension, $arguments);
} }
/** /**
* Send flash message and forward to given action * Send flash message and forward to given action
* *
...@@ -105,7 +107,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti ...@@ -105,7 +107,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
$this->forward($action, $controller, $extension, $arguments); $this->forward($action, $controller, $extension, $arguments);
} }
/** /**
* Send flash message and redirect to given action * Send flash message and redirect to given action
* *
...@@ -123,7 +124,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti ...@@ -123,7 +124,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
$this->redirect($action, $controller, $extension, $arguments); $this->redirect($action, $controller, $extension, $arguments);
} }
/** /**
* Send flash message and forward to given action * Send flash message and forward to given action
* *
...@@ -141,7 +141,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti ...@@ -141,7 +141,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
$this->forward($action, $controller, $extension, $arguments); $this->forward($action, $controller, $extension, $arguments);
} }
/** /**
* Clear cache of given pages * Clear cache of given pages
* *
...@@ -156,7 +155,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti ...@@ -156,7 +155,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
} }
} }
/** /**
* Adds the base uri if not already in place. * Adds the base uri if not already in place.
* *
......
...@@ -14,6 +14,9 @@ namespace T3o\TerFe2\Controller\Eid; ...@@ -14,6 +14,9 @@ namespace T3o\TerFe2\Controller\Eid;
* The TYPO3 project - inspiring people to share! * The TYPO3 project - inspiring people to share!
*/ */
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/** /**
* Class \T3o\TerFe2\Controller\Eid\ExtensionController * Class \T3o\TerFe2\Controller\Eid\ExtensionController
*/ */
...@@ -57,17 +60,27 @@ class ExtensionController ...@@ -57,17 +60,27 @@ class ExtensionController
/** /**
* @return void * @return void
*/ */
protected function findAllWithRepositoryUrlAsPackageSource() protected function findAllWithValidComposerName()
{ {
$extensions = $this->databaseConnection->exec_SELECTgetRows( $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_terfe2_domain_model_extension');
'*', $expr = $queryBuilder->expr();
'tx_terfe2_domain_model_extension',
'hidden = 0 and deleted = 0 and repository_clone_url <> ""' $result = $queryBuilder->select(
); 'ext_key',
'composer_name'
)
->from('tx_terfe2_domain_model_extension')
->where(
$expr->neq(
'composer_name',
$queryBuilder->createNamedParameter('')
)
)
->execute();
foreach ($extensions as $extension) { while ($extension = $result->fetch()) {
$this->jsonArray['data'][$extension['ext_key']] = array( $this->jsonArray['data'][$extension['ext_key']] = array(
'repository_clone_url' => $extension['repository_clone_url'], 'composer_name' => $extension['composer_name'],
); );
} }
......
...@@ -14,7 +14,7 @@ namespace T3o\TerFe2\Controller; ...@@ -14,7 +14,7 @@ namespace T3o\TerFe2\Controller;
* The TYPO3 project - inspiring people to share! * The TYPO3 project - inspiring people to share!
*/ */
use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface; use T3o\TerFe2\Validation\Validator\ComposerNameValidator;
/** /**
* Controller for the extension object * Controller for the extension object
...@@ -235,6 +235,18 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController ...@@ -235,6 +235,18 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController
} }
} }
public function initializeUpdateAction()
{
if (!$this->request->hasArgument('packagistConfirm')) {
return;
}
ComposerNameValidator::$composerNameIsConfirmed = (bool)$this->request->getArgument('packagistConfirm');
if (!ComposerNameValidator::$composerNameIsConfirmed) {
$extensionProperties = $this->request->getArgument('extension');
$extensionProperties['composerName'] = '';
$this->request->setArgument('extension', $extensionProperties);
}
}
/** /**
* Updates an existing extension * Updates an existing extension
...@@ -242,6 +254,9 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController ...@@ -242,6 +254,9 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController
* @param \T3o\TerFe2\Domain\Model\Extension $extension extension to update * @param \T3o\TerFe2\Domain\Model\Extension $extension extension to update
* @param string $tag * @param string $tag
* @param string $save * @param string $save
*
* @validate $extension \T3o\TerFe2\Validation\Validator\ExtensionArgumentValidator
*
* @return void * @return void
*/ */
public function updateAction(\T3o\TerFe2\Domain\Model\Extension $extension, $tag = '', $save = '') public function updateAction(\T3o\TerFe2\Domain\Model\Extension $extension, $tag = '', $save = '')
......
...@@ -87,13 +87,6 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity ...@@ -87,13 +87,6 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
*/ */
protected $repositoryUrl = ''; protected $repositoryUrl = '';
/**
* External repository clone url
*
* @var string
*/
protected $repositoryCloneUrl = '';
/** /**
* Link to an external manual * Link to an external manual
* *
...@@ -115,9 +108,17 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity ...@@ -115,9 +108,17 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
*/ */
protected $expire; protected $expire;
/**
* Composer name of extension on packagist.org
*
* @var string
* @validate \T3o\TerFe2\Validation\Validator\ComposerNameValidator
*/
protected $composerName = '';
/** /**
* Constructor. Initializes all ObjectStorage instances. * Initialize all ObjectStorage instances.
*/ */
public function __construct() public function __construct()
{ {
...@@ -435,26 +436,6 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity ...@@ -435,26 +436,6 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
return $this->repositoryUrl; return $this->repositoryUrl;
} }
/**
* Sets repositoryCloneUrl
*
* @param string $repositoryCloneUrl
*/
public function setRepositoryCloneUrl(string $repositoryCloneUrl)
{
$this->repositoryCloneUrl = trim($repositoryCloneUrl);
}
/**
* Gets repositoryCloneUrl
*
* @return string
*/
public function getRepositoryCloneUrl(): string
{
return $this->repositoryCloneUrl;
}
/** /**
* Returns all votes for the extension * Returns all votes for the extension
* *
...@@ -596,4 +577,20 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity ...@@ -596,4 +577,20 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
return $dateOfFirstUpload; return $dateOfFirstUpload;
} }
/**
* @return string
*/
public function getComposerName()
{
return $this->composerName;
}
/**
* @param string $composerName
*/
public function setComposerName($composerName)
{
$this->composerName = $composerName;
}
} }
...@@ -104,9 +104,9 @@ class TerIndexer extends \ApacheSolrForTypo3\Solr\IndexQueue\Indexer ...@@ -104,9 +104,9 @@ class TerIndexer extends \ApacheSolrForTypo3\Solr\IndexQueue\Indexer
// composer support // composer support
$document->setField('supportsComposer_boolS', false); $document->setField('supportsComposer_boolS', false);
$document->setField('composerName_stringS', ''); $document->setField('composerName_stringS', '');
if ($extension->getLastVersion()->getComposerName()) { if ($extension->getComposerName()) {
$document->setField('supportsComposer_boolS', true); $document->setField('supportsComposer_boolS', true);
$document->setField('composerName_stringS', $extension->getLastVersion()->getComposerName()); $document->setField('composerName_stringS', $extension->getComposerName());
} }
// does this extension supports different versions? // does this extension supports different versions?
......
...@@ -324,6 +324,8 @@ class ImportExtensionsFromQueueTask extends Task ...@@ -324,6 +324,8 @@ class ImportExtensionsFromQueueTask extends Task
'composer_info' => $extData['composerinfo'] 'composer_info' => $extData['composerinfo']
]; ];
$this->ensureValidComposerNameInExtension($extUid, $extData['composerinfo']);
$tableName = 'tx_terfe2_domain_model_version'; $tableName = 'tx_terfe2_domain_model_version';
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable($tableName); $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
...@@ -333,6 +335,37 @@ class ImportExtensionsFromQueueTask extends Task ...@@ -333,6 +335,37 @@ class ImportExtensionsFromQueueTask extends Task
return $connectionPool->getConnectionForTable($tableName)->lastInsertId($tableName); return $connectionPool->getConnectionForTable($tableName)->lastInsertId($tableName);
} }
private function ensureValidComposerNameInExtension(int $extUid, string $composerInfoJson)
{
$composerInfo = @\json_decode($composerInfoJson, true);
if (empty($composerInfo['name'])) {
return;
}
$tableName = 'tx_terfe2_domain_model_extension';
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
$persistedComposerName = $queryBuilder->select('composer_name')
->from($tableName)
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($extUid, \PDO::PARAM_INT))
)
->execute()
->fetchColumn();
if (empty($persistedComposerName) || $persistedComposerName === $composerInfo['name']) {
return;
}
$queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder->update($tableName)
->set('composer_name', '')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($extUid, \PDO::PARAM_INT))
)
->execute();
}
/** /**
* @param int $versionUid * @param int $versionUid
* @param int $extUid * @param int $extUid
......
<?php
namespace T3o\TerFe2\Validation\Validator;
/*
* 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!
*/
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
/**
* Validator for composer name
*/
class ComposerNameValidator extends AbstractValidator
{
protected $acceptsEmptyValues = false;
public static $composerNameIsConfirmed = false;
/**
* Returns false, if given composer name is not set, invalid or too long
*
* @param mixed $value The value that should be validated
* @return boolean TRUE if the value is valid, FALSE if an error occured
*/
public function isValid($value)
{
$value = (string)$value;
if ($value === '') {
if (!self::$composerNameIsConfirmed) {
return true;
}
$this->addError('This composer name must not be empty.', 1527507206);
return false;
}
if (!$this->isValidComposerName($value)) {
$this->addError('This composer name is not valid.', 1527501477);
return false;
}
return true;
}
private function isValidComposerName(string $composerName): bool
{
if (strlen($composerName) > 60) {
return false;
}
if (substr_count($composerName, '/') !== 1) {
return false;
}
$cleanedComposerName = preg_replace('/[^a-z0-9\/_-]/', '', $composerName);
return $cleanedComposerName === $composerName;
}
}
<?php
namespace T3o\TerFe2\Validation\Validator;
/*
* 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!
*/
use GuzzleHttp\Exception\RequestException;
use T3o\TerFe2\Domain\Model\Extension;
use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
/**
* Validator for composer name on packagist.org
*/
class ExtensionArgumentValidator extends AbstractValidator
{
/**
* Returns false, if given composer name is not registered on packagist.org
*
* @param mixed $value The value that should be validated
* @return boolean TRUE if the value is valid, FALSE if an error occured
*/
public function isValid($value)
{
if (!$value instanceof Extension) {
$this->addError('This argument is not of type Extension.', 1527537602);
return false;
}
if (empty($value->getComposerName())) {
return true;
}
if (!$this->composerNameMatchesComposerNameInLatestVersion($value)) {
$this->addError('This composer name does not match composer name in last version of uploaded extension.', 1527538363);
return false;
}
if (!$this->isRegisteredOnPackagist($value->getComposerName())) {
$this->addError('This composer name is not registered on packagist.org.', 1527500413);
return false;
}
return true;
}
private function composerNameMatchesComposerNameInLatestVersion(Extension $extension)
{
return $extension->getLastVersion() !== null && $extension->getComposerName() === $extension->getLastVersion()->getComposerName();
}
private function isRegisteredOnPackagist(string $composerName): bool
{
$requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
try {
$response = $requestFactory->request(
'https://packagist.org/packages/' . $composerName,
'HEAD',
[
'connect_timeout' => 2,
'allow_redirects' => false,
]
);
return $response->getStatusCode() === 200;
} catch (RequestException $e) {
return false;
}
}
}
...@@ -19,10 +19,10 @@ return array( ...@@ -19,10 +19,10 @@ return array(
'iconfile' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extRelPath('ter_fe2') . 'Resources/Public/Icons/extension.gif', 'iconfile' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extRelPath('ter_fe2') . 'Resources/Public/Icons/extension.gif',
), ),
'interface' => array( 'interface' => array(
'showRecordFieldList' => 'ext_key,forge_link,last_update,last_maintained,tags,versions,last_version,frontend_user,downloads,repository_url,repository_clone_url,paypal_url,external_manual,expire', 'showRecordFieldList' => 'ext_key,forge_link,last_update,last_maintained,tags,versions,last_version,frontend_user,downloads,composer_name,repository_url,paypal_url,external_manual,expire',
), ),
'types' => array( 'types' => array(
'1' => array('showitem' => 'ext_key,forge_link,last_update,last_maintained,tags,versions,last_version,frontend_user,downloads,repository_url,repository_clone_url,paypal_url,external_manual,expire'), '1' => array('showitem' => 'ext_key,forge_link,last_update,last_maintained,tags,versions,last_version,frontend_user,downloads,composer_name,repository_url,paypal_url,external_manual,expire'),
), ),
'palettes' => array( 'palettes' => array(
'1' => array('showitem' => ''), '1' => array('showitem' => ''),
...@@ -100,6 +100,15 @@ return array( ...@@ -100,6 +100,15 @@ return array(
'eval' => 'trim', 'eval' => 'trim',
), ),
), ),
'composer_name' => array(
'exclude' => 1,
'label' => 'LLL:EXT:ter_fe2/Resources/Private/Language/locallang_db.xlf:tx_terfe2_domain_model_extension.composer_name',
'config' => array(
'type' => 'input',
'size' => 60,
'eval' => 'trim',
),
),
'last_upload' => array( 'last_upload' => array(
'exclude' => 1, 'exclude' => 1,
'label' => 'LLL:EXT:ter_fe2/Resources/Private/Language/locallang_db.xlf:tx_terfe2_domain_model_extension.last_upload', 'label' => 'LLL:EXT:ter_fe2/Resources/Private/Language/locallang_db.xlf:tx_terfe2_domain_model_extension.last_upload',
...@@ -188,16 +197,6 @@ return array( ...@@ -188,16 +197,6 @@ return array(
'eval' => 'trim', 'eval' => 'trim',
), ),
), ),
'repository_clone_url' => array(
'exclude' => 1,
'label' => 'LLL:EXT:ter_fe2/Resources/Private/Language/locallang_db.xlf:tx_terfe2_domain_model_extension.repository_clone_url',
'config' => array(
'type' => 'text',
'rows' => 10,
'cols' => 40,
'eval' => 'trim',
),
),
'external_manual' => array( 'external_manual' => array(
'exclude' => 1, 'exclude' => 1,
'label' => 'LLL:EXT:ter_fe2/Resources/Private/Language/locallang_db.xlf:tx_terfe2_domain_model_extension.external_manual', 'label' => 'LLL:EXT:ter_fe2/Resources/Private/Language/locallang_db.xlf:tx_terfe2_domain_model_extension.external_manual',
......
...@@ -12,6 +12,15 @@ ...@@ -12,6 +12,15 @@
<trans-unit id="tx_terfe2_domain_model_extension.forge_link" xml:space="preserve"> <trans-unit id="tx_terfe2_domain_model_extension.forge_link" xml:space="preserve">
<source>Link to issue tracker</source> <source>Link to issue tracker</source>
</trans-unit> </trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.composer_name" xml:space="preserve">
<source>Composer name of extension</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.packagist_confirm" xml:space="preserve">
<source>Published on Packagist</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.packagist_confirm_message" xml:space="preserve">
<source>I confirm the extension is published on https://packagist.org and I maintain the package</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.last_update" xml:space="preserve"> <trans-unit id="tx_terfe2_domain_model_extension.last_update" xml:space="preserve">
<source>Last update</source> <source>Last update</source>
</trans-unit> </trans-unit>
...@@ -42,12 +51,6 @@ ...@@ -42,12 +51,6 @@
<trans-unit id="tx_terfe2_domain_model_extension.repository_url" xml:space="preserve"> <trans-unit id="tx_terfe2_domain_model_extension.repository_url" xml:space="preserve">