Commit 8f79744d authored by Benni Mack's avatar Benni Mack
Browse files

[!!!][TASK] Refactor generation of extensions.xml.gz

This change re-implements the generation of the XML file for TYPO3 installations
by moving the relevant code from EXT:ter to ter_fe2.

CombinedExtensionRepository -> does the collection of available extensions
ExtensionIndexService -> handles the XML generation and writing to files

This way, all functionality can be separated and exchanged if necessary.

A CLI command "ter:createExtensionIndexXml" is added which
also shows some more useful output, however the logic is still in
the "UpdateCurrentVersionListTask" class in EXT:ter.

The methods:
tx_ter_helper->requestUpdateOfExtensionIndexFile
tx_ter_helper->writeExtensionIndexfile

are removed, and thus, all of this logic is migrated to Doctrine DBAL.

Root composer.json is adapted so the necessary PHP extensions are available
in your IDE.
parent 22a78011
Pipeline #9048 passed with stages
in 6 minutes and 59 seconds
......@@ -27,6 +27,9 @@
],
"require": {
"php": ">=7.0",
"ext-pdo": "*",
"ext-dom": "*",
"ext-zlib": "*",
"t3o/ter-layout": "^0.1",
"t3o/ter-soap": "^2.0",
"t3o/ter-frontend": "^0.4",
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ecec99c9b9b50f55527928f2d30f488f",
"content-hash": "aaa1147d06f6fd0d5c691408e91ba9f3",
"packages": [
{
"name": "adoy/fastcgi-client",
......@@ -7125,7 +7125,10 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.0"
"php": ">=7.0",
"ext-pdo": "*",
"ext-dom": "*",
"ext-zlib": "*"
},
"platform-dev": [],
"platform-overrides": {
......
......@@ -14,6 +14,8 @@ namespace T3o\Ter\Task;
* The TYPO3 project - inspiring people to share!
*/
use T3o\TerFe2\Domain\Repository\CombinedExtensionRepository;
use T3o\TerFe2\Service\ExtensionIndexService;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -76,25 +78,17 @@ class UpdateCurrentVersionListTask extends \TYPO3\CMS\Extbase\Scheduler\Task
*/
protected function generateExtensionFilesForExtensionManager(): bool
{
// Check extension configuration
if (empty($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['ter'])) {
throw new \Exception('No extension configuration found in $TYPO3_CONF_VARS', 1303220916);
}
// Check extension repository path
$extensionConfig = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['ter'];
if (empty($extensionConfig['repositoryDir'])) {
throw new \Exception('No repository path found in extension configuration', 1303220917);
$indexCreator = GeneralUtility::makeInstance(ExtensionIndexService::class);
if ($indexCreator->isOldUpdateRequested()) {
return false;
}
$startTime = microtime();
$extensionDetails = GeneralUtility::makeInstance(CombinedExtensionRepository::class)->getExtensionDetailsWithVersionsAndDownloadNumbers();
$dom = $indexCreator->compileXmlStructure($extensionDetails, $startTime);
// Write new extensions xml file
$repositoryDir = rtrim($extensionConfig['repositoryDir'], '/') . '/';
$dummyObject = new \stdClass();
$dummyObject->repositoryDir = $repositoryDir;
/** @var \tx_ter_helper $terHelper */
$terHelper = GeneralUtility::makeInstance(\tx_ter_helper::class, $dummyObject);
$terHelper->writeExtensionIndexFile();
$indexCreator->writeXmlToFile($dom);
$indexCreator->writeSerializedExtensionInformationToFile($extensionDetails);
return true;
}
}
......@@ -191,7 +191,7 @@ class tx_ter_api
$this->uploadExtension_writeExtensionAndIconFile($extensionInfoData, $filesData);
$this->uploadExtension_writeExtensionInfoToDB($accountData, $extensionInfoData, $filesData);
$this->helperObj->requestUpdateOfExtensionIndexFile();
$this->requestUpdateOfExtensionIndexFile();
static::notifyExtensionVersionUpload($extensionInfoData);
......@@ -276,7 +276,7 @@ class tx_ter_api
// Upload...
$instance->uploadExtension_writeExtensionAndIconFile($extensionInfoData, $filesData);
$instance->uploadExtension_writeExtensionInfoToDB($accountData, $extensionInfoData, $filesData);
$instance->helperObj->requestUpdateOfExtensionIndexFile();
$instance->requestUpdateOfExtensionIndexFile();
static::notifyExtensionVersionUpload($extensionInfoData);
......@@ -323,7 +323,7 @@ class tx_ter_api
}
$this->deleteExtension_deleteFromDBAndRemoveFiles($extensionKey, $version);
$this->helperObj->requestUpdateOfExtensionIndexFile();
$this->requestUpdateOfExtensionIndexFile();
return [
'resultCode' => TX_TER_RESULT_EXTENSIONSUCCESSFULLYDELETED,
......@@ -570,7 +570,7 @@ class tx_ter_api
throw new \T3o\Ter\Exception\UnauthorizedException('Access denied.', TX_TER_ERROR_MODIFYEXTENSIONKEY_ACCESSDENIED);
}
$resultCode = $this->modifyExtensionKey_writeModifiedKeyRecordIntoDB($accountData, $modifyExtensionKeyData);
$this->helperObj->requestUpdateOfExtensionIndexFile();
$this->requestUpdateOfExtensionIndexFile();
} else {
$resultCode = TX_TER_ERROR_MODIFYEXTENSIONKEY_KEYDOESNOTEXIST;
}
......@@ -611,7 +611,7 @@ class tx_ter_api
$this->setReviewState_writeNewStateIntoDB($setReviewStateData);
//Regeneration of index file is currently deactived:
//$this->helperObj->requestUpdateOfExtensionIndexFile();
//$this->requestUpdateOfExtensionIndexFile();
return [
'resultCode' => TX_TER_RESULT_GENERAL_OK,
......@@ -693,7 +693,7 @@ class tx_ter_api
0
);
// Update extension index file
$this->helperObj->requestUpdateOfExtensionIndexFile();
$this->requestUpdateOfExtensionIndexFile();
// Return results including list of error messages if any
if (!empty($errorMessages)) {
......@@ -1662,4 +1662,9 @@ class tx_ter_api
}
}
}
protected function requestUpdateOfExtensionIndexFile(): void
{
GeneralUtility::makeInstance(\T3o\TerFe2\Service\ExtensionIndexService::class)->requestUpdate();
}
}
......@@ -324,233 +324,6 @@ class tx_ter_helper
return $latestVersion;
}
/**
* Sets a flag so the cron job knows that the extensions.xml.gz file has to be
* regenerated. Call this whenever data has changed which also exists in
* extensions.xml.gz
*
* Note: Depending on the cron job it might take a while until the index file really
* has been updated. See "cli/build-extension-index.php" for more information
*
* @access public
*/
public function requestUpdateOfExtensionIndexFile()
{
GeneralUtility::writeFile(
$this->pluginObj->repositoryDir . 'extensions.xml.gz.needsupdate',
'Dear cron-job. The extensions.xml.gz file needs to be regenerated, please do so as soon as you find the time for it.' . chr(
10
) . 'Thanks, your TER helper class'
);
}
/**
* Updates the "extensions.xml" file which contains an index of all uploaded
* extensions in the TER.
*
* @access public
* @throws \T3o\Ter\Exception\InternalServerErrorException
*/
public function writeExtensionIndexfile()
{
GeneralUtility::devLog('writing extension index!', 'tx_ter_helper', 0);
if (!@is_dir($this->pluginObj->repositoryDir)) {
throw new \T3o\Ter\Exception\InternalServerErrorException(
'Extension repository directory does not exist.',
TX_TER_ERROR_GENERAL_EXTREPDIRDOESNTEXIST
);
}
// Check if update of files requested
$updateRequestedFile = $this->pluginObj->repositoryDir . 'extensions.xml.gz.needsupdate';
if (file_exists($updateRequestedFile) && @filemtime($updateRequestedFile) <= @filemtime($this->pluginObj->repositoryDir . 'extensions.xml.gz')) {
return;
}
$trackTime = microtime();
$res = $this->getDatabaseConnection()->exec_SELECTquery(
'uid,tstamp,extensionkey,version,title,description,state,reviewstate,category,downloadcounter,t3xfilemd5',
'tx_ter_extensions',
'1'
);
// Read the extension records from the DB:
$extensionsAndVersionsArr = [];
$extensionsTotalDownloadsArr = [];
while ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) {
$res2 = $this->getDatabaseConnection()->exec_SELECTquery(
'ownerusername,downloadcounter',
'tx_ter_extensionkeys',
'extensionkey=' . $this->getDatabaseConnection()->fullQuoteStr($row['extensionkey'], 'tx_ter_extensionkeys')
);
$extensionKeyRow = $this->getDatabaseConnection()->sql_fetch_assoc($res2);
$row['ownerusername'] = $extensionKeyRow['ownerusername'];
$extensionsTotalDownloadsArr[$row['extensionkey']] = $extensionKeyRow['downloadcounter'];
$res2 = $this->getDatabaseConnection()->exec_SELECTquery(
'lastuploaddate,uploadcomment,dependencies,composerinfo,authorname,authoremail,authorcompany',
'tx_ter_extensiondetails',
'extensionuid=' . (int)$row['uid']
);
$detailsRow = $this->getDatabaseConnection()->sql_fetch_assoc($res2);
if (is_array($detailsRow)) {
$row = $row + $detailsRow;
}
$extensionsAndVersionsArr [$row['extensionkey']]['versions'][$row['version']] = $row;
}
// Prepare the DOM object:
$dom = new DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;
$extensionsObj = $dom->appendChild(new DOMElement('extensions'));
$documentationService = GeneralUtility::makeInstance(\T3o\TerFe2\Service\DocumentationService::class);
// Create the nested XML structure:
foreach ($extensionsAndVersionsArr as $extensionKey => $extensionVersionsArr) {
$extensionObj = $extensionsObj->appendChild(new DOMElement('extension'));
$extensionObj->appendChild(new DOMAttr('extensionkey', $extensionKey));
$extensionObj->appendChild(
new DOMElement(
'downloadcounter',
$this->xmlentities($extensionsTotalDownloadsArr[$extensionKey])
)
);
$extensionRecord = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getConnectionForTable('tx_terfe2_domain_model_extension')
->select(
['uid', 'external_manual'],
'tx_terfe2_domain_model_extension',
[
'ext_key' => $extensionKey
]
)
->fetch();
foreach ($extensionVersionsArr['versions'] as $versionNumber => $extensionVersionArr) {
$versionObj = $extensionObj->appendChild(new DOMElement('version'));
$versionObj->appendChild(new DOMAttr('version', $versionNumber));
$versionObj->appendChild(new DOMElement('title', $this->xmlentities($extensionVersionArr['title'])));
$versionObj->appendChild(new DOMElement('description', $this->xmlentities($extensionVersionArr['description'])));
$versionObj->appendChild(new DOMElement('state', $this->xmlentities($extensionVersionArr['state'])));
$versionObj->appendChild(new DOMElement('reviewstate', intval($extensionVersionArr['reviewstate'])));
$versionObj->appendChild(new DOMElement('category', $this->xmlentities($extensionVersionArr['category'])));
if ($extensionVersionArr['category'] === 'distribution') {
$prefixPathOnServer = PATH_site . 'fileadmin/ter/';
$prefixDistributionFilePath = $extensionKey[0] . '/' . $extensionKey[1] . '/' . $extensionKey . '_' . $versionNumber . '_';
$distributionImage = $prefixDistributionFilePath . 'Distribution.png';
$distributionWelcomeImage = $prefixDistributionFilePath . 'DistributionWelcome.png';
if (is_file($prefixPathOnServer . $distributionImage)) {
$versionObj->appendChild(new DOMElement('distributionImage', $this->xmlentities($distributionImage)));
}
if (is_file($prefixPathOnServer . $distributionWelcomeImage)) {
$versionObj->appendChild(new DOMElement('distributionImageWelcome', $this->xmlentities($distributionWelcomeImage)));
}
}
$versionObj->appendChild(
new DOMElement(
'downloadcounter',
$this->xmlentities($extensionVersionArr['downloadcounter'])
)
);
$versionObj->appendChild(new DOMElement('lastuploaddate', $extensionVersionArr['lastuploaddate']));
$versionObj->appendChild(
new DOMElement(
'uploadcomment',
$this->xmlentities($extensionVersionArr['uploadcomment'])
)
);
$versionObj->appendChild(new DOMElement('dependencies', $extensionVersionArr['dependencies']));
$versionObj->appendChild(new DOMElement('composerinfo', $extensionVersionArr['composerinfo']));
$versionObj->appendChild(new DOMElement('authorname', $this->xmlentities($extensionVersionArr['authorname'])));
$versionObj->appendChild(new DOMElement('authoremail', $this->xmlentities($extensionVersionArr['authoremail'])));
$versionObj->appendChild(
new DOMElement(
'authorcompany',
$this->xmlentities($extensionVersionArr['authorcompany'])
)
);
$versionObj->appendChild(
new DOMElement(
'ownerusername',
$this->xmlentities($extensionVersionArr['ownerusername'])
)
);
$versionObj->appendChild(new DOMElement('t3xfilemd5', $extensionVersionArr['t3xfilemd5']));
$documentationLink = '';
try {
$documentationLink = $documentationService->getDocumentationLink($extensionKey, $versionNumber, true);
} catch (Exception $e) {
}
$versionObj->appendChild(new DOMElement('documentation_link', $extensionRecord['external_manual'] ?: $documentationLink));
}
}
$extensionsObj->appendChild(new DOMComment('Index created at ' . date('D M j G:i:s T Y')));
$extensionsObj->appendChild(new DOMComment('Index created in ' . (microtime() - $trackTime) . ' ms'));
// Write XML data to disk:
$fh = fopen($this->pluginObj->repositoryDir . 'new-extensions.xml.gz', 'wb');
if (!$fh) {
throw new \T3o\Ter\Exception\InternalServerErrorException(
'Write error while writing extensions index file: ' . $this->pluginObj->repositoryDir . 'extensions.xml',
TX_TER_ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGEXTENSIONSINDEX
);
}
fwrite($fh, gzencode($dom->saveXML(), 9));
fclose($fh);
if (!@filesize($this->pluginObj->repositoryDir . 'new-extensions.xml.gz') > 0) {
GeneralUtility::devLog('Newly created extension index is zero bytes!', 'tx_ter_helper', 0);
throw new \T3o\Ter\Exception\InternalServerErrorException(
'Write error while writing extensions index file (zero bytes): ' . $this->pluginObj->repositoryDir . 'extensions.xml',
TX_TER_ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGEXTENSIONSINDEX
);
}
@unlink($this->pluginObj->repositoryDir . 'extensions.xml.gz');
rename($this->pluginObj->repositoryDir . 'new-extensions.xml.gz', $this->pluginObj->repositoryDir . 'extensions.xml.gz');
GeneralUtility::writeFile(
$this->pluginObj->repositoryDir . 'extensions.md5',
md5_file($this->pluginObj->repositoryDir . 'extensions.xml.gz')
);
// Write serialized array file to disk:
$fh = fopen($this->pluginObj->repositoryDir . 'new-extensions.bin', 'wb');
if (!$fh) {
throw new \T3o\Ter\Exception\InternalServerErrorException(
'Write error while writing extensions index file: ' . $this->pluginObj->repositoryDir . 'extensions.bin',
TX_TER_ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGEXTENSIONSINDEX
);
}
fwrite($fh, serialize($extensionsAndVersionsArr));
fclose($fh);
if (!@filesize($this->pluginObj->repositoryDir . 'new-extensions.bin') > 0) {
GeneralUtility::devLog('Newly created extension index is zero bytes!', 'tx_ter_helper', 0);
throw new \T3o\Ter\Exception\InternalServerErrorException(
'Write error while writing extensions index file (zero bytes): ' . $this->pluginObj->repositoryDir . 'extensions.bin',
TX_TER_ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGEXTENSIONSINDEX
);
}
@unlink($this->pluginObj->repositoryDir . 'extensions.bin');
rename($this->pluginObj->repositoryDir . 'new-extensions.bin', $this->pluginObj->repositoryDir . 'extensions.bin');
}
/**
* Equivalent to htmlentities but for XML content
*
* @param string $string : String to encode
* @return string &,",',< and > replaced by entities
* @access public
*/
public function xmlentities($string)
{
// Until I have found a better solution for guaranteeing valid characters, I use this regex:
$string = (preg_replace('/[^\w\s"%&\[\]\(\)\.\,\;\:\/\?\{\}!\$\-\/\@]/u', '', $string));
return str_replace(['&', '"', "'", '<', '>'], ['&amp;', '&quot;', '&apos;', '&lt;', '&gt;'], $string);
}
/***
* Load an instance of the BE_USER to use with TCEFORM
*
......
<?php
declare(strict_types = 1);
/*
* This file is part of TYPO3 CMS-extension "ter_fe2", created by Benni Mack.
*
* 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\TerFe2\Command;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use T3o\Ter\Exception\InternalServerErrorException;
use T3o\TerFe2\Domain\Repository\CombinedExtensionRepository;
use T3o\TerFe2\Service\ExtensionIndexService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Creates the extensions.xml.gz file from all versions of all released extensions, with a lot of metadata,
* and stores it (safely) into fileadmin/ter/.
*/
class CreateExtensionIndexCommand extends Command implements LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* @var CombinedExtensionRepository
*/
protected $combinedExtensionRepository;
/**
* @var ExtensionIndexService
*/
protected $extensionIndexService;
public function __construct(string $name = null, CombinedExtensionRepository $combinedExtensionRepository = null, ExtensionIndexService $extensionIndexService = null)
{
$this->combinedExtensionRepository = $combinedExtensionRepository ?? GeneralUtility::makeInstance(CombinedExtensionRepository::class);
$this->extensionIndexService = $extensionIndexService ?? GeneralUtility::makeInstance(ExtensionIndexService::class);
parent::__construct($name);
}
protected function configure()
{
$this->setDescription('Creates extensions.xml.gz by finding all published extensions (and their versions)');
}
/**
* Updates the "extensions.xml" file which contains an index of all uploaded
* extensions in the TER.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$io->section('Writing extension index');
// Check if update of files requested
if ($this->extensionIndexService->isOldUpdateRequested()) {
$io->error('There was a .needsupdate file, but an already generated file is there. Aborting.');
return 1;
}
$processStarted = microtime();
// Read the extension records from the DB:
$io->writeln('Fetching all published extensions and their versions from the database. Starting at ' . date('d-m-Y H:i:s'));
$extensionsAndVersionsArr = $this->combinedExtensionRepository->getExtensionDetailsWithVersionsAndDownloadNumbers();
$io->writeln('Building a XML node structure out of the entries Starting at ' . date('d-m-Y H:i:s'));
$dom = $this->extensionIndexService->compileXmlStructure($extensionsAndVersionsArr, $processStarted);
try {
$io->writeln('Saving the XML to a file. Starting at ' . date('d-m-Y H:i:s'));
$fileName = $this->extensionIndexService->writeXmlToFile($dom);
$io->success('Stored in ' . $fileName);
$io->writeln('Saving the serialized extension list to a file. Starting at ' . date('d-m-Y H:i:s'));
$serializedDataFileName = $this->extensionIndexService->writeSerializedExtensionInformationToFile($extensionsAndVersionsArr);
$io->success('Stored in ' . $serializedDataFileName);
} catch (InternalServerErrorException $e) {
$io->error($e->getMessage());
return 1;
}
$io->success('New extensions index file created. Finished on ' . date('d-m-Y H:i:s'));
return 0;
}
}
<?php
declare(strict_types = 1);
namespace T3o\TerFe2\Domain\Repository;
/*
* This file is part of TYPO3 CMS-extension "ter_fe2", created by Benni Mack.
*
* 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 T3o\TerFe2\Service\DocumentationService;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This class combines all data of an extension and all versions from various database tables
*
* tx_ter_extensions -> contains all versions of all extensions
* tx_ter_extensionkeys -> contains all registered extension keys (even if there was no version uploaded)
* tx_ter_extensiondetails -> contains more information of an extension version
* tx_terfe2_domain_model_extension -> contains information of an external manual
*
* Currently this class is used as a wrapper, but should vanish once we migrated the database structures properly
*/
class CombinedExtensionRepository
{
/**
* @var DocumentationService
*/
protected $documentationService;
public function __construct(DocumentationService $documentationService = null)
{
$this->documentationService = $documentationService ?? GeneralUtility::makeInstance(DocumentationService::class);
}
/**
* Fetches all information about a given list of extension keys that is not version-specific.
*
* @param array $extensionKeys
* @return array
*/
protected function getBasicExtensionInformation(array $extensionKeys): array
{
$basicExtensionInformation = [];
foreach ($extensionKeys as $extensionKey) {
$keysQueryBuilder = $this->getQueryBuilder('tx_ter_extensionkeys');
$extensionKeyRow = $keysQueryBuilder
->select('ownerusername', 'downloadcounter')
->from('tx_ter_extensionkeys')
->where(
$keysQueryBuilder->expr()->eq('extensionkey', $keysQueryBuilder->createNamedParameter($extensionKey))
)
->execute()
->fetch();
$manualQueryBuilder = $this->getQueryBuilder('tx_terfe2_domain_model_extension');
$manualRecord = $manualQueryBuilder
->select('uid', 'external_manual')
->from('tx_terfe2_domain_model_extension')
->where(
$manualQueryBuilder->expr()->eq('ext_key', $manualQueryBuilder->createNamedParameter($extensionKey))
)
->execute()
->fetch();
$basicExtensionInformation[$extensionKey] = [
'ownerusername' => $extensionKeyRow['ownerusername'],
'downloads' => $extensionKeyRow['downloadcounter'],
'external_manual' => $manualRecord['external_manual'] ?: ''
];
}
return $basicExtensionInformation;
}
public function getExtensionDetailsWithVersionsAndDownloadNumbers(): array
{
$groupedVersionsByExtension = [];
$queryBuilder = $this->getQueryBuilder('tx_ter_extensions');
$versions = $queryBuilder
->select('uid', 'tstamp', 'extensionkey', 'version', 'title', 'description', 'state', 'reviewstate', 'category', 'downloadcounter', 't3xfilemd5')
->from('tx_ter_extensions')
->execute()
->fetchAll();
$usedExtensionKeys = array_column($versions, 'extensionkey');
$usedExtensionKeys = array_unique($usedExtensionKeys);
$basicExtensionInformation = $this->getBasicExtensionInformation($usedExtensionKeys);
foreach ($versions as $row) {
$extensionKey = $row['extensionkey'];
$versionNumber = $row['version'];
$genericExtensionInformation = $basicExtensionInformation[$extensionKey];
$groupedVersionsByExtension[$extensionKey]['ownerusername'] = $genericExtensionInformation['ownerusername'];
$groupedVersionsByExtension[$extensionKey]['downloads'] = $genericExtensionInformation['downloads'];
$detailsQueryBuilder = $this->getQueryBuilder('tx_ter_extensiondetails');
$detailsRow = $detailsQueryBuilder
->select('lastuploaddate', 'uploadcomment', 'dependencies', 'composerinfo', 'authorname', 'authoremail', 'authorcompany')
->from('tx_ter_extensiondetails')
->where(
$detailsQueryBuilder->expr()->eq('extensionuid', $detailsQueryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT))
)
->execute()
->fetch();
if (is_array($detailsRow)) {
$row = $row + $detailsRow;
}