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;
*/
abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{
/**
* @var \T3o\TerFe2\Security\Role;
*/
......@@ -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
*
......@@ -60,7 +64,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
return \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate($label, $extensionKey, $arguments);
}
/**
* Send flash message and redirect to given action
*
......@@ -87,7 +90,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
$this->redirect($action, $controller, $extension, $arguments);
}
/**
* Send flash message and forward to given action
*
......@@ -105,7 +107,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
$this->forward($action, $controller, $extension, $arguments);
}
/**
* Send flash message and redirect to given action
*
......@@ -123,7 +124,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
$this->redirect($action, $controller, $extension, $arguments);
}
/**
* Send flash message and forward to given action
*
......@@ -141,7 +141,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
$this->forward($action, $controller, $extension, $arguments);
}
/**
* Clear cache of given pages
*
......@@ -156,7 +155,6 @@ abstract class AbstractController extends \TYPO3\CMS\Extbase\Mvc\Controller\Acti
}
}
/**
* Adds the base uri if not already in place.
*
......
......@@ -14,6 +14,9 @@ namespace T3o\TerFe2\Controller\Eid;
* 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
*/
......@@ -57,17 +60,27 @@ class ExtensionController
/**
* @return void
*/
protected function findAllWithRepositoryUrlAsPackageSource()
protected function findAllWithValidComposerName()
{
$extensions = $this->databaseConnection->exec_SELECTgetRows(
'*',
'tx_terfe2_domain_model_extension',
'hidden = 0 and deleted = 0 and repository_clone_url <> ""'
);
$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();
foreach ($extensions as $extension) {
while ($extension = $result->fetch()) {
$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;
* 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
......@@ -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
......@@ -242,6 +254,9 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController
* @param \T3o\TerFe2\Domain\Model\Extension $extension extension to update
* @param string $tag
* @param string $save
*
* @validate $extension \T3o\TerFe2\Validation\Validator\ExtensionArgumentValidator
*
* @return void
*/
public function updateAction(\T3o\TerFe2\Domain\Model\Extension $extension, $tag = '', $save = '')
......
......@@ -87,13 +87,6 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
*/
protected $repositoryUrl = '';
/**
* External repository clone url
*
* @var string
*/
protected $repositoryCloneUrl = '';
/**
* Link to an external manual
*
......@@ -115,9 +108,17 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
*/
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()
{
......@@ -435,26 +436,6 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
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
*
......@@ -596,4 +577,20 @@ class Extension extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
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
// composer support
$document->setField('supportsComposer_boolS', false);
$document->setField('composerName_stringS', '');
if ($extension->getLastVersion()->getComposerName()) {
if ($extension->getComposerName()) {
$document->setField('supportsComposer_boolS', true);
$document->setField('composerName_stringS', $extension->getLastVersion()->getComposerName());
$document->setField('composerName_stringS', $extension->getComposerName());
}
// does this extension supports different versions?
......
......@@ -324,6 +324,8 @@ class ImportExtensionsFromQueueTask extends Task
'composer_info' => $extData['composerinfo']
];
$this->ensureValidComposerNameInExtension($extUid, $extData['composerinfo']);
$tableName = 'tx_terfe2_domain_model_version';
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
......@@ -333,6 +335,37 @@ class ImportExtensionsFromQueueTask extends Task
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 $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(
'iconfile' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extRelPath('ter_fe2') . 'Resources/Public/Icons/extension.gif',
),
'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(
'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(
'1' => array('showitem' => ''),
......@@ -100,6 +100,15 @@ return array(
'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(
'exclude' => 1,
'label' => 'LLL:EXT:ter_fe2/Resources/Private/Language/locallang_db.xlf:tx_terfe2_domain_model_extension.last_upload',
......@@ -188,16 +197,6 @@ return array(
'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(
'exclude' => 1,
'label' => 'LLL:EXT:ter_fe2/Resources/Private/Language/locallang_db.xlf:tx_terfe2_domain_model_extension.external_manual',
......
......@@ -12,6 +12,15 @@
<trans-unit id="tx_terfe2_domain_model_extension.forge_link" xml:space="preserve">
<source>Link to issue tracker</source>
</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">
<source>Last update</source>
</trans-unit>
......@@ -42,12 +51,6 @@
<trans-unit id="tx_terfe2_domain_model_extension.repository_url" xml:space="preserve">
<source>Link to repository</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.repository_clone_url" xml:space="preserve">
<source>Publicly accessible clone url</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.repository_clone_url.description" xml:space="preserve">
<source>The publicly accessible clone url will be used to fetch your repository for http://composer.typo3.org from the given url instead of the uploaded TER codebase. Therefore double check that the url is available for everyone. Otherwise your extension might disappear from http://composer.typo3.org.</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.external_manual" xml:space="preserve">
<source>External manual</source>
</trans-unit>
......
......@@ -12,6 +12,9 @@
<trans-unit id="tx_terfe2_domain_model_extension.forge_link" xml:space="preserve">
<source>Link to issue tracker</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.composer_name" xml:space="preserve">
<source>Composer name of extension published on https://packagist.org</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.last_update" xml:space="preserve">
<source>Last update</source>
</trans-unit>
......@@ -42,9 +45,6 @@
<trans-unit id="tx_terfe2_domain_model_extension.repository_url" xml:space="preserve">
<source>Link to repository</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.repository_clone_url" xml:space="preserve">
<source>Publicly accessible clone url</source>
</trans-unit>
<trans-unit id="tx_terfe2_domain_model_extension.external_manual" xml:space="preserve">
<source>External manual</source>
</trans-unit>
......
......@@ -12,6 +12,25 @@
<f:render partial="FormErrors" />
<f:if condition="{extension.lastVersion.composerName}">
<div class="form-group row">
<label for="packagistConfirm" class="col-3 col-form-label">
<f:translate key="tx_terfe2_domain_model_extension.packagist_confirm" />
</label>
<div class="col-9">
<f:form.checkbox id="packagistConfirm" name="packagistConfirm" value="1" checked="{f:if(condition: '{extension.composerName}', then: '1', else: '0')}" />
&nbsp;<f:translate key="tx_terfe2_domain_model_extension.packagist_confirm_message" />
</div>
</div>
<div class="form-group row js-composer-name-input">
<label for="composerName" class="col-3 col-form-label">
<f:translate key="tx_terfe2_domain_model_extension.composer_name" />
</label>
<div class="col-9">
<f:form.textfield class="form-control" id="composerName" property="composerName" value="{extension.composerName}" additionalAttributes="{placeholder:'{extension.lastVersion.composerName}'}" />
</div>
</div>
</f:if>
<div class="form-group row">
<label for="forgeLink" class="col-3 col-form-label">
<f:translate key="tx_terfe2_domain_model_extension.forge_link" />
......@@ -20,7 +39,6 @@
<f:form.textfield class="form-control" id="forgeLink" property="forgeLink" additionalAttributes="{placeholder:'https://external.repository.org/your-project/issues'}" />
</div>
</div>
<div class="form-group row">
<label for="repositoryUrl" class="col-3 col-form-label">
<f:translate key="tx_terfe2_domain_model_extension.repository_url" />
......
......@@ -38,5 +38,13 @@ jQuery(document).ready(function ($) {
});
}
var $packagistField = $("input[name='tx_terfe2_pi1[packagistConfirm]']");
var $composerNameInput = $(".js-composer-name-input");
$composerNameInput.toggle($packagistField.is( ":checked" ));
$packagistField.click(function() {
$composerNameInput.toggle(this.checked);
});
});
......@@ -7,6 +7,7 @@ CREATE TABLE tx_terfe2_domain_model_extension (
ext_key tinytext,
forge_link tinytext,
composer_name varchar(60) DEFAULT '' NOT NULL,
last_upload int(11) unsigned DEFAULT '0',
last_maintained int(11) unsigned DEFAULT '0',
tags int(11) unsigned DEFAULT '0' NOT NULL,
......@@ -15,7 +16,6 @@ CREATE TABLE tx_terfe2_domain_model_extension (
frontend_user tinytext,
downloads int(11) unsigned DEFAULT '0' NOT NULL,
repository_url varchar(255) DEFAULT '' NOT NULL,
repository_clone_url varchar(255) DEFAULT '' NOT NULL,
external_manual varchar(255) DEFAULT '' NOT NULL,
paypal_url varchar(255) DEFAULT '' NOT NULL,
expire int(11) unsigned default '0' NOT NULL,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment